name: Publish Signage Release # Builds, proves, and publishes one Signage release. This is deliberately separate from CI: only an # operator's workflow_dispatch on a selected branch can start it. The normal release ref is main. # Every local gate, including the full external-package integration and ADR 0055 redistribution # verification, finishes before the release/tag is created or an existing release is touched. on: workflow_dispatch: inputs: version: description: The Signage version to publish, equal to package and module metadata. required: true type: string permissions: contents: read # Never cancel a publication part way, and never run two at once. A rerun safely resumes an # identical partial upload, but refuses a tag, release, or asset that differs from this run. concurrency: group: publish-signage-release cancel-in-progress: false jobs: release-gate: name: release gate runs-on: ubuntu-latest timeout-minutes: 75 outputs: sha256: ${{ steps.canonical.outputs.sha256 }} env: DOCKER_BUILDKIT: "1" EKKLESIA_CI_JOB_KEY: signage-publish-release RELEASE_VERSION: ${{ inputs.version }} RELEASE_TAG: v${{ inputs.version }} RELEASE_REVISION: ${{ github.sha }} steps: - name: Check out the operator-selected revision uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Node.js at the repository's version uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version-file: .node-version - name: Install the reviewed build and release tooling run: | corepack enable corepack install pnpm install --frozen-lockfile # Match Core's manual publisher: releases come only from reviewed main. The source version is # checked before the expensive gate, and the selected commit remains the immutable tag target. - name: Validate requested version and selected revision run: | [[ "$GITHUB_REF" == refs/heads/main ]] || { echo "publish: select main; got $GITHUB_REF" >&2 exit 1 } [[ "$RELEASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "publish: version must be X.Y.Z; got $RELEASE_VERSION" >&2 exit 1 } [[ "$RELEASE_REVISION" =~ ^[0-9a-f]{40}$ ]] [[ "$(git rev-parse HEAD)" == "$RELEASE_REVISION" ]] [[ -z "$(git status --porcelain)" ]] node --input-type=module -e ' import { readFileSync } from "node:fs"; const requested = process.env.RELEASE_VERSION; const packageVersion = JSON.parse(readFileSync("package.json", "utf8")).version; const moduleVersion = JSON.parse(readFileSync("ekklesia.module.json", "utf8")).version; if (packageVersion !== requested || moduleVersion !== requested) { console.error(`publish: requested ${requested}, package ${packageVersion}, module ${moduleVersion}`); process.exit(1); } console.log(`publish: source metadata agrees on ${requested}`); ' # This is the only package build in the workflow. Every later step consumes this directory. - name: Build the canonical Module Package pair once id: canonical run: bash ci/package.sh build/release-assets - name: Verify the canonical Module Package env: EXPECTED_PACKAGE_SHA256: ${{ steps.canonical.outputs.sha256 }} run: | pnpm validate python3 ci/release-assets.py verify-package \ build/release-assets "$RELEASE_TAG" "$RELEASE_REVISION" - name: Fetch the pinned Ekklesia integration harness run: bash ci/fetch-platform.sh - name: Sweep stale CI stacks from the runner run: bash build/platform/scripts/ci-sweep.sh --stale - name: Run full integration with the exact release package env: EXPECTED_PACKAGE_SHA256: ${{ steps.canonical.outputs.sha256 }} run: bash ci/integration.sh "$GITHUB_WORKSPACE/build/release-assets" - name: Remove this publication's CI stacks if: always() run: | if [ -f build/platform/scripts/ci-sweep.sh ]; then bash build/platform/scripts/ci-sweep.sh --job fi - name: Build and verify required redistribution assets env: EXPECTED_PACKAGE_SHA256: ${{ steps.canonical.outputs.sha256 }} run: | package="$(find build/release-assets -maxdepth 1 -type f -name 'signage-*-linux-x64-glibc.tar.gz')" pnpm exec ekklesia-corresponding-source acquire \ --package "$package" --cache "$RUNNER_TEMP/signage-corresponding-sources" pnpm exec ekklesia-corresponding-source build \ --package "$package" --cache "$RUNNER_TEMP/signage-corresponding-sources" \ --out build/release-assets source="${package%.tar.gz}-corresponding-source.tar.gz" metadata="${package%.tar.gz}.redistribution.json" pnpm exec ekklesia-corresponding-source verify \ --package "$package" --source "$source" --metadata "$metadata" python3 ci/release-assets.py verify \ build/release-assets "$RELEASE_TAG" "$RELEASE_REVISION" # The write-authorized job receives only the completely proven bundle. Artifact actions v3 # are required by Gitea; v4 refuses servers other than github.com. - name: Hand the proven release bundle to the publisher uses: actions/upload-artifact@c6a3b2bd78b3985e4b2f15397fec357f0fd808de # v3.2.2-node20 with: name: signage-release-assets path: build/release-assets/ if-no-files-found: error retention-days: 90 publish: name: publish needs: release-gate if: github.ref == 'refs/heads/main' permissions: contents: write runs-on: ubuntu-latest timeout-minutes: 15 env: EXPECTED_PACKAGE_SHA256: ${{ needs.release-gate.outputs.sha256 }} RELEASE_VERSION: ${{ inputs.version }} RELEASE_TAG: v${{ inputs.version }} RELEASE_REVISION: ${{ github.sha }} steps: # The checked-out helper is the same reviewed revision the gate proved. Credentials are not # persisted; the automatic token is exposed only to the final API call below. - name: Check out the proven revision uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Receive the proven release bundle uses: actions/download-artifact@ad191675b41f6a5b46da9a048cb6893812da158b # v3.1.0-node20 with: name: signage-release-assets path: build/release-assets/ - name: Recheck the received release bundle run: | python3 ci/release-assets.py verify \ build/release-assets "$RELEASE_TAG" "$RELEASE_REVISION" # Only this final step can mutate Gitea. It creates v at the selected commit and a # prerelease if absent, then uploads the already-tested bytes. A rerun resumes only identical # state. Every asset is read back without Authorization and checked byte for byte. - name: Create the release and publish the exact tested assets env: GITHUB_TOKEN: ${{ github.token }} run: | python3 ci/release-assets.py publish \ build/release-assets "$RELEASE_TAG" "$RELEASE_REVISION"