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 CHANGED
@@ -1,6 +1,6 @@
1
1
  language: "en-US"
2
2
  reviews:
3
- profile: "chill"
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 != '{{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
- # Runs only when spec:approved is added
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: "PVT_kwHODpFFL84BXg7h"
21
- STATUS_FIELD_ID: "PVTSSF_lAHODpFFL84BXg7hzhStRHI"
22
- STATUS_DEVELOPMENT: "2c9e78e6"
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
- check-drift:
21
- name: Detect Project Board Drift
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
- permissions:
26
- issues: write
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: Validate Board Configuration
29
- if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
30
- uses: actions/github-script@v8
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 = process.env.PROJECT_ID;
35
- const statusFieldId = process.env.STATUS_FIELD_ID;
36
- const envVars = {
37
- 'STATUS_BRAINSTORMING': process.env.STATUS_BRAINSTORMING,
38
- 'STATUS_DETAILING': process.env.STATUS_DETAILING,
39
- 'STATUS_APPROVAL': process.env.STATUS_APPROVAL,
40
- 'STATUS_DEVELOPMENT': process.env.STATUS_DEVELOPMENT,
41
- 'STATUS_TESTING': process.env.STATUS_TESTING,
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
- let result;
69
- try {
70
- result = await github.graphql(query, { projectId });
71
- } catch (error) {
72
- console.log("❌ Error fetching project. Verify your PROJECT_ID.");
73
- console.log(error);
74
- return;
75
- }
76
-
77
- const project = result.node;
78
- if (!project) {
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
- const validOptions = statusField.options;
90
- const validOptionIds = validOptions.map(o => o.id);
91
-
92
- let hasDrift = false;
93
- let missingVars = [];
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
- for (const [varName, id] of Object.entries(envVars)) {
96
- if (id && !id.startsWith('{{') && !validOptionIds.includes(id)) {
97
- hasDrift = true;
98
- missingVars.push(varName);
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
- if (hasDrift) {
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
- const body = `🚨 **Agentic PDLC Drift Detected**\n\nThe following columns mapped in your \`.github/workflows/project-automation.yml\` no longer exist in your project board:\n\n**${missingVars.join(", ")}**\n\n### How to fix it:\nHere is the list of current columns in your board with their valid IDs. Please update the \`env\` block in your \`.github/workflows/project-automation.yml\` and \`.github/workflows/pdlc-health-check.yml\`.\n\n${table}`;
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: body,
114
+ body,
117
115
  labels: ['bug']
118
116
  });
117
+
118
+ core.setFailed(`Missing columns: ${missing.join(', ')}`);
119
119
  } else {
120
- console.log("✅ No drift detected. Board configuration is healthy.");
120
+ console.log('✅ No drift detected. All expected columns found on board.');
121
121
  }