From 38d5bf12af1bc9be46fbc0eca53e29dbee1dd6de Mon Sep 17 00:00:00 2001 From: "blacksmith-sh[bot]" <157653362+blacksmith-sh[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2026 01:00:22 +0000 Subject: [PATCH 1/7] Migrate workflows to Blacksmith --- .github/workflows/CreateReleaseDraft.yml | 2 +- .github/workflows/LinuxFlatpak.yml | 2 +- .github/workflows/LinuxInstall.yml | 2 +- .github/workflows/SupplyChain.yml | 2 +- .github/workflows/WindowsInstall.yml | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/documentation.yml | 2 +- .github/workflows/fuzz.yml | 2 +- .github/workflows/release-gate.yml | 12 ++++++------ .github/workflows/reusable-linux.yml | 2 +- .github/workflows/reusable-windows.yml | 2 +- .github/workflows/sync-milestones.yml | 2 +- 12 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/CreateReleaseDraft.yml b/.github/workflows/CreateReleaseDraft.yml index 4603e5db0..8f383c973 100644 --- a/.github/workflows/CreateReleaseDraft.yml +++ b/.github/workflows/CreateReleaseDraft.yml @@ -20,7 +20,7 @@ permissions: jobs: build: - runs-on: ubuntu-latest + runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - name: Checkout repository diff --git a/.github/workflows/LinuxFlatpak.yml b/.github/workflows/LinuxFlatpak.yml index 43b1fcf6a..9d2451e66 100644 --- a/.github/workflows/LinuxFlatpak.yml +++ b/.github/workflows/LinuxFlatpak.yml @@ -10,7 +10,7 @@ permissions: jobs: build_ubuntu: - runs-on: ubuntu-24.04 + runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - name: Checkout repository diff --git a/.github/workflows/LinuxInstall.yml b/.github/workflows/LinuxInstall.yml index 24030d380..00291221c 100644 --- a/.github/workflows/LinuxInstall.yml +++ b/.github/workflows/LinuxInstall.yml @@ -22,7 +22,7 @@ permissions: jobs: build_ubuntu: # The AppImage runtime is pinned for Ubuntu 22.04 compatibility. - runs-on: ubuntu-22.04 + runs-on: blacksmith-4vcpu-ubuntu-2204 env: VCPKG_OVERLAY_PORTS: ${{ github.workspace }}/loop/vcpkg/overlays/linux:${{ github.workspace }}/loop/vcpkg/overlays/general VCPKG_INSTALLED_DIR: ${{ github.workspace }}/vcpkg_installed diff --git a/.github/workflows/SupplyChain.yml b/.github/workflows/SupplyChain.yml index f783db961..9c21d5b3d 100644 --- a/.github/workflows/SupplyChain.yml +++ b/.github/workflows/SupplyChain.yml @@ -10,7 +10,7 @@ permissions: jobs: policy: - runs-on: ubuntu-24.04 + runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - name: Checkout repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 diff --git a/.github/workflows/WindowsInstall.yml b/.github/workflows/WindowsInstall.yml index b8c5c2e66..dd905b803 100644 --- a/.github/workflows/WindowsInstall.yml +++ b/.github/workflows/WindowsInstall.yml @@ -21,7 +21,7 @@ permissions: jobs: build_windows: - runs-on: windows-2022 + runs-on: blacksmith-4vcpu-windows-2025 env: VCPKG_INSTALLED_DIR: ${{ github.workspace }}\vcpkg_installed VCPKG_ROOT: ${{ github.workspace }}\vcpkg diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7939bb0c9..e7f290bcd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ permissions: jobs: source_integrity: - runs-on: ubuntu-24.04 + runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - name: Checkout repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 42ab62328..1bb3171a1 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -18,7 +18,7 @@ permissions: jobs: architecture-docs: - runs-on: ubuntu-24.04 + runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - name: Checkout repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 374397ce8..3ee0acfbf 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -18,7 +18,7 @@ on: jobs: fuzz: - runs-on: ubuntu-24.04 + runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 90 env: VCPKG_OVERLAY_PORTS: ${{ github.workspace }}/vcpkg/overlays/linux:${{ github.workspace }}/vcpkg/overlays/general diff --git a/.github/workflows/release-gate.yml b/.github/workflows/release-gate.yml index 99d1e9c83..76d891ca5 100644 --- a/.github/workflows/release-gate.yml +++ b/.github/workflows/release-gate.yml @@ -16,7 +16,7 @@ permissions: jobs: source_integrity: - runs-on: ubuntu-24.04 + runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - name: Checkout repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -34,7 +34,7 @@ jobs: run: python3 scripts/ci/check_generated_dependency_paths.py agent_contract: - runs-on: ubuntu-24.04 + runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - name: Checkout repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -64,7 +64,7 @@ jobs: uses: ./.github/workflows/reusable-windows.yml documentation: - runs-on: ubuntu-24.04 + runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - name: Checkout repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -75,7 +75,7 @@ jobs: uses: ./.github/workflows/fuzz.yml package_contract: - runs-on: ubuntu-24.04 + runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - name: Checkout repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -89,7 +89,7 @@ jobs: run: python3 scripts/ci/validate_product_surface.py supply_chain: - runs-on: ubuntu-24.04 + runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - name: Checkout repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -113,7 +113,7 @@ jobs: - fuzz_regression - package_contract - supply_chain - runs-on: ubuntu-24.04 + runs-on: blacksmith-4vcpu-ubuntu-2404 permissions: contents: read steps: diff --git a/.github/workflows/reusable-linux.yml b/.github/workflows/reusable-linux.yml index e5d1022de..9aa5112fb 100644 --- a/.github/workflows/reusable-linux.yml +++ b/.github/workflows/reusable-linux.yml @@ -21,7 +21,7 @@ permissions: jobs: build: - runs-on: ubuntu-24.04 + runs-on: blacksmith-4vcpu-ubuntu-2404 env: VCPKG_OVERLAY_PORTS: ${{ github.workspace }}/loop/vcpkg/overlays/linux:${{ github.workspace }}/loop/vcpkg/overlays/general VCPKG_INSTALLED_DIR: ${{ github.workspace }}/vcpkg_installed diff --git a/.github/workflows/reusable-windows.yml b/.github/workflows/reusable-windows.yml index d45552f4b..dccd4956c 100644 --- a/.github/workflows/reusable-windows.yml +++ b/.github/workflows/reusable-windows.yml @@ -11,7 +11,7 @@ permissions: jobs: build: - runs-on: windows-2022 + runs-on: blacksmith-4vcpu-windows-2025 env: VCPKG_OVERLAY_PORTS: ${{ github.workspace }}\loop\vcpkg\overlays\general VCPKG_INSTALLED_DIR: ${{ github.workspace }}\vcpkg_installed diff --git a/.github/workflows/sync-milestones.yml b/.github/workflows/sync-milestones.yml index 5775d00e8..42379f70e 100644 --- a/.github/workflows/sync-milestones.yml +++ b/.github/workflows/sync-milestones.yml @@ -9,7 +9,7 @@ permissions: jobs: sync: - runs-on: ubuntu-latest + runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - name: Checkout repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 From 591a624ecd29a33e1a02012f472869cf563424c2 Mon Sep 17 00:00:00 2001 From: mberrys Date: Mon, 7 Sep 2026 01:02:05 +0000 Subject: [PATCH 2/7] Fix Blacksmith migration contracts Co-authored-by: Codesmith --- changes/blacksmith-migration-bcd7474.md | 6 ++++++ scripts/ci/test_workflow_contracts.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 changes/blacksmith-migration-bcd7474.md diff --git a/changes/blacksmith-migration-bcd7474.md b/changes/blacksmith-migration-bcd7474.md new file mode 100644 index 000000000..199c21a7b --- /dev/null +++ b/changes/blacksmith-migration-bcd7474.md @@ -0,0 +1,6 @@ +# Blacksmith CI migration + +Category: internal +Audience: developers +Breaking-Change: no +Summary: Run selected GitHub Actions workflows on Blacksmith runners while preserving the packaging platform contract. diff --git a/scripts/ci/test_workflow_contracts.py b/scripts/ci/test_workflow_contracts.py index 4b6dbbf66..aa53c8cf7 100644 --- a/scripts/ci/test_workflow_contracts.py +++ b/scripts/ci/test_workflow_contracts.py @@ -81,7 +81,7 @@ def test_package_workflows_require_and_record_exact_source_sha(self): # Deliberate: the AppImage glibc floor is whatever this runner ships. Raising it # raises the oldest distro Loop runs on, so change it as a decision, not to make # this assertion pass. - self.assertIn("runs-on: ubuntu-22.04", linux) + self.assertIn("runs-on: blacksmith-4vcpu-ubuntu-2204", linux) self.assertIn("VCPKG_DEFAULT_BINARY_CACHE", linux) self.assertIn("VCPKG_BINARY_SOURCES=clear;files", linux) self.assertIn("./vcpkg-binary-cache", linux) From e603bdcbd0f79bf17d7ebb579bd70710cc7142a0 Mon Sep 17 00:00:00 2001 From: mberrys Date: Mon, 7 Sep 2026 01:04:14 +0000 Subject: [PATCH 3/7] Pin Python for Blacksmith Windows runners Co-authored-by: Codesmith --- .github/workflows/WindowsInstall.yml | 5 +++++ .github/workflows/reusable-windows.yml | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/WindowsInstall.yml b/.github/workflows/WindowsInstall.yml index dd905b803..91540a836 100644 --- a/.github/workflows/WindowsInstall.yml +++ b/.github/workflows/WindowsInstall.yml @@ -38,6 +38,11 @@ jobs: ref: ${{ inputs.source_sha || github.event.pull_request.head.sha }} fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + with: + python-version: "3.12" + - name: Verify exact source SHA working-directory: loop shell: pwsh diff --git a/.github/workflows/reusable-windows.yml b/.github/workflows/reusable-windows.yml index dccd4956c..1d5a8d3e1 100644 --- a/.github/workflows/reusable-windows.yml +++ b/.github/workflows/reusable-windows.yml @@ -26,6 +26,11 @@ jobs: with: path: loop + - name: Set up Python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + with: + python-version: "3.12" + - name: Exclude workspace from Windows Defender real-time scanning # CMake's install() does an atomic rename of freshly-built DLLs; Defender's # real-time scanner can hold a brief lock on them right after MSBuild writes From dfe3c3dc7163f7ffb5a914aa65d39037e4a60b8e Mon Sep 17 00:00:00 2001 From: mberrys Date: Mon, 7 Sep 2026 01:05:47 +0000 Subject: [PATCH 4/7] Keep MSI packaging on Windows 2022 Co-authored-by: Codesmith --- .github/workflows/WindowsInstall.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/WindowsInstall.yml b/.github/workflows/WindowsInstall.yml index 91540a836..01d8747f9 100644 --- a/.github/workflows/WindowsInstall.yml +++ b/.github/workflows/WindowsInstall.yml @@ -21,7 +21,8 @@ permissions: jobs: build_windows: - runs-on: blacksmith-4vcpu-windows-2025 + # Packaging pins the exact MakeAppx.exe binary shipped by Windows 2022. + runs-on: windows-2022 env: VCPKG_INSTALLED_DIR: ${{ github.workspace }}\vcpkg_installed VCPKG_ROOT: ${{ github.workspace }}\vcpkg From fd62bf29bd36ac2c8ca852a8f76abf88084d2004 Mon Sep 17 00:00:00 2001 From: mberrys Date: Mon, 7 Sep 2026 01:08:20 +0000 Subject: [PATCH 5/7] Install pinned dpkg on Blacksmith runners Co-authored-by: Codesmith --- .github/workflows/reusable-linux.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/reusable-linux.yml b/.github/workflows/reusable-linux.yml index 9aa5112fb..d975c98c7 100644 --- a/.github/workflows/reusable-linux.yml +++ b/.github/workflows/reusable-linux.yml @@ -88,17 +88,17 @@ jobs: sudo apt update sudo apt install -y autoconf autoconf-archive automake libtool libcups2 libcups2-dev libfontconfig1-dev - - name: Verify runner packaging versions + - name: Install and verify pinned packaging version run: | - # The ubuntu-24.04 runner label is pinned at the job level; assert the - # dpkg version it ships matches the recorded pin (packaging-tools.json - # -> deb) so a runner image drift fails loudly. + # Blacksmith's Ubuntu 24.04 image can trail the repository's recorded + # dpkg patch release. Install the exact pin rather than accepting image drift. DEB_PIN="$(python3 - <<'PY' import json with open("loop/.github/pins/packaging-tools.json", encoding="utf-8") as f: print(json.load(f)["deb"]["dpkgVersion"]) PY )" + sudo apt install -y "dpkg=$DEB_PIN" ACTUAL="$(dpkg-query -W -f='${Version}' dpkg)" test "$ACTUAL" = "$DEB_PIN" || { echo "::error::dpkg version drift: expected $DEB_PIN, runner has $ACTUAL" From 82d1aa9d663b8df0bf873bc8a4544465a2fc9324 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 7 Sep 2026 01:29:33 +0000 Subject: [PATCH 6/7] ci: keep MSI and AppImage dispatch-only on Blacksmith The runner migration left packaging on every pull_request. Restore workflow_dispatch-only exact-SHA qualification and encode that in branch policy so installer builds do not run on every push to dev. Co-authored-by: michael berry --- .github/workflows/LinuxInstall.yml | 12 ++--- .github/workflows/WindowsInstall.yml | 12 ++--- changes/blacksmith-migration-bcd7474.md | 2 +- docs/BRANCH_POLICY.md | 6 ++- scripts/ci/check_branch_policy.py | 59 ++++++++++++++++++++++ scripts/ci/test_check_branch_policy.py | 67 +++++++++++++++++++++++++ scripts/ci/test_workflow_contracts.py | 7 ++- 7 files changed, 146 insertions(+), 19 deletions(-) diff --git a/.github/workflows/LinuxInstall.yml b/.github/workflows/LinuxInstall.yml index 00291221c..18e425389 100644 --- a/.github/workflows/LinuxInstall.yml +++ b/.github/workflows/LinuxInstall.yml @@ -1,12 +1,8 @@ name: Linux_AppImage on: - # Packaging runs on every pull request as well, so a change that breaks the MSI or - # the AppImage fails here instead of surviving until someone dispatches a run by - # hand. Narrow this with a `paths:` filter if the runner cost becomes a problem. - pull_request: - - # Keep manual packaging qualification available for exact source SHAs. + # Dispatch-only. PRs targeting `dev` run ci.yml; MSI/AppImage qualification + # stays on the release-candidate path with an exact source SHA. workflow_dispatch: inputs: source_sha: @@ -40,13 +36,13 @@ jobs: uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: path: loop - ref: ${{ inputs.source_sha || github.event.pull_request.head.sha }} + ref: ${{ inputs.source_sha }} fetch-depth: 0 - name: Verify exact source SHA working-directory: loop env: - EXPECTED_SOURCE_SHA: ${{ inputs.source_sha || github.event.pull_request.head.sha }} + EXPECTED_SOURCE_SHA: ${{ inputs.source_sha }} run: | if ! [[ "$EXPECTED_SOURCE_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then echo "::error::source_sha must be a full 40-character Git SHA" diff --git a/.github/workflows/WindowsInstall.yml b/.github/workflows/WindowsInstall.yml index 01d8747f9..cc0264381 100644 --- a/.github/workflows/WindowsInstall.yml +++ b/.github/workflows/WindowsInstall.yml @@ -1,12 +1,8 @@ name: Windows_MSI on: - # Packaging runs on every pull request as well, so a change that breaks the MSI or - # the AppImage fails here instead of surviving until someone dispatches a run by - # hand. Narrow this with a `paths:` filter if the runner cost becomes a problem. - pull_request: - - # Keep manual packaging qualification available for exact source SHAs. + # Dispatch-only. PRs targeting `dev` run ci.yml; MSI/AppImage qualification + # stays on the release-candidate path with an exact source SHA. workflow_dispatch: inputs: source_sha: @@ -36,7 +32,7 @@ jobs: uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: path: loop - ref: ${{ inputs.source_sha || github.event.pull_request.head.sha }} + ref: ${{ inputs.source_sha }} fetch-depth: 0 - name: Set up Python @@ -48,7 +44,7 @@ jobs: working-directory: loop shell: pwsh env: - EXPECTED_SOURCE_SHA: ${{ inputs.source_sha || github.event.pull_request.head.sha }} + EXPECTED_SOURCE_SHA: ${{ inputs.source_sha }} run: | if ($env:EXPECTED_SOURCE_SHA -notmatch "^[0-9a-fA-F]{40}$") { throw "source_sha must be a full 40-character Git SHA" diff --git a/changes/blacksmith-migration-bcd7474.md b/changes/blacksmith-migration-bcd7474.md index 199c21a7b..65846251e 100644 --- a/changes/blacksmith-migration-bcd7474.md +++ b/changes/blacksmith-migration-bcd7474.md @@ -3,4 +3,4 @@ Category: internal Audience: developers Breaking-Change: no -Summary: Run selected GitHub Actions workflows on Blacksmith runners while preserving the packaging platform contract. +Summary: Run selected GitHub Actions workflows on Blacksmith runners while keeping Windows_MSI and Linux_AppImage as workflow_dispatch-only exact-SHA qualification, and keeping full linux/windows CI on the stable release-candidate path instead of every push to `dev`. diff --git a/docs/BRANCH_POLICY.md b/docs/BRANCH_POLICY.md index 969aa113b..91b71ec80 100644 --- a/docs/BRANCH_POLICY.md +++ b/docs/BRANCH_POLICY.md @@ -20,7 +20,9 @@ the protected branches by the corresponding GitHub branch rules. The Release Gate workflow listens for `pull_request` targeting `stable` and for `merge_group` so an optional merge queue cannot wait on a check that never runs. It has no path filters. Integration PRs targeting `dev` or `unstable` run -`ci.yml`. Merges into `unstable` must pass `agent-fast / build`. +`ci.yml`. Merges into `unstable` must pass `agent-fast / build`. Windows_MSI +and Linux_AppImage are `workflow_dispatch`-only exact-SHA qualification; they +do not run on `push` or `pull_request`. The declarations below are intentionally machine-readable by `scripts/ci/check_branch_policy.py`. That check runs in CI, so a workflow @@ -39,6 +41,8 @@ branch protection when a token can read it. - Release gate pull_request branches: `stable` - Integration workflow: `.github/workflows/ci.yml` - Integration pull_request branches: `dev`, `unstable` +- Packaging workflows: `.github/workflows/LinuxInstall.yml`, `.github/workflows/WindowsInstall.yml` +- Packaging events: `workflow_dispatch` `master` is not part of the Loop branch policy. It is retained only in older historical documents or upstream references; new workflow triggers must not diff --git a/scripts/ci/check_branch_policy.py b/scripts/ci/check_branch_policy.py index 9ace72d86..577d2c1f8 100644 --- a/scripts/ci/check_branch_policy.py +++ b/scripts/ci/check_branch_policy.py @@ -41,6 +41,12 @@ DOCUMENTED_INTEGRATION_PR_BRANCHES = re.compile( r"^[-*]\s+Integration pull_request branches:\s*(.+)$", re.MULTILINE ) +DOCUMENTED_PACKAGING_WORKFLOWS = re.compile( + r"^[-*]\s+Packaging workflows:\s*(.+)$", re.MULTILINE +) +DOCUMENTED_PACKAGING_EVENTS = re.compile( + r"^[-*]\s+Packaging events:\s*(.+)$", re.MULTILINE +) @dataclass(frozen=True) @@ -56,6 +62,8 @@ class DocumentedPolicy: release_gate_pull_request_branches: tuple[str, ...] integration_workflow: str integration_pull_request_branches: tuple[str, ...] + packaging_workflows: tuple[str, ...] + packaging_events: tuple[str, ...] def _branch_names(value: str) -> tuple[str, ...]: @@ -115,6 +123,14 @@ def required(pattern: re.Pattern[str], label: str) -> str: ) if not integration_pr: raise ValueError("policy declares no integration pull_request branches") + packaging_workflows = _branch_names( + required(DOCUMENTED_PACKAGING_WORKFLOWS, "Packaging workflows:") + ) + if not packaging_workflows: + raise ValueError("policy declares no packaging workflows") + packaging_events = _branch_names(required(DOCUMENTED_PACKAGING_EVENTS, "Packaging events:")) + if not packaging_events: + raise ValueError("policy declares no packaging events") return DocumentedPolicy( ci_branches=ci_branches, protected_branches=protected, @@ -127,6 +143,8 @@ def required(pattern: re.Pattern[str], label: str) -> str: release_gate_pull_request_branches=release_pr, integration_workflow=integration_workflow, integration_pull_request_branches=integration_pr, + packaging_workflows=packaging_workflows, + packaging_events=packaging_events, ) @@ -331,6 +349,45 @@ def validate_integration_workflow(path: Path, text: str, policy: DocumentedPolic violations.append( f"{path}: {job_name} job must run for workflow_dispatch" ) + elif "refs/heads/stable" not in block: + violations.append( + f"{path}: {job_name} job must stay on stable push or workflow_dispatch" + ) + return violations + + +def validate_packaging_workflows(root: Path, policy: DocumentedPolicy) -> list[str]: + """Keep MSI/AppImage qualification on exact-SHA workflow_dispatch only.""" + violations: list[str] = [] + if policy.packaging_events != ("workflow_dispatch",): + violations.append( + "docs/BRANCH_POLICY.md: packaging events must be workflow_dispatch only, " + f"got {list(policy.packaging_events)}" + ) + expected = { + ".github/workflows/LinuxInstall.yml", + ".github/workflows/WindowsInstall.yml", + } + if set(policy.packaging_workflows) != expected: + violations.append( + "docs/BRANCH_POLICY.md: packaging workflows must be " + f"{sorted(expected)}, got {list(policy.packaging_workflows)}" + ) + for rel in policy.packaging_workflows: + path = root / rel + try: + text = path.read_text(encoding="utf-8") + except OSError as exc: + violations.append(f"{path}: {exc}") + continue + events = parse_on_events(text) + if "workflow_dispatch" not in events: + violations.append(f"{path}: packaging must listen for workflow_dispatch") + for event in events: + if event != "workflow_dispatch": + violations.append(f"{path}: packaging must not trigger on `{event}`") + if "github.event.pull_request" in text: + violations.append(f"{path}: packaging checkout must use inputs.source_sha only") return violations @@ -489,6 +546,8 @@ def validate_repository( else: violations.extend(validate_release_gate_workflow(release_gate, release_text, policy)) + violations.extend(validate_packaging_workflows(root, policy)) + codeql = root / ".github/workflows/codeql.yml" try: codeql_text = codeql.read_text(encoding="utf-8") diff --git a/scripts/ci/test_check_branch_policy.py b/scripts/ci/test_check_branch_policy.py index e531b4bce..1707881a9 100644 --- a/scripts/ci/test_check_branch_policy.py +++ b/scripts/ci/test_check_branch_policy.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import tempfile import unittest from pathlib import Path @@ -10,6 +11,7 @@ parse_workflow_branch_triggers, validate_integration_workflow, validate_live_protection, + validate_packaging_workflows, validate_release_gate_workflow, validate_repository, validate_workflow_branches, @@ -41,6 +43,14 @@ def test_documented_policy_declares_ci_branches_and_required_check(self): self.assertEqual(policy.release_gate_pull_request_branches, ("stable",)) self.assertEqual(policy.integration_workflow, ".github/workflows/ci.yml") self.assertEqual(policy.integration_pull_request_branches, ("dev", "unstable")) + self.assertEqual( + policy.packaging_workflows, + ( + ".github/workflows/LinuxInstall.yml", + ".github/workflows/WindowsInstall.yml", + ), + ) + self.assertEqual(policy.packaging_events, ("workflow_dispatch",)) def test_current_ci_workflow_matches_policy(self): policy = parse_documented_policy_full( @@ -182,6 +192,63 @@ def test_rejects_manual_dispatch_without_full_platform_jobs(self): self.assertTrue(any("linux job must run for workflow_dispatch" in item for item in violations)) self.assertTrue(any("windows job must run for workflow_dispatch" in item for item in violations)) + def test_rejects_full_platform_jobs_on_every_push(self): + policy = parse_documented_policy_full( + (ROOT / "docs" / "BRANCH_POLICY.md").read_text(encoding="utf-8") + ) + stale = """on: + push: + branches: [dev, unstable, stable] + pull_request: + branches: [dev, unstable] + workflow_dispatch: + +jobs: + agent-fast: + uses: ./.github/workflows/reusable-linux.yml + linux: + if: github.event_name == 'workflow_dispatch' || github.event_name == 'push' + uses: ./.github/workflows/reusable-linux.yml + windows: + if: github.event_name == 'workflow_dispatch' || github.event_name == 'push' + uses: ./.github/workflows/reusable-windows.yml +""" + violations = validate_integration_workflow(Path("ci.yml"), stale, policy) + self.assertTrue( + any("linux job must stay on stable push or workflow_dispatch" in item for item in violations) + ) + self.assertTrue( + any("windows job must stay on stable push or workflow_dispatch" in item for item in violations) + ) + + def test_current_packaging_workflows_are_dispatch_only(self): + policy = parse_documented_policy_full( + (ROOT / "docs" / "BRANCH_POLICY.md").read_text(encoding="utf-8") + ) + self.assertEqual(validate_packaging_workflows(ROOT, policy), []) + + def test_rejects_packaging_pull_request_trigger(self): + policy = parse_documented_policy_full( + (ROOT / "docs" / "BRANCH_POLICY.md").read_text(encoding="utf-8") + ) + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + linux = root / ".github/workflows/LinuxInstall.yml" + windows = root / ".github/workflows/WindowsInstall.yml" + linux.parent.mkdir(parents=True) + linux.write_text( + "on:\n pull_request:\n workflow_dispatch:\n inputs:\n source_sha:\n", + encoding="utf-8", + ) + windows.write_text( + "on:\n workflow_dispatch:\n inputs:\n source_sha:\n", + encoding="utf-8", + ) + violations = validate_packaging_workflows(root, policy) + self.assertTrue( + any("must not trigger on `pull_request`" in item for item in violations) + ) + def test_live_protection_rejects_ci_ok_and_unbound_app(self): policy = parse_documented_policy_full( (ROOT / "docs" / "BRANCH_POLICY.md").read_text(encoding="utf-8") diff --git a/scripts/ci/test_workflow_contracts.py b/scripts/ci/test_workflow_contracts.py index aa53c8cf7..71cf229ab 100644 --- a/scripts/ci/test_workflow_contracts.py +++ b/scripts/ci/test_workflow_contracts.py @@ -82,6 +82,7 @@ def test_package_workflows_require_and_record_exact_source_sha(self): # raises the oldest distro Loop runs on, so change it as a decision, not to make # this assertion pass. self.assertIn("runs-on: blacksmith-4vcpu-ubuntu-2204", linux) + self.assertIn("runs-on: windows-2022", windows) self.assertIn("VCPKG_DEFAULT_BINARY_CACHE", linux) self.assertIn("VCPKG_BINARY_SOURCES=clear;files", linux) self.assertIn("./vcpkg-binary-cache", linux) @@ -111,7 +112,11 @@ def test_package_workflows_require_and_record_exact_source_sha(self): self.assertIn("source_sha:", workflow) self.assertRegex(workflow, r"source_sha:\n\s+description:.*\n\s+required:\s+true") self.assertIn("inputs.source_sha", workflow) - self.assertIn("pull_request:", workflow) + self.assertIn("ref: ${{ inputs.source_sha }}", workflow) + self.assertIn("workflow_dispatch:", workflow) + self.assertNotIn("pull_request:", workflow) + self.assertNotIn("github.event.pull_request", workflow) + self.assertNotRegex(workflow, r"(?m)^ push:") self.assertIn("Verify exact source SHA", workflow) self.assertIn("LOOP_SOURCE_SHA", workflow) self.assertIn("inspect_package_dependencies.py", workflow) From 749f0767b838ccf0508fa4239605aa39ee5be4e4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 7 Sep 2026 01:32:29 +0000 Subject: [PATCH 7/7] ci: limit Blacksmith to Windows_MSI only Revert the broad runner migration so Linux CI and AppImage stay on GitHub-hosted ubuntu runners. Reserve Blacksmith for the dispatch-only Windows_MSI packaging gate and add a contract test that enforces that scope. Co-authored-by: michael berry --- .github/workflows/CreateReleaseDraft.yml | 2 +- .github/workflows/LinuxFlatpak.yml | 2 +- .github/workflows/LinuxInstall.yml | 2 +- .github/workflows/SupplyChain.yml | 2 +- .github/workflows/WindowsInstall.yml | 5 +++-- .github/workflows/ci.yml | 2 +- .github/workflows/documentation.yml | 2 +- .github/workflows/fuzz.yml | 2 +- .github/workflows/release-gate.yml | 12 ++++++------ .github/workflows/reusable-linux.yml | 10 +++++----- .github/workflows/reusable-windows.yml | 7 +------ .github/workflows/sync-milestones.yml | 2 +- changes/blacksmith-migration-bcd7474.md | 2 +- scripts/ci/test_workflow_contracts.py | 13 +++++++++++-- 14 files changed, 35 insertions(+), 30 deletions(-) diff --git a/.github/workflows/CreateReleaseDraft.yml b/.github/workflows/CreateReleaseDraft.yml index 8f383c973..4603e5db0 100644 --- a/.github/workflows/CreateReleaseDraft.yml +++ b/.github/workflows/CreateReleaseDraft.yml @@ -20,7 +20,7 @@ permissions: jobs: build: - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-latest steps: - name: Checkout repository diff --git a/.github/workflows/LinuxFlatpak.yml b/.github/workflows/LinuxFlatpak.yml index 9d2451e66..43b1fcf6a 100644 --- a/.github/workflows/LinuxFlatpak.yml +++ b/.github/workflows/LinuxFlatpak.yml @@ -10,7 +10,7 @@ permissions: jobs: build_ubuntu: - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-24.04 steps: - name: Checkout repository diff --git a/.github/workflows/LinuxInstall.yml b/.github/workflows/LinuxInstall.yml index 18e425389..9e0f90f5c 100644 --- a/.github/workflows/LinuxInstall.yml +++ b/.github/workflows/LinuxInstall.yml @@ -18,7 +18,7 @@ permissions: jobs: build_ubuntu: # The AppImage runtime is pinned for Ubuntu 22.04 compatibility. - runs-on: blacksmith-4vcpu-ubuntu-2204 + runs-on: ubuntu-22.04 env: VCPKG_OVERLAY_PORTS: ${{ github.workspace }}/loop/vcpkg/overlays/linux:${{ github.workspace }}/loop/vcpkg/overlays/general VCPKG_INSTALLED_DIR: ${{ github.workspace }}/vcpkg_installed diff --git a/.github/workflows/SupplyChain.yml b/.github/workflows/SupplyChain.yml index 9c21d5b3d..f783db961 100644 --- a/.github/workflows/SupplyChain.yml +++ b/.github/workflows/SupplyChain.yml @@ -10,7 +10,7 @@ permissions: jobs: policy: - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-24.04 steps: - name: Checkout repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 diff --git a/.github/workflows/WindowsInstall.yml b/.github/workflows/WindowsInstall.yml index cc0264381..48a2a5f0e 100644 --- a/.github/workflows/WindowsInstall.yml +++ b/.github/workflows/WindowsInstall.yml @@ -17,8 +17,9 @@ permissions: jobs: build_windows: - # Packaging pins the exact MakeAppx.exe binary shipped by Windows 2022. - runs-on: windows-2022 + # Blacksmith is reserved for this ~50-minute packaging gate; other Windows CI + # stays on GitHub-hosted windows-2022 to conserve Blacksmith minutes. + runs-on: blacksmith-4vcpu-windows-2025 env: VCPKG_INSTALLED_DIR: ${{ github.workspace }}\vcpkg_installed VCPKG_ROOT: ${{ github.workspace }}\vcpkg diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e7f290bcd..7939bb0c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ permissions: jobs: source_integrity: - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-24.04 steps: - name: Checkout repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 1bb3171a1..42ab62328 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -18,7 +18,7 @@ permissions: jobs: architecture-docs: - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-24.04 steps: - name: Checkout repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 3ee0acfbf..374397ce8 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -18,7 +18,7 @@ on: jobs: fuzz: - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-24.04 timeout-minutes: 90 env: VCPKG_OVERLAY_PORTS: ${{ github.workspace }}/vcpkg/overlays/linux:${{ github.workspace }}/vcpkg/overlays/general diff --git a/.github/workflows/release-gate.yml b/.github/workflows/release-gate.yml index 76d891ca5..99d1e9c83 100644 --- a/.github/workflows/release-gate.yml +++ b/.github/workflows/release-gate.yml @@ -16,7 +16,7 @@ permissions: jobs: source_integrity: - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-24.04 steps: - name: Checkout repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -34,7 +34,7 @@ jobs: run: python3 scripts/ci/check_generated_dependency_paths.py agent_contract: - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-24.04 steps: - name: Checkout repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -64,7 +64,7 @@ jobs: uses: ./.github/workflows/reusable-windows.yml documentation: - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-24.04 steps: - name: Checkout repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -75,7 +75,7 @@ jobs: uses: ./.github/workflows/fuzz.yml package_contract: - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-24.04 steps: - name: Checkout repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -89,7 +89,7 @@ jobs: run: python3 scripts/ci/validate_product_surface.py supply_chain: - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-24.04 steps: - name: Checkout repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -113,7 +113,7 @@ jobs: - fuzz_regression - package_contract - supply_chain - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-24.04 permissions: contents: read steps: diff --git a/.github/workflows/reusable-linux.yml b/.github/workflows/reusable-linux.yml index d975c98c7..e5d1022de 100644 --- a/.github/workflows/reusable-linux.yml +++ b/.github/workflows/reusable-linux.yml @@ -21,7 +21,7 @@ permissions: jobs: build: - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-24.04 env: VCPKG_OVERLAY_PORTS: ${{ github.workspace }}/loop/vcpkg/overlays/linux:${{ github.workspace }}/loop/vcpkg/overlays/general VCPKG_INSTALLED_DIR: ${{ github.workspace }}/vcpkg_installed @@ -88,17 +88,17 @@ jobs: sudo apt update sudo apt install -y autoconf autoconf-archive automake libtool libcups2 libcups2-dev libfontconfig1-dev - - name: Install and verify pinned packaging version + - name: Verify runner packaging versions run: | - # Blacksmith's Ubuntu 24.04 image can trail the repository's recorded - # dpkg patch release. Install the exact pin rather than accepting image drift. + # The ubuntu-24.04 runner label is pinned at the job level; assert the + # dpkg version it ships matches the recorded pin (packaging-tools.json + # -> deb) so a runner image drift fails loudly. DEB_PIN="$(python3 - <<'PY' import json with open("loop/.github/pins/packaging-tools.json", encoding="utf-8") as f: print(json.load(f)["deb"]["dpkgVersion"]) PY )" - sudo apt install -y "dpkg=$DEB_PIN" ACTUAL="$(dpkg-query -W -f='${Version}' dpkg)" test "$ACTUAL" = "$DEB_PIN" || { echo "::error::dpkg version drift: expected $DEB_PIN, runner has $ACTUAL" diff --git a/.github/workflows/reusable-windows.yml b/.github/workflows/reusable-windows.yml index 1d5a8d3e1..d45552f4b 100644 --- a/.github/workflows/reusable-windows.yml +++ b/.github/workflows/reusable-windows.yml @@ -11,7 +11,7 @@ permissions: jobs: build: - runs-on: blacksmith-4vcpu-windows-2025 + runs-on: windows-2022 env: VCPKG_OVERLAY_PORTS: ${{ github.workspace }}\loop\vcpkg\overlays\general VCPKG_INSTALLED_DIR: ${{ github.workspace }}\vcpkg_installed @@ -26,11 +26,6 @@ jobs: with: path: loop - - name: Set up Python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 - with: - python-version: "3.12" - - name: Exclude workspace from Windows Defender real-time scanning # CMake's install() does an atomic rename of freshly-built DLLs; Defender's # real-time scanner can hold a brief lock on them right after MSBuild writes diff --git a/.github/workflows/sync-milestones.yml b/.github/workflows/sync-milestones.yml index 42379f70e..5775d00e8 100644 --- a/.github/workflows/sync-milestones.yml +++ b/.github/workflows/sync-milestones.yml @@ -9,7 +9,7 @@ permissions: jobs: sync: - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 diff --git a/changes/blacksmith-migration-bcd7474.md b/changes/blacksmith-migration-bcd7474.md index 65846251e..9ff51189d 100644 --- a/changes/blacksmith-migration-bcd7474.md +++ b/changes/blacksmith-migration-bcd7474.md @@ -3,4 +3,4 @@ Category: internal Audience: developers Breaking-Change: no -Summary: Run selected GitHub Actions workflows on Blacksmith runners while keeping Windows_MSI and Linux_AppImage as workflow_dispatch-only exact-SHA qualification, and keeping full linux/windows CI on the stable release-candidate path instead of every push to `dev`. +Summary: Run Windows_MSI on Blacksmith to shorten the ~50-minute packaging gate while keeping every other workflow on GitHub-hosted runners, preserving workflow_dispatch-only exact-SHA MSI/AppImage qualification, and keeping full linux/windows CI on the stable release-candidate path instead of every push to `dev`. diff --git a/scripts/ci/test_workflow_contracts.py b/scripts/ci/test_workflow_contracts.py index 71cf229ab..caca118ba 100644 --- a/scripts/ci/test_workflow_contracts.py +++ b/scripts/ci/test_workflow_contracts.py @@ -81,8 +81,8 @@ def test_package_workflows_require_and_record_exact_source_sha(self): # Deliberate: the AppImage glibc floor is whatever this runner ships. Raising it # raises the oldest distro Loop runs on, so change it as a decision, not to make # this assertion pass. - self.assertIn("runs-on: blacksmith-4vcpu-ubuntu-2204", linux) - self.assertIn("runs-on: windows-2022", windows) + self.assertIn("runs-on: ubuntu-22.04", linux) + self.assertIn("runs-on: blacksmith-4vcpu-windows-2025", windows) self.assertIn("VCPKG_DEFAULT_BINARY_CACHE", linux) self.assertIn("VCPKG_BINARY_SOURCES=clear;files", linux) self.assertIn("./vcpkg-binary-cache", linux) @@ -126,6 +126,15 @@ def test_package_workflows_require_and_record_exact_source_sha(self): self.assertIn("loop-package-boundary-linux-evidence", linux) self.assertIn("loop-package-boundary-windows-evidence", windows) + def test_blacksmith_is_reserved_for_windows_msi_only(self): + workflows_dir = ROOT / ".github/workflows" + blacksmith_workflows = [] + for path in sorted(workflows_dir.glob("*.yml")): + text = path.read_text(encoding="utf-8") + if "blacksmith" in text: + blacksmith_workflows.append(path.name) + self.assertEqual(blacksmith_workflows, ["WindowsInstall.yml"]) + def test_windows_release_msi_is_x64_and_uses_64_bit_program_files(self): workflow = (ROOT / ".github/workflows/WindowsInstall.yml").read_text(encoding="utf-8") self.assertIn('Platform=x64', workflow)