diff --git a/.github/workflows/ql3-ci.yml b/.github/workflows/ql3-ci.yml index faa49477..6d62f596 100644 --- a/.github/workflows/ql3-ci.yml +++ b/.github/workflows/ql3-ci.yml @@ -1788,9 +1788,14 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v6 + - uses: pnpm/action-setup@v6 - uses: actions/setup-node@v6 with: node-version: '24.18.0' + cache: pnpm + cache-dependency-path: pnpm-lock.yaml + - name: Install workspace dependencies without lifecycle scripts + run: pnpm install --frozen-lockfile --ignore-scripts - name: Download the exact control amd64 Cluster Alpha bundle uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -1894,9 +1899,14 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v6 + - uses: pnpm/action-setup@v6 - uses: actions/setup-node@v6 with: node-version: '24.18.0' + cache: pnpm + cache-dependency-path: pnpm-lock.yaml + - name: Install workspace dependencies without lifecycle scripts + run: pnpm install --frozen-lockfile --ignore-scripts - name: Download the exact amd64 Local Alpha trial kit uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -1948,9 +1958,14 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v6 + - uses: pnpm/action-setup@v6 - uses: actions/setup-node@v6 with: node-version: '24.18.0' + cache: pnpm + cache-dependency-path: pnpm-lock.yaml + - name: Install workspace dependencies without lifecycle scripts + run: pnpm install --frozen-lockfile --ignore-scripts - name: Download the closed Local Alpha milestone uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: diff --git a/scripts/ql3-alpha-stage-index.cjs b/scripts/ql3-alpha-stage-index.cjs index 6ba3e5d5..a6bbe854 100644 --- a/scripts/ql3-alpha-stage-index.cjs +++ b/scripts/ql3-alpha-stage-index.cjs @@ -559,6 +559,9 @@ function auditAlphaStageIndexWorkflow(root = DEFAULT_ROOT) { `if: ${condition}`, ' - local-alpha-milestone\n', ' - cluster-alpha-milestone\n', + 'pnpm/action-setup@v6', + 'cache-dependency-path: pnpm-lock.yaml', + 'pnpm install --frozen-lockfile --ignore-scripts', `name: ql3-alpha-${'${{ github.sha }}'}-local-${'${{ inputs.local_alpha_variant }}'}-milestone`, `name: ql3-alpha-${'${{ github.sha }}'}-cluster-milestone`, 'scripts/ql3-alpha-stage-index.cjs', @@ -571,11 +574,15 @@ function auditAlphaStageIndexWorkflow(root = DEFAULT_ROOT) { if (!stage || tokens.some((token) => !stage.includes(token))) { findings.push('ALPHA_STAGE_INDEX_FINALIZER_CONTRACT_DRIFT'); } + const dependencyInstallIndex = stage.indexOf( + 'pnpm install --frozen-lockfile --ignore-scripts', + ); const finalizeIndex = stage.indexOf('--mode=finalize'); const auditIndex = stage.indexOf('--mode=audit'); const uploadIndex = stage.indexOf('actions/upload-artifact@'); if ( - finalizeIndex < 0 || + dependencyInstallIndex < 0 || + finalizeIndex <= dependencyInstallIndex || auditIndex <= finalizeIndex || uploadIndex <= auditIndex ) { diff --git a/scripts/ql3-cluster-alpha-milestone.cjs b/scripts/ql3-cluster-alpha-milestone.cjs index 0d8c612a..59b32397 100644 --- a/scripts/ql3-cluster-alpha-milestone.cjs +++ b/scripts/ql3-cluster-alpha-milestone.cjs @@ -494,6 +494,9 @@ function auditClusterAlphaMilestoneWorkflow(root = DEFAULT_ROOT) { const milestone = jobBlock(workflow, 'cluster-alpha-milestone'); const tokens = [ 'name: Finalize the Cluster Alpha integration milestone', + 'pnpm/action-setup@v6', + 'cache-dependency-path: pnpm-lock.yaml', + 'pnpm install --frozen-lockfile --ignore-scripts', 'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c', 'scripts/ql3-cluster-alpha-milestone.cjs', '--mode=finalize', @@ -517,11 +520,15 @@ function auditClusterAlphaMilestoneWorkflow(root = DEFAULT_ROOT) { ) { findings.push('CLUSTER_MILESTONE_FINALIZER_CONTRACT_DRIFT'); } + const dependencyInstallIndex = milestone.indexOf( + 'pnpm install --frozen-lockfile --ignore-scripts', + ); const finalizerIndex = milestone.indexOf('--mode=finalize'); const auditIndex = milestone.indexOf('--mode=audit'); const uploadIndex = milestone.indexOf('actions/upload-artifact@'); if ( - finalizerIndex < 0 || + dependencyInstallIndex < 0 || + finalizerIndex <= dependencyInstallIndex || auditIndex <= finalizerIndex || uploadIndex <= auditIndex ) { diff --git a/scripts/ql3-local-alpha-milestone.cjs b/scripts/ql3-local-alpha-milestone.cjs index dca62a73..634eb20e 100644 --- a/scripts/ql3-local-alpha-milestone.cjs +++ b/scripts/ql3-local-alpha-milestone.cjs @@ -527,6 +527,9 @@ function auditLocalAlphaMilestoneWorkflow(root = DEFAULT_ROOT) { const milestoneTokens = [ ' name: Finalize the Local Alpha milestone', ' needs:', + 'pnpm/action-setup@v6', + 'cache-dependency-path: pnpm-lock.yaml', + 'pnpm install --frozen-lockfile --ignore-scripts', 'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c', `name: ql3-alpha-${'${{ github.sha }}'}-local-${'${{ inputs.local_alpha_variant }}'}-amd64`, `name: ql3-alpha-${'${{ github.sha }}'}-local-${'${{ inputs.local_alpha_variant }}'}-arm64`, @@ -547,11 +550,15 @@ function auditLocalAlphaMilestoneWorkflow(root = DEFAULT_ROOT) { ) { findings.push('MILESTONE_FINALIZER_CONTRACT_DRIFT'); } + const dependencyInstallIndex = milestone.indexOf( + 'pnpm install --frozen-lockfile --ignore-scripts', + ); const finalizerIndex = milestone.indexOf('--mode=finalize'); const auditIndex = milestone.indexOf('--mode=audit'); const uploadIndex = milestone.lastIndexOf('actions/upload-artifact@'); if ( - finalizerIndex < 0 || + dependencyInstallIndex < 0 || + finalizerIndex <= dependencyInstallIndex || auditIndex <= finalizerIndex || uploadIndex <= auditIndex ) { diff --git a/test/back/ql3AlphaStageIndex.test.cjs b/test/back/ql3AlphaStageIndex.test.cjs index 1bc0888b..41ce187d 100644 --- a/test/back/ql3AlphaStageIndex.test.cjs +++ b/test/back/ql3AlphaStageIndex.test.cjs @@ -327,6 +327,34 @@ test('workflow audit rejects a partial or prematurely uploaded stage index', (t) ); }); +test('workflow audit rejects a stage finalizer without installed dependencies', (t) => { + const fixtureRoot = fs.realpathSync( + fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-alpha-stage-dependencies-')), + ); + t.after(() => fs.rmSync(fixtureRoot, { recursive: true, force: true })); + fs.mkdirSync(path.join(fixtureRoot, '.github/workflows'), { + recursive: true, + }); + const source = fs.readFileSync( + path.join(root, '.github/workflows/ql3-ci.yml'), + 'utf8', + ); + const marker = '\n alpha-stage-index:\n'; + const markerIndex = source.indexOf(marker); + const workflow = `${source.slice(0, markerIndex)}${source + .slice(markerIndex) + .replace('pnpm install --frozen-lockfile --ignore-scripts', 'true')}`; + fs.writeFileSync( + path.join(fixtureRoot, '.github/workflows/ql3-ci.yml'), + workflow, + ); + const report = auditAlphaStageIndexWorkflow(fixtureRoot); + assert.equal(report.compatible, false); + assert.ok( + report.findings.includes('ALPHA_STAGE_INDEX_FINALIZER_CONTRACT_DRIFT'), + ); +}); + test('CLI grammar keeps cross-index audit explicit', () => { assert.deepEqual( parseArguments([ diff --git a/test/back/ql3ClusterAlphaMilestone.test.cjs b/test/back/ql3ClusterAlphaMilestone.test.cjs index 4fed389f..d410ad97 100644 --- a/test/back/ql3ClusterAlphaMilestone.test.cjs +++ b/test/back/ql3ClusterAlphaMilestone.test.cjs @@ -216,3 +216,27 @@ test('workflow audit rejects missing final offline audit', (t) => { report.findings.includes('CLUSTER_MILESTONE_FINALIZER_CONTRACT_DRIFT'), ); }); + +test('workflow audit rejects a finalizer without installed dependencies', (t) => { + const fixtureRoot = fs.mkdtempSync( + path.join(os.tmpdir(), 'ql3-cluster-alpha-dependencies-'), + ); + t.after(() => fs.rmSync(fixtureRoot, { recursive: true, force: true })); + const workflowDirectory = path.join(fixtureRoot, '.github/workflows'); + fs.mkdirSync(workflowDirectory, { recursive: true }); + const source = fs.readFileSync( + path.join(root, '.github/workflows/ql3-ci.yml'), + 'utf8', + ); + const marker = '\n cluster-alpha-milestone:\n'; + const markerIndex = source.indexOf(marker); + const workflow = `${source.slice(0, markerIndex)}${source + .slice(markerIndex) + .replace('pnpm install --frozen-lockfile --ignore-scripts', 'true')}`; + fs.writeFileSync(path.join(workflowDirectory, 'ql3-ci.yml'), workflow); + const report = auditClusterAlphaMilestoneWorkflow(fixtureRoot); + assert.equal(report.compatible, false); + assert.ok( + report.findings.includes('CLUSTER_MILESTONE_FINALIZER_CONTRACT_DRIFT'), + ); +}); diff --git a/test/back/ql3LocalAlphaMilestone.test.cjs b/test/back/ql3LocalAlphaMilestone.test.cjs index 9716337d..d5998954 100644 --- a/test/back/ql3LocalAlphaMilestone.test.cjs +++ b/test/back/ql3LocalAlphaMilestone.test.cjs @@ -325,6 +325,35 @@ test('workflow audit rejects a partial milestone finalizer', (t) => { ); }); +test('workflow audit rejects a finalizer without installed dependencies', (t) => { + const fixtureRoot = fs.realpathSync( + fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-local-alpha-dependencies-')), + ); + t.after(() => fs.rmSync(fixtureRoot, { recursive: true, force: true })); + fs.mkdirSync(path.join(fixtureRoot, '.github/workflows'), { + recursive: true, + }); + const source = fs.readFileSync( + path.join(root, '.github/workflows/ql3-ci.yml'), + 'utf8', + ); + const marker = '\n local-alpha-milestone:\n'; + const markerIndex = source.indexOf(marker); + const workflow = `${source.slice(0, markerIndex)}${source + .slice(markerIndex) + .replace('pnpm install --frozen-lockfile --ignore-scripts', 'true')}`; + fs.writeFileSync( + path.join(fixtureRoot, '.github/workflows/ql3-ci.yml'), + workflow, + ); + const report = auditLocalAlphaMilestoneWorkflow(fixtureRoot); + assert.equal(report.compatible, false); + assert.equal( + report.findings.includes('MILESTONE_FINALIZER_CONTRACT_DRIFT'), + true, + ); +}); + test('CLI grammar separates finalization, index audit and workflow audit', () => { assert.deepEqual( parseArguments(['--mode=audit', '--milestone=/tmp/ql3-alpha-milestone']),