|
@@ -417,24 +417,63 @@ describe('Python release workflows', () => {
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
describe('Issue lifecycle workflow', () => {
|
|
describe('Issue lifecycle workflow', () => {
|
|
|
- it('uses explicit review handoff events without rerunning when a draft becomes ready', () => {
|
|
|
|
|
|
|
+ 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 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 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()
|
|
|
|
|
+ // Keep the subscription-type gates: issue-lifecycle does not re-subscribe
|
|
|
|
|
+ // ready_for_review (issue-policy owns that) and only reacts to submitted
|
|
|
|
|
+ // review events.
|
|
|
const lifecyclePullRequest = workflowEvent(lifecycle, 'pull_request')
|
|
const lifecyclePullRequest = workflowEvent(lifecycle, 'pull_request')
|
|
|
const lifecycleReview = workflowEvent(lifecycle, 'pull_request_review')
|
|
const lifecycleReview = workflowEvent(lifecycle, 'pull_request_review')
|
|
|
- const lifecycleJob = workflowJob(lifecycle, 'lifecycle')
|
|
|
|
|
- const policy = loadWorkflow('.github/workflows/issue-policy.yml')
|
|
|
|
|
- const policyPullRequest = workflowEvent(policy, 'pull_request')
|
|
|
|
|
-
|
|
|
|
|
expect(lifecyclePullRequest.types).not.toContain('ready_for_review')
|
|
expect(lifecyclePullRequest.types).not.toContain('ready_for_review')
|
|
|
expect(lifecyclePullRequest.types).toContain('review_requested')
|
|
expect(lifecyclePullRequest.types).toContain('review_requested')
|
|
|
expect(lifecycleReview.types).toEqual(['submitted'])
|
|
expect(lifecycleReview.types).toEqual(['submitted'])
|
|
|
- expect(lifecycleJob.if).toBe(
|
|
|
|
|
- "${{ github.event_name != 'pull_request_review' || (github.event.action == 'submitted' && github.event.review.state == 'changes_requested') }}",
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ 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')
|
|
|
expect(policyPullRequest.types).toContain('ready_for_review')
|
|
expect(policyPullRequest.types).toContain('ready_for_review')
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+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', () => {
|
|
describe('Git hooks', () => {
|
|
|
it('leaves frozen Agent Note sidecars to the archive verifier', () => {
|
|
it('leaves frozen Agent Note sidecars to the archive verifier', () => {
|
|
|
const lefthook = loadWorkflow('lefthook.yml')
|
|
const lefthook = loadWorkflow('lefthook.yml')
|