From 3263ec47755f6984945a8681cdd4a2ee8a6d5481 Mon Sep 17 00:00:00 2001 From: Christopher Harrison Date: Wed, 26 Aug 2026 09:57:42 -0700 Subject: [PATCH 01/10] ci(validate): configure git identity in verify-deltas before git am The verify-deltas job runs `build-branches.mjs --check`, which applies the module delta patches with `git am`. On a fresh CI runner git has no committer identity, so `git am` aborts with "fatal: empty ident name", failing the job for any PR that touches course-build/. The sibling build-and-test job already configures an identity for the same reason; add the equivalent step to verify-deltas. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3750cefd-d322-4f02-9184-c8ac3eba2fff --- .github/workflows/validate-branches.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/validate-branches.yml b/.github/workflows/validate-branches.yml index 1b30b99..2eed427 100644 --- a/.github/workflows/validate-branches.yml +++ b/.github/workflows/validate-branches.yml @@ -57,6 +57,10 @@ jobs: node-version: '22' - name: Ensure base commit present run: git fetch --no-tags origin "$(node -e "console.log(require('./course-build/manifest.json').base.sha)")" || true + - name: Configure git identity (for git am) + run: | + git config user.name "acc-course-bot" + git config user.email "acc-course-bot@users.noreply.github.com" - name: Deterministic delta check (trees, assets, ancestry) run: node course-build/scripts/build-branches.mjs --check - name: Self-test (classification + path detection) From a1cee749691ce76791bc9877bfb7776fb40c0433 Mon Sep 17 00:00:00 2001 From: Christopher Harrison Date: Wed, 26 Aug 2026 09:59:31 -0700 Subject: [PATCH 02/10] ci(validate): run gitleaks OSS binary instead of the licensed action gitleaks/gitleaks-action@v2 refuses to run without a paid GITLEAKS_LICENSE secret on organization repos, so the secret-scan gate failed on every PR with "missing gitleaks license". Download and run the pinned gitleaks OSS binary (v8.30.1) directly with `gitleaks git`, preserving the full commit-history secret scan (non-zero exit on any finding) without the license dependency. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3750cefd-d322-4f02-9184-c8ac3eba2fff --- .github/workflows/validate-branches.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/validate-branches.yml b/.github/workflows/validate-branches.yml index 2eed427..b560cf8 100644 --- a/.github/workflows/validate-branches.yml +++ b/.github/workflows/validate-branches.yml @@ -73,6 +73,8 @@ jobs: echo "Buildable: $branches" # Secret scan of the delta store and everything the built branches would contain. + # Runs the gitleaks OSS binary directly rather than gitleaks/gitleaks-action@v2, + # which requires a paid GITLEAKS_LICENSE secret for organization repositories. secret-scan: runs-on: ubuntu-latest steps: @@ -81,10 +83,15 @@ jobs: fetch-depth: 0 ref: ${{ inputs.ref || github.ref }} - name: gitleaks (delta store + course-build) - uses: gitleaks/gitleaks-action@v2 env: - GITLEAKS_ENABLE_UPLOAD_ARTIFACT: 'false' - GITLEAKS_ENABLE_SUMMARY: 'true' + GITLEAKS_VERSION: '8.30.1' + run: | + set -euo pipefail + curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" -o /tmp/gitleaks.tar.gz + tar -xzf /tmp/gitleaks.tar.gz -C /tmp gitleaks + /tmp/gitleaks version + # Scan the full commit history (fetch-depth: 0). Exits non-zero on any finding. + /tmp/gitleaks git . --redact --verbose # Heavy gate: build each buildable learner branch from deltas and run the suites # that exist in that cumulative state (web / .NET / Java / Python / Playwright), From d7520d7c54a8c10bbf9ed161b207827588ae199d Mon Sep 17 00:00:00 2001 From: Christopher Harrison Date: Wed, 26 Aug 2026 10:16:20 -0700 Subject: [PATCH 03/10] ci(validate): provision /data volume before build-and-test suites The learner-branch services default their SQLite DBs to /data/.db (overridable via *_DB_PATH), where /data is a mounted volume under docker compose. The build-and-test job runs `mvn test` / `dotnet test` / `pytest` directly on the runner, which has no such volume, so the Spring services (workforce/audit/auth) abort at context load with "path to '/data/workforce.db': '/data' does not exist", failing every module. Create a writable /data on the runner to mirror the runtime contract. The fix stays at the CI layer: touching the app config instead would alter the learner-branch trees that verify-deltas pins via manifest.expectedTreeSha. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3750cefd-d322-4f02-9184-c8ac3eba2fff --- .github/workflows/validate-branches.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/validate-branches.yml b/.github/workflows/validate-branches.yml index b560cf8..3aee603 100644 --- a/.github/workflows/validate-branches.yml +++ b/.github/workflows/validate-branches.yml @@ -128,6 +128,14 @@ jobs: run: | git config user.name "acc-course-bot" git config user.email "acc-course-bot@users.noreply.github.com" + - name: Provision /data volume for service DBs + # The services default their SQLite databases to /data/.db (overridable via + # *_DB_PATH), where /data is a mounted volume under docker compose. Bare `mvn test` + # / `dotnet test` / `pytest` on the runner have no such volume, so the Spring + # services abort at startup with "path to '/data/...': '/data' does not exist". + # Create it writable to mirror the runtime contract without touching learner-branch + # app files (which would change the delta-store tree SHAs verify-deltas asserts). + run: sudo mkdir -p /data && sudo chmod 777 /data - name: Build + validate ${{ matrix.start_branch }} run: bash course-build/scripts/validate-branch.sh "${{ matrix.start_branch }}" env: From ecf9685bfdb46ed8e0802dc28d0319b0de135415 Mon Sep 17 00:00:00 2001 From: Christopher Harrison Date: Wed, 26 Aug 2026 10:21:09 -0700 Subject: [PATCH 04/10] ci(validate): install pytest and gate python suites on real test files The build-and-test job failed with "No module named pytest": the Python step ran `python -m pytest` but only `pip install -e services/`, which omits pytest (declared in the service's `dev` extra). It also ran pytest whenever a tests/ dir existed, but early modules ship a tests/ dir with only a README, so pytest would exit 5 (no tests collected). Only run pytest when real test files (test_*.py / *_test.py) are present, and in that case install the dev extra (pins pytest/pytest-asyncio) with a direct pytest install as a fallback so `python -m pytest` is always available. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3750cefd-d322-4f02-9184-c8ac3eba2fff --- course-build/scripts/validate-branch.sh | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/course-build/scripts/validate-branch.sh b/course-build/scripts/validate-branch.sh index 5bbd2c7..fb672d4 100755 --- a/course-build/scripts/validate-branch.sh +++ b/course-build/scripts/validate-branch.sh @@ -77,8 +77,18 @@ echo "==> Java services build (all on Java 21; audit/auth target Java 17 bytecod echo "==> Python services install + pytest" for svc in reporting-svc notifications-svc; do if [ -f "services/$svc/pyproject.toml" ]; then - pip install -e "services/$svc" - if [ -d "services/$svc/tests" ]; then ( cd "services/$svc" && python -m pytest -q ); fi + # Early modules ship a tests/ dir containing only a README; real test files + # (test_*.py / *_test.py) appear from M03 onward. Only run pytest when they exist, + # otherwise just verify the service is installable. + if ls "services/$svc"/tests/test_*.py "services/$svc"/tests/*_test.py >/dev/null 2>&1; then + # Install the dev extra (pins pytest/pytest-asyncio) when declared, and guarantee + # pytest is importable so `python -m pytest` never fails with "No module named pytest". + pip install -e "services/$svc[dev]" + python -c "import pytest" 2>/dev/null || pip install pytest pytest-asyncio + ( cd "services/$svc" && python -m pytest -q ) + else + pip install -e "services/$svc" + fi fi done From 915373261997c57d4706d0a4ec922f8999b157cc Mon Sep 17 00:00:00 2001 From: Christopher Harrison Date: Wed, 26 Aug 2026 10:34:45 -0700 Subject: [PATCH 05/10] ci(validate): health-gate the full stack before Playwright e2e The QR e2e (present from start-of-module-06) failed with "element not found": the asset detail page fetches assets-svc AND workforce-svc and hides its body (including the QR card) if either call throws. Playwright's webServer starts `npm run dev` (the whole stack) but only waits for the web app on :4321, so the slower JVM workforce-svc was usually still booting when the spec ran, and both retries lost the same race. Pre-start the stack, wait for web + assets-svc + workforce-svc /health, then run Playwright reusing the warm server (reuseExistingServer is true when CI is unset), keeping --retries=2. Only the services the current specs need are gated, so earlier modules whose specs never touch the backends are unaffected. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3750cefd-d322-4f02-9184-c8ac3eba2fff --- course-build/scripts/validate-branch.sh | 52 ++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/course-build/scripts/validate-branch.sh b/course-build/scripts/validate-branch.sh index fb672d4..7b2cfd6 100755 --- a/course-build/scripts/validate-branch.sh +++ b/course-build/scripts/validate-branch.sh @@ -96,7 +96,57 @@ echo "==> Playwright e2e (if present in this state)" if [ -f playwright.config.ts ]; then npm ci --no-audit --no-fund npx playwright install --with-deps chromium - CI=1 npm run test:e2e + + # playwright.config's webServer runs `npm run dev` (the whole stack) but only waits for + # the web app on :4321. The slower backends -- the JVM services especially -- are often + # still booting when specs start, so any spec that transitively needs them flakes as + # "element not found" (e.g. the asset detail page fetches assets-svc AND workforce-svc, + # and hides its content -- including the QR card -- if either call fails). Pre-start the + # full stack ourselves, wait for every service's /health, then run Playwright reusing the + # warm server (reuseExistingServer is true when CI is unset) so specs only run once the + # whole stack is ready. + npm run dev >/tmp/e2e-stack.log 2>&1 & + STACK_PID=$! + + wait_health() { + local name="$1" url="$2" mode="${3:-ok}" i + for i in $(seq 1 120); do + if [ "$mode" = "any" ]; then + # "server responding at all" -- the web root can 5xx while backends warm up. + curl -s -o /dev/null "$url" && { echo " ready: $name"; return 0; } + else + curl -sf "$url" >/dev/null 2>&1 && { echo " ready: $name"; return 0; } + fi + if ! kill -0 "$STACK_PID" 2>/dev/null; then echo "ERROR: dev stack exited before $name was ready" >&2; return 1; fi + sleep 2 + done + echo "ERROR: timed out waiting for $name ($url)" >&2; return 1 + } + + stack_ok=1 + wait_health "web" "http://localhost:4321/" "any" || stack_ok=0 + [ "$stack_ok" = 1 ] && { wait_health "assets-svc" "http://localhost:5001/health" || stack_ok=0; } + [ "$stack_ok" = 1 ] && { wait_health "workforce-svc" "http://localhost:5002/health" || stack_ok=0; } + # reporting/notifications/audit/auth also start via `npm run dev`; the current specs only + # need web + assets-svc + workforce-svc, so we don't gate on the rest (keeps this robust + # for earlier modules whose specs never touch them). + + if [ "$stack_ok" != 1 ]; then + echo "==== dev stack log (tail) ===="; tail -n 150 /tmp/e2e-stack.log || true + kill "$STACK_PID" 2>/dev/null || true + exit 1 + fi + + # Reuse the already-warm stack (CI unset -> reuseExistingServer true); keep CI-style retries. + set +e + env -u CI npx playwright test --retries=2 + e2e_rc=$? + set -e + kill "$STACK_PID" 2>/dev/null || true + if [ "$e2e_rc" -ne 0 ]; then + echo "ERROR: Playwright e2e failed (exit $e2e_rc)" >&2 + exit "$e2e_rc" + fi fi echo "==> ${START_BRANCH}: all applicable suites passed" From 1488cbb88a3ab9500ea0d493c28a3fe7e629caa7 Mon Sep 17 00:00:00 2001 From: Christopher Harrison Date: Wed, 26 Aug 2026 10:42:58 -0700 Subject: [PATCH 06/10] ci(validate): add e2e failure diagnostics (endpoints + error-context) Temporary diagnostics on Playwright e2e failure: dump the web /assets links, assets-svc responses, the rendered detail page's QR/error markers, and Playwright's error-context, to pinpoint why the QR spec fails with a warm stack. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3750cefd-d322-4f02-9184-c8ac3eba2fff --- course-build/scripts/validate-branch.sh | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/course-build/scripts/validate-branch.sh b/course-build/scripts/validate-branch.sh index 7b2cfd6..e6c0e2e 100755 --- a/course-build/scripts/validate-branch.sh +++ b/course-build/scripts/validate-branch.sh @@ -142,11 +142,24 @@ if [ -f playwright.config.ts ]; then env -u CI npx playwright test --retries=2 e2e_rc=$? set -e - kill "$STACK_PID" 2>/dev/null || true if [ "$e2e_rc" -ne 0 ]; then echo "ERROR: Playwright e2e failed (exit $e2e_rc)" >&2 + echo "==== diag: web /assets detail links ====" + curl -s http://localhost:4321/assets | grep -oE 'href="/assets/[^"]*"' | sort -u | head -20 || true + echo "==== diag: assets-svc GET /assets (first 400 chars) ====" + curl -s http://localhost:5001/assets | head -c 400 || true; echo + echo "==== diag: assets-svc GET /assets/1 status ====" + curl -s -o /dev/null -w '%{http_code}\n' http://localhost:5001/assets/1 || true + echo "==== diag: web /assets/1 (qr / error markers) ====" + curl -s http://localhost:4321/assets/1 | grep -ioE 'qr code|alert-danger[^<]*|[0-9]{3} [A-Za-z ]+from http[^<"]*' | head -20 || true + echo "==== Playwright error-context ====" + find test-results -name 'error-context.md' 2>/dev/null | while read -r f; do echo "--- $f ---"; sed -n '1,50p' "$f"; done || true + echo "==== dev stack log (tail) ====" + tail -n 100 /tmp/e2e-stack.log || true + kill "$STACK_PID" 2>/dev/null || true exit "$e2e_rc" fi + kill "$STACK_PID" 2>/dev/null || true fi echo "==> ${START_BRANCH}: all applicable suites passed" From 45ea6ba0585c41ed9812202297c14cbf74bc592c Mon Sep 17 00:00:00 2001 From: Christopher Harrison Date: Wed, 26 Aug 2026 10:50:41 -0700 Subject: [PATCH 07/10] ci(validate): sharpen e2e failure diagnostics (real asset id + error-context) Curl the actual first-by-tag asset id, its /qr endpoint, and dump the full Playwright error-context ARIA snapshot plus assets-svc stack log lines, all set -e safe, to pinpoint why the QR heading is absent with a warm stack. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3750cefd-d322-4f02-9184-c8ac3eba2fff --- course-build/scripts/validate-branch.sh | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/course-build/scripts/validate-branch.sh b/course-build/scripts/validate-branch.sh index e6c0e2e..9a486a9 100755 --- a/course-build/scripts/validate-branch.sh +++ b/course-build/scripts/validate-branch.sh @@ -144,18 +144,21 @@ if [ -f playwright.config.ts ]; then set -e if [ "$e2e_rc" -ne 0 ]; then echo "ERROR: Playwright e2e failed (exit $e2e_rc)" >&2 - echo "==== diag: web /assets detail links ====" - curl -s http://localhost:4321/assets | grep -oE 'href="/assets/[^"]*"' | sort -u | head -20 || true - echo "==== diag: assets-svc GET /assets (first 400 chars) ====" - curl -s http://localhost:5001/assets | head -c 400 || true; echo - echo "==== diag: assets-svc GET /assets/1 status ====" - curl -s -o /dev/null -w '%{http_code}\n' http://localhost:5001/assets/1 || true - echo "==== diag: web /assets/1 (qr / error markers) ====" - curl -s http://localhost:4321/assets/1 | grep -ioE 'qr code|alert-danger[^<]*|[0-9]{3} [A-Za-z ]+from http[^<"]*' | head -20 || true + set +e + first_id="$(curl -s http://localhost:5001/assets | sed -n 's/.*"id":\([0-9]*\).*/\1/p' | head -1)" + echo "==== diag: assets-svc GET /assets/${first_id} status ====" + curl -s -o /dev/null -w '%{http_code}\n' "http://localhost:5001/assets/${first_id}" + echo "==== diag: assets-svc GET /assets/${first_id}/qr status ====" + curl -s -o /dev/null -w '%{http_code}\n' "http://localhost:5001/assets/${first_id}/qr" + echo "==== diag: web /assets/${first_id} contains 'QR code'? ====" + curl -s "http://localhost:4321/assets/${first_id}" | grep -c -i 'qr code' + echo "==== diag: web /assets/${first_id} error alert (if any) ====" + curl -s "http://localhost:4321/assets/${first_id}" | grep -o 'alert-danger[^<]*' | head -3 echo "==== Playwright error-context ====" - find test-results -name 'error-context.md' 2>/dev/null | while read -r f; do echo "--- $f ---"; sed -n '1,50p' "$f"; done || true - echo "==== dev stack log (tail) ====" - tail -n 100 /tmp/e2e-stack.log || true + find test-results -name 'error-context.md' 2>/dev/null | head -1 | while read -r f; do echo "--- $f ---"; sed -n '1,80p' "$f"; done + echo "==== dev stack log (assets restarts / errors) ====" + grep -iE 'assets|restart|Now listening|Application started|error|exception' /tmp/e2e-stack.log | tail -60 + set -e kill "$STACK_PID" 2>/dev/null || true exit "$e2e_rc" fi From f5bafe58fd8a0dcd924c2ac97514c6e9ce22c240 Mon Sep 17 00:00:00 2001 From: Christopher Harrison Date: Wed, 26 Aug 2026 11:10:30 -0700 Subject: [PATCH 08/10] ci(validate): revert e2e pre-start/health-gate + diagnostics (wrong theory) The QR e2e failure was misdiagnosed as dev-server watcher flakiness. It is actually a deterministic selector bug in the test itself (fixed separately in the module-05 delta). Revert validate-branch.sh's e2e block to the original CI=1 npm run test:e2e; the elaborate stack pre-start/health-gate and the temporary diagnostics were built on the wrong theory and add no value (YAGNI). The /data provisioning and pytest fixes are unaffected. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3750cefd-d322-4f02-9184-c8ac3eba2fff --- course-build/scripts/validate-branch.sh | 68 +------------------------ 1 file changed, 1 insertion(+), 67 deletions(-) diff --git a/course-build/scripts/validate-branch.sh b/course-build/scripts/validate-branch.sh index 9a486a9..fb672d4 100755 --- a/course-build/scripts/validate-branch.sh +++ b/course-build/scripts/validate-branch.sh @@ -96,73 +96,7 @@ echo "==> Playwright e2e (if present in this state)" if [ -f playwright.config.ts ]; then npm ci --no-audit --no-fund npx playwright install --with-deps chromium - - # playwright.config's webServer runs `npm run dev` (the whole stack) but only waits for - # the web app on :4321. The slower backends -- the JVM services especially -- are often - # still booting when specs start, so any spec that transitively needs them flakes as - # "element not found" (e.g. the asset detail page fetches assets-svc AND workforce-svc, - # and hides its content -- including the QR card -- if either call fails). Pre-start the - # full stack ourselves, wait for every service's /health, then run Playwright reusing the - # warm server (reuseExistingServer is true when CI is unset) so specs only run once the - # whole stack is ready. - npm run dev >/tmp/e2e-stack.log 2>&1 & - STACK_PID=$! - - wait_health() { - local name="$1" url="$2" mode="${3:-ok}" i - for i in $(seq 1 120); do - if [ "$mode" = "any" ]; then - # "server responding at all" -- the web root can 5xx while backends warm up. - curl -s -o /dev/null "$url" && { echo " ready: $name"; return 0; } - else - curl -sf "$url" >/dev/null 2>&1 && { echo " ready: $name"; return 0; } - fi - if ! kill -0 "$STACK_PID" 2>/dev/null; then echo "ERROR: dev stack exited before $name was ready" >&2; return 1; fi - sleep 2 - done - echo "ERROR: timed out waiting for $name ($url)" >&2; return 1 - } - - stack_ok=1 - wait_health "web" "http://localhost:4321/" "any" || stack_ok=0 - [ "$stack_ok" = 1 ] && { wait_health "assets-svc" "http://localhost:5001/health" || stack_ok=0; } - [ "$stack_ok" = 1 ] && { wait_health "workforce-svc" "http://localhost:5002/health" || stack_ok=0; } - # reporting/notifications/audit/auth also start via `npm run dev`; the current specs only - # need web + assets-svc + workforce-svc, so we don't gate on the rest (keeps this robust - # for earlier modules whose specs never touch them). - - if [ "$stack_ok" != 1 ]; then - echo "==== dev stack log (tail) ===="; tail -n 150 /tmp/e2e-stack.log || true - kill "$STACK_PID" 2>/dev/null || true - exit 1 - fi - - # Reuse the already-warm stack (CI unset -> reuseExistingServer true); keep CI-style retries. - set +e - env -u CI npx playwright test --retries=2 - e2e_rc=$? - set -e - if [ "$e2e_rc" -ne 0 ]; then - echo "ERROR: Playwright e2e failed (exit $e2e_rc)" >&2 - set +e - first_id="$(curl -s http://localhost:5001/assets | sed -n 's/.*"id":\([0-9]*\).*/\1/p' | head -1)" - echo "==== diag: assets-svc GET /assets/${first_id} status ====" - curl -s -o /dev/null -w '%{http_code}\n' "http://localhost:5001/assets/${first_id}" - echo "==== diag: assets-svc GET /assets/${first_id}/qr status ====" - curl -s -o /dev/null -w '%{http_code}\n' "http://localhost:5001/assets/${first_id}/qr" - echo "==== diag: web /assets/${first_id} contains 'QR code'? ====" - curl -s "http://localhost:4321/assets/${first_id}" | grep -c -i 'qr code' - echo "==== diag: web /assets/${first_id} error alert (if any) ====" - curl -s "http://localhost:4321/assets/${first_id}" | grep -o 'alert-danger[^<]*' | head -3 - echo "==== Playwright error-context ====" - find test-results -name 'error-context.md' 2>/dev/null | head -1 | while read -r f; do echo "--- $f ---"; sed -n '1,80p' "$f"; done - echo "==== dev stack log (assets restarts / errors) ====" - grep -iE 'assets|restart|Now listening|Application started|error|exception' /tmp/e2e-stack.log | tail -60 - set -e - kill "$STACK_PID" 2>/dev/null || true - exit "$e2e_rc" - fi - kill "$STACK_PID" 2>/dev/null || true + CI=1 npm run test:e2e fi echo "==> ${START_BRANCH}: all applicable suites passed" From 4ea5a3702f5dc5b9ff4205f50491d1b02e0bf50b Mon Sep 17 00:00:00 2001 From: Christopher Harrison Date: Wed, 26 Aug 2026 11:17:03 -0700 Subject: [PATCH 09/10] fix(course): correct QR e2e selector so it opens a real asset, not /assets/new The QR Playwright spec (shipped in the module-05 delta, present in start-of-module-06 and -07) used `page.locator('a[href^="/assets/"]').first()`. On the assets list page the "+ New asset" button (href="/assets/new") renders before the asset table rows, so `.first()` opened the create form -- which has no "QR code" heading -- and the test failed deterministically on every run. This is why start-of-module-06/07 were the only build-and-test jobs failing. Scope the selector to the asset table body (`tbody a[href^="/assets/"]`) so it selects the first real asset detail link. Verified deterministically with Playwright against the page's DOM structure (old selector -> /assets/new, new selector -> first asset), and the asset detail page is already confirmed to render the QR card. Because module deltas are cumulative, this changes the reconstructed trees for start-of-module-06 and -07, so the two expectedTreeSha values in manifest.json are updated to match. `build-branches.mjs --check` passes for all modules. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3750cefd-d322-4f02-9184-c8ac3eba2fff --- ...feat-module-05-add-QR-barcode-support-QA-agent-resea.patch | 2 +- course-build/manifest.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/course-build/deltas/module-05/0001-feat-module-05-add-QR-barcode-support-QA-agent-resea.patch b/course-build/deltas/module-05/0001-feat-module-05-add-QR-barcode-support-QA-agent-resea.patch index b262338..f09435b 100644 --- a/course-build/deltas/module-05/0001-feat-module-05-add-QR-barcode-support-QA-agent-resea.patch +++ b/course-build/deltas/module-05/0001-feat-module-05-add-QR-barcode-support-QA-agent-resea.patch @@ -540,7 +540,7 @@ index 0000000..dee69bc + test('asset detail page shows an accessible QR code', async ({ page }) => { + // Land on the assets list and open the first asset's detail page. + await page.goto('/assets'); -+ const firstAsset = page.locator('a[href^="/assets/"]').first(); ++ const firstAsset = page.locator('tbody a[href^="/assets/"]').first(); + await firstAsset.click(); + + // The QR card heading and the accessible QR image are present. diff --git a/course-build/manifest.json b/course-build/manifest.json index efc4eb3..f3ce618 100644 --- a/course-build/manifest.json +++ b/course-build/manifest.json @@ -134,7 +134,7 @@ "patches": [ "0001-feat-module-05-add-QR-barcode-support-QA-agent-resea.patch" ], - "expectedTreeSha": "ba8126ae1414dbcc25cfed544071c848e33fcf1c", + "expectedTreeSha": "848992615da50a401c9db17da55840e5f6d93e67", "expectedAssets": [ ".github/agents/qa.agent.md", "reports/qr-code-research.md", @@ -156,7 +156,7 @@ "patches": [ "0001-feat-module-06-modernize-audit-svc-auth-svc-to-Sprin.patch" ], - "expectedTreeSha": "54a47a032fe12e6f9496040d93c6ed123c28f8b3", + "expectedTreeSha": "cb9a6a19492fa0b7d285685e875f1f03acfc27f7", "expectedAssets": [ ".github/lsp.json", ".github/agents/java-migrator.agent.md", From 6f93ec611013c88241b971caa8e0c62f006e925c Mon Sep 17 00:00:00 2001 From: Christopher Harrison Date: Wed, 26 Aug 2026 11:39:33 -0700 Subject: [PATCH 10/10] ci(validate): verify gitleaks tarball checksum before executing Address CodeQL/Copilot review: the secret-scan job downloaded and executed a release tarball with no integrity verification, an avoidable supply-chain risk for the CI runner. Pin the upstream SHA256 (from gitleaks_8.30.1_checksums.txt) and verify the download with `sha256sum -c` before extracting or running it. The checksum was verified against the real release artifact. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3750cefd-d322-4f02-9184-c8ac3eba2fff --- .github/workflows/validate-branches.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/validate-branches.yml b/.github/workflows/validate-branches.yml index 3aee603..7c606c6 100644 --- a/.github/workflows/validate-branches.yml +++ b/.github/workflows/validate-branches.yml @@ -85,9 +85,15 @@ jobs: - name: gitleaks (delta store + course-build) env: GITLEAKS_VERSION: '8.30.1' + # SHA256 of gitleaks_${VERSION}_linux_x64.tar.gz from the upstream + # gitleaks_${VERSION}_checksums.txt release asset. Bump both together on upgrade. + GITLEAKS_SHA256: '551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb' run: | set -euo pipefail - curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" -o /tmp/gitleaks.tar.gz + tarball="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" + curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/${tarball}" -o /tmp/gitleaks.tar.gz + # Verify integrity before extracting/executing (pinned supply-chain check). + echo "${GITLEAKS_SHA256} /tmp/gitleaks.tar.gz" | sha256sum -c - tar -xzf /tmp/gitleaks.tar.gz -C /tmp gitleaks /tmp/gitleaks version # Scan the full commit history (fetch-depth: 0). Exits non-zero on any finding.