Hand an async coder's rejection handler to the .then that follows it #2133
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
| name: CI | |
| on: | |
| push: | |
| branches: | |
| - main | |
| - "v\\d+.\\d+.\\d+-patch" | |
| pull_request: | |
| # For running the drift check on demand between pushes to main. | |
| workflow_dispatch: | |
| jobs: | |
| sury: | |
| name: Sury - Build & Test & Pack | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v2 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24.16.0 | |
| cache: "pnpm" | |
| - run: pnpm install | |
| - run: pnpm lint:deadcode | |
| - run: pnpm build | |
| working-directory: packages/sury | |
| # The *.res.mjs beside each *.res are compiled output, checked in because | |
| # they ship in the published package. Every job that needs them rebuilds | |
| # them first, so a commit that forgot to regenerate passes the whole suite | |
| # while the file in git says something else - the drift is only ever read | |
| # by a consumer installing from npm. The build above just regenerated | |
| # them; anything that moved was stale in the commit. | |
| - name: Compiled ReScript matches its source | |
| run: ../../scripts/assert-no-drift.sh '*.res.mjs' | |
| working-directory: packages/sury | |
| # `coverage` below typechecks only what vitest is pointed at | |
| # (tests/**/*_test.ts). This covers src/ and scripts/ too - the fuzzers | |
| # live in scripts/ and reach the library through its public entry, so | |
| # without this a renamed export leaves them calling a name that no longer | |
| # exists and nothing says so until someone runs them by hand. | |
| - run: pnpm typecheck | |
| working-directory: packages/sury | |
| - run: pnpm coverage | |
| working-directory: packages/sury | |
| # Guards the `escapeFree` flag, which switches jsonString to splicing a | |
| # value between bare quotes with no escaping - so a format flagged wrongly | |
| # emits syntactically broken JSON. That claim is a property of each | |
| # format's regex, and nothing else in the suite checks it. Roughly 90s. | |
| - run: pnpm fuzz:escfree | |
| working-directory: packages/sury | |
| # The only check that the union compiler's dispatch matches a sequential | |
| # try of each member (CLAUDE.md, "Changing the union compiler"). Fixed | |
| # seed so a red run reproduces locally with the same command. Shares | |
| # scripts/knownBugs.ts with fuzz:schema. | |
| - run: pnpm fuzz:union --seed=1 | |
| working-directory: packages/sury | |
| # A spec pins one schema's code and the values it writes down, and says | |
| # nothing about the branch no spec reaches (CLAUDE.md, "Fuzzing a single | |
| # schema"). The default invocation is the gate: forty seeds, both families. | |
| # It fails on a finding scripts/knownBugs.ts does not list, and on an | |
| # entry there it no longer reaches. Fixed range so a red run reproduces | |
| # locally with the same command. ~25s. | |
| - run: pnpm fuzz:schema | |
| working-directory: packages/sury | |
| # A form field's reading depends on its wrapper as much as its type, and | |
| # a spec covers one schema (CLAUDE.md, "Changing the form codec"). This | |
| # crosses the two and checks what a golden cannot: that a field works in | |
| # both directions or neither, that an encode leaves its input alone, that | |
| # a value survives an encode and the decode of it, and that every entry | |
| # list a client could send is either rejected or read as a value the | |
| # schema itself accepts. Exhaustive, so no seed. Seconds. | |
| - run: pnpm fuzz:formdata | |
| working-directory: packages/sury | |
| # Which reading a link takes - pack or unpack - is written down by a slot, | |
| # a field position or a declared payload and mirrored by `reverse`, and a | |
| # spec pins one link (CLAUDE.md, "Changing the content codec"). This | |
| # crosses every source with every target and slot and checks that a link | |
| # compiles both directions or neither, that its reverse reads the same | |
| # values, that a value survives decode, encode, decode, and that a slot | |
| # does what it declares. Exhaustive, so no seed. Seconds. | |
| - run: pnpm fuzz:content | |
| working-directory: packages/sury | |
| # artifact_test.ts checks jsr.json against package.json; only JSR itself | |
| # checks the parts npm has no opinion on (slow types, the exclude list). | |
| - run: npx --yes jsr@0.14.3 publish --dry-run --allow-dirty | |
| working-directory: packages/sury/artifacts | |
| - uses: codecov/codecov-action@v7 | |
| with: | |
| files: packages/sury/coverage/lcov.info | |
| # Doesn't work with rescript v12 | |
| # - run: pnpm lint:stdlib | |
| # working-directory: packages/sury | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: sury | |
| path: packages/sury/artifacts | |
| compliance: | |
| name: JSON Schema Test Suite | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v2 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24.16.0 | |
| cache: "pnpm" | |
| - run: pnpm install | |
| # Fetches the pinned upstream revision (see suite-ref.json) and fails if | |
| # the score drifts from packages/json-schema-test-suite/goldens/ in | |
| # either direction - a fix that improves coverage is expected to land the | |
| # regenerated golden alongside it. | |
| - name: Restore suite checkout | |
| uses: actions/cache@v4 | |
| with: | |
| path: packages/json-schema-test-suite/.suite | |
| key: json-schema-test-suite-${{ hashFiles('packages/json-schema-test-suite/suite-ref.json') }} | |
| - run: pnpm compliance | |
| protobuf-compliance: | |
| name: Protobuf - Conformance & Test Suite | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v2 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24.16.0 | |
| cache: "pnpm" | |
| - run: pnpm install | |
| # Our own corpus: fast, readable, and checked against two reference | |
| # implementations. It says what broke. | |
| - name: Protobuf test suite | |
| shell: bash | |
| run: | | |
| set -o pipefail | |
| pnpm protobuf:compliance | tee compliance.txt | |
| echo "### $(tail -n 1 compliance.txt)" >> "$GITHUB_STEP_SUMMARY" | |
| # Google's own conformance runner, which generates its cases inside the | |
| # binary - the one that says whether we are right. Pinned corpus, cached | |
| # by the commit it is pinned to. | |
| - name: Restore conformance corpus | |
| uses: actions/cache@v4 | |
| with: | |
| path: packages/protobuf-conformance/.upstream | |
| key: protobuf-conformance-${{ hashFiles('packages/protobuf-conformance/conformance-ref.json') }} | |
| - name: Protobuf conformance suite | |
| shell: bash | |
| run: | | |
| set -o pipefail | |
| pnpm protobuf:conformance | tee conformance.txt | |
| echo "### $(tail -n 1 conformance.txt)" >> "$GITHUB_STEP_SUMMARY" | |
| # The four pages under docs/benchmarks/ are generated, and this is what keeps | |
| # them true. Three of their four sections are the same on any machine, so | |
| # every run checks them and fails on a page that no longer matches a fresh | |
| # measurement. The fourth is timings, which move with the runner: those are | |
| # not committed anywhere, but remeasured on main into the charts the pages | |
| # embed. A benchmark page nobody regenerates is worse than no page, and main | |
| # is protected, so the numbers that have to be rewritten by a bot live on a | |
| # branch of their own instead. | |
| benchmarks: | |
| name: Benchmarks | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| # Writing is only reached on the main branch, and only to the `benchmarks` | |
| # branch the charts are published to. | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| # The push below needs the token this leaves behind. | |
| persist-credentials: true | |
| - uses: pnpm/action-setup@v2 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24.16.0 | |
| cache: "pnpm" | |
| - run: pnpm install | |
| - name: Pages match a fresh measurement | |
| run: pnpm benchmarks | |
| # On the ref rather than the event: `benchmarks` is one branch every page | |
| # reads, and a dispatch or a v*-patch push is a non-pull-request event | |
| # too, so either would replace main's timings with a measurement of code | |
| # that is not on main. | |
| # | |
| # Pushing a branch rather than a commit to main is what keeps this off the | |
| # protected-branch rule, and the branch carries nothing this workflow runs | |
| # on, so there is no `[skip ci]` to remember. | |
| - name: Publish the timings | |
| if: github.ref == 'refs/heads/main' | |
| run: pnpm benchmarks --publish | |
| performance: | |
| name: Performance (PR) | |
| if: github.event_name == 'pull_request' | |
| runs-on: ubuntu-24.04 | |
| concurrency: | |
| group: performance-${{ github.event.pull_request.number }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| persist-credentials: false | |
| # The spec performance step below builds its baseline from the PR's | |
| # base branch, which a shallow checkout doesn't have. | |
| fetch-depth: 0 | |
| - uses: pnpm/action-setup@v2 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24.16.0 | |
| cache: "pnpm" | |
| - run: pnpm install | |
| # Every spec measured against the same library built from the PR's base | |
| # branch - schema creation, creation+compilation, and each example. Both | |
| # versions run interleaved in one process, so a shared runner's noise hits | |
| # them equally and cancels in the ratio. Advisory: a regression never | |
| # fails the job (a failed *measurement* still does). | |
| - name: Spec performance | |
| # For pipefail: the default `bash -e` would let `tee` mask a failure. | |
| shell: bash | |
| run: pnpm --filter=sury spec check --perf=only | tee spec-perf.txt | |
| - uses: actions/upload-artifact@v4 | |
| id: spec-perf-artifact | |
| with: | |
| name: spec-perf | |
| path: spec-perf.txt | |
| - name: Format spec performance comment | |
| run: npx tsx packages/spec/perfComment.ts spec-perf.txt spec-perf-comment.md | |
| env: | |
| ARTIFACT_URL: ${{ steps.spec-perf-artifact.outputs.artifact-url }} | |
| # The PR's head, not GITHUB_SHA - on a pull_request event that's the | |
| # ephemeral merge commit, which names nothing a reader can go look at. | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| # A new comment per push, not one edited in place. Editing keeps the PR | |
| # tidy at the cost of the history: the numbers a reviewer read are gone | |
| # the moment the next push lands, and nothing says whether a delta was | |
| # already there two commits ago or arrived with the one under review. | |
| # Each comment names the head it measured, so the thread reads in order. | |
| - name: Post spec performance comment | |
| if: github.event.pull_request.head.repo.full_name == github.repository | |
| uses: actions/github-script@v7 | |
| env: | |
| BODY_FILE: spec-perf-comment.md | |
| with: | |
| script: | | |
| const fs = require("fs"); | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body: fs.readFileSync(process.env.BODY_FILE, "utf8"), | |
| }); | |
| # The `performance` job above only ever compares a PR against its own base, so | |
| # a change small enough to sit under the noise floor leaves no trace anywhere | |
| # - and twenty of them in a row still leave none. This measures main against | |
| # the last release instead, where that same drift has had time to add up to | |
| # something the floor can see. Reported to the run summary, not gated: the | |
| # answer to a real drift is a fix, not a red main. | |
| performance-drift: | |
| name: Performance drift since last release | |
| if: github.event_name != 'pull_request' | |
| runs-on: ubuntu-24.04 | |
| # Reads the repo and writes a file ($GITHUB_STEP_SUMMARY); it never calls | |
| # the API, so it needs strictly less than the `performance` job above. | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| persist-credentials: false | |
| # Needs the tags, and every commit back to the one they point at. | |
| fetch-depth: 0 | |
| - uses: pnpm/action-setup@v2 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24.16.0 | |
| cache: "pnpm" | |
| - run: pnpm install | |
| - name: Spec performance vs last release | |
| # bash for pipefail, as above. | |
| shell: bash | |
| run: | | |
| tag=$(git describe --tags --abbrev=0) | |
| echo "baseline: $tag" | |
| pnpm --filter=sury spec check --perf=only --against "$tag" | tee spec-perf.txt | |
| - name: Write drift to run summary | |
| run: npx tsx packages/spec/perfComment.ts spec-perf.txt "$GITHUB_STEP_SUMMARY" "Performance drift since last release" | |
| # e2e needs this slice on every run, so it stays its own job. | |
| ppx-build-linux: | |
| uses: ./.github/workflows/ppx-build.yml | |
| with: | |
| target: linux | |
| runs-on: ubuntu-24.04 | |
| # Static linking pins the binary off the runner's glibc. The macOS and | |
| # Windows toolchains have no working equivalent. | |
| build-flags: --profile static | |
| # The other four slices only feed the published tarball, and macOS runners | |
| # bill at 10x. A PR that breaks them breaks the linux build too, near enough. | |
| ppx-build-rest: | |
| if: github.event_name != 'pull_request' | |
| strategy: | |
| # A slice failing to build is per-platform news; the others still tell us | |
| # whether the failure is local to one toolchain. | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - target: linux-arm | |
| runs-on: ubuntu-24.04-arm | |
| build-flags: --profile static | |
| - target: macos | |
| # Rosetta translates x64 -> arm64, never the reverse, so the x64 | |
| # slice needs an actual Intel runner rather than macos-latest. | |
| runs-on: macos-15-intel | |
| build-flags: "" | |
| - target: macos-arm | |
| runs-on: macos-latest | |
| build-flags: "" | |
| - target: windows | |
| runs-on: windows-2025 | |
| build-flags: "" | |
| uses: ./.github/workflows/ppx-build.yml | |
| with: | |
| target: ${{ matrix.target }} | |
| runs-on: ${{ matrix.runs-on }} | |
| build-flags: ${{ matrix.build-flags }} | |
| pack-sury-ppx: | |
| name: Pack sury-ppx | |
| # Skipped on PRs because ppx-build-rest is: there is no tarball to pack | |
| # without all five slices. | |
| needs: [ppx-build-linux, ppx-build-rest] | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v2 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24.16.0 | |
| cache: "pnpm" | |
| - run: pnpm install | |
| - name: Download ppx artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: ppx-build-* | |
| path: binaries | |
| - name: Move ppx artifacts | |
| run: | | |
| mv binaries/ppx-build-linux/bin.exe packages/sury-ppx/ppx-linux.exe | |
| mv binaries/ppx-build-linux-arm/bin.exe packages/sury-ppx/ppx-linux-arm.exe | |
| mv binaries/ppx-build-macos/bin.exe packages/sury-ppx/ppx-osx.exe | |
| mv binaries/ppx-build-macos-arm/bin.exe packages/sury-ppx/ppx-osx-arm.exe | |
| mv binaries/ppx-build-windows/bin.exe packages/sury-ppx/ppx-windows.exe | |
| # Artifact download drops the executable bit. install.cjs chmods the | |
| # binary it picks, but installers that skip postinstall (pnpm >=10 by | |
| # default) fall through to the `bin` script, which execs it as-is. | |
| chmod +x packages/sury-ppx/ppx-*.exe | |
| - name: npm pack (sury-ppx) | |
| run: npm pack | |
| working-directory: packages/sury-ppx | |
| # The last gate before an artifact becomes a published tarball: what the | |
| # binaries are, what they need at runtime, and what reached the package. | |
| - name: Verify the ppx binaries and tarball | |
| run: npx tsx packages/sury-ppx/checkBinaries.ts packages/sury-ppx/sury-ppx-*.tgz | |
| - name: "Upload artifact: sury-ppx npm package" | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: sury-ppx | |
| path: packages/sury-ppx/sury-ppx-*.tgz | |
| e2e: | |
| name: E2E Test | |
| needs: [ppx-build-linux] | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v2 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24.16.0 | |
| cache: "pnpm" | |
| - run: pnpm install | |
| - name: Download ppx linux artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: ppx-build-linux | |
| path: binaries/linux | |
| - name: Move ppx artifacts | |
| run: | | |
| mv binaries/linux/bin.exe packages/sury-ppx/ppx-linux.exe | |
| - name: Install ppx | |
| run: pnpm ppx:install | |
| working-directory: packages/e2e | |
| - run: pnpm rescript && pnpm test | |
| working-directory: packages/e2e | |
| # Same guard as the sury job, for the ppx-generated output. This one is | |
| # never published, but it is the only readable record of what the ppx | |
| # emits, and the step above regenerates it before asserting on it - so a | |
| # stale commit here is invisible in every other signal. | |
| - name: Compiled ReScript matches its source | |
| run: ../../scripts/assert-no-drift.sh '*.res.mjs' | |
| working-directory: packages/e2e |