Repository navigation
compat: test against what the polyrepos actually shipped #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Peeja
wants to merge
26
commits into
main
Choose a base branch
from
claude/compat-polyrepo
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
2c1570a
compat: test against what the polyrepos actually shipped
Peeja 5b0ec5b
Round one: the suite could not boot, and nothing compiled it
claude 008335b
Round two: the fix for the fleet list asked the registry for services…
claude a7434b4
Round three: seven required image variables, counted with a glob that…
claude a15f00b
A version image tag is a cut release; the previous commit said otherwise
claude 12eed73
Stop describing the release mechanism; three revisions got it wrong
claude d18b640
Compat runs on the release pull request, and a refresher keeps its an…
claude 2864c29
Round six: a release PR's compat check could be cancelled by an unrel…
claude 1691b59
Round seven: both comments argued for the code from a false premise
claude f98e7e2
Review: cut three comments that carried a service count, add RELEASE.md
claude 26e9429
RELEASE.md: the arming recipe was wrong, and understated what blocks …
claude cb54210
Review round two: switch compat-refresh to dispatch, cut TestPinnedPeer
claude 36a0130
The release roadmap belongs in the plan, not in RELEASE.md
claude 3b9157e
Wrong home: the release flow is this build's work, not a post-consoli…
claude e4c8c16
Review round three: two cycles, not two steps; cut two over-explanations
claude c0489ce
RELEASE.md: just call it a review, and drop a sentence that made no s…
claude 1cdf10c
Fall back to a floating :main, and say so, rather than skipping
claude 44fa2b5
compat.yml's header still described the re-run design cb542109 replaced
claude 1e1fe5e
Review round: the guard's own header claimed a mutation that does not…
claude 93a17f6
Round two: the GOWORK assertion passed on exactly the case it was add…
claude b96d59e
The flake was a BrokenPipeError in the script, not contention in the …
claude f991dcc
Correction: the symptom in that comment was inferred, not observed
claude d2d0652
The uniqueness argument was wrong, and the comment had become a chang…
claude 344c764
"No message" was false, and I had the message in front of me
claude 6c776cd
A release pull request's compat check will be red, and that is the an…
claude 191c860
A red means do not release, whichever way it went red
claude File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| name: compat-refresh | ||
|
|
||
| # Keep every open release pull request's compat answer current. | ||
| # | ||
| # HOW: a scheduled run -- so it runs on `main`, the only ref a schedule can run | ||
| # on -- dispatches compat.yml at each open `release/*` branch. A dispatched run | ||
| # records its check runs against that branch's head commit, which is the pull | ||
| # request's head commit, so the fresh answer replaces the check where the merge | ||
| # decision is made. | ||
| # | ||
| # WHY THIS EXISTS AT ALL. compat.yml asks "does the release proposed here still | ||
| # work against what is already deployed?" -- and the answer changes without the | ||
| # pull request changing, because the deployed side is read live from the | ||
| # registry. An upstream release lands or a `:main` moves, the baselines move | ||
| # with them, and a release pull request that was green yesterday is no longer | ||
| # known to be green. Nothing about the pull request triggers anything, so | ||
| # without this its check is simply old. | ||
| # | ||
| # WHY NOT A SCHEDULE ON compat.yml ITSELF. That version would test `main`, and a | ||
| # result about `main` has no decision attached to it: `main` is not deployed, so | ||
| # a red there leaves you working out which pending release it implicates -- | ||
| # which is the work the release pull request was supposed to have done for you. | ||
| # | ||
| # WHAT IT GIVES UP: nothing runs while no release pull request is open. An | ||
| # upstream incompatibility that lands in a quiet week is found when the next | ||
| # release is proposed rather than when it appeared. Deliberate -- the earlier | ||
| # signal had nobody to act on it -- but if the lead time turns out to matter, | ||
| # the answer is a scheduled run that opens or annotates a release pull request, | ||
| # not one that tests `main`. | ||
|
|
||
| on: | ||
| schedule: | ||
| # 07:00 UTC daily -- after the US evening merges, before the EU morning. | ||
| - cron: '0 7 * * *' | ||
| workflow_dispatch: | ||
|
|
||
| # `actions: write` is what `POST /actions/workflows/{id}/dispatches` requires, | ||
| # and dispatching compat.yml is this workflow's whole job. It is also the scope | ||
| # that would let this token start release.yml: `workflow_dispatch` is one of the | ||
| # two events exempt from the rule that GITHUB_TOKEN-triggered events start no | ||
| # run. GitHub has no narrower scope to ask for; what bounds this is that the | ||
| # step below dispatches one named workflow at branches it derived by reading. | ||
| # https://docs.github.com/en/actions/concepts/security/github_token | ||
| permissions: | ||
| contents: read | ||
| pull-requests: read | ||
| actions: write | ||
|
|
||
| concurrency: | ||
| group: ${{ github.workflow }} | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| refresh: | ||
| name: Dispatch compat on every open release pull request | ||
| runs-on: ubuntu-24.04 | ||
| timeout-minutes: 10 | ||
| steps: | ||
| - name: Dispatch | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| REPO: ${{ github.repository }} | ||
| run: | | ||
| set -euo pipefail | ||
|
|
||
| # DERIVED from the open pull requests, never a list kept here: a | ||
| # release branch this file had not been told about would otherwise go | ||
| # unrefreshed, which is the failure mode that looks exactly like | ||
| # success. | ||
| # | ||
| # ASSIGNED AND TESTED, not piped into mapfile, because "could not ask" | ||
| # and "nothing to refresh" are different answers and the pipe form | ||
| # cannot tell them apart -- `mapfile < <(...)` discards the subshell's | ||
| # status, so a gh that cannot reach the API produces an empty list and | ||
| # a green run that refreshed nothing. | ||
| if ! raw=$(gh pr list --repo "$REPO" --state open --limit 100 \ | ||
| --json number,headRefName,headRefOid \ | ||
| --jq '.[] | select(.headRefName | startswith("release/")) | ||
| | "\(.number)\t\(.headRefName)\t\(.headRefOid)"'); then | ||
| echo "::error::could not list pull requests. Refusing to report that" \ | ||
| "as 'nothing to refresh': every open release pull request would" \ | ||
| "keep a stale green check and nobody would know." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| # `grep .` drops a blank line rather than letting one become an | ||
| # element the tally would then count. | ||
| mapfile -t prs < <(printf '%s' "$raw" | grep . || true) | ||
|
|
||
| if [ "${#prs[@]}" -eq 0 ]; then | ||
| echo "No open release/* pull request. Nothing to refresh." | ||
| exit 0 | ||
| fi | ||
|
|
||
| failed=0 | ||
| for row in "${prs[@]}"; do | ||
| num=${row%%$'\t'*}; rest=${row#*$'\t'} | ||
| branch=${rest%%$'\t'*}; sha=${rest##*$'\t'} | ||
| echo "::group::#$num ($branch @ ${sha:0:8})" | ||
|
|
||
| # Dispatched at the BRANCH, which runs compat.yml as that branch has | ||
| # it and at whatever its tip is now -- so a head that moved between | ||
| # the listing above and here is tested as it currently stands, not | ||
| # as it was. compat.yml is written for this: its job condition is | ||
| # `event_name != 'pull_request' || head_ref starts with release/`, | ||
| # so a dispatch satisfies it on any ref. | ||
| # | ||
| # EACH IS INDEPENDENT: a refusal is counted and the loop carries on, | ||
| # because aborting here would leave every later pull request untried | ||
| # on account of one. | ||
| if ! gh workflow run compat.yml --repo "$REPO" --ref "$branch"; then | ||
| echo "::error::#$num ($branch): could not dispatch compat." \ | ||
| "Its compat check still shows an earlier answer against" \ | ||
| "today's fleet." >&2 | ||
| failed=$((failed + 1)) | ||
| fi | ||
| echo "::endgroup::" | ||
| done | ||
|
|
||
| echo | ||
| echo "${#prs[@]} open release pull request(s): $failed could not be dispatched." | ||
|
|
||
| # Non-zero on a failure, so a stale check has somewhere to be seen. | ||
| [ "$failed" -eq 0 ] |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,200 @@ | ||
| name: compat | ||
|
|
||
| # Does what we are about to ship still work against what is already deployed? | ||
| # | ||
| # `ci`, `itest` and `e2e` all boot this commit against itself, which is the | ||
| # guarantee the monorepo gives for free and is a development-time one. The | ||
| # field is a mix: we run sprue and hilt, third-party operators run piri and | ||
| # ingot and upgrade on their own schedule. This workflow tests that skew. | ||
| # | ||
| # IT RUNS ON A RELEASE PULL REQUEST, AND NOWHERE ELSE ON ITS OWN. A pull request | ||
| # from a `release/<svc>` branch is the one place the question has an answer | ||
| # somebody acts on: red means do not merge this release. The same run against | ||
| # `main` produces a result with no decision attached -- `main` is not deployed, | ||
| # so a red there leaves you working out which pending release it implicates. | ||
| # | ||
| # It takes no service. Unlike release.yml it tests the whole stack against the | ||
| # baseline set, so a release pull request for any service gets the same run; | ||
| # the branch name is only the signal that a release is being proposed. | ||
| # | ||
| # THE ANSWER GOES STALE WITHOUT THE CODE CHANGING, which is why this file has no | ||
| # `schedule:` and `compat-refresh.yml` does. The pinned side is read live from | ||
| # the registry, so the same commit can face a different fleet tomorrow: an | ||
| # upstream release lands or a `:main` moves, the baselines move with it, and a | ||
| # release pull request that was green is no longer known to be. That workflow | ||
| # dispatches THIS one at each open release branch, daily. A dispatched run | ||
| # records its check runs against the branch's head commit, which is the pull | ||
| # request's head commit, so the fresh answer lands where the merge decision is | ||
| # made -- measured on run 35760677003, whose 11 check runs all landed on the | ||
| # dispatched commit. It used to RE-RUN the pull request's own run instead, | ||
| # which put the answer in the same place but expired 30 days after the | ||
| # original; a dispatch has no such ceiling. | ||
| # | ||
| # Also on demand. | ||
| # | ||
| # THE PINNED SIDE IS THE POLYREPOS' IMAGES -- ghcr.io/fil-forge/<svc>. Those | ||
| # are what ships to the network today, so they are what "already deployed" | ||
| # means. This repository publishes no images (images.yml runs with push: false) | ||
| # and carries no service-prefixed tags; neither is a prerequisite here, which | ||
| # is why this can run before the release-ownership question is settled. | ||
| # | ||
| # THE BASELINE SET COMES FROM THE REGISTRY, not from git tags, because the two | ||
| # disagree: the polyrepos tag git `v0.2.4` and publish the image `0.2.4`. | ||
| # Reading the registry asks the question the test actually has -- "what can I | ||
| # pin against?" -- and cannot drift from it. | ||
| # | ||
| # FIVE OF THE EIGHT HAVE CUT NO RELEASE, so their baseline is `:main` resolved | ||
| # to a digest. This job used to skip when any service lacked one, which made | ||
| # the check GREEN IN 0.126s HAVING BOOTED NOTHING -- the job ran because SOME | ||
| # service could be pinned, the test needed EVERY service pinned, and a skipped | ||
| # Go test exits 0. There is now no skip: the script produces a complete | ||
| # baseline set or fails, and compat-baselines.sh's own header says what a | ||
| # floating baseline is worth and what it is not. | ||
|
|
||
| on: | ||
| # No `branches:` filter would help: it matches the BASE branch, and what | ||
| # selects a release here is the HEAD branch. The job condition below does it. | ||
| pull_request: | ||
| # No inputs. There was a `window` input, for a pinned-peer test that has | ||
| # since been cut; nothing read it. | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| # KEYED BY REF, which it was not until this trigger arrived. A group of just | ||
| # `${{ github.workflow }}` was harmless while this ran only on a schedule and on | ||
| # demand; with `pull_request:` it is not. GitHub holds one run per group and | ||
| # cancels any PREVIOUSLY PENDING one when a newer arrives, so with a global key | ||
| # a release pull request's hour-long compat run would make every other pull | ||
| # request's compat run pend behind it -- and the third arrival would cancel the | ||
| # second outright. A release pull request could have its compat check cancelled | ||
| # by an unrelated pull request, which is the one thing this workflow must not do. | ||
| # https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/control-the-concurrency-of-workflows-and-jobs | ||
| # | ||
| # `head_ref` for a pull request, `ref` for a dispatch; a re-run reuses its | ||
| # original event, so it lands back in the same group. | ||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| baselines: | ||
| name: Resolve the baselines | ||
| # A release pull request, or a dispatch. Every other pull request skips | ||
| # this job, and `compat` needs it, so both skip together -- measured on | ||
| # run 35788998286, an ordinary pull request, where both report `skipped`. | ||
| # That matters because the compat job below is capped at an hour. | ||
| # | ||
| # NOT "costs an ordinary pull request nothing", which this said until the | ||
| # concurrency note above was written: the run is still created and still | ||
| # joins the group. What it costs is a queue slot, which is why that group | ||
| # is now keyed by ref. | ||
| if: >- | ||
| github.event_name != 'pull_request' || | ||
| startsWith(github.head_ref, 'release/') | ||
| runs-on: ubuntu-latest | ||
| # TWO JSON OBJECTS, not one entry per service. A hand-written list here was | ||
| # wrong in both available directions at once: it named `base_sprue`, which | ||
| # the test read as COMPAT_BASELINE_UPLOAD and never saw, and it omitted the | ||
| # four services the fleet gained. `outputs:` cannot be generated, so the | ||
| # thing that varies moves inside a value instead. | ||
| outputs: | ||
| baselines: ${{ steps.resolve.outputs.baselines }} | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Resolve baselines from the registry | ||
| id: resolve | ||
| run: .github/scripts/compat-baselines.sh >> "$GITHUB_OUTPUT" | ||
|
|
||
| compat: | ||
| name: Version skew | ||
| needs: baselines | ||
| # NO CONDITION, deliberately. There was one -- `pinnable == 'true'`, true | ||
| # when ANY service could be pinned -- and the test needs EVERY service | ||
| # pinned, so the job ran and the test skipped and the check went green | ||
| # having booted nothing. Two conditions that differ by one quantifier, one | ||
| # in YAML and one in Go, with a green check in the gap. | ||
| # | ||
| # Now the baselines job either produces a complete set or fails, so there | ||
| # is one place that decides and it is the one that reads the registry. | ||
| runs-on: ubuntu-24.04 | ||
| timeout-minutes: 60 | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: actions/setup-go@v5 | ||
| with: | ||
| go-version-file: go.work | ||
| cache-dependency-path: | | ||
| piri/go.sum | ||
| hilt/go.sum | ||
| sprue/go.sum | ||
| ingot/go.sum | ||
| smelt/go.sum | ||
|
|
||
| # Expanded here rather than listed as `env:` entries, for the reason the | ||
| # `outputs:` comment gives. The key is already the package name the test | ||
| # asks for, so this adds no mapping of its own -- it writes exactly what | ||
| # the registry answered. | ||
| - name: Expand the resolved versions | ||
| env: | ||
| BASELINES: ${{ needs.baselines.outputs.baselines }} | ||
| run: | | ||
| set -euo pipefail | ||
| python3 - <<'EOF' >> "$GITHUB_ENV" | ||
| import json, os, re | ||
| for k, v in json.loads(os.environ["BASELINES"]).items(): | ||
| # The key comes from a package name the registry confirmed, so it | ||
| # cannot carry a newline -- but $GITHUB_ENV is parsed line by | ||
| # line, and a check here costs nothing. | ||
| assert re.fullmatch(r"[A-Z0-9_]+", k), k | ||
| assert "\n" not in v, v | ||
| print(f"COMPAT_BASELINE_{k}={v}") | ||
| EOF | ||
| echo "--- resolved ---"; grep '^COMPAT_' "$GITHUB_ENV" | ||
|
|
||
| # ASSERTED, not assumed. The line below says GOWORK stays on, and | ||
| # nothing checked it: with it off, workspace.Detect() fails and the | ||
| # suite used to skip green. It is a hard failure in the test now, but | ||
| # failing here says which knob rather than which symptom. | ||
| # | ||
| # TWO CONDITIONS, because `go env GOWORK` prints the LITERAL STRING | ||
| # `off` when GOWORK=off -- it is empty only when no go.work was found at | ||
| # all. A `[ -n "$w" ]` on its own therefore passes on exactly the case | ||
| # this step is written about, which is rule 5's partial guard that reads | ||
| # as a total one. pkg/workspace tests both (`gowork == "" || gowork == | ||
| # "off"`); this now matches it. | ||
| - name: the workspace is active | ||
| run: | | ||
| set -euo pipefail | ||
| w=$(go env GOWORK) | ||
| if [ -z "$w" ] || [ "$w" = off ]; then | ||
| echo "::error::go env GOWORK is '${w:-<empty>}', so pkg/workspace has no fleet to build from HEAD and this suite cannot compare anything." | ||
| exit 1 | ||
| fi | ||
| echo "GOWORK=$w" | ||
|
|
||
| # GOWORK stays ON: the services NOT pinned to a released image are built | ||
| # from HEAD by pkg/workspace, which is the whole point of the comparison. | ||
| - name: Run compatibility suite | ||
| working-directory: smelt | ||
| run: go test -tags compat -v -timeout 55m -parallel 1 ./tests/compat/... | ||
|
|
||
| - name: Dump container state on failure | ||
| if: failure() | ||
| run: | | ||
| mkdir -p /tmp/container-logs | ||
| for c in $(docker ps -a --format '{{.Names}}'); do | ||
| docker logs "$c" > "/tmp/container-logs/$c.log" 2>&1 || true | ||
| echo "::group::$c"; tail -200 "/tmp/container-logs/$c.log"; echo "::endgroup::" | ||
| done | ||
|
|
||
| - name: Upload container logs | ||
| if: failure() | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: compat-container-logs | ||
| path: /tmp/container-logs/ | ||
| if-no-files-found: ignore | ||
| retention-days: 7 |
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
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.