PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m11s
PR Checks / Backend Smoke (pull_request) Successful in 9s
PR Checks / Build Backend (no push) (pull_request) Successful in 18s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m19s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 36s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 6m3s
Staging health polling asked only whether something answered HTTP 200 at the base URL. It could not tell the new deployment from the old one, so journeys could pass against the previous release, and concurrent merges could move the staging tags underneath a run in flight. Each staging run now mints a build ID and stamps all three images with the commit and that ID, as labels and — for frontend and backend — as a build-time JSON file that environment overrides cannot rewrite. /release.json reports both identities uncached, and scripts/ci/release.py polls for the expected pair before and after the journeys. Only then are the captured build digests tagged verified-<sha>. Promotion resolves those verified tags to immutable digests, revalidates their labels, and refuses a mixed or incomplete set before any :prod tag moves. The whole staging workflow shares one concurrency group with cancellation disabled, so releases serialise. The scripts are stdlib-only and unit-tested against mocked registry and HTTP behaviour; PR checks now run the pipeline and CI suites too. The runbook records what this cannot prove locally, and that the first rollout needs a commit built by this workflow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
254 lines
8.2 KiB
YAML
254 lines
8.2 KiB
YAML
name: Stage (build -> staging -> E2E gate)
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- main
|
|
|
|
# Serialise the entire build/deploy/test cycle: no other run can move staging tags.
|
|
concurrency:
|
|
group: staging-release
|
|
cancel-in-progress: false
|
|
|
|
env:
|
|
REGISTRY: privaterepo.sitaru.org
|
|
BACKEND_IMAGE_NAME: ${{ gitea.repository }}-backend
|
|
FRONTEND_IMAGE_NAME: ${{ gitea.repository }}-frontend
|
|
PIPELINE_IMAGE_NAME: ${{ gitea.repository }}-pipeline
|
|
|
|
jobs:
|
|
prepare:
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
build_id: ${{ steps.identity.outputs.build_id }}
|
|
steps:
|
|
- id: identity
|
|
run: python3 -c 'import uuid; print("build_id=" + uuid.uuid4().hex)' >> "$GITHUB_OUTPUT"
|
|
|
|
build-backend:
|
|
needs: [prepare]
|
|
outputs:
|
|
digest: ${{ steps.build.outputs.digest }}
|
|
name: Build Backend (FastAPI)
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@v3
|
|
with:
|
|
buildkitd-config-inline: |
|
|
[registry."docker.io"]
|
|
mirrors = ["10.0.1.224:6000"]
|
|
[registry."10.0.1.224:6000"]
|
|
http = true
|
|
insecure = true
|
|
|
|
- name: Log in to Gitea Container Registry
|
|
uses: docker/login-action@v3
|
|
with:
|
|
registry: ${{ env.REGISTRY }}
|
|
username: ${{ gitea.actor }}
|
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
|
|
|
- name: Extract metadata for Backend Docker image
|
|
id: meta-backend
|
|
uses: docker/metadata-action@v5
|
|
with:
|
|
images: ${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE_NAME }}
|
|
tags: |
|
|
type=sha
|
|
type=raw,value=staging
|
|
|
|
- name: Build and push Backend Docker image
|
|
id: build
|
|
uses: docker/build-push-action@v5
|
|
with:
|
|
context: .
|
|
file: ./Dockerfile
|
|
push: true
|
|
build-args: |
|
|
BUILD_SHA=${{ gitea.sha }}
|
|
BUILD_ID=${{ needs.prepare.outputs.build_id }}
|
|
tags: ${{ steps.meta-backend.outputs.tags }}
|
|
labels: ${{ steps.meta-backend.outputs.labels }}
|
|
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE_NAME }}:buildcache
|
|
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE_NAME }}:buildcache,mode=max
|
|
|
|
build-frontend:
|
|
needs: [prepare]
|
|
outputs:
|
|
digest: ${{ steps.build.outputs.digest }}
|
|
name: Build Frontend (Next.js)
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@v3
|
|
with:
|
|
buildkitd-config-inline: |
|
|
[registry."docker.io"]
|
|
mirrors = ["10.0.1.224:6000"]
|
|
[registry."10.0.1.224:6000"]
|
|
http = true
|
|
insecure = true
|
|
|
|
- name: Log in to Gitea Container Registry
|
|
uses: docker/login-action@v3
|
|
with:
|
|
registry: ${{ env.REGISTRY }}
|
|
username: ${{ gitea.actor }}
|
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
|
|
|
- name: Extract metadata for Frontend Docker image
|
|
id: meta-frontend
|
|
uses: docker/metadata-action@v5
|
|
with:
|
|
images: ${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE_NAME }}
|
|
tags: |
|
|
type=sha
|
|
type=raw,value=staging
|
|
|
|
- name: Build and push Frontend Docker image
|
|
id: build
|
|
uses: docker/build-push-action@v5
|
|
with:
|
|
context: ./nextjs-app
|
|
file: ./nextjs-app/Dockerfile
|
|
push: true
|
|
build-args: |
|
|
BUILD_SHA=${{ gitea.sha }}
|
|
BUILD_ID=${{ needs.prepare.outputs.build_id }}
|
|
tags: ${{ steps.meta-frontend.outputs.tags }}
|
|
labels: ${{ steps.meta-frontend.outputs.labels }}
|
|
# Cache disabled due to registry size limits
|
|
|
|
build-pipeline:
|
|
needs: [prepare]
|
|
outputs:
|
|
digest: ${{ steps.build.outputs.digest }}
|
|
name: Build Pipeline (Meltano + dbt + Airflow)
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@v3
|
|
with:
|
|
buildkitd-config-inline: |
|
|
[registry."docker.io"]
|
|
mirrors = ["10.0.1.224:6000"]
|
|
[registry."10.0.1.224:6000"]
|
|
http = true
|
|
insecure = true
|
|
|
|
- name: Log in to Gitea Container Registry
|
|
uses: docker/login-action@v3
|
|
with:
|
|
registry: ${{ env.REGISTRY }}
|
|
username: ${{ gitea.actor }}
|
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
|
|
|
- name: Extract metadata for Pipeline Docker image
|
|
id: meta-pipeline
|
|
uses: docker/metadata-action@v5
|
|
with:
|
|
images: ${{ env.REGISTRY }}/${{ env.PIPELINE_IMAGE_NAME }}
|
|
tags: |
|
|
type=sha
|
|
type=raw,value=staging
|
|
|
|
- name: Build and push Pipeline Docker image
|
|
id: build
|
|
uses: docker/build-push-action@v5
|
|
with:
|
|
context: ./pipeline
|
|
file: ./pipeline/Dockerfile
|
|
push: true
|
|
build-args: |
|
|
BUILD_SHA=${{ gitea.sha }}
|
|
BUILD_ID=${{ needs.prepare.outputs.build_id }}
|
|
tags: ${{ steps.meta-pipeline.outputs.tags }}
|
|
labels: ${{ steps.meta-pipeline.outputs.labels }}
|
|
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.PIPELINE_IMAGE_NAME }}:buildcache
|
|
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.PIPELINE_IMAGE_NAME }}:buildcache,mode=max
|
|
|
|
deploy-staging:
|
|
name: Deploy to Staging
|
|
runs-on: ubuntu-latest
|
|
needs: [prepare, build-backend, build-frontend, build-pipeline]
|
|
steps:
|
|
- name: Trigger staging stack update
|
|
run: curl -fsSk -X POST "${{ secrets.PORTAINER_STAGING_WEBHOOK }}"
|
|
|
|
- uses: actions/checkout@v4
|
|
- name: Verify deployed release identity
|
|
run: python3 scripts/ci/release.py wait
|
|
env:
|
|
BASE_URL: ${{ secrets.STAGING_BASE_URL }}
|
|
EXPECTED_SHA: ${{ gitea.sha }}
|
|
EXPECTED_BUILD_ID: ${{ needs.prepare.outputs.build_id }}
|
|
|
|
e2e-staging:
|
|
name: E2E Journeys against Staging
|
|
runs-on: ubuntu-latest
|
|
needs: [prepare, deploy-staging, build-backend, build-frontend, build-pipeline]
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Set up Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 22
|
|
|
|
- name: Install Playwright
|
|
working-directory: e2e
|
|
run: |
|
|
npm ci
|
|
npx playwright install --with-deps chromium
|
|
|
|
- name: Verify release before journeys
|
|
run: python3 scripts/ci/release.py wait --timeout 10
|
|
env:
|
|
BASE_URL: ${{ secrets.STAGING_BASE_URL }}
|
|
EXPECTED_SHA: ${{ gitea.sha }}
|
|
EXPECTED_BUILD_ID: ${{ needs.prepare.outputs.build_id }}
|
|
|
|
- name: Run E2E journeys
|
|
working-directory: e2e
|
|
run: npx playwright test
|
|
env:
|
|
BASE_URL: ${{ secrets.STAGING_BASE_URL }}
|
|
EXPECTED_SHA: ${{ gitea.sha }}
|
|
EXPECTED_BUILD_ID: ${{ needs.prepare.outputs.build_id }}
|
|
|
|
- name: Verify release after journeys
|
|
run: python3 scripts/ci/release.py wait --timeout 10
|
|
env:
|
|
BASE_URL: ${{ secrets.STAGING_BASE_URL }}
|
|
EXPECTED_SHA: ${{ gitea.sha }}
|
|
EXPECTED_BUILD_ID: ${{ needs.prepare.outputs.build_id }}
|
|
|
|
- uses: docker/setup-buildx-action@v3
|
|
- uses: docker/login-action@v3
|
|
with:
|
|
registry: ${{ env.REGISTRY }}
|
|
username: ${{ gitea.actor }}
|
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
|
- name: Mark tested image digests as verified
|
|
run: python3 scripts/ci/release.py verify
|
|
env:
|
|
EXPECTED_SHA: ${{ gitea.sha }}
|
|
EXPECTED_BUILD_ID: ${{ needs.prepare.outputs.build_id }}
|
|
BACKEND_DIGEST: ${{ needs.build-backend.outputs.digest }}
|
|
FRONTEND_DIGEST: ${{ needs.build-frontend.outputs.digest }}
|
|
PIPELINE_DIGEST: ${{ needs.build-pipeline.outputs.digest }}
|
|
|
|
# Production deployment is a second, manual approval: see promote.yml
|
|
# ("Promote to Production (manual)") and docs/DEPLOY.md.
|