create-agentic-pdlc 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.coderabbit.yaml +7 -1
- package/.github/workflows/add-to-board.yml +55 -7
- package/.github/workflows/agent-trigger.yml +57 -25
- package/.github/workflows/board-reconciliation.yml +176 -0
- package/.github/workflows/pdlc-health-check.yml +81 -81
- package/.github/workflows/project-automation.yml +256 -209
- package/adapters/claude-code/skill.md +5 -5
- package/bin/cli.js +65 -11
- package/docs/superpowers/plans/2026-06-05-archive-card-on-issue-close.md +105 -0
- package/docs/superpowers/plans/2026-06-05-project-id-actions-variable.md +336 -0
- package/docs/superpowers/specs/2026-06-05-project-id-actions-variable-design.md +114 -0
- package/package.json +1 -1
- package/scripts/derive-column.js +20 -0
- package/templates/.github/workflows/add-to-board.yml +2 -2
- package/templates/.github/workflows/agent-trigger.yml +2 -2
- package/templates/.github/workflows/pdlc-health-check.yml +2 -2
- package/templates/.github/workflows/project-automation.yml +47 -8
- package/tests/cli.test.js +86 -0
|
@@ -8,188 +8,230 @@ on:
|
|
|
8
8
|
issues:
|
|
9
9
|
types: [labeled, closed]
|
|
10
10
|
|
|
11
|
-
env:
|
|
12
|
-
PROJECT_ID: "PVT_kwHODpFFL84BXg7h"
|
|
13
|
-
STATUS_FIELD_ID: "PVTSSF_lAHODpFFL84BXg7hzhStRHI"
|
|
14
|
-
STATUS_IDEA: "bb6e5a20"
|
|
15
|
-
STATUS_BRAINSTORMING: "8eb07c5b"
|
|
16
|
-
STATUS_DETAILING: "9f6ce70e"
|
|
17
|
-
STATUS_APPROVAL: "31bf4610"
|
|
18
|
-
STATUS_DEVELOPMENT: "2c9e78e6"
|
|
19
|
-
STATUS_TESTING: "96b59ade"
|
|
20
|
-
STATUS_CODE_REVIEW_PR: "86ca9720"
|
|
21
|
-
STATUS_PRODUCTION: "1581e5bd"
|
|
22
|
-
|
|
23
11
|
jobs:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if: github.event_name == 'issues' && github.event.action == 'labeled'
|
|
12
|
+
resolve-ids:
|
|
13
|
+
name: Resolve Board Column IDs
|
|
14
|
+
if: github.event_name != 'issues' || github.event.action != 'closed'
|
|
28
15
|
runs-on: ubuntu-latest
|
|
29
16
|
env:
|
|
30
17
|
PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
|
|
18
|
+
outputs:
|
|
19
|
+
project_id: ${{ steps.resolve.outputs.project_id }}
|
|
20
|
+
status_field_id: ${{ steps.resolve.outputs.status_field_id }}
|
|
21
|
+
status_brainstorming: ${{ steps.resolve.outputs.status_brainstorming }}
|
|
22
|
+
status_detailing: ${{ steps.resolve.outputs.status_detailing }}
|
|
23
|
+
status_approval: ${{ steps.resolve.outputs.status_approval }}
|
|
24
|
+
status_development: ${{ steps.resolve.outputs.status_development }}
|
|
25
|
+
status_code_review_pr: ${{ steps.resolve.outputs.status_code_review_pr }}
|
|
26
|
+
status_production: ${{ steps.resolve.outputs.status_production }}
|
|
31
27
|
steps:
|
|
32
|
-
- name:
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
- name: Resolve column IDs by name
|
|
29
|
+
id: resolve
|
|
30
|
+
if: ${{ env.PROJECT_TOKEN != '' && vars.PROJECT_ID != '' }}
|
|
31
|
+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
|
35
32
|
with:
|
|
36
33
|
github-token: ${{ env.PROJECT_TOKEN }}
|
|
37
34
|
script: |
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (labelName === 'stage:brainstorming') {
|
|
43
|
-
targetStatusId = process.env.STATUS_BRAINSTORMING;
|
|
44
|
-
stageName = 'Brainstorming';
|
|
45
|
-
} else if (labelName === 'stage:detailing') {
|
|
46
|
-
targetStatusId = process.env.STATUS_DETAILING;
|
|
47
|
-
stageName = 'Detailing';
|
|
48
|
-
} else if (labelName === 'stage:approval') {
|
|
49
|
-
targetStatusId = process.env.STATUS_APPROVAL;
|
|
50
|
-
stageName = 'Approval';
|
|
51
|
-
} else if (labelName === 'stage:development') {
|
|
52
|
-
targetStatusId = process.env.STATUS_DEVELOPMENT;
|
|
53
|
-
stageName = 'Development';
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (!targetStatusId) {
|
|
57
|
-
console.log('No stage PDLC label found. Skipping.');
|
|
35
|
+
const projectId = '${{ vars.PROJECT_ID }}';
|
|
36
|
+
if (!projectId || projectId === '{{PROJECT_ID}}') {
|
|
37
|
+
core.warning('vars.PROJECT_ID not set — board features disabled');
|
|
58
38
|
return;
|
|
59
39
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
40
|
+
const { node } = await github.graphql(`
|
|
41
|
+
query($id: ID!) {
|
|
42
|
+
node(id: $id) {
|
|
43
|
+
... on ProjectV2 {
|
|
44
|
+
fields(first: 20) {
|
|
45
|
+
nodes {
|
|
46
|
+
... on ProjectV2SingleSelectField {
|
|
47
|
+
id
|
|
48
|
+
name
|
|
49
|
+
options { id name }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
73
53
|
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
`, { id: projectId });
|
|
57
|
+
const statusField = node.fields.nodes.find(f => f.name === 'Status');
|
|
58
|
+
if (!statusField) { core.setFailed('Status field not found on board'); return; }
|
|
59
|
+
const opt = (name) => statusField.options.find(o => o.name.includes(name))?.id ?? '';
|
|
60
|
+
const ids = {
|
|
61
|
+
brainstorming: opt('Brainstorming'),
|
|
62
|
+
detailing: opt('Detailing'),
|
|
63
|
+
approval: opt('Approval'),
|
|
64
|
+
development: opt('Development'),
|
|
65
|
+
code_review_pr: opt('Code Review'),
|
|
66
|
+
production: opt('Production'),
|
|
78
67
|
};
|
|
68
|
+
const missing = Object.entries(ids).filter(([, v]) => !v).map(([k]) => k);
|
|
69
|
+
if (missing.length > 0) {
|
|
70
|
+
core.setFailed(`Required Status columns not found on board: ${missing.join(', ')}`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
core.setOutput('project_id', projectId);
|
|
74
|
+
core.setOutput('status_field_id', statusField.id);
|
|
75
|
+
core.setOutput('status_brainstorming', ids.brainstorming);
|
|
76
|
+
core.setOutput('status_detailing', ids.detailing);
|
|
77
|
+
core.setOutput('status_approval', ids.approval);
|
|
78
|
+
core.setOutput('status_development', ids.development);
|
|
79
|
+
core.setOutput('status_code_review_pr', ids.code_review_pr);
|
|
80
|
+
core.setOutput('status_production', ids.production);
|
|
81
|
+
console.log('✅ Board IDs resolved by name');
|
|
79
82
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
# name: architecture-violation → 💡 Idea
|
|
86
|
-
# if: github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'architecture-violation'
|
|
87
|
-
# runs-on: ubuntu-latest
|
|
88
|
-
# steps:
|
|
89
|
-
# - name: Move issue to Idea
|
|
90
|
-
# if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
|
|
91
|
-
# uses: actions/github-script@v8
|
|
92
|
-
# with:
|
|
93
|
-
# github-token: ${{ env.PROJECT_TOKEN }}
|
|
94
|
-
# script: |
|
|
95
|
-
# const { issue: { number, node_id } } = context.payload;
|
|
96
|
-
# const { addProjectV2ItemById: { item } } = await github.graphql(`
|
|
97
|
-
# mutation($p: ID!, $c: ID!) {
|
|
98
|
-
# addProjectV2ItemById(input: {projectId: $p, contentId: $c}) { item { id } }
|
|
99
|
-
# }`, { p: process.env.PROJECT_ID, c: node_id });
|
|
100
|
-
# await github.graphql(`
|
|
101
|
-
# mutation($p: ID!, $i: ID!, $f: ID!, $v: ProjectV2FieldValue!) {
|
|
102
|
-
# updateProjectV2ItemFieldValue(input: {projectId: $p, itemId: $i, fieldId: $f, value: $v}) {
|
|
103
|
-
# projectV2Item { id }
|
|
104
|
-
# }
|
|
105
|
-
# }`, {
|
|
106
|
-
# p: process.env.PROJECT_ID, i: item.id, f: process.env.STATUS_FIELD_ID,
|
|
107
|
-
# v: { singleSelectOptionId: process.env.STATUS_IDEA }
|
|
108
|
-
# });
|
|
109
|
-
# console.log(`Issue #${number} moved to Idea`);
|
|
110
|
-
|
|
111
|
-
# PR Opened → Move linked issue to Code Review / PR
|
|
112
|
-
move-card-on-pr-open:
|
|
113
|
-
name: Open PR → Code Review / PR
|
|
114
|
-
if: github.event_name == 'pull_request' && (github.event.action == 'opened' || github.event.action == 'reopened')
|
|
83
|
+
# Single dispatcher — f(event) → target column → move card
|
|
84
|
+
move-card:
|
|
85
|
+
name: Board Dispatcher
|
|
86
|
+
needs: [resolve-ids]
|
|
87
|
+
if: needs.resolve-ids.outputs.project_id != ''
|
|
115
88
|
runs-on: ubuntu-latest
|
|
116
89
|
env:
|
|
117
90
|
PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
|
|
91
|
+
PROJECT_ID: ${{ needs.resolve-ids.outputs.project_id }}
|
|
92
|
+
STATUS_FIELD_ID: ${{ needs.resolve-ids.outputs.status_field_id }}
|
|
93
|
+
STATUS_BRAINSTORMING: ${{ needs.resolve-ids.outputs.status_brainstorming }}
|
|
94
|
+
STATUS_DETAILING: ${{ needs.resolve-ids.outputs.status_detailing }}
|
|
95
|
+
STATUS_APPROVAL: ${{ needs.resolve-ids.outputs.status_approval }}
|
|
96
|
+
STATUS_DEVELOPMENT: ${{ needs.resolve-ids.outputs.status_development }}
|
|
97
|
+
STATUS_CODE_REVIEW_PR: ${{ needs.resolve-ids.outputs.status_code_review_pr }}
|
|
98
|
+
STATUS_PRODUCTION: ${{ needs.resolve-ids.outputs.status_production }}
|
|
118
99
|
steps:
|
|
119
|
-
-
|
|
120
|
-
|
|
121
|
-
|
|
100
|
+
- uses: actions/checkout@v5.0.1
|
|
101
|
+
- name: Dispatch board move
|
|
102
|
+
if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '' }}
|
|
103
|
+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
|
122
104
|
with:
|
|
123
105
|
github-token: ${{ env.PROJECT_TOKEN }}
|
|
124
106
|
script: |
|
|
125
|
-
const
|
|
107
|
+
const { classifyItem } = require('./scripts/derive-column.js');
|
|
126
108
|
const { owner, repo } = context.repo;
|
|
109
|
+
const eventName = context.eventName;
|
|
110
|
+
const action = context.payload.action;
|
|
127
111
|
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
112
|
+
const COLUMN_KEY_TO_ENV = {
|
|
113
|
+
'code_review_pr': 'STATUS_CODE_REVIEW_PR',
|
|
114
|
+
'development': 'STATUS_DEVELOPMENT',
|
|
115
|
+
'approval': 'STATUS_APPROVAL',
|
|
116
|
+
'detailing': 'STATUS_DETAILING',
|
|
117
|
+
'brainstorming': 'STATUS_BRAINSTORMING',
|
|
118
|
+
};
|
|
133
119
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
addProjectV2ItemById(input: {projectId: $p, contentId: $c}) { item { id } }
|
|
138
|
-
}`, { p: process.env.PROJECT_ID, c: nodeId });
|
|
120
|
+
// f(event) → { targetStatusId, linkedNodeIds, stageLabelsToClear, prLabelSwap }
|
|
121
|
+
async function deriveMove() {
|
|
122
|
+
const col = (key) => process.env[key];
|
|
139
123
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
124
|
+
// Issue labeled → stage column (uses shared classify logic)
|
|
125
|
+
if (eventName === 'issues' && action === 'labeled') {
|
|
126
|
+
const labelNames = (context.payload.issue.labels ?? []).map(l => l.name);
|
|
127
|
+
const columnKey = classifyItem(labelNames);
|
|
128
|
+
const targetStatusId = columnKey ? col(COLUMN_KEY_TO_ENV[columnKey]) : null;
|
|
129
|
+
if (!targetStatusId) return null;
|
|
130
|
+
return {
|
|
131
|
+
targetStatusId,
|
|
132
|
+
nodeIds: [context.payload.issue.node_id],
|
|
133
|
+
issueNumbers: [],
|
|
134
|
+
stageLabelsToClear: [],
|
|
135
|
+
prLabelSwap: null,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
150
138
|
|
|
151
|
-
|
|
139
|
+
// PR opened/reopened → Code Review
|
|
140
|
+
if (eventName === 'pull_request' && (action === 'opened' || action === 'reopened')) {
|
|
141
|
+
const prNumber = context.payload.pull_request.number;
|
|
142
|
+
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
|
|
143
|
+
const linkedIssues = [...(pr.body ?? '').matchAll(/(?:Closes?|Fixes?|Resolves?)\s+#(\d+)/gi)]
|
|
144
|
+
.map(m => parseInt(m[1]));
|
|
145
|
+
const stageLabels = ['stage:development', 'stage:brainstorming', 'stage:detailing', 'jules'];
|
|
152
146
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
147
|
+
if (linkedIssues.length > 0) {
|
|
148
|
+
const nodes = await Promise.all(linkedIssues.map(async n => {
|
|
149
|
+
const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number: n });
|
|
150
|
+
return { nodeId: issue.node_id, issueNumber: n };
|
|
151
|
+
}));
|
|
152
|
+
return {
|
|
153
|
+
targetStatusId: col('STATUS_CODE_REVIEW_PR'),
|
|
154
|
+
nodeIds: nodes.map(n => n.nodeId),
|
|
155
|
+
issueNumbers: nodes.map(n => n.issueNumber),
|
|
156
|
+
stageLabelsToClear: stageLabels,
|
|
157
|
+
prLabelSwap: { add: 'pr:in-review', prNumber },
|
|
158
|
+
};
|
|
159
|
+
} else {
|
|
160
|
+
return {
|
|
161
|
+
targetStatusId: col('STATUS_CODE_REVIEW_PR'),
|
|
162
|
+
nodeIds: [context.payload.pull_request.node_id],
|
|
163
|
+
issueNumbers: [],
|
|
164
|
+
stageLabelsToClear: [],
|
|
165
|
+
prLabelSwap: { add: 'pr:in-review', prNumber },
|
|
166
|
+
};
|
|
159
167
|
}
|
|
160
|
-
console.log(`Issue #${n} → Code Review / PR`);
|
|
161
168
|
}
|
|
162
|
-
} else {
|
|
163
|
-
await moveItem(pr.node_id);
|
|
164
|
-
console.log(`PR #${prNumber} → Code Review / PR (no linked issue)`);
|
|
165
|
-
}
|
|
166
169
|
|
|
167
|
-
|
|
170
|
+
// PR review approved → Code Review + swap PR labels
|
|
171
|
+
if (eventName === 'pull_request_review' && context.payload.review.state === 'approved') {
|
|
172
|
+
const prNumber = context.payload.pull_request.number;
|
|
173
|
+
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
|
|
174
|
+
const linkedIssues = [...(pr.body ?? '').matchAll(/(?:Closes?|Fixes?|Resolves?)\s+#(\d+)/gi)]
|
|
175
|
+
.map(m => parseInt(m[1]));
|
|
168
176
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
177
|
+
if (linkedIssues.length > 0) {
|
|
178
|
+
const nodes = await Promise.all(linkedIssues.map(async n => {
|
|
179
|
+
const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number: n });
|
|
180
|
+
return { nodeId: issue.node_id, issueNumber: n };
|
|
181
|
+
}));
|
|
182
|
+
return {
|
|
183
|
+
targetStatusId: col('STATUS_CODE_REVIEW_PR'),
|
|
184
|
+
nodeIds: nodes.map(n => n.nodeId),
|
|
185
|
+
issueNumbers: nodes.map(n => n.issueNumber),
|
|
186
|
+
stageLabelsToClear: [],
|
|
187
|
+
prLabelSwap: { remove: 'pr:in-review', add: 'pr:approved', prNumber },
|
|
188
|
+
};
|
|
189
|
+
} else {
|
|
190
|
+
return {
|
|
191
|
+
targetStatusId: col('STATUS_CODE_REVIEW_PR'),
|
|
192
|
+
nodeIds: [context.payload.pull_request.node_id],
|
|
193
|
+
issueNumbers: [],
|
|
194
|
+
stageLabelsToClear: [],
|
|
195
|
+
prLabelSwap: { remove: 'pr:in-review', add: 'pr:approved', prNumber },
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
185
199
|
|
|
186
|
-
|
|
187
|
-
|
|
200
|
+
// PR merged → Production
|
|
201
|
+
if (eventName === 'pull_request' && action === 'closed' && context.payload.pull_request.merged) {
|
|
202
|
+
const prNumber = context.payload.pull_request.number;
|
|
203
|
+
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
|
|
204
|
+
const linkedIssues = [...(pr.body ?? '').matchAll(/(?:Closes?|Fixes?|Resolves?)\s+#(\d+)/gi)]
|
|
205
|
+
.map(m => parseInt(m[1]));
|
|
188
206
|
|
|
189
|
-
|
|
190
|
-
|
|
207
|
+
if (linkedIssues.length > 0) {
|
|
208
|
+
const nodes = await Promise.all(linkedIssues.map(async n => {
|
|
209
|
+
const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number: n });
|
|
210
|
+
return { nodeId: issue.node_id, issueNumber: n };
|
|
211
|
+
}));
|
|
212
|
+
return {
|
|
213
|
+
targetStatusId: col('STATUS_PRODUCTION'),
|
|
214
|
+
nodeIds: nodes.map(n => n.nodeId),
|
|
215
|
+
issueNumbers: nodes.map(n => n.issueNumber),
|
|
216
|
+
stageLabelsToClear: ['stage:approval'],
|
|
217
|
+
prLabelSwap: null,
|
|
218
|
+
};
|
|
219
|
+
} else {
|
|
220
|
+
return {
|
|
221
|
+
targetStatusId: col('STATUS_PRODUCTION'),
|
|
222
|
+
nodeIds: [context.payload.pull_request.node_id],
|
|
223
|
+
issueNumbers: [],
|
|
224
|
+
stageLabelsToClear: [],
|
|
225
|
+
prLabelSwap: null,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
191
232
|
|
|
192
|
-
|
|
233
|
+
// Move a single item to target column
|
|
234
|
+
async function moveItem(nodeId, targetStatusId) {
|
|
193
235
|
const { addProjectV2ItemById: { item } } = await github.graphql(`
|
|
194
236
|
mutation($p: ID!, $c: ID!) {
|
|
195
237
|
addProjectV2ItemById(input: {projectId: $p, contentId: $c}) { item { id } }
|
|
@@ -202,81 +244,86 @@ jobs:
|
|
|
202
244
|
}
|
|
203
245
|
}`, {
|
|
204
246
|
p: process.env.PROJECT_ID, i: item.id, f: process.env.STATUS_FIELD_ID,
|
|
205
|
-
v: { singleSelectOptionId:
|
|
247
|
+
v: { singleSelectOptionId: targetStatusId }
|
|
206
248
|
});
|
|
207
|
-
}
|
|
249
|
+
}
|
|
208
250
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
await github.rest.issues.removeLabel({ owner, repo, issue_number: n, name: 'stage:testing' }).catch(() => {});
|
|
215
|
-
}
|
|
216
|
-
console.log(`Issue #${n} → Code Review / PR`);
|
|
217
|
-
}
|
|
218
|
-
} else {
|
|
219
|
-
await moveItem(pr.node_id);
|
|
220
|
-
console.log(`PR #${prNumber} → Code Review / PR (no linked issue)`);
|
|
251
|
+
// Execute
|
|
252
|
+
const move = await deriveMove();
|
|
253
|
+
if (!move) {
|
|
254
|
+
console.log('No board move needed for this event. Skipping.');
|
|
255
|
+
return;
|
|
221
256
|
}
|
|
222
257
|
|
|
223
|
-
|
|
224
|
-
|
|
258
|
+
for (const nodeId of move.nodeIds) {
|
|
259
|
+
await moveItem(nodeId, move.targetStatusId);
|
|
260
|
+
}
|
|
225
261
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
|
|
233
|
-
steps:
|
|
234
|
-
- name: Move issue to Production
|
|
235
|
-
if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
|
|
236
|
-
uses: actions/github-script@v8
|
|
237
|
-
with:
|
|
238
|
-
github-token: ${{ env.PROJECT_TOKEN }}
|
|
239
|
-
script: |
|
|
240
|
-
const prNumber = context.payload.pull_request.number;
|
|
241
|
-
const { owner, repo } = context.repo;
|
|
242
|
-
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
|
|
243
|
-
const body = pr.body ?? '';
|
|
244
|
-
const linkedIssues = [...body.matchAll(/(?:Closes?|Fixes?|Resolves?)\s+#(\d+)/gi)].map(m => parseInt(m[1]));
|
|
262
|
+
for (const issueNumber of move.issueNumbers) {
|
|
263
|
+
for (const label of move.stageLabelsToClear) {
|
|
264
|
+
await github.rest.issues.removeLabel({ owner, repo, issue_number: issueNumber, name: label })
|
|
265
|
+
.catch(e => { if (e.status !== 404) { core.error(`Failed to remove label ${label}: ${e.message}`); throw e; } });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
245
268
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
addProjectV2ItemById(input: {projectId: $p, contentId: $c}) { item { id } }
|
|
250
|
-
}`, { p: process.env.PROJECT_ID, c: nodeId });
|
|
251
|
-
await github.graphql(`
|
|
252
|
-
mutation($p: ID!, $i: ID!, $f: ID!, $v: ProjectV2FieldValue!) {
|
|
253
|
-
updateProjectV2ItemFieldValue(input: {projectId: $p, itemId: $i, fieldId: $f, value: $v}) {
|
|
254
|
-
projectV2Item { id }
|
|
255
|
-
}
|
|
256
|
-
}`, { p: process.env.PROJECT_ID, i: item.id, f: process.env.STATUS_FIELD_ID,
|
|
257
|
-
v: { singleSelectOptionId: process.env.STATUS_PRODUCTION } });
|
|
258
|
-
};
|
|
269
|
+
if (move.stageLabelsToClear.length > 0 && move.issueNumbers.length === 0) {
|
|
270
|
+
// no linked issue numbers tracked for stage-label events (issue is identified by nodeId only)
|
|
271
|
+
}
|
|
259
272
|
|
|
260
|
-
if (
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
await
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
273
|
+
if (move.prLabelSwap) {
|
|
274
|
+
const { prNumber, remove, add } = move.prLabelSwap;
|
|
275
|
+
if (remove) {
|
|
276
|
+
await github.rest.issues.removeLabel({ owner, repo, issue_number: prNumber, name: remove })
|
|
277
|
+
.catch(e => { if (e.status !== 404) { core.error(`Failed to remove PR label ${remove}: ${e.message}`); throw e; } });
|
|
278
|
+
}
|
|
279
|
+
if (add) {
|
|
280
|
+
await github.rest.issues.addLabels({ owner, repo, issue_number: prNumber, labels: [add] });
|
|
268
281
|
}
|
|
269
|
-
} else {
|
|
270
|
-
await moveItem(pr.node_id);
|
|
271
282
|
}
|
|
272
283
|
|
|
284
|
+
console.log(`✅ Board move complete → ${move.targetStatusId} (${move.nodeIds.length} item(s))`);
|
|
285
|
+
|
|
286
|
+
# OPTIONAL: Uncomment to enable architecture-violation → Idea
|
|
287
|
+
# move-violation-to-board:
|
|
288
|
+
# name: architecture-violation → 💡 Idea
|
|
289
|
+
# needs: [resolve-ids]
|
|
290
|
+
# if: github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'architecture-violation'
|
|
291
|
+
# runs-on: ubuntu-latest
|
|
292
|
+
# env:
|
|
293
|
+
# PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
|
|
294
|
+
# PROJECT_ID: ${{ needs.resolve-ids.outputs.project_id }}
|
|
295
|
+
# STATUS_FIELD_ID: ${{ needs.resolve-ids.outputs.status_field_id }}
|
|
296
|
+
# STATUS_IDEA: ${{ needs.resolve-ids.outputs.status_idea }}
|
|
297
|
+
# steps:
|
|
298
|
+
# - name: Move issue to Idea
|
|
299
|
+
# if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '' }}
|
|
300
|
+
# uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
|
301
|
+
# with:
|
|
302
|
+
# github-token: ${{ env.PROJECT_TOKEN }}
|
|
303
|
+
# script: |
|
|
304
|
+
# const { issue: { number, node_id } } = context.payload;
|
|
305
|
+
# const { addProjectV2ItemById: { item } } = await github.graphql(`
|
|
306
|
+
# mutation($p: ID!, $c: ID!) {
|
|
307
|
+
# addProjectV2ItemById(input: {projectId: $p, contentId: $c}) { item { id } }
|
|
308
|
+
# }`, { p: process.env.PROJECT_ID, c: node_id });
|
|
309
|
+
# await github.graphql(`
|
|
310
|
+
# mutation($p: ID!, $i: ID!, $f: ID!, $v: ProjectV2FieldValue!) {
|
|
311
|
+
# updateProjectV2ItemFieldValue(input: {projectId: $p, itemId: $i, fieldId: $f, value: $v}) {
|
|
312
|
+
# projectV2Item { id }
|
|
313
|
+
# }
|
|
314
|
+
# }`, {
|
|
315
|
+
# p: process.env.PROJECT_ID, i: item.id, f: process.env.STATUS_FIELD_ID,
|
|
316
|
+
# v: { singleSelectOptionId: process.env.STATUS_IDEA }
|
|
317
|
+
# });
|
|
318
|
+
# console.log(`Issue #${number} moved to Idea`);
|
|
319
|
+
|
|
273
320
|
cleanup-labels-on-close:
|
|
274
321
|
name: Issue closed → strip stage/agent labels
|
|
275
322
|
if: github.event_name == 'issues' && github.event.action == 'closed'
|
|
276
323
|
runs-on: ubuntu-latest
|
|
277
324
|
steps:
|
|
278
325
|
- name: Remove transient labels
|
|
279
|
-
uses: actions/github-script@v8
|
|
326
|
+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
|
280
327
|
with:
|
|
281
328
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
282
329
|
script: |
|
|
@@ -284,7 +331,7 @@ jobs:
|
|
|
284
331
|
const issue_number = context.payload.issue.number;
|
|
285
332
|
const toRemove = [
|
|
286
333
|
'stage:brainstorming', 'stage:detailing',
|
|
287
|
-
'stage:approval', 'stage:development',
|
|
334
|
+
'stage:approval', 'stage:development',
|
|
288
335
|
'pr:in-review', 'jules'
|
|
289
336
|
];
|
|
290
337
|
for (const label of toRemove) {
|
|
@@ -58,22 +58,22 @@ If any of these files are missing, you are in **Setup Mode**. Do not proceed wit
|
|
|
58
58
|
- **CLAUDE.md:** If `.agentic-pdlc/templates/CLAUDE.md` exists and `CLAUDE.md` does not yet exist at the project root, write it — replacing only `{{PROJECT_NAME}}` with the project name. Skip if CLAUDE.md already exists (never downgrade).
|
|
59
59
|
- **Lite profile:** If `cli-context.json` has `"profile": "lite"`, skip steps that reference `docs/pdlc.md`, `project-automation.yml`, `agent-trigger.yml`, and `pdlc-health-check.yml` — these are not installed in lite.
|
|
60
60
|
6. Offer to run the `gh` commands for labels (`spec:approved`, `pr:in-review`, `pr:approved`, `architecture-violation`).
|
|
61
|
-
7. **`
|
|
61
|
+
7. **`PROJECT_TOKEN` secret (required for board automation):**
|
|
62
62
|
|
|
63
63
|
Read `patAutoSet` from `.agentic-pdlc/cli-context.json`:
|
|
64
64
|
|
|
65
|
-
**If `patAutoSet === true`:** The CLI already configured this secret automatically. Print `✅
|
|
65
|
+
**If `patAutoSet === true`:** The CLI already configured this secret automatically. Print `✅ PROJECT_TOKEN is configured.` and continue to Step 8 — do not ask the user anything.
|
|
66
66
|
|
|
67
67
|
**If `patAutoSet === false` (org repo):** Show the block below and wait for the user to reply "done" or "secret set" before continuing:
|
|
68
68
|
|
|
69
|
-
> Your repo is in an organization. For security, `
|
|
69
|
+
> Your repo is in an organization. For security, `PROJECT_TOKEN` must be a dedicated PAT (not your personal OAuth token). Without it, all board card movements in CI will silently skip — no error surfaced.
|
|
70
70
|
>
|
|
71
71
|
> 1. Open: **github.com/settings/tokens** → *Generate new token (classic)*
|
|
72
|
-
> 2. Name: `
|
|
72
|
+
> 2. Name: `PROJECT_TOKEN — <repo-name>`
|
|
73
73
|
> 3. Select scopes: ✅ `repo` + ✅ `project`
|
|
74
74
|
> 4. Copy the token, then run:
|
|
75
75
|
> ```
|
|
76
|
-
> gh secret set
|
|
76
|
+
> gh secret set PROJECT_TOKEN --body "<your-token>" --repo <owner>/<repo>
|
|
77
77
|
> ```
|
|
78
78
|
> 5. Reply **"done"** when finished.
|
|
79
79
|
8. **IMPORTANT:** Delete the setup prompt file by running exactly:
|