Skip to content

Compare the pinned roster against the published one [#24] - #192

Merged
iderex merged 1 commit into
mainfrom
ci/the-pinned-roster-is-compared-against-what-is-published
Aug 25, 2026
Merged

Compare the pinned roster against the published one [#24]#192
iderex merged 1 commit into
mainfrom
ci/the-pinned-roster-is-compared-against-what-is-published

Conversation

@iderex

@iderex iderex commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

What was wrong

The build reads a pinned roster so that it is reproducible and works with no
network, and nothing said when that copy had fallen behind:

git grep -l -i 'roster' 9a11335 -- '.github/workflows/' ; echo "exit=$?"
exit=1

Run 2026-08-25. 9a11335 is the mainline this branch was cut from. A copy
nobody checks is a copy that silently goes stale, and the pinned copy landed
under #26 with nothing watching it.

What this issue waited on was entry 6 of #7, and it is answered:

gh issue view 7 --repo Flowfin/site --json comments --jq '.comments[].body' \
  | grep -o 'Answer to entry 6, decided 2026-08-24: option (b)[^.]*\.'
Answer to entry 6, decided 2026-08-24: option (b) - the site builds against fixtures now and vendors the published roster file once it lands in the data repository; until then the freshness checks stay disabled by name.

Run 2026-08-25.

What this does

internal/freshness compares the pinned rows against the published ones, names
every row that differs and which field it differs in, and writes nothing: the
difference is the evidence for the change somebody makes, and a run that quietly
resolved it would destroy that.

It fails closed in both directions a comparison can be absent, and the two are
reported apart because only one of them ends on its own. A fetch that could not
be made is UNRESOLVED and reds, because a copy that was not compared is not a
current one. Nothing published at the address is OFF and reds too, rather than
passing, which is the failure the whole comparison exists against.

The published file is read leniently. A published file this tree's parser would
refuse is still a published file the copy has fallen behind, and reporting it as
unreadable would hide the difference behind a second problem. What refuses a
roster the build may not use is the parser, on the build's own path.

It is a package of its own rather than a function in internal/roster, and the
reason is the dependency graph rather than tidiness. The parser is on the path
every build takes, this reaches the network, and a build that linked a client it
never calls is a build whose offline property rests on nobody calling it:

go list -deps ./internal/site | grep -c '^github.com/Flowfin/site/internal/freshness$'
0

Run 2026-08-25 at 5e7bdde.

The address is derived and the file name is not decided anywhere. The
catalogue manifest and the design token file are both in the served directory of
the repository that holds machine-readable data, and
decisions/0001-where-the-plugin-list-comes-from.md puts the roster beside the
manifest:

gh api 'repos/Flowfin/hub/git/trees/HEAD?recursive=1' --jq '[.tree[].path|select(startswith("docs/"))]'
["docs/CNAME","docs/design-system.html","docs/design-tokens.json","docs/index.html","docs/manifest.json"]

Run 2026-08-25. So the constant reads docs/roster.json beside those two. The
name itself is settled nowhere, and that costs nothing it should not: a name
guessed wrong reports OFF and reds, which is a corrected constant rather than a
false pass. It is written down here so the next reader does not take it for a
decided address.

The workflow carries no schedule, and that is the state rather than an oversight

Nothing is published at that address, so every run today reports OFF and reds.
That is the right verdict, and a weekly red for a state nobody can repair from
this repository would be an entry the failure watchdog beside it opens every day
and a red somebody learns to ignore.

Entry 6's answer says the freshness checks stay disabled by name until the file
lands, and the workflow is that sentence carried out: the comparison exists, it
is named, it can be asked for by hand, and the one condition that turns its
timer on is written in the file rather than remembered.

So the clause of the done-when reading the scheduled run reds is met as far as
the run is concerned and not as far as the timer is: the run reds on an
introduced difference and names the differing rows, proved below, and no timer
asks it to yet. That is disclosed here rather than left for somebody to find in
the workflow.

Closes

Closes #24.

What was run

The gate, at the commit being pushed, 5e7bdde:

go run . ci
gate: 7 legs, in order: format, vet, test, build, links, sitemap, invariants
  needs-network was not asked for. Asking costs a request to the public name from whatever machine runs it, and a verdict that moves when somebody else's service does rather than when this tree changes. Ask with: go run ./harness/needs-network
  format: ok, 65 file(s)
  vet: ok
  test: ok, 31 test file(s)
  build: ok, 20 file(s)
  links: every reference that stays inside this site resolves to a file the build wrote
  sitemap: every page the build wrote is listed once, and every entry has a page behind it
  invariants: ok, 39 rule(s) decided, 1 owed and not decided
7 of 7 legs ran. None was skipped.

No test was skipped, and none needed a display or elevation.

The verb against the real address, which is the state today

go run . roster
roster: 12 row(s) pinned in data/roster.json
  OFF, nothing answered at https://raw.githubusercontent.com/Flowfin/hub/HEAD/docs/roster.json, so this run compared nothing
  what ends this state is that file being published; until then the copy in this tree is held to nothing
roster: no file is published at https://raw.githubusercontent.com/Flowfin/hub/HEAD/docs/roster.json, so this run compared nothing and is not a run that found the copy current
exit status 1

Run 2026-08-25 at 5e7bdde. That is the one reading in this body taken over the
network, and it is the state the workflow's missing schedule is about.

The guards bite

The difference dropped on the way to the verdict, which is what a comparison
that reported and passed would look like:

go test ./internal/freshness/
--- FAIL: TestADifferenceRedsTheRunAndNamesTheRow (0.01s)
--- FAIL: TestAnEditedSentenceNamesTheRowAndBothSentences (0.00s)
--- FAIL: TestThePublishedFileIsReadLenientlyAndUnreadableBytesAreUnresolved (0.00s)
FAIL

The OFF state made to pass, which is the failure this check exists to prevent
and the one it would be easiest to switch off the first time the red is
inconvenient:

go test ./internal/freshness/
--- FAIL: TestNothingPublishedIsReportedAsOffRatherThanGreen (0.00s)
FAIL

The fetch failure made to pass, which is the same failure arriving through the
other door:

go test ./internal/freshness/
--- FAIL: TestAFetchThatFailedRedsTheRun (0.00s)
FAIL

All three run 2026-08-25, each edit reverted before the next.

What the suite asserts that the runs above do not

A difference reds the run in each of the five shapes one arrives in, and each
names the row: a sentence edited where it is published, a state that moved, a
repository that moved, a row published this tree does not carry, and a row this
tree carries that is not published. The refusal for an edited sentence carries
both sentences, because a message naming the row and not the field sends the
next reader back to diff two files by hand. Bytes that are not an array of rows
are unresolved rather than a difference. A tree with no pinned copy is refused
rather than compared against nothing. And the order of the rows is deliberately
not a difference: the ordering lives in the data because it is the order the
site presents them in, and two files carrying the same rows in another order are
level.

The means

Go, and the shape this repository already uses for a comparison against a file
somebody else publishes. internal/tokens does the same thing for the design
token values, down to the fetcher being a parameter so the suite reaches no
network and the workflow proving afterwards that the run wrote nothing. Copying
that shape rather than inventing a second one is the point: a reader who has
read one has read both, and the two workflows differ only in what they compare.

Nothing is added to the module graph: net/http and encoding/json.

The alternative refused was putting the comparison in internal/roster, beside
the parser. It reads well and it would put a network client on the path of every
build, which is the property the pinned copy exists to protect.

Who read it

Nobody but me. The ruleset requires no approving review, so this is merged by
whoever opened it, and the evidence above stands in place of a second reader.

The build reads a pinned roster so that it is reproducible and works with no
network, and nothing said when that copy had fallen behind. A copy nobody checks
is a copy that silently goes stale.

`internal/freshness` compares the two, names every row that differs and which
field it differs in, and writes nothing: the difference is the evidence for the
change somebody makes, and a run that quietly resolved it would destroy that. It
fails closed in both directions a comparison can be absent. A fetch that could
not be made is unresolved and reds, because a copy that was not compared is not
a current one. Nothing published at the address is reported as OFF and reds too,
rather than passing, which is the failure the whole comparison exists against.

It is a package of its own rather than a function in `internal/roster`, and the
reason is the dependency graph rather than tidiness: the parser is on the path
every build takes, this reaches the network, and a build that linked a client it
never calls is a build whose offline property rests on nobody calling it.

The workflow carries no schedule. Nothing is published at that address, so every
run today reports OFF, and a weekly red for a state nobody can repair from this
repository would be an entry the failure watchdog opens every day. Entry 6 of #7
says the freshness checks stay disabled by name until the file lands, and the
workflow is that sentence carried out: the comparison exists, it is named, it
can be asked for by hand, and the one condition that turns its timer on is
written in the file rather than remembered.

Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com>
@iderex iderex added the ci label Aug 25, 2026
@iderex iderex self-assigned this Aug 25, 2026
@iderex
iderex merged commit 440c545 into main Aug 25, 2026
17 checks passed
@iderex
iderex deleted the ci/the-pinned-roster-is-compared-against-what-is-published branch August 25, 2026 03:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Vendor the roster and check it stays fresh

1 participant