|
|
@@ -417,17 +417,25 @@ describe('Python release workflows', () => {
|
|
|
})
|
|
|
|
|
|
describe('Issue lifecycle workflow', () => {
|
|
|
- it('runs the lifecycle job on every PR/review event so it passes instead of skipping', () => {
|
|
|
+ it('runs the lifecycle job on every PR/review event but gates token and board steps', () => {
|
|
|
const lifecycle = loadWorkflow('.github/workflows/issue-lifecycle.yml')
|
|
|
const policy = loadWorkflow('.github/workflows/issue-policy.yml')
|
|
|
const lifecycleJob = workflowJob(lifecycle, 'lifecycle')
|
|
|
+ if (!Array.isArray(lifecycleJob.steps)) throw new TypeError('Issue lifecycle job must define steps')
|
|
|
|
|
|
- // The lifecycle job has no workflow-level `if`, so it is listed on every
|
|
|
- // pull_request / pull_request_review event and reports success (the handler
|
|
|
- // no-ops for non-changes-requested reviews) instead of a gray "skipped" check.
|
|
|
+ // The job has no job-level `if`, so it is listed on every pull_request /
|
|
|
+ // pull_request_review event and reports success instead of a gray skip. The
|
|
|
+ // write-capable steps are gated at step level so approved/commented reviews
|
|
|
+ // never mint a Project/Issue App token nor touch the board.
|
|
|
expect(lifecycle.on).toHaveProperty('pull_request')
|
|
|
expect(lifecycle.on).toHaveProperty('pull_request_review')
|
|
|
expect(lifecycleJob.if).toBeUndefined()
|
|
|
+ const gated = "${{ github.event_name != 'pull_request_review' || github.event.review.state == 'changes_requested' }}"
|
|
|
+ const steps = lifecycleJob.steps.filter(isRecord)
|
|
|
+ const tokenStep = steps.find(s => s.name === 'Create project token')
|
|
|
+ const handleStep = steps.find(s => s.name === 'Handle repository event')
|
|
|
+ expect(tokenStep).toMatchObject({ if: gated })
|
|
|
+ expect(handleStep).toMatchObject({ if: gated })
|
|
|
|
|
|
// issue-policy owns PR validation; it is read-only and a real gate.
|
|
|
const policyPullRequest = workflowEvent(policy, 'pull_request')
|
|
|
@@ -435,6 +443,29 @@ describe('Issue lifecycle workflow', () => {
|
|
|
})
|
|
|
})
|
|
|
|
|
|
+describe('npm release workflows', () => {
|
|
|
+ it('keeps publication dispatch-only and pack in the PR workflow', () => {
|
|
|
+ // pack stays in the PR/master release workflows so a PR proves the set packs.
|
|
|
+ for (const file of ['release.yml', 'release-vendor.yml']) {
|
|
|
+ const workflow = loadWorkflow(`.github/workflows/${file}`)
|
|
|
+ if (!isRecord(workflow.jobs)) throw new TypeError(`${file} must define jobs`)
|
|
|
+ expect(Object.keys(workflow.jobs).sort()).toEqual(['pack'])
|
|
|
+ }
|
|
|
+
|
|
|
+ // publication is workflow_dispatch-only (never a PR check) and keeps the
|
|
|
+ // npm-publish environment plus the shared dist-tag group.
|
|
|
+ for (const file of ['release-publish.yml', 'release-vendor-publish.yml']) {
|
|
|
+ const workflow = loadWorkflow(`.github/workflows/${file}`)
|
|
|
+ if (!isRecord(workflow.on) || !isRecord(workflow.jobs)) throw new TypeError(`${file} must define on and jobs`)
|
|
|
+ expect(Object.keys(workflow.on)).toEqual(['workflow_dispatch'])
|
|
|
+ const publish = workflow.jobs.publish
|
|
|
+ if (!isRecord(publish)) throw new TypeError(`${file} must define a publish job`)
|
|
|
+ expect(publish.environment).toBe('npm-publish')
|
|
|
+ expect(publish.concurrency).toMatchObject({ group: 'Release-publish' })
|
|
|
+ }
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
describe('Git hooks', () => {
|
|
|
it('leaves frozen Agent Note sidecars to the archive verifier', () => {
|
|
|
const lefthook = loadWorkflow('lefthook.yml')
|