Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ jobs:
needs: test
if: github.event_name == 'push'
runs-on: ubuntu-latest
# Bloc 97: the image this job builds, verifies and pushes, named once.
# `main` publishes the stable channel (:latest), every other branch the
# rolling one (:dev) — the split README.md documents for ML_HELPER_IMAGE.
#
# It deliberately carries compose's own variable name: the Docker Compose
# check below never set it, so compose fell back to its built-in :dev
# default. On dev that happened to be the tag this job had just built; on
# main, where the job builds :latest, compose looked for an image that had
# never existed on the runner and the step died in under a second with
# "No such image: ghcr.io/magicgg91/ml-helper:dev" — before any container
# started, so nothing about it was a health problem. One name, read by
# every step here and by compose itself, cannot drift like that again.
env:
ML_HELPER_IMAGE: "ghcr.io/magicgg91/ml-helper:${{ github.ref_name == 'main' && 'latest' || 'dev' }}"
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
Expand All @@ -44,7 +58,7 @@ jobs:
with:
context: .
load: true
tags: ghcr.io/magicgg91/ml-helper:${{ github.ref_name == 'main' && 'latest' || 'dev' }}
tags: ${{ env.ML_HELPER_IMAGE }}
- name: Verify container startup
run: |
mkdir -p "$RUNNER_TEMP/ml-helper-data"
Expand All @@ -53,7 +67,7 @@ jobs:
-e DATABASE_URL=file:/app/data/ml-helper.db \
-e NEXTAUTH_URL=http://localhost:3000 \
-e NEXTAUTH_SECRET=container-startup-test-secret \
ghcr.io/magicgg91/ml-helper:${{ github.ref_name == 'main' && 'latest' || 'dev' }} \
"$ML_HELPER_IMAGE" \
node -e "console.log('Container startup verified')"
- name: Verify Docker Compose health
run: |
Expand Down Expand Up @@ -92,7 +106,7 @@ jobs:
--header "Accept: application/vnd.github+json" \
--header "X-GitHub-Api-Version: 2022-11-28" \
"${{ github.api_url }}/repos/${{ github.repository }}/statuses/${{ github.sha }}" \
--data '{"state":"failure","context":"docker-compose/health","description":"Docker Compose app container did not become healthy"}'
--data '{"state":"failure","context":"docker-compose/health","description":"Docker Compose app container did not start or did not become healthy"}'
# Bloc 84: a plain `docker push` of the image loaded in the step above
# started failing deterministically with "unknown blob" (2 runs in a
# row, immediately on the first layer, no prior occurrence in 225+
Expand All @@ -108,5 +122,5 @@ jobs:
with:
context: .
push: true
tags: ghcr.io/magicgg91/ml-helper:${{ github.ref_name == 'main' && 'latest' || 'dev' }}
tags: ${{ env.ML_HELPER_IMAGE }}

30 changes: 30 additions & 0 deletions src/foundation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,33 @@ describe("Docker healthcheck", () => {
expect(healthcheck).toContain('path: "/api/health"');
});
});

// Bloc 97: the "Verify Docker Compose health" job failed on the first push to
// main since that step was written (CI run 34835198084). It never set
// ML_HELPER_IMAGE, so compose used its own :dev default while the job had
// built and loaded :latest — "No such image: ghcr.io/magicgg91/ml-helper:dev",
// in under a second, before any container existed. Two files each spelled the
// tag out on their own, and only the main branch made them disagree.
describe("CI image tag", () => {
const workflow = readFileSync(".github/workflows/ci.yml", "utf8");
const compose = readFileSync("docker-compose.yml", "utf8");

it("hands Docker Compose the very image the job just built", () => {
// The name is the link between the two files: compose reads this variable,
// so the workflow has to be what sets it.
expect(compose).toContain("${ML_HELPER_IMAGE:-");
expect(workflow).toMatch(/^\s+ML_HELPER_IMAGE: /m);
});

it("spells the registry tag exactly once, so no step can drift", () => {
// Every other reference goes through ML_HELPER_IMAGE. A second literal is
// how the branch-dependent tag came apart in the first place. Comment
// lines don't count — the one above quotes the tag on purpose.
const effective = workflow
.split("\n")
.filter((line) => !line.trimStart().startsWith("#"))
.join("\n");
const literals = effective.match(/ghcr\.io\/magicgg91\/ml-helper:/g) ?? [];
expect(literals).toHaveLength(1);
});
});
Loading