From dd7506cead8e98fe1da192dd2826669ba8add07b Mon Sep 17 00:00:00 2001 From: callumalpass Date: Wed, 16 Sep 2026 23:10:27 +1000 Subject: [PATCH 1/4] perf(ci): select native checks and deduplicate Rust qualification --- .github/workflows/desktop-release.yml | 35 +++++++++++++- .github/workflows/server-ci.yml | 52 +++++++++++++++----- scripts/ci/desktop-test-plan.mjs | 25 ++++++++++ scripts/lib/ci-work-selection.test.mjs | 67 ++++++++++++++++++++++++++ 4 files changed, 166 insertions(+), 13 deletions(-) create mode 100644 scripts/ci/desktop-test-plan.mjs create mode 100644 scripts/lib/ci-work-selection.test.mjs diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index 244a4cf66..fb8833aa7 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -10,6 +10,15 @@ on: pull_request: paths: - ".github/workflows/desktop-release.yml" + - "scripts/ci/desktop-test-plan.mjs" + - "scripts/lib/ci-work-selection.test.mjs" + - "Cargo.toml" + - "Cargo.lock" + - "crates/**" + - ".cargo/**" + - "rust-toolchain*" + - "deploy/docker/mdbase-rs-revision" + - "package.json" - "apps/editor/**" - "apps/desktop/**" - "packages/**" @@ -31,6 +40,26 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: + select-tests: + if: github.event_name == 'pull_request' + runs-on: ubuntu-24.04 + outputs: + headless: ${{ steps.plan.outputs.headless }} + windows: ${{ steps.plan.outputs.windows }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24 + - id: plan + env: + BASE: ${{ github.event.pull_request.base.sha }} + HEAD: ${{ github.event.pull_request.head.sha }} + run: node scripts/ci/desktop-test-plan.mjs "$BASE" "$HEAD" + build: if: github.event_name == 'workflow_dispatch' && startsWith(github.ref, 'refs/tags/v') name: ${{ matrix.label }} @@ -569,7 +598,8 @@ jobs: --input - headless-package-smoke: - if: github.event_name != 'push' + needs: select-tests + if: always() && (github.event_name == 'workflow_dispatch' || (needs.select-tests.result == 'success' && needs.select-tests.outputs.headless == 'true')) name: ${{ matrix.label }} permissions: contents: read @@ -695,7 +725,8 @@ jobs: run: pnpm --filter mdbase-editor test windows-package-smoke: - if: github.event_name != 'push' + needs: select-tests + if: always() && (github.event_name == 'workflow_dispatch' || (needs.select-tests.result == 'success' && needs.select-tests.outputs.windows == 'true')) name: Windows Store package smoke test runs-on: windows-2025 permissions: diff --git a/.github/workflows/server-ci.yml b/.github/workflows/server-ci.yml index 9eaaa1aaa..8a3fb76e7 100644 --- a/.github/workflows/server-ci.yml +++ b/.github/workflows/server-ci.yml @@ -29,7 +29,7 @@ jobs: GH_TOKEN: ${{ github.token }} run: | if [[ "$GITHUB_EVENT_NAME" == pull_request ]]; then - if jq -e 'any(.pull_request.labels[]?; .name == "ci:full")' \ + if jq -e 'any(.pull_request.labels[]?; .name == "ci:full" or .name == "ci:benchmark-binaries")' \ "$GITHUB_EVENT_PATH" >/dev/null then echo 'run_full=true' >>"$GITHUB_OUTPUT" @@ -425,12 +425,49 @@ jobs: key: hosted-provider-${{ steps.mdbase_revision.outputs.revision }} cache-on-failure: true - run: cargo fmt --all --check + - run: scripts/check-cargo-features - run: cargo clippy --locked --workspace --all-targets -- -D warnings - run: cargo test --locked --workspace env: MDBASE_CONNECT_ENV: test MDBASE_CONNECT_SECRET_BACKEND: insecure-test-file - run: cargo build --locked --workspace + - name: Archive runtime binaries for transfer measurement + if: github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'ci:benchmark-binaries') + run: | + tar -cf "$RUNNER_TEMP/binaries.tar" -C target/debug mdbase mdbase-connect-hosted-provider + du -b "$RUNNER_TEMP/binaries.tar" >>"$GITHUB_STEP_SUMMARY" + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'ci:benchmark-binaries') + with: + name: benchmark-linux-binaries + path: ${{ runner.temp }}/binaries.tar + compression-level: 1 + retention-days: 1 + if-no-files-found: error + + # Measurement only: does not supply qualification inputs or replace builds. + # Compare producer wait + transfer with the parallel shard build timings. + binary-transfer-benchmark: + name: Binary transfer benchmark (${{ matrix.shard }}) + needs: hosted-provider-rust + if: github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'ci:benchmark-binaries') + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + shard: [local-relay, sync, provider, files, files-adversarial, desktop] + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: benchmark-linux-binaries + - name: Extract and exercise transferred binaries + run: | + tar -xf binaries.tar + ./mdbase --help >/dev/null + ./mdbase-connect-hosted-provider --help >/dev/null + du -b binaries.tar >>"$GITHUB_STEP_SUMMARY" hosted-provider-system: name: System suite (${{ matrix.shard }}) @@ -504,16 +541,9 @@ jobs: cache-on-failure: true - run: pnpm install --frozen-lockfile - run: pnpm build - - run: cargo fmt --all --check - - run: pnpm check:cargo-features - - run: cargo clippy --locked --workspace --all-targets -- -D warnings - # Compile and test every Rust crate against the exact SDK revision used - # by the production provider image. This keeps path-only developer - # dependencies from making a clean checkout unreproducible. - - run: cargo test --locked --workspace - env: - MDBASE_CONNECT_ENV: test - MDBASE_CONNECT_SECRET_BACKEND: insecure-test-file + # The hosted-provider-rust job checks this exact lockfile/SDK/toolchain + # configuration once; Qualification requires it as well as every shard. + # Keep builds local until measured artifact fan-out beats cached builds. - run: cargo build --locked --workspace - name: Restore Playwright cache if: matrix.browser diff --git a/scripts/ci/desktop-test-plan.mjs b/scripts/ci/desktop-test-plan.mjs new file mode 100644 index 000000000..f25adb656 --- /dev/null +++ b/scripts/ci/desktop-test-plan.mjs @@ -0,0 +1,25 @@ +import { execFileSync } from "node:child_process"; +import { appendFileSync } from "node:fs"; +import { pathToFileURL } from "node:url"; + +// Deliberately use narrow exclusions, not an incomplete dependency allowlist. +// Unknown/shared inputs and mixed changes retain all native checks. +export function desktopTestPlan(paths) { + return { + headless: paths.length === 0 || paths.some((path) => !path.startsWith("apps/editor/")), + windows: paths.length === 0 || paths.some((path) => !/^apps\/editor\/.*\.css$/.test(path)) + }; +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + const [base, head] = process.argv.slice(2); + if (!base || !head) throw new Error("Expected base and head revisions"); + // --no-renames includes both sides of moves so native inputs cannot disappear. + const paths = execFileSync("git", ["diff", "--no-renames", "--name-only", "-z", `${base}...${head}`], { + encoding: "utf8" + }).split("\0").filter(Boolean); + const plan = desktopTestPlan(paths); + console.log(JSON.stringify({ paths, ...plan }, null, 2)); + appendFileSync(process.env.GITHUB_OUTPUT, Object.entries(plan) + .map(([key, value]) => `${key}=${value}\n`).join("")); +} diff --git a/scripts/lib/ci-work-selection.test.mjs b/scripts/lib/ci-work-selection.test.mjs new file mode 100644 index 000000000..3fadb8501 --- /dev/null +++ b/scripts/lib/ci-work-selection.test.mjs @@ -0,0 +1,67 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import test from "node:test"; +import { desktopTestPlan } from "../ci/desktop-test-plan.mjs"; + +const workflow = (name) => readFileSync(new URL(`../../.github/workflows/${name}.yml`, import.meta.url), "utf8"); + +test("editor styling skips native CLI and Windows packaging, not release regressions", () => { + assert.deepEqual(desktopTestPlan(["apps/editor/src/styles.css"]), { headless: false, windows: false }); + assert.deepEqual(desktopTestPlan(["apps/editor/src/main.ts"]), { headless: false, windows: true }); + assert.deepEqual(desktopTestPlan(["apps/editor/package.json"]), { headless: false, windows: true }); + assert.match(workflow("desktop-release"), /cross-platform-release-tests:\n if: github.event_name != 'push'/); +}); + +test("shared, native, unknown, empty and mixed inputs retain native checks", () => { + for (const path of ["Cargo.lock", "Cargo.toml", "crates/cli/src/main.rs", "deploy/docker/mdbase-rs-revision", "apps/desktop/src/main.ts", "packages/sdk/src/index.ts", "pnpm-lock.yaml", ".github/workflows/desktop-release.yml", "scripts/package-headless-cli.mjs", "unknown/input"]) { + for (const paths of [[path], ["apps/editor/src/styles.css", path]]) { + assert.deepEqual(desktopTestPlan(paths), { headless: true, windows: true }); + } + } + assert.deepEqual(desktopTestPlan([]), { headless: true, windows: true }); +}); + +test("native selection retains explicit dispatch checks and includes Rust input triggers", () => { + const desktop = workflow("desktop-release"); + for (const job of ["headless-package-smoke", "windows-package-smoke"]) { + const block = desktop.split(` ${job}:\n`)[1].split(/\n [a-z][\w-]+:\n/)[0]; + assert.match(block, /needs: select-tests/); + assert.match(block, /always\(\) && \(github.event_name == 'workflow_dispatch' \|\|/); + assert.match(block, /needs.select-tests.result == 'success'/); + } + for (const path of ["Cargo.toml", "Cargo.lock", "crates/**", "deploy/docker/mdbase-rs-revision", "scripts/ci/desktop-test-plan.mjs"]) { + assert.ok(desktop.includes(`- "${path}"`)); + } +}); + +test("same-configuration Rust checks run once and remain required for qualification", () => { + const server = workflow("server-ci"); + const rust = server.split(" hosted-provider-rust:\n")[1].split(" binary-transfer-benchmark:\n")[0]; + const shards = server.split(" hosted-provider-system:\n")[1].split(" qualification:\n")[0]; + for (const command of ["cargo fmt --all --check", "scripts/check-cargo-features", "cargo clippy --locked --workspace --all-targets -- -D warnings", "cargo test --locked --workspace"]) { + assert.ok(rust.includes(command), command); + assert.ok(!shards.includes(command), command); + } + for (const block of [rust, shards]) { + assert.ok(block.includes("cp deploy/docker/Cargo.lock.hosted-provider Cargo.lock")); + assert.ok(block.includes("cargo build --locked --workspace")); + assert.ok(block.includes("1.94.0")); + } + const gate = server.split(" qualification:\n")[1]; + assert.match(gate, /- hosted-provider-rust/); + assert.match(gate, /- hosted-provider-system/); + assert.match(gate, /"\$PROVIDER_RUST"/); + assert.match(gate, /"\$PROVIDER_SYSTEM"/); + assert.doesNotMatch(shards, /needs: hosted-provider-rust/); +}); + +test("binary transfer remains opt-in and outside the qualification gate", () => { + const server = workflow("server-ci"); + assert.match(server, /\.name == "ci:full" or \.name == "ci:benchmark-binaries"/); + assert.match(server, /compression-level: 1/); + const benchmark = server.split(" binary-transfer-benchmark:\n")[1].split(" hosted-provider-system:\n")[0]; + assert.match(benchmark, /needs: hosted-provider-rust/); + assert.match(benchmark, /github.event_name == 'pull_request' && contains/); + assert.match(benchmark, /\.\/mdbase --help/); + assert.doesNotMatch(server.split(" qualification:\n")[1], /binary-transfer-benchmark/); +}); From 823f835e6c1b011e59a5ec2b9ac1d3a552d66342 Mon Sep 17 00:00:00 2001 From: callumalpass Date: Wed, 16 Sep 2026 23:18:12 +1000 Subject: [PATCH 2/4] fix(ci): verify pinned upgrade fixtures independently of newer releases --- .github/previous-release.env | 4 +-- docs/release-contract.md | 26 ++++++++++------ scripts/lib/upgrade-release-identity.test.mjs | 31 +++++++++---------- scripts/lib/upgrade-test-contract.test.mjs | 9 +++--- test/upgrade/SERVER-RETAINED-V2.md | 6 ++-- test/upgrade/lib.sh | 27 ++++++++-------- test/upgrade/server-from-previous | 2 +- 7 files changed, 56 insertions(+), 49 deletions(-) diff --git a/.github/previous-release.env b/.github/previous-release.env index 05aea88ae..7346e13cd 100644 --- a/.github/previous-release.env +++ b/.github/previous-release.env @@ -1,5 +1,5 @@ -# Exact server image from the release immediately preceding this candidate. -# Update this file as part of each release-preparation change. +# Versioned published upgrade fixture; not the mutable newest-release pointer. +# Advance deliberately during release preparation; publication alone cannot invalidate CI. MDBASE_CONNECT_PREVIOUS_RELEASE=v0.1.0-beta.101 MDBASE_CONNECT_PREVIOUS_RELEASE_COMMIT=4d0bdf9ec566a15c117249ca8d5b24107f1faecb MDBASE_CONNECT_PREVIOUS_SERVER_IMAGE=ghcr.io/mdbase-dev/mdbase-connect-server@sha256:836962e2322701360aadaf1b1873447803cf4603e003803e9880ff9ca85b1e9c diff --git a/docs/release-contract.md b/docs/release-contract.md index cf1dc8cc2..2b11e69e9 100644 --- a/docs/release-contract.md +++ b/docs/release-contract.md @@ -52,13 +52,21 @@ components. ## Upgrade predecessor and historical regressions -`.github/previous-release.env` identifies the immediate published predecessor. -Update its annotated tag, full commit and immutable server/provider digests as -part of release preparation; ordinary upgrade qualification still requires it to -be the unique newest non-draft GitHub release. Following beta100 publication, -qualification uses beta100 (`6bd9420690295d3c5f65e68edcbf8e3e55612614`) and its -exact signed server/provider images. Advancing this fixture preserves the -unique-newest-release check; the retained beta95 and beta94 lanes do not move. This fixture refresh does not itself publish or deploy a release. +`.github/previous-release.env` identifies a versioned published upgrade fixture, +not a live newest-release pointer. Advance its annotated tag, full commit and +immutable server/provider digests deliberately during release preparation. +Ordinary CI queries the exact pinned release, requires non-draft published +metadata, verifies its annotated origin tag resolves to the pinned commit, and +checks pulled image source/revision labels. A newer publication cannot invalidate +an unchanged candidate or retroactively change the tested upgrade pair. Missing, +draft, malformed or mismatched fixture identities still fail; no latest-release +fallback or verification bypass exists. + +These checks prove compatibility with the recorded fixture, not the currently +deployed predecessor. The actual deployment pair remains independently bound +and qualified by private release preparation and staging below. The retained +beta95 and beta94 lanes do not move. A fixture refresh itself neither publishes +nor deploys a release. Historical regressions and candidate qualification are separate. The beta94 schema-38→41 prelude and beta95 retained-v2 provider rollback scenarios use the @@ -71,8 +79,8 @@ metadata and annotated origin tags are checked. These tests do not qualify the current candidate or authorize restoring beta95/beta99 in production. The same required provider CI job separately runs `--current-upgrade` against -`.github/previous-release.env`, retaining the unique-newest-published-release -check. It exercises predecessor-issued v1/v2 authority and exact receipts through +`.github/previous-release.env`, retaining exact published-fixture identity +verification. It exercises predecessor-issued v1/v2 authority and exact receipts through candidate migration and restart, verifies an unchanged historical ledger prefix and canonical authority, installs a cancellation fence, and actually attempts predecessor startup on the resulting database. Same-schema predecessors must diff --git a/scripts/lib/upgrade-release-identity.test.mjs b/scripts/lib/upgrade-release-identity.test.mjs index eeb94d83d..35640d88f 100644 --- a/scripts/lib/upgrade-release-identity.test.mjs +++ b/scripts/lib/upgrade-release-identity.test.mjs @@ -61,8 +61,8 @@ ${image ? 'upgrade_verify_previous_image "$MDBASE_CONNECT_PREVIOUS_SERVER_IMAGE" TAG: tag, NETWORK: network, EXPECTED_URL: historical ? "https://api.github.com/repos/mdbase-dev/mdbase-connect/releases/tags/v0.1.0-beta.95" - : "https://api.github.com/repos/mdbase-dev/mdbase-connect/releases?per_page=100", - METADATA: typeof metadata === "string" ? metadata : JSON.stringify(metadata ?? (historical ? release95 : [release101, release95])), + : "https://api.github.com/repos/mdbase-dev/mdbase-connect/releases/tags/v0.1.0-beta.101", + METADATA: typeof metadata === "string" ? metadata : JSON.stringify(metadata ?? (historical ? release95 : release101)), REFS: refs ?? annotated(tag, commit), INSPECTION: JSON.stringify(inspection ?? [{ Config: { Labels: { "org.opencontainers.image.source": "https://github.com/mdbase-dev/mdbase-connect", @@ -78,25 +78,24 @@ ${image ? 'upgrade_verify_previous_image "$MDBASE_CONNECT_PREVIOUS_SERVER_IMAGE" return { code, stderr, calls: await readFile(calls, "utf8").catch(() => "") }; } -test("ordinary predecessor verifies newest beta101 and its annotated origin tag", async (context) => { +test("ordinary fixture verifies exact beta101 without consulting the mutable release inventory", async (context) => { + // The mock rejects every endpoint except the pinned tag. Publication of + // beta102 (or a hundred later releases) cannot affect this request/result. const result = await verify(context); assert.equal(result.code, 0, result.stderr); - assert.match(result.calls, /releases\?per_page=100\ngit\n$/); -}); - -test("ordinary qualification cannot substitute the historical beta95 pin for newest beta101", async (context) => { - const result = await verify(context, { override: 'source "$ROOT/.github/retained-v2-predecessor.env"' }); - assert.equal(result.code, 1); - assert.match(result.stderr, /not the unique newest non-draft/); - assert.doesNotMatch(result.calls, /^git$/m); + assert.match(result.calls, /releases\/tags\/v0\.1\.0-beta\.101\ngit\n$/); + assert.doesNotMatch(result.calls, /releases\?/); }); for (const [name, metadata] of [ - ["older pin", [release95, release101]], - ["duplicate release", [release101, release101]], - ["draft only", [{ ...release101, draft: true }]], - ["missing release", []], - ["object instead of inventory", release101], + ["wrong tag", release95], + ["newer release substituted for pin", { ...release101, tag_name: "v0.1.0-beta.102" }], + ["release inventory instead of exact metadata", [release101, release95]], + ["draft", { ...release101, draft: true }], + ["missing release", {}], + ["unpublished", { ...release101, published_at: null }], + ["empty publication date", { ...release101, published_at: "" }], + ["invalid identity", { ...release101, id: "101" }], ["malformed JSON", "not-json"] ]) test(`ordinary predecessor rejects ${name}`, async (context) => { const result = await verify(context, { metadata }); diff --git a/scripts/lib/upgrade-test-contract.test.mjs b/scripts/lib/upgrade-test-contract.test.mjs index 3cbb02432..ed497502f 100644 --- a/scripts/lib/upgrade-test-contract.test.mjs +++ b/scripts/lib/upgrade-test-contract.test.mjs @@ -20,13 +20,13 @@ test("upgrade workflows delegate scenario behavior to versioned test programs", assert.doesNotMatch(workflow, /INSERT INTO hosted_provider_/); }); -test("upgrade pins the exact immediate predecessor", async () => { +test("upgrade pins an exact versioned published fixture", async () => { const fixture = await readFile( resolve(repoRoot, ".github/previous-release.env"), "utf8" ); - assert.equal(fixture, `# Exact server image from the release immediately preceding this candidate. -# Update this file as part of each release-preparation change. + assert.equal(fixture, `# Versioned published upgrade fixture; not the mutable newest-release pointer. +# Advance deliberately during release preparation; publication alone cannot invalidate CI. MDBASE_CONNECT_PREVIOUS_RELEASE=v0.1.0-beta.101 MDBASE_CONNECT_PREVIOUS_RELEASE_COMMIT=4d0bdf9ec566a15c117249ca8d5b24107f1faecb MDBASE_CONNECT_PREVIOUS_SERVER_IMAGE=ghcr.io/mdbase-dev/mdbase-connect-server@sha256:836962e2322701360aadaf1b1873447803cf4603e003803e9880ff9ca85b1e9c @@ -37,7 +37,8 @@ MDBASE_CONNECT_PREVIOUS_PROVIDER_IMAGE=ghcr.io/mdbase-dev/mdbase-connect-hosted- test("both upgrade programs execute release and pulled-image verification", async () => { const helpers = await readFile(resolve(repoRoot, "test/upgrade/lib.sh"), "utf8"); assert.match(helpers, /upgrade_verify_previous_release\(\)/); - assert.match(helpers, /api\.github\.com\/repos\/mdbase-dev\/mdbase-connect\/releases\?per_page=100/); + assert.match(helpers, /api\.github\.com\/repos\/mdbase-dev\/mdbase-connect\/releases\/tags\/\$release/); + assert.doesNotMatch(helpers, /releases\?per_page|unique newest/); assert.match(helpers, /git -C "\$repo_root" ls-remote --exit-code --tags origin/); assert.match(helpers, /"refs\/tags\/\$release\^\{\}"/); assert.match(helpers, /upgrade_verify_previous_image\(\)/); diff --git a/test/upgrade/SERVER-RETAINED-V2.md b/test/upgrade/SERVER-RETAINED-V2.md index 999089d76..8d5c6daf3 100644 --- a/test/upgrade/SERVER-RETAINED-V2.md +++ b/test/upgrade/SERVER-RETAINED-V2.md @@ -24,9 +24,9 @@ Inputs require beta.95, commit `408c67bc10f128e0833f0da62cb3efb9d94657d7`, and the digest-only server image in `.github/retained-v2-predecessor.env`. The full release/commit/image pair is fixed, its published non-draft release is verified by tag, and the annotated origin tag must peel to that exact commit. -The default lane separately uses `.github/previous-release.env` and requires the -newest published release (currently beta96); historical beta95 is not substituted -for that immediate predecessor. The helper verifies +The default lane separately verifies the exact published fixture recorded in +`.github/previous-release.env`; newer publications do not change its inputs. +Historical beta95 is not substituted for that recorded fixture. The helper verifies OCI source/revision labels, source policy phases, and the actual images' generated fresh semantic ceilings `[1]` and `[1,2]`. Containers run by resolved image ID throughout. This is local binary evidence, not signature/attestation, diff --git a/test/upgrade/lib.sh b/test/upgrade/lib.sh index e909b0df7..cff3385a1 100755 --- a/test/upgrade/lib.sh +++ b/test/upgrade/lib.sh @@ -20,7 +20,7 @@ upgrade_verify_previous_release() { local repo_root=$1 local release=${MDBASE_CONNECT_PREVIOUS_RELEASE:-} local commit=${MDBASE_CONNECT_PREVIOUS_RELEASE_COMMIT:-} - local releases_json + local metadata local -a curl_headers=() if [[ ! $release =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$ ]]; then @@ -34,23 +34,22 @@ upgrade_verify_previous_release() { if [[ -n ${GITHUB_TOKEN:-} ]]; then curl_headers=(--header "authorization: Bearer $GITHUB_TOKEN") fi - if ! releases_json=$(curl --fail-with-body --silent --show-error \ + # CI verifies its versioned fixture, not the mutable release-list head. + # Publishing another release must not invalidate an unchanged source tree. + if ! metadata=$(curl --fail-with-body --silent --show-error \ --connect-timeout 5 --max-time 20 --retry 2 --retry-all-errors \ --header 'accept: application/vnd.github+json' \ "${curl_headers[@]}" \ - 'https://api.github.com/repos/mdbase-dev/mdbase-connect/releases?per_page=100'); then - printf 'Could not query bounded GitHub release metadata.\n' >&2 + "https://api.github.com/repos/mdbase-dev/mdbase-connect/releases/tags/$release"); then + printf 'Could not query pinned GitHub release metadata for %s.\n' "$release" >&2 return 1 fi if ! jq -e --arg expected "$release" ' - type == "array" and - length <= 100 and - all(.[]; type == "object" and (.draft | type) == "boolean" and (.tag_name | type) == "string") and - ([.[] | select(.draft == false)] | length) > 0 and - ([.[] | select(.draft == false)][0].tag_name == $expected) and - ([.[] | select(.draft == false and .tag_name == $expected)] | length) == 1 - ' <<<"$releases_json" >/dev/null; then - printf '%s is not the unique newest non-draft mdbase-connect GitHub release.\n' "$release" >&2 + type == "object" and .tag_name == $expected and .draft == false and + (.id | type) == "number" and + (.published_at | type) == "string" and (.published_at | length) > 0 + ' <<<"$metadata" >/dev/null; then + printf 'Pinned release %s is missing, draft or malformed.\n' "$release" >&2 return 1 fi @@ -58,8 +57,8 @@ upgrade_verify_previous_release() { } # This historical lane is fixed to the original beta95 bytes, not a caller's -# arbitrary older release. Newest-release verification above remains mandatory -# for the ordinary immediate-predecessor lane. +# arbitrary older release. The ordinary lane independently verifies its own +# checked-in published fixture, annotated tag, commit and image identities. upgrade_verify_retained_v2_release() { local repo_root=$1 metadata local -a curl_headers=() diff --git a/test/upgrade/server-from-previous b/test/upgrade/server-from-previous index ce6f89e58..4e212993a 100755 --- a/test/upgrade/server-from-previous +++ b/test/upgrade/server-from-previous @@ -16,7 +16,7 @@ case ${1:-} in *) printf 'Usage: server-from-previous [--retained-v2-pending]\n' >&2; exit 2 ;; esac -# The immediate predecessor is independent of the immutable beta95 regression. +# The versioned upgrade fixture is independent of the immutable beta95 regression. # shellcheck disable=SC1091 source "$repo_root/.github/previous-release.env" From 795818d9ec159ab2d1f2c052494c87b7db0248a9 Mon Sep 17 00:00:00 2001 From: callumalpass Date: Wed, 16 Sep 2026 23:19:56 +1000 Subject: [PATCH 3/4] docs(ci): record native selection and measured binary fan-out costs --- docs/ci-qualification.md | 61 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/docs/ci-qualification.md b/docs/ci-qualification.md index 1ef2b6ee6..17b221766 100644 --- a/docs/ci-qualification.md +++ b/docs/ci-qualification.md @@ -26,6 +26,67 @@ JSON manifest containing the commit, Git tree, package and Cargo lock hashes, the mdbase engine revision, and the Server CI workflow hash. A reused main run also records the exact upstream merge-queue run. +## Desktop PR selection + +`Desktop Release` keeps cross-platform editor/release regression tests on every +matching PR. Editor-only changes skip the standalone headless CLI build matrix; +CSS-only editor changes also skip Windows Store packaging. Mixed changes, +shared inputs, unknown paths, and empty diffs retain native checks. The selector +uses the complete base-to-head merge-base diff, with renames expanded to both +paths. Explicit release dispatches still run all native checks. + +## Rust checks and binary transfer measurement + +The `hosted-provider-rust` job owns formatting, resolved feature validation, +workspace Clippy, and workspace unit tests for the hosted-provider lockfile, +pinned engine revision, and Linux toolchain configuration. System shards build +that same configuration locally and run their own suites, without repeating +those workspace checks. `Qualification` still requires both the Rust job and +all shards. macOS/Windows coverage remains separate. + +To measure binary fan-out, label a PR `ci:benchmark-binaries`. This opts into +full Server CI and adds a measurement-only upload of the CLI/provider runtime +binaries, followed by six matching Linux runner downloads. The tar preserves +executable permissions; each consumer extracts it and exercises both binaries. +Artifacts expire after one day. This never replaces a shard build, contributes +no qualification evidence, and is not a release artifact. Remove the label +when the experiment is complete. + +Compare the archive/upload/download/extraction steps and producer readiness +against the same run's per-shard `cargo build` steps using `CI timings`. Include +producer dependency wait in wall-clock estimates and all six downloads in +runner-minute estimates. Do not compare transfer time with the old combined +Clippy/unit-test/build cost: those duplicate checks have already been removed. +The `files-adversarial` suite invokes Cargo tests directly, so shipping only +runtime binaries cannot remove its compilation requirements. A dedicated +build-only producer would need its own end-to-end measurement before adoption. + +### Initial measurement (2026-09-16) + +[Run 35100242511](https://github.com/mdbase-dev/mdbase-connect/actions/runs/35100242511), +source `dd7506cead8e98fe1da192dd2826669ba8add07b`, completed all six transfer +probes successfully: + +- Compressed runtime artifact: 247,093,162 bytes (about 236 MiB). +- Archive step: under the API's one-second timing resolution; upload including + compression: 12 seconds. +- Download steps: 7, 7, 12, 9, 13, 19 seconds; extraction and both `--help` + probes: 0–1 seconds each. +- Independent shard builds: desktop 179s, files 156s, local-relay 189s, + provider 175s, files-adversarial 181s, sync 132s (1,012 runner-seconds total). +- The existing Rust qualification producer completed after 454 seconds; + consumers started two seconds later. Its unit tests and Clippy are on that + dependency path. Waiting for that job would delay runtime tests versus the + independent builds, despite cheap transfers. + +Decision: retain independent builds for now. Transfer is inexpensive enough to +justify a future **build-first** producer experiment, not a dependency on the +existing qualification job. This is one run, not a cold/warm-cache study or an +end-to-end artifact-fed system-suite qualification. Runtime probes do not prove +all suite dependencies portable, and files-adversarial still compiles tests. +The run's two upgrade jobs failed on the then-existing mutable-newest-release +policy; that failure does not invalidate the completed transfer measurements. + ## Windows daemon task qualification Full Server CI calls `windows-daemon-lifecycle.yml` and requires its result in From 2673d7cc1c02af020f2d049cc6f9ef3e7f94e7d3 Mon Sep 17 00:00:00 2001 From: callumalpass Date: Wed, 16 Sep 2026 23:22:06 +1000 Subject: [PATCH 4/4] fix(ci): keep selected native jobs cancellable on superseded PRs --- .github/workflows/desktop-release.yml | 4 ++-- scripts/lib/ci-work-selection.test.mjs | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index fb8833aa7..5eefcb0a4 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -599,7 +599,7 @@ jobs: headless-package-smoke: needs: select-tests - if: always() && (github.event_name == 'workflow_dispatch' || (needs.select-tests.result == 'success' && needs.select-tests.outputs.headless == 'true')) + if: ${{ !cancelled() && (github.event_name == 'workflow_dispatch' || (needs.select-tests.result == 'success' && needs.select-tests.outputs.headless == 'true')) }} name: ${{ matrix.label }} permissions: contents: read @@ -726,7 +726,7 @@ jobs: windows-package-smoke: needs: select-tests - if: always() && (github.event_name == 'workflow_dispatch' || (needs.select-tests.result == 'success' && needs.select-tests.outputs.windows == 'true')) + if: ${{ !cancelled() && (github.event_name == 'workflow_dispatch' || (needs.select-tests.result == 'success' && needs.select-tests.outputs.windows == 'true')) }} name: Windows Store package smoke test runs-on: windows-2025 permissions: diff --git a/scripts/lib/ci-work-selection.test.mjs b/scripts/lib/ci-work-selection.test.mjs index 3fadb8501..819999150 100644 --- a/scripts/lib/ci-work-selection.test.mjs +++ b/scripts/lib/ci-work-selection.test.mjs @@ -26,7 +26,8 @@ test("native selection retains explicit dispatch checks and includes Rust input for (const job of ["headless-package-smoke", "windows-package-smoke"]) { const block = desktop.split(` ${job}:\n`)[1].split(/\n [a-z][\w-]+:\n/)[0]; assert.match(block, /needs: select-tests/); - assert.match(block, /always\(\) && \(github.event_name == 'workflow_dispatch' \|\|/); + assert.match(block, /!cancelled\(\) && \(github.event_name == 'workflow_dispatch' \|\|/); + assert.doesNotMatch(block, /always\(\)/); assert.match(block, /needs.select-tests.result == 'success'/); } for (const path of ["Cargo.toml", "Cargo.lock", "crates/**", "deploy/docker/mdbase-rs-revision", "scripts/ci/desktop-test-plan.mjs"]) {