diff --git a/.github/workflows/check-test.yml b/.github/workflows/check-test.yml index 71e1b4f83ed..da054adff7f 100644 --- a/.github/workflows/check-test.yml +++ b/.github/workflows/check-test.yml @@ -1,5 +1,8 @@ name: check-test +# Pull requests run the fast tier: the affected unit tests on Linux. The full tier — unit tests on Windows, the +# compiled builds, and the Playwright suites on both OSes — runs on pushes to dev and beta, on nightly and manual +# runs, and as the gate release.yml calls before it publishes a stable release. on: push: branches: @@ -13,6 +16,12 @@ on: description: Run only the quarantined browser tests (packages/app/e2e/QUARANTINE.md) type: boolean default: false + workflow_call: + inputs: + quarantine: + description: Run only the quarantined browser tests (packages/app/e2e/QUARANTINE.md) + type: boolean + default: false concurrency: # A new push to a pull request cancels its stale run. Every push and manual run keeps its own group, so each @@ -26,11 +35,18 @@ permissions: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + # Push and pull request runs test only what changed. Manual runs and the release gate test everything: they + # have no base commit to diff against. + AFFECTED: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }} + # Pull requests diff against the merge's first parent; pushes diff against the previous dev head. + TURBO_SCM_BASE: ${{ github.event_name == 'pull_request' && format('{0}^1', github.sha) || github.event.before }} + TURBO_SCM_HEAD: ${{ github.sha }} jobs: + # Only the browser suites need this, and pull requests run no browser suites. affected: name: affected packages - if: ${{ !github.event.pull_request.draft }} + if: ${{ !github.event.pull_request.draft && github.event_name != 'pull_request' }} runs-on: blacksmith-4vcpu-ubuntu-2404 outputs: e2e: ${{ steps.packages.outputs.app }} @@ -39,8 +55,7 @@ jobs: uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: token: ${{ secrets.GITHUB_TOKEN }} - # Pull requests diff against the merge's first parent; pushes diff against the previous dev head. - fetch-depth: ${{ github.event_name == 'pull_request' && 2 || 0 }} + fetch-depth: 0 - name: Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 @@ -49,11 +64,8 @@ jobs: - name: Find affected packages id: packages - env: - TURBO_SCM_BASE: ${{ github.event_name == 'pull_request' && format('{0}^1', github.sha) || github.event.before }} - TURBO_SCM_HEAD: ${{ github.sha }} run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + if [ "$AFFECTED" != "true" ]; then echo "app=true" >> "$GITHUB_OUTPUT" exit 0 fi @@ -67,11 +79,8 @@ jobs: strategy: fail-fast: false matrix: - settings: - - name: linux - host: blacksmith-4vcpu-ubuntu-2404 - - name: windows - host: blacksmith-4vcpu-windows-2025 + # Pull requests run Linux only; Windows runs the same tests on pushes, nightly runs and the release gate. + settings: ${{ fromJSON(github.event_name == 'pull_request' && '[{"name":"linux","host":"blacksmith-4vcpu-ubuntu-2404"}]' || '[{"name":"linux","host":"blacksmith-4vcpu-ubuntu-2404"},{"name":"windows","host":"blacksmith-4vcpu-windows-2025"}]') }} runs-on: ${{ matrix.settings.host }} defaults: run: @@ -109,15 +118,13 @@ jobs: timeout-minutes: 20 run: | # The runners have four vCPUs, and each Bun test process performs its own concurrent work. - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - GITHUB_ACTIONS=false bun turbo test --concurrency=3 + if [ "$AFFECTED" = "true" ]; then + GITHUB_ACTIONS=false bun turbo test --affected --concurrency=3 exit 0 fi - GITHUB_ACTIONS=false bun turbo test --affected --concurrency=3 + GITHUB_ACTIONS=false bun turbo test --concurrency=3 env: OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }} - TURBO_SCM_BASE: ${{ github.event_name == 'pull_request' && format('{0}^1', github.sha) || github.event.before }} - TURBO_SCM_HEAD: ${{ github.sha }} - name: Verify packed codemode package if: runner.os == 'Linux' @@ -128,14 +135,11 @@ jobs: if: runner.os == 'Linux' timeout-minutes: 15 run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - bun turbo verify:package --filter=@opencode/sdk + if [ "$AFFECTED" = "true" ]; then + bun turbo verify:package --affected --filter=@opencode/sdk exit 0 fi - bun turbo verify:package --affected --filter=@opencode/sdk - env: - TURBO_SCM_BASE: ${{ github.event_name == 'pull_request' && format('{0}^1', github.sha) || github.event.before }} - TURBO_SCM_HEAD: ${{ github.sha }} + bun turbo verify:package --filter=@opencode/sdk - name: Check generated client if: runner.os == 'Linux' @@ -149,7 +153,8 @@ jobs: build: name: build (${{ matrix.settings.name }}) - if: ${{ !inputs.quarantine && !github.event.pull_request.draft }} + # Compiling the CLI and smoke-testing its service lifecycle is part of the full tier. + if: ${{ !inputs.quarantine && github.event_name != 'pull_request' && !github.event.pull_request.draft }} strategy: fail-fast: false matrix: @@ -167,8 +172,7 @@ jobs: uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: token: ${{ secrets.GITHUB_TOKEN }} - # Pull requests diff against the merge's first parent; pushes diff against the previous dev head. - fetch-depth: ${{ github.event_name == 'pull_request' && 2 || 0 }} + fetch-depth: 0 - name: Setup Bun uses: ./.github/actions/setup-bun @@ -189,14 +193,11 @@ jobs: if: ${{ !cancelled() && runner.os == 'Linux' }} timeout-minutes: 10 run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - bun turbo build --concurrency=3 --filter='@opencode/cli^...' --filter='@opencode/app^...' + if [ "$AFFECTED" = "true" ]; then + bun turbo build --affected --concurrency=3 --filter='@opencode/cli^...' --filter='@opencode/app^...' exit 0 fi - bun turbo build --affected --concurrency=3 --filter='@opencode/cli^...' --filter='@opencode/app^...' - env: - TURBO_SCM_BASE: ${{ github.event_name == 'pull_request' && format('{0}^1', github.sha) || github.event.before }} - TURBO_SCM_HEAD: ${{ github.sha }} + bun turbo build --concurrency=3 --filter='@opencode/cli^...' --filter='@opencode/app^...' - name: Setup Node build runtime if: ${{ !cancelled() }} @@ -223,8 +224,12 @@ jobs: strategy: fail-fast: false matrix: - # Beta PRs gate on Linux; Windows runs on pushes and manual runs (nightly-beta.yml dispatches beta). - settings: ${{ fromJSON(github.base_ref == 'beta' && '[{"name":"linux","host":"blacksmith-4vcpu-ubuntu-2404"}]' || '[{"name":"linux","host":"blacksmith-4vcpu-ubuntu-2404"},{"name":"windows","host":"blacksmith-4vcpu-windows-2025"}]') }} + # Browser suites only run on pushes, nightly runs and the release gate, so every run covers both OSes. + settings: + - name: linux + host: blacksmith-4vcpu-ubuntu-2404 + - name: windows + host: blacksmith-4vcpu-windows-2025 quarantine: ${{ fromJSON(inputs.quarantine && '[true]' || '[false]') }} # Playwright splits whole spec files across shards by test count, not duration. Every app-N # entry must use the same total, and the entries must cover 1..total. The service worker suite @@ -242,7 +247,7 @@ jobs: PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.playwright-browsers # Beta only (packages/app/e2e/QUARANTINE.md): gate legs skip quarantined tests, retry once and stop after 8 # failures; quarantine legs run only those tests, to the end. Other branches keep the config defaults. - PLAYWRIGHT_ARGS: ${{ matrix.quarantine && '--grep @quarantine --pass-with-no-tests --retries=1' || (github.base_ref == 'beta' || github.ref == 'refs/heads/beta') && '--grep-invert @quarantine --max-failures=8 --retries=1' || '' }} + PLAYWRIGHT_ARGS: ${{ matrix.quarantine && '--grep @quarantine --pass-with-no-tests --retries=1' || github.ref == 'refs/heads/beta' && '--grep-invert @quarantine --max-failures=8 --retries=1' || '' }} defaults: run: shell: bash @@ -289,7 +294,7 @@ jobs: run: bun --cwd packages/app test:e2e:built --shard=${{ matrix.suite.shard }} ${{ env.PLAYWRIGHT_ARGS }} env: CI: true - timeout-minutes: ${{ (matrix.quarantine || github.base_ref == 'beta' || github.ref == 'refs/heads/beta') && 10 || 15 }} + timeout-minutes: ${{ (matrix.quarantine || github.ref == 'refs/heads/beta') && 10 || 15 }} - name: Verify service worker precaching and upgrades if: matrix.suite.shard == '2/2' && !matrix.quarantine @@ -302,14 +307,14 @@ jobs: run: bun --cwd packages/session-ui test:components ${{ env.PLAYWRIGHT_ARGS }} env: CI: true - timeout-minutes: ${{ (matrix.quarantine || github.base_ref == 'beta' || github.ref == 'refs/heads/beta') && 10 || 15 }} + timeout-minutes: ${{ (matrix.quarantine || github.ref == 'refs/heads/beta') && 10 || 15 }} - name: Run app component tests if: matrix.suite.id == 'app-components' run: bun --cwd packages/app test:components ${{ env.PLAYWRIGHT_ARGS }} env: CI: true - timeout-minutes: ${{ (matrix.quarantine || github.base_ref == 'beta' || github.ref == 'refs/heads/beta') && 10 || 15 }} + timeout-minutes: ${{ (matrix.quarantine || github.ref == 'refs/heads/beta') && 10 || 15 }} - name: Upload Playwright artifacts if: always() @@ -326,17 +331,18 @@ jobs: packages/app/component-tests/test-results packages/app/component-tests/playwright-report - # Branch rules require one e2e check per OS. These keep those names and pass only when every - # leg above passed, or when no leg was needed; a failing leg on either OS fails both checks. + # One check per OS for the browser suites: it passes only when every leg above passed, or when no leg was + # needed because the changed packages have no browser tests. e2e: name: e2e (${{ matrix.os }}) needs: [affected, e2e-suite] - if: ${{ !cancelled() && !inputs.quarantine && !github.event.pull_request.draft }} + if: ${{ !cancelled() && !inputs.quarantine && github.event_name != 'pull_request' && !github.event.pull_request.draft }} strategy: fail-fast: false matrix: os: [linux, windows] - runs-on: blacksmith-4vcpu-ubuntu-2404 + # Aggregates results only; these do not need a paid runner. + runs-on: ubuntu-24.04 steps: - name: Check browser test results env: @@ -344,9 +350,5 @@ jobs: SUITES: ${{ needs.e2e-suite.result }} EXPECTED: ${{ needs.affected.outputs.e2e == 'true' && 'success' || 'skipped' }} run: | - if [ "${{ matrix.os }}" = windows ] && [ "${{ github.base_ref }}" = beta ]; then - echo "Windows browser tests run on beta pushes and nightly, not on beta pull requests" - exit 0 - fi echo "affected packages: $AFFECTED, browser test legs: $SUITES (expected $EXPECTED)" test "$AFFECTED" = success && test "$SUITES" = "$EXPECTED" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f3787ea643b..3321fda7c8e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -79,6 +79,20 @@ jobs: INPUT_VERSION: ${{ inputs.version }} INPUT_RELEASE_NOTES: ${{ inputs.release_notes }} + # ---- Gate: the full check suite, before anything is published ------------------------------------------------ + + # release.yml does not wait for the check-test push run on the same commit, so a stable release would publish + # whatever that run later says. This gate runs the whole suite — unit tests on both OSes, the compiled builds + # and the browser suites — before any publish job below. Continuous dev and beta builds skip it: they publish + # their own channel from a dev push, whose check-test run covers the same commit. + verify: + needs: plan + if: needs.plan.outputs.channel == 'latest' + permissions: + contents: read + checks: write + uses: ./.github/workflows/check-test.yml + # A versioned release builds from a commit with its versions and changelog on the dispatched dev commit, # tagged v. Runs git only. tag: @@ -585,7 +599,7 @@ jobs: # Build provenance for the npm tarballs and Linux archives. No secrets; id-token only for Sigstore. publish-attestations: - needs: [plan, build-npm] + needs: [plan, build-npm, verify] if: ${{ !cancelled() && !failure() && needs.plan.outputs.cli == 'true' && needs.plan.outputs.dry_run != 'true' }} runs-on: ubuntu-24.04 permissions: @@ -606,7 +620,7 @@ jobs: ${{ runner.temp }}/in/build-archives/opencode* publish-npm: - needs: [plan, sign-macos, sign-windows, publish-attestations] + needs: [plan, sign-macos, sign-windows, publish-attestations, verify] if: ${{ !cancelled() && !failure() && needs.plan.outputs.cli == 'true' }} runs-on: ubuntu-24.04 environment: ${{ needs.plan.outputs.dry_run != 'true' && 'release' || '' }} @@ -633,7 +647,7 @@ jobs: GH_TOKEN: ${{ github.token }} publish-cli: - needs: [plan, sign-macos, sign-windows, publish-npm] + needs: [plan, sign-macos, sign-windows, publish-npm, verify] if: ${{ !cancelled() && !failure() && needs.plan.outputs.cli == 'true' }} runs-on: ubuntu-24.04 environment: ${{ needs.plan.outputs.dry_run != 'true' && 'release' || '' }} @@ -664,7 +678,7 @@ jobs: CLOUDFLARE_API_TOKEN: ${{ steps.cloudflare-token.outputs.token }} publish-desktop: - needs: [plan, sign-macos, sign-desktop-windows-installer] + needs: [plan, sign-macos, sign-desktop-windows-installer, verify] if: ${{ !cancelled() && !failure() && needs.plan.outputs.desktop == 'true' }} runs-on: ubuntu-24.04 environment: ${{ needs.plan.outputs.dry_run != 'true' && 'release' || '' }} @@ -699,7 +713,7 @@ jobs: env: *cloudflare publish-docker: - needs: [plan, build-docker] + needs: [plan, build-docker, verify] if: ${{ !cancelled() && !failure() && needs.plan.outputs.release == 'true' }} runs-on: ubuntu-24.04 environment: ${{ needs.plan.outputs.dry_run != 'true' && 'release' || '' }} @@ -719,7 +733,7 @@ jobs: GH_TOKEN: ${{ github.token }} publish-homebrew: - needs: [plan, sign-macos, sign-windows, publish-cli] + needs: [plan, sign-macos, sign-windows, publish-cli, verify] if: ${{ !cancelled() && !failure() && needs.plan.outputs.release == 'true' }} runs-on: ubuntu-24.04 environment: ${{ needs.plan.outputs.dry_run != 'true' && 'release' || '' }} @@ -748,7 +762,7 @@ jobs: # The AUR package installs the published npm tarballs, so it follows the npm publish. publish-aur: - needs: [plan, sign-macos, sign-windows, publish-npm] + needs: [plan, sign-macos, sign-windows, publish-npm, verify] if: ${{ !cancelled() && !failure() && needs.plan.outputs.release == 'true' }} runs-on: ubuntu-24.04 environment: ${{ needs.plan.outputs.dry_run != 'true' && 'release' || '' }} @@ -777,7 +791,7 @@ jobs: # WinGet manifests as data. Submission to microsoft/winget-pkgs stays paused until the packages are accepted. publish-winget: - needs: [plan, sign-windows, sign-desktop-windows-installer] + needs: [plan, sign-windows, sign-desktop-windows-installer, verify] if: ${{ !cancelled() && !failure() && needs.plan.outputs.release == 'true' }} runs-on: ubuntu-24.04 env: *env @@ -797,7 +811,7 @@ jobs: path: ${{ runner.temp }}/out publish-github-release: - needs: [plan, sign-macos, sign-windows, sign-desktop-windows-installer, publish-cli, publish-desktop] + needs: [plan, sign-macos, sign-windows, sign-desktop-windows-installer, publish-cli, publish-desktop, verify] if: ${{ !cancelled() && !failure() && needs.plan.outputs.release == 'true' }} runs-on: ubuntu-24.04 environment: ${{ needs.plan.outputs.dry_run != 'true' && 'release' || '' }} @@ -821,7 +835,7 @@ jobs: OPENCODE_RELEASE_NOTES: ${{ inputs.release_notes }} publish-discord: - needs: [plan, publish-github-release] + needs: [plan, publish-github-release, verify] if: ${{ !cancelled() && !failure() && needs.plan.outputs.release == 'true' }} runs-on: ubuntu-24.04 environment: ${{ needs.plan.outputs.dry_run != 'true' && 'release' || '' }} @@ -851,7 +865,7 @@ jobs: # Brings the release commit back to dev. Pushing to dev is blocked by rulesets, so it opens a PR. publish-version-pr: - needs: [plan, tag, publish-npm, publish-cli, publish-desktop, publish-github-release] + needs: [plan, tag, publish-npm, publish-cli, publish-desktop, publish-github-release, verify] if: ${{ !cancelled() && !failure() && needs.plan.outputs.channel == 'latest' }} runs-on: ubuntu-24.04 permissions: