Skip to content

Commit 7366e91

Browse files
authored
fix: release pipeline + 4-digit version support (v0.8.10) (#34)
* fix: replace bash 4+ associative arrays with bash 3-compatible helpers check_structural_fitness() used `declare -A` and check_doc_inventory() used `local -A`, both requiring bash 4+. Stock macOS ships bash 3.2, causing the script to crash silently and skip check_unprocessed() and check_mode(). Replaced with string-based key=value helpers (_kv_get, _kv_set, _kv_keys) and linear search helpers (_is_root_doc, _is_project_doc). Fixes 3 pre-existing test failures. * fix: tighten version regex and add gist bridge comment Version validation regex in update-check tightened from the loose ^[0-9]+\.[0-9.]+$ (accepted malformed versions like 1..2) to ^[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?$ (enforces exactly X.Y.Z or X.Y.Z.W), matching the roadmap-audit pattern. Added comment explaining the permanent gist bridge for pre-0.8.8.1 installs. * test: add 4-digit version and update-check tests 16 new tests: semver 4-digit comparisons (5), regex validation for valid and invalid versions (14), update-check upgrade detection with MICRO versions (2). Fixed stale setup test expectations (2→3 default skills, 3→4 with --with-native). 45 tests, 0 failures. * feat: auto-tag GitHub Action on VERSION change Creates annotated git tags (vX.Y.Z.W) automatically when VERSION changes on merge to main. Idempotent: skips if tag already exists. Validates VERSION format before tagging. Uses default GITHUB_TOKEN with contents:write. * docs: add versioning section to README, update roadmap example README now defines MAJOR.MINOR.PATCH.MICRO semantics with examples. Replaced stale gist-migration example in roadmap skill with generic example. * chore: bump version and changelog (v0.8.10)
1 parent 7a30488 commit 7366e91

9 files changed

Lines changed: 240 additions & 30 deletions

File tree

.github/workflows/auto-tag.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: Auto-tag on VERSION change
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths: [VERSION]
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
tag:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
with:
17+
fetch-depth: 0
18+
19+
- name: Read version and create tag
20+
run: |
21+
VERSION=$(cat VERSION | tr -d '[:space:]')
22+
23+
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?$'; then
24+
echo "Invalid VERSION format: '$VERSION'"
25+
exit 1
26+
fi
27+
28+
TAG="v${VERSION}"
29+
30+
if git rev-parse "$TAG" >/dev/null 2>&1; then
31+
echo "Tag $TAG already exists — skipping."
32+
exit 0
33+
fi
34+
35+
echo "Creating tag $TAG"
36+
git config user.name "github-actions[bot]"
37+
git config user.email "github-actions[bot]@users.noreply.github.com"
38+
git tag -a "$TAG" -m "Release ${VERSION}"
39+
git push origin "$TAG"
40+
echo "Tagged $TAG successfully."

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [0.8.10] - 2026-04-15
6+
7+
### Added
8+
- GitHub Action (`auto-tag.yml`) to create git tags automatically when VERSION changes on merge to main. Idempotent: skips if tag already exists.
9+
- Update-check and 4-digit version tests in `test-update.sh`: semver comparisons, regex validation, upgrade detection with MICRO versions.
10+
- Versioning section in README defining MAJOR.MINOR.PATCH.MICRO semantics.
11+
12+
### Changed
13+
- Version validation regex in `bin/update-check` tightened from `^[0-9]+\.[0-9.]+$` to `^[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?$`. Now rejects malformed versions (double dots, trailing dots, 5+ segments) while accepting X.Y.Z and X.Y.Z.W.
14+
15+
### Fixed
16+
- Added gist bridge comment in `bin/update-check` documenting why the old gist URL (pre-0.8.8.1) must be kept alive as a permanent upgrade bridge.
17+
- `bin/roadmap-audit`: replaced bash 4+ associative arrays (`declare -A`, `local -A`) with bash 3-compatible helpers. The old code crashed on stock macOS (bash 3.2), silently skipping `check_unprocessed()` and `check_mode()`. Fixed 3 pre-existing test failures.
18+
- `scripts/test-update.sh`: updated setup test expectations from 2 to 3 default skills (full-review was added in v0.8.0 but test wasn't updated). Fixed 2 pre-existing test failures.
19+
520
## [0.8.9.0] - 2026-04-14
621

722
### Changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,19 @@ native_trigger_file: ".context/snapshot-trigger"
176176

177177
- [Implementation Guide](docs/debug-infrastructure-guide.md) — How to add debug infrastructure to a new SwiftUI app
178178

179+
## Versioning
180+
181+
4-digit SemVer: `MAJOR.MINOR.PATCH.MICRO`
182+
183+
| Segment | Meaning | Example |
184+
|---------|---------|---------|
185+
| MAJOR | Breaking changes | 1.0.0 |
186+
| MINOR | New features, new skills | 0.9.0 |
187+
| PATCH | Bug fixes, behavior changes | 0.8.10 |
188+
| MICRO | Doc-only, config-only, no behavior change | 0.8.9.0 |
189+
190+
Source of truth: `VERSION` file. Tags created automatically on merge to main.
191+
179192
## License
180193

181194
[MIT](LICENSE)

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.8.9.0
1+
0.8.10

bin/roadmap-audit

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,16 +1013,22 @@ check_structural_fitness() {
10131013
local section="none"
10141014
local in_preflight=0
10151015

1016-
# Associative arrays for counting
1017-
declare -A group_task_counts
1018-
declare -A track_task_counts
1016+
# Bash 3-compatible key=value storage (no associative arrays)
1017+
# Format: "key1=val1 key2=val2 ..."
1018+
local group_task_counts=""
1019+
local track_task_counts=""
1020+
1021+
# Helpers: get/set/incr values in "key=val key=val" strings
1022+
_kv_get() { echo "$1" | tr ' ' '\n' | grep "^$2=" | tail -1 | cut -d= -f2; }
1023+
_kv_set() { echo "$1" | tr ' ' '\n' | grep -v "^$2=" | tr '\n' ' '; printf "%s=%s" "$2" "$3"; }
1024+
_kv_keys() { echo "$1" | tr ' ' '\n' | grep '=' | cut -d= -f1 | sort; }
10191025

10201026
while IFS= read -r line; do
10211027
# Group heading
10221028
if echo "$line" | grep -qE '^## Group [0-9]+:'; then
10231029
current_group=$(echo "$line" | sed -E 's/^## Group ([0-9]+):.*/\1/')
10241030
group_count=$((group_count + 1))
1025-
group_task_counts[$current_group]=0
1031+
group_task_counts=$(_kv_set "$group_task_counts" "$current_group" 0)
10261032
section="group"
10271033
in_preflight=0
10281034
current_track=""
@@ -1033,7 +1039,7 @@ check_structural_fitness() {
10331039
if echo "$line" | grep -qE '^### Track [0-9]+[A-Z]:'; then
10341040
current_track=$(echo "$line" | sed -E 's/^### Track ([0-9]+[A-Z]):.*/\1/')
10351041
track_count=$((track_count + 1))
1036-
track_task_counts[$current_track]=0
1042+
track_task_counts=$(_kv_set "$track_task_counts" "$current_track" 0)
10371043
section="track"
10381044
in_preflight=0
10391045
continue
@@ -1055,10 +1061,12 @@ check_structural_fitness() {
10551061
if [ "$section" != "none" ] && echo "$line" | grep -qE '^- \*\*'; then
10561062
task_count=$((task_count + 1))
10571063
if [ -n "$current_group" ]; then
1058-
group_task_counts[$current_group]=$(( ${group_task_counts[$current_group]:-0} + 1 ))
1064+
local cur=$(_kv_get "$group_task_counts" "$current_group")
1065+
group_task_counts=$(_kv_set "$group_task_counts" "$current_group" $(( ${cur:-0} + 1 )))
10591066
fi
10601067
if [ "$in_preflight" -eq 0 ] && [ -n "$current_track" ]; then
1061-
track_task_counts[$current_track]=$(( ${track_task_counts[$current_track]:-0} + 1 ))
1068+
local cur=$(_kv_get "$track_task_counts" "$current_track")
1069+
track_task_counts=$(_kv_set "$track_task_counts" "$current_track" $(( ${cur:-0} + 1 )))
10621070
fi
10631071
continue
10641072
fi
@@ -1067,7 +1075,8 @@ check_structural_fitness() {
10671075
if [ "$in_preflight" -eq 1 ] && echo "$line" | grep -qE '^- [^*]'; then
10681076
task_count=$((task_count + 1))
10691077
if [ -n "$current_group" ]; then
1070-
group_task_counts[$current_group]=$(( ${group_task_counts[$current_group]:-0} + 1 ))
1078+
local cur=$(_kv_get "$group_task_counts" "$current_group")
1079+
group_task_counts=$(_kv_set "$group_task_counts" "$current_group" $(( ${cur:-0} + 1 )))
10711080
fi
10721081
continue
10731082
fi
@@ -1082,18 +1091,20 @@ check_structural_fitness() {
10821091
# Group sizes
10831092
if [ "$group_count" -gt 0 ]; then
10841093
local sizes=""
1085-
for g in $(echo "${!group_task_counts[@]}" | tr ' ' '\n' | sort -n); do
1094+
for g in $(_kv_keys "$group_task_counts"); do
1095+
local val=$(_kv_get "$group_task_counts" "$g")
10861096
[ -n "$sizes" ] && sizes="${sizes},"
1087-
sizes="${sizes}${g}=${group_task_counts[$g]}"
1097+
sizes="${sizes}${g}=${val}"
10881098
done
10891099
echo "GROUP_SIZES: $sizes"
10901100

10911101
# Track sizes
10921102
if [ "$track_count" -gt 0 ]; then
10931103
local tsizes=""
1094-
for t in $(echo "${!track_task_counts[@]}" | tr ' ' '\n' | sort); do
1104+
for t in $(_kv_keys "$track_task_counts"); do
1105+
local val=$(_kv_get "$track_task_counts" "$t")
10951106
[ -n "$tsizes" ] && tsizes="${tsizes},"
1096-
tsizes="${tsizes}${t}=${track_task_counts[$t]}"
1107+
tsizes="${tsizes}${t}=${val}"
10971108
done
10981109
echo "TRACK_SIZES: $tsizes"
10991110
fi
@@ -1102,13 +1113,12 @@ check_structural_fitness() {
11021113
if [ "$group_count" -ge 2 ]; then
11031114
local max_size=0
11041115
local min_size=999999
1105-
for g in "${!group_task_counts[@]}"; do
1106-
local s=${group_task_counts[$g]}
1116+
for g in $(_kv_keys "$group_task_counts"); do
1117+
local s=$(_kv_get "$group_task_counts" "$g")
11071118
[ "$s" -gt "$max_size" ] && max_size=$s
11081119
[ "$s" -lt "$min_size" ] && min_size=$s
11091120
done
11101121
if [ "$min_size" -gt 0 ]; then
1111-
# Bash integer division with 2 decimal places
11121122
local ratio_int=$((max_size * 100 / min_size))
11131123
local ratio_whole=$((ratio_int / 100))
11141124
local ratio_frac=$((ratio_int % 100))
@@ -1134,10 +1144,15 @@ check_doc_inventory() {
11341144
local file_count=0
11351145
local files_output=""
11361146

1137-
# Known doc types for labeling
1138-
local -A known_types=()
1139-
for doc in "${ROOT_DOCS[@]}"; do known_types["$doc"]="root doc"; done
1140-
for doc in "${DOCS_DIR_DOCS[@]}"; do known_types["$doc"]="project doc"; done
1147+
# Known doc types for labeling (bash 3 compatible)
1148+
_is_root_doc() {
1149+
for doc in "${ROOT_DOCS[@]}"; do [ "$doc" = "$1" ] && return 0; done
1150+
return 1
1151+
}
1152+
_is_project_doc() {
1153+
for doc in "${DOCS_DIR_DOCS[@]}"; do [ "$doc" = "$1" ] && return 0; done
1154+
return 1
1155+
}
11411156

11421157
# Scan all .md files (including known ones, for inventory)
11431158
while IFS= read -r filepath; do
@@ -1150,8 +1165,10 @@ check_doc_inventory() {
11501165
todo_count=$(count_todo_patterns "$filepath")
11511166

11521167
local doc_type="unknown"
1153-
if [ -n "${known_types[$basename]+x}" ]; then
1154-
doc_type="${known_types[$basename]}"
1168+
if _is_root_doc "$basename"; then
1169+
doc_type="root doc"
1170+
elif _is_project_doc "$basename"; then
1171+
doc_type="project doc"
11551172
elif echo "$filepath" | grep -q '/docs/designs/' 2>/dev/null; then
11561173
doc_type="design doc"
11571174
elif echo "$filepath" | grep -q '/docs/archive/' 2>/dev/null; then

bin/update-check

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ MARKER_FILE="$STATE_DIR/just-upgraded-from"
1919
SNOOZE_FILE="$STATE_DIR/update-snoozed"
2020
VERSION_FILE="$EXTEND_DIR/VERSION"
2121
REMOTE_URL="${GSTACK_EXTEND_REMOTE_URL:-https://raw.githubusercontent.com/kbitz/gstack-extend/main/VERSION}"
22+
# NOTE: Pre-0.8.8.1 installs still check a gist at the old URL
23+
# (gist ID 511b8a4861f7b4df3a7fedd42911c701). That gist is kept as a
24+
# permanent bridge so those installs can discover the upgrade to this
25+
# raw GitHub URL. Do not delete the gist.
2226

2327
# ─── Force flag (busts cache + snooze) ──────────────────────
2428
if [ "${1:-}" = "--force" ]; then
@@ -150,7 +154,7 @@ REMOTE="$(curl -sf --max-time 5 "$REMOTE_URL" 2>/dev/null || true)"
150154
REMOTE="$(echo "$REMOTE" | tr -d '[:space:]')"
151155

152156
# Validate: must look like a version number (reject HTML error pages)
153-
if ! echo "$REMOTE" | grep -qE '^[0-9]+\.[0-9.]+$'; then
157+
if ! echo "$REMOTE" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?$'; then
154158
# Invalid or empty response — assume up to date
155159
echo "UP_TO_DATE $LOCAL" > "$CACHE_FILE"
156160
exit 0

docs/PROGRESS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Build the /browse-native skill and validate it against real macOS apps.
66

77
| Version | Date | Summary |
88
|---------|------|---------|
9+
| 0.8.10 | 2026-04-15 | Release pipeline fix: tightened update-check version regex, auto-tag GitHub Action, 4-digit version tests, README versioning section (MICRO semantics), gist bridge comment. Fixed bash 3 crash in roadmap-audit (associative arrays), fixed stale setup test expectations. 128 tests, 0 failures. |
910
| 0.8.9.0 | 2026-04-14 | Roadmap audit accepts 4-digit versions (X.Y.Z.W). MICRO segment for doc-only/config bumps. Removed stale design docs, trimmed CLAUDE.md, refreshed roadmap (Track 2A done, added Track 1B: Roadmap Onboarding). |
1011
| 0.8.8.1 | 2026-04-14 | Public-ready cleanup: MIT license, gist-to-raw-GitHub version check migration, removed archived design docs with internal references, anonymized app name across docs and skill, SSH→HTTPS clone URL. |
1112
| 0.8.8 | 2026-04-13 | /roadmap triage mode now runs the freshness scan (Step 3.5) before classifying items into groups. Previously, triage slotted new items into potentially-complete groups because the scan was gated to update mode only. Stale/completed tasks are now always cleaned before new items get placed. Early exit no longer skips the freshness check. |

0 commit comments

Comments
 (0)