@bradygaster/squad-sdk 0.8.17 → 0.8.18

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.
Files changed (64) hide show
  1. package/dist/adapter/client.d.ts.map +1 -1
  2. package/dist/adapter/client.js +2 -0
  3. package/dist/adapter/client.js.map +1 -1
  4. package/dist/config/init.d.ts +43 -2
  5. package/dist/config/init.d.ts.map +1 -1
  6. package/dist/config/init.js +389 -46
  7. package/dist/config/init.js.map +1 -1
  8. package/dist/index.d.ts +8 -13
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +6 -9
  11. package/dist/index.js.map +1 -1
  12. package/dist/ralph/index.js +5 -5
  13. package/dist/ralph/index.js.map +1 -1
  14. package/dist/remote/bridge.d.ts +2 -0
  15. package/dist/remote/bridge.d.ts.map +1 -1
  16. package/dist/remote/bridge.js +34 -4
  17. package/dist/remote/bridge.js.map +1 -1
  18. package/dist/resolution.d.ts +13 -0
  19. package/dist/resolution.d.ts.map +1 -1
  20. package/dist/resolution.js +9 -1
  21. package/dist/resolution.js.map +1 -1
  22. package/dist/sharing/consult.d.ts +226 -0
  23. package/dist/sharing/consult.d.ts.map +1 -0
  24. package/dist/sharing/consult.js +818 -0
  25. package/dist/sharing/consult.js.map +1 -0
  26. package/dist/sharing/index.d.ts +2 -1
  27. package/dist/sharing/index.d.ts.map +1 -1
  28. package/dist/sharing/index.js +2 -1
  29. package/dist/sharing/index.js.map +1 -1
  30. package/package.json +4 -2
  31. package/templates/casting-history.json +4 -0
  32. package/templates/casting-policy.json +35 -0
  33. package/templates/casting-registry.json +3 -0
  34. package/templates/ceremonies.md +41 -0
  35. package/templates/charter.md +53 -0
  36. package/templates/constraint-tracking.md +38 -0
  37. package/templates/copilot-instructions.md +46 -0
  38. package/templates/history.md +10 -0
  39. package/templates/identity/now.md +9 -0
  40. package/templates/identity/wisdom.md +15 -0
  41. package/templates/mcp-config.md +98 -0
  42. package/templates/multi-agent-format.md +28 -0
  43. package/templates/orchestration-log.md +27 -0
  44. package/templates/plugin-marketplace.md +49 -0
  45. package/templates/raw-agent-output.md +37 -0
  46. package/templates/roster.md +60 -0
  47. package/templates/routing.md +54 -0
  48. package/templates/run-output.md +50 -0
  49. package/templates/scribe-charter.md +119 -0
  50. package/templates/skill.md +24 -0
  51. package/templates/skills/project-conventions/SKILL.md +56 -0
  52. package/templates/squad.agent.md +1146 -0
  53. package/templates/workflows/squad-ci.yml +24 -0
  54. package/templates/workflows/squad-docs.yml +50 -0
  55. package/templates/workflows/squad-heartbeat.yml +316 -0
  56. package/templates/workflows/squad-insider-release.yml +61 -0
  57. package/templates/workflows/squad-issue-assign.yml +161 -0
  58. package/templates/workflows/squad-label-enforce.yml +181 -0
  59. package/templates/workflows/squad-main-guard.yml +129 -0
  60. package/templates/workflows/squad-preview.yml +55 -0
  61. package/templates/workflows/squad-promote.yml +121 -0
  62. package/templates/workflows/squad-release.yml +77 -0
  63. package/templates/workflows/squad-triage.yml +260 -0
  64. 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."