Skip to content

Commit e93a3b4

Browse files
ci(release): qualify supported self-hosted platforms (#1616)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 970366f commit e93a3b4

5 files changed

Lines changed: 187 additions & 6 deletions

File tree

.changeset/curvy-corners-heal.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
---
3+
4+
No release note: regenerate the API client with the already-pinned generator; the API contract is unchanged.

.github/workflows/release.yml

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,8 +431,113 @@ jobs:
431431
candidate-application-image: ghcr.io/ls1intum/hephaestus/application-server@${{ needs.tag-images.outputs.application-server-digest }}
432432
postgres-image: ghcr.io/ls1intum/hephaestus/postgres@${{ needs.tag-images.outputs.postgres-digest }}
433433
candidate-source-sha: ${{ needs.release.outputs.sha }}
434+
435+
supported-host-smoke:
436+
needs: [release, tag-images]
437+
if: needs.release.outputs.released == 'true'
438+
name: Host smoke (${{ matrix.architecture }})
439+
strategy:
440+
fail-fast: false
441+
matrix:
442+
include:
443+
- architecture: amd64
444+
runner: ubuntu-24.04
445+
- architecture: arm64
446+
runner: ubuntu-24.04-arm
447+
runs-on: ${{ matrix.runner }}
448+
timeout-minutes: 20
449+
permissions:
450+
# The release is still a draft here, and draft releases are visible only to tokens with push
451+
# access, so `gh release download` in prepare-release-lock.ts needs contents: write.
452+
contents: write
453+
packages: read
454+
steps:
455+
- name: Check out the released tree
456+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
457+
with:
458+
ref: ${{ needs.release.outputs.sha }}
459+
persist-credentials: false
460+
461+
- uses: ./.github/actions/setup-bun
462+
463+
- name: Install cosign
464+
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
465+
466+
- name: Login to GitHub Container Registry
467+
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
468+
with:
469+
registry: ghcr.io
470+
username: ${{ github.actor }}
471+
password: ${{ secrets.GITHUB_TOKEN }}
472+
473+
- name: Smoke-test the blessed install
474+
env:
475+
ARCHITECTURE: ${{ matrix.architecture }}
476+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
477+
RELEASE_SHA: ${{ needs.release.outputs.sha }}
478+
TAG_NAME: ${{ needs.release.outputs.tag_name }}
479+
run: |
480+
set -euo pipefail
481+
cd docker/self-host
482+
cleanup() {
483+
status=$?
484+
if [ "$status" -ne 0 ]; then
485+
docker compose --env-file .env --env-file release-lock.env logs --no-color || true
486+
fi
487+
docker compose --env-file .env --env-file release-lock.env down -v --remove-orphans || true
488+
exit "$status"
489+
}
490+
trap cleanup EXIT
491+
492+
actual_arch=$(docker version --format '{{.Server.Arch}}')
493+
[ "$actual_arch" = "$ARCHITECTURE" ] || { echo "::error::Expected native $ARCHITECTURE, got $actual_arch"; exit 1; }
494+
. /etc/os-release
495+
[ "$ID" = ubuntu ] && [ "$VERSION_ID" = 24.04 ] || { echo "::error::Ubuntu 24.04 is required"; exit 1; }
496+
docker_major=$(docker version --format '{{.Server.Version}}' | cut -d. -f1)
497+
[ "$docker_major" -ge 28 ] || { echo "::error::Docker Engine 28 or newer is required"; exit 1; }
498+
compose_version=$(docker compose version --short)
499+
[ "$(printf '%s\n' 2.24.4 "$compose_version" | sort -V | head -n1)" = 2.24.4 ] || {
500+
echo "::error::Docker Compose 2.24.4 or newer is required"; exit 1; }
501+
502+
./setup.sh
503+
sed -i \
504+
-e 's/^APP_HOSTNAME=$/APP_HOSTNAME=hephaestus-smoke.invalid/' \
505+
-e 's/^ACME_EMAIL=$/ACME_EMAIL=release-smoke@example.invalid/' \
506+
-e 's/^GH_OAUTH_CLIENT_ID=$/GH_OAUTH_CLIENT_ID=release-smoke/' \
507+
-e 's/^GH_OAUTH_CLIENT_SECRET=$/GH_OAUTH_CLIENT_SECRET=release-smoke-secret/' \
508+
-e 's/^HEPHAESTUS_AUTH_BOOTSTRAP_ADMINS=$/HEPHAESTUS_AUTH_BOOTSTRAP_ADMINS=github:1/' \
509+
.env
510+
node ../../scripts/prepare-release-lock.ts "$TAG_NAME"
511+
docker compose --env-file .env --env-file release-lock.env config --quiet
512+
docker compose --env-file .env --env-file release-lock.env up -d --wait --wait-timeout 600
513+
curl --fail --insecure --silent --show-error --max-time 10 --noproxy '*' \
514+
--resolve hephaestus-smoke.invalid:443:127.0.0.1 \
515+
https://hephaestus-smoke.invalid/ >/dev/null
516+
517+
jq -n \
518+
--arg schemaVersion "1" \
519+
--arg release "$TAG_NAME" \
520+
--arg commit "$RELEASE_SHA" \
521+
--arg testedAt "$(date -u +%FT%TZ)" \
522+
--arg os "$(. /etc/os-release; printf '%s %s' "$NAME" "$VERSION_ID")" \
523+
--arg architecture "$actual_arch" \
524+
--arg dockerEngine "$(docker version --format '{{.Server.Version}}')" \
525+
--arg dockerCompose "$compose_version" \
526+
--argjson services "$(docker compose --env-file .env --env-file release-lock.env ps --all --format json | jq -s 'map({service: .Service, state: .State, health: .Health, exitCode: .ExitCode}) | sort_by(.service)')" \
527+
'{schemaVersion: ($schemaVersion | tonumber), release: $release, commit: $commit,
528+
testedAt: $testedAt, host: {os: $os, architecture: $architecture,
529+
dockerEngine: $dockerEngine, dockerCompose: $dockerCompose}, services: $services}' \
530+
> "../../host-smoke-$ARCHITECTURE.json"
531+
532+
- name: Upload smoke-test record
533+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
534+
with:
535+
name: host-smoke-${{ matrix.architecture }}
536+
path: host-smoke-${{ matrix.architecture }}.json
537+
if-no-files-found: error
538+
434539
publish-release:
435-
needs: [release, tag-images, upgrade-test]
540+
needs: [release, tag-images, upgrade-test, supported-host-smoke]
436541
if: needs.release.outputs.released == 'true'
437542
timeout-minutes: 20
438543
runs-on: ubuntu-latest
@@ -460,6 +565,34 @@ jobs:
460565
username: ${{ github.actor }}
461566
password: ${{ secrets.GITHUB_TOKEN }}
462567

