CI / verify (pull_request) Successful in 1m4s
CI / infrastructure-smoke (pull_request) Successful in 38s
CI / migration-smoke (pull_request) Successful in 1m53s
CI / package-release (pull_request) Successful in 32s
CI / image-build (pull_request) Successful in 22s
CI / deploy-smoke (same-origin) (pull_request) Successful in 49s
CI / deploy-smoke (split) (pull_request) Successful in 1m39s
CI / package-publication (pull_request) Successful in 59s
The CI package-release gate passed, but the upload found no files: the artifact actions leave hidden files out, and .package-release starts with a dot. Keep the release in package-train instead, in both workflows, and test that no artifact path is hidden. Co-Authored-By: Claude Opus 5.5 <[email protected]>
336 lines
15 KiB
YAML
336 lines
15 KiB
YAML
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 [email protected] --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 [email protected] --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 [email protected] --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-train
|
|
|
|
- name: Describe the proven release train
|
|
id: release
|
|
run: |
|
|
release="$(find package-train -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, from a directory whose name does
|
|
# not start with a dot: the artifact actions leave hidden files out. 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-train/
|
|
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 [email protected] --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-train/
|
|
|
|
- name: Check it is exactly that train
|
|
env:
|
|
EXPECTED_MANIFEST_SHA256: ${{ needs.package-release.outputs.manifest_sha256 }}
|
|
run: |
|
|
release="$(find package-train -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-train -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
|