Skip to content

Commit 16653b0

Browse files
committed
feat: implement tag-driven GitHub Release automation and update release documentation
1 parent 54977ed commit 16653b0

7 files changed

Lines changed: 741 additions & 10 deletions

File tree

.documentation/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- Tag-driven GitHub Release automation that packages the built site together with DevSpark release docs
1213
- World-class README.md with comprehensive documentation
1314
- Complete documentation suite in `/documentation` folder
1415
- Contributing guidelines and project governance
@@ -17,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1718

1819
### Changed
1920

21+
- `/devspark.release` now relies on the upgraded release context contract in `.documentation/scripts/powershell/release-context.ps1`
2022
- Improved project organization with centralized documentation
2123
- Enhanced copilot instructions for better AI assistance
2224

.documentation/DEPLOYMENT.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,35 @@ jobs:
171171
- ❌ No server-side features
172172
- ❌ Limited custom domain features
173173
174+
### 2.1 GitHub Releases From DevSpark
175+
176+
The repository now supports a tag-driven GitHub Release flow that complements `/devspark.release`.
177+
178+
Release contract:
179+
180+
1. Run `/devspark.release` and commit the generated release documentation.
181+
2. Create a semantic version tag that matches the release folder name, for example `v2.1.0`.
182+
3. Push both the commit and the tag.
183+
4. GitHub Actions builds the site, packages the built `docs/` output together with `.documentation/releases/vX.Y.Z/`, and publishes a GitHub Release automatically.
184+
185+
Example:
186+
187+
```bash
188+
git add -A
189+
git commit -m "docs: release v2.1.0"
190+
git push origin main
191+
git tag v2.1.0
192+
git push origin v2.1.0
193+
```
194+
195+
The resulting GitHub Release includes:
196+
197+
- A release body sourced from `.documentation/releases/vX.Y.Z/release-notes.md`
198+
- A zip bundle containing the production site and release documentation
199+
- A SHA-256 checksum for the bundle
200+
201+
The workflow will fail if the pushed tag does not have matching release documentation committed in `.documentation/releases/vX.Y.Z/`.
202+
174203
### 3. Netlify
175204

176205
Popular alternative with excellent developer experience.

.documentation/Guide.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,12 @@ DevSpark commands are invoked via `/devspark.<command>` in Claude Code. Scripts
6060
| `/devspark.pr-review` | Constitution-aware PR review | `get-pr-context.sh` |
6161
| `/devspark.quickfix` | Lightweight bug fix workflow | `quickfix-context.sh` |
6262
| `/devspark.site-audit` | Audit documentation quality | `site-audit.sh` |
63-
| `/devspark.release` | Review and prepare release artifacts | `release-context.sh` |
63+
| `/devspark.release` | Review and prepare release artifacts, then pair with a pushed `vX.Y.Z` tag to publish a GitHub Release package | `release-context.sh` |
64+
65+
### Release Automation
66+
67+
`/devspark.release` is expected to produce the versioned release docs under `.documentation/releases/vX.Y.Z/`.
68+
When that release commit is tagged and the tag is pushed, `.github/workflows/github-release.yml` builds the site and publishes a GitHub Release using those generated release notes and docs.
6469

6570
---
6671

.documentation/scripts/powershell/release-context.ps1

