name: Publish tagged container on: push: tags: ["v*"] permissions: contents: read checks: read actions: read packages: write id-token: write jobs: validate-tag: runs-on: ubuntu-latest outputs: version: ${{ steps.tag.outputs.version }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - id: tag run: python scripts/release_version.py "$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT" - name: Require tag version to match project metadata run: | project_version=$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml","rb"))["project"]["version"])') expected_version="${{ steps.tag.outputs.project_version }}" test "$project_version" = "$expected_version" - name: Require the tagged commit to be on main run: git merge-base --is-ancestor "$GITHUB_SHA" origin/main verify-required-checks: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Require successful quality and heavy validation for this commit uses: actions/github-script@v7 with: script: | const required = [ "python", "benchmark-helpers", "frontend (console)", "frontend (tui)", "rust", "image-light-smoke", "heavy-smoke-and-restore", ]; const runs = await github.paginate(github.rest.checks.listForRef, { owner: context.repo.owner, repo: context.repo.repo, ref: context.sha, per_page: 100, }); const failed = required.filter((name) => !runs.some((run) => run.name === name && run.conclusion === "success") ); if (failed.length) { core.setFailed(`Missing successful checks for ${context.sha}: ${failed.join(", ")}`); } - name: Download exact-commit validation artifacts uses: actions/github-script@v7 with: script: | const fs = require("fs"); const required = [ ["quality.yaml", "light-production-validation", "light.zip"], ["heavy-validation.yaml", "heavy-production-validation", "heavy.zip"], ]; for (const [workflow, artifactName, output] of required) { const response = await github.rest.actions.listWorkflowRuns({ owner: context.repo.owner, repo: context.repo.repo, workflow_id: workflow, head_sha: context.sha, status: "completed", per_page: 100, }); const run = response.data.workflow_runs.find( (item) => item.conclusion === "success" ); if (!run) { core.setFailed(`No successful ${workflow} run found for ${context.sha}`); return; } const artifacts = await github.paginate( github.rest.actions.listWorkflowRunArtifacts, { owner: context.repo.owner, repo: context.repo.repo, run_id: run.id, per_page: 100, } ); const artifact = artifacts.find( (item) => item.name === artifactName && !item.expired ); if (!artifact) { core.setFailed(`Missing ${artifactName} on workflow run ${run.id}`); return; } const archive = await github.rest.actions.downloadArtifact({ owner: context.repo.owner, repo: context.repo.repo, artifact_id: artifact.id, archive_format: "zip", }); fs.writeFileSync(output, Buffer.from(archive.data)); } - name: Enforce recorded product, restore, security, and benchmark gates run: | mkdir -p gate-input/light gate-input/heavy gate-artifacts unzip -q light.zip -d gate-input/light unzip -q heavy.zip -d gate-input/heavy python scripts/assemble_release_artifacts.py --light gate-input/light --heavy gate-input/heavy --output gate-artifacts python scripts/check_release_readiness.py gate-artifacts - uses: actions/upload-artifact@v4 with: name: release-evidence-${{ github.ref_name }} path: gate-artifacts/ publish: runs-on: ubuntu-latest needs: [validate-tag, verify-required-checks] steps: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 with: name: release-evidence-${{ github.ref_name }} path: gate-artifacts - name: Load and verify the gated image run: | gzip -dc gate-artifacts/brainapi-image.tar.gz | docker load expected=$(python -c 'import json; print(json.load(open("gate-artifacts/security.json"))["image_digest"])') actual=$(docker image inspect brainapi:ci --format '{{.Id}}') test "$actual" = "$expected" - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - id: publish name: Publish the unchanged validated image run: | target="ghcr.io/lumen-labs/brainapi:$GITHUB_REF_NAME" docker tag brainapi:ci "$target" docker push "$target" digest=$(docker image inspect "$target" --format '{{index .RepoDigests 0}}' | cut -d@ -f2) test -n "$digest" echo "$digest" > image-digest.txt - uses: actions/upload-artifact@v4 with: name: release-image-digest-${{ needs.validate-tag.outputs.version }} path: image-digest.txt