568+
- name: Download supported-host smoke records
569+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
570+
with:
571+
pattern: host-smoke-*
572+
path: host-smoke
573+
merge-multiple: true
574+
575+
- name: Verify supported-host smoke records
576+
env:
577+
RELEASE_SHA: ${{ needs.release.outputs.sha }}
578+
TAG_NAME: ${{ needs.release.outputs.tag_name }}
579+
run: |
580+
set -euo pipefail
581+
jq -s -e --arg release "$TAG_NAME" --arg commit "$RELEASE_SHA" '
582+
length == 2 and
583+
([.[].host.architecture] | sort) == ["amd64", "arm64"] and
584+
all(.[];
585+
.schemaVersion == 1 and .release == $release and .commit == $commit and
586+
(.testedAt | fromdateiso8601) and
587+
.host.os == "Ubuntu 24.04" and
588+
(.host.dockerEngine | type) == "string" and
589+
(.host.dockerCompose | type) == "string" and
590+
(.services | length) > 0 and
591+
([.services[].service] | length) == ([.services[].service] | unique | length) and
592+
all(.services[];
593+
if .state == "exited" then .exitCode == 0
594+
else .state == "running" and .health == "healthy" end))' host-smoke/*.json >/dev/null
595+
463596
- name: Verify durable assets from a clean environment
464597
env:
465598
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -563,6 +696,7 @@ jobs:
563696
TAG_NAME: ${{ needs.release.outputs.tag_name }}
564697
run: |
565698
set -euo pipefail
699+
gh release upload "$TAG_NAME" host-smoke/*.json --repo "${{ github.repository }}"
566700
gh release edit "$TAG_NAME" --repo "${{ github.repository }}" --draft=false
567701
[ "$(gh release view "$TAG_NAME" --repo "${{ github.repository }}" --json isImmutable --jq .isImmutable)" = true ] || {
568702
echo "::error::Repository immutable releases must be enabled before publishing"; exit 1; }

docs/admin/install.mdx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,22 @@ The stack you get:
2222

2323
## Requirements
2424

25+
### Supported host matrix
26+
27+
Hephaestus supports the blessed single-host stack on the following native (not emulated) hosts:
28+
29+
| Distribution | CPU architecture |
30+
| --- | --- |
31+
| Ubuntu 24.04 LTS | x86-64 (`amd64`) |
32+
| Ubuntu 24.04 LTS | ARMv8 64-bit (`arm64`) |
33+
34+
Both require Docker Engine 28 or newer and Docker Compose 2.24.4 or newer. Docker supports these
35+
hosts according to its [Ubuntu installation requirements](https://docs.docker.com/engine/install/ubuntu/#os-requirements).
36+
Each release is qualified on both matrix cells before publication.
37+
38+
Only the listed native hosts and the unmodified self-hosted Compose topology are supported. All other
39+
hosts, runtimes, and topologies are outside release qualification.
40+
2541
- **4 vCPUs, 8 GB RAM, 40 GB SSD** recommended. Absolute floor: 2 vCPUs / **8 GB RAM** — the two JVMs
2642
ship with container memory limits of 5 GB (`application-server`) and 2 GB (`webhook-server`), so a
2743
smaller host does not swap, it OOM-kills. On a host below that, lower both limits before the first
@@ -30,8 +46,7 @@ The stack you get:
3046
- **AI practice review adds real memory**: each concurrent review sandbox may use up to
3147
4 GiB. The default caps it at 1 concurrent sandbox; raise
3248
`SANDBOX_MAX_CONCURRENT` only with RAM to match.
33-
- 64-bit Linux with **Docker Engine ≥ 24** and **Docker Compose ≥ 2.24.4** (`docker compose version`),
34-
plus `git` and `openssl`.
49+
- A host in the supported matrix, plus `git` and `openssl`.
3550
- A **DNS A record** for your hostname pointing at the host, with ports **80 and 443**
3651
reachable from the internet (Let's Encrypt HTTP-01, OAuth callbacks, webhooks).
3752
- Outbound HTTPS to `ghcr.io` and `docker.io` for images, `github.com` and `api.github.com` for the

docs/contributor/release-management.mdx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ sequenceDiagram
3535
M->>R: Create draft vX.Y.Z release
3636
R->>R: Generate and verify digest-bound evidence
3737
R->>R: Upgrade seeded data from the previous stable release
38+
R->>R: Qualify supported hosts
3839
R->>R: Promote X.Y.Z / X.Y / latest image tags
3940
R->>R: Publish release and deploy staging
4041
Note over R: Production still requires approval
@@ -56,10 +57,19 @@ sequenceDiagram
5657
ruleset, so the absent required checks don't block the merge. Versioning also moves any
5758
`### Next release` section in `MIGRATION.md` under the version being cut.
5859
3. **Merging the Version PR cuts the release.** The workflow creates a draft release at the merge
59-
commit. After its evidence and seeded-upgrade gates pass, it promotes the CI-built images to
60+
commit. After its evidence, seeded-upgrade, and supported-host gates pass, it promotes the CI-built images to
6061
`X.Y.Z`, `X.Y`, and `latest`, publishes the release, deploys staging, and requests production
6162
approval.
6263

64+
## Supported-host qualification
65+
66+
Every release boots its signed, immutable image lock through the self-hosted topology on native Ubuntu
67+
24.04 `amd64` and `arm64` hosts. The gate enforces the [published support matrix](../admin/install#supported-host-matrix),
68+
waits for stack readiness, and exercises HTTPS ingress. Either cell can block publication.
69+
70+
Each cell's status record is attached to the immutable GitHub release. This qualification covers a
71+
clean boot; the seeded-upgrade gate separately proves the supported migration path.
72+
6373
## Supply-chain evidence
6474

6575
A release remains a draft until every first-party and upstream production image in

scripts/release-deployment-policy.test.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ await test("deployment uses Compose metadata, waits for readiness, and preserves
2121
await test("release publication requires the seeded upgrade gate", () => {
2222
const upgrade = readFileSync(".github/workflows/release-upgrade.yml", "utf8");
2323

24-
assert.match(release, /publish-release:\n\s+needs: \[release, tag-images, upgrade-test\]/);
24+
assert.match(
25+
release,
26+
/publish-release:\n\s+needs: \[release, tag-images, upgrade-test, supported-host-smoke\]/,
27+
);
2528
assert.match(
2629
release,
2730
/candidate-application-image: .*@\$\{\{ needs\.tag-images\.outputs\.application-server-digest \}\}/,
@@ -36,6 +39,21 @@ await test("release publication requires the seeded upgrade gate", () => {
3639
upgrade,
3740
/ref: \$\{\{ inputs\.candidate-source-sha \|\| inputs\.candidate-sha \|\| github\.sha \}\}/,
3841
);
39-
const upgradeGate = release.match(/ {2}upgrade-test:[\s\S]*?\n {2}publish-release:/)?.[0] ?? "";
42+
const upgradeGate =
43+
release.match(/ {2}upgrade-test:[\s\S]*?\n {2}supported-host-smoke:/)?.[0] ?? "";
4044
assert.doesNotMatch(upgradeGate, /secrets: inherit/);
4145
});
46+
47+
await test("release publication requires native smoke tests for every supported host", () => {
48+
const smokeGate =
49+
release.match(/ {2}supported-host-smoke:[\s\S]*?\n {2}publish-release:/)?.[0] ?? "";
50+
51+
assert.match(smokeGate, /architecture: amd64\n\s+runner: ubuntu-24\.04/);
52+
assert.match(smokeGate, /architecture: arm64\n\s+runner: ubuntu-24\.04-arm/);
53+
assert.match(smokeGate, /up -d --wait --wait-timeout 600/);
54+
assert.match(smokeGate, /prepare-release-lock\.ts/);
55+
// Draft releases are visible only to tokens with push access; a read-only token cannot
56+
// download the still-draft release lock and would fail every smoke run.
57+
assert.match(smokeGate, /contents: write/);
58+
assert.match(release, /gh release upload "\$TAG_NAME" host-smoke\/\*\.json/);
59+
});

0 commit comments

Comments
 (0)