Lines changed: 241 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,181 @@ param(
1111

1212
. (Join-Path $PSScriptRoot 'common.ps1')
1313

14+
# Multi-app support (T086)
15+
if (-not (Get-Command Detect-DevSparkMode -ErrorAction SilentlyContinue)) {
16+
. "$PSScriptRoot/common.ps1"
17+
}
18+
19+
function Test-CompletedTasks {
20+
param([string]$Content)
21+
22+
if (-not $Content) {
23+
return $false
24+
}
25+
26+
$unchecked = ([regex]::Matches($Content, '^\s*- \[ \]', 'Multiline')).Count
27+
$checked = ([regex]::Matches($Content, '^\s*- \[[xX]\]', 'Multiline')).Count
28+
return ($unchecked -eq 0 -and $checked -gt 0)
29+
}
30+
31+
function Get-ArchiveRecovery {
32+
param(
33+
[string]$RepoRoot,
34+
[string]$FromDate,
35+
[string]$ToDate
36+
)
37+
38+
$archiveRoot = Join-Path $RepoRoot '.archive'
39+
$completedSpecs = @()
40+
$quickfixes = @()
41+
$fromCutoffDate = $null
42+
$toCutoffDate = $null
43+
44+
if ($FromDate) {
45+
try {
46+
$fromCutoffDate = ([datetime]$FromDate).Date
47+
} catch {
48+
$fromCutoffDate = $null
49+
}
50+
}
51+
52+
if ($ToDate) {
53+
try {
54+
$toCutoffDate = ([datetime]$ToDate).Date
55+
} catch {
56+
$toCutoffDate = $null
57+
}
58+
}
59+
60+
if (-not (Test-Path $archiveRoot)) {
61+
return @{
62+
Specs = @()
63+
Quickfixes = @()
64+
}
65+
}
66+
67+
Get-ChildItem -Path $archiveRoot -Directory -ErrorAction SilentlyContinue | ForEach-Object {
68+
$batchDate = $null
69+
if ($_.Name -match '^\d{4}-\d{2}-\d{2}$') {
70+
try {
71+
$batchDate = ([datetime]::ParseExact($_.Name, 'yyyy-MM-dd', $null)).Date
72+
} catch {
73+
$batchDate = $null
74+
}
75+
}
76+
77+
if ($fromCutoffDate -and $batchDate -and $batchDate -lt $fromCutoffDate) {
78+
return
79+
}
80+
81+
if ($toCutoffDate -and $batchDate -and $batchDate -gt $toCutoffDate) {
82+
return
83+
}
84+
85+
$specArchiveRoot = Join-Path $_.FullName '.documentation/specs'
86+
if (Test-Path $specArchiveRoot) {
87+
Get-ChildItem -Path $specArchiveRoot -Directory -ErrorAction SilentlyContinue | ForEach-Object {
88+
$tasksFile = Join-Path $_.FullName 'tasks.md'
89+
if (-not (Test-Path $tasksFile)) {
90+
return
91+
}
92+
93+
$content = Get-Content $tasksFile -Raw -ErrorAction SilentlyContinue
94+
if (Test-CompletedTasks -Content $content) {
95+
$completedSpecs += $_.Name
96+
}
97+
}
98+
}
99+
100+
$quickfixArchiveRoot = Join-Path $_.FullName '.documentation/quickfixes'
101+
if (Test-Path $quickfixArchiveRoot) {
102+
$quickfixes += Get-ChildItem -Path $quickfixArchiveRoot -Filter 'QF-*.md' -ErrorAction SilentlyContinue |
103+
ForEach-Object { $_.BaseName }
104+
}
105+
}
106+
107+
return @{
108+
Specs = @($completedSpecs | Where-Object { $_ } | Sort-Object -Unique)
109+
Quickfixes = @($quickfixes | Where-Object { $_ } | Sort-Object -Unique)
110+
}
111+
}
112+
113+
function Get-HistoryRecovery {
114+
param(
115+
[string]$ScriptPath,
116+
[string]$BaseRef,
117+
[string]$FromDate,
118+
[string]$ToDate
119+
)
120+
121+
$empty = @{
122+
Specs = @()
123+
Quickfixes = @()
124+
ArchiveMovesDetected = $false
125+
ReleaseFrom = $FromDate
126+
ReleaseTo = $ToDate
127+
Commits = @()
128+
Contributors = @()
129+
MergedPrNumbers = @()
130+
MergedPrCount = 0
131+
PrReviews = @()
132+
PrReviewSummary = @{
133+
matched_reviews = 0
134+
files_changed = 0
135+
tests_added = 0
136+
breaking_changes = 0
137+
resolved_high_findings = 0
138+
}
139+
}
140+
141+
if (-not (Test-Path $ScriptPath) -or -not (Test-HasGit)) {
142+
return $empty
143+
}
144+
145+
try {
146+
$historyJson = if ($BaseRef) {
147+
& $ScriptPath -BaseRef $BaseRef -FromDate $FromDate -ToDate $ToDate -Json
148+
} else {
149+
& $ScriptPath -FromDate $FromDate -ToDate $ToDate -Json
150+
}
151+
152+
$history = $historyJson | ConvertFrom-Json
153+
return @{
154+
Specs = @($history.RECOVERED_SPECS | Where-Object { $_.completed } | ForEach-Object { $_.name } | Sort-Object -Unique)
155+
Quickfixes = @($history.RECOVERED_QUICKFIXES | ForEach-Object { $_.id } | Sort-Object -Unique)
156+
ArchiveMovesDetected = [bool]$history.ARCHIVE_MOVES_DETECTED
157+
ReleaseFrom = $history.RELEASE_FROM
158+
ReleaseTo = $history.RELEASE_TO
159+
Commits = @($history.COMMITS)
160+
Contributors = @($history.CONTRIBUTORS)
161+
MergedPrNumbers = @($history.MERGED_PR_NUMBERS)
162+
MergedPrCount = [int]$history.MERGED_PR_COUNT
163+
PrReviews = @($history.PR_REVIEWS)
164+
PrReviewSummary = $history.PR_REVIEW_SUMMARY
165+
}
166+
} catch {
167+
return $empty
168+
}
169+
}
170+
14171
# Parse arguments
15172
$versionArg = ""
16-
foreach ($arg in $Arguments) {
173+
$releaseFromArg = ""
174+
for ($index = 0; $index -lt $Arguments.Count; $index++) {
175+
$arg = $Arguments[$index]
17176
if ($arg -match '^v?\d+\.\d+') {
18177
$versionArg = $arg -replace '^v', ''
178+
continue
179+
}
180+
181+
if ($arg -eq '--from' -and $index + 1 -lt $Arguments.Count) {
182+
$releaseFromArg = $Arguments[$index + 1]
183+
$index++
184+
continue
185+
}
186+
187+
if ($arg -match '^--from=(.+)$') {
188+
$releaseFromArg = $matches[1]
19189
}
20190
}
21191

@@ -83,14 +253,19 @@ if (Test-HasGit) {
83253
}
84254
}
85255

256+
# Get timestamp
257+
$timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
258+
$releaseDate = Get-Date -Format "yyyy-MM-dd"
259+
$releaseFrom = if ($releaseFromArg) { $releaseFromArg } elseif ($lastReleaseDate) { ([datetime]$lastReleaseDate).ToString('yyyy-MM-dd') } else { '' }
260+
$releaseTo = $releaseDate
261+
86262
# Find completed and pending specs
87263
$completedSpecs = @()
88264
$pendingSpecs = @()
89265

90266
if (Test-Path $specsDir) {
91267
Get-ChildItem -Path $specsDir -Directory | ForEach-Object {
92268
$specName = $_.Name
93-
# Skip pr-review directory
94269
if ($specName -eq "pr-review") { return }
95270

96271
$tasksFile = Join-Path $_.FullName "tasks.md"
@@ -121,6 +296,26 @@ if (Test-Path $quickfixDir) {
121296
ForEach-Object { $_.BaseName }
122297
}
123298

299+
$activeCompletedSpecs = @($completedSpecs | Sort-Object -Unique)
300+
$activeQuickfixes = @($quickfixes | Sort-Object -Unique)
301+
302+
$archiveRecovery = Get-ArchiveRecovery -RepoRoot $repoRoot -FromDate $releaseFrom -ToDate $releaseTo
303+
$historyRecovery = Get-HistoryRecovery -ScriptPath (Join-Path $PSScriptRoot 'release-history-context.ps1') -BaseRef $lastTag -FromDate $releaseFrom -ToDate $releaseTo
304+
305+
$completedSpecs = @($activeCompletedSpecs + $archiveRecovery.Specs + $historyRecovery.Specs | Where-Object { $_ } | Sort-Object -Unique)
306+
$quickfixes = @($activeQuickfixes + $archiveRecovery.Quickfixes + $historyRecovery.Quickfixes | Where-Object { $_ } | Sort-Object -Unique)
307+
308+
$recoveredCompletedSpecs = @($completedSpecs | Where-Object { $_ -notin $activeCompletedSpecs })
309+
$recoveredQuickfixes = @($quickfixes | Where-Object { $_ -notin $activeQuickfixes })
310+
$archiveRecoveryUsed = [bool](($archiveRecovery.Specs.Count + $archiveRecovery.Quickfixes.Count) -gt 0)
311+
$historyRecoveryUsed = [bool](($historyRecovery.Specs.Count + $historyRecovery.Quickfixes.Count) -gt 0)
312+
$contributors = @($historyRecovery.Contributors | Where-Object { $_ } | Sort-Object -Unique)
313+
$commitsSince = $historyRecovery.Commits.Count
314+
$mergedPrNumbers = @($historyRecovery.MergedPrNumbers | Sort-Object -Unique)
315+
$mergedPrCount = [int]$historyRecovery.MergedPrCount
316+
$prReviews = @($historyRecovery.PrReviews)
317+
$prReviewSummary = $historyRecovery.PrReviewSummary
318+
124319
# Calculate next version if not provided
125320
$nextVersion = $versionArg
126321
$versionBump = "patch"
@@ -145,9 +340,7 @@ if (-not $nextVersion) {
145340
}
146341
}
147342

148-
# Get contributors
149-
$contributors = @()
150-
if (Test-HasGit) {
343+
if ($contributors.Count -eq 0 -and (Test-HasGit)) {
151344
try {
152345
if ($lastTag) {
153346
$contributors = git log "$lastTag..HEAD" --format='%aN' 2>$null | Sort-Object -Unique
@@ -158,9 +351,19 @@ if (Test-HasGit) {
158351
} catch { }
159352
}
160353

161-
# Get timestamp
162-
$timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
163-
$releaseDate = Get-Date -Format "yyyy-MM-dd"
354+
# DevSpark version stamp info
355+
$versionStampPath = Join-Path $repoRoot ".devspark/VERSION"
356+
$legacyVersionStampPath = Join-Path $repoRoot ".documentation/DEVSPARK_VERSION"
357+
$installedVersion = ""
358+
if (Test-Path $versionStampPath) {
359+
try {
360+
$installedVersion = ((Get-Content $versionStampPath -ErrorAction SilentlyContinue) | Select-String '^version:\s*(.+)$' | Select-Object -First 1).Matches.Groups[1].Value.Trim()
361+
} catch { }
362+
} elseif (Test-Path $legacyVersionStampPath) {
363+
try {
364+
$installedVersion = (Get-Content $legacyVersionStampPath -TotalCount 1 -ErrorAction SilentlyContinue).Trim()
365+
} catch { }
366+
}
164367

165368
# Output
166369
if ($Json) {
@@ -175,17 +378,33 @@ if ($Json) {
175378
VERSION_SOURCE = $versionSource
176379
NEXT_VERSION = $nextVersion
177380
VERSION_BUMP = $versionBump
381+
RELEASE_FROM = $releaseFrom
382+
RELEASE_TO = $releaseTo
383+
ACTIVE_COMPLETED_SPECS = $activeCompletedSpecs
178384
COMPLETED_SPECS = $completedSpecs
385+
RECOVERED_COMPLETED_SPECS = $recoveredCompletedSpecs
179386
PENDING_SPECS = $pendingSpecs
387+
ACTIVE_QUICKFIXES = $activeQuickfixes
180388
QUICKFIXES = $quickfixes
389+
RECOVERED_QUICKFIXES = $recoveredQuickfixes
181390
LAST_TAG = $lastTag
182391
LAST_RELEASE_DATE = $lastReleaseDate
183392
COMMITS_SINCE_RELEASE = $commitsSince
184393
CONTRIBUTORS = $contributors
394+
MERGED_PR_NUMBERS = $mergedPrNumbers
395+
MERGED_PR_COUNT = $mergedPrCount
396+
PR_REVIEWS = $prReviews
397+
PR_REVIEW_SUMMARY = $prReviewSummary
398+
ARCHIVE_RECOVERY_USED = $archiveRecoveryUsed
399+
HISTORY_RECOVERY_USED = $historyRecoveryUsed
400+
HISTORY_ARCHIVE_MOVES_DETECTED = [bool]$historyRecovery.ArchiveMovesDetected
185401
TIMESTAMP = $timestamp
186402
RELEASE_DATE = $releaseDate
187403
DRY_RUN = [bool]$DryRun
188-
} | ConvertTo-Json -Compress
404+
DEVSPARK_VERSION_PATH = $versionStampPath
405+
LEGACY_DEVSPARK_VERSION_PATH = $legacyVersionStampPath
406+
INSTALLED_VERSION = $installedVersion
407+
} | ConvertTo-Json -Depth 6
189408
}
190409
else {
191410
Write-Output "Release Context"
@@ -194,12 +413,25 @@ else {
194413
Write-Output "Current Version: $currentVersion (from $versionSource)"
195414
Write-Output "Next Version: $nextVersion ($versionBump bump)"
196415
Write-Output "Last Release: $lastTag ($lastReleaseDate)"
416+
Write-Output "Release Window: $releaseFrom -> $releaseTo"
197417
Write-Output "Commits Since: $commitsSince"
198418
Write-Output ""
199419
Write-Output "Completed Specs: $($completedSpecs.Count)"
200420
Write-Output "Pending Specs: $($pendingSpecs.Count)"
201421
Write-Output "Quickfixes: $($quickfixes.Count)"
202422
Write-Output "Contributors: $($contributors.Count)"
423+
Write-Output "Merged PRs: $mergedPrCount"
424+
if ($recoveredCompletedSpecs.Count -gt 0 -or $recoveredQuickfixes.Count -gt 0) {
425+
Write-Output ''
426+
Write-Output "Recovered Specs: $($recoveredCompletedSpecs.Count)"
427+
Write-Output "Recovered Quickfixes: $($recoveredQuickfixes.Count)"
428+
}
429+
Write-Output ""
430+
if ($installedVersion) {
431+
Write-Output "Installed DevSpark Version: $installedVersion"
432+
} else {
433+
Write-Output "Installed DevSpark Version: (VERSION stamp not found)"
434+
}
203435
if ($DryRun) {
204436
Write-Output ""
205437
Write-Output "** DRY RUN MODE - No changes will be made **"

0 commit comments

Comments
 (0)