diff --git a/.github/scripts/check-required-pr-context.mjs b/.github/scripts/check-required-pr-context.mjs index 270bd975b5b..a0f1bd20906 100644 --- a/.github/scripts/check-required-pr-context.mjs +++ b/.github/scripts/check-required-pr-context.mjs @@ -18,8 +18,8 @@ export function verifyRequiredPrContext(parseYaml) { } const gate = providers[0].job const condition = gate.if?.replace(/^\$\{\{\s*|\s*\}\}$/g, '').trim() - if (condition !== '!cancelled()') { - throw new Error('Required PR aggregate must run unless the workflow is cancelled.') + if (condition !== '!cancelled() && github.event.pull_request.draft != true') { + throw new Error('Required PR aggregate must run unless cancelled or the PR is a draft.') } if (gate['continue-on-error'] || gate.steps?.some((step) => step['continue-on-error'])) { throw new Error('Required PR aggregate must not ignore failures.') diff --git a/.github/workflows/ci-cache-warmup-platforms.yml b/.github/workflows/ci-cache-warmup-platforms.yml index 11b96179bbd..06ba3eaeb3b 100644 --- a/.github/workflows/ci-cache-warmup-platforms.yml +++ b/.github/workflows/ci-cache-warmup-platforms.yml @@ -45,6 +45,7 @@ on: - 'config/scripts/windows-rust-executable-build.mjs' - 'resources/build/icon.ico' pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] paths: - '.github/workflows/ci-cache-warmup-platforms.yml' - '.github/workflows/ci-cache-warmup-platform-jobs.yml' @@ -58,4 +59,5 @@ concurrency: jobs: warm-platforms: + if: github.event_name != 'pull_request' || github.event.pull_request.draft != true uses: ./.github/workflows/ci-cache-warmup-platform-jobs.yml diff --git a/.github/workflows/ci-cache-warmup.yml b/.github/workflows/ci-cache-warmup.yml index 9310d87a133..e360f5b02e8 100644 --- a/.github/workflows/ci-cache-warmup.yml +++ b/.github/workflows/ci-cache-warmup.yml @@ -48,6 +48,7 @@ on: - 'native/windows-registry/**' - 'native/windows-security/**' pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] paths: - '.github/workflows/ci-cache-warmup.yml' - '.github/actions/prepare-headless-compiler/**' @@ -63,6 +64,7 @@ concurrency: jobs: warm: + if: github.event_name != 'pull_request' || github.event.pull_request.draft != true concurrency: group: ci-cache-warmup-linux-x64-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: ${{ github.event_name != 'schedule' }} diff --git a/.github/workflows/ci-pnpm-verification-pilot.yml b/.github/workflows/ci-pnpm-verification-pilot.yml index 2a35a52fcd0..bbbc989c720 100644 --- a/.github/workflows/ci-pnpm-verification-pilot.yml +++ b/.github/workflows/ci-pnpm-verification-pilot.yml @@ -2,6 +2,7 @@ name: pnpm verification cache pilot on: pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] paths: ['.github/workflows/ci-pnpm-verification-pilot.yml'] workflow_dispatch: @@ -17,6 +18,7 @@ env: jobs: seed: + if: github.event_name != 'pull_request' || github.event.pull_request.draft != true strategy: matrix: runner: [ubuntu-latest, ubuntu-24.04-arm] diff --git a/.github/workflows/cloud-verify.yml b/.github/workflows/cloud-verify.yml index fe4877d47de..c39edcb11a3 100644 --- a/.github/workflows/cloud-verify.yml +++ b/.github/workflows/cloud-verify.yml @@ -2,6 +2,7 @@ name: Cloud Verify on: pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] paths: - cloud/** - src/packages/process-host/** @@ -29,6 +30,7 @@ defaults: jobs: # Public-repository hosted runners preserve Blacksmith allowance for macOS. security: + if: github.event_name != 'pull_request' || github.event.pull_request.draft != true name: Secret scan runs-on: ubuntu-22.04 steps: @@ -79,6 +81,7 @@ jobs: # Compiles the workspace. No Postgres service: nothing here reaches a # database, and the service container costs ~13s of startup. build: + if: github.event_name != 'pull_request' || github.event.pull_request.draft != true runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 @@ -99,6 +102,7 @@ jobs: # packages it needs through pretest hooks, so it does not depend on # `pnpm build` having run. test: + if: github.event_name != 'pull_request' || github.event.pull_request.draft != true runs-on: ubuntu-22.04 services: postgres: @@ -135,6 +139,7 @@ jobs: # Fork pull requests reach this job, so it never configures a backend, never plans, and never # holds a credential. Only the relay root ships here; foundation and apps stay private. terraform: + if: github.event_name != 'pull_request' || github.event.pull_request.draft != true runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/computer-e2e.yml b/.github/workflows/computer-e2e.yml index 23642aeff19..ed88f584678 100644 --- a/.github/workflows/computer-e2e.yml +++ b/.github/workflows/computer-e2e.yml @@ -2,6 +2,7 @@ name: Computer-use e2e on: pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] paths: - '.github/workflows/computer-e2e.yml' - 'config/electron-builder.config.cjs' @@ -65,7 +66,7 @@ concurrency: jobs: native-smoke: - if: github.event_name == 'pull_request' + if: github.event_name == 'pull_request' && github.event.pull_request.draft != true permissions: contents: read strategy: @@ -141,6 +142,7 @@ jobs: # Hosted macOS runners cannot receive persistent Accessibility/Screen Recording # grants. Keep the real native build/owner-loss checks on every trigger instead. mac-native-owner-smoke: + if: github.event_name != 'pull_request' || github.event.pull_request.draft != true runs-on: macos-15 permissions: contents: read diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 7830df9eacf..a0660167955 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -6,6 +6,7 @@ on: release: types: [published] pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] paths: - 'docs/site/**' - '.github/workflows/docs.yml' @@ -36,7 +37,7 @@ jobs: # including forks. check: name: Build and test - if: github.event_name == 'pull_request' + if: github.event_name == 'pull_request' && github.event.pull_request.draft != true runs-on: ubuntu-latest timeout-minutes: 15 steps: diff --git a/.github/workflows/git-command-termination-runtime.yml b/.github/workflows/git-command-termination-runtime.yml index 6b49177ed56..60f82987909 100644 --- a/.github/workflows/git-command-termination-runtime.yml +++ b/.github/workflows/git-command-termination-runtime.yml @@ -1,6 +1,7 @@ name: Git command termination runtime on: pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] paths: - 'src/main/git/command-runner/spawned-command-tree-kill*' - '.github/workflows/git-command-termination-runtime.yml' @@ -12,6 +13,7 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: windows-exit: + if: github.event_name != 'pull_request' || github.event.pull_request.draft != true runs-on: windows-latest timeout-minutes: 20 env: diff --git a/.github/workflows/macos-updater-tests.yml b/.github/workflows/macos-updater-tests.yml index 980998117ca..cf7d149ece8 100644 --- a/.github/workflows/macos-updater-tests.yml +++ b/.github/workflows/macos-updater-tests.yml @@ -2,6 +2,7 @@ name: macOS updater regression tests on: pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] paths: - '.github/workflows/macos-updater-tests.yml' - 'src/main/macos-update-running-instances*' @@ -24,6 +25,7 @@ concurrency: jobs: updater: + if: github.event_name != 'pull_request' || github.event.pull_request.draft != true runs-on: macos-15 timeout-minutes: 20 steps: diff --git a/.github/workflows/mobile.yml b/.github/workflows/mobile.yml index 1ab747aa8ce..21b5c10891f 100644 --- a/.github/workflows/mobile.yml +++ b/.github/workflows/mobile.yml @@ -6,6 +6,8 @@ on: - opened - synchronize - reopened + - ready_for_review + - converted_to_draft paths: - 'mobile/**' # Why all of src/shared: mobile imports hundreds of its modules, directly and through the RPC @@ -94,7 +96,7 @@ concurrency: jobs: verify: name: Mobile verify - if: github.event_name == 'pull_request' + if: github.event_name == 'pull_request' && github.event.pull_request.draft != true # Why ARM: 209s of this job is Vitest and nothing here needs x86: no Android SDK, emulator, gradle, # Hermes or Watchman, no docker, and no artifacts. The Gemfile.lock lists the generic `ruby` # platform, so frozen bundler installs without an aarch64-linux entry. diff --git a/.github/workflows/node-server-tests.yml b/.github/workflows/node-server-tests.yml index f0956770247..6d3552fca7a 100644 --- a/.github/workflows/node-server-tests.yml +++ b/.github/workflows/node-server-tests.yml @@ -2,7 +2,7 @@ name: Headless Node server on: pull_request: - types: [opened, synchronize, reopened, ready_for_review] + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] paths: - 'src/**' - 'config/**' diff --git a/.github/workflows/performance-contracts.yml b/.github/workflows/performance-contracts.yml index 2e3be1f658c..e76ba2981c7 100644 --- a/.github/workflows/performance-contracts.yml +++ b/.github/workflows/performance-contracts.yml @@ -5,6 +5,7 @@ on: - cron: '15 9 * * *' workflow_dispatch: pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] paths: - '.github/workflows/performance-contracts.yml' - 'config/vitest.performance.config.ts' @@ -40,6 +41,7 @@ concurrency: jobs: contracts: + if: github.event_name != 'pull_request' || github.event.pull_request.draft != true strategy: fail-fast: false matrix: diff --git a/.github/workflows/pi-owner-runtime.yml b/.github/workflows/pi-owner-runtime.yml index 17e08fa0940..da85f5b6637 100644 --- a/.github/workflows/pi-owner-runtime.yml +++ b/.github/workflows/pi-owner-runtime.yml @@ -1,6 +1,7 @@ name: Pi owner runtime verification on: pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] paths: - 'src/main/pi/**' - '!src/main/pi/**/*.test.ts' @@ -15,6 +16,7 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: runtime: + if: github.event_name != 'pull_request' || github.event.pull_request.draft != true strategy: fail-fast: false matrix: diff --git a/.github/workflows/pi-provider-runtime.yml b/.github/workflows/pi-provider-runtime.yml index 31046737499..4cde4ab5f69 100644 --- a/.github/workflows/pi-provider-runtime.yml +++ b/.github/workflows/pi-provider-runtime.yml @@ -1,6 +1,7 @@ name: Pi extension provider verification on: pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] paths: - 'src/shared/commit-message-agent-specs-primary.ts' - 'tests/tools/pi-provider-runtime-smoke.mjs' @@ -12,6 +13,7 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: runtime: + if: github.event_name != 'pull_request' || github.event.pull_request.draft != true strategy: fail-fast: false matrix: diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index aa6b78fdd4f..a92de3c7ce4 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -8,6 +8,7 @@ on: - synchronize - reopened - ready_for_review + - converted_to_draft concurrency: group: pr-checks-${{ github.event.pull_request.number }} @@ -24,6 +25,7 @@ jobs: # Per-job outputs also skip git-compat/xterm/packaging/shell when those # inputs are unchanged; empty diffs fail closed and run everything. code_paths: + if: github.event_name != 'pull_request' || github.event.pull_request.draft != true name: detect changes and check repository guards # Start routing without waiting behind the GitHub-hosted test queue. runs-on: blacksmith-2vcpu-ubuntu-2404 @@ -1515,7 +1517,7 @@ jobs: verify: name: PR checks passed # Publish an explicit result after every required dependency settles. - if: ${{ !cancelled() }} + if: ${{ !cancelled() && github.event.pull_request.draft != true }} needs: - code_paths - preflight diff --git a/.github/workflows/preflight-benchmark.yml b/.github/workflows/preflight-benchmark.yml index 44b3617b3eb..2b29d4fa7f8 100644 --- a/.github/workflows/preflight-benchmark.yml +++ b/.github/workflows/preflight-benchmark.yml @@ -1,6 +1,7 @@ name: Preflight comparison on: pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] paths: - .github/workflows/preflight-benchmark.yml - config/scripts/benchmark-preflight-*.mjs @@ -15,6 +16,7 @@ env: ORCA_BACKGROUND_LAUNCH: '1' jobs: compilers: + if: github.event_name != 'pull_request' || github.event.pull_request.draft != true runs-on: ubuntu-24.04-arm timeout-minutes: 25 steps: @@ -37,6 +39,7 @@ jobs: path: .build/preflight-benchmark/ retention-days: 30 static-analysis: + if: github.event_name != 'pull_request' || github.event.pull_request.draft != true runs-on: ubuntu-24.04-arm timeout-minutes: 25 steps: @@ -59,6 +62,7 @@ jobs: path: .build/preflight-benchmark/ retention-days: 30 compatibility-imports: + if: github.event_name != 'pull_request' || github.event.pull_request.draft != true runs-on: ubuntu-24.04-arm timeout-minutes: 10 steps: @@ -82,6 +86,7 @@ jobs: path: .build/preflight-benchmark/ retention-days: 30 localization-extraction: + if: github.event_name != 'pull_request' || github.event.pull_request.draft != true runs-on: ubuntu-24.04-arm timeout-minutes: 12 steps: diff --git a/.github/workflows/preflight-cli-platforms.yml b/.github/workflows/preflight-cli-platforms.yml index bd3c08e14c9..2f77f269092 100644 --- a/.github/workflows/preflight-cli-platforms.yml +++ b/.github/workflows/preflight-cli-platforms.yml @@ -2,6 +2,7 @@ name: Preflight CLI platforms on: pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] paths: - 'src/main/ipc/preflight*' - 'src/main/ipc/agent-detection-shell-path.ts' @@ -22,6 +23,7 @@ concurrency: jobs: preflight: + if: github.event_name != 'pull_request' || github.event.pull_request.draft != true name: Preflight CLI (${{ matrix.os }}) runs-on: ${{ matrix.os }} timeout-minutes: 20 diff --git a/.github/workflows/preflight-critical-path-benchmark.yml b/.github/workflows/preflight-critical-path-benchmark.yml index cfabf00e3cc..7935b302425 100644 --- a/.github/workflows/preflight-critical-path-benchmark.yml +++ b/.github/workflows/preflight-critical-path-benchmark.yml @@ -1,6 +1,7 @@ name: Preflight critical-path comparison on: pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] paths: - .github/workflows/preflight-critical-path-benchmark.yml - config/scripts/benchmark-ci-preflight-critical-path.mjs @@ -14,6 +15,7 @@ env: ORCA_BACKGROUND_LAUNCH: '1' jobs: compare: + if: github.event_name != 'pull_request' || github.event.pull_request.draft != true runs-on: ubuntu-24.04-arm timeout-minutes: 20 strategy: diff --git a/.github/workflows/release-javascript-benchmark.yml b/.github/workflows/release-javascript-benchmark.yml index 6a5fc4efa31..40f7edcb8ad 100644 --- a/.github/workflows/release-javascript-benchmark.yml +++ b/.github/workflows/release-javascript-benchmark.yml @@ -2,6 +2,7 @@ name: Release JavaScript comparison on: pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] paths: ['.github/workflows/release-javascript-benchmark.yml'] workflow_dispatch: @@ -20,6 +21,7 @@ env: jobs: bundle: + if: github.event_name != 'pull_request' || github.event.pull_request.draft != true uses: ./.github/workflows/release-javascript.yml with: ref: ${{ github.sha }} diff --git a/.github/workflows/release-ref-validation.yml b/.github/workflows/release-ref-validation.yml index 665967af0c3..42a55772e53 100644 --- a/.github/workflows/release-ref-validation.yml +++ b/.github/workflows/release-ref-validation.yml @@ -2,6 +2,7 @@ name: Release ref validation on: pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] paths: - '.github/workflows/adhoc-mac-build.yml' - '.github/workflows/dev-channel-win-build.yml' @@ -19,6 +20,7 @@ concurrency: jobs: validate: + if: github.event_name != 'pull_request' || github.event.pull_request.draft != true strategy: fail-fast: false matrix: diff --git a/.github/workflows/skill-update-roundtrip.yml b/.github/workflows/skill-update-roundtrip.yml index 7115ea38e28..6a1e58ca798 100644 --- a/.github/workflows/skill-update-roundtrip.yml +++ b/.github/workflows/skill-update-roundtrip.yml @@ -6,6 +6,7 @@ name: Skill update round trip # run marks the default-branch tip failed, so the repo shows a red X. on: pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] paths: &skill-roundtrip-paths - 'skills/**' - 'resources/skills/**' @@ -28,6 +29,7 @@ concurrency: jobs: roundtrip: + if: github.event_name != 'pull_request' || github.event.pull_request.draft != true strategy: fail-fast: false matrix: diff --git a/.github/workflows/ssh-hostile-hosts.yml b/.github/workflows/ssh-hostile-hosts.yml index b5f55598322..a425906dabe 100644 --- a/.github/workflows/ssh-hostile-hosts.yml +++ b/.github/workflows/ssh-hostile-hosts.yml @@ -7,7 +7,7 @@ name: SSH hostile hosts # or the slot build changes, and on demand. on: pull_request: - types: [opened, synchronize, reopened, ready_for_review] + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] paths: # The managed deploy still runs on these relay-named install and runtime primitives. - 'src/main/ssh/ssh-relay-*' diff --git a/.github/workflows/ssh-windows-hosts.yml b/.github/workflows/ssh-windows-hosts.yml index 7715031fe93..fd5e24dd33e 100644 --- a/.github/workflows/ssh-windows-hosts.yml +++ b/.github/workflows/ssh-windows-hosts.yml @@ -6,7 +6,7 @@ name: SSH Windows hosts # sessions behind logging shims, and deploys orcad on the pinned node.exe in both DefaultShell cases. on: pull_request: - types: [opened, synchronize, reopened, ready_for_review] + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] paths: - 'src/main/ssh/ssh-relay-*' - 'src/main/ssh/*runtime*' diff --git a/.github/workflows/win-orcad-serve-switch-e2e.yml b/.github/workflows/win-orcad-serve-switch-e2e.yml index 2e432308e14..7b8970a95a6 100644 --- a/.github/workflows/win-orcad-serve-switch-e2e.yml +++ b/.github/workflows/win-orcad-serve-switch-e2e.yml @@ -8,7 +8,7 @@ name: Windows packaged orcad serve switch E2E on: pull_request: - types: [opened, synchronize, reopened, ready_for_review] + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] paths: - 'src/main/daemon/**' - '!src/main/daemon/**/*.test.ts' diff --git a/.github/workflows/windows-typecheck-parity.yml b/.github/workflows/windows-typecheck-parity.yml index 0f87096717e..7b15f958ec3 100644 --- a/.github/workflows/windows-typecheck-parity.yml +++ b/.github/workflows/windows-typecheck-parity.yml @@ -2,6 +2,7 @@ name: Windows typecheck parity on: pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] paths: - 'package.json' - 'pnpm-lock.yaml' @@ -25,6 +26,7 @@ concurrency: jobs: parity: + if: github.event_name != 'pull_request' || github.event.pull_request.draft != true name: Windows compiler diagnostics and file selection runs-on: windows-2022 timeout-minutes: 40 diff --git a/.github/workflows/wsl-orcad-e2e.yml b/.github/workflows/wsl-orcad-e2e.yml index 7ec1c3a3d0e..a6012ded27e 100644 --- a/.github/workflows/wsl-orcad-e2e.yml +++ b/.github/workflows/wsl-orcad-e2e.yml @@ -7,6 +7,7 @@ name: WSL orcad E2E # Hosted WSL2 was proven in #27783: WSL 2.7.14, import 13-18 s, cold boot 8-14 s. on: pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] paths: - 'src/main/wsl/**' - '!src/main/wsl/**/*.test.ts' @@ -38,6 +39,7 @@ env: jobs: # The distro is linux-x64-glibc; its node-pty slot only builds on Linux, and nothing else is needed. template: + if: github.event_name != 'pull_request' || github.event.pull_request.draft != true name: Build the linux-x64-glibc orcad template runs-on: ubuntu-latest timeout-minutes: 20 diff --git a/.github/workflows/zcode-credential-contract.yml b/.github/workflows/zcode-credential-contract.yml index 402a552c4af..b9688b1bf9e 100644 --- a/.github/workflows/zcode-credential-contract.yml +++ b/.github/workflows/zcode-credential-contract.yml @@ -1,6 +1,7 @@ name: Coding Plan credential contract on: pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] paths: - 'src/main/zcode/**' - 'src/shared/node-bounded-file-reader*' @@ -25,6 +26,7 @@ concurrency: cancel-in-progress: true jobs: credential-files: + if: github.event_name != 'pull_request' || github.event.pull_request.draft != true name: ZCode credentials (${{ matrix.os }}) strategy: fail-fast: false diff --git a/.github/workflows/zcode-transcript-contract.yml b/.github/workflows/zcode-transcript-contract.yml index bb2b86cec48..4c27c29ab73 100644 --- a/.github/workflows/zcode-transcript-contract.yml +++ b/.github/workflows/zcode-transcript-contract.yml @@ -1,6 +1,7 @@ name: ZCode transcript contract on: pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] paths: - 'src/main/ai-vault/session-scanner-zcode*' - 'src/main/ai-vault/session-scanner-opencode*' @@ -18,6 +19,7 @@ concurrency: cancel-in-progress: true jobs: transcript-readers: + if: github.event_name != 'pull_request' || github.event.pull_request.draft != true strategy: fail-fast: false matrix: diff --git a/config/scripts/ci-draft-pr-admission.test.mjs b/config/scripts/ci-draft-pr-admission.test.mjs new file mode 100644 index 00000000000..64080566973 --- /dev/null +++ b/config/scripts/ci-draft-pr-admission.test.mjs @@ -0,0 +1,70 @@ +import { readFileSync, readdirSync } from 'node:fs' +import { expect, it } from 'vitest' +import { parse } from 'yaml' +import { compileAggregateExpression } from './pr-aggregate-gate-context.mjs' + +const workflows = readdirSync('.github/workflows').flatMap((file) => { + const workflow = parse(readFileSync(`.github/workflows/${file}`, 'utf8')) + // The LoC reporter updates PR metadata rather than running CI. + return Object.hasOwn(workflow.on ?? {}, 'pull_request') && file !== 'pr-test-loc.yml' + ? [{ file, workflow }] + : [] +}) + +function admitted(condition, draft, needs = {}, eventName = 'pull_request') { + return ( + compileAggregateExpression(condition ?? 'true', ['github', 'needs', 'inputs'])({ + github: { + event_name: eventName, + repository: 'stablyai/orca', + event: { pull_request: { draft } } + }, + needs, + inputs: {} + }).coerceString() === 'true' + ) +} + +it.each(workflows)('$file starts no CI jobs for a draft PR', ({ workflow }) => { + const needs = Object.fromEntries( + Object.keys(workflow.jobs).map((id) => [id, { result: 'skipped', outputs: {} }]) + ) + for (const [id, job] of Object.entries(workflow.jobs)) { + // Jobs without a status function inherit success(), so skipped parents block them. + if (job.needs && !/\b(?:cancelled|always|success|failure)\(/.test(job.if ?? '')) { + continue + } + expect(admitted(job.if, true, needs), id).toBe(false) + } +}) + +it.each(workflows)('$file starts CI when ready and cancels it when drafted', ({ workflow }) => { + const types = workflow.on.pull_request.types + expect(types).toEqual( + expect.arrayContaining([ + 'opened', + 'synchronize', + 'reopened', + 'ready_for_review', + 'converted_to_draft' + ]) + ) + expect(Object.values(workflow.jobs).some((job) => !job.needs && admitted(job.if, false))).toBe( + true + ) + const concurrency = workflow.concurrency['cancel-in-progress'] + expect(typeof concurrency === 'boolean' ? concurrency : admitted(concurrency, true)).toBe(true) +}) + +it.each(workflows)('$file keeps its non-PR entry points eligible', ({ workflow }) => { + const needs = Object.fromEntries( + Object.keys(workflow.jobs).map((id) => [id, { result: 'success', outputs: {} }]) + ) + for (const event of Object.keys(workflow.on).filter((name) => name !== 'pull_request')) { + for (const [id, job] of Object.entries(workflow.jobs)) { + expect(admitted(job.if, true, needs, event), `${event}/${id}`).toBe( + admitted(job.if, false, needs, event) + ) + } + } +}) diff --git a/config/scripts/pr-aggregate-gate-context.mjs b/config/scripts/pr-aggregate-gate-context.mjs index d6968ec5b58..061a7db328c 100644 --- a/config/scripts/pr-aggregate-gate-context.mjs +++ b/config/scripts/pr-aggregate-gate-context.mjs @@ -24,7 +24,7 @@ export function compileAggregateExpression(expression, contexts = ['needs']) { ).evaluate() } -export const aggregateShouldRun = compileAggregateExpression(aggregateJob.if) +export const aggregateShouldRun = compileAggregateExpression(aggregateJob.if, ['needs', 'github']) const aggregateEnvironmentExpressions = Object.fromEntries( Object.entries(aggregateShell.env).map(([name, expression]) => [ name, diff --git a/config/scripts/pr-aggregate-gate.test.mjs b/config/scripts/pr-aggregate-gate.test.mjs index 3d5e07b156b..56a6618a8f1 100644 --- a/config/scripts/pr-aggregate-gate.test.mjs +++ b/config/scripts/pr-aggregate-gate.test.mjs @@ -12,7 +12,10 @@ import { const results = ['success', 'failure', 'cancelled', 'skipped', '', 'unknown', undefined, null] const admitted = (needs, cancelled = false) => - aggregateShouldRun({ needs }, cancelled).coerceString() === 'true' + aggregateShouldRun( + { needs, github: { event: { pull_request: { draft: false } } } }, + cancelled + ).coerceString() === 'true' describe('explicit PR aggregate result', () => { it('keeps every required dependency and the existing failure reporter', () => { @@ -27,7 +30,6 @@ describe('explicit PR aggregate result', () => { } expect(aggregateJob['continue-on-error']).toBeUndefined() expect(aggregateShell['continue-on-error']).toBeUndefined() - expect(aggregateJob.if).toBe('${{ !cancelled() }}') expect(aggregateShell.run).toContain('"$CODE_PATHS" != "success"') const checked = [...aggregateShell.run.matchAll(/^check_job (\S+) /gm)].map((match) => match[1]) expect(checked).toEqual(aggregateRequiredJobs) diff --git a/config/scripts/pr-ready-check-reuse.test.mjs b/config/scripts/pr-ready-check-reuse.test.mjs index b37fbb92123..0e34329a562 100644 --- a/config/scripts/pr-ready-check-reuse.test.mjs +++ b/config/scripts/pr-ready-check-reuse.test.mjs @@ -151,7 +151,6 @@ describe('ready-for-review required check reuse', () => { expect(detector.steps.find((step) => step.id === 'e2e_filter').if).toBe( "github.event.pull_request.draft != true && steps.filter.outputs.should_run == 'true'" ) - expect(workflow.jobs.verify.if).toBe('${{ !cancelled() }}') expect(workflow.jobs.verify.needs).toEqual([ 'code_paths', 'preflight', @@ -163,7 +162,7 @@ describe('ready-for-review required check reuse', () => { ]) }) - it.each(['pr-test-loc.yml', 'mobile.yml'])( + it.each(['pr-test-loc.yml'])( 'does not rerun draft-independent %s on readiness changes', (file) => { const source = readFileSync(`.github/workflows/${file}`, 'utf8') diff --git a/config/scripts/required-pr-check-name.test.mjs b/config/scripts/required-pr-check-name.test.mjs index 6cd9199bf64..d57e24efd98 100644 --- a/config/scripts/required-pr-check-name.test.mjs +++ b/config/scripts/required-pr-check-name.test.mjs @@ -22,7 +22,6 @@ it('checks context names even when workflow-only changes skip root tests', () => (candidate) => candidate.run === 'bun .github/scripts/check-required-pr-context.mjs' ) expect(step).toBeDefined() - expect(job.if).toBeUndefined() expect(step.if).toBeUndefined() expect(step['continue-on-error']).toBeUndefined() const checkout = job.steps.find((candidate) => candidate.uses?.startsWith('actions/checkout@')) @@ -45,7 +44,7 @@ it('rejects aggregate conditions that hide a successful result', () => { return workflow } expect(() => verifyRequiredPrContext(parseChanged), String(condition)).toThrow( - 'Required PR aggregate must run unless the workflow is cancelled.' + 'Required PR aggregate must run unless cancelled or the PR is a draft.' ) } })