create-agentic-pdlc 2.3.0 → 2.4.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/metrics/raw/2026-W22.jsonl +114 -0
- package/.github/ISSUE_TEMPLATE/bug.md +53 -0
- package/.github/ISSUE_TEMPLATE/feature.md +54 -0
- package/.github/ISSUE_TEMPLATE/task.md +33 -0
- package/.github/workflows/add-to-board.yml +1 -1
- package/.github/workflows/agent-trigger.yml +4 -4
- package/.github/workflows/agentic-metrics.yml +150 -27
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/npm-publish.yml +2 -2
- package/.github/workflows/pdlc-health-check.yml +1 -1
- package/.github/workflows/pdlc-stage-gate.yml +2 -2
- package/.github/workflows/project-automation.yml +51 -12
- package/.github/workflows/qa-agent.yml +22 -11
- package/.github/workflows/qa-gate.yml +51 -0
- package/AGENTS.md +50 -8
- package/CLAUDE.md +2 -0
- package/SETUP.md +2 -1
- package/adapters/claude-code/skill.md +32 -11
- package/adapters/hooks/pdlc-stage-gate.sh +3 -8
- package/bin/cli.js +23 -2
- package/docs/pdlc.md +5 -5
- package/docs/superpowers/plans/2026-05-28-jules-label-pat-split.md +240 -0
- package/docs/superpowers/plans/2026-05-29-agentic-pulse-rework-taxonomy.md +474 -0
- package/docs/superpowers/plans/2026-05-29-qa-gate-enforcement.md +354 -0
- package/docs/superpowers/specs/2026-05-29-agentic-pulse-rework-taxonomy-design.md +122 -0
- package/package.json +1 -1
- package/templates/.github/ISSUE_TEMPLATE/bug.md +53 -0
- package/templates/.github/ISSUE_TEMPLATE/feature.md +54 -0
- package/templates/.github/ISSUE_TEMPLATE/task.md +33 -0
- package/templates/.github/workflows/add-to-board.yml +4 -4
- package/templates/.github/workflows/agent-trigger.yml +22 -13
- package/{.agentic-pdlc/templates → templates}/.github/workflows/agentic-metrics.yml +150 -27
- package/templates/.github/workflows/ci.yml +1 -1
- package/templates/.github/workflows/pdlc-health-check.yml +1 -1
- package/templates/.github/workflows/pdlc-stage-gate.yml +2 -2
- package/templates/.github/workflows/project-automation.yml +71 -32
- package/templates/.github/workflows/qa-agent.yml +32 -18
- package/templates/.github/workflows/qa-gate.yml +51 -0
- package/templates/AGENTS.md +57 -29
- package/templates/docs/pdlc.md +4 -4
- package/.agentic-pdlc/templates/.github/CODEOWNERS +0 -5
- package/.agentic-pdlc/templates/.github/copilot-instructions.md +0 -12
- package/.agentic-pdlc/templates/.github/workflows/add-to-board.yml +0 -38
- package/.agentic-pdlc/templates/.github/workflows/agent-trigger.yml +0 -146
- package/.agentic-pdlc/templates/.github/workflows/auto-approve.yml +0 -16
- package/.agentic-pdlc/templates/.github/workflows/ci.yml +0 -54
- package/.agentic-pdlc/templates/.github/workflows/pdlc-health-check.yml +0 -121
- package/.agentic-pdlc/templates/.github/workflows/pdlc-stage-gate.yml +0 -51
- package/.agentic-pdlc/templates/.github/workflows/project-automation.yml +0 -274
- package/.agentic-pdlc/templates/.github/workflows/protect-workflows.yml +0 -21
- package/.agentic-pdlc/templates/.github/workflows/qa-agent.yml +0 -128
- package/.agentic-pdlc/templates/AGENTS.md +0 -104
- package/.agentic-pdlc/templates/docs/pdlc.md +0 -123
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
name: PDLC Health Check (Drift Detection)
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
workflow_dispatch:
|
|
5
|
-
schedule:
|
|
6
|
-
- cron: '0 8 * * 1' # Every Monday at 8am
|
|
7
|
-
|
|
8
|
-
env:
|
|
9
|
-
PROJECT_ID: "{{PROJECT_ID}}"
|
|
10
|
-
STATUS_FIELD_ID: "{{STATUS_FIELD_ID}}"
|
|
11
|
-
STATUS_BRAINSTORMING: "{{ID_BRAINSTORMING}}"
|
|
12
|
-
STATUS_DETAILING: "{{ID_DETAILING}}"
|
|
13
|
-
STATUS_APPROVAL: "{{ID_APPROVAL}}"
|
|
14
|
-
STATUS_DEVELOPMENT: "{{ID_DEVELOPMENT}}"
|
|
15
|
-
STATUS_TESTING: "{{ID_TESTING}}"
|
|
16
|
-
STATUS_CODE_REVIEW_PR: "{{ID_CODE_REVIEW_PR}}"
|
|
17
|
-
STATUS_PRODUCTION: "{{ID_PRODUCTION}}"
|
|
18
|
-
|
|
19
|
-
jobs:
|
|
20
|
-
check-drift:
|
|
21
|
-
name: Detect Project Board Drift
|
|
22
|
-
runs-on: ubuntu-latest
|
|
23
|
-
env:
|
|
24
|
-
PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
|
|
25
|
-
permissions:
|
|
26
|
-
issues: write
|
|
27
|
-
steps:
|
|
28
|
-
- name: Validate Board Configuration
|
|
29
|
-
if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
|
|
30
|
-
uses: actions/github-script@v7
|
|
31
|
-
with:
|
|
32
|
-
github-token: ${{ env.PROJECT_TOKEN }}
|
|
33
|
-
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) {
|
|
49
|
-
... on ProjectV2 {
|
|
50
|
-
title
|
|
51
|
-
fields(first: 20) {
|
|
52
|
-
nodes {
|
|
53
|
-
... on ProjectV2SingleSelectField {
|
|
54
|
-
id
|
|
55
|
-
name
|
|
56
|
-
options {
|
|
57
|
-
id
|
|
58
|
-
name
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
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
|
-
}
|
|
88
|
-
|
|
89
|
-
const validOptions = statusField.options;
|
|
90
|
-
const validOptionIds = validOptions.map(o => o.id);
|
|
91
|
-
|
|
92
|
-
let hasDrift = false;
|
|
93
|
-
let missingVars = [];
|
|
94
|
-
|
|
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
|
-
}
|
|
101
|
-
|
|
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
|
-
});
|
|
109
|
-
|
|
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}`;
|
|
111
|
-
|
|
112
|
-
await github.rest.issues.create({
|
|
113
|
-
owner: context.repo.owner,
|
|
114
|
-
repo: context.repo.repo,
|
|
115
|
-
title: '🚨 Agentic PDLC Drift Detected in Project Board',
|
|
116
|
-
body: body,
|
|
117
|
-
labels: ['bug']
|
|
118
|
-
});
|
|
119
|
-
} else {
|
|
120
|
-
console.log("✅ No drift detected. Board configuration is healthy.");
|
|
121
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
name: PDLC Stage Gate
|
|
2
|
-
on:
|
|
3
|
-
pull_request:
|
|
4
|
-
types: [opened, synchronize, reopened, labeled, unlabeled]
|
|
5
|
-
|
|
6
|
-
permissions:
|
|
7
|
-
pull-requests: read
|
|
8
|
-
issues: read
|
|
9
|
-
|
|
10
|
-
jobs:
|
|
11
|
-
stage-gate:
|
|
12
|
-
name: PDLC Stage Gate
|
|
13
|
-
runs-on: ubuntu-latest
|
|
14
|
-
steps:
|
|
15
|
-
- name: Check stage:approval
|
|
16
|
-
env:
|
|
17
|
-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
18
|
-
run: |
|
|
19
|
-
set -e
|
|
20
|
-
REPO="${{ github.repository }}"
|
|
21
|
-
PR_NUMBER="${{ github.event.pull_request.number }}"
|
|
22
|
-
|
|
23
|
-
PR_LABELS=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json labels --jq '[.labels[].name] | join(" ")')
|
|
24
|
-
if echo "$PR_LABELS" | grep -qw "hotfix"; then
|
|
25
|
-
echo "✅ Hotfix label — stage gate bypassed."
|
|
26
|
-
exit 0
|
|
27
|
-
fi
|
|
28
|
-
|
|
29
|
-
PR_BODY=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json body --jq '.body // ""')
|
|
30
|
-
ISSUE_NUMS=$(echo "$PR_BODY" | grep -oiE '(Closes?|Fixes?|Resolves?)\s+#([0-9]+)' | grep -oE '[0-9]+' || true)
|
|
31
|
-
|
|
32
|
-
if [ -z "$ISSUE_NUMS" ]; then
|
|
33
|
-
echo "❌ No linked issues in PR body."
|
|
34
|
-
echo " Add 'Closes #N' to PR body, or add 'hotfix' label to PR for emergencies."
|
|
35
|
-
exit 1
|
|
36
|
-
fi
|
|
37
|
-
|
|
38
|
-
for NUM in $ISSUE_NUMS; do
|
|
39
|
-
LABELS=$(gh issue view "$NUM" --repo "$REPO" --json labels --jq '[.labels[].name] | join(" ")' 2>/dev/null || echo "")
|
|
40
|
-
if echo "$LABELS" | grep -qw "stage:approval" || echo "$LABELS" | grep -qw "spec:approved" || echo "$LABELS" | grep -qw "stage:development"; then
|
|
41
|
-
echo "✅ Issue #$NUM approved"
|
|
42
|
-
else
|
|
43
|
-
STAGE=$(echo "$LABELS" | tr ' ' '\n' | grep "^stage:" | head -1 || echo "none")
|
|
44
|
-
echo "❌ Issue #$NUM missing approval (current: $STAGE)"
|
|
45
|
-
echo " Required: stage:approval OR spec:approved OR stage:development label on the issue."
|
|
46
|
-
echo " Emergency bypass: add 'hotfix' label to this PR."
|
|
47
|
-
exit 1
|
|
48
|
-
fi
|
|
49
|
-
done
|
|
50
|
-
|
|
51
|
-
echo "✅ All linked issues approved."
|
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
name: PDLC Board Automation
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
pull_request:
|
|
5
|
-
types: [opened, reopened, closed, labeled]
|
|
6
|
-
pull_request_review:
|
|
7
|
-
types: [submitted]
|
|
8
|
-
issues:
|
|
9
|
-
types: [labeled]
|
|
10
|
-
|
|
11
|
-
env:
|
|
12
|
-
PROJECT_ID: "{{PROJECT_ID}}"
|
|
13
|
-
STATUS_FIELD_ID: "{{STATUS_FIELD_ID}}"
|
|
14
|
-
STATUS_IDEA: "{{ID_IDEA}}"
|
|
15
|
-
STATUS_BRAINSTORMING: "{{ID_BRAINSTORMING}}"
|
|
16
|
-
STATUS_DETAILING: "{{ID_DETAILING}}"
|
|
17
|
-
STATUS_APPROVAL: "{{ID_APPROVAL}}"
|
|
18
|
-
STATUS_DEVELOPMENT: "{{ID_DEVELOPMENT}}"
|
|
19
|
-
STATUS_TESTING: "{{ID_TESTING}}"
|
|
20
|
-
STATUS_CODE_REVIEW_PR: "{{ID_CODE_REVIEW_PR}}"
|
|
21
|
-
STATUS_PRODUCTION: "{{ID_PRODUCTION}}"
|
|
22
|
-
|
|
23
|
-
jobs:
|
|
24
|
-
# Issue Labeled → Move Upstream
|
|
25
|
-
move-card-on-label:
|
|
26
|
-
name: Upstream Label → Move Card
|
|
27
|
-
if: github.event_name == 'issues' && github.event.action == 'labeled'
|
|
28
|
-
runs-on: ubuntu-latest
|
|
29
|
-
env:
|
|
30
|
-
PROJECT_PAT: ${{ secrets.PROJECT_PAT }}
|
|
31
|
-
steps:
|
|
32
|
-
- name: Detect Label and Move Issue
|
|
33
|
-
if: ${{ env.PROJECT_PAT != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
|
|
34
|
-
uses: actions/github-script@v7
|
|
35
|
-
with:
|
|
36
|
-
github-token: ${{ env.PROJECT_PAT }}
|
|
37
|
-
script: |
|
|
38
|
-
const labelName = context.payload.label.name;
|
|
39
|
-
let targetStatusId = null;
|
|
40
|
-
let stageName = null;
|
|
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
|
-
} else if (labelName === 'stage:testing') {
|
|
55
|
-
targetStatusId = process.env.STATUS_TESTING;
|
|
56
|
-
stageName = 'Testing';
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (!targetStatusId) {
|
|
60
|
-
console.log('No stage PDLC label found. Skipping.');
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const { issue: { number, node_id } } = context.payload;
|
|
65
|
-
|
|
66
|
-
const moveItem = async (nodeId, statusId) => {
|
|
67
|
-
const { addProjectV2ItemById: { item } } = await github.graphql(`
|
|
68
|
-
mutation($p: ID!, $c: ID!) {
|
|
69
|
-
addProjectV2ItemById(input: {projectId: $p, contentId: $c}) { item { id } }
|
|
70
|
-
}`, { p: process.env.PROJECT_ID, c: nodeId });
|
|
71
|
-
|
|
72
|
-
await github.graphql(`
|
|
73
|
-
mutation($p: ID!, $i: ID!, $f: ID!, $v: ProjectV2FieldValue!) {
|
|
74
|
-
updateProjectV2ItemFieldValue(input: {projectId: $p, itemId: $i, fieldId: $f, value: $v}) {
|
|
75
|
-
projectV2Item { id }
|
|
76
|
-
}
|
|
77
|
-
}`, {
|
|
78
|
-
p: process.env.PROJECT_ID, i: item.id, f: process.env.STATUS_FIELD_ID,
|
|
79
|
-
v: { singleSelectOptionId: statusId }
|
|
80
|
-
});
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
await moveItem(node_id, targetStatusId);
|
|
84
|
-
console.log(`Issue #${number} moved to ${stageName}`);
|
|
85
|
-
|
|
86
|
-
# OPTIONAL: Uncomment to enable architecture-violation → Idea
|
|
87
|
-
# move-violation-to-board:
|
|
88
|
-
# name: architecture-violation → 💡 Idea
|
|
89
|
-
# if: github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'architecture-violation'
|
|
90
|
-
# runs-on: ubuntu-latest
|
|
91
|
-
# steps:
|
|
92
|
-
# - name: Move issue to Idea
|
|
93
|
-
# if: ${{ env.PROJECT_PAT != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
|
|
94
|
-
# uses: actions/github-script@v7
|
|
95
|
-
# with:
|
|
96
|
-
# github-token: ${{ env.PROJECT_PAT }}
|
|
97
|
-
# script: |
|
|
98
|
-
# const { issue: { number, node_id } } = context.payload;
|
|
99
|
-
# const { addProjectV2ItemById: { item } } = await github.graphql(`
|
|
100
|
-
# mutation($p: ID!, $c: ID!) {
|
|
101
|
-
# addProjectV2ItemById(input: {projectId: $p, contentId: $c}) { item { id } }
|
|
102
|
-
# }`, { p: process.env.PROJECT_ID, c: node_id });
|
|
103
|
-
# await github.graphql(`
|
|
104
|
-
# mutation($p: ID!, $i: ID!, $f: ID!, $v: ProjectV2FieldValue!) {
|
|
105
|
-
# updateProjectV2ItemFieldValue(input: {projectId: $p, itemId: $i, fieldId: $f, value: $v}) {
|
|
106
|
-
# projectV2Item { id }
|
|
107
|
-
# }
|
|
108
|
-
# }`, {
|
|
109
|
-
# p: process.env.PROJECT_ID, i: item.id, f: process.env.STATUS_FIELD_ID,
|
|
110
|
-
# v: { singleSelectOptionId: process.env.STATUS_IDEA }
|
|
111
|
-
# });
|
|
112
|
-
# console.log(`Issue #${number} moved to Idea`);
|
|
113
|
-
|
|
114
|
-
# PR Opened → Move linked issue to Testing (Variant B — QA Agent enabled)
|
|
115
|
-
move-card-on-pr-open:
|
|
116
|
-
name: Open PR → Testing
|
|
117
|
-
if: github.event_name == 'pull_request' && (github.event.action == 'opened' || github.event.action == 'reopened')
|
|
118
|
-
runs-on: ubuntu-latest
|
|
119
|
-
env:
|
|
120
|
-
PROJECT_PAT: ${{ secrets.PROJECT_PAT }}
|
|
121
|
-
steps:
|
|
122
|
-
- name: Move linked issue to Testing
|
|
123
|
-
if: ${{ env.PROJECT_PAT != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
|
|
124
|
-
uses: actions/github-script@v7
|
|
125
|
-
with:
|
|
126
|
-
github-token: ${{ env.PROJECT_PAT }}
|
|
127
|
-
script: |
|
|
128
|
-
const prNumber = context.payload.pull_request.number;
|
|
129
|
-
const { owner, repo } = context.repo;
|
|
130
|
-
|
|
131
|
-
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
|
|
132
|
-
const body = pr.body ?? '';
|
|
133
|
-
|
|
134
|
-
// Extract issues linked via "Closes #N", "Fixes #N", "Resolves #N"
|
|
135
|
-
const linkedIssues = [...body.matchAll(/(?:Closes?|Fixes?|Resolves?)\s+#(\d+)/gi)]
|
|
136
|
-
.map(m => parseInt(m[1]));
|
|
137
|
-
|
|
138
|
-
const moveItem = async (nodeId) => {
|
|
139
|
-
const { addProjectV2ItemById: { item } } = await github.graphql(`
|
|
140
|
-
mutation($p: ID!, $c: ID!) {
|
|
141
|
-
addProjectV2ItemById(input: {projectId: $p, contentId: $c}) { item { id } }
|
|
142
|
-
}`, { p: process.env.PROJECT_ID, c: nodeId });
|
|
143
|
-
|
|
144
|
-
await github.graphql(`
|
|
145
|
-
mutation($p: ID!, $i: ID!, $f: ID!, $v: ProjectV2FieldValue!) {
|
|
146
|
-
updateProjectV2ItemFieldValue(input: {projectId: $p, itemId: $i, fieldId: $f, value: $v}) {
|
|
147
|
-
projectV2Item { id }
|
|
148
|
-
}
|
|
149
|
-
}`, {
|
|
150
|
-
p: process.env.PROJECT_ID, i: item.id, f: process.env.STATUS_FIELD_ID,
|
|
151
|
-
v: { singleSelectOptionId: process.env.STATUS_TESTING }
|
|
152
|
-
});
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
if (linkedIssues.length > 0) {
|
|
156
|
-
for (const n of linkedIssues) {
|
|
157
|
-
const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number: n });
|
|
158
|
-
await moveItem(issue.node_id);
|
|
159
|
-
console.log(`Issue #${n} → Testing`);
|
|
160
|
-
}
|
|
161
|
-
} else {
|
|
162
|
-
await moveItem(pr.node_id);
|
|
163
|
-
console.log(`PR #${prNumber} → Testing (no linked issue)`);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
await github.rest.issues.addLabels({ owner, repo, issue_number: prNumber, labels: ['pr:in-review'] }).catch(() => {});
|
|
167
|
-
|
|
168
|
-
# QA Approved → Move linked issue to Code Review / PR
|
|
169
|
-
move-card-on-qa-pass:
|
|
170
|
-
name: qa:approved → Code Review / PR
|
|
171
|
-
if: github.event_name == 'pull_request' && github.event.action == 'labeled' && github.event.label.name == 'qa:approved'
|
|
172
|
-
runs-on: ubuntu-latest
|
|
173
|
-
env:
|
|
174
|
-
PROJECT_PAT: ${{ secrets.PROJECT_PAT }}
|
|
175
|
-
steps:
|
|
176
|
-
- name: Move linked issue to Code Review / PR
|
|
177
|
-
if: ${{ env.PROJECT_PAT != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
|
|
178
|
-
uses: actions/github-script@v7
|
|
179
|
-
with:
|
|
180
|
-
github-token: ${{ env.PROJECT_PAT }}
|
|
181
|
-
script: |
|
|
182
|
-
const prNumber = context.payload.pull_request.number;
|
|
183
|
-
const { owner, repo } = context.repo;
|
|
184
|
-
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
|
|
185
|
-
const body = pr.body ?? '';
|
|
186
|
-
const linkedIssues = [...body.matchAll(/(?:Closes?|Fixes?|Resolves?)\s+#(\d+)/gi)].map(m => parseInt(m[1]));
|
|
187
|
-
const moveItem = async (nodeId) => {
|
|
188
|
-
const { addProjectV2ItemById: { item } } = await github.graphql(`
|
|
189
|
-
mutation($p: ID!, $c: ID!) {
|
|
190
|
-
addProjectV2ItemById(input: {projectId: $p, contentId: $c}) { item { id } }
|
|
191
|
-
}`, { p: process.env.PROJECT_ID, c: nodeId });
|
|
192
|
-
await github.graphql(`
|
|
193
|
-
mutation($p: ID!, $i: ID!, $f: ID!, $v: ProjectV2FieldValue!) {
|
|
194
|
-
updateProjectV2ItemFieldValue(input: {projectId: $p, itemId: $i, fieldId: $f, value: $v}) {
|
|
195
|
-
projectV2Item { id }
|
|
196
|
-
}
|
|
197
|
-
}`, {
|
|
198
|
-
p: process.env.PROJECT_ID, i: item.id, f: process.env.STATUS_FIELD_ID,
|
|
199
|
-
v: { singleSelectOptionId: process.env.STATUS_CODE_REVIEW_PR }
|
|
200
|
-
});
|
|
201
|
-
};
|
|
202
|
-
if (linkedIssues.length > 0) {
|
|
203
|
-
for (const n of linkedIssues) {
|
|
204
|
-
const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number: n });
|
|
205
|
-
await moveItem(issue.node_id);
|
|
206
|
-
console.log(`Issue #${n} → Code Review / PR`);
|
|
207
|
-
}
|
|
208
|
-
} else {
|
|
209
|
-
await moveItem(pr.node_id);
|
|
210
|
-
console.log(`PR #${prNumber} → Code Review / PR (no linked issue)`);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
# Review Approved → Add Label
|
|
214
|
-
move-card-on-review-approved:
|
|
215
|
-
name: Approved PR → Add Label
|
|
216
|
-
if: github.event_name == 'pull_request_review' && github.event.review.state == 'approved'
|
|
217
|
-
runs-on: ubuntu-latest
|
|
218
|
-
env:
|
|
219
|
-
PROJECT_PAT: ${{ secrets.PROJECT_PAT }}
|
|
220
|
-
steps:
|
|
221
|
-
- name: Swap PR labels
|
|
222
|
-
if: ${{ env.PROJECT_PAT != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
|
|
223
|
-
uses: actions/github-script@v7
|
|
224
|
-
with:
|
|
225
|
-
github-token: ${{ env.PROJECT_PAT }}
|
|
226
|
-
script: |
|
|
227
|
-
const prNumber = context.payload.pull_request.number;
|
|
228
|
-
const { owner, repo } = context.repo;
|
|
229
|
-
try { await github.rest.issues.removeLabel({ owner, repo, issue_number: prNumber, name: 'pr:in-review' }); } catch {}
|
|
230
|
-
await github.rest.issues.addLabels({ owner, repo, issue_number: prNumber, labels: ['pr:approved'] }).catch(() => {});
|
|
231
|
-
|
|
232
|
-
# PR Merged → Production
|
|
233
|
-
move-card-on-pr-merge:
|
|
234
|
-
name: Merged PR → Production
|
|
235
|
-
if: github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged == true
|
|
236
|
-
runs-on: ubuntu-latest
|
|
237
|
-
env:
|
|
238
|
-
PROJECT_PAT: ${{ secrets.PROJECT_PAT }}
|
|
239
|
-
steps:
|
|
240
|
-
- name: Move issue to Production
|
|
241
|
-
if: ${{ env.PROJECT_PAT != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
|
|
242
|
-
uses: actions/github-script@v7
|
|
243
|
-
with:
|
|
244
|
-
github-token: ${{ env.PROJECT_PAT }}
|
|
245
|
-
script: |
|
|
246
|
-
const prNumber = context.payload.pull_request.number;
|
|
247
|
-
const { owner, repo } = context.repo;
|
|
248
|
-
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
|
|
249
|
-
const body = pr.body ?? '';
|
|
250
|
-
const linkedIssues = [...body.matchAll(/(?:Closes?|Fixes?|Resolves?)\s+#(\d+)/gi)].map(m => parseInt(m[1]));
|
|
251
|
-
|
|
252
|
-
const moveItem = async (nodeId) => {
|
|
253
|
-
const { addProjectV2ItemById: { item } } = await github.graphql(`
|
|
254
|
-
mutation($p: ID!, $c: ID!) {
|
|
255
|
-
addProjectV2ItemById(input: {projectId: $p, contentId: $c}) { item { id } }
|
|
256
|
-
}`, { p: process.env.PROJECT_ID, c: nodeId });
|
|
257
|
-
await github.graphql(`
|
|
258
|
-
mutation($p: ID!, $i: ID!, $f: ID!, $v: ProjectV2FieldValue!) {
|
|
259
|
-
updateProjectV2ItemFieldValue(input: {projectId: $p, itemId: $i, fieldId: $f, value: $v}) {
|
|
260
|
-
projectV2Item { id }
|
|
261
|
-
}
|
|
262
|
-
}`, { p: process.env.PROJECT_ID, i: item.id, f: process.env.STATUS_FIELD_ID,
|
|
263
|
-
v: { singleSelectOptionId: process.env.STATUS_PRODUCTION } });
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
if (linkedIssues.length > 0) {
|
|
267
|
-
for (const n of linkedIssues) {
|
|
268
|
-
const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number: n });
|
|
269
|
-
await moveItem(issue.node_id);
|
|
270
|
-
console.log(`Issue #${n} → Production`);
|
|
271
|
-
}
|
|
272
|
-
} else {
|
|
273
|
-
await moveItem(pr.node_id);
|
|
274
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
name: Protect Workflows
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
pull_request:
|
|
5
|
-
types: [opened, synchronize, reopened, labeled]
|
|
6
|
-
paths:
|
|
7
|
-
- '.github/**'
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
block-unauthorized-edits:
|
|
11
|
-
name: Protect .github directory
|
|
12
|
-
runs-on: ubuntu-latest
|
|
13
|
-
steps:
|
|
14
|
-
- name: Check for Human Approval
|
|
15
|
-
if: ${{ !contains(github.event.pull_request.labels.*.name, 'human-approved') }}
|
|
16
|
-
run: |
|
|
17
|
-
echo "🚨 Modifications in the .github/ directory detected."
|
|
18
|
-
echo "As a security measure, this repository blocks workflow edits by default."
|
|
19
|
-
echo "If you are an AI Agent, do not add the approval label yourself."
|
|
20
|
-
echo "If you are the human owner, please add the 'human-approved' label to this PR to allow merging."
|
|
21
|
-
exit 1
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
name: AI QA Agent
|
|
2
|
-
on:
|
|
3
|
-
pull_request:
|
|
4
|
-
types: [opened, synchronize, reopened]
|
|
5
|
-
|
|
6
|
-
permissions:
|
|
7
|
-
pull-requests: write
|
|
8
|
-
contents: read
|
|
9
|
-
issues: read
|
|
10
|
-
models: read
|
|
11
|
-
|
|
12
|
-
jobs:
|
|
13
|
-
qa:
|
|
14
|
-
name: AC Coverage Verification (GitHub Models)
|
|
15
|
-
runs-on: ubuntu-latest
|
|
16
|
-
env:
|
|
17
|
-
PROJECT_PAT: ${{ secrets.PROJECT_PAT }}
|
|
18
|
-
PROJECT_ID: "{{PROJECT_ID}}"
|
|
19
|
-
STATUS_FIELD_ID: "{{STATUS_FIELD_ID}}"
|
|
20
|
-
STATUS_CODE_REVIEW_PR: "{{ID_CODE_REVIEW_PR}}"
|
|
21
|
-
steps:
|
|
22
|
-
- uses: actions/checkout@v4
|
|
23
|
-
with:
|
|
24
|
-
fetch-depth: 0
|
|
25
|
-
|
|
26
|
-
- name: Verify AC Coverage via GitHub Models
|
|
27
|
-
env:
|
|
28
|
-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
29
|
-
run: |
|
|
30
|
-
set -e
|
|
31
|
-
|
|
32
|
-
PR_NUMBER="${{ github.event.pull_request.number }}"
|
|
33
|
-
BASE="${{ github.event.pull_request.base.sha }}"
|
|
34
|
-
HEAD="${{ github.event.pull_request.head.sha }}"
|
|
35
|
-
|
|
36
|
-
# Get PR diff (truncated to 8000 chars to stay within context limits)
|
|
37
|
-
DIFF=$(git diff "$BASE" "$HEAD" | head -c 64000)
|
|
38
|
-
|
|
39
|
-
# Extract linked issues from PR body
|
|
40
|
-
PR_BODY=$(gh pr view "$PR_NUMBER" --json body --jq '.body // ""')
|
|
41
|
-
ISSUE_NUMS=$(echo "$PR_BODY" | grep -oiE '(Closes?|Fixes?|Resolves?)\s+#([0-9]+)' | grep -oE '[0-9]+' || true)
|
|
42
|
-
|
|
43
|
-
# Build acceptance criteria context
|
|
44
|
-
AC_CONTEXT=""
|
|
45
|
-
if [ -n "$ISSUE_NUMS" ]; then
|
|
46
|
-
for NUM in $ISSUE_NUMS; do
|
|
47
|
-
ISSUE_BODY=$(gh issue view "$NUM" --json body --jq '.body // ""' 2>/dev/null || echo "")
|
|
48
|
-
AC_CONTEXT="${AC_CONTEXT}\\n\\n--- Issue #${NUM} ---\\n${ISSUE_BODY}"
|
|
49
|
-
done
|
|
50
|
-
fi
|
|
51
|
-
|
|
52
|
-
if [ -z "$AC_CONTEXT" ]; then
|
|
53
|
-
AC_CONTEXT="No linked issue found. Evaluate if the PR description is self-contained."
|
|
54
|
-
fi
|
|
55
|
-
|
|
56
|
-
# Serialize prompt as JSON string and call GitHub Models API (30s timeout)
|
|
57
|
-
PROMPT_JSON=$(printf '%s' "You are a senior QA engineer. Review whether this PR diff satisfies the Acceptance Criteria below.\n\nACCEPTANCE CRITERIA:\n${AC_CONTEXT}\n\nPR DIFF:\n${DIFF}\n\nFirst line of your response must be exactly one word: PASS or FAIL. Second line: brief explanation (max 3 sentences)." | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))')
|
|
58
|
-
|
|
59
|
-
RESPONSE=$(curl -sf -X POST \
|
|
60
|
-
"https://models.github.ai/inference/chat/completions" \
|
|
61
|
-
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
|
|
62
|
-
-H "Content-Type: application/json" \
|
|
63
|
-
-d "{\"model\":\"gpt-4o-mini\",\"messages\":[{\"role\":\"user\",\"content\":${PROMPT_JSON}}]}" \
|
|
64
|
-
--max-time 30 || echo "API_ERROR")
|
|
65
|
-
|
|
66
|
-
if [ "$RESPONSE" = "API_ERROR" ]; then
|
|
67
|
-
GH_TOKEN="$PROJECT_PAT" gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/labels" --method POST -f 'labels[]=infra:qa-broken'
|
|
68
|
-
gh pr comment "$PR_NUMBER" --body "🤖 **QA Agent:** Could not reach GitHub Models API. Manual review required."
|
|
69
|
-
exit 0
|
|
70
|
-
fi
|
|
71
|
-
|
|
72
|
-
VERDICT=$(echo "$RESPONSE" | python3 -c 'import json,sys,re; d=json.load(sys.stdin); t=d.get("choices",[{}])[0].get("message",{}).get("content","").strip(); first=t.split("\n")[0].upper() if t else ""; print("FAIL" if re.search(r"\bFAIL\b",first) else "PASS" if re.search(r"\bPASS\b",first) else "API_ERROR")')
|
|
73
|
-
EXPLANATION=$(echo "$RESPONSE" | python3 -c 'import json,sys; d=json.load(sys.stdin); t=d.get("choices",[{}])[0].get("message",{}).get("content","").strip(); lines=t.split("\n",1); print(lines[1].strip() if len(lines)>1 else "")')
|
|
74
|
-
|
|
75
|
-
if echo "$VERDICT" | grep -q "^PASS"; then
|
|
76
|
-
GH_TOKEN="$PROJECT_PAT" gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/labels" --method POST -f 'labels[]=qa:approved'
|
|
77
|
-
gh pr comment "$PR_NUMBER" --body "🤖 **QA Agent:** AC coverage verified. ${EXPLANATION}"
|
|
78
|
-
elif echo "$VERDICT" | grep -q "^FAIL"; then
|
|
79
|
-
GH_TOKEN="$PROJECT_PAT" gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/labels" --method POST -f 'labels[]=qa:needs-work'
|
|
80
|
-
gh pr comment "$PR_NUMBER" --body "🤖 **QA Agent:** AC coverage insufficient. ${EXPLANATION}"
|
|
81
|
-
else
|
|
82
|
-
GH_TOKEN="$PROJECT_PAT" gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/labels" --method POST -f 'labels[]=infra:qa-broken'
|
|
83
|
-
gh pr comment "$PR_NUMBER" --body "🤖 **QA Agent:** Could not parse GitHub Models response. Manual review required."
|
|
84
|
-
fi
|
|
85
|
-
|
|
86
|
-
- name: Move board card to Code Review on qa:approved
|
|
87
|
-
if: ${{ env.PROJECT_PAT != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
|
|
88
|
-
uses: actions/github-script@v7
|
|
89
|
-
with:
|
|
90
|
-
github-token: ${{ env.PROJECT_PAT }}
|
|
91
|
-
script: |
|
|
92
|
-
const prNumber = context.payload.pull_request.number;
|
|
93
|
-
const { owner, repo } = context.repo;
|
|
94
|
-
|
|
95
|
-
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
|
|
96
|
-
if (!pr.labels.some(l => l.name === 'qa:approved')) {
|
|
97
|
-
console.log('qa:approved not on PR — skipping board move');
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const body = pr.body ?? '';
|
|
102
|
-
const linkedIssues = [...body.matchAll(/(?:Closes?|Fixes?|Resolves?)\s+#(\d+)/gi)]
|
|
103
|
-
.map(m => parseInt(m[1]));
|
|
104
|
-
|
|
105
|
-
const moveItem = async (nodeId) => {
|
|
106
|
-
const { addProjectV2ItemById: { item } } = await github.graphql(`
|
|
107
|
-
mutation($p: ID!, $c: ID!) {
|
|
108
|
-
addProjectV2ItemById(input: {projectId: $p, contentId: $c}) { item { id } }
|
|
109
|
-
}`, { p: process.env.PROJECT_ID, c: nodeId });
|
|
110
|
-
await github.graphql(`
|
|
111
|
-
mutation($p: ID!, $i: ID!, $f: ID!, $v: ProjectV2FieldValue!) {
|
|
112
|
-
updateProjectV2ItemFieldValue(input: {projectId: $p, itemId: $i, fieldId: $f, value: $v}) {
|
|
113
|
-
projectV2Item { id }
|
|
114
|
-
}
|
|
115
|
-
}`, { p: process.env.PROJECT_ID, i: item.id, f: process.env.STATUS_FIELD_ID,
|
|
116
|
-
v: { singleSelectOptionId: process.env.STATUS_CODE_REVIEW_PR } });
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
if (linkedIssues.length > 0) {
|
|
120
|
-
for (const n of linkedIssues) {
|
|
121
|
-
const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number: n });
|
|
122
|
-
await moveItem(issue.node_id);
|
|
123
|
-
console.log(`Issue #${n} → Code Review / PR`);
|
|
124
|
-
}
|
|
125
|
-
} else {
|
|
126
|
-
await moveItem(pr.node_id);
|
|
127
|
-
console.log(`PR #${prNumber} → Code Review / PR (no linked issue)`);
|
|
128
|
-
}
|