create-agentic-pdlc 2.4.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/.agentic-pdlc/hooks/pdlc-stage-gate.sh +37 -10
- package/.claude/settings.json +18 -0
- package/.coderabbit.yaml +41 -0
- 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 +252 -259
- package/CLAUDE.md +1 -1
- package/README.md +33 -32
- package/adapters/claude-code/skill.md +12 -8
- package/bin/cli.js +607 -213
- package/docs/superpowers/plans/2026-06-04-spec-format-issue-template.md +160 -0
- package/docs/superpowers/plans/2026-06-04-two-tier-installer.md +1056 -0
- 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-04-spec-format-issue-template-design.md +46 -0
- package/docs/superpowers/specs/2026-06-05-project-id-actions-variable-design.md +114 -0
- package/package.json +2 -2
- 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/templates/full/CLAUDE.md +30 -0
- package/templates/lite/AGENTS.md +121 -0
- package/templates/lite/CLAUDE.md +44 -0
- package/tests/cli.test.js +118 -0
- package/.github/workflows/agentic-metrics.yml +0 -545
- package/.github/workflows/qa-agent.yml +0 -139
- package/.github/workflows/qa-gate.yml +0 -51
- /package/templates/{AGENTS.md → full/AGENTS.md} +0 -0
- /package/templates/{docs → full/docs}/pdlc.md +0 -0
|
@@ -1,39 +1,66 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# PDLC Stage Gate — blocks gh pr create without
|
|
2
|
+
# PDLC Stage Gate — blocks gh pr create and file edits without spec:approved on linked issue.
|
|
3
3
|
# Bypass: branch prefix hotfix/ skips all checks.
|
|
4
4
|
|
|
5
5
|
INPUT=$(cat)
|
|
6
6
|
COMMAND=$(echo "$INPUT" | node -e "const d=JSON.parse(require('fs').readFileSync(0)); console.log(d.command || '')" 2>/dev/null || echo "")
|
|
7
|
+
FILE_PATH=$(echo "$INPUT" | node -e "const d=JSON.parse(require('fs').readFileSync(0)); console.log(d.file_path || '')" 2>/dev/null || echo "")
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
IS_PR_CREATE=false
|
|
10
|
+
IS_FILE_EDIT=false
|
|
11
|
+
|
|
12
|
+
if echo "$COMMAND" | grep -q "gh pr create"; then
|
|
13
|
+
IS_PR_CREATE=true
|
|
14
|
+
elif [ -n "$FILE_PATH" ]; then
|
|
15
|
+
IS_FILE_EDIT=true
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
if [ "$IS_PR_CREATE" != "true" ] && [ "$IS_FILE_EDIT" != "true" ]; then
|
|
9
19
|
exit 0
|
|
10
20
|
fi
|
|
11
21
|
|
|
12
22
|
BRANCH=$(git branch --show-current 2>/dev/null || echo "")
|
|
13
23
|
|
|
14
24
|
if echo "$BRANCH" | grep -qE "^hotfix/"; then
|
|
15
|
-
echo "✅ PDLC: Hotfix branch — stage gate bypassed."
|
|
16
25
|
exit 0
|
|
17
26
|
fi
|
|
18
27
|
|
|
19
28
|
ISSUE_NUM=$(echo "$BRANCH" | grep -oE '[0-9]+' | head -1)
|
|
20
29
|
|
|
21
30
|
if [ -z "$ISSUE_NUM" ]; then
|
|
31
|
+
if [ "$IS_FILE_EDIT" = "true" ]; then
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
22
34
|
echo "❌ PDLC Stage Gate: Cannot determine issue from branch '$BRANCH'."
|
|
23
35
|
echo " Use: feat/<issue-number>-<description> or hotfix/<issue-number>-<description>"
|
|
24
36
|
exit 1
|
|
25
37
|
fi
|
|
26
38
|
|
|
27
|
-
LABELS=$(gh issue view "$ISSUE_NUM" --json labels --jq '[.labels[].name] | join(" ")' 2>/dev/null
|
|
39
|
+
LABELS=$(gh issue view "$ISSUE_NUM" --json labels --jq '[.labels[].name] | join(" ")' 2>/dev/null)
|
|
40
|
+
if [ $? -ne 0 ] || [ -z "$LABELS" ]; then
|
|
41
|
+
echo "❌ PDLC Stage Gate: Could not fetch labels for issue #$ISSUE_NUM."
|
|
42
|
+
echo " Missing condition: spec:approved"
|
|
43
|
+
echo " Next step: verify gh auth and issue number, then have PM add spec:approved."
|
|
44
|
+
exit 1
|
|
45
|
+
fi
|
|
28
46
|
|
|
29
|
-
if echo "$LABELS" | grep -qw "
|
|
30
|
-
echo "✅ PDLC: Issue #$ISSUE_NUM approved — gate passed."
|
|
47
|
+
if echo "$LABELS" | grep -qw "spec:approved"; then
|
|
31
48
|
exit 0
|
|
32
49
|
fi
|
|
33
50
|
|
|
34
51
|
STAGE=$(echo "$LABELS" | tr ' ' '\n' | grep "^stage:" | head -1 || echo "none")
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
echo "
|
|
38
|
-
echo "
|
|
52
|
+
|
|
53
|
+
if [ "$IS_PR_CREATE" = "true" ]; then
|
|
54
|
+
echo "❌ PDLC Stage Gate: PR creation blocked — issue #$ISSUE_NUM missing spec:approved."
|
|
55
|
+
echo " Current stage: $STAGE"
|
|
56
|
+
echo " Missing condition: spec:approved (set by PM after reviewing spec in issue body)"
|
|
57
|
+
echo " Next step: complete spec → stage:approval → wait for PM to add spec:approved."
|
|
58
|
+
echo " Emergency bypass: rename branch to hotfix/<issue-number>-<description>."
|
|
59
|
+
else
|
|
60
|
+
echo "❌ PDLC Stage Gate: File edit blocked — issue #$ISSUE_NUM missing spec:approved."
|
|
61
|
+
echo " Current stage: $STAGE"
|
|
62
|
+
echo " Missing condition: spec:approved (set by PM after reviewing spec in issue body)"
|
|
63
|
+
echo " Next step: complete spec → stage:approval → wait for PM to add spec:approved."
|
|
64
|
+
echo " Emergency bypass: rename branch to hotfix/<issue-number>-<description>."
|
|
65
|
+
fi
|
|
39
66
|
exit 1
|
package/.claude/settings.json
CHANGED
|
@@ -9,6 +9,24 @@
|
|
|
9
9
|
"command": "bash .agentic-pdlc/hooks/pdlc-stage-gate.sh"
|
|
10
10
|
}
|
|
11
11
|
]
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"matcher": "Edit",
|
|
15
|
+
"hooks": [
|
|
16
|
+
{
|
|
17
|
+
"type": "command",
|
|
18
|
+
"command": "bash .agentic-pdlc/hooks/pdlc-stage-gate.sh"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"matcher": "Write",
|
|
24
|
+
"hooks": [
|
|
25
|
+
{
|
|
26
|
+
"type": "command",
|
|
27
|
+
"command": "bash .agentic-pdlc/hooks/pdlc-stage-gate.sh"
|
|
28
|
+
}
|
|
29
|
+
]
|
|
12
30
|
}
|
|
13
31
|
]
|
|
14
32
|
}
|
package/.coderabbit.yaml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
language: "en-US"
|
|
2
|
+
reviews:
|
|
3
|
+
profile: "assertive"
|
|
4
|
+
request_changes_workflow: false
|
|
5
|
+
high_level_summary: true
|
|
6
|
+
poem: false
|
|
7
|
+
auto_review:
|
|
8
|
+
enabled: true
|
|
9
|
+
drafts: false
|
|
10
|
+
base_branches:
|
|
11
|
+
- main
|
|
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.
|
|
19
|
+
- path: ".github/workflows/**"
|
|
20
|
+
instructions: >
|
|
21
|
+
Pay close attention to GitHub Actions syntax correctness, trigger
|
|
22
|
+
semantics (push/pull_request/workflow_run events and their filters),
|
|
23
|
+
expression syntax (${{ }}), and shell quoting in run steps.
|
|
24
|
+
Flag any logic that could cause silent failures or unintended trigger
|
|
25
|
+
behavior.
|
|
26
|
+
review_status: true
|
|
27
|
+
auto_apply_labels: true
|
|
28
|
+
labeling_instructions:
|
|
29
|
+
- label: "architecture-violation"
|
|
30
|
+
instructions: >
|
|
31
|
+
Apply when the PR introduces runtime behavior that violates documented
|
|
32
|
+
architectural invariants: code that bypasses the PDLC stage gate at runtime,
|
|
33
|
+
application code that programmatically sets or removes labels reserved for
|
|
34
|
+
automation (stage:*, spec:approved, qa:*) outside of their designated
|
|
35
|
+
automation workflows, business logic added directly to templates instead of
|
|
36
|
+
the CLI, or behavior that directly contradicts invariants in AGENTS.md or
|
|
37
|
+
CLAUDE.md. Do NOT apply for configuration changes to tools (e.g., updating
|
|
38
|
+
.coderabbit.yaml, GitHub Actions workflows, or CI config files) — those are
|
|
39
|
+
infrastructure, not invariant violations.
|
|
40
|
+
chat:
|
|
41
|
+
auto_reply: true
|
|
@@ -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`);
|