name: CI # The server runs Gitea 1.27.2 (docs/development/security.md, "What this server honours"): # # * `concurrency:` is honoured: a newer push to a pull request cancels the run it supersedes, so a # cancelled run's own cleanup is never guaranteed (see the CI stack sweep in each stack job). # * `permissions:` is supported by 1.27 with Gitea's own scopes, but what the job token can do on # this server is not yet proven, so nothing here relies on it being read-only. # * `timeout-minutes:` is no longer listed as unsupported, but is not verified here either; # teardown relies on the stack sweep and the runner's own reaper. # # `persist-credentials: false` below is unaffected by any of this and is enforced by the action # itself. See docs/development/security.md. on: pull_request: push: branches: - main permissions: contents: read concurrency: group: ci-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: verify: runs-on: ubuntu-latest timeout-minutes: 15 steps: - name: Check out repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # `actions/checkout` defaults to leaving the job token in `.git/config`, where any later # step — including one a pull request added to this file — could read or reuse it. Nothing # in this workflow pushes, so the credential is never needed after checkout. persist-credentials: false - name: Set up Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version-file: .node-version - name: Activate pinned pnpm run: | corepack enable corepack prepare pnpm@11.12.0 --activate - name: Install dependencies run: pnpm install --frozen-lockfile - name: Verify repository run: pnpm ci:verify infrastructure-smoke: needs: verify runs-on: ubuntu-latest timeout-minutes: 10 env: # Names this job's stacks, so its final step removes exactly these (scripts/lib/ci-project.sh). EKKLESIA_CI_JOB_KEY: infrastructure-smoke steps: - name: Check out repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # `actions/checkout` defaults to leaving the job token in `.git/config`, where any later # step — including one a pull request added to this file — could read or reuse it. Nothing # in this workflow pushes, so the credential is never needed after checkout. persist-credentials: false # A job the runner killed (cancelled, or past the runner's own time limit) never ran its # cleanup. Anything a CI job left on this shared daemon more than three hours ago goes first. - name: Sweep stale CI stacks from the runner run: bash scripts/ci-sweep.sh --stale - name: Prove the sweep removes exactly what it should, against real Docker resources run: bash scripts/test-ci-sweep.sh - name: Exercise PostgreSQL and S3 infrastructure run: bash scripts/smoke-infrastructure.sh # Whatever happened above, this job's own stacks do not outlive it. - name: Remove this job's CI stacks if: always() run: bash scripts/ci-sweep.sh --job migration-smoke: needs: verify runs-on: ubuntu-latest timeout-minutes: 15 env: # Names this job's stacks, so its final step removes exactly these (scripts/lib/ci-project.sh). EKKLESIA_CI_JOB_KEY: migration-smoke steps: - name: Check out repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # `actions/checkout` defaults to leaving the job token in `.git/config`, where any later # step — including one a pull request added to this file — could read or reuse it. Nothing # in this workflow pushes, so the credential is never needed after checkout. persist-credentials: false # A job the runner killed (cancelled, or past the runner's own time limit) never ran its # cleanup. Anything a CI job left on this shared daemon more than three hours ago goes first. - name: Sweep stale CI stacks from the runner run: bash scripts/ci-sweep.sh --stale - name: Set up Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version-file: .node-version - name: Activate pinned pnpm run: | corepack enable corepack prepare pnpm@11.12.0 --activate - name: Install dependencies run: pnpm install --frozen-lockfile - name: Exercise transactional migrations against PostgreSQL run: pnpm test:migrations:integration # Whatever happened above, this job's own stacks do not outlive it. - name: Remove this job's CI stacks if: always() run: bash scripts/ci-sweep.sh --job package-release: needs: verify runs-on: ubuntu-latest timeout-minutes: 15 # What the gate proved, for the job that consumes its artifact to check it received exactly that. outputs: version: ${{ steps.release.outputs.version }} revision: ${{ steps.release.outputs.revision }} manifest_sha256: ${{ steps.release.outputs.manifest_sha256 }} steps: - name: Check out repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # `actions/checkout` defaults to leaving the job token in `.git/config`, where any later # step — including one a pull request added to this file — could read or reuse it. Nothing # in this workflow pushes, so the credential is never needed after checkout. persist-credentials: false - name: Set up Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version-file: .node-version - name: Activate pinned pnpm run: | corepack enable corepack prepare pnpm@11.12.0 --activate - name: Install dependencies run: pnpm install --frozen-lockfile # ADR 0053's package release gate: build the public package train from this commit, build it # again from a second checkout and require byte-identical artifacts, then install exactly those # tarballs (offline, no registry) into a module outside this repository and prove it builds, # packages, and verifies. Nothing is published and no credential exists here. A future publish # job must depend on this job and publish only the artifacts it proved. - name: Build, reproduce, and consume the public package train run: pnpm test:package-release --keep .package-release - name: Describe the proven release train id: release run: | release="$(find .package-release -mindepth 1 -maxdepth 1 -type d)" manifest="$release/ekklesia-package-train.json" { echo "version=$(node -p 'JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")).version' "$manifest")" echo "revision=$(node -p 'JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")).source.revision' "$manifest")" echo "manifest_sha256=$(sha256sum "$manifest" | cut -d' ' -f1)" } >>"$GITHUB_OUTPUT" cat "$manifest" # The exact release artifacts, for the jobs that consume them. Version 3 of the artifact # actions: version 4 refuses any server that is not github.com, which this Gitea is not, while # Gitea implements the protocol version 3 uses. A consumer checks the files, not the wrapper. - name: Hand the release train on uses: actions/upload-artifact@c6a3b2bd78b3985e4b2f15397fec357f0fd808de # v3.2.2-node20 with: name: package-train path: .package-release/ if-no-files-found: error retention-days: 7 # The publication tool, given exactly the artifact the package-release job proved, against a # disposable Gitea 1.27.2 (scripts/test-package-publication.sh). Never the real registry, and no # credential: the disposable registry's accounts are created and discarded inside the job. package-publication: needs: package-release runs-on: ubuntu-latest timeout-minutes: 15 env: # Names this job's stacks, so its final step removes exactly these (scripts/lib/ci-project.sh). EKKLESIA_CI_JOB_KEY: package-publication steps: - name: Check out repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false # A job the runner killed (cancelled, or past the runner's own time limit) never ran its # cleanup. Anything a CI job left on this shared daemon more than three hours ago goes first. - name: Sweep stale CI stacks from the runner run: bash scripts/ci-sweep.sh --stale - name: Set up Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version-file: .node-version - name: Activate pinned pnpm run: | corepack enable corepack prepare pnpm@11.12.0 --activate - name: Install dependencies run: pnpm install --frozen-lockfile - name: Receive the release train the gate proved uses: actions/download-artifact@ad191675b41f6a5b46da9a048cb6893812da158b # v3.1.0-node20 with: name: package-train path: .package-release/ - name: Check it is exactly that train env: EXPECTED_MANIFEST_SHA256: ${{ needs.package-release.outputs.manifest_sha256 }} run: | release="$(find .package-release -mindepth 1 -maxdepth 1 -type d)" actual="$(sha256sum "$release/ekklesia-package-train.json" | cut -d' ' -f1)" if [[ -z "$EXPECTED_MANIFEST_SHA256" || "$actual" != "$EXPECTED_MANIFEST_SHA256" ]]; then echo "The received release manifest is not the one the package-release job proved." >&2 exit 1 fi (cd "$release" && sha256sum --check --strict SHA256SUMS) - name: Publish it to a disposable Gitea, and prove the round trip and recovery run: bash scripts/test-package-publication.sh --release "$(find .package-release -mindepth 1 -maxdepth 1 -type d)" # Whatever happened above, this job's own stacks do not outlive it. - name: Remove this job's CI stacks if: always() run: bash scripts/ci-sweep.sh --job image-build: needs: verify runs-on: ubuntu-latest timeout-minutes: 20 env: # Names this job's stacks, so its final step removes exactly these (scripts/lib/ci-project.sh). EKKLESIA_CI_JOB_KEY: image-build # Cache mounts in deploy/Dockerfile need BuildKit; this selects it even where the Docker CLI # has no buildx plugin. DOCKER_BUILDKIT: "1" steps: - name: Check out repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # `actions/checkout` defaults to leaving the job token in `.git/config`, where any later # step — including one a pull request added to this file — could read or reuse it. Nothing # in this workflow pushes, so the credential is never needed after checkout. persist-credentials: false # A job the runner killed (cancelled, or past the runner's own time limit) never ran its # cleanup. Anything a CI job left on this shared daemon more than three hours ago goes first. - name: Sweep stale CI stacks from the runner run: bash scripts/ci-sweep.sh --stale # The one generic platform image (ADR 0044, ADR 0048), proved rather than assumed: it contains no # module, no module template, and no module manifest; its module storage mount point is empty; # EKKLESIA_MODULES_DIR names it; and a checkout with a populated modules/ directory builds an # identical runtime file system. Build only — no registry login, no credentials, no push, no tag # anywhere but this runner's local image store. Tagged per run and attempt, and labeled like # every CI stack, so this job's final step removes the images even if a build is killed. - name: Build the generic platform image and prove it holds no modules run: bash scripts/check-image.sh # Whatever happened above, this job's own stacks do not outlive it. - name: Remove this job's CI stacks if: always() run: bash scripts/ci-sweep.sh --job deploy-smoke: needs: verify runs-on: ubuntu-latest timeout-minutes: 30 # Both topologies (ADR 0045), each on the one generic image: same-origin, the default every # installation gets, and split, which installs a test-only module into module storage, removes # it, and restores it (ADR 0048), and drives a real headless browser across the two origins. strategy: fail-fast: false matrix: topology: - same-origin - split env: # Names this job's stacks, so its final step removes exactly these (scripts/lib/ci-project.sh). EKKLESIA_CI_JOB_KEY: deploy-smoke-${{ matrix.topology }} DOCKER_BUILDKIT: "1" EKKLESIA_SMOKE_TOPOLOGY: ${{ matrix.topology }} steps: - name: Check out repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # `actions/checkout` defaults to leaving the job token in `.git/config`, where any later # step — including one a pull request added to this file — could read or reuse it. Nothing # in this workflow pushes, so the credential is never needed after checkout. persist-credentials: false # A job the runner killed (cancelled, or past the runner's own time limit) never ran its # cleanup. Anything a CI job left on this shared daemon more than three hours ago goes first. - name: Sweep stale CI stacks from the runner run: bash scripts/ci-sweep.sh --stale # The reference deployment end to end (ADR 0044): the generic platform image, base + proxy with # Caddy's internal CA (test-only), an explicit migration apply against this job's own fresh # PostgreSQL, then the API, Dashboard, storage delivery, client-address chain, and verify-full # database TLS through the proxy. Every secret is generated for this run and discarded with # it; nothing is pushed, published, or logged in to. - name: Smoke-test the reference deployment (${{ matrix.topology }}) run: bash scripts/smoke-deploy.sh # Whatever happened above, this job's own stacks do not outlive it. - name: Remove this job's CI stacks if: always() run: bash scripts/ci-sweep.sh --job