From 10c0f65d09816bb2b3980f392c13681f5847ba20 Mon Sep 17 00:00:00 2001 From: m-wells Date: Wed, 24 Jun 2026 17:18:18 -0400 Subject: [PATCH 1/2] ci: migrate to GitHub-hosted runners + attest release builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move CI and Release off the self-hosted fleet (homeserver-pool ARC pool, the Windows VM, and the M2 Max macOS runner) onto GitHub-hosted runners, and attach build-provenance attestations to every release archive. Runner migration: - ci.yml `check`: homeserver-pool -> ubuntu-latest (container parity kept: still runs inside ghcr.io/twowells/rust-ci:latest). - release.yml `verify` / `publish-crate` / `release`: homeserver-pool -> ubuntu-latest (publish-crate keeps the rust-ci container; crates.io Trusted Publishing OIDC is unchanged on hosted). - release.yml `build` matrix: Linux homeserver-pool -> ubuntu-latest (keeps rust-ci container) Windows [self-hosted,windows] -> windows-latest macOS [self-hosted,macos] -> macos-14 (Apple Silicon) - Windows `Package (zip)` keeps `shell: powershell` — windows-latest ships Windows PowerShell 5.1 with Compress-Archive built in. - Header/inline comments updated to drop the ARC/VM/M2 Max provisioning notes now that the runners are hosted. Build-provenance attestation (release.yml): - Add `attestations: write` to top-level permissions (id-token: write was already present for crates.io OIDC and is reused for provenance signing). - In the `release` job, sign the archives with actions/attest-build-provenance@v4 between download-artifact and action-gh-release. Globs cover the Linux/macOS tar.gz and Windows zip; Lattice ships no .sha256 sidecars, so they match exactly the archives. No source, Makefile, crates.io publishing, version-verify, or artifact- naming changes; no pull_request trigger (tracked separately). Claude-Session: https://claude.ai/code/session_01U5qTPzFxSPun9VccwbLcCy --- .github/workflows/ci.yml | 24 ++++++--------- .github/workflows/release.yml | 56 ++++++++++++++++++++++------------- 2 files changed, 45 insertions(+), 35 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c38ca53..bc77761 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,20 +1,14 @@ # CI — lint, build, and test Lattice on every push. # -# Runs on the self-hosted ARC pool `homeserver-pool` (ephemeral Linux pods, -# minRunners: 0 — a fresh container is spun up per job and torn down after). +# Runs on GitHub-hosted `ubuntu-latest` inside the ghcr.io/twowells/rust-ci +# image, which bakes in rustup + the pinned toolchain + cc/make/git/pkg-config, +# so the job needs nothing provisioned on the runner itself. # # There is intentionally NO `pull_request` trigger. Fork PRs fire only -# pull_request events (their push happened in the fork), so with -# push/workflow_dispatch-only triggers a fork PR can never schedule a job onto -# the self-hosted runners. If you later want CI for outside contributors' PRs, -# add a SEPARATE workflow with `on: pull_request` pinned to -# `runs-on: ubuntu-latest` (GitHub-hosted, free on public repos, fork-safe). -# -# Runner provisioning required on `homeserver-pool` (bake into the ARC image or -# install via ansible): -# - rustup (the pinned 1.95 toolchain + rustfmt/clippy install -# themselves from rust-toolchain.toml on first cargo call) -# - cc/gcc, git, make, pkg-config (make check shells out to all of these) +# pull_request events (their push happened in the fork), so push/ +# workflow_dispatch-only triggers mean a fork PR never schedules a job here. +# If you later want CI for outside contributors' PRs, add a SEPARATE workflow +# with `on: pull_request` (also `ubuntu-latest`, free on public repos). name: CI on: @@ -28,8 +22,8 @@ concurrency: jobs: check: - runs-on: homeserver-pool - # The ARC pod is a bare runner with no Rust. This image bakes in rustup + the + runs-on: ubuntu-latest + # The hosted runner has no pinned Rust. This image bakes in rustup + the # pinned toolchain + cc/make/git, so the job doesn't reprovision every run. container: ghcr.io/twowells/rust-ci:latest # Inside a container job GitHub's default shell is sh (dash); force bash so diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 72b568f..1e1072f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,12 +1,19 @@ # Release — on a vX.Y.Z tag push: # 1. verify the tag matches Cargo.toml's version -# 2. build cross-platform binaries on the self-hosted fleet +# 2. build cross-platform binaries on GitHub-hosted runners # 3. publish-crate to crates.io via Trusted Publishing (OIDC — no stored token) -# 4. release cut a GitHub Release with the binaries attached +# 4. release cut a GitHub Release with the binaries attached + attested # # Triggered ONLY by tag pushes, which require write access — a fork PR can never -# trigger this, so it is safe to run on the persistent Windows/macOS runners -# (the publish credential is never exposed to untrusted code). +# trigger this, so the publish credential is never exposed to untrusted code. +# +# Runners are GitHub-hosted: Linux builds inside ghcr.io/twowells/rust-ci (which +# carries rustup + the pinned toolchain + cc), Windows on windows-latest (MSVC), +# macOS on macos-14 (Apple Silicon, Xcode CLT). rustup self-installs the pinned +# toolchain from rust-toolchain.toml on each, so nothing is pre-provisioned. +# +# Release archives are attested with build provenance (actions/attest-build- +# provenance), giving each binary a verifiable link back to this workflow run. # # This is the first workflow to create GitHub Releases; v0.1.0 was tagged before # it existed (a pre-history crates.io name-grab), so v0.2.0 is the first Release. @@ -14,9 +21,6 @@ # One-time setup before the first successful run: # - crates.io → crate Settings → Trusted Publishing → add repo TwoWells/Lattice # and workflow file `release.yml`. -# - macOS → register the M2 Max runner (labels: self-hosted,macos,arm64). -# - Runner provisioning: rustup everywhere; MSVC Build Tools on Windows; -# Xcode Command Line Tools on macOS; cc/gcc on Linux. name: Release on: @@ -25,12 +29,13 @@ on: permissions: contents: write # create the GitHub Release - id-token: write # OIDC token for crates.io Trusted Publishing + id-token: write # OIDC token for crates.io Trusted Publishing + provenance signing + attestations: write # write build-provenance attestations for the release archives jobs: # Guard: a pushed tag whose version disagrees with Cargo.toml is a mistake. verify: - runs-on: homeserver-pool + runs-on: ubuntu-latest outputs: version: ${{ steps.v.outputs.version }} steps: @@ -56,19 +61,19 @@ jobs: matrix: include: - target: x86_64-unknown-linux-gnu - runner: homeserver-pool + runner: ubuntu-latest container: ghcr.io/twowells/rust-ci:latest archive: tar - target: x86_64-pc-windows-msvc - runner: [self-hosted, windows, x64] + runner: windows-latest archive: zip - target: aarch64-apple-darwin - runner: [self-hosted, macos, arm64] + runner: macos-14 archive: tar runs-on: ${{ matrix.runner }} - # Linux builds inside the rust-ci image (bare ARC pod). Windows/macOS define - # no matrix container, so this evaluates empty and they build natively on the - # VM / M2 Max, which carry their own MSVC / Xcode toolchains. + # Linux builds inside the rust-ci image on the hosted runner. Windows/macOS + # define no matrix container, so this evaluates empty and they build natively + # on windows-latest / macos-14, which carry their own MSVC / Xcode toolchains. container: ${{ matrix.container }} steps: - uses: actions/checkout@v6 @@ -91,8 +96,9 @@ jobs: - name: Package (zip) if: matrix.archive == 'zip' - # The self-hosted Windows VM has Windows PowerShell 5.1 (powershell), not - # PowerShell 7 (pwsh). Compress-Archive exists in 5.1, so this is unchanged. + # windows-latest ships both Windows PowerShell 5.1 (powershell) and + # PowerShell 7 (pwsh); we pin `powershell` (5.1) deliberately for a stable, + # always-present shell. Compress-Archive is built in there, so this works. shell: powershell run: | New-Item -ItemType Directory -Force dist | Out-Null @@ -109,8 +115,8 @@ jobs: # a re-run (or a re-pushed tag) is a harmless no-op rather than a hard failure. publish-crate: needs: verify - runs-on: homeserver-pool - # Bare ARC pod → run in the toolchain image (cargo publish + the curl guard). + runs-on: ubuntu-latest + # Hosted runner → run in the toolchain image (cargo publish + the curl guard). container: ghcr.io/twowells/rust-ci:latest # Container default shell is sh (dash); force bash for the guard/publish steps. defaults: @@ -157,13 +163,23 @@ jobs: release: needs: [verify, build, publish-crate] - runs-on: homeserver-pool + runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v8 with: path: dist merge-multiple: true + # Sign a build-provenance attestation for every release archive, linking + # each binary back to this workflow run. The globs cover the Linux/macOS + # tar.gz and the Windows zip; Lattice ships no .sha256 sidecars, so these + # match exactly the archives and nothing else. + - uses: actions/attest-build-provenance@v4 + with: + subject-path: | + dist/lattice-*.tar.gz + dist/lattice-*.zip + - uses: softprops/action-gh-release@v3 with: tag_name: ${{ github.ref_name }} From 770ecff8e7d46b47a7236daf06483b1611b7dc88 Mon Sep 17 00:00:00 2001 From: m-wells Date: Wed, 24 Jun 2026 20:01:04 -0400 Subject: [PATCH 2/2] test: loosen anti-quadratic CPU-time bounds for slower hosted runners The four inline.rs and ~8 block.rs anti-quadratic guards assert per-thread CPU time under a fixed bound. Those bounds (inline 5s, block 10s) were tuned on the fast self-hosted runner; GitHub-hosted runners are slower per core, so the dollar-run guard hit 5.8s and tripped the 5s inline bound on hosted CI. Raise INLINE_SLOW_BOUND 5s->20s and SLOW_BOUND 10s->30s. Genuine quadratic blowup is orders of magnitude worse (minutes), so the guards still catch it; the looser bound just survives slower CI hardware without flaking. Claude-Session: https://claude.ai/code/session_01U5qTPzFxSPun9VccwbLcCy --- src/block.rs | 5 ++++- src/inline.rs | 9 ++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/block.rs b/src/block.rs index 43b76e2..73f0bee 100644 --- a/src/block.rs +++ b/src/block.rs @@ -8074,7 +8074,10 @@ mod tests { /// cross-process contention (e.g. a concurrent full-suite run saturating /// every core) cannot inflate it — unlike a wall-clock bound. The parse is /// single-threaded, so the calling thread's CPU time captures all the work. - const SLOW_BOUND: std::time::Duration = std::time::Duration::from_secs(10); + /// Set generously to tolerate slower CI hardware (GitHub-hosted runners are + /// markedly slower per core than the self-hosted box this was tuned on); + /// genuine quadratic blowup is orders of magnitude worse and still trips it. + const SLOW_BOUND: std::time::Duration = std::time::Duration::from_secs(30); #[test] fn deeply_nested_block_quotes_hit_limit() { diff --git a/src/inline.rs b/src/inline.rs index a80b543..4b75c7f 100644 --- a/src/inline.rs +++ b/src/inline.rs @@ -1884,9 +1884,12 @@ mod tests { /// descheduled, so cross-process contention (e.g. a concurrent full-suite /// run saturating every core) cannot inflate it — unlike a wall-clock /// bound. A linear parse burns ~constant CPU regardless of load; a - /// quadratic regression burns orders of magnitude more, so this bound - /// stays meaningful without flaking. - const INLINE_SLOW_BOUND: std::time::Duration = std::time::Duration::from_secs(5); + /// quadratic regression burns orders of magnitude more (seconds → minutes). + /// The bound is set generously so slower CI hardware still clears it + /// (GitHub-hosted runners are markedly slower per core than the self-hosted + /// box this was originally tuned on — the dollar-run case measured ~5.8s + /// there), while genuine quadratic blowup never could. + const INLINE_SLOW_BOUND: std::time::Duration = std::time::Duration::from_secs(20); #[test] fn unclosed_bracket_run_is_not_quadratic() {