diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml deleted file mode 100644 index 2c0bbea..0000000 --- a/.github/actionlint.yaml +++ /dev/null @@ -1,3 +0,0 @@ -self-hosted-runner: - labels: - - ci1-integration diff --git a/.github/workflows/ci-hosted.yml b/.github/workflows/ci-hosted.yml new file mode 100644 index 0000000..d640123 --- /dev/null +++ b/.github/workflows/ci-hosted.yml @@ -0,0 +1,32 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + verify: + name: Verify Node ${{ matrix.node-version }} + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + node-version: + - 22 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: ${{ matrix.node-version }} + cache: npm + - run: npm ci + - run: npm run verify + - run: node --test scripts/release-evidence.test.mjs diff --git a/.github/workflows/ci1.yml b/.github/workflows/ci1.yml deleted file mode 100644 index c36e6c6..0000000 --- a/.github/workflows/ci1.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: CI - -on: - pull_request: - push: - branches: - - main - -permissions: - contents: read - -jobs: - verify: - name: Verify Node ${{ matrix.node-version }} - runs-on: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && 'ubuntu-latest' || 'ci1-integration' }} - timeout-minutes: 20 - strategy: - fail-fast: false - matrix: - node-version: - - 22 - steps: - - name: Require trusted source - env: - EVENT_NAME: ${{ github.event_name }} - HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }} - REPOSITORY: ${{ github.repository }} - run: | - set -euo pipefail - if [[ "$EVENT_NAME" == pull_request && "$HEAD_REPOSITORY" != "$REPOSITORY" ]]; then - echo 'Fork PRs require a separately qualified runner policy' >&2 - exit 1 - fi - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Require qualified ci-1 image - run: | - python3 scripts/verify-ci1-image.py - sha256sum --check --strict /etc/ci1-image/manifests.sha256 - - name: Test image admission refusals - run: python3 -m unittest discover -s scripts -p ci1_image_test.py -v - - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 - with: - node-version: ${{ matrix.node-version }} - cache: npm - - run: npm ci - - run: npm run verify - - run: node --test scripts/release-evidence.test.mjs diff --git a/internal/agent-sessions/ci1-cli-20261019.md b/internal/agent-sessions/ci1-cli-20261019.md index 7416451..aba063e 100644 --- a/internal/agent-sessions/ci1-cli-20261019.md +++ b/internal/agent-sessions/ci1-cli-20261019.md @@ -49,3 +49,14 @@ fingerprint-named Incus alias pointing to itself. The provider rejects remote names and retargeted aliases. The new operator receipt documents that authority; the guest's manifest is a content/age check rather than image attestation. No pool, image, host limit, product secret, deployment or runtime changed. + +## Public repository policy + +On 2026-10-19 the owner confirmed that CLI and SDK repositories are public and +made polymorfa/cli public. Live API readback confirmed CLI and SDK visibility. +The CLI CI destination is therefore standard GitHub-hosted Ubuntu for both +pull requests and main pushes. The ci-1 image pin, admission helper and +controller receipt were removed from the active source. The full Node 22 +verification and release-evidence commands are preserved, with a read-only +job token and no persisted checkout credentials. This section supersedes the +private-repository CI routing described above; prior results remain historical. diff --git a/internal/docs/ci-hosted.md b/internal/docs/ci-hosted.md new file mode 100644 index 0000000..f11ea29 --- /dev/null +++ b/internal/docs/ci-hosted.md @@ -0,0 +1,24 @@ +# Public CLI CI + +The owner made polymorfa/cli public on 2026-10-19. CLI and SDK repositories use +standard GitHub-hosted runners. They are excluded from ci-1, whose runner group +has public-repository access disabled. + +`.github/workflows/ci-hosted.yml` preserves the CI display name, Verify Node 22 +check and all existing source commands: `npm ci`, `npm run verify`, and the three +release-evidence tests. It runs on pull requests, including forks, and main +pushes using `ubuntu-latest`, read-only repository contents, no persisted +checkout credentials, npm caching and a 20-minute deadline. The workflow uses +`pull_request`, not a privileged target event, and declares no secrets. + +No ci-1 manifest, image pin, private tool cache or self-hosted runner is required. +New workflow identity allows source qualification without enabling an old +Blacksmith workflow. Existing source checks, production audit, build and package +dry-run are preserved. Actual GitHub qualification and the first main push +remain required before calling this cutover verified. + +CLI commands, SDK dependency, API contracts, package and lockfile, public product +docs/changelog, product flags and Admin controls are unaffected: this changes +only CI routing. The publication workflow is unchanged, schedule/manual only, +and retains matching SDK/API acceptance. No package publication or product +deployment is invoked. Staging and regional rename #744 remain held. diff --git a/internal/docs/ci1-qualified-pool.json b/internal/docs/ci1-qualified-pool.json deleted file mode 100644 index 6fe9bcf..0000000 --- a/internal/docs/ci1-qualified-pool.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "schema": 1, - "observed_at": "2026-10-19T19:00:23.666736+00:00", - "controller": "ci-1", - "scalesets": [ - { - "id": 1, - "name": "ci1-general", - "image": "505d3336ea46318175907ac2192df5002406731bd2990a344ba2ee98c1c89ab0", - "enabled": true - }, - { - "id": 2, - "name": "ci1-integration", - "image": "505d3336ea46318175907ac2192df5002406731bd2990a344ba2ee98c1c89ab0", - "enabled": true - } - ], - "incus_project": "ci1", - "alias": { - "name": "505d3336ea46318175907ac2192df5002406731bd2990a344ba2ee98c1c89ab0", - "target": "505d3336ea46318175907ac2192df5002406731bd2990a344ba2ee98c1c89ab0" - }, - "authority": "GARM controller scale-set image pin and provider fingerprint-alias enforcement; guest manifest is a separate content and age check", - "mutation": "none; read-only verification", - "requalification": "Any new fingerprint needs explicit pool changes and the coordinated image/lifecycle qualification", - "provider_wrapper": { - "path": "/opt/garm/providers.d/ci1-incus", - "sha256": "a091d8c0f8a08be521d754d046622b6fcc66f127c9bea52d60082f2a3d424813", - "matches_source": "https://github.com/polymorfa/polymorfa/blob/d35de858d73fe9c6d9e6110637a18a3dfe57fb56/infra/ci1/provider-admission.sh", - "checks": [ - "local immutable fingerprint required", - "fingerprint-named alias must target that exact fingerprint", - "remote names rejected" - ] - } -} diff --git a/internal/docs/ci1-runners.md b/internal/docs/ci1-runners.md deleted file mode 100644 index c82f7b2..0000000 --- a/internal/docs/ci1-runners.md +++ /dev/null @@ -1,86 +0,0 @@ -# CLI checks on ci-1 - -The owner approved the GARM + Incus cutover and paused new paid CI runs. This -change qualifies the private CLI repository on disposable Linux workers. The -[coordinated plan](https://github.com/polymorfa/polymorfa/blob/d35de858d73fe9c6d9e6110637a18a3dfe57fb56/internal/docs/superpowers/plans/2026-10-19-ci1-runner-images-and-cutover.md) -owns the host, image, lifecycle proof and environment holds. - -## Workflow and image contract - -`ci1.yml` keeps the CI display name, Verify Node 22 check and all three existing -commands: `npm ci`, `npm run verify` and the release-evidence tests. It uses -`ci1-integration` with eight CPUs and 32 GiB RAM, a 20-minute job deadline, read-only -repository permission and no checkout credential persistence. The already qualified integration pool lets this CLI check run alongside the -monorepo general queue, within the unchanged host budget. Fork PRs run only a -failing guard on a GitHub-hosted runner, before checkout; the preserved Verify -Node 22 check fails instead of reporting a successful skip. Trusted source work -always uses ci-1. No source-test fallback exists. - -The new file identity lets this PR qualify while old paid workflow 338004193 -stays disabled. Do not enable the old identity before inspecting its source; -the default branch still selects Blacksmith until the cutover merges. - -Before installation, the job validates the shared image's base fingerprint, -machine/tool input hashes, age of at most seven days, architecture and CPU shape. -It then verifies the image's checksum manifest with strict parsing; any malformed -line fails admission. Node setup selects cached Node 22. Existing npm cache behavior and the lockfile-based installation remain. -The image's pnpm cache belongs to the monorepo; no CLI npm-cache hit is inferred. -A changed image definition requires new qualification and explicit pin updates. - -The qualified image fingerprint is -`505d3336ea46318175907ac2192df5002406731bd2990a344ba2ee98c1c89ab0`. -The controller's scale set 2 (`ci1-integration`) pins this exact final fingerprint; -scale set 1 does too. A live readback on 2026-10-19 confirmed both pins and the -fingerprint-named local Incus alias targeting that same fingerprint. The installed -provider wrapper SHA-256 also matches its committed source. The -[readback receipt](ci1-qualified-pool.json) records the binding. The controller -provider refuses remote or non-fingerprint image names and rejects retargeted -aliases. Image identity is bound by this controller configuration; the guest -manifest checks content and age. A new image needs explicit operator pool -changes and full qualification under the coordinated plan. - -Its definition source is `6481b9e199d96fb3488f34d6335ac41169f4fb5d` in the monorepo. -The underlying Ubuntu base is -`2139edd9da4026da8a66c0557af7eee4789101104eb27860bacd57086219cebb`. -Public source dependencies only are installed; no customer credential, release -signing key or production secret is added to CI or the image. - -## Qualification and rollout - -Source dependency: CLI main `c21af71f13e68230769d1a2ca17985d143bf6c6e`. -Seven local admission tests cover the accepted image, wrong architecture/shape, -expiry/future clock, wrong base, changed/missing definition or tool hashes, -fork guard outcomes before checkout and malformed checksum rows. The two new -regressions failed before the fix and pass afterward; all source test commands -remain unchanged. -The unchanged local `npm run verify` passed all 110 test files and 1,051 tests, -types, lint, formatting, build, package dry-run and production audit (zero -production vulnerabilities). Three release-evidence tests also passed. -Local workflow lint and diff checks pass. GitHub source qualification and the -first main-branch run remain required. Host lifecycle proof is separate from CLI -source acceptance. - -The publication workflow is unchanged. It runs on its schedule or manual -invocation, requires matching SDK/API acceptance and never runs merely because -this CI PR merges. This change does not authorize a package publication or -staging deployment. Staging and regional rename holds remain in force. - -On a bad image, drain/rebuild the worker pool under the monorepo runbook and -qualify it again. Do not silently re-enable paid CI. Reverting the source change -does not re-enable workflow 338004193; paid resumption needs an owner cost decision. - -## Impact and session provenance - -CI and operator documentation are updated. CLI commands, credential handling, -SDK dependency, API contracts, public docs/changelog, product release flags, -Admin controls, billing, Usage, logs, analytics and customer interfaces are -unaffected because no runtime, dependency or product configuration changes. -No customer feature or release flag is introduced. Linear is unavailable in this -session; no tracker synchronization is claimed. - -Collaborator purpshell; Codex/gpt-6.1-sol; T3 thread -`8ad9e447-6fd5-4fc5-8e96-189326af2c28`. The existing registration is -[monorepo PR 511](https://github.com/polymorfa/polymorfa/pull/511). -The [sanitized manual checkpoint](../agent-sessions/ci1-cli-20261019.md) records -visible instructions and tool outcomes, with capture gaps. It provides -provenance, not GitHub acceptance or release authority. diff --git a/scripts/ci1_image_test.py b/scripts/ci1_image_test.py deleted file mode 100644 index 8204c79..0000000 --- a/scripts/ci1_image_test.py +++ /dev/null @@ -1,89 +0,0 @@ -"""Fail-closed admission for the shared, pinned ci-1 image.""" -import copy -import datetime -import importlib.util -import pathlib -import hashlib -import os -import subprocess -import tempfile -import textwrap -import unittest - -spec = importlib.util.spec_from_file_location("ci1_image", pathlib.Path(__file__).with_name("verify-ci1-image.py")) -image = importlib.util.module_from_spec(spec) -spec.loader.exec_module(image) - - -class AdmissionTest(unittest.TestCase): - def setUp(self): - self.now = datetime.datetime(2026, 10, 19, 18, 0, tzinfo=datetime.timezone.utc) - self.manifest = { - "built_at": "2026-10-19T14:41:57+00:00", - "base_fingerprint": "2139edd9da4026da8a66c0557af7eee4789101104eb27860bacd57086219cebb", - "inputs": { - "infra/ci1/machine.json": "0900619bac06c6a81f5d7535107985bf8d9cf11c6dd2079273d8ab81d9ebac3a", - "infra/ci1/tools.lock.json": "4c2a7cac77ebd8221054b2e78dfdd81e15327fb92d3e80d8986a73fb5e326d15", - }, - } - - def test_qualified_image(self): - image.validate_image(self.manifest, self.now, "x86_64", 8) - - def test_wrong_shape_or_architecture(self): - for arch, cpus in [("aarch64", 8), ("x86_64", 4)]: - with self.subTest(arch=arch, cpus=cpus), self.assertRaises(ValueError): - image.validate_image(self.manifest, self.now, arch, cpus) - - def test_expired_image_or_future_clock(self): - for built in ["2026-10-11T18:00:00+00:00", "2026-10-19T18:02:00+00:00", "2026-10-19T14:41:57"]: - with self.subTest(built=built), self.assertRaises(ValueError): - image.validate_image({**self.manifest, "built_at": built}, self.now, "x86_64", 8) - - def test_wrong_base(self): - with self.assertRaises(ValueError): - image.validate_image({**self.manifest, "base_fingerprint": "0" * 64}, self.now, "x86_64", 8) - - def test_changed_or_missing_definition_and_tools(self): - for name in self.manifest["inputs"]: - for value in [None, "0" * 64]: - bad = copy.deepcopy(self.manifest) - if value is None: - del bad["inputs"][name] - else: - bad["inputs"][name] = value - with self.subTest(name=name, value=value), self.assertRaises(ValueError): - image.validate_image(bad, self.now, "x86_64", 8) - - -class WorkflowAdmissionTest(unittest.TestCase): - def setUp(self): - self.workflow = pathlib.Path(__file__).parents[1].joinpath(".github/workflows/ci1.yml").read_text() - self.scratch = pathlib.Path.home() / ".polymorfa-agent-work" / "cli-ci1-admission-tests" - self.scratch.mkdir(parents=True, exist_ok=True) - - def test_malformed_checksum_row_fails(self): - with tempfile.TemporaryDirectory(dir=self.scratch) as directory: - root = pathlib.Path(directory) - data = root / "protected-file" - data.write_bytes(b"qualified bytes") - record = root / "manifest.sha256" - record.write_text(hashlib.sha256(data.read_bytes()).hexdigest() + " " + str(data) + "\n") - command = next(line.strip().split() for line in self.workflow.splitlines() if line.strip().startswith("sha256sum --check")) - command[-1] = str(record) - result = subprocess.run(command, capture_output=True) - self.assertEqual(result.returncode, 0, result.stderr.decode()) - record.write_text(record.read_text() + "malformed entry\n") - result = subprocess.run(command, capture_output=True) - self.assertNotEqual(result.returncode, 0, result.stdout.decode()) - - def test_fork_guard_fails_before_checkout(self): - marker = " - name: Require trusted source\n" - self.assertIn(marker, self.workflow) - script = self.workflow.split(marker, 1)[1].split(" run: |\n", 1)[1].split("\n - ", 1)[0] - script = textwrap.dedent(script) - for event, head, expected in [("pull_request", "polymorfa/cli", 0), ("push", "", 0), ("pull_request", "external/cli", 1), ("pull_request", "", 1)]: - with self.subTest(event=event, head=head): - result = subprocess.run(["bash", "-c", script], env={**os.environ, "EVENT_NAME": event, "HEAD_REPOSITORY": head, "REPOSITORY": "polymorfa/cli"}, capture_output=True) - self.assertEqual(result.returncode, expected) - self.assertLess(self.workflow.index(marker), self.workflow.index(" - uses: actions/checkout@")) diff --git a/scripts/verify-ci1-image.py b/scripts/verify-ci1-image.py deleted file mode 100644 index 6891d8f..0000000 --- a/scripts/verify-ci1-image.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python3 -"""Validate the shared ci-1 image before installing CLI dependencies.""" -import datetime -import json -import os -import pathlib -import platform - -BASE = "2139edd9da4026da8a66c0557af7eee4789101104eb27860bacd57086219cebb" -INPUTS = { - "infra/ci1/machine.json": "0900619bac06c6a81f5d7535107985bf8d9cf11c6dd2079273d8ab81d9ebac3a", - "infra/ci1/tools.lock.json": "4c2a7cac77ebd8221054b2e78dfdd81e15327fb92d3e80d8986a73fb5e326d15", -} - - -def validate_image(data, now, architecture, cpus): - if architecture != "x86_64" or cpus != 8: - raise ValueError("ci-1 requires the qualified 8-CPU x86_64 integration VM") - built = datetime.datetime.fromisoformat(data["built_at"]) - if built.tzinfo is None: - raise ValueError("ci-1 image timestamp requires a timezone") - age = now - built - if age.total_seconds() < -60 or age > datetime.timedelta(days=7): - raise ValueError("ci-1 image age or clock is invalid; rebuild before running CI") - if data["base_fingerprint"] != BASE: - raise ValueError("ci-1 base image does not match the qualified definition") - for name, expected in INPUTS.items(): - if data["inputs"].get(name) != expected: - raise ValueError("ci-1 image definition or tools changed; qualify before running CI") - - -if __name__ == "__main__": - manifest = pathlib.Path("/etc/ci1-image/input-manifest.json") - data = json.loads(manifest.read_text()) - validate_image(data, datetime.datetime.now(datetime.timezone.utc), platform.machine(), len(os.sched_getaffinity(0))) - print("ci-1 image age, base, tool definition and integration VM shape qualified")