Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
11 KiB
phase, reviewed, depth, files_reviewed, files_reviewed_list, findings, status, fix_summary
| phase | reviewed | depth | files_reviewed | files_reviewed_list | findings | status | fix_summary | |||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 08-gitea-ci | 2026-06-11T00:00:00Z | deep | 3 |
|
|
fixes_applied |
|
Phase 8: Code Review Report (Re-Review, Post-Split)
Reviewed: 2026-06-11 Depth: deep Files Reviewed: 3 Status: issues_found
Summary
Re-review of Phase 8 (Gitea CI) against the post-split state: publish is now its own
publish.yml triggered on push: branches: [main], and ci.yml holds the three PR-gated
jobs (fast-checks, api, harness).
Resolved since prior review:
- WR-01 (publish ran without test gating / orphaned pending status) — RESOLVED. Publish
is split into
publish.yml, triggered only onpushtomain. PR jobs gate the merge via required status checks + branch protection (direct/force push blocked). Noneeds:is needed because publish never shares a workflow invocation with the test jobs. Architecture confirmed intentional; not re-raised.
Still open (re-located to the split files):
- WR-02 (harness HTML report built then discarded) — still open, now in
ci.yml. - WR-03 (unguarded
${GITHUB_SHA:0:7}→ malformed tag) — still open, now inpublish.yml. - WR-04 (non-atomic two-push,
:latestbefore immutable tag) — still open, now inpublish.yml. - IN-01 (>3072-byte UNIQUE indexes, MariaDB-only) — still open in
0000_baseline.sql.
New findings from the deep pass:
- WR-05 (secret interpolated into
run:script body viaechoinstead ofenv:) — new. - IN-02 (
lintstep is a documented no-op that masks lint failures) — new.
No Critical findings. The publish flow has no test needs: by design (gated by branch
protection), so it is not flagged. The dominant theme is robustness/observability gaps in the
publish + harness steps and MariaDB-specific schema portability.
Narrative Findings (AI reviewer)
Warnings
WR-02: Harness HTML report is generated but never uploaded — FIXED (commit 44a9c30)
Resolution: extended the existing
ChristopherHX/gitea-upload-artifact@v4failure step'spath:to a multi-line list uploading bothapps/pwa/test-results/andapps/pwa/playwright-report/. Reporter unchanged; step stillif: failure().
File: .gitea/workflows/ci.yml:293 (report generation) and :301-307 (upload step)
Issue: The harness runs Playwright with --reporter=list,html. The html reporter writes
its output to apps/pwa/playwright-report/ (Playwright's default outputFolder, not overridden
in playwright.config.ts). The failure-artifact upload step (if: failure()) only uploads
path: apps/pwa/test-results/. The HTML report — the most useful artifact for triaging a remote
CI failure — is built on every run and then discarded when the runner is torn down. test-results/
contains traces/screenshots/videos but not the navigable HTML report.
Fix: Either drop html from the reporter (saves build time if it is genuinely unwanted), or
upload it. Preferred — add the report to the existing upload, or a second upload step:
- name: Upload Playwright HTML report
if: failure()
uses: https://github.com/ChristopherHX/gitea-upload-artifact@v4
with:
name: playwright-report-${{ github.run_id }}
path: apps/pwa/playwright-report/
retention-days: 14
(Or set path: | with both apps/pwa/test-results/ and apps/pwa/playwright-report/ on the
existing step.)
WR-03: Unguarded ${GITHUB_SHA:0:7} can emit a malformed image tag — FIXED (commit 6bcf867)
Resolution: added
set -euo pipefail+ a: "${GITHUB_SHA:?...}"fail-closed guard beforeSHORT_SHA=${GITHUB_SHA:0:7}, so an empty SHA aborts the step instead of producing:v1.1-.
File: .gitea/workflows/publish.yml:44
Issue: SHORT_SHA=${GITHUB_SHA:0:7} has no guard for an empty/unset GITHUB_SHA. If the
runner does not populate GITHUB_SHA (Gitea Actions env parity is not guaranteed across runner
versions; the comment only asserts it via probe P-13, not a runtime check), SHORT_SHA becomes
empty and the immutable tag silently degrades to git.bergerhouse.net/luckberg/familysync-api:v1.1-
— a valid-but-wrong tag that overwrites the milestone pointer and destroys rollback traceability.
Because the default Actions shell runs with pipefail/-e but NOT -u, the empty expansion does
not error; it proceeds.
Fix: Fail closed when the SHA is missing:
set -euo pipefail
if [ -z "${GITHUB_SHA:-}" ]; then
echo "GITHUB_SHA is empty — cannot compute immutable tag" >&2
exit 1
fi
SHORT_SHA=${GITHUB_SHA:0:7}
WR-04: Build-and-push is non-atomic and pushes :latest before the immutable tag — FIXED (commit 4001cd5)
Resolution: reordered the pushes so the immutable
:<milestone>-<sha>tag goes first,:latestsecond; addedset -euo pipefailso the step stops on the first failed push.
File: .gitea/workflows/publish.yml:64-72
Issue: The step runs docker build then two sequential docker push calls. :latest is
pushed first (line 71), then :<milestone>-<sha> (line 72). If the second push fails (registry
hiccup, auth expiry, network), :latest already moved to the new image while the immutable,
rollback-traceable tag was never published — the exact tag operators would reach for to roll back
does not exist, but :latest already advanced. Although the Actions default shell injects -e
(so a failed first command aborts the step), ordering still means a partial-failure window leaves
:latest ahead of the immutable record. Push order should be immutable-first.
Fix: Push the immutable tag first, then :latest, and make the shell strict explicitly:
set -euo pipefail
docker build --target production \
-f apps/api/Dockerfile \
-t ${{ steps.tags.outputs.latest }} \
-t ${{ steps.tags.outputs.sha_tag }} \
.
docker push ${{ steps.tags.outputs.sha_tag }} # immutable first
docker push ${{ steps.tags.outputs.latest }} # move pointer only after immutable lands
WR-05: Registry PAT is interpolated into the run: script body instead of passed via env: — FIXED (commit 58861d9)
Resolution: bound
REGISTRY_PATthrough step-levelenv:and replaced theecho "${{ secrets.REGISTRY_PAT }}" | ...pipe withprintf '%s' "$REGISTRY_PAT" | .... Existing PAT-via-stdin and REGISTRY_PAT-naming comments preserved.
File: .gitea/workflows/publish.yml:55-60
Issue: echo "${{ secrets.REGISTRY_PAT }}" | docker login ... --password-stdin interpolates
the secret into the shell script text at template-expansion time. Two problems:
(1) Robustness — if the PAT ever contains a shell-significant character or a trailing newline,
echo may mangle or split it (echo is not safe for arbitrary strings; printf %s is). A
mangled-but-nonempty password produces a confusing unauthorized rather than a clear failure.
(2) Surface — template-substituting a secret into the script body is the documented anti-pattern
versus binding it through env: (the script then references $REGISTRY_PAT), which keeps the
secret out of the rendered command line / step definition and is the recommended pattern for
Actions-compatible runners. Gitea's log scrubber masks it either way, so this is a robustness/
hardening WARNING, not a leak.
Fix:
- name: Docker login
env:
REGISTRY_PAT: ${{ secrets.REGISTRY_PAT }}
run: |
set -euo pipefail
printf '%s' "$REGISTRY_PAT" | docker login git.bergerhouse.net \
--username luckberg --password-stdin
Info
IN-01: Two UNIQUE constraints exceed the 3072-byte index limit (MariaDB-only) — FIXED, docs-only (commit bf09110)
Resolution: added explanatory SQL comments above
uniq_calendar_user_urlanduniq_push_endpointin the already-applied0000_baseline.sql, documenting the over-length-index dependency on MariaDB 11's long-unique HASH behavior. NO schema, column, or index altered (migration is live on main/production).
File: apps/api/src/db/migrations/0000_baseline.sql:100 (uniq_push_endpoint) and :48
(uniq_calendar_user_url)
Issue: With the default utf8mb4 charset (4 bytes/char):
uniq_push_endpoint UNIQUE(endpoint)whereendpoint varchar(2048)→ 2048 × 4 = 8192 bytes.uniq_calendar_user_url UNIQUE(user_id, url)whereurl varchar(1024)→ 4 + 1024 × 4 = 4100 bytes. Both exceed InnoDB's 3072-byte index-key limit. They succeed on MariaDB 11.x (the CI service imagemariadb:11) because MariaDB silently builds over-length UNIQUE constraints as long-unique HASH indexes. The same DDL fails hard on MySQL 8 and on MariaDB configured withinnodb_large_prefixsemantics disabled or a stricter SQL mode. This is engine-pinned, not a bug on the current target — the project hard-constrains to MariaDB (no PostgreSQL/MySQL) — so it is INFO. Fix: No change required while MariaDB is the only target. If portability is ever wanted, either (a) shorten the columns (e.g.url varchar(768),endpointhashed to achar(64)digest column with the UNIQUE on the digest), or (b) add an explicit comment inschema.tsdocumenting the MariaDB long-unique-HASH dependency so a future MySQL migration is not silently broken. Schema.ts already carries a CR-02 note on theendpoint(2048)width; extend it to record the index-limit caveat.
IN-02: Lint step is a no-op that will mask real lint failures once ESLint is wired — DEFERRED to Phase 13
Not fixed this phase. Wiring a real ESLint gate (
pnpm -r --if-present lint) is Phase 13's deliverable.ci.yml's lint step left untouched intentionally.
File: .gitea/workflows/ci.yml:31-32 (pnpm lint)
Issue: Per the in-file comment, no package defines a lint script, so pnpm lint (root) prints
ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT but exits 0 — the step is green regardless. This is acceptable
for the current phase (lint wiring is explicitly out of scope), but it is a latent trap: when a
lint script is later added to one package, pnpm lint at the root still will not run it unless
the invocation is pnpm -r lint, and even then --if-present semantics differ. The step gives a
false sense that linting is enforced.
Fix: When lint is wired, switch to pnpm -r --if-present lint (runs lint only in packages that
define it, fails the job on real lint errors) and remove the no-op comment. No action this phase;
tracked so the green-but-empty step is not mistaken for working lint enforcement.
Reviewed: 2026-06-11 Reviewer: Claude (gsd-code-reviewer) Depth: deep