Skip to content

feat(compat): PR-gated v2 drop-in compat testing (bootstrap)#77

Draft
phramz wants to merge 18 commits into
mainfrom
feat/pr-gated-v2-compat
Draft

feat(compat): PR-gated v2 drop-in compat testing (bootstrap)#77
phramz wants to merge 18 commits into
mainfrom
feat/pr-gated-v2-compat

Conversation

@phramz

@phramz phramz commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Wires the "diffs against goldens" step the 2026-04-23 local-CI-unification spec anticipated but PR 3 didn't fully implement. Every piece around it existed (phpup compat-diff subcommand, probe.sh, docs/compat-matrix.md allowlist, test/compat/testdata/ location allocated); this PR delivers the actual comparison wiring + the refresh/report infrastructure.

  • 8 axis-representative fixtures opt in via a new compat: true YAML flag
  • phpup internal test-cell calls compatdiff.DiffFiles per opted-in fixture when running in the canonical noble/x86_64/8.4 cell; non-canonical cells skip
  • New phpup internal golden-capture + make compat-refresh-goldens for the maintainer side
  • New .github/workflows/compat-golden-refresh.yml — weekly cron + workflow_dispatch + (bootstrap) pull_request trigger on itself; opens a drift PR when v2/PPA bytes shift
  • New compat-report job in ci.yml — posts a sticky PR comment listing deviations with the on-demand refresh escape hatch; updates the sticky to "cleared" when a red PR goes green

Design reference: docs/superpowers/specs/2026-04-24-pr-gated-v2-compat-design.md

Bootstrap done

Goldens captured via the self-triggering pull_request hook and merged into this branch in 935e739 chore: refresh v2 compat goldens. Weekly cron + gh workflow run compat-golden-refresh.yml escape hatch are live for future refreshes.

CI state — canonical cell is RED by design

The noble/x86_64/8.4 pipeline cell fails on real, pre-existing drift between our action and v2 — exactly what the gate is designed to surface. The compat-report sticky comment on this PR enumerates ~12 ini keys per fixture where v2 emits stock upstream defaults and our bundles currently don't: expose_php, log_errors, max_input_time, opcache.{enable,enable_cli,jit,jit_buffer_size,memory_consumption,revalidate_freq,validate_timestamps}, session.{gc_maxlifetime,save_handler}, plus xdebug.{mode,start_with_request} on coverage: xdebug.

None of these are regressions introduced by this PR — they're gaps in our internal/compose layer that predate the gate. The PR demonstrates the machinery works: the gate fires, the artifact is produced, the comment renders, the escape hatch is documented.

Blocking merge until maintainer decides per-deviation:

  • Fix in our action (preferred long-term — probably one compose-layer change to ship stock php.ini-production + jit.ini + xdebug.ini overlays)
  • Per-key allowlist entries in docs/compat-matrix.md with reason: explaining each accepted divergence
  • Some mix

I deliberately did NOT land blanket kind: ignore allowlist entries in this PR — they'd mask the real drift the gate is correctly catching, which is scope creep beyond "build the compat infrastructure."

Test plan

  • All testsuite unit tests green locally (make check clean)
  • shouldRunCompat gate unit-tested for non-canonical skip + canonical pass + canonical fail paths
  • Deviation artifact writer tested for create/append/cross-cell-reject/empty-devs-reject/writer-error-diagnostic
  • Public compatdiff.DiffFiles API tested for no-deviation, reports-deviation, invalid-probe
  • compat-golden-refresh workflow opens a drift PR on this branch (PR chore: refresh v2 compat goldens #78, fast-forward merged)
  • compat-report job posts a correctly-formatted sticky PR comment on canonical-cell failure (see this PR's comments)
  • 19 of 20 pipeline cells green; canonical cell red with documented, expected deviations
  • compat-report "cleared" path verified once drift is resolved (follow-up PR)

Files of note

  • docs/superpowers/specs/2026-04-24-pr-gated-v2-compat-design.md — design
  • internal/testsuite/testcell.go:runFixture — step 6 compat-diff gate
  • internal/testsuite/goldencapture.gophpup internal golden-capture
  • internal/compatdiff/public.goDiffFiles/Deviation exports
  • internal/testsuite/deviations.go — per-cell artifact writer
  • .github/workflows/compat-golden-refresh.yml — refresh workflow
  • .github/workflows/ci.ymlcompat-report job + canonical-cell artifact upload
  • test/compat/testdata/golden/v2/*.json — 8 captured v2 probes

phramz and others added 17 commits April 24, 2026 15:51
Completes the "diffs against goldens" step the 2026-04-23 local-CI
unification spec anticipated but never implemented. Goldens captured
once on ubuntu-24.04 x86_64 / PHP 8.4, committed to
test/compat/testdata/golden/v2/. Per-PR compat-diff runs inside the
existing noble/amd64/8.4 pipeline cell; deviations fail the cell and
a compat-report job posts (or deletes) a sticky PR comment that
surfaces the on-demand golden-refresh escape hatch. A weekly
workflow opens a drift PR when v2 or the Ondrej PPA shifts. No
separate compat workflow, no v2 install at PR time.
Extend testCellOpts with CompatMatrix/GoldenDir/DeviationsPath flags,
wire them into parseTestCellFlags, add the shouldRunCompat gate and
compat-diff step inside runFixture (step 6, after invariants), add the
RW deviations mount in buildCellMounts, and thread all three new flags
from runCell into the inner test-cell command.
GitHub's workflow_dispatch requires the workflow file to already exist
on the default branch before it can be dispatched from any ref. That's
a chicken-and-egg for the initial capture of the feature branch's
goldens: we can't dispatch against feat/pr-gated-v2-compat because the
file isn't on main yet.

Adding a pull_request trigger scoped to this file's own path sidesteps
the restriction (pull_request workflows run from the PR's head, not
from main). It also keeps this safety net permanently: any future edit
to the capture pipeline re-exercises it on its own PR before landing
on main.
git diff --quiet ignores untracked files, so on the initial capture
(where every JSON is new to the tree) it returned clean and the
drift-PR step was silently skipped. Switch to git status --porcelain,
which surfaces adds, modifications, and deletions uniformly.

Observed on the PR #77 bootstrap run (#24897651795): 8 goldens were
staged into the tree but drift=false fired because none had been
tracked previously.
On pull_request triggers github.ref_name is the synthetic
<pr-number>/merge ref — not a real branch — so
peter-evans/create-pull-request fails with "couldn't find remote ref"
when it tries to rebase onto it. github.head_ref holds the PR's
source branch, which is what we actually want.

Observed on PR #77 bootstrap run (#24897788494) after the drift-
detection fix: 8 goldens committed cleanly, then the PR-open step
errored on git fetch 77/merge.
The JSONs under test/compat/testdata/golden/v2/ are captured from
shivammathur/setup-php@<sha> via test/compat/probe.sh. probe.sh emits
compact single-line arrays; prettier wants one element per line. Every
weekly refresh would otherwise force a follow-up reformat commit and
cause lint churn on both the feature PR and every drift PR.

Goldens are frozen captured data, not source — same rationale as
gitignoring generated artifacts.
buildCellMounts was mounting test/compat/testdata/golden/ as /golden,
so captured files at .../golden/v2/bare.json landed at /golden/v2/
bare.json inside the container. But runFixture looks for /golden/
<fixture>.json (no v2/ subpath), causing every opted-in fixture to
fail with "golden missing".

Mount the v2 subdirectory directly so the container path matches what
the consumer expects. Preserves the version namespace on the host
side (room for future golden/v3/ if the upstream pin ever bumps in a
non-backward-compatible way).

Observed on PR #77 ci run #24898111265: all 8 compat fixtures failed
with "golden missing for fixture X at /golden/X.json: run \`gh
workflow run compat-golden-refresh.yml\`" despite goldens being
present on the branch.
jq's JSON-string parser rejects \` (backtick preceded by backslash) as
an invalid escape sequence — only the standard JSON escapes (", \, /,
b, f, n, r, t, u) are valid. Using literal backticks inside concatenated
string segments produces the same markdown output without tripping the
lexer.

Observed on PR #77 ci run #24898366328: compat-report's "Render comment
body" step exited with "jq: 8 compile errors" and the 8 per-fixture
deviations never reached the sticky comment.
@github-actions

github-actions Bot commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

⚠️ Compat-diff detected deviations from shivammathur/setup-php@v2

This PR's phpup install behavior diverges from the pinned v2 baseline in ways the compat-matrix allowlist doesn't cover.

Deviations

Fixture bare (noble-x86_64-8.4)

  • ini.expose_php: ours=``, theirs=1
  • ini.log_errors: ours=``, theirs=1
  • ini.max_input_time: ours=``, theirs=-1
  • ini.opcache.enable: ours=``, theirs=1
  • ini.opcache.enable_cli: ours=``, theirs=0
  • ini.opcache.jit: ours=``, theirs=1235
  • ini.opcache.jit_buffer_size: ours=``, theirs=256M
  • ini.opcache.memory_consumption: ours=``, theirs=128
  • ini.opcache.revalidate_freq: ours=``, theirs=2
  • ini.opcache.validate_timestamps: ours=``, theirs=1
  • ini.session.gc_maxlifetime: ours=``, theirs=1440
  • ini.session.save_handler: ours=``, theirs=files

Fixture exclusion (noble-x86_64-8.4)

  • ini.expose_php: ours=``, theirs=1
  • ini.log_errors: ours=``, theirs=1
  • ini.max_input_time: ours=``, theirs=-1
  • ini.session.gc_maxlifetime: ours=``, theirs=1440
  • ini.session.save_handler: ours=``, theirs=files

Fixture coverage-pcov (noble-x86_64-8.4)

  • ini.expose_php: ours=``, theirs=1
  • ini.log_errors: ours=``, theirs=1
  • ini.max_input_time: ours=``, theirs=-1
  • ini.opcache.enable: ours=``, theirs=1
  • ini.opcache.enable_cli: ours=``, theirs=0
  • ini.opcache.jit: ours=``, theirs=1235
  • ini.opcache.jit_buffer_size: ours=``, theirs=256M
  • ini.opcache.memory_consumption: ours=``, theirs=128
  • ini.opcache.revalidate_freq: ours=``, theirs=2
  • ini.opcache.validate_timestamps: ours=``, theirs=1
  • ini.session.gc_maxlifetime: ours=``, theirs=1440
  • ini.session.save_handler: ours=``, theirs=files

Fixture ini-and-coverage (noble-x86_64-8.4)

  • ini.expose_php: ours=``, theirs=1
  • ini.log_errors: ours=``, theirs=1
  • ini.max_input_time: ours=``, theirs=-1
  • ini.opcache.enable: ours=``, theirs=1
  • ini.opcache.enable_cli: ours=``, theirs=0
  • ini.opcache.jit: ours=``, theirs=1235
  • ini.opcache.jit_buffer_size: ours=``, theirs=256M
  • ini.opcache.memory_consumption: ours=``, theirs=128
  • ini.opcache.revalidate_freq: ours=``, theirs=2
  • ini.opcache.validate_timestamps: ours=``, theirs=1
  • ini.session.gc_maxlifetime: ours=``, theirs=1440
  • ini.session.save_handler: ours=``, theirs=files
  • ini.xdebug.mode: ours=``, theirs=coverage
  • ini.xdebug.start_with_request: ours=``, theirs=default

Fixture ini-file-development (noble-x86_64-8.4)

  • ini.expose_php: ours=``, theirs=1
  • ini.log_errors: ours=``, theirs=1
  • ini.max_input_time: ours=``, theirs=-1
  • ini.opcache.enable: ours=``, theirs=1
  • ini.opcache.enable_cli: ours=``, theirs=0
  • ini.opcache.jit: ours=``, theirs=1235
  • ini.opcache.jit_buffer_size: ours=``, theirs=256M
  • ini.opcache.memory_consumption: ours=``, theirs=128
  • ini.opcache.revalidate_freq: ours=``, theirs=2
  • ini.opcache.validate_timestamps: ours=``, theirs=1
  • ini.session.gc_maxlifetime: ours=``, theirs=1440
  • ini.session.save_handler: ours=``, theirs=files

Fixture multi-ext (noble-x86_64-8.4)

  • ini.expose_php: ours=``, theirs=1
  • ini.log_errors: ours=``, theirs=1
  • ini.max_input_time: ours=``, theirs=-1
  • ini.opcache.enable: ours=``, theirs=1
  • ini.opcache.enable_cli: ours=``, theirs=0
  • ini.opcache.jit: ours=``, theirs=1235
  • ini.opcache.jit_buffer_size: ours=``, theirs=256M
  • ini.opcache.memory_consumption: ours=``, theirs=128
  • ini.opcache.revalidate_freq: ours=``, theirs=2
  • ini.opcache.validate_timestamps: ours=``, theirs=1
  • ini.session.gc_maxlifetime: ours=``, theirs=1440
  • ini.session.save_handler: ours=``, theirs=files

Fixture none-reset (noble-x86_64-8.4)

  • ini.expose_php: ours=``, theirs=1
  • ini.log_errors: ours=``, theirs=1
  • ini.max_input_time: ours=``, theirs=-1
  • ini.session.gc_maxlifetime: ours=``, theirs=1440
  • ini.session.save_handler: ours=``, theirs=files

Fixture single-ext (noble-x86_64-8.4)

  • ini.expose_php: ours=``, theirs=1
  • ini.log_errors: ours=``, theirs=1
  • ini.max_input_time: ours=``, theirs=-1
  • ini.opcache.enable: ours=``, theirs=1
  • ini.opcache.enable_cli: ours=``, theirs=0
  • ini.opcache.jit: ours=``, theirs=1235
  • ini.opcache.jit_buffer_size: ours=``, theirs=256M
  • ini.opcache.memory_consumption: ours=``, theirs=128
  • ini.opcache.revalidate_freq: ours=``, theirs=2
  • ini.opcache.validate_timestamps: ours=``, theirs=1
  • ini.session.gc_maxlifetime: ours=``, theirs=1440
  • ini.session.save_handler: ours=``, theirs=files

What to do

  1. If this is a regression in your change — fix it, or add an entry to the deviations allowlist in docs/compat-matrix.md with a reason: explaining why it's intentional. The YAML block is delimited by <!-- compat-harness:deviations:start --> / <!-- compat-harness:deviations:end -->.

  2. If this looks like upstream v2 drift (e.g., the PPA shipped a new package between golden refreshes) — ask a maintainer to force-refresh the goldens:

    gh workflow run compat-golden-refresh.yml
    

    Merge the resulting drift PR (chore: refresh v2 compat goldens), then rebase this PR onto main.

…rker

From the final holistic review on #77:

1. Add a concurrency group to compat-golden-refresh.yml so a Monday
   cron firing while a PR-trigger run is in flight doesn't race on
   the shared chore/compat-golden-refresh branch. cancel-in-progress
   is false because the workflow opens a PR — cancelling mid-run
   could leave it half-pushed.

2. Update the spec to match what the compat-report job actually does:
   on a red→green transition it REWRITES the sticky comment body to
   the "✅ Compat-diff cleared" marker (preserving audit trail)
   rather than DELETING the comment. The implementation chose the
   replace path deliberately; the spec text is now consistent.
@phramz

phramz commented Apr 24, 2026

Copy link
Copy Markdown
Contributor Author

Final review — applied

Landed the two pre-merge items from the holistic review in af7ecc5:

  • Concurrency group on compat-golden-refresh.yml (prevents Monday-cron + PR-trigger races on the shared chore/compat-golden-refresh branch)
  • Spec prose now describes the "rewrite sticky to ✅ Cleared marker" behaviour the implementation actually does, instead of the "delete comment" wording the spec originally had

Deferred items (tracked as GitHub issues after you review)

The holistic review flagged four follow-ups that aren't merge-blocking. Recommending filing issues for each rather than landing them in this PR:

  • Extract the compat-report bash render block to .github/workflows/scripts/render-compat-report.sh + add a shunit2 test (make test-compat-report feeding a fixture deviations.json through the renderer). Would have caught the jq backtick-escape bug (98e152f) before it hit CI.
  • Add a one-paragraph README under test/compat/testdata/golden/v2/ explaining that bare.json == single-ext.json == multi-ext.json is expected (v2's hosted-runner image pre-installs redis+xdebug so the probe output converges).
  • DRY the 8-fixture matrix in compat-golden-refresh.yml vs test/compat/fixtures.yaml — either a phpup internal list-compat-fixtures --as-json-matrix subcommand or a setup step that generates the matrix at workflow runtime.
  • Unify the two //nolint:gosec // G306 sites (deviations.go:72 and runner.go:458) under a shared writeCIArtifact(path, data) helper.

And the substantive follow-up flagged when the PR went canonical-cell-red:

  • Close the stock-PHP-ini-defaults gap that the gate now surfaces (~12 ini keys per fixture: expose_php/log_errors/max_input_time/opcache./session./xdebug.*). Design-level discussion TBD — probably a compose-layer change in internal/compose to ship php.ini-production + jit.ini + xdebug.ini overlays, which would make most of the canonical-cell deviations disappear without any allowlist changes.

Shall I file these as GitHub issues, or do you want to review and file yourself?

@phramz

phramz commented Apr 25, 2026

Copy link
Copy Markdown
Contributor Author

Five tracking issues filed for the deferred follow-ups:

Infrastructure side of this PR is complete; the canonical-cell red is the gate doing its job. Ready for your call on whether to merge as-is and chase #79 in a follow-up, or hold this PR until #79 lands so the cell is green at merge time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant