@bradygaster/squad-sdk 0.8.17 → 0.8.19
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/dist/adapter/client.d.ts.map +1 -1
- package/dist/adapter/client.js +2 -0
- package/dist/adapter/client.js.map +1 -1
- package/dist/config/init.d.ts +43 -2
- package/dist/config/init.d.ts.map +1 -1
- package/dist/config/init.js +389 -46
- package/dist/config/init.js.map +1 -1
- package/dist/index.d.ts +8 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -9
- package/dist/index.js.map +1 -1
- package/dist/ralph/index.js +5 -5
- package/dist/ralph/index.js.map +1 -1
- package/dist/remote/bridge.d.ts +2 -0
- package/dist/remote/bridge.d.ts.map +1 -1
- package/dist/remote/bridge.js +34 -4
- package/dist/remote/bridge.js.map +1 -1
- package/dist/resolution.d.ts +13 -0
- package/dist/resolution.d.ts.map +1 -1
- package/dist/resolution.js +9 -1
- package/dist/resolution.js.map +1 -1
- package/dist/sharing/consult.d.ts +226 -0
- package/dist/sharing/consult.d.ts.map +1 -0
- package/dist/sharing/consult.js +818 -0
- package/dist/sharing/consult.js.map +1 -0
- package/dist/sharing/index.d.ts +2 -1
- package/dist/sharing/index.d.ts.map +1 -1
- package/dist/sharing/index.js +2 -1
- package/dist/sharing/index.js.map +1 -1
- package/package.json +207 -205
- package/templates/casting-history.json +4 -0
- package/templates/casting-policy.json +35 -0
- package/templates/casting-registry.json +3 -0
- package/templates/ceremonies.md +41 -0
- package/templates/charter.md +53 -0
- package/templates/constraint-tracking.md +38 -0
- package/templates/copilot-instructions.md +46 -0
- package/templates/history.md +10 -0
- package/templates/identity/now.md +9 -0
- package/templates/identity/wisdom.md +15 -0
- package/templates/mcp-config.md +98 -0
- package/templates/multi-agent-format.md +28 -0
- package/templates/orchestration-log.md +27 -0
- package/templates/plugin-marketplace.md +49 -0
- package/templates/raw-agent-output.md +37 -0
- package/templates/roster.md +60 -0
- package/templates/routing.md +54 -0
- package/templates/run-output.md +50 -0
- package/templates/scribe-charter.md +119 -0
- package/templates/skill.md +24 -0
- package/templates/skills/project-conventions/SKILL.md +56 -0
- package/templates/squad.agent.md +1146 -0
- package/templates/workflows/squad-ci.yml +24 -0
- package/templates/workflows/squad-docs.yml +50 -0
- package/templates/workflows/squad-heartbeat.yml +316 -0
- package/templates/workflows/squad-insider-release.yml +61 -0
- package/templates/workflows/squad-issue-assign.yml +161 -0
- package/templates/workflows/squad-label-enforce.yml +181 -0
- package/templates/workflows/squad-main-guard.yml +129 -0
- package/templates/workflows/squad-preview.yml +55 -0
- package/templates/workflows/squad-promote.yml +121 -0
- package/templates/workflows/squad-release.yml +77 -0
- package/templates/workflows/squad-triage.yml +260 -0
- package/templates/workflows/sync-squad-labels.yml +169 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
name: Squad Label Enforce
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
issues:
|
|
5
|
+
types: [labeled]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
issues: write
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
enforce:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Enforce mutual exclusivity
|
|
18
|
+
uses: actions/github-script@v7
|
|
19
|
+
with:
|
|
20
|
+
script: |
|
|
21
|
+
const issue = context.payload.issue;
|
|
22
|
+
const appliedLabel = context.payload.label.name;
|
|
23
|
+
|
|
24
|
+
// Namespaces with mutual exclusivity rules
|
|
25
|
+
const EXCLUSIVE_PREFIXES = ['go:', 'release:', 'type:', 'priority:'];
|
|
26
|
+
|
|
27
|
+
// Skip if not a managed namespace label
|
|
28
|
+
if (!EXCLUSIVE_PREFIXES.some(p => appliedLabel.startsWith(p))) {
|
|
29
|
+
core.info(`Label ${appliedLabel} is not in a managed namespace — skipping`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const allLabels = issue.labels.map(l => l.name);
|
|
34
|
+
|
|
35
|
+
// Handle go: namespace (mutual exclusivity)
|
|
36
|
+
if (appliedLabel.startsWith('go:')) {
|
|
37
|
+
const otherGoLabels = allLabels.filter(l =>
|
|
38
|
+
l.startsWith('go:') && l !== appliedLabel
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
if (otherGoLabels.length > 0) {
|
|
42
|
+
// Remove conflicting go: labels
|
|
43
|
+
for (const label of otherGoLabels) {
|
|
44
|
+
await github.rest.issues.removeLabel({
|
|
45
|
+
owner: context.repo.owner,
|
|
46
|
+
repo: context.repo.repo,
|
|
47
|
+
issue_number: issue.number,
|
|
48
|
+
name: label
|
|
49
|
+
});
|
|
50
|
+
core.info(`Removed conflicting label: ${label}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Post update comment
|
|
54
|
+
await github.rest.issues.createComment({
|
|
55
|
+
owner: context.repo.owner,
|
|
56
|
+
repo: context.repo.repo,
|
|
57
|
+
issue_number: issue.number,
|
|
58
|
+
body: `🏷️ Triage verdict updated → \`${appliedLabel}\``
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Auto-apply release:backlog if go:yes and no release target
|
|
63
|
+
if (appliedLabel === 'go:yes') {
|
|
64
|
+
const hasReleaseLabel = allLabels.some(l => l.startsWith('release:'));
|
|
65
|
+
if (!hasReleaseLabel) {
|
|
66
|
+
await github.rest.issues.addLabels({
|
|
67
|
+
owner: context.repo.owner,
|
|
68
|
+
repo: context.repo.repo,
|
|
69
|
+
issue_number: issue.number,
|
|
70
|
+
labels: ['release:backlog']
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await github.rest.issues.createComment({
|
|
74
|
+
owner: context.repo.owner,
|
|
75
|
+
repo: context.repo.repo,
|
|
76
|
+
issue_number: issue.number,
|
|
77
|
+
body: `📋 Marked as \`release:backlog\` — assign a release target when ready.`
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
core.info('Applied release:backlog for go:yes issue');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Remove release: labels if go:no
|
|
85
|
+
if (appliedLabel === 'go:no') {
|
|
86
|
+
const releaseLabels = allLabels.filter(l => l.startsWith('release:'));
|
|
87
|
+
if (releaseLabels.length > 0) {
|
|
88
|
+
for (const label of releaseLabels) {
|
|
89
|
+
await github.rest.issues.removeLabel({
|
|
90
|
+
owner: context.repo.owner,
|
|
91
|
+
repo: context.repo.repo,
|
|
92
|
+
issue_number: issue.number,
|
|
93
|
+
name: label
|
|
94
|
+
});
|
|
95
|
+
core.info(`Removed release label from go:no issue: ${label}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Handle release: namespace (mutual exclusivity)
|
|
102
|
+
if (appliedLabel.startsWith('release:')) {
|
|
103
|
+
const otherReleaseLabels = allLabels.filter(l =>
|
|
104
|
+
l.startsWith('release:') && l !== appliedLabel
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
if (otherReleaseLabels.length > 0) {
|
|
108
|
+
// Remove conflicting release: labels
|
|
109
|
+
for (const label of otherReleaseLabels) {
|
|
110
|
+
await github.rest.issues.removeLabel({
|
|
111
|
+
owner: context.repo.owner,
|
|
112
|
+
repo: context.repo.repo,
|
|
113
|
+
issue_number: issue.number,
|
|
114
|
+
name: label
|
|
115
|
+
});
|
|
116
|
+
core.info(`Removed conflicting label: ${label}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Post update comment
|
|
120
|
+
await github.rest.issues.createComment({
|
|
121
|
+
owner: context.repo.owner,
|
|
122
|
+
repo: context.repo.repo,
|
|
123
|
+
issue_number: issue.number,
|
|
124
|
+
body: `🏷️ Release target updated → \`${appliedLabel}\``
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Handle type: namespace (mutual exclusivity)
|
|
130
|
+
if (appliedLabel.startsWith('type:')) {
|
|
131
|
+
const otherTypeLabels = allLabels.filter(l =>
|
|
132
|
+
l.startsWith('type:') && l !== appliedLabel
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
if (otherTypeLabels.length > 0) {
|
|
136
|
+
for (const label of otherTypeLabels) {
|
|
137
|
+
await github.rest.issues.removeLabel({
|
|
138
|
+
owner: context.repo.owner,
|
|
139
|
+
repo: context.repo.repo,
|
|
140
|
+
issue_number: issue.number,
|
|
141
|
+
name: label
|
|
142
|
+
});
|
|
143
|
+
core.info(`Removed conflicting label: ${label}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
await github.rest.issues.createComment({
|
|
147
|
+
owner: context.repo.owner,
|
|
148
|
+
repo: context.repo.repo,
|
|
149
|
+
issue_number: issue.number,
|
|
150
|
+
body: `🏷️ Issue type updated → \`${appliedLabel}\``
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Handle priority: namespace (mutual exclusivity)
|
|
156
|
+
if (appliedLabel.startsWith('priority:')) {
|
|
157
|
+
const otherPriorityLabels = allLabels.filter(l =>
|
|
158
|
+
l.startsWith('priority:') && l !== appliedLabel
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
if (otherPriorityLabels.length > 0) {
|
|
162
|
+
for (const label of otherPriorityLabels) {
|
|
163
|
+
await github.rest.issues.removeLabel({
|
|
164
|
+
owner: context.repo.owner,
|
|
165
|
+
repo: context.repo.repo,
|
|
166
|
+
issue_number: issue.number,
|
|
167
|
+
name: label
|
|
168
|
+
});
|
|
169
|
+
core.info(`Removed conflicting label: ${label}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
await github.rest.issues.createComment({
|
|
173
|
+
owner: context.repo.owner,
|
|
174
|
+
repo: context.repo.repo,
|
|
175
|
+
issue_number: issue.number,
|
|
176
|
+
body: `🏷️ Priority updated → \`${appliedLabel}\``
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
core.info(`Label enforcement complete for ${appliedLabel}`);
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
name: Squad Protected Branch Guard
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches: [main, preview, insider]
|
|
6
|
+
types: [opened, synchronize, reopened]
|
|
7
|
+
push:
|
|
8
|
+
branches: [main, preview, insider]
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
pull-requests: read
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
guard:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Check for forbidden paths
|
|
21
|
+
uses: actions/github-script@v7
|
|
22
|
+
with:
|
|
23
|
+
script: |
|
|
24
|
+
// Fetch all files changed - handles both PR and push events
|
|
25
|
+
let files = [];
|
|
26
|
+
|
|
27
|
+
if (context.eventName === 'pull_request') {
|
|
28
|
+
// PR event: use pulls.listFiles API
|
|
29
|
+
let page = 1;
|
|
30
|
+
while (true) {
|
|
31
|
+
const resp = await github.rest.pulls.listFiles({
|
|
32
|
+
owner: context.repo.owner,
|
|
33
|
+
repo: context.repo.repo,
|
|
34
|
+
pull_number: context.payload.pull_request.number,
|
|
35
|
+
per_page: 100,
|
|
36
|
+
page
|
|
37
|
+
});
|
|
38
|
+
files.push(...resp.data);
|
|
39
|
+
if (resp.data.length < 100) break;
|
|
40
|
+
page++;
|
|
41
|
+
}
|
|
42
|
+
} else if (context.eventName === 'push') {
|
|
43
|
+
// Push event: compare against base branch
|
|
44
|
+
const base = context.payload.before;
|
|
45
|
+
const head = context.payload.after;
|
|
46
|
+
|
|
47
|
+
// If this is not a force push and base exists, compare commits
|
|
48
|
+
if (base && base !== '0000000000000000000000000000000000000000') {
|
|
49
|
+
const comparison = await github.rest.repos.compareCommits({
|
|
50
|
+
owner: context.repo.owner,
|
|
51
|
+
repo: context.repo.repo,
|
|
52
|
+
base,
|
|
53
|
+
head
|
|
54
|
+
});
|
|
55
|
+
files = comparison.data.files || [];
|
|
56
|
+
} else {
|
|
57
|
+
// Force push or initial commit: list all files in the current tree
|
|
58
|
+
core.info('Force push detected or initial commit, checking tree state');
|
|
59
|
+
const { data: tree } = await github.rest.git.getTree({
|
|
60
|
+
owner: context.repo.owner,
|
|
61
|
+
repo: context.repo.repo,
|
|
62
|
+
tree_sha: head,
|
|
63
|
+
recursive: 'true'
|
|
64
|
+
});
|
|
65
|
+
files = tree.tree
|
|
66
|
+
.filter(item => item.type === 'blob')
|
|
67
|
+
.map(item => ({ filename: item.path, status: 'added' }));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check each file against forbidden path rules
|
|
72
|
+
// Allow removals — deleting forbidden files from protected branches is fine
|
|
73
|
+
const forbidden = files
|
|
74
|
+
.filter(f => f.status !== 'removed')
|
|
75
|
+
.map(f => f.filename)
|
|
76
|
+
.filter(f => {
|
|
77
|
+
// .ai-team/** and .squad/** — ALL team state files, zero exceptions
|
|
78
|
+
if (f === '.ai-team' || f.startsWith('.ai-team/') || f === '.squad' || f.startsWith('.squad/')) return true;
|
|
79
|
+
// .ai-team-templates/** — Squad's own templates, stay on dev
|
|
80
|
+
if (f === '.ai-team-templates' || f.startsWith('.ai-team-templates/')) return true;
|
|
81
|
+
// team-docs/** — ALL internal team docs, zero exceptions
|
|
82
|
+
if (f.startsWith('team-docs/')) return true;
|
|
83
|
+
// docs/proposals/** — internal design proposals, stay on dev
|
|
84
|
+
if (f.startsWith('docs/proposals/')) return true;
|
|
85
|
+
return false;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (forbidden.length === 0) {
|
|
89
|
+
core.info('✅ No forbidden paths found in PR — all clear.');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Build a clear, actionable error message
|
|
94
|
+
const lines = [
|
|
95
|
+
'## 🚫 Forbidden files detected in PR to main',
|
|
96
|
+
'',
|
|
97
|
+
'The following files must NOT be merged into `main`.',
|
|
98
|
+
'`.ai-team/` and `.squad/` are runtime team state — they belong on dev branches only.',
|
|
99
|
+
'`.ai-team-templates/` is Squad\'s internal planning — it belongs on dev branches only.',
|
|
100
|
+
'`team-docs/` is internal team content — it belongs on dev branches only.',
|
|
101
|
+
'`docs/proposals/` is internal design proposals — it belongs on dev branches only.',
|
|
102
|
+
'',
|
|
103
|
+
'### Forbidden files found:',
|
|
104
|
+
'',
|
|
105
|
+
...forbidden.map(f => `- \`${f}\``),
|
|
106
|
+
'',
|
|
107
|
+
'### How to fix:',
|
|
108
|
+
'',
|
|
109
|
+
'```bash',
|
|
110
|
+
'# Remove tracked .ai-team/ files (keeps local copies):',
|
|
111
|
+
'git rm --cached -r .ai-team/',
|
|
112
|
+
'',
|
|
113
|
+
'# Remove tracked .squad/ files (keeps local copies):',
|
|
114
|
+
'git rm --cached -r .squad/',
|
|
115
|
+
'',
|
|
116
|
+
'# Remove tracked team-docs/ files:',
|
|
117
|
+
'git rm --cached -r team-docs/',
|
|
118
|
+
'',
|
|
119
|
+
'# Commit the removal and push:',
|
|
120
|
+
'git commit -m "chore: remove forbidden paths from PR"',
|
|
121
|
+
'git push',
|
|
122
|
+
'```',
|
|
123
|
+
'',
|
|
124
|
+
'> ⚠️ `.ai-team/` and `.squad/` are committed on `dev` and feature branches by design.',
|
|
125
|
+
'> The guard workflow is the enforcement mechanism that keeps these files off `main` and `preview`.',
|
|
126
|
+
'> `git rm --cached` untracks them from this PR without deleting your local copies.',
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
core.setFailed(lines.join('\n'));
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
name: Squad Preview Validation
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [preview]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
validate:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- uses: actions/setup-node@v4
|
|
17
|
+
with:
|
|
18
|
+
node-version: 22
|
|
19
|
+
|
|
20
|
+
- name: Validate version consistency
|
|
21
|
+
run: |
|
|
22
|
+
VERSION=$(node -e "console.log(require('./package.json').version)")
|
|
23
|
+
if ! grep -q "## \[$VERSION\]" CHANGELOG.md 2>/dev/null; then
|
|
24
|
+
echo "::error::Version $VERSION not found in CHANGELOG.md — update CHANGELOG.md before release"
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
echo "✅ Version $VERSION validated in CHANGELOG.md"
|
|
28
|
+
|
|
29
|
+
- name: Run tests
|
|
30
|
+
run: node --test test/*.test.js
|
|
31
|
+
|
|
32
|
+
- name: Check no .ai-team/ or .squad/ files are tracked
|
|
33
|
+
run: |
|
|
34
|
+
FOUND_FORBIDDEN=0
|
|
35
|
+
if git ls-files --error-unmatch .ai-team/ 2>/dev/null; then
|
|
36
|
+
echo "::error::❌ .ai-team/ files are tracked on preview — this must not ship."
|
|
37
|
+
FOUND_FORBIDDEN=1
|
|
38
|
+
fi
|
|
39
|
+
if git ls-files --error-unmatch .squad/ 2>/dev/null; then
|
|
40
|
+
echo "::error::❌ .squad/ files are tracked on preview — this must not ship."
|
|
41
|
+
FOUND_FORBIDDEN=1
|
|
42
|
+
fi
|
|
43
|
+
if [ $FOUND_FORBIDDEN -eq 1 ]; then
|
|
44
|
+
exit 1
|
|
45
|
+
fi
|
|
46
|
+
echo "✅ No .ai-team/ or .squad/ files tracked — clean for release."
|
|
47
|
+
|
|
48
|
+
- name: Validate package.json version
|
|
49
|
+
run: |
|
|
50
|
+
VERSION=$(node -e "console.log(require('./package.json').version)")
|
|
51
|
+
if [ -z "$VERSION" ]; then
|
|
52
|
+
echo "::error::❌ No version field found in package.json."
|
|
53
|
+
exit 1
|
|
54
|
+
fi
|
|
55
|
+
echo "✅ package.json version: $VERSION"
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
name: Squad Promote
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
dry_run:
|
|
7
|
+
description: 'Dry run — show what would happen without pushing'
|
|
8
|
+
required: false
|
|
9
|
+
default: 'false'
|
|
10
|
+
type: choice
|
|
11
|
+
options: ['false', 'true']
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: write
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
dev-to-preview:
|
|
18
|
+
name: Promote dev → preview
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
with:
|
|
23
|
+
fetch-depth: 0
|
|
24
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
25
|
+
|
|
26
|
+
- name: Configure git
|
|
27
|
+
run: |
|
|
28
|
+
git config user.name "github-actions[bot]"
|
|
29
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
30
|
+
|
|
31
|
+
- name: Fetch all branches
|
|
32
|
+
run: git fetch --all
|
|
33
|
+
|
|
34
|
+
- name: Show current state (dry run info)
|
|
35
|
+
run: |
|
|
36
|
+
echo "=== dev HEAD ===" && git log origin/dev -1 --oneline
|
|
37
|
+
echo "=== preview HEAD ===" && git log origin/preview -1 --oneline
|
|
38
|
+
echo "=== Files that would be stripped ==="
|
|
39
|
+
git diff origin/preview..origin/dev --name-only | grep -E "^(\.(ai-team|squad|ai-team-templates|squad-templates)|team-docs/|docs/proposals/)" || echo "(none)"
|
|
40
|
+
|
|
41
|
+
- name: Merge dev → preview (strip forbidden paths)
|
|
42
|
+
if: ${{ inputs.dry_run == 'false' }}
|
|
43
|
+
run: |
|
|
44
|
+
git checkout preview
|
|
45
|
+
git merge origin/dev --no-commit --no-ff -X theirs || true
|
|
46
|
+
|
|
47
|
+
# Strip forbidden paths from merge commit
|
|
48
|
+
git rm -rf --cached --ignore-unmatch \
|
|
49
|
+
.ai-team/ \
|
|
50
|
+
.squad/ \
|
|
51
|
+
.ai-team-templates/ \
|
|
52
|
+
.squad-templates/ \
|
|
53
|
+
team-docs/ \
|
|
54
|
+
"docs/proposals/" || true
|
|
55
|
+
|
|
56
|
+
# Commit if there are staged changes
|
|
57
|
+
if ! git diff --cached --quiet; then
|
|
58
|
+
git commit -m "chore: promote dev → preview (v$(node -e "console.log(require('./package.json').version)"))"
|
|
59
|
+
git push origin preview
|
|
60
|
+
echo "✅ Pushed preview branch"
|
|
61
|
+
else
|
|
62
|
+
echo "ℹ️ Nothing to commit — preview is already up to date"
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
- name: Dry run complete
|
|
66
|
+
if: ${{ inputs.dry_run == 'true' }}
|
|
67
|
+
run: echo "🔍 Dry run complete — no changes pushed."
|
|
68
|
+
|
|
69
|
+
preview-to-main:
|
|
70
|
+
name: Promote preview → main (release)
|
|
71
|
+
needs: dev-to-preview
|
|
72
|
+
runs-on: ubuntu-latest
|
|
73
|
+
steps:
|
|
74
|
+
- uses: actions/checkout@v4
|
|
75
|
+
with:
|
|
76
|
+
fetch-depth: 0
|
|
77
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
78
|
+
|
|
79
|
+
- name: Configure git
|
|
80
|
+
run: |
|
|
81
|
+
git config user.name "github-actions[bot]"
|
|
82
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
83
|
+
|
|
84
|
+
- name: Fetch all branches
|
|
85
|
+
run: git fetch --all
|
|
86
|
+
|
|
87
|
+
- name: Show current state
|
|
88
|
+
run: |
|
|
89
|
+
echo "=== preview HEAD ===" && git log origin/preview -1 --oneline
|
|
90
|
+
echo "=== main HEAD ===" && git log origin/main -1 --oneline
|
|
91
|
+
echo "=== Version ===" && node -e "console.log('v' + require('./package.json').version)"
|
|
92
|
+
|
|
93
|
+
- name: Validate preview is release-ready
|
|
94
|
+
run: |
|
|
95
|
+
git checkout preview
|
|
96
|
+
VERSION=$(node -e "console.log(require('./package.json').version)")
|
|
97
|
+
if ! grep -q "## \[$VERSION\]" CHANGELOG.md 2>/dev/null; then
|
|
98
|
+
echo "::error::Version $VERSION not found in CHANGELOG.md — update before releasing"
|
|
99
|
+
exit 1
|
|
100
|
+
fi
|
|
101
|
+
echo "✅ Version $VERSION has CHANGELOG entry"
|
|
102
|
+
|
|
103
|
+
# Verify no forbidden files on preview
|
|
104
|
+
FORBIDDEN=$(git ls-files | grep -E "^(\.(ai-team|squad|ai-team-templates|squad-templates)/|team-docs/|docs/proposals/)" || true)
|
|
105
|
+
if [ -n "$FORBIDDEN" ]; then
|
|
106
|
+
echo "::error::Forbidden files found on preview: $FORBIDDEN"
|
|
107
|
+
exit 1
|
|
108
|
+
fi
|
|
109
|
+
echo "✅ No forbidden files on preview"
|
|
110
|
+
|
|
111
|
+
- name: Merge preview → main
|
|
112
|
+
if: ${{ inputs.dry_run == 'false' }}
|
|
113
|
+
run: |
|
|
114
|
+
git checkout main
|
|
115
|
+
git merge origin/preview --no-ff -m "chore: promote preview → main (v$(node -e "console.log(require('./package.json').version)"))"
|
|
116
|
+
git push origin main
|
|
117
|
+
echo "✅ Pushed main — squad-release.yml will tag and publish the release"
|
|
118
|
+
|
|
119
|
+
- name: Dry run complete
|
|
120
|
+
if: ${{ inputs.dry_run == 'true' }}
|
|
121
|
+
run: echo "🔍 Dry run complete — no changes pushed."
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
name: Squad Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: write
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
release:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
with:
|
|
16
|
+
fetch-depth: 0
|
|
17
|
+
|
|
18
|
+
- uses: actions/setup-node@v4
|
|
19
|
+
with:
|
|
20
|
+
node-version: 22
|
|
21
|
+
|
|
22
|
+
- name: Run tests
|
|
23
|
+
run: node --test test/*.test.js
|
|
24
|
+
|
|
25
|
+
- name: Validate version consistency
|
|
26
|
+
run: |
|
|
27
|
+
VERSION=$(node -e "console.log(require('./package.json').version)")
|
|
28
|
+
if ! grep -q "## \[$VERSION\]" CHANGELOG.md 2>/dev/null; then
|
|
29
|
+
echo "::error::Version $VERSION not found in CHANGELOG.md — update CHANGELOG.md before release"
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
echo "✅ Version $VERSION validated in CHANGELOG.md"
|
|
33
|
+
|
|
34
|
+
- name: Read version from package.json
|
|
35
|
+
id: version
|
|
36
|
+
run: |
|
|
37
|
+
VERSION=$(node -e "console.log(require('./package.json').version)")
|
|
38
|
+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
39
|
+
echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
|
|
40
|
+
echo "📦 Version: $VERSION (tag: v$VERSION)"
|
|
41
|
+
|
|
42
|
+
- name: Check if tag already exists
|
|
43
|
+
id: check_tag
|
|
44
|
+
run: |
|
|
45
|
+
if git rev-parse "refs/tags/${{ steps.version.outputs.tag }}" >/dev/null 2>&1; then
|
|
46
|
+
echo "exists=true" >> "$GITHUB_OUTPUT"
|
|
47
|
+
echo "⏭️ Tag ${{ steps.version.outputs.tag }} already exists — skipping release."
|
|
48
|
+
else
|
|
49
|
+
echo "exists=false" >> "$GITHUB_OUTPUT"
|
|
50
|
+
echo "🆕 Tag ${{ steps.version.outputs.tag }} does not exist — creating release."
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
- name: Create git tag
|
|
54
|
+
if: steps.check_tag.outputs.exists == 'false'
|
|
55
|
+
run: |
|
|
56
|
+
git config user.name "github-actions[bot]"
|
|
57
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
58
|
+
git tag -a "${{ steps.version.outputs.tag }}" -m "Release ${{ steps.version.outputs.tag }}"
|
|
59
|
+
git push origin "${{ steps.version.outputs.tag }}"
|
|
60
|
+
|
|
61
|
+
- name: Create GitHub Release
|
|
62
|
+
if: steps.check_tag.outputs.exists == 'false'
|
|
63
|
+
env:
|
|
64
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
65
|
+
run: |
|
|
66
|
+
gh release create "${{ steps.version.outputs.tag }}" \
|
|
67
|
+
--title "${{ steps.version.outputs.tag }}" \
|
|
68
|
+
--generate-notes \
|
|
69
|
+
--latest
|
|
70
|
+
|
|
71
|
+
- name: Verify release
|
|
72
|
+
if: steps.check_tag.outputs.exists == 'false'
|
|
73
|
+
env:
|
|
74
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
75
|
+
run: |
|
|
76
|
+
gh release view "${{ steps.version.outputs.tag }}"
|
|
77
|
+
echo "✅ Release ${{ steps.version.outputs.tag }} created and verified."
|