diff --git a/.github/scripts/ci-gate.mjs b/.github/scripts/ci-gate.mjs index c44ea58798..3938e6c273 100644 --- a/.github/scripts/ci-gate.mjs +++ b/.github/scripts/ci-gate.mjs @@ -11,6 +11,11 @@ export function gateFailures(needs, mode) { const routedJobs = { frontend: "frontend", packages: "packages", "github-scripts": "github_scripts", "windows-standard-check": "windows_win7_bundle", "windows-win7-bundle": "windows_win7_bundle", "duckdb-windows-driver": "duckdb_windows", jdbc: "jdbc", "offline-jdbc-release": "offline_jdbc", "nix-packaging": "nix" }; + // Draft PRs intentionally skip the heavy validation jobs; the gate may only + // demand them once ready_for_review re-triggers the workflow without draft. + const draft = needs.changes.outputs.draft === "true"; + const draftSkippable = new Set(["rust-test", "frontend-test", "agent-java", "agent-go", "agent-rust", "agent-integration", + "packages", "windows-standard-check", "windows-win7-bundle", "duckdb-windows-driver", "jdbc", "offline-jdbc-release", "nix-packaging"]); if (mode === "all" && !Object.values(routedJobs).every((output) => ["true", "false"].includes(needs.changes.outputs[output]))) { return ["missing or invalid job selection outputs"]; } @@ -18,18 +23,18 @@ export function gateFailures(needs, mode) { return ["missing or invalid frontend selection output"]; } const expected = mode === "rust" ? { - "fast-checks": plan.fast, "rust-fmt-clippy": plan.rust, "rust-test": plan.rust, + "fast-checks": plan.fast, "rust-fmt-clippy": plan.rust, "rust-test": plan.rust && !draft, } : mode === "agents" ? { - "fast-checks": plan.fast, "agent-checks": plan.agents, "agent-java": plan.agent_java, "agent-go": plan.agent_go_changed, - "agent-rust": plan.agent_rust_changed, "agent-integration": plan.agent_integration_changed, + "fast-checks": plan.fast, "agent-checks": plan.agents, "agent-java": plan.agent_java && !draft, "agent-go": plan.agent_go_changed && !draft, + "agent-rust": plan.agent_rust_changed && !draft, "agent-integration": plan.agent_integration_changed && !draft, } : mode === "frontend" ? { "frontend-checks": needs.changes.outputs.frontend === "true", "frontend-typecheck": needs.changes.outputs.frontend === "true", - "frontend-test": needs.changes.outputs.frontend === "true", + "frontend-test": needs.changes.outputs.frontend === "true" && !draft, } : mode === "all" ? { rust: true, agents: true, "fast-checks": plan.fast, ...Object.fromEntries(Object.entries(routedJobs) - .map(([job, output]) => [job, needs.changes.outputs[output] === "true"])), + .map(([job, output]) => [job, needs.changes.outputs[output] === "true" && !(draft && draftSkippable.has(job))])), } : null; if (!expected) return [`unknown gate: ${mode}`]; return Object.entries(expected).flatMap(([job, required]) => { @@ -43,5 +48,5 @@ if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.ar if (failures.length) { console.error(failures.join("\n")); process.exitCode = 1; - } else console.log("All selected CI jobs succeeded; only unselected jobs may be skipped."); + } else console.log("All selected CI jobs succeeded; only unselected or draft-skipped jobs may be skipped."); } diff --git a/.github/scripts/ci-plan.test.mjs b/.github/scripts/ci-plan.test.mjs index 87d5a963a3..76907a172c 100644 --- a/.github/scripts/ci-plan.test.mjs +++ b/.github/scripts/ci-plan.test.mjs @@ -338,6 +338,30 @@ test("the frontend gate checks every selected frontend job", () => { assert.deepEqual(gateFailures(needs, "frontend"), []); }); +test("draft runs may skip heavy validation jobs without failing gates", () => { + const needs = results(["Cargo.lock"]); + needs.changes.outputs.draft = "true"; + for (const job of ["rust-test", "agent-java", "agent-go", "agent-rust", "agent-integration"]) { + needs[job] = { result: "skipped" }; + } + assert.deepEqual(gateFailures(needs, "rust"), []); + assert.deepEqual(gateFailures(needs, "agents"), []); + needs.changes.outputs.frontend = "true"; + needs["frontend-checks"] = { result: "success" }; + needs["frontend-typecheck"] = { result: "success" }; + needs["frontend-test"] = { result: "skipped" }; + assert.deepEqual(gateFailures(needs, "frontend"), []); + // Draft only relaxes the skippable set: a real failure is still a failure, + // and jobs the draft policy keeps (clippy, fast-checks) stay required. + needs["frontend-test"].result = "failure"; + assert.ok(gateFailures(needs, "frontend").length); + needs["rust-fmt-clippy"].result = "skipped"; + assert.ok(gateFailures(needs, "rust").length); + // Without the draft output the heavy jobs are required again. + delete needs.changes.outputs.draft; + assert.ok(gateFailures(needs, "rust").length); +}); + test("git diff routing includes both sides of renames, deleted files, and unusual filenames", () => { const directory = mkdtempSync(path.join(tmpdir(), "dbx-ci-plan-")); const git = (...args) => execFileSync("git", args, { cwd: directory, encoding: "utf8", stdio: "pipe" }).trim(); diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 974a21e97f..a12874d411 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,9 @@ on: branches: [ main ] pull_request: branches: [ main ] + # ready_for_review is required: heavy jobs are skipped while a PR is a + # draft, so flipping to ready must re-trigger the full CI. + types: [opened, synchronize, reopened, ready_for_review] concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -92,7 +95,7 @@ jobs: frontend-test: needs: changes - if: needs.changes.outputs.frontend == 'true' + if: needs.changes.outputs.frontend == 'true' && needs.changes.outputs.draft != 'true' runs-on: ubuntu-24.04 env: NPM_CONFIG_LOGLEVEL: error @@ -164,7 +167,7 @@ jobs: packages: needs: changes - if: needs.changes.outputs.packages == 'true' + if: needs.changes.outputs.packages == 'true' && needs.changes.outputs.draft != 'true' runs-on: ubuntu-24.04 env: # Unsupported-platform package warnings are expected while validating cross-platform package metadata. @@ -287,7 +290,7 @@ jobs: windows-standard-check: needs: changes - if: needs.changes.outputs.windows_win7_bundle == 'true' + if: needs.changes.outputs.windows_win7_bundle == 'true' && needs.changes.outputs.draft != 'true' runs-on: windows-2022 timeout-minutes: 45 env: @@ -316,7 +319,7 @@ jobs: windows-win7-bundle: needs: changes - if: needs.changes.outputs.windows_win7_bundle == 'true' + if: needs.changes.outputs.windows_win7_bundle == 'true' && needs.changes.outputs.draft != 'true' runs-on: windows-2022 timeout-minutes: 120 env: @@ -706,7 +709,7 @@ jobs: duckdb-windows-driver: needs: changes - if: needs.changes.outputs.duckdb_windows == 'true' + if: needs.changes.outputs.duckdb_windows == 'true' && needs.changes.outputs.draft != 'true' runs-on: windows-2022 timeout-minutes: 60 env: @@ -855,7 +858,7 @@ jobs: needs: - changes - fast-checks - if: needs.changes.outputs.rust == 'true' + if: needs.changes.outputs.rust == 'true' && needs.changes.outputs.draft != 'true' runs-on: ubuntu-24.04 env: # sccache cannot reuse Cargo incremental artifacts, so avoid generating them in CI. @@ -994,7 +997,7 @@ jobs: jdbc: needs: changes - if: needs.changes.outputs.jdbc == 'true' + if: needs.changes.outputs.jdbc == 'true' && needs.changes.outputs.draft != 'true' runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v7 @@ -1022,7 +1025,7 @@ jobs: offline-jdbc-release: needs: changes - if: needs.changes.outputs.offline_jdbc == 'true' + if: needs.changes.outputs.offline_jdbc == 'true' && needs.changes.outputs.draft != 'true' runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v7 @@ -1062,7 +1065,7 @@ jobs: nix-packaging: needs: changes - if: needs.changes.outputs.nix == 'true' + if: needs.changes.outputs.nix == 'true' && needs.changes.outputs.draft != 'true' runs-on: ubuntu-24.04 continue-on-error: true steps: @@ -1102,6 +1105,9 @@ jobs: agent_rust_changed: ${{ steps.plan.outputs.agent_rust_changed }} agent_integration_changed: ${{ steps.plan.outputs.agent_integration_changed }} plan: ${{ steps.plan.outputs.plan }} + # True only for pull_request events on draft PRs; heavy jobs skip and + # ci-gate.mjs must not demand them until ready_for_review re-triggers. + draft: ${{ github.event_name == 'pull_request' && github.event.pull_request.draft == true }} steps: - uses: actions/checkout@v7 with: @@ -1332,7 +1338,7 @@ jobs: run: python3 scripts/validate_agents.py agent-java: needs: [changes, fast-checks, agent-checks] - if: needs.changes.outputs.agent_java == 'true' + if: needs.changes.outputs.agent_java == 'true' && needs.changes.outputs.draft != 'true' runs-on: ubuntu-24.04 defaults: run: @@ -1360,7 +1366,7 @@ jobs: run: python3 scripts/validate_agent_jars.py agent-rust: needs: [changes, fast-checks, agent-checks] - if: needs.changes.outputs.agent_rust_changed == 'true' + if: needs.changes.outputs.agent_rust_changed == 'true' && needs.changes.outputs.draft != 'true' runs-on: ubuntu-24.04 env: # sccache cannot reuse Cargo incremental artifacts, so avoid generating them in CI. @@ -1472,7 +1478,7 @@ jobs: matrix: ${{ fromJSON(needs.changes.outputs.agent_rust) }} agent-go: needs: [changes, fast-checks, agent-checks] - if: needs.changes.outputs.agent_go_changed == 'true' + if: needs.changes.outputs.agent_go_changed == 'true' && needs.changes.outputs.draft != 'true' runs-on: ubuntu-24.04 defaults: run: @@ -1496,7 +1502,7 @@ jobs: matrix: ${{ fromJSON(needs.changes.outputs.agent_go) }} agent-integration: needs: [changes, fast-checks, agent-checks] - if: needs.changes.outputs.agent_integration_changed == 'true' + if: needs.changes.outputs.agent_integration_changed == 'true' && needs.changes.outputs.draft != 'true' runs-on: ubuntu-24.04 defaults: run: