diff --git a/.github/workflows/ci-master.yml b/.github/workflows/ci-master.yml index 5620e6af63..d69a860098 100644 --- a/.github/workflows/ci-master.yml +++ b/.github/workflows/ci-master.yml @@ -14,6 +14,7 @@ on: - larger-runner-benchmark - consolidated-runner-benchmark - ssh-helper + - wine # New master pushes and manual runs replace obsolete checks in this workflow/ref. # Standby drills share cancellation; use completed runs as readiness evidence. @@ -49,9 +50,9 @@ jobs: secrets: DEEPSEEK_API_KEY_EXTERNAL: ${{ secrets.DEEPSEEK_API_KEY_EXTERNAL }} - # Wine runs once here, independently of the native serial standby aggregates. + # Wine runs on master pushes or an explicit suite=wine dispatch, independently of native standbys. windows: - if: github.event_name == 'push' && github.ref == 'refs/heads/master' + if: (github.event_name == 'push' && github.ref == 'refs/heads/master') || (github.event_name == 'workflow_dispatch' && inputs.suite == 'wine') runs-on: ubuntu-latest name: windows node 24 / wine timeout-minutes: 15 diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 1574ec9e2f..c30d75af0d 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -24,18 +24,7 @@ env: jobs: pack: name: Pack npm tarballs - runs-on: >- - ${{ vars.DSH_CI_FAILOVER_LINUX == 'selfhosted' - && github.repository == 'deepseek-harness/deepseek-harness' - && github.actor != 'dependabot[bot]' - && (github.event_name == 'workflow_dispatch' - || (github.event_name == 'push' && github.ref == 'refs/heads/master') - || (github.event_name == 'pull_request' - && github.event.pull_request.head.repo.full_name == github.repository - && github.event.pull_request.head.repo.fork == false - && github.event.pull_request.user.login != 'dependabot[bot]')) - && fromJSON('["self-hosted", "linux", "x64", "vm-backup"]') - || 'ubuntu-24.04' }} + runs-on: ubuntu-24.04 steps: # Complete history: the release scripts read tags. - uses: actions/checkout@v7.0.1 diff --git a/scripts/ci-workflow.spec.ts b/scripts/ci-workflow.spec.ts index c3ac5f299b..79e39fe354 100644 --- a/scripts/ci-workflow.spec.ts +++ b/scripts/ci-workflow.spec.ts @@ -138,7 +138,7 @@ describe('CI workflow', () => { expect(job.steps).toContainEqual(expect.objectContaining({ run: command })) } for (const [file, command] of [ - ['release-publish.yml', 'pnpm run build:official'], + ['release-publish.yml', 'pnpm run build:official --serial'], ['release-vendor-publish.yml', 'pnpm run build:lib:host'], ] as const) { expect(workflowJob(loadWorkflow('.github/workflows/' + file), 'pack').steps) @@ -743,7 +743,7 @@ describe('CI workflow', () => { expect(uploadIndex).toBeGreaterThan(benchmarkIndex) }) - it('splits the Wine Host TypeScript compile and retains its heap budget', () => { + it('splits both Wine TypeScript faces and retains their aggregate heap budgets', () => { const wineGates = readFileSync(resolve(root, 'scripts/wine-windows-gates.sh'), 'utf8') const references = wineGates.indexOf( @@ -757,6 +757,43 @@ describe('CI workflow', () => { expect(references).toBeGreaterThan(metadata) expect(aggregate).toBeGreaterThan(references) expect(wineGates).not.toContain('-b tsconfig.host.json') + const clientReferences = wineGates.indexOf('wine_node "$scratch/logs/client-references.log" --import tsx/esm scripts/compile-referenced-projects.ts client') + const clientAggregate = wineGates.indexOf('wine_node "$scratch/logs/client-tsc.log" --max-old-space-size=8192 "$tsc_js" -p tsconfig.client.json --pretty false') + expect(clientReferences).toBeGreaterThan(aggregate) + expect(clientAggregate).toBeGreaterThan(clientReferences) + expect(wineGates).not.toContain('-b tsconfig.client.json') + expect(wineGates).toContain(' "$scratch/logs/client-references.log" \\') + }) + + it('runs only Wine for its manual suite and retains master-push scheduling', () => { + const master = loadWorkflow('.github/workflows/ci-master.yml') + if (!isRecord(master.on) || !isRecord(master.on.workflow_dispatch) + || !isRecord(master.on.workflow_dispatch.inputs) || !isRecord(master.on.workflow_dispatch.inputs.suite) + || !isRecord(master.jobs)) throw new TypeError('CI master must define manual suites and jobs') + expect(master.on.workflow_dispatch.inputs.suite.options).toEqual([ + 'larger-runner-benchmark', 'consolidated-runner-benchmark', 'ssh-helper', 'wine', + ]) + const wine = workflowJob(master, 'windows') + if (typeof wine.if !== 'string') throw new TypeError('Wine must define a trigger condition') + for (const [event, ref, suite, expected] of [ + ['push', 'refs/heads/master', '', true], + ['push', 'refs/heads/topic', '', false], + ['pull_request', 'refs/pull/42/merge', 'wine', false], + ['workflow_dispatch', 'refs/heads/topic', 'wine', true], + ['workflow_dispatch', 'refs/tags/dsh-v0.2.2-alpha.2', 'wine', true], + ['workflow_dispatch', 'refs/heads/topic', 'ssh-helper', false], + ['workflow_dispatch', 'refs/heads/topic', 'larger-runner-benchmark', false], + ['workflow_dispatch', 'refs/heads/topic', 'consolidated-runner-benchmark', false], + ] as const) { + expect(runInNewContext(wine.if, { github: { event_name: event, ref }, inputs: { suite } }, { timeout: 1000 })).toBe(expected) + } + const enabled = Object.entries(master.jobs).filter(([, job]) => { + if (!isRecord(job)) throw new TypeError('CI master jobs must be objects') + if (typeof job.if === 'boolean') return job.if + if (typeof job.if !== 'string') throw new TypeError('CI master jobs must define trigger conditions') + return runInNewContext(job.if, { github: { event_name: 'workflow_dispatch', ref: 'refs/heads/topic' }, inputs: { suite: 'wine' } }, { timeout: 1000 }) === true + }).map(([name]) => name) + expect(enabled).toEqual(['windows']) }) it('cancels superseded master runs without changing the post-merge job inventory', () => { diff --git a/scripts/tests/ci-master-platforms.spec.ts b/scripts/tests/ci-master-platforms.spec.ts index c009f7fc6e..4940f60d7b 100644 --- a/scripts/tests/ci-master-platforms.spec.ts +++ b/scripts/tests/ci-master-platforms.spec.ts @@ -126,7 +126,10 @@ describe('master-only platform scheduling', () => { it('runs Wine once on hosted master CI and seeds its own apt cache', () => { const master = workflow('ci-master.yml') const wine = master.jobs.windows! - expect(wine).toMatchObject({ if: masterPush, 'runs-on': 'ubuntu-latest' }) + expect(wine).toMatchObject({ + if: "(github.event_name == 'push' && github.ref == 'refs/heads/master') || (github.event_name == 'workflow_dispatch' && inputs.suite == 'wine')", + 'runs-on': 'ubuntu-latest', + }) expect(wine.needs).toBeUndefined() expect(wine['continue-on-error']).toBeUndefined() expect(master.jobs['wine-apt-cache']).toBeUndefined() diff --git a/scripts/tests/ci-release-selfhosted.spec.ts b/scripts/tests/ci-release-selfhosted.spec.ts index 3e567da75a..fe2fcaf40c 100644 --- a/scripts/tests/ci-release-selfhosted.spec.ts +++ b/scripts/tests/ci-release-selfhosted.spec.ts @@ -163,8 +163,13 @@ for (const [file, jobIds] of [['release.yml', ['dependencies', 'pack']], ['relea }) } -it.each(['release-publish.yml', 'release-vendor-publish.yml'])('keeps %s manual and entirely hosted', (file) => { - const publish = workflow(file) +it('keeps vendor publication manual and entirely hosted', () => { + const publish = workflow('release-vendor-publish.yml') + expect(Object.keys(publish.on)).toEqual(['workflow_dispatch']) + for (const job of Object.values(publish.jobs)) expect(job['runs-on']).toBe(hosted) +}) +it('keeps DSH publication manual and entirely hosted', () => { + const publish = workflow('release-publish.yml') expect(Object.keys(publish.on)).toEqual(['workflow_dispatch']) for (const job of Object.values(publish.jobs)) expect(job['runs-on']).toBe(hosted) }) diff --git a/scripts/wine-windows-gates.sh b/scripts/wine-windows-gates.sh index edd01072b5..5a8181928f 100755 --- a/scripts/wine-windows-gates.sh +++ b/scripts/wine-windows-gates.sh @@ -245,10 +245,9 @@ grep -q '^smoke: win32 x64' "$scratch/logs/smoke.log" || { echo 'wine-windows-ga # The build mirrors the root build:lib:host and build:lib:client scripts: # generate metadata, compile the Host package projects, type-check the Host # aggregate program, bundle the Host face, bundle apps/desktop from the Host -# lib/ outputs, then compile and bundle the Client face. The Host compile is two -# invocations because that is how the supported build splits it; a single -# `tsc -b` holds the reference graph and the aggregate program in one process -# and exceeds the heap the Host face is budgeted. +# lib/ outputs, then compile and bundle the Client face. Each face compiles +# package references separately from its aggregate program, matching the +# supported build and releasing the reference graph before aggregate checking. # Both statuses are captured so one failure cannot hide the other's result. build_gate() { wine_node "$scratch/logs/host-typert.log" --import tsx/esm scripts/generate-typert.ts || return $? @@ -256,7 +255,8 @@ build_gate() { wine_node "$scratch/logs/host-tsc.log" --max-old-space-size=8192 "$tsc_js" -p tsconfig.host.json --pretty false || return $? wine_node "$scratch/logs/host-tsdown.log" "$tsdown_js" --env.DSH_BUILD_FACE host || return $? (cd apps/desktop && wine_node "$scratch/logs/desktop-tsdown.log" "../../$tsdown_js") || return $? - wine_node "$scratch/logs/client-tsc.log" "$tsc_js" -b tsconfig.client.json --pretty false || return $? + wine_node "$scratch/logs/client-references.log" --import tsx/esm scripts/compile-referenced-projects.ts client || return $? + wine_node "$scratch/logs/client-tsc.log" --max-old-space-size=8192 "$tsc_js" -p tsconfig.client.json --pretty false || return $? wine_node "$scratch/logs/client-tsdown.log" "$tsdown_js" --env.DSH_BUILD_FACE client } site_gate() { @@ -288,6 +288,7 @@ report 'build (Typert, Host tsc/tsdown, desktop tsdown, Client tsc/tsdown)' "$bu "$scratch/logs/host-tsc.log" \ "$scratch/logs/host-tsdown.log" \ "$scratch/logs/desktop-tsdown.log" \ + "$scratch/logs/client-references.log" \ "$scratch/logs/client-tsc.log" \ "$scratch/logs/client-tsdown.log" report 'production site (vitepress build)' "$site_status" "$scratch/logs/site.log"