ci: a docs-only PR still bought three OS matrices, and four guards a comment could satisfy (#378, #379) - #402
Merged
Conversation
…comment could satisfy (#378, #379) Two things that were both about doing work twice. #378 -- THE DOCS FILTER REACHES THE BUILD AND TEST JOBS. #351/#357 stopped documentation-only pull requests running the acceptance matrix and stopped there. The three-OS test matrix, the go job and the cross-compile kept running, which is most of the twenty minutes #350 was actually complaining about. `go`, `crossplatform`, `ui` and `cross` now read `needs.changes.outputs.code` too, and each grows a step that says out loud why a green check did no work. THE CONDITION IS ON EVERY STEP AND ON NO JOB, including the three jobs that have no matrix and for which a job-level `if:` would be correct today. A skipped ordinary job reports skipped and satisfies branch protection; a skipped MATRIX job never expands, so its per-leg contexts are not skipped but absent, and the pull request can never satisfy the ruleset. That is #351, and #349 sat at fifteen green checks with no way in until #357 undid it. The distance between the safe spelling and the outage is one `strategy:` block added by somebody thinking about Go versions, so the rule is flat and has no exception to re-derive. It is also no longer only a rule. internal/testenv/docsgate_test.go fails if this output is ever read from a job-level `if:`, and fails if a job that asked for the gate leaves any step ungated. It found one pre-existing hole on its first run: acceptance's `Diagnostics (what was it waiting for)` step, missed by #357, harmless only because a documentation-only run has no suite step to fail so `failure()` was never true. Coincidence of shape, not a property. #379 -- readUI AND stripJSComments ARE testenv.ReadUI AND testenv.StripJSComments. A guard that greps a .tsx for a string passes forever if the string survives in a comment -- measured on #367. internal/db had a stripper and internal/oauth did not, because the only way to get one was to paste forty lines into a second package, so all four oauth guards over ui/src were defeatable that way. They now import one implementation. The two Go helpers had drifted from internal/testenv's own readRepoFile in one respect and the stronger version won: an empty UI file is a failure that names the file, not an empty string handed to a caller whose next assertion passes by examining nothing. The TypeScript port in ui/src/lib/tour-drift.test.ts stays -- vitest cannot call Go -- and was measured identical: both run over all 96 .ts/.tsx files under ui/src, output hashed per file, byte-identical on every one. tsUnion's "comments are kept deliberately ... in exchange for nothing" is reversed and says why: it was buying a union whose members could be deleted into a comment, and a body bounded by the first `;` that a semicolon in one of those comments could truncate. Claude-Session: https://claude.ai/code/session_01HeLrWaDmsNeeNSbHQfEofX
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Closes #378. Closes #379.
Two adjacent issues, both about not doing work twice.
#378 — the docs filter reaches the build and test jobs
ci.yml'schangesjob already computed the predicate and only the acceptancematrix read it.
go,crossplatform,uiandcrossnow read it too, andeach gains a step that says out loud why a green check did no work.
The trap, and how I satisfied myself this cannot recreate #351
I did not reason about which jobs "look like" required checks — I asked:
Every job I touched publishes a required context:
gogo build, vet, testcrossplatformtest: ubuntu-latest,test: macos-latest,test: windows-latestuiui typecheck, lint, buildcrosscross-compile all release targetscrossplatformis the one that would have reproduced the outage. Its conditionis on every step; the job carries none.
The other three are gated the same way even though a job-level
if:would becorrect for them today. A skipped ordinary job reports
skippedand branchprotection accepts it. The reason for one flat rule is that the distance between
the safe spelling and the outage is a single
strategy:block, added by someonethinking about Go versions with no reason to be thinking about branch
protection. The price is four runner allocations that do nothing on a docs-only
PR, against the twenty minutes of compute they replace.
Verified mechanically rather than by eye — every step of every gated job carries
the condition, and no job-level
if:reads it:containeris untouched and correctly keeps its job-levelif:— it reads adifferent job's output and its per-leg contexts are deliberately not required.
It is now enforced, not remembered
internal/testenv/docsgate_test.gofails ifneeds.changes.outputs.codeis everread from a job-level
if:, and fails if a job that declaredneeds: changesleaves any step ungated. Red and green fixtures under
testdata/docsgate/— including the green one that looks wrong (container'sshape), so the rule cannot grow an allowlist.
It found a pre-existing hole on its first run: acceptance's
Diagnostics (what was it waiting for)step was missed by #357. Harmless onlybecause a docs-only run has no suite step to fail, so
failure()was never true— a coincidence of this job's shape, not a property. Fixed here.
Residual risk, stated
needs: changesmeans a failingchangesjob skips these jobs, and forcrossplatformthat means no per-leg contexts. This is recoverable by re-runningthe job (unlike #351, which was deterministic and permanent), and it is the same
exposure the
acceptancejob has carried since #357. Not worked around, becausethe alternative — expanding the matrix when the gate job failed — would report
green having examined nothing, which is strictly worse.
Out of scope: semgrep/gitleaks/govulncheck live in
security.yml, which has nochangesjob of its own. #378 scopes itself to "applyingci.yml's existingoutput to more jobs".
#379 —
readUIandstripJSCommentspromoted tointernal/testenvtestenv.ReadUIandtestenv.StripJSCommentsininternal/testenv/uisource.go, alongsideFreeUDPWindow/ReserveUDPanddocumented in the same register — the "four copies of freeUDPPort" note in
ports.gois the same argument.Every copy, and whether they had drifted
internal/db/facebook_ui_drift_test.go—readUI,stripJSComments,quotedBeforeinternal/testenv/pagesdeploy_test.go—readRepoFileui/src/lib/tour-drift.test.ts—stripJSComments,quotedBeforeThey had drifted, in one respect, and I kept the stronger behaviour.
internal/db'sreadUIreturned whatever it read, including nothing.internal/testenv's ownreadRepoFileadditionally fatals on an empty filewith "every assertion that reads it would pass by examining nothing".
ReadUIkeeps the empty-file check, because a guard asserting a substring isabsent —
if strings.Contains(src, "status?.ingest")in the ingest-headerguard is exactly that shape — passes over an empty file while asserting nothing.
Failing names the file; passing vacuously names nothing.
The TypeScript port has not drifted, and that is measured rather than
eyeballed. Both implementations were run over all 96
.ts/.tsxfiles underui/srcand their output hashed per file: byte-identical on every one. It staysa separate copy because vitest cannot call Go, and it names this one as its
origin.
One documented behaviour change, called out
tsUnionininternal/oauth/ui_drift_test.gosaid comments were "keptdeliberately" and that stripping them would "make the file harder to read in
exchange for nothing". That is now reversed, and the comment says why:
strings.Contains(union, "\"tags\""), so a memberdeleted into a comment kept the guard green over a union that could no longer
name the field (demonstrated below);
;after the type name, so a semicoloninside one of those explanatory comments truncates the union early and hides
every member after it.
Nobody's reading experience changes — the notes are still in
types.ts. Theyjust stop counting as declarations.
internal/db/ingest_header_drift_test.go's "why this lives in internal/db"note is updated: it said the move to
internal/enginewas blocked on thispromotion. The blocker is gone; the move itself is left to its own change
because it changes which package a failure points at.
Mutation testing
Each mutation was applied to a committed tree, the named test run with
-vto confirm it actually ran, and the file restored from a backup with
command cp -f. Where a guard was previously comment-defeatable, the samemutation was also run against the pre-change guard to show it used to pass —
otherwise the fix is unevidenced.
| "tags"deleted fromMetaField, text left in a comment inside the unionTestUITypesCanNameEveryMetadataFieldMetadataField "tags" is absent from the UI's MetaField unioncapabilities.tsblock-commented outTestTheUICapabilityMatrixAgreesWithGopreset "kick" is in capabilities.go but absent from the UI matrixtags: tagList,deleted from the push payload, text left in a commentTestTheComposerCanSendFacebookTagsthe composer's push body carries no tags field{withCompliance.length > 0 && (→{false && (, condition left in a JSX commentTestTheComposerSaysWhenAPushCarriesStoredCompliancethe composer never mentions stored complianceuseIngestLive()replaced by the #367 shape, name left in a commentTestTheHeaderAsksTheAppsOneQuestionAboutBeingLiveAppLayout no longer calls useIngestLiveui/src/hooks/useLiveData.tstruncated to zero bytesTestIngestLiveIsDerivedFromArrivingBytes... is empty; every assertion that reads it would pass by examining nothingif:added tocrossplatform— #351 verbatimTestTheDocumentationGateIsNeverReadFromAJobLevelIfci.yml :: job crossplatform :: reads needs.changes.outputs.code from a job-levelif:goTestTheDocumentationGateIsNeverReadFromAJobLevelIfci.yml :: job go :: step #7 (unnamed) :: has no needs.changes.outputs.code in itsif:A/B/C/D "before" columns are the pre-change guard from
HEAD, run against thesame mutated UI file: all four were green over source that could no longer do
what they claim to watch.
After every restore,
git diffoverui/was empty.Honest note on F: on this tree the empty-file check is belt-and-braces — the
guard that would pass vacuously is preceded by a
strings.Indexthat fatalsfirst. It fires and names the file, which is the point; it is not currently
load-bearing.
Gates
go build ./...,go vet ./...,gofmt -l ./cmd ./internal,go test ./...— all clean.No files under
ui/are changed, so the UI toolchain gates do not apply.https://claude.ai/code/session_01HeLrWaDmsNeeNSbHQfEofX