diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index 91e3f9b80..852002f9c 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -13,12 +13,7 @@ # every push to `main`. Required status checks retain their names. # Notes : `cancel-in-progress: false` โ€” every main push gets a full run # No trigger on feat/* or fix/* (frequent changes). -# Trust : push, manual dispatch and same-repository pull requests use the -# repository self-hosted Linux/Windows runners; fork pull requests -# and dependabot keep GitHub-hosted runners. Routing is exposure -# reduction, not a security boundary (a fork PR runs its own copy of -# this workflow), so the self-hosted services stay stopped until the -# repository admission policy is accepted. +# Runners : All events use GitHub-hosted Ubuntu and Windows runners. # ============================================================================ name: ๐Ÿงช CI ยท Test @@ -56,16 +51,13 @@ jobs: settings: - name: linux host: ubuntu-latest - selfHosted: [self-hosted, Linux, X64] # windows dropped: this fork runs on free windows-latest runners # (not the paid Blacksmith 4vCPU hosts upstream uses), and the # opencode:test suite (3048 tests, many spawning real CLI # subprocesses) doesn't fit the standard runner's slower # process-spawn/IO within a reasonable CI budget. E2E Tests # (windows) is unaffected and still covers the platform. - # Trusted events only; GitHub-hosted remains the fallback for fork pull - # requests and dependabot. - runs-on: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.user.login != 'dependabot[bot]' && github.actor != 'dependabot[bot]')) && matrix.settings.selfHosted || matrix.settings.host }} + runs-on: ${{ matrix.settings.host }} defaults: run: shell: bash @@ -87,7 +79,7 @@ jobs: with: check: unit job-name: Unit Tests (${{ matrix.settings.name }}) - runner-label: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.user.login != 'dependabot[bot]' && github.actor != 'dependabot[bot]')) && join(matrix.settings.selfHosted, ',') || matrix.settings.host }} + runner-label: ${{ matrix.settings.host }} - name: Setup Go if: steps.evidence.outputs.reused != 'true' && (runner.os == 'Linux') @@ -120,16 +112,10 @@ jobs: # tool.glob tests hit ripgrep; without the system binary, binary.ts # downloads rg from GitHub releases every run (temp XDG per preload), # gambling on network stability โ€” ECONNRESET fails the test. - # GitHub-hosted runners install it with sudo; the self-hosted runner - # account has no sudo, so require host provisioning and fail clearly. run: | if command -v rg >/dev/null 2>&1; then exit 0 fi - if [ "$RUNNER_ENVIRONMENT" = "self-hosted" ]; then - echo "::error::ripgrep (rg) is required but missing on this self-hosted runner. Provision it on the host; CI jobs run without sudo." - exit 1 - fi sudo apt-get update && sudo apt-get install -y ripgrep - name: Cache Turbo @@ -159,8 +145,10 @@ jobs: # time it runs โ€” that silence can look like a hang but isn't one. # 20m was never sized against real data; give it margin above the # measured baseline instead of racing it. + # Hosted runners execute workspace tasks one at a time. This avoids + # CPU contention between builds and tests timing out 250ms Node workers. timeout-minutes: 35 - run: GITHUB_ACTIONS=false bun turbo test + run: GITHUB_ACTIONS=false bun turbo test --concurrency=1 env: OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }} @@ -214,13 +202,9 @@ jobs: settings: - name: linux host: ubuntu-latest - selfHosted: [self-hosted, Linux, X64] - name: windows host: windows-latest - selfHosted: [self-hosted, Windows, X64] - # Trusted events only; GitHub-hosted remains the fallback for fork pull - # requests and dependabot. - runs-on: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.user.login != 'dependabot[bot]' && github.actor != 'dependabot[bot]')) && matrix.settings.selfHosted || matrix.settings.host }} + runs-on: ${{ matrix.settings.host }} env: PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.playwright-browsers defaults: @@ -245,7 +229,7 @@ jobs: with: check: e2e job-name: E2E Tests (${{ matrix.settings.name }}) - runner-label: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.user.login != 'dependabot[bot]' && github.actor != 'dependabot[bot]')) && join(matrix.settings.selfHosted, ',') || matrix.settings.host }} + runner-label: ${{ matrix.settings.host }} - name: Setup Bun if: steps.evidence.outputs.reused != 'true' @@ -271,11 +255,7 @@ jobs: key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright-version.outputs.version }}-chromium - name: Install Playwright system dependencies - # GitHub-hosted runners install the Chromium system libraries here. - # The self-hosted runner account has no sudo: the host is provisioned - # up front and the verification step below fails actionably when a - # library is missing. - if: steps.evidence.outputs.reused != 'true' && (runner.os == 'Linux') && (runner.environment == 'github-hosted') + if: steps.evidence.outputs.reused != 'true' && (runner.os == 'Linux') working-directory: packages/app run: bunx playwright install-deps chromium @@ -284,18 +264,6 @@ jobs: working-directory: packages/app run: bunx playwright install chromium - - name: Verify Playwright Chromium dependencies - # Self-hosted only: execute the resolved browser so the dynamic loader - # proves every required system library is present. No sudo is used. - if: steps.evidence.outputs.reused != 'true' && (runner.os == 'Linux') && (runner.environment == 'self-hosted') - working-directory: packages/app - run: | - browser="$(node -e "process.stdout.write(require('@playwright/test').chromium.executablePath())")" - if ! "$browser" --version; then - echo "::error::Chromium cannot start on this self-hosted runner. Provision the Playwright Chromium system dependencies on the host; CI jobs run without sudo." - exit 1 - fi - - name: Verify DAG artifact storage on Windows if: steps.evidence.outputs.reused != 'true' && runner.os == 'Windows' working-directory: packages/opencode diff --git a/.github/workflows/ci-typecheck.yml b/.github/workflows/ci-typecheck.yml index 45a72882e..8712d25ab 100644 --- a/.github/workflows/ci-typecheck.yml +++ b/.github/workflows/ci-typecheck.yml @@ -9,12 +9,7 @@ # state-machine and persistence changes before they merge to main. # Notes : No push trigger on feat/* or fix/* (frequent changes); PRs cover # them. -# Trust : push, manual dispatch and same-repository pull requests use the -# repository self-hosted Linux runner; fork pull requests and -# dependabot keep GitHub-hosted runners. Routing is exposure -# reduction, not a security boundary (a fork PR runs its own copy of -# this workflow), so the self-hosted services stay stopped until the -# repository admission policy is accepted. +# Runners : All events use GitHub-hosted Ubuntu runners. # ============================================================================ name: ๐Ÿ” CI ยท Typecheck @@ -37,9 +32,7 @@ permissions: jobs: typecheck: name: Typecheck - # Trusted events only; GitHub-hosted remains the fallback for fork pull - # requests and dependabot. - runs-on: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.user.login != 'dependabot[bot]' && github.actor != 'dependabot[bot]')) && fromJSON('["self-hosted","Linux","X64"]') || 'ubuntu-latest' }} + runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 @@ -56,7 +49,7 @@ jobs: with: check: typecheck job-name: Typecheck - runner-label: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.user.login != 'dependabot[bot]' && github.actor != 'dependabot[bot]')) && 'self-hosted,Linux,X64' || 'ubuntu-latest' }} + runner-label: ubuntu-latest - name: Setup Bun if: steps.evidence.outputs.reused != 'true' diff --git a/.github/workflows/release-fork.yml b/.github/workflows/release-fork.yml index 7cc16f00c..ea4f16f61 100644 --- a/.github/workflows/release-fork.yml +++ b/.github/workflows/release-fork.yml @@ -63,7 +63,7 @@ jobs: version: name: Resolve GraphAgent Version if: github.event_name == 'workflow_dispatch' - runs-on: [self-hosted, Linux, X64] + runs-on: ubuntu-latest timeout-minutes: 10 outputs: channel: ${{ steps.release-version.outputs.channel }} @@ -107,7 +107,7 @@ jobs: package-templates: name: Package Reference Templates if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' - runs-on: [self-hosted, Linux, X64] + runs-on: ubuntu-latest timeout-minutes: 30 permissions: contents: read @@ -178,11 +178,11 @@ jobs: fail-fast: false matrix: include: - - runner: [self-hosted, Linux, X64] + - runner: ubuntu-latest name: linux - runner: macos-latest name: macos - - runner: [self-hosted, Windows, X64] + - runner: windows-latest name: windows runs-on: ${{ matrix.runner }} timeout-minutes: 45 @@ -316,7 +316,7 @@ jobs: name: Prepare Release Candidate needs: [version, build-cli, package-templates] if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' - runs-on: [self-hosted, Linux, X64] + runs-on: ubuntu-latest timeout-minutes: 15 steps: - name: Checkout Repository @@ -415,7 +415,7 @@ jobs: name: Create GitHub Release needs: [version, prepare-release] if: inputs.create_release && github.ref == 'refs/heads/main' - runs-on: [self-hosted, Linux, X64] + runs-on: ubuntu-latest timeout-minutes: 10 permissions: contents: write @@ -487,7 +487,7 @@ jobs: register: name: Register Workflow if: github.event_name == 'push' - runs-on: [self-hosted, Linux, X64] + runs-on: ubuntu-latest timeout-minutes: 5 steps: - name: Register Workflow diff --git a/.github/workflows/runner-smoke.yml b/.github/workflows/runner-smoke.yml index 90b2d40ed..4bbd7bc3d 100644 --- a/.github/workflows/runner-smoke.yml +++ b/.github/workflows/runner-smoke.yml @@ -1,10 +1,9 @@ # ============================================================================ # Runner smoke # ---------------------------------------------------------------------------- -# Purpose : Dispatch-only probe for the repository-scoped self-hosted runners -# on 192.168.34.92 (Ubuntu) and 192.168.34.93 (Windows) +# Purpose : Dispatch-only probe for GitHub-hosted runners # Trigger : Manual `workflow_dispatch` with a required linux/windows choice -# Jobs : exactly one platform job per dispatch, standard self-hosted labels +# Jobs : exactly one platform job per dispatch, standard GitHub-hosted labels # Notes : no checkout, no third-party actions, no secrets and no token; each # job is bounded to 10 minutes and reports runner/account identity # and installed tool versions only. @@ -16,7 +15,7 @@ on: workflow_dispatch: inputs: platform: - description: Platform to exercise on its self-hosted runner + description: Platform to exercise on its GitHub-hosted runner required: true type: choice options: @@ -34,7 +33,7 @@ jobs: smoke-linux: name: Runner smoke (linux) if: inputs.platform == 'linux' - runs-on: [self-hosted, Linux, X64] + runs-on: ubuntu-latest timeout-minutes: 10 permissions: {} steps: @@ -71,7 +70,7 @@ jobs: smoke-windows: name: Runner smoke (windows) if: inputs.platform == 'windows' - runs-on: [self-hosted, Windows, X64] + runs-on: windows-latest timeout-minutes: 10 permissions: {} steps: diff --git a/script/ci-runner-routing.test.mjs b/script/ci-runner-routing.test.mjs index fa4ede0d8..db272ca83 100644 --- a/script/ci-runner-routing.test.mjs +++ b/script/ci-runner-routing.test.mjs @@ -2,15 +2,6 @@ import { test } from "node:test" import assert from "node:assert/strict" import { readFileSync } from "node:fs" -// The one trusted-event predicate that gates every self-hosted route: pushes, -// manual dispatches, and pull requests whose head branch lives in this -// repository. Fork pull requests and dependabot keep GitHub-hosted runners. -// This routing reduces exposure only; a fork pull request runs its own copy of -// the workflow, so the self-hosted services stay stopped until the repository -// admission policy is accepted. -const TRUSTED = - "(github.event_name == 'push' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.user.login != 'dependabot[bot]' && github.actor != 'dependabot[bot]'))" - const workflow = (name) => readFileSync(new URL(`../.github/workflows/${name}`, import.meta.url), "utf8") const repositoryFile = (name) => readFileSync(new URL(`../${name}`, import.meta.url), "utf8") @@ -26,58 +17,35 @@ const namedSteps = (source, name) => { return steps } -await test("self-hosted routes are gated by the trusted-event predicate and keep a hosted fallback", () => { +await test("active CI routes use GitHub-hosted runners and evidence labels", () => { for (const file of ["ci-typecheck.yml", "ci-test.yml"]) { const lines = workflow(file).split("\n") const routes = lines.filter((line) => line.trimStart().startsWith("runs-on:")) - const selfHostedRoutes = routes.filter( - (line) => line.includes("self-hosted") || line.includes("matrix.settings.selfHosted"), - ) - assert(selfHostedRoutes.length > 0, `${file}: no self-hosted route`) - for (const line of selfHostedRoutes) { - assert(line.includes(TRUSTED), `${file}: ungated self-hosted route\n${line.trim()}`) - assert( - line.includes("'ubuntu-latest'") || line.includes("matrix.settings.host"), - `${file}: self-hosted route lost its hosted fallback\n${line.trim()}`, - ) - } const labels = lines.filter((line) => line.trimStart().startsWith("runner-label:")) assert.equal(labels.length, routes.length, `${file}: one evidence label per route`) - for (const line of labels) { - assert(line.includes(TRUSTED), `${file}: ungated evidence label\n${line.trim()}`) - assert( - line.includes("self-hosted") || line.includes("matrix.settings.selfHosted"), - `${file}: evidence label lacks the self-hosted platform\n${line.trim()}`, - ) - } + assert(!lines.some((line) => line.includes("self-hosted")), `${file}: self-hosted route remains`) + assert(!lines.some((line) => line.includes("TRUSTED")), `${file}: trusted-event routing remains`) } }) -await test("the typecheck job selects the standard self-hosted Linux labels only when trusted", () => { +await test("the typecheck job and evidence check use the same hosted Linux label", () => { const typecheck = workflow("ci-typecheck.yml") - assert( - typecheck.includes(`runs-on: \${{ ${TRUSTED} && fromJSON('["self-hosted","Linux","X64"]') || 'ubuntu-latest' }}`), - "typecheck route", - ) - assert( - typecheck.includes(`runner-label: \${{ ${TRUSTED} && 'self-hosted,Linux,X64' || 'ubuntu-latest' }}`), - "typecheck evidence label", - ) + assert(typecheck.includes("runs-on: ubuntu-latest"), "typecheck hosted route") + assert(typecheck.includes("runner-label: ubuntu-latest"), "typecheck evidence label") }) -await test("the test matrix keeps names and OS keys while carrying standard self-hosted label arrays", () => { +await test("the test matrix keeps names and OS keys while routing to its hosted label", () => { const file = workflow("ci-test.yml") - assert(file.includes(`runs-on: \${{ ${TRUSTED} && matrix.settings.selfHosted || matrix.settings.host }}`)) + assert(file.includes("runs-on: \${{ matrix.settings.host }}")) for (const line of [ - `runner-label: \${{ ${TRUSTED} && join(matrix.settings.selfHosted, ',') || matrix.settings.host }}`, - "selfHosted: [self-hosted, Linux, X64]", - "selfHosted: [self-hosted, Windows, X64]", + "runner-label: \${{ matrix.settings.host }}", + "host: ubuntu-latest", + "host: windows-latest", "name: Unit Tests (${{ matrix.settings.name }})", "name: E2E Tests (${{ matrix.settings.name }})", ]) assert(file.includes(line), line) - assert.equal(file.split("selfHosted: [self-hosted, Linux, X64]").length - 1, 2, "linux matrix rows") - assert.equal(file.split("selfHosted: [self-hosted, Windows, X64]").length - 1, 1, "windows matrix row") + assert(!file.includes("selfHosted:"), "self-hosted matrix labels remain") }) await test("every main PR runs the full Linux unit and Linux/Windows E2E gates", () => { @@ -93,50 +61,39 @@ await test("every main PR runs the full Linux unit and Linux/Windows E2E gates", const unit = file.slice(file.indexOf("\n unit-tests:"), file.indexOf("\n e2e-tests:")) const e2e = file.slice(file.indexOf("\n e2e-tests:")) assert(unit.includes("name: Unit Tests (${{ matrix.settings.name }})"), "Linux unit status check") - for (const step of ["GITHUB_ACTIONS=false bun turbo test", "go test ./...", "bun run test:httpapi:ci"]) + for (const step of [ + "GITHUB_ACTIONS=false bun turbo test --concurrency=1", + "go test ./...", + "bun run test:httpapi:ci", + ]) assert(unit.includes(step), `unit gate: ${step}`) assert(!/^ if:/m.test(e2e.slice(0, e2e.indexOf(" strategy:"))), "E2E job must run on every triggered PR") - for (const name of ["linux", "windows"]) - assert(e2e.includes(`- name: ${name}\n`), `${name} E2E matrix entry`) + for (const name of ["linux", "windows"]) assert(e2e.includes(`- name: ${name}\n`), `${name} E2E matrix entry`) assert(e2e.includes("run: bun --cwd packages/app test:e2e:local"), "Playwright E2E gate") }) -await test("self-hosted Linux jobs never require sudo: prerequisites are checked and fail actionably", () => { +await test("hosted Linux jobs install missing prerequisites and Playwright dependencies", () => { const file = workflow("ci-test.yml") - assert(!file.includes("run: sudo apt-get"), "a step still installs with sudo directly") - assert(file.includes('if [ "$RUNNER_ENVIRONMENT" = "self-hosted" ]; then'), "ripgrep self-hosted branch") - assert( - file.includes( - "::error::ripgrep (rg) is required but missing on this self-hosted runner. Provision it on the host; CI jobs run without sudo.", - ), - "ripgrep actionable failure", - ) - assert( - file.includes("(runner.environment == 'github-hosted')"), - "Playwright system dependencies install on hosted runners only", - ) + assert(file.includes("if command -v rg >/dev/null 2>&1; then"), "ripgrep prerequisite check") + assert(file.includes("sudo apt-get update && sudo apt-get install -y ripgrep"), "hosted ripgrep install") + assert(!file.includes("RUNNER_ENVIRONMENT"), "self-hosted prerequisite branch remains") assert( - file.includes("(runner.environment == 'self-hosted')"), - "Playwright Chromium verification runs on self-hosted runners", - ) - assert( - file.includes( - "::error::Chromium cannot start on this self-hosted runner. Provision the Playwright Chromium system dependencies on the host; CI jobs run without sudo.", - ), - "Playwright actionable failure", + file.includes("steps.evidence.outputs.reused != 'true' && (runner.os == 'Linux')"), + "Linux Playwright dependencies are installed", ) + assert(!file.includes("runner.environment == 'self-hosted'"), "self-hosted Chromium check remains") }) -await test("manual release routes trusted Linux and Windows work while keeping macOS hosted", () => { +await test("manual release routes every platform to GitHub-hosted runners", () => { const file = workflow("release-fork.yml") assert(!file.includes("pull_request:"), "release workflow must not accept pull request code") assert(file.includes("if: github.event_name == 'workflow_dispatch'"), "release work is not dispatch-gated") - assert(file.includes("runner: [self-hosted, Linux, X64]"), "Linux build route") - assert(file.includes("runner: [self-hosted, Windows, X64]"), "Windows build route") - assert(file.includes("runner: macos-latest"), "macOS hosted route") - assert.equal(file.split("runs-on: [self-hosted, Linux, X64]").length - 1, 5, "trusted Linux jobs") - assert(!file.includes("runs-on: ubuntu-latest"), "trusted Linux job left on a hosted runner") + assert(file.includes("runner: ubuntu-latest"), "Linux build route") + assert(file.includes("runner: windows-latest"), "Windows build route") + assert(file.includes("runner: macos-latest"), "macOS build route") + assert.equal(file.split("runs-on: ubuntu-latest").length - 1, 5, "Linux jobs use hosted runners") + assert(!file.includes("self-hosted"), "self-hosted release route remains") }) await test("manual releases reject non-main refs and publish stable artifacts only", () => { @@ -263,7 +220,10 @@ await test("release publication uses an anonymous fail-closed Linux bootstrap", assert(publishSetup, "publish bootstrap missing") assert(publishSetup.includes('GH_CLI_VERSION: "2.101.0"'), "inline version pin") - assert(publishSetup.includes("9bca2d1c16825f109907a23307628a2f0698fbf99662b73a5cf0b020293072b8"), "inline Linux SHA pin") + assert( + publishSetup.includes("9bca2d1c16825f109907a23307628a2f0698fbf99662b73a5cf0b020293072b8"), + "inline Linux SHA pin", + ) assert(publishSetup.includes('"${RUNNER_OS:-}" != "Linux"'), "inline OS rejection") assert(publishSetup.includes('"${RUNNER_ARCH:-}" != "X64"'), "inline architecture rejection") assert(!publishSetup.includes("GH_TOKEN"), "bootstrap step must not receive GH_TOKEN") diff --git a/script/ci-runner-smoke.test.mjs b/script/ci-runner-smoke.test.mjs index 7bc4ffa35..53efaacb8 100644 --- a/script/ci-runner-smoke.test.mjs +++ b/script/ci-runner-smoke.test.mjs @@ -2,7 +2,7 @@ import { test } from "node:test" import assert from "node:assert/strict" import { readFileSync } from "node:fs" -// The dispatch-only probe for the repository-scoped self-hosted runners. It +// The dispatch-only probe for GitHub-hosted runners. It // must never execute repository code, request a token, run on macOS or expose // anything beyond runner attribution, so every property below is asserted // against the workflow text directly (no YAML dependency). @@ -29,18 +29,10 @@ await test("runner smoke is dispatch-only with a required linux/windows choice", assert(!trigger.includes(forbidden), `forbidden trigger: ${forbidden}`) }) -await test("runner smoke selects exactly one standard self-hosted platform per dispatch", () => { +await test("runner smoke selects exactly one GitHub-hosted platform per dispatch", () => { const routes = workflow.split("\n").filter((line) => line.includes("runs-on:")) assert.equal(routes.length, 2, "one route per platform") - assert( - routes.some((line) => line.includes("[self-hosted, Linux, X64]")), - "linux labels", - ) - assert( - routes.some((line) => line.includes("[self-hosted, Windows, X64]")), - "windows labels", - ) - for (const line of routes) assert(!/macos|darwin|arm64/i.test(line), `macOS route: ${line.trim()}`) + assert.deepEqual(routes.map((line) => line.trim()).sort(), ["runs-on: ubuntu-latest", "runs-on: windows-latest"]) assert(workflow.includes("if: inputs.platform == 'linux'"), "linux guard") assert(workflow.includes("if: inputs.platform == 'windows'"), "windows guard") })