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
package/.coderabbit.yaml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
language: "en-US"
|
|
2
2
|
reviews:
|
|
3
|
-
profile: "
|
|
3
|
+
profile: "assertive"
|
|
4
4
|
request_changes_workflow: false
|
|
5
5
|
high_level_summary: true
|
|
6
6
|
poem: false
|
|
@@ -10,6 +10,12 @@ reviews:
|
|
|
10
10
|
base_branches:
|
|
11
11
|
- main
|
|
12
12
|
path_instructions:
|
|
13
|
+
- path: "bin/cli.js"
|
|
14
|
+
instructions: >
|
|
15
|
+
All user-facing console.log strings must use the t() translation helper.
|
|
16
|
+
New strings must be added to the i18n object and referenced via i18n.key_name.
|
|
17
|
+
Flag any hardcoded English-only string in console output as a violation of
|
|
18
|
+
the project's i18n pattern.
|
|
13
19
|
- path: ".github/workflows/**"
|
|
14
20
|
instructions: >
|
|
15
21
|
Pay close attention to GitHub Actions syntax correctness, trigger
|
|
@@ -4,21 +4,69 @@ on:
|
|
|
4
4
|
issues:
|
|
5
5
|
types: [opened]
|
|
6
6
|
|
|
7
|
-
env:
|
|
8
|
-
PROJECT_ID: "PVT_kwHODpFFL84BXg7h"
|
|
9
|
-
STATUS_FIELD_ID: "PVTSSF_lAHODpFFL84BXg7hzhStRHI"
|
|
10
|
-
STATUS_IDEA: "bb6e5a20"
|
|
11
|
-
|
|
12
7
|
jobs:
|
|
8
|
+
resolve-ids:
|
|
9
|
+
name: Resolve Board Column IDs
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
env:
|
|
12
|
+
PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
|
|
13
|
+
outputs:
|
|
14
|
+
project_id: ${{ steps.resolve.outputs.project_id }}
|
|
15
|
+
status_field_id: ${{ steps.resolve.outputs.status_field_id }}
|
|
16
|
+
status_idea: ${{ steps.resolve.outputs.status_idea }}
|
|
17
|
+
steps:
|
|
18
|
+
- name: Resolve column IDs by name
|
|
19
|
+
id: resolve
|
|
20
|
+
if: ${{ env.PROJECT_TOKEN != '' && vars.PROJECT_ID != '' }}
|
|
21
|
+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
|
22
|
+
with:
|
|
23
|
+
github-token: ${{ env.PROJECT_TOKEN }}
|
|
24
|
+
script: |
|
|
25
|
+
const projectId = '${{ vars.PROJECT_ID }}';
|
|
26
|
+
if (!projectId || projectId === '{{PROJECT_ID}}') {
|
|
27
|
+
core.warning('vars.PROJECT_ID not set — board features disabled');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const { node } = await github.graphql(`
|
|
31
|
+
query($id: ID!) {
|
|
32
|
+
node(id: $id) {
|
|
33
|
+
... on ProjectV2 {
|
|
34
|
+
fields(first: 20) {
|
|
35
|
+
nodes {
|
|
36
|
+
... on ProjectV2SingleSelectField {
|
|
37
|
+
id
|
|
38
|
+
name
|
|
39
|
+
options { id name }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
`, { id: projectId });
|
|
47
|
+
const statusField = node.fields.nodes.find(f => f.name === 'Status');
|
|
48
|
+
if (!statusField) { core.setFailed('Status field not found on board'); return; }
|
|
49
|
+
const opt = (name) => statusField.options.find(o => o.name.includes(name))?.id ?? '';
|
|
50
|
+
const ideaId = opt('Idea');
|
|
51
|
+
if (!ideaId) { core.setFailed('Required Status column "Idea" not found on board'); return; }
|
|
52
|
+
core.setOutput('project_id', projectId);
|
|
53
|
+
core.setOutput('status_field_id', statusField.id);
|
|
54
|
+
core.setOutput('status_idea', ideaId);
|
|
55
|
+
console.log('✅ Board IDs resolved by name');
|
|
56
|
+
|
|
13
57
|
add-to-board:
|
|
14
58
|
name: Auto-add new issue to board
|
|
59
|
+
needs: [resolve-ids]
|
|
15
60
|
runs-on: ubuntu-latest
|
|
16
61
|
env:
|
|
17
62
|
PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
|
|
63
|
+
PROJECT_ID: ${{ needs.resolve-ids.outputs.project_id }}
|
|
64
|
+
STATUS_FIELD_ID: ${{ needs.resolve-ids.outputs.status_field_id }}
|
|
65
|
+
STATUS_IDEA: ${{ needs.resolve-ids.outputs.status_idea }}
|
|
18
66
|
steps:
|
|
19
67
|
- name: Add issue to project board
|
|
20
|
-
if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '
|
|
21
|
-
uses: actions/github-script@v8
|
|
68
|
+
if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '' }}
|
|
69
|
+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
|
22
70
|
with:
|
|
23
71
|
github-token: ${{ env.PROJECT_TOKEN }}
|
|
24
72
|
script: |
|
|
@@ -7,9 +7,56 @@ on:
|
|
|
7
7
|
types: [labeled]
|
|
8
8
|
|
|
9
9
|
jobs:
|
|
10
|
+
resolve-ids:
|
|
11
|
+
name: Resolve Board Column IDs
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
env:
|
|
14
|
+
PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
|
|
15
|
+
outputs:
|
|
16
|
+
project_id: ${{ steps.resolve.outputs.project_id }}
|
|
17
|
+
status_field_id: ${{ steps.resolve.outputs.status_field_id }}
|
|
18
|
+
status_development: ${{ steps.resolve.outputs.status_development }}
|
|
19
|
+
steps:
|
|
20
|
+
- name: Resolve column IDs by name
|
|
21
|
+
id: resolve
|
|
22
|
+
if: ${{ env.PROJECT_TOKEN != '' && vars.PROJECT_ID != '' }}
|
|
23
|
+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
|
24
|
+
with:
|
|
25
|
+
github-token: ${{ env.PROJECT_TOKEN }}
|
|
26
|
+
script: |
|
|
27
|
+
const projectId = '${{ vars.PROJECT_ID }}';
|
|
28
|
+
if (!projectId || projectId === '{{PROJECT_ID}}') {
|
|
29
|
+
core.warning('vars.PROJECT_ID not set — board features disabled');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const { node } = await github.graphql(`
|
|
33
|
+
query($id: ID!) {
|
|
34
|
+
node(id: $id) {
|
|
35
|
+
... on ProjectV2 {
|
|
36
|
+
fields(first: 20) {
|
|
37
|
+
nodes {
|
|
38
|
+
... on ProjectV2SingleSelectField {
|
|
39
|
+
id
|
|
40
|
+
name
|
|
41
|
+
options { id name }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
`, { id: projectId });
|
|
49
|
+
const statusField = node.fields.nodes.find(f => f.name === 'Status');
|
|
50
|
+
if (!statusField) { core.setFailed('Status field not found on board'); return; }
|
|
51
|
+
const opt = (name) => statusField.options.find(o => o.name.includes(name))?.id ?? '';
|
|
52
|
+
core.setOutput('project_id', projectId);
|
|
53
|
+
core.setOutput('status_field_id', statusField.id);
|
|
54
|
+
core.setOutput('status_development', opt('Development'));
|
|
55
|
+
console.log('✅ Board IDs resolved by name');
|
|
56
|
+
|
|
10
57
|
trigger-implementation-agent:
|
|
11
58
|
name: Advance issue to stage:development
|
|
12
|
-
|
|
59
|
+
needs: [resolve-ids]
|
|
13
60
|
if: github.event.label.name == 'spec:approved'
|
|
14
61
|
runs-on: ubuntu-latest
|
|
15
62
|
permissions:
|
|
@@ -17,40 +64,28 @@ jobs:
|
|
|
17
64
|
contents: read
|
|
18
65
|
env:
|
|
19
66
|
PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
|
|
20
|
-
PROJECT_ID:
|
|
21
|
-
STATUS_FIELD_ID:
|
|
22
|
-
STATUS_DEVELOPMENT:
|
|
67
|
+
PROJECT_ID: ${{ needs.resolve-ids.outputs.project_id }}
|
|
68
|
+
STATUS_FIELD_ID: ${{ needs.resolve-ids.outputs.status_field_id }}
|
|
69
|
+
STATUS_DEVELOPMENT: ${{ needs.resolve-ids.outputs.status_development }}
|
|
23
70
|
steps:
|
|
24
71
|
- name: Swap labels — stage:approval → stage:development
|
|
25
|
-
uses: actions/github-script@v8
|
|
72
|
+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
|
26
73
|
with:
|
|
27
74
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
28
75
|
script: |
|
|
29
76
|
const { owner, repo } = context.repo;
|
|
30
77
|
const issue_number = context.payload.issue.number;
|
|
31
|
-
|
|
32
78
|
try {
|
|
33
|
-
await github.rest.issues.removeLabel({
|
|
34
|
-
owner,
|
|
35
|
-
repo,
|
|
36
|
-
issue_number,
|
|
37
|
-
name: 'stage:approval'
|
|
38
|
-
});
|
|
79
|
+
await github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'stage:approval' });
|
|
39
80
|
} catch (error) {
|
|
40
81
|
console.log('Label stage:approval not found or could not be removed');
|
|
41
82
|
}
|
|
42
|
-
|
|
43
|
-
await github.rest.issues.addLabels({
|
|
44
|
-
owner,
|
|
45
|
-
repo,
|
|
46
|
-
issue_number,
|
|
47
|
-
labels: ['stage:development']
|
|
48
|
-
});
|
|
83
|
+
await github.rest.issues.addLabels({ owner, repo, issue_number, labels: ['stage:development'] });
|
|
49
84
|
|
|
50
85
|
- name: Move board card to Development
|
|
51
|
-
if: ${{ env.PROJECT_TOKEN != '' }}
|
|
86
|
+
if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '' }}
|
|
52
87
|
continue-on-error: true
|
|
53
|
-
uses: actions/github-script@v8
|
|
88
|
+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
|
54
89
|
with:
|
|
55
90
|
github-token: ${{ env.PROJECT_TOKEN }}
|
|
56
91
|
script: |
|
|
@@ -71,7 +106,6 @@ jobs:
|
|
|
71
106
|
|
|
72
107
|
trigger-agent-on-violation:
|
|
73
108
|
name: Flag architecture violation
|
|
74
|
-
# Runs when architecture-violation is added (Sentinel flow)
|
|
75
109
|
if: github.event.label.name == 'architecture-violation'
|
|
76
110
|
runs-on: ubuntu-latest
|
|
77
111
|
permissions:
|
|
@@ -79,12 +113,11 @@ jobs:
|
|
|
79
113
|
contents: read
|
|
80
114
|
steps:
|
|
81
115
|
- name: Comment on issue
|
|
82
|
-
uses: actions/github-script@v8
|
|
116
|
+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
|
83
117
|
with:
|
|
84
118
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
85
119
|
script: |
|
|
86
120
|
const issueNumber = context.payload.issue.number;
|
|
87
|
-
|
|
88
121
|
const body = [
|
|
89
122
|
`⚠️ **Architecture violation detected.** Please fix the issue described above.`,
|
|
90
123
|
'',
|
|
@@ -96,7 +129,6 @@ jobs:
|
|
|
96
129
|
'- Fix only what the violation points out — do not refactor unrelated code',
|
|
97
130
|
`- Include \`Closes #${issueNumber}\` in the PR body`,
|
|
98
131
|
].join('\n');
|
|
99
|
-
|
|
100
132
|
await github.rest.issues.createComment({
|
|
101
133
|
owner: context.repo.owner,
|
|
102
134
|
repo: context.repo.repo,
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
name: Board Reconciliation (Drift Heal)
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
- cron: '0 */6 * * *'
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
resolve-ids:
|
|
10
|
+
name: Resolve Board Column IDs
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
env:
|
|
13
|
+
PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
|
|
14
|
+
outputs:
|
|
15
|
+
project_id: ${{ steps.resolve.outputs.project_id }}
|
|
16
|
+
status_field_id: ${{ steps.resolve.outputs.status_field_id }}
|
|
17
|
+
status_brainstorming: ${{ steps.resolve.outputs.status_brainstorming }}
|
|
18
|
+
status_detailing: ${{ steps.resolve.outputs.status_detailing }}
|
|
19
|
+
status_approval: ${{ steps.resolve.outputs.status_approval }}
|
|
20
|
+
status_development: ${{ steps.resolve.outputs.status_development }}
|
|
21
|
+
status_code_review_pr: ${{ steps.resolve.outputs.status_code_review_pr }}
|
|
22
|
+
steps:
|
|
23
|
+
- name: Resolve column IDs by name
|
|
24
|
+
id: resolve
|
|
25
|
+
if: ${{ env.PROJECT_TOKEN != '' && vars.PROJECT_ID != '' }}
|
|
26
|
+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
|
27
|
+
with:
|
|
28
|
+
github-token: ${{ env.PROJECT_TOKEN }}
|
|
29
|
+
script: |
|
|
30
|
+
const projectId = '${{ vars.PROJECT_ID }}';
|
|
31
|
+
if (!projectId || projectId === '{{PROJECT_ID}}') {
|
|
32
|
+
core.warning('vars.PROJECT_ID not set — board features disabled');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const { node } = await github.graphql(`
|
|
36
|
+
query($id: ID!) {
|
|
37
|
+
node(id: $id) {
|
|
38
|
+
... on ProjectV2 {
|
|
39
|
+
fields(first: 20) {
|
|
40
|
+
nodes {
|
|
41
|
+
... on ProjectV2SingleSelectField {
|
|
42
|
+
id
|
|
43
|
+
name
|
|
44
|
+
options { id name }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
`, { id: projectId });
|
|
52
|
+
const statusField = node.fields.nodes.find(f => f.name === 'Status');
|
|
53
|
+
if (!statusField) { core.setFailed('Status field not found on board'); return; }
|
|
54
|
+
const opt = (name) => statusField.options.find(o => o.name.includes(name))?.id ?? '';
|
|
55
|
+
const ids = {
|
|
56
|
+
brainstorming: opt('Brainstorming'),
|
|
57
|
+
detailing: opt('Detailing'),
|
|
58
|
+
approval: opt('Approval'),
|
|
59
|
+
development: opt('Development'),
|
|
60
|
+
code_review_pr: opt('Code Review'),
|
|
61
|
+
};
|
|
62
|
+
const missing = Object.entries(ids).filter(([, v]) => !v).map(([k]) => k);
|
|
63
|
+
if (missing.length > 0) {
|
|
64
|
+
core.setFailed(`Required Status columns not found on board: ${missing.join(', ')}`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
core.setOutput('project_id', projectId);
|
|
68
|
+
core.setOutput('status_field_id', statusField.id);
|
|
69
|
+
core.setOutput('status_brainstorming', ids.brainstorming);
|
|
70
|
+
core.setOutput('status_detailing', ids.detailing);
|
|
71
|
+
core.setOutput('status_approval', ids.approval);
|
|
72
|
+
core.setOutput('status_development', ids.development);
|
|
73
|
+
core.setOutput('status_code_review_pr', ids.code_review_pr);
|
|
74
|
+
console.log('✅ Board IDs resolved by name');
|
|
75
|
+
|
|
76
|
+
reconcile-board:
|
|
77
|
+
name: Reconcile Board (idempotent drift heal)
|
|
78
|
+
needs: [resolve-ids]
|
|
79
|
+
if: needs.resolve-ids.outputs.project_id != ''
|
|
80
|
+
runs-on: ubuntu-latest
|
|
81
|
+
env:
|
|
82
|
+
PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
|
|
83
|
+
PROJECT_ID: ${{ needs.resolve-ids.outputs.project_id }}
|
|
84
|
+
STATUS_FIELD_ID: ${{ needs.resolve-ids.outputs.status_field_id }}
|
|
85
|
+
STATUS_BRAINSTORMING: ${{ needs.resolve-ids.outputs.status_brainstorming }}
|
|
86
|
+
STATUS_DETAILING: ${{ needs.resolve-ids.outputs.status_detailing }}
|
|
87
|
+
STATUS_APPROVAL: ${{ needs.resolve-ids.outputs.status_approval }}
|
|
88
|
+
STATUS_DEVELOPMENT: ${{ needs.resolve-ids.outputs.status_development }}
|
|
89
|
+
STATUS_CODE_REVIEW_PR: ${{ needs.resolve-ids.outputs.status_code_review_pr }}
|
|
90
|
+
steps:
|
|
91
|
+
- uses: actions/checkout@v5.0.1
|
|
92
|
+
- name: Reconcile all open board items
|
|
93
|
+
if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '' }}
|
|
94
|
+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
|
95
|
+
with:
|
|
96
|
+
github-token: ${{ env.PROJECT_TOKEN }}
|
|
97
|
+
script: |
|
|
98
|
+
const { classifyItem } = require('./scripts/derive-column.js');
|
|
99
|
+
|
|
100
|
+
const COLUMN_KEY_TO_ID = {
|
|
101
|
+
'code_review_pr': process.env.STATUS_CODE_REVIEW_PR,
|
|
102
|
+
'development': process.env.STATUS_DEVELOPMENT,
|
|
103
|
+
'approval': process.env.STATUS_APPROVAL,
|
|
104
|
+
'detailing': process.env.STATUS_DETAILING,
|
|
105
|
+
'brainstorming': process.env.STATUS_BRAINSTORMING,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Paginate all board items
|
|
109
|
+
let allItems = [];
|
|
110
|
+
let cursor = null;
|
|
111
|
+
do {
|
|
112
|
+
const { node } = await github.graphql(`
|
|
113
|
+
query($id: ID!, $cursor: String) {
|
|
114
|
+
node(id: $id) {
|
|
115
|
+
... on ProjectV2 {
|
|
116
|
+
items(first: 50, after: $cursor) {
|
|
117
|
+
pageInfo { hasNextPage endCursor }
|
|
118
|
+
nodes {
|
|
119
|
+
id
|
|
120
|
+
fieldValueByName(name: "Status") {
|
|
121
|
+
... on ProjectV2ItemFieldSingleSelectValue { optionId }
|
|
122
|
+
}
|
|
123
|
+
content {
|
|
124
|
+
... on Issue {
|
|
125
|
+
number
|
|
126
|
+
state
|
|
127
|
+
labels(first: 20) { nodes { name } }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
`, { id: process.env.PROJECT_ID, cursor });
|
|
136
|
+
const page = node.items;
|
|
137
|
+
allItems = allItems.concat(page.nodes);
|
|
138
|
+
cursor = page.pageInfo.hasNextPage ? page.pageInfo.endCursor : null;
|
|
139
|
+
} while (cursor);
|
|
140
|
+
|
|
141
|
+
let checked = 0, corrected = 0;
|
|
142
|
+
|
|
143
|
+
for (const item of allItems) {
|
|
144
|
+
// Skip PRs and closed issues
|
|
145
|
+
if (!item.content || item.content.state !== 'OPEN') continue;
|
|
146
|
+
|
|
147
|
+
const labelNames = item.content.labels.nodes.map(l => l.name);
|
|
148
|
+
const columnKey = classifyItem(labelNames);
|
|
149
|
+
if (!columnKey) continue; // no actionable label — skip (e.g. Idea column items)
|
|
150
|
+
|
|
151
|
+
const targetId = COLUMN_KEY_TO_ID[columnKey];
|
|
152
|
+
if (!targetId) continue; // column not resolved (shouldn't happen)
|
|
153
|
+
|
|
154
|
+
const currentId = item.fieldValueByName?.optionId ?? null;
|
|
155
|
+
checked++;
|
|
156
|
+
|
|
157
|
+
if (currentId === targetId) continue; // already correct — no mutation
|
|
158
|
+
|
|
159
|
+
await github.graphql(`
|
|
160
|
+
mutation($p: ID!, $i: ID!, $f: ID!, $v: ProjectV2FieldValue!) {
|
|
161
|
+
updateProjectV2ItemFieldValue(input: {projectId: $p, itemId: $i, fieldId: $f, value: $v}) {
|
|
162
|
+
projectV2Item { id }
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
`, {
|
|
166
|
+
p: process.env.PROJECT_ID,
|
|
167
|
+
i: item.id,
|
|
168
|
+
f: process.env.STATUS_FIELD_ID,
|
|
169
|
+
v: { singleSelectOptionId: targetId },
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
corrected++;
|
|
173
|
+
console.log(`Corrected #${item.content.number}: ${currentId ?? 'none'} → ${columnKey}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log(`✅ Reconciliation complete: ${checked} items checked, ${corrected} corrected`);
|
|
@@ -5,117 +5,117 @@ on:
|
|
|
5
5
|
schedule:
|
|
6
6
|
- cron: '0 8 * * 1' # Every Monday at 8am
|
|
7
7
|
|
|
8
|
-
env:
|
|
9
|
-
PROJECT_ID: "PVT_kwHODpFFL84BXg7h"
|
|
10
|
-
STATUS_FIELD_ID: "PVTSSF_lAHODpFFL84BXg7hzhStRHI"
|
|
11
|
-
STATUS_BRAINSTORMING: "8eb07c5b"
|
|
12
|
-
STATUS_DETAILING: "9f6ce70e"
|
|
13
|
-
STATUS_APPROVAL: "31bf4610"
|
|
14
|
-
STATUS_DEVELOPMENT: "2c9e78e6"
|
|
15
|
-
STATUS_TESTING: "96b59ade"
|
|
16
|
-
STATUS_CODE_REVIEW_PR: "86ca9720"
|
|
17
|
-
STATUS_PRODUCTION: "1581e5bd"
|
|
18
|
-
|
|
19
8
|
jobs:
|
|
20
|
-
|
|
21
|
-
name:
|
|
9
|
+
resolve-ids:
|
|
10
|
+
name: Resolve Board Column IDs
|
|
22
11
|
runs-on: ubuntu-latest
|
|
23
12
|
env:
|
|
24
13
|
PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
|
|
25
|
-
|
|
26
|
-
|
|
14
|
+
outputs:
|
|
15
|
+
project_id: ${{ steps.resolve.outputs.project_id }}
|
|
16
|
+
status_field_id: ${{ steps.resolve.outputs.status_field_id }}
|
|
17
|
+
status_brainstorming: ${{ steps.resolve.outputs.status_brainstorming }}
|
|
18
|
+
status_detailing: ${{ steps.resolve.outputs.status_detailing }}
|
|
19
|
+
status_approval: ${{ steps.resolve.outputs.status_approval }}
|
|
20
|
+
status_development: ${{ steps.resolve.outputs.status_development }}
|
|
21
|
+
status_code_review_pr: ${{ steps.resolve.outputs.status_code_review_pr }}
|
|
22
|
+
status_production: ${{ steps.resolve.outputs.status_production }}
|
|
27
23
|
steps:
|
|
28
|
-
- name:
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
- name: Resolve column IDs by name
|
|
25
|
+
id: resolve
|
|
26
|
+
if: ${{ env.PROJECT_TOKEN != '' && vars.PROJECT_ID != '' }}
|
|
27
|
+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
|
31
28
|
with:
|
|
32
29
|
github-token: ${{ env.PROJECT_TOKEN }}
|
|
33
30
|
script: |
|
|
34
|
-
const projectId =
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
'STATUS_CODE_REVIEW_PR': process.env.STATUS_CODE_REVIEW_PR,
|
|
43
|
-
'STATUS_PRODUCTION': process.env.STATUS_PRODUCTION
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const query = `
|
|
47
|
-
query($projectId: ID!) {
|
|
48
|
-
node(id: $projectId) {
|
|
31
|
+
const projectId = '${{ vars.PROJECT_ID }}';
|
|
32
|
+
if (!projectId || projectId === '{{PROJECT_ID}}') {
|
|
33
|
+
core.warning('vars.PROJECT_ID not set — board features disabled');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const { node } = await github.graphql(`
|
|
37
|
+
query($id: ID!) {
|
|
38
|
+
node(id: $id) {
|
|
49
39
|
... on ProjectV2 {
|
|
50
|
-
title
|
|
51
40
|
fields(first: 20) {
|
|
52
41
|
nodes {
|
|
53
42
|
... on ProjectV2SingleSelectField {
|
|
54
43
|
id
|
|
55
44
|
name
|
|
56
|
-
options {
|
|
57
|
-
id
|
|
58
|
-
name
|
|
59
|
-
}
|
|
45
|
+
options { id name }
|
|
60
46
|
}
|
|
61
47
|
}
|
|
62
48
|
}
|
|
63
49
|
}
|
|
64
50
|
}
|
|
65
51
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
console.log("❌ Project not found.");
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const statusField = project.fields.nodes.find(f => f.id === statusFieldId);
|
|
84
|
-
if (!statusField) {
|
|
85
|
-
console.log("❌ Status field not found.");
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
52
|
+
`, { id: projectId });
|
|
53
|
+
const statusField = node.fields.nodes.find(f => f.name === 'Status');
|
|
54
|
+
if (!statusField) { core.setFailed('Status field not found on board'); return; }
|
|
55
|
+
const opt = (name) => statusField.options.find(o => o.name.includes(name))?.id ?? '';
|
|
56
|
+
core.setOutput('project_id', projectId);
|
|
57
|
+
core.setOutput('status_field_id', statusField.id);
|
|
58
|
+
core.setOutput('status_brainstorming', opt('Brainstorming'));
|
|
59
|
+
core.setOutput('status_detailing', opt('Detailing'));
|
|
60
|
+
core.setOutput('status_approval', opt('Approval'));
|
|
61
|
+
core.setOutput('status_development', opt('Development'));
|
|
62
|
+
core.setOutput('status_code_review_pr', opt('Code Review'));
|
|
63
|
+
core.setOutput('status_production', opt('Production'));
|
|
64
|
+
console.log('✅ Board IDs resolved by name');
|
|
88
65
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
66
|
+
check-drift:
|
|
67
|
+
name: Detect Project Board Drift
|
|
68
|
+
needs: [resolve-ids]
|
|
69
|
+
runs-on: ubuntu-latest
|
|
70
|
+
env:
|
|
71
|
+
PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
|
|
72
|
+
PROJECT_ID: ${{ needs.resolve-ids.outputs.project_id }}
|
|
73
|
+
permissions:
|
|
74
|
+
issues: write
|
|
75
|
+
steps:
|
|
76
|
+
- name: Validate Board Configuration
|
|
77
|
+
if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '' }}
|
|
78
|
+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
|
79
|
+
with:
|
|
80
|
+
github-token: ${{ env.PROJECT_TOKEN }}
|
|
81
|
+
script: |
|
|
82
|
+
const expectedColumns = [
|
|
83
|
+
'Brainstorming', 'Detailing', 'Approval',
|
|
84
|
+
'Development', 'Code Review', 'Production'
|
|
85
|
+
];
|
|
94
86
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
|
|
87
|
+
const resolvedIds = {
|
|
88
|
+
'Brainstorming': '${{ needs.resolve-ids.outputs.status_brainstorming }}',
|
|
89
|
+
'Detailing': '${{ needs.resolve-ids.outputs.status_detailing }}',
|
|
90
|
+
'Approval': '${{ needs.resolve-ids.outputs.status_approval }}',
|
|
91
|
+
'Development': '${{ needs.resolve-ids.outputs.status_development }}',
|
|
92
|
+
'Code Review': '${{ needs.resolve-ids.outputs.status_code_review_pr }}',
|
|
93
|
+
'Production': '${{ needs.resolve-ids.outputs.status_production }}',
|
|
94
|
+
};
|
|
101
95
|
|
|
102
|
-
|
|
103
|
-
console.log("🚨 Drift detected! The following mapped columns no longer exist: " + missingVars.join(", "));
|
|
104
|
-
|
|
105
|
-
let table = "| Column Name | New ID |\n|---|---|\n";
|
|
106
|
-
validOptions.forEach(opt => {
|
|
107
|
-
table += `| ${opt.name} | \`${opt.id}\` |\n`;
|
|
108
|
-
});
|
|
96
|
+
const missing = expectedColumns.filter(col => !resolvedIds[col]);
|
|
109
97
|
|
|
110
|
-
|
|
98
|
+
if (missing.length > 0) {
|
|
99
|
+
const body = [
|
|
100
|
+
'🚨 **Agentic PDLC Drift Detected**',
|
|
101
|
+
'',
|
|
102
|
+
'The following expected columns were not found on the board:',
|
|
103
|
+
'',
|
|
104
|
+
missing.map(c => `- **${c}**`).join('\n'),
|
|
105
|
+
'',
|
|
106
|
+
'The board may have been recreated or columns renamed.',
|
|
107
|
+
'Check `vars.PROJECT_ID` and verify column names match expected values.',
|
|
108
|
+
].join('\n');
|
|
111
109
|
|
|
112
110
|
await github.rest.issues.create({
|
|
113
111
|
owner: context.repo.owner,
|
|
114
112
|
repo: context.repo.repo,
|
|
115
113
|
title: '🚨 Agentic PDLC Drift Detected in Project Board',
|
|
116
|
-
body
|
|
114
|
+
body,
|
|
117
115
|
labels: ['bug']
|
|
118
116
|
});
|
|
117
|
+
|
|
118
|
+
core.setFailed(`Missing columns: ${missing.join(', ')}`);
|
|
119
119
|
} else {
|
|
120
|
-
console.log(
|
|
120
|
+
console.log('✅ No drift detected. All expected columns found on board.');
|
|
121
121
|
}
|