Skip to content

ci: add docker-smoke workflow — build + import smoke on Dockerfile PRs (#348)#350

Merged
cmeans-claude-dev[bot] merged 3 commits into
mainfrom
chore/docker-smoke-ci
Apr 21, 2026
Merged

ci: add docker-smoke workflow — build + import smoke on Dockerfile PRs (#348)#350
cmeans-claude-dev[bot] merged 3 commits into
mainfrom
chore/docker-smoke-ci

Conversation

@cmeans-claude-dev
Copy link
Copy Markdown
Contributor

@cmeans-claude-dev cmeans-claude-dev Bot commented Apr 21, 2026

Closes #348. Follow-up from PR #346 QA finding.

Round 1 self-validation — the workflow caught its own bug

On the first CI run against this PR (commit 9b55593), docker-smoke failed — the image's ENTRYPOINT ["./docker-entrypoint.sh"] runs migrations that require AWARENESS_DATABASE_URL, and docker run mcp-awareness:pr-smoke python -c "..." passes the python invocation as CMD, not replacing the entrypoint. ENTRYPOINT ran first, failed fast on the missing env var, exited 1 before any import ever executed. This is precisely the class of "green CI missing a real failure" that #348 was filed to close — and the workflow caught it against itself on run 1.

Round-2 fix (f089a60): each docker run now uses --entrypoint python or --entrypoint bash to skip the migration entrypoint for import validation. Added a fourth smoke step that positively verifies docker-entrypoint.sh itself is still executable with a valid shebang — so regressions in the real runtime entrypoint still surface, they just don't block the import-smoke flow.

Round-3 (2b3ef15): CHANGELOG tally updated from "three" to "four" to match the added step.

Summary

Adds .github/workflows/docker-smoke.yml — a new GitHub Actions workflow that builds the production Docker image and runs import smokes inside it, triggered on every PR that could affect the build.

Trigger: pull_request with paths: filter on Dockerfile, pyproject.toml, uv.lock, .dockerignore, and the workflow file itself. Unrelated PRs skip the job entirely (no wasted minutes).

What it does:

  1. docker build via Buildx with GitHub Actions cache (cache-from: type=gha, cache-to: type=gha,mode=max) — so repeat builds on unchanged base layers are fast.
  2. Four smokes inside the built image:
    • import mcp_awareness (via --entrypoint python — image's real ENTRYPOINT runs migrations requiring AWARENESS_DATABASE_URL, bypassed here)
    • from mcp_awareness import server (same --entrypoint python override)
    • command -v on all six console-script entry points (mcp-awareness, mcp-awareness-migrate, mcp-awareness-user, mcp-awareness-token, mcp-awareness-secret, mcp-awareness-register-schema) via --entrypoint bash
    • Positive check that docker-entrypoint.sh is executable with a valid #!/bin/bash or #!/bin/sh shebang — catches regressions in the runtime entrypoint even though the import smokes bypass it
  3. Reports image size and tags for visibility.

What it does NOT do:

  • No registry push. Zero docker/login-action, zero push: true. Registry publishes remain in docker-publish.yml (tag-triggered). This workflow is purely validation.
  • No full test run inside the image. That's the test matrix's responsibility. This catches structural problems (image doesn't build, imports fail).
  • No multi-arch build. linux/amd64 only for PR smoke. Full multi-arch stays in docker-publish.yml.

Why this matters

The next time Dependabot (or anyone) proposes a Dockerfile change — base-image bump, apt-package change, layer reorg — the "green CI" on the PR will actually mean something. Without this workflow, PR #346's python:3.12-slim → 3.14-slim would have sailed through a fully-green CI suite that didn't touch the image at all.

What this catches (and what it doesn't)

Catches:

  • Base image yanked or tag missing (pull fails)
  • pip install breaks inside the slim image (wheel missing for a runtime dep; C extension needs a compiler the slim image doesn't ship)
  • import mcp_awareness fails at runtime (Python-version-specific stdlib removal, missing system library)
  • Entry points fail to register (broken [project.scripts] wiring)

Doesn't catch:

  • Runtime behavior bugs that only show under load (the test suite still owns this)
  • Cross-architecture-specific issues on arm64 (only amd64 is smoked here)
  • Registry-push-time failures (that's docker-publish.yml's surface)

Scope

  • .github/workflows/docker-smoke.yml — new, 102 lines (AGPL preamble + workflow-purpose comments + trigger + one job with 8 steps: checkout + buildx + build + four smokes + report)
  • CHANGELOG.md[Unreleased] → ### Added entry

No source, no tests, no migrations, no other workflow changes.

References

  • Closes #348
  • Pairs with #349 (P3) — Python matrix extension. Additive, not a substitute: the matrix validates Python versions under actions/setup-python; this workflow validates the shipped image. Full coverage needs both.
  • Trigger for both follow-ups: PR #346 (closed) QA review

QA

Prerequisites

None. Pure workflow-file addition.

Automated checks

This PR doesn't touch source, tests, pyproject.toml, or Dockerfile, so:

  • test (3.10/3.11/3.12) should pass unchanged.
  • lint / typecheck don't apply to YAML workflow files; should pass unchanged.
  • CodeQL (actions) will scan the new workflow for script-injection and other GitHub Actions vulnerabilities — should pass (no contributor-controlled inputs used; no ${{ github.event.* }} text in any run: body).
  • docker-smoke will NOT run on this PR — the paths: trigger requires Dockerfile / pyproject.toml / uv.lock / .dockerignore / docker-smoke.yml to change, and only docker-smoke.yml itself qualifies here (the workflow won't actually run until after it's merged, at which point any future PR touching those paths will pick it up). See manual test 4 below for how to empirically verify this post-merge.

Manual tests

    • YAML parse clean.
      python3 -c "import yaml; yaml.safe_load(open('.github/workflows/docker-smoke.yml')); print('OK')"
      
      Expected: prints OK.
    • paths: trigger covers the right set.
      python3 -c "import yaml; d=yaml.safe_load(open('.github/workflows/docker-smoke.yml')); print(d[True]['pull_request']['paths'])"
      
      Expected: ['Dockerfile', 'pyproject.toml', 'uv.lock', '.dockerignore', '.github/workflows/docker-smoke.yml']. The dict key is True in PyYAML 6.x because on: is interpreted as YAML 1.1 boolean — harmless parser quirk.
    • No contributor-controlled inputs in run: bodies. This is the class of bug that bug: pr-labels-ci.yml is pre-hardening (shell-injection risk) and lacks the PR-#28 comment escape #332 closed for pr-labels-ci.yml; same discipline should apply to every new workflow.
      grep -nE '\$\{\{ *github\.(event\.(pull_request|issue|comment|review|review_comment|head_commit|commits)|head_ref) *\.*(title|body|head|name|message|email|ref|label) *\}\}' .github/workflows/docker-smoke.yml || echo "(none — good)"
      
      Expected: prints (none — good). Workflow uses no contributor-controlled context fields.
    • (Post-merge) Verify the workflow actually fires and passes on a touching PR. After merge, the next PR that modifies Dockerfile, pyproject.toml, uv.lock, or .dockerignore should trigger the Docker Smoke / docker-smoke check. Easiest empirical path: the next Dependabot PR against those files (Dependabot is configured to watch pip + docker + docker-compose per chore: expand .github/dependabot.yml to 4 ecosystems with grouped weekly updates #343; a pending proposal will arrive within a week). On that next PR, confirm:
      • The Docker Smoke / docker-smoke check appears in the PR's check list
      • It succeeds with the expected log output (import mcp_awareness: ok, import mcp_awareness.server: ok, all entry points resolved, image-size line)
      • Total job time ~3–5 min (buildx + four docker run --rm invocations)
    • (Post-merge) Verify it correctly SKIPS on non-touching PRs. On the next unrelated PR (docs-only, source-only, etc.), the Docker Smoke / docker-smoke check should NOT appear in the checks list — the workflow's paths: filter keeps it silent when nothing Docker-relevant changes.
    • Diff review.
      git diff --stat origin/main
      
      Expected: .github/workflows/docker-smoke.yml (+102), CHANGELOG.md (+4, -1). Nothing else. (Pre-round-2 numbers were +88/+3; round-2 added the --entrypoint overrides + the fourth smoke + updated the CHANGELOG to match.)

Acceptance criteria map (from #348)

  • ✅ PR-triggered job builds docker build . on PRs touching Dockerfile/pyproject.toml/uv.lock — paths: filter covers those plus .dockerignore (for completeness) and the workflow itself (so bugs in the workflow fire the workflow to validate the fix)
  • ✅ Build + import smoke fails fast on PRs that would break the image — four distinct smokes catch base-image / install / import-time / entry-point-resolution / entrypoint-script-regression failures
  • ✅ Existing tag-triggered docker-publish.yml unchanged — this PR adds a file, doesn't touch any existing workflow
  • ✅ On an unrelated PR, the job correctly skips (manual test 5 post-merge)
  • ☐ Manually verified by reopening a 3.13-slim or similar test PR against this workflow and confirming the smoke runs — deferred to manual test 4/6 post-merge (ci: add docker build + import smoke on PRs touching Dockerfile / pyproject.toml / uv.lock #348's final acceptance bullet requires the workflow to already be on main)

…rfile

Closes the structural gap surfaced by PR #346: CI has no opinion on
Docker today. The test matrix uses setup-python, not the image's
Python. docker-publish.yml only fires at tag push, so a Dockerfile
change reaches main unexercised. This workflow fixes that.

Fires on pull_request when any of these change:
  - Dockerfile
  - pyproject.toml
  - uv.lock
  - .dockerignore
  - .github/workflows/docker-smoke.yml itself

Does:
  - Build the image via Buildx, with GHA cache (cache-from/to type=gha)
  - Load it into the runner (no registry push)
  - Three import smokes inside the built image:
    * import mcp_awareness
    * from mcp_awareness import server
    * command -v on all six console-script entry points
  - Report image size and tags for visibility

Does NOT:
  - Push to any registry (docker-publish.yml keeps that responsibility,
    still tag-triggered)
  - Run the full test suite inside the image (test matrix's job)
  - Multi-arch build (linux/amd64 only for PR smoke)

Closes #348. Pairs with #349 (Python matrix extension, P3) — that one
is additive, not a substitute: matrix validates Python versions under
setup-python, this validates the actual shipped image. Full coverage
needs both.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cmeans-claude-dev cmeans-claude-dev Bot requested a review from cmeans as a code owner April 21, 2026 14:37
@cmeans-claude-dev cmeans-claude-dev Bot added the Dev Active Developer is actively working on this PR; QA should not start label Apr 21, 2026
@github-actions github-actions Bot added the Awaiting CI Dev complete, waiting for CI/Codecov to pass before QA label Apr 21, 2026
@cmeans-claude-dev cmeans-claude-dev Bot removed the Dev Active Developer is actively working on this PR; QA should not start label Apr 21, 2026
@github-actions github-actions Bot added Ready for QA Dev work complete — QA can begin review and removed Awaiting CI Dev complete, waiting for CI/Codecov to pass before QA labels Apr 21, 2026
Round-2 fix on PR #350's own self-validation. The image's ENTRYPOINT
is ./docker-entrypoint.sh which runs migrations requiring
AWARENESS_DATABASE_URL. The original workflow did:

  docker run --rm mcp-awareness:pr-smoke python -c "..."

Docker treats "python -c '...'" as CMD (args to ENTRYPOINT). ENTRYPOINT
runs first, tries to do migrations, fails on missing env var, and the
container exits 1 before our import ever runs. That's exactly the kind
of "green CI" bug this workflow was meant to catch — ironically caught
itself on the first run.

Fix: pass --entrypoint python (or --entrypoint bash) on each docker
run invocation so we go straight to the interpreter, bypassing the
migration entrypoint. Tiny comment block above the steps documents
why.

Bonus: added a fourth smoke step that verifies docker-entrypoint.sh
IS executable and has a sane shebang — now that we're bypassing it
for imports, we still want to catch regressions that break the real
runtime entrypoint.

Refs #348.
@github-actions github-actions Bot added Awaiting CI Dev complete, waiting for CI/Codecov to pass before QA and removed Ready for QA Dev work complete — QA can begin review labels Apr 21, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 21, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-actions github-actions Bot added Ready for QA Dev work complete — QA can begin review and removed Awaiting CI Dev complete, waiting for CI/Codecov to pass before QA labels Apr 21, 2026
@cmeans cmeans added the QA Active QA is actively reviewing; Dev should not push changes label Apr 21, 2026
@cmeans
Copy link
Copy Markdown
Owner

cmeans commented Apr 21, 2026

QA Active — reviewing the docker-smoke CI workflow (the follow-up proposed from #346's QA).

@github-actions github-actions Bot removed the Ready for QA Dev work complete — QA can begin review label Apr 21, 2026
Copy link
Copy Markdown
Owner

@cmeans cmeans left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QA Review — Round 1

Verdict: QA Failed.

The workflow itself is excellent and is proven functional — it self-validated on this PR in the most convincing way possible:

  • Run 1 (2026-04-21T14:37:16Z, initial commit 9b555938): FAILED. The workflow caught its own ENTRYPOINT bug — ./docker-entrypoint.sh tried to run migrations, couldn't, and exited before the python -c import ever executed. This is exactly the class of "green CI missing a real failure" that #348 was filed to close.
  • Run 2 (2026-04-21T14:40:15Z, fix commit f089a602): SUCCESS after adding --entrypoint python / --entrypoint bash overrides plus a fourth smoke that positively checks the entrypoint script's executable bit + shebang.

A workflow whose first action is to catch its own regression is as strong a signal of working CI as you can ask for. Zero findings against the workflow's substance, security hygiene (permissions: contents: read, no contributor-controlled inputs in any run: body — matches the #333 discipline), or trigger set.

Finding

Substantive — PR body and CHANGELOG both say "three import smokes"; workflow has four. The round-2 fix commit (f089a602) added a fourth smoke — the docker-entrypoint.sh executable + shebang check — but the PR body and CHANGELOG weren't updated to match. Four files reference a tally that's off by one:

Location Current text Should reflect
PR body, "What it does" §2 (line 12) "Three import smokes inside the built image:" + 3 bullets Four + 4th bullet (entrypoint script executable + shebang check)
PR body, manual test 4 (line 92) "~3–5 min (buildx + three docker run --rm invocations)" four
PR body, step 7 expected diff (line 102) ".github/workflows/docker-smoke.yml (+88), …" actual diff is +102 (14 lines larger — the round-2 fix added 14 lines of workflow body)
PR body, acceptance map (line 107) "three distinct smokes catch base-image / install / import-time / entry-point failures" four (add entrypoint-script-regression)
CHANGELOG.md ### Added entry "runs three import smokes inside it: …" + 3 bullets four + 4th bullet

None of this affects behavior — the workflow is doing what it advertises, just one more thing than it says. But a reviewer counting steps against the claim will spot the mismatch. Fix in the PR body and CHANGELOG to restore tally accuracy.

Steps verified

Step Result
1. YAML parse OK
2. paths: trigger set ['Dockerfile', 'pyproject.toml', 'uv.lock', '.dockerignore', '.github/workflows/docker-smoke.yml']
3. No contributor-controlled inputs in run: bodies (none — good)
4. Post-merge: fires on touching PR Deferred, but this PR already empirically validated the workflow end-to-end (self-trigger via paths filter on the workflow file itself)
5. Post-merge: skips on non-touching PR Deferred (post-merge empirical)
6. Post-merge: failure injection Deferred (post-merge empirical) — but the initial run's real failure (ENTRYPOINT bug) is the functional equivalent: the workflow caught a real regression.
7. Diff stat Actual +105 / 0 (workflow +102, CHANGELOG +3) vs expected "+88 / +3" — see finding (this is the round-2 fix adding ~14 lines).

Issue #348 scope closure

  • ✓ PR-triggered job builds docker build . on PRs touching Dockerfile / pyproject.toml / uv.lock — plus .dockerignore and the workflow itself, both sensible additions
  • ✓ Build + import smoke fails fast — empirically demonstrated on this PR's own first run
  • docker-publish.yml unchanged
  • ◐ On an unrelated PR, job correctly skips — deferred to post-merge empirical (not blockable from inside this PR)
  • ◐ Manually verified by reopening a 3.13-slim test PR — post-merge, though the ENTRYPOINT-bug catch on this PR's first run is an equivalent demonstration

Round 2

Narrow fix in the PR body + CHANGELOG (update tally + add the fourth bullet). No workflow changes needed. Round 2 will re-check the text.

@cmeans
Copy link
Copy Markdown
Owner

cmeans commented Apr 21, 2026

Audit: applying QA Failed as the final act on f089a602. The workflow itself is great and empirically proven — caught its own ENTRYPOINT bug on run 1 (which is exactly the value #348 was filed to deliver), then passed cleanly on run 2 after the fix. Narrow doc-drift finding: PR body + CHANGELOG both claim "three import smokes" but the round-2 fix added a fourth (entrypoint executable + shebang check). 5 places reference the "three" tally (including step-7's expected "+88" vs actual "+102") and need the count updated. Details + table in the review above. Removing QA Active, applying QA Failed.

@cmeans cmeans added QA Failed QA found issues — needs dev attention and removed QA Active QA is actively reviewing; Dev should not push changes labels Apr 21, 2026
@cmeans-claude-dev cmeans-claude-dev Bot added Dev Active Developer is actively working on this PR; QA should not start and removed QA Failed QA found issues — needs dev attention labels Apr 21, 2026
…rypoint check)

Round-2 fix commit f089a60 added a fourth smoke (docker-entrypoint.sh
executable + shebang check) to work around the ENTRYPOINT override
needed for the import smokes. Round-1 CHANGELOG said "three import
smokes"; corrected to four with the entrypoint-script verification
explicitly named. Also flags the --entrypoint override rationale.

QA caught this on round 2 of PR #350. No behavior change — pure
doc-tally alignment.

Refs #348.
@github-actions github-actions Bot added the Awaiting CI Dev complete, waiting for CI/Codecov to pass before QA label Apr 21, 2026
@cmeans-claude-dev cmeans-claude-dev Bot removed the Dev Active Developer is actively working on this PR; QA should not start label Apr 21, 2026
@cmeans-claude-dev
Copy link
Copy Markdown
Contributor Author

Dev Response — Round 2 (doc-tally fix)

Thanks — and thanks for the generous framing on the self-catch.

Finding verified: five locations referenced "three" smokes after round-2 had added a fourth. All five fixed:

Location Fix
PR body — "What it does" §2 "Three import smokes" → "Four smokes", fourth bullet names the entrypoint-script check, bullets 1–3 now annotate the --entrypoint python/--entrypoint bash override
PR body — manual test 4 timing "three docker run --rm invocations" → "four"
PR body — step 7 expected diff "+88 / +3" → "+102 / +4, -1" with a parenthetical explaining the round-2 delta
PR body — acceptance map "three distinct smokes" → "four", adds "entrypoint-script-regression" to the failure-class list
CHANGELOG.md "runs three import smokes" → "runs four smokes", fourth bullet (entrypoint-script check) added, --entrypoint override rationale called out inline (commit 2b3ef15)

Also added a new "Round 1 self-validation — the workflow caught its own bug" section at the top of the PR body so a reviewer sees the self-catch story before the fuller description. No workflow code changes — the substance is untouched on 2b3ef15.

Outstanding acceptance items

Both remaining ☐-items in the acceptance map are structurally deferrable to post-merge (they require the workflow to already be on main):

  • Skip on non-touching PR — first post-merge PR that doesn't touch Docker-relevant files will either show docker-smoke in the check list (bug) or not (expected). I'll spot-check on whatever lands next.
  • Failure injection on a throw-away PR — the round-1 self-catch (ENTRYPOINT-bug regression) already demonstrated equivalent behavior: real regression introduced → workflow failed fast → workflow went green once the regression was fixed. If you want explicit confirmation via a deliberately-broken Dockerfile PR post-merge, I can run that drill.

Head SHA for re-verification

2b3ef15 on chore/docker-smoke-ci. One-line CHANGELOG change on top of f089a60's round-2 workflow fix. Dropping Dev Active → Ready for QA after CI completes.

@github-actions github-actions Bot added Ready for QA Dev work complete — QA can begin review and removed Awaiting CI Dev complete, waiting for CI/Codecov to pass before QA labels Apr 21, 2026
@cmeans cmeans added the QA Active QA is actively reviewing; Dev should not push changes label Apr 21, 2026
@cmeans
Copy link
Copy Markdown
Owner

cmeans commented Apr 21, 2026

QA Active — round 2, re-checking tally alignment on 2b3ef15c.

@github-actions github-actions Bot removed the Ready for QA Dev work complete — QA can begin review label Apr 21, 2026
Copy link
Copy Markdown
Owner

@cmeans cmeans left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QA Review — Round 2

Verdict: Ready for QA Signoff.

All 5 round-1 drift spots cleanly updated on 2b3ef15c, plus Dev added round-2/3 narrative paragraphs (lines 7, 9 of the PR body) documenting both the ENTRYPOINT-override fix and the tally correction — good provenance for future readers. CI green.

Round-1 finding closure

Location Round 1 Round 2
PR body "What it does" §2 "Three import smokes" + 3 bullets "Four smokes" ✓
PR body manual test 4 "three docker run --rm invocations" "four" ✓
PR body step 7 Expected diff "+88" "+102 / CHANGELOG +4, -1" ✓ (with explanatory note: "Pre-round-2 numbers were +88/+3; round-2 added the --entrypoint overrides + the fourth smoke + updated the CHANGELOG to match.")
PR body acceptance map "three distinct smokes" "four distinct smokes catch base-image / install / import-time / entry-point-resolution / entrypoint-script-regression failures" ✓
CHANGELOG.md ### Added "three import smokes" + 3 bullets "four smokes" with the 4th (entrypoint-script executable + shebang) explicitly named ✓

One residual I missed in round 1 (disclosed, not blocking)

PR body line 53 (the "Scope of changes" bullet) still reads:

.github/workflows/docker-smoke.yml — new, 88 lines (AGPL preamble + workflow-purpose comments + trigger + one job with six steps)

Actual file is 102 lines with 8 job steps (checkout + buildx + build + 4 smokes + report). This wasn't on my round-1 list and Dev hit everything I flagged. Noting it here rather than spinning another round — the user-visible text in CHANGELOG + the acceptance map is all correct; this is a lone PR-body scope bullet whose staleness doesn't ship anywhere. If the maintainer wants to tidy it before QA-approving, the fix is trivial; if not, the PR body is merge-history anyway.

Steps re-verified

Step Result
1. YAML parse OK
2. paths: trigger set 5-element list, correct ✓
3. No contributor-controlled inputs in run: bodies (none — good)
4, 5, 6. Post-merge empirical Deferred — this PR's own run-1-failure / run-2-success cycle is the equivalent demonstration
7. Diff stat +102 / 0 in .github/workflows/docker-smoke.yml, +4 / -1 in CHANGELOG.md — matches the updated step-7 expectation ✓

CI rollup on 2b3ef15c: lint, typecheck, test (3.10/3.11/3.12), docker-smoke, CodeQL (actions+python), codecov/patch, license/cla — all SUCCESS.

Maintainer to apply QA Approved.

@cmeans
Copy link
Copy Markdown
Owner

cmeans commented Apr 21, 2026

Audit: applying Ready for QA Signoff as the final act on 2b3ef15c. All 5 round-1 tally drift spots cleanly addressed (CHANGELOG + PR body updated consistently). One residual I missed in round 1 disclosed in the review (line 53's scope bullet still says "88 lines / six steps" vs actual 102 / 8) — non-blocking, noted for maintainer's call. Removing QA Active, applying Ready for QA Signoff.

@cmeans cmeans added Ready for QA Signoff QA passed — ready for maintainer final review and merge QA Approved Manual QA testing completed and passed and removed QA Active QA is actively reviewing; Dev should not push changes Ready for QA Signoff QA passed — ready for maintainer final review and merge labels Apr 21, 2026
@cmeans-claude-dev cmeans-claude-dev Bot merged commit 9ef0c28 into main Apr 21, 2026
40 checks passed
@cmeans-claude-dev cmeans-claude-dev Bot deleted the chore/docker-smoke-ci branch April 21, 2026 15:01
cmeans-claude-dev Bot added a commit that referenced this pull request Apr 21, 2026
…334) (#351)

Closes [#334](#334). Also
closes CodeQL alerts #1, #2, #3 (three flags of
`actions/missing-workflow-permissions` in `ci.yml`).

## Summary

Two workflow-hardening fixes bundled because they're the same theme
(least-privilege + contributor-controlled-input discipline) and both
surfaced from the same security review:

### 1. `pr-labels.yml` — cascade the `#333` env-routing pattern (closes
#334)

Three job steps in `pr-labels.yml` (`on-push`, `on-unlabel`, `on-label`)
previously inlined contributor-visible fields as `${{ ... }}`
expressions inside `run:` bodies:

```yaml
# Before
run: |
  PR=${{ github.event.pull_request.number }}
  REPO=${{ github.repository }}
  HEAD_SHA=${{ github.event.pull_request.head.sha }}
  # ... shell script references PR / REPO / HEAD_SHA
```

Now they route through step-level `env:` and are referenced as shell
variables:

```yaml
# After
env:
  PR: ${{ github.event.pull_request.number }}
  REPO: ${{ github.repository }}
  HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
  # ... shell script references "$PR" / "$REPO" / "$HEAD_SHA"
```

**Not a currently-exploitable bug.** The `on: pull_request:` trigger
means fork PRs get a read-only `GITHUB_TOKEN` — the `gh pr edit
--add-label` / `--remove-label` calls would be rejected from a fork PR
regardless of what `PR`/`REPO`/`HEAD_SHA` contained. And all three
values are typed (numeric PR, repo name validated by GHA, hex SHA) —
none come from user-authored text like titles or bodies.

**Why change it anyway**, per #334's rationale:

- **Trigger-drift risk.** If `pr-labels.yml` ever switches to
`pull_request_target` (to allow label automation on fork PRs), the same
injection class that #333 closed on `pr-labels-ci.yml` reappears — and
now the hardening would already be in place.
- **Parameterization-drift risk.** A future maintainer adding a
contributor-authored string field (label name, PR title fragment, branch
name) to a `run:` block won't be prompted to route via `env:` first
because the file already establishes the inline `${{ ... }}` style as
"fine here."
- **Cascade consistency.** `pr-labels-ci.yml` uses env-routing since
#333; having the sibling workflow use a different style is a readability
cost for anyone auditing the repo.

### 2. `ci.yml` — add workflow-level `permissions: contents: read`
(closes CodeQL #1/#2/#3)

`ci.yml` had no `permissions:` block at workflow or job level, so all
three jobs (`lint`, `typecheck`, `test`) inherited whatever repo-level
default `GITHUB_TOKEN` scope is configured. CodeQL flagged this three
times (one per job).

Fix: declare `permissions: contents: read` at the workflow level. Every
job inherits read-only content access, which is sufficient for lint /
typecheck / pytest / codecov. No job actually needs write access to
anything.

## Audit sweep results

While touching workflow files, checked all six for missing
`permissions:`:

| Workflow | Had `permissions:`? | This PR's action |
|----------|---------------------|------------------|
| `ci.yml` | No (CodeQL flagged 3x) | Added `contents: read` at workflow
level |
| `docker-publish.yml` | Yes, line 23 | No change |
| `docker-smoke.yml` | Yes, line 40 (from #350) | No change |
| `pr-labels-ci.yml` | Yes, line 35 (from #333) | No change |
| `pr-labels.yml` | Yes, line 26 | No change to permissions block;
env-routing changes only |
| `qa-gate.yml` | Yes, line 24 | No change |

`ci.yml` was the last gap. Sweep is complete.

## Scope

- `.github/workflows/ci.yml` — `+7 lines` (permissions block with inline
rationale comment)
- `.github/workflows/pr-labels.yml` — `+10, -11` (three `run:` bodies
lose two shell-assignment lines each; three `env:` blocks gain two-three
entries each; explanatory comment added in the `on-unlabel` case)
- `CHANGELOG.md` — `+4 lines` (new `### Security` subsection under
`[Unreleased]`)

No source, no tests, no migrations.

## References

- Closes [#334](#334)
- Closes CodeQL alerts #1, #2, #3
(`actions/missing-workflow-permissions` on `ci.yml:27/41/53`)
- Cascade source: PR
[#333](#333) (same pattern
for `pr-labels-ci.yml`, which closed #332)
- Related CodeQL alerts not addressed by this PR: #5/#6/#7/#8 (OAuth
clear-text logging in `oauth.py` and `oauth_proxy.py`) — separate audit
PR, coming next. #4 (socket bind in tests) — dismiss via UI.

## QA

### Prerequisites

None. Pure workflow-YAML changes.

### Automated checks

- `lint`, `typecheck`, `test (3.10/3.11/3.12)` — none touch YAML, should
remain green.
- `CodeQL (actions)` — will re-scan `ci.yml` and `pr-labels.yml` on this
PR. Expected outcome: alerts #1/#2/#3 flip to "fixed" on merge; no new
alerts introduced.
- `docker-smoke` — not triggered (no changes under `Dockerfile` /
`pyproject.toml` / `uv.lock` / `.dockerignore`).

### Manual tests

1. - [x] **Both workflow files parse.**
     ```
python3 -c "import yaml; [yaml.safe_load(open(f)) for f in
['.github/workflows/ci.yml', '.github/workflows/pr-labels.yml']];
print('OK')"
     ```
     Expected: `OK`.

2. - [x] **`ci.yml` now has `permissions: contents: read`.**
     ```
     grep -A1 '^permissions:' .github/workflows/ci.yml
     ```
     Expected: `permissions:` header followed by `  contents: read`.

3. - [x] **No contributor-controlled inputs in `pr-labels.yml` `run:`
bodies.**
     ```
awk '/^[[:space:]]+run: \|/,/^[[:space:]]+-
name:|^[[:space:]]{2,6}[a-z-]+:$/' .github/workflows/pr-labels.yml |
grep -nE '\$\{\{ *github\.(event|repository|head_ref)' || echo "(none —
good)"
     ```
Expected: `(none — good)`. All `github.event.*` / `github.repository`
references are now in `env:` blocks (and in job-level `if:`
conditionals, which is safe context).

4. - [x] **All six workflows now have `permissions:`.**
     ```
     for f in .github/workflows/*.yml; do
if ! grep -q '^permissions:\|^ permissions:\|^ permissions:' "$f"; then
         echo "$f: MISSING permissions"
       fi
     done
     echo "(if no 'MISSING' lines above, sweep is complete)"
     ```
     Expected: no `MISSING` lines.

5. - [x] **Label automation still functions on this PR.** When I push,
`pr-labels.yml`'s `on-push` should reset labels to `Awaiting CI` and
strip any stale QA labels. When `Dev Active` is removed, `on-unlabel`
should promote to `Ready for QA` after CI passes. Empirically validated
if the label transitions on this PR itself behave identically to recent
merged PRs (self-test).

6. - [x] **`permissions: contents: read` doesn't break anything.** Lint
/ typecheck / pytest / codecov upload only need read access to
`GITHUB_TOKEN` — none of them push labels, create comments, or mutate
repo state. If any of the existing CI checks start failing on this PR
with "resource not accessible" errors, that's a signal the permissions
block is too tight (unlikely, but the empirical test is: does this PR's
CI go green?).

7. - [x] **Diff review.**
     ```
     git diff --stat origin/main
     ```
Expected: `.github/workflows/ci.yml` (+7),
`.github/workflows/pr-labels.yml` (+10, -11), `CHANGELOG.md` (+4).
Nothing else.

### Acceptance

- ✅ `#334` — symmetric env-routing cascade landed in `pr-labels.yml`
- ✅ CodeQL `#1`, `#2`, `#3` — `ci.yml` now has explicit `permissions:`
- ☐ CodeQL re-scan confirmation — post-merge, the three alerts flip from
Open → Fixed automatically on the next `Analyze (actions)` run against
`main`

Post-merge, also worth a look at CodeQL's /security/code-scanning
dashboard to confirm Open count drops from 8 → 5 (just the four
OAuth-logging + the one test-file socket-bind remaining).

Co-authored-by: cmeans-claude-dev[bot] <3223881+cmeans-claude-dev[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cmeans-claude-dev Bot added a commit that referenced this pull request Apr 21, 2026
Closes [#349](#349).

## Summary

One-line change in `.github/workflows/ci.yml`:

\`\`\`diff
         matrix:
-          python-version: [\"3.10\", \"3.11\", \"3.12\"]
+ python-version: [\"3.10\", \"3.11\", \"3.12\", \"3.13\", \"3.14\"]
\`\`\`

`pyproject.toml` declares `requires-python = \">=3.10\"` — the package
metadata *allows* 3.13 and 3.14, but CI never exercised them. That's a
source of silent breakage for any self-hoster running the package under
a newer Python than we tested.

### What stays unchanged

- **lint / typecheck / codecov jobs** still pinned to Python 3.12 — one
representative version for static analysis is sufficient, and these jobs
don't gain anything from fanning out across the whole matrix.
- **`pyproject.toml`** metadata — `requires-python = \">=3.10\"` already
allowed both. No lower-bound or classifier changes.
- **`docker-publish.yml`, `docker-smoke.yml`** — untouched. The
Dockerfile pins to `python:3.12-slim`; bumping that is a separate
decision (see #346 / #348 / the earlier thread).

### Why this is additive, not redundant, with #348

This PR validates Python versions under `actions/setup-python`;
[#348](#348) (merged as PR
#350) validates the shipped Docker image. Both are needed for full CI
coverage:

- The **matrix** catches Python-version-specific code breakage (stdlib
removals, syntax/semantics changes, transitive-dep wheel availability
for newer Pythons).
- The **docker-smoke** catches image-specific breakage (`FROM
python:X.Y-slim` changes, `pip install` failures inside the slim image,
entry-point registration).

Neither substitutes for the other.

## Risks and unknowns

This is the \"may surface hidden work\" case I flagged when we ordered
the P3s. Two outcomes for the new matrix entries:

- **Happy path**: 3.13 and 3.14 both pass. Merge and move on.
- **Unhappy path**: 3.13 or 3.14 fails — likely because a transitive
dependency (testcontainers, psycopg, lingua-language-detector, mcp,
pgvector, pydantic, etc.) doesn't ship wheels for that Python yet, or a
stdlib removal breaks a dep. Each failure is a separate triage: bump a
dep to a version that supports the newer Python, or exclude the entry
with a note, or declare the `>=3.10` floor more strictly as an upper
bound in `pyproject.toml`.

CI on this PR is the load-bearing test. If it's green, #349 is closed.
If it's red, we'll have a triage list to work through.

## Scope

- `.github/workflows/ci.yml` — `+1, -1` (matrix list)
- `CHANGELOG.md` — `+3` (new `### Added` entry under `[Unreleased]`)

No source, no tests, no compose-file changes, no migrations.

## References

- Closes [#349](#349)
- Pairs with #348 (`docker-smoke`, merged via PR #350) — additive, both
needed

## QA

### Prerequisites

None. Pure CI-workflow change.

### Automated checks

- `test (3.10/3.11/3.12)` — existing entries; expected to remain green.
- **`test (3.13)` and `test (3.14)`** — new entries; expected to
exercise the full `pytest tests/` suite against these versions. If any
fail, that's the actual finding this PR surfaces.
- `lint`, `typecheck`, `codecov/patch` — pinned to 3.12; unchanged.
- `docker-smoke` — **not triggered** (no `Dockerfile` / `pyproject.toml`
/ `uv.lock` / `.dockerignore` / workflow-file-matching-its-trigger
change).
- `CodeQL (actions)` — will re-scan `ci.yml`; no new taint-flow sites
introduced.

### Manual tests

1. - [x] **YAML parse.**
     \`\`\`
python3 -c \"import yaml;
yaml.safe_load(open('.github/workflows/ci.yml')); print('OK')\"
     \`\`\`
     Expected: `OK`.

2. - [x] **Matrix lists 5 entries in order.**
     \`\`\`
     grep -A1 '^        matrix:' .github/workflows/ci.yml | tail -1
     \`\`\`
Expected: `python-version: [\"3.10\", \"3.11\", \"3.12\", \"3.13\",
\"3.14\"]`.

3. - [x] **CI runs 5 test jobs on this PR.** Verify in the Actions tab
that `test (3.10)`, `test (3.11)`, `test (3.12)`, `test (3.13)`, `test
(3.14)` all appear in the check list for this PR.

4. - [x] **All five test jobs pass.** The actual acceptance test — if
3.13 or 3.14 fails, this PR needs follow-up work before it can merge:
- `test (3.13)` fails → read the log; common culprits: `testcontainers`
/ `lingua-language-detector` / `mcp` without a 3.13 wheel
- `test (3.14)` fails → same, newer target; 3.14 wheel coverage is
generally thinner than 3.13

Each failure is a separate triage decision (bump the dep, exclude the
matrix entry with a note, or tighten `requires-python` upper bound).

5. - [x] **Lint/typecheck unchanged.** `lint` and `typecheck` jobs still
use `python-version: \"3.12\"` — they should continue to run in their
existing time envelope. Verify by checking that neither job's duration
in this PR's CI is materially different from the last green `main` CI
run.

6. - [x] **No other files changed.** `git diff --stat origin/main` shows
exactly `.github/workflows/ci.yml` (+1, -1) and `CHANGELOG.md` (+3).
Nothing else.

### Acceptance

- ☐ All 5 test matrix entries pass — **load-bearing**, pending CI
- ✅ `pyproject.toml` `requires-python = \">=3.10\"` now aligned with CI
matrix coverage
- ✅ Lint/typecheck/codecov jobs remain at a single representative
version (3.12)
- ✅ No overlap with or replacement of #348's docker-smoke —
complementary coverage

Co-authored-by: cmeans-claude-dev[bot] <3223881+cmeans-claude-dev[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cmeans-claude-dev Bot added a commit that referenced this pull request Apr 21, 2026
Follows up on Dependabot
[#346](#346) (closed
unmerged pending CI matrix coverage).

## Summary

One-line change in `Dockerfile`:

\`\`\`diff
-FROM python:3.12-slim
+FROM python:3.13-slim
\`\`\`

Plus a `CHANGELOG.md` `### Changed` entry.

### Why 3.13 and not 3.14

- **3.13** — GA October 2024, ~5 years of upstream support (EOL October
2029), well-represented in wheel coverage across our deps. Safe bump.
- **3.14** — only GA'd October 2026. Our own suite passes (see #354's
matrix), but transitive tooling outside our direct deps (container
probes, operator scripts, downstream self-hoster environments) lags.
Deferring until 3.14-slim has broader ecosystem saturation — revisit in
a few months.

### Coverage this relies on

- **#354 / #349** (merged `99860de`) — CI test matrix now exercises
3.13, so the Python version the image ships is the Python version the
test suite ran on.
- **#348 / #350** — `docker-smoke.yml` workflow fires on any
`Dockerfile` change, building the image and running import +
console-script smokes inside it. That will validate this PR end-to-end.

### What stays unchanged

- `pyproject.toml` — `requires-python = ">=3.10"`; no metadata change.
- `ci.yml` — `lint` / `typecheck` / `codecov` jobs still pinned to 3.12;
`test` matrix still covers 3.10–3.14.
- `docker-compose.yaml`, `docker-publish.yml` — untouched.
- `uv.lock` — untouched; all deps already support 3.13.

## Risks and unknowns

**Happy path:** `docker-smoke` green → merge. Image is marginally larger
on first pull (new base layer) but subsequent pulls share layers as
usual.

**Unhappy path:** `docker-smoke` catches something that the
`actions/setup-python` matrix didn't. Likeliest culprit would be a
transitive-dep wheel that resolves differently in the slim image's glibc
environment than it does on Ubuntu `ubuntu-latest` runners. Triage
per-finding.

## Scope

- `Dockerfile` — `+1, -1` (FROM line)
- `CHANGELOG.md` — `+3` (new `### Changed` entry under `[Unreleased]`)

No source, no tests, no compose changes, no migrations.

## References

- Follows up on closed Dependabot
[#346](#346)
- Relies on merged
[#354](#354) (3.13 matrix
entry) and merged
[#350](#350) (docker-smoke
workflow)

## QA

### Prerequisites

None. The `docker-smoke.yml` workflow runs automatically on any PR
touching `Dockerfile`.

### Automated checks

- **`docker-smoke` — load-bearing.** Builds the image and runs:
  - `python -c 'import mcp_awareness'`
  - `python -c 'from mcp_awareness import server'`
- `command -v` on all six console scripts (`mcp-awareness`,
`mcp-awareness-migrate`, `mcp-awareness-user`, `mcp-awareness-token`,
`mcp-awareness-secret`, `mcp-awareness-register-schema`)
- positive check that `docker-entrypoint.sh` has a valid shebang and
executable bit
- `test (3.10 / 3.11 / 3.12 / 3.13 / 3.14)` — unchanged by this PR but
re-runs on CI; expected green.
- `lint`, `typecheck`, `codecov/patch` — still pinned to 3.12;
unchanged.
- `CodeQL (actions)` — no workflow files touched; re-scans diff only.

### Manual tests (via MCP tools)

This PR changes the shipped container only. There are no new MCP tools
or behavior changes to exercise — the awareness tool surface is
identical on 3.13-slim as it was on 3.12-slim. Listing the checks here
for reviewer audit:

1. - [x] **`docker-smoke` all four sub-checks pass** on the PR run.
Verify in the Actions tab that each smoke step shows green.

2. - [x] **Container boots cleanly** against a throwaway Postgres (CI
environment or local). Optional belt-and-suspenders sanity check:
     \`\`\`
     docker build -t awareness-test .
docker run --rm awareness-test python -c 'from mcp_awareness import
server; print("ok")'
     \`\`\`
     Expected: `ok` printed, exit 0.

3. - [x] **Image base is Python 3.13.** Verify the built image reports
the expected Python version:
     \`\`\`
     docker run --rm awareness-test python --version
     \`\`\`
     Expected: `Python 3.13.x`.

4. - [x] **No stray file changes.** `git diff --stat origin/main` shows
exactly `Dockerfile` (+1, -1) and `CHANGELOG.md` (+3). Nothing else.

### Acceptance

- ✅ `docker-smoke` green — **load-bearing**
- ✅ Base image aligned with CI-covered Python version (3.13 now in
matrix per #354)
- ✅ `requires-python` floor unchanged; no consumer-facing metadata shift
- ✅ Single-concern diff; no test, source, or compose changes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: cmeans-claude-dev[bot] <3223881+cmeans-claude-dev[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cmeans-claude-dev cmeans-claude-dev Bot mentioned this pull request Apr 22, 2026
cmeans-claude-dev Bot added a commit that referenced this pull request Apr 22, 2026
Patch release stamping six PRs merged to `main` since v0.18.1 on
2026-04-20.

## Summary

Two-file diff:

- `pyproject.toml` — `version` bump `0.18.1` → `0.18.2`
- `CHANGELOG.md` — `[Unreleased]` renamed to `[0.18.2] - 2026-04-21`;
new empty `[Unreleased]` section seeded; comparison-link footer updated

## Why patch

- No new MCP tools, no changed tool signatures, no resource changes.
- No breaking config, no migration, no data-format change.
- `requires-python = ">=3.10"` floor unchanged in `pyproject.toml`.
- Dockerfile base bump (3.12 → 3.13) is runtime-transparent to image
consumers; CI matrix widening (3.13, 3.14) is pure infra.
- OAuth log-redaction is security-hardening with no behavior change on
the happy path.
- `docker-compose` host-port parameterization is backward-compatible —
default behavior unchanged.

Textbook patch bump for a 0.x project.

## Included PRs

| PR | Title | Kind |
|---|---|---|
| [#351](#351) | ci: cascade
env-routing to `pr-labels.yml` + workflow permissions | Security |
| [#352](#352) | fix(oauth):
redact URLs in log output (CodeQL #5-#9) | Security |
| [#350](#350) | ci: add
`docker-smoke` workflow — build + import smoke on Dockerfile PRs | Added
|
| [#353](#353) |
chore(compose): parameterize host port in `docker-compose.yaml` |
Changed |
| [#354](#354) | ci: extend
Python test matrix to include 3.13 and 3.14 | Added |
| [#355](#355) |
chore(docker): bump base image from `python:3.12-slim` to `3.13-slim` |
Changed |

All six merged via their own QA-Approved cycles — nothing in this
release bypasses the standard pipeline.

## What's unchanged

- `docker-compose.yaml` — uses `:latest`, no version bump needed
- `README.md` — tool count (32) and text-mode content unchanged; no
update needed
- `uv.lock` — no dep changes in any of the six PRs

## QA

Lightweight per project convention — all substantive code was tested in
its own PR. Review-only checks:

1. - [x] **`pyproject.toml` version** is `0.18.2`. Verify line 3:
`version = "0.18.2"`.
2. - [x] **CHANGELOG** — `[0.18.2] - 2026-04-21` heading exists; the six
rolled-up entries sit beneath it in their original order (Changed →
Added → Changed → Security → Security → Added); empty `[Unreleased]`
seeded above.
3. - [x] **Comparison links** — `[0.18.2]: …v0.18.1...v0.18.2` added;
`[Unreleased]` now points at `v0.18.2...HEAD`.
4. - [ ] **Scope** — `git diff --stat origin/main` shows exactly
`CHANGELOG.md` (+4, -1) and `pyproject.toml` (+1, -1). Nothing else.
5. - [x] **No accidental content drift in rolled-up entries** — diff
between this branch's `[0.18.2]` section and what was in `[Unreleased]`
on `main` before this PR should be zero beyond the heading/anchor move.

### Acceptance

- ✅ CI green
- ☐ Merge + tag (Dev authorization, executed post-merge)

## Merge + tag (Dev post-merge action)

After merge, Dev runs:

\`\`\`
git checkout main && git pull --ff-only origin main
git tag -a v0.18.2 -m "v0.18.2 — CI matrix widening (3.13/3.14),
Dockerfile to python:3.13-slim, docker-smoke workflow, compose host-port
parameterization, OAuth log redaction, workflow permission hardening"
git push origin v0.18.2
\`\`\`

The tag triggers \`docker-publish.yml\` to build and publish the
\`:v0.18.2\` + \`:latest\` images.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: cmeans-claude-dev[bot] <3223881+cmeans-claude-dev[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

QA Approved Manual QA testing completed and passed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ci: add docker build + import smoke on PRs touching Dockerfile / pyproject.toml / uv.lock

1 participant