feat(0.17.0): crystallize BROOMVA_ROOT convention (BRO-1223 follow-up… #16
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release on merge | |
| # Auto-tag + auto-publish GitHub Release whenever VERSION changes on main. | |
| # Composes with validate-release.yml (PR gate) so this workflow trusts that | |
| # the merged VERSION is semver, monotonic, and matched by a CHANGELOG section. | |
| # | |
| # Idempotent: if `vX.Y.Z` already exists (e.g. tagged manually before this | |
| # workflow shipped) the run skips silently — no overwrite, no duplicate. | |
| on: | |
| push: | |
| branches: [main] | |
| paths: | |
| - VERSION | |
| permissions: | |
| contents: write # tag push + gh release create | |
| concurrency: | |
| group: release-on-merge | |
| cancel-in-progress: false | |
| jobs: | |
| release: | |
| name: Tag + GitHub Release | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # full history so we can check existing tags | |
| - name: Read VERSION | |
| id: version | |
| run: | | |
| set -euo pipefail | |
| v="$(tr -d '[:space:]' < VERSION)" | |
| if ! printf '%s' "$v" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then | |
| echo "::error file=VERSION::not semver X.Y.Z: '$v'" | |
| exit 1 | |
| fi | |
| echo " version=$v" | |
| echo "version=$v" >> "$GITHUB_OUTPUT" | |
| echo "tag=v$v" >> "$GITHUB_OUTPUT" | |
| - name: Check existing tag | |
| id: tag_check | |
| run: | | |
| set -euo pipefail | |
| tag="${{ steps.version.outputs.tag }}" | |
| if git rev-parse "$tag" >/dev/null 2>&1; then | |
| echo " $tag already exists — skipping release." | |
| echo "exists=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo " $tag is new — will create." | |
| echo "exists=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Extract release notes from CHANGELOG | |
| if: steps.tag_check.outputs.exists == 'false' | |
| id: notes | |
| run: | | |
| set -euo pipefail | |
| v="${{ steps.version.outputs.version }}" | |
| awk -v ver="$v" ' | |
| $0 ~ "^## " ver "( |$)" { flag=1; next } | |
| flag && /^## / { exit } | |
| flag { print } | |
| ' CHANGELOG.md > /tmp/release-notes.md | |
| if [ ! -s /tmp/release-notes.md ]; then | |
| echo "::error file=CHANGELOG.md::no '## $v' section found" | |
| exit 1 | |
| fi | |
| # Title = first `### ` heading inside the section, else fall back to vX.Y.Z. | |
| title=$(awk '/^### / { sub(/^### /, ""); print; exit }' /tmp/release-notes.md) | |
| [ -z "$title" ] && title="${{ steps.version.outputs.tag }}" | |
| echo " title=$title" | |
| # Multi-line outputs need the heredoc form. | |
| { | |
| echo "title<<EOT" | |
| echo "$title" | |
| echo "EOT" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Tag + create GitHub Release | |
| if: steps.tag_check.outputs.exists == 'false' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| tag="${{ steps.version.outputs.tag }}" | |
| title="${{ steps.notes.outputs.title }}" | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git tag -a "$tag" -m "$tag — $title" | |
| git push origin "$tag" | |
| gh release create "$tag" \ | |
| --title "$tag — $title" \ | |
| --notes-file /tmp/release-notes.md | |
| echo " ✓ released $tag" | |
| # Package the skill directory as a tarball and publish it as a release | |
| # asset, so vendored installs (no `.git`) can self-upgrade via | |
| # `bstack upgrade --self` (≥ 0.9.0). The tarball excludes ephemeral | |
| # state — only the in-repo skill source ships. | |
| - name: Package + publish vendored upgrade tarball | |
| if: steps.tag_check.outputs.exists == 'false' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| tag="${{ steps.version.outputs.tag }}" | |
| tarball="bstack-${tag}.tar.gz" | |
| # Build a clean staging tree — the extracted root must contain | |
| # VERSION + bin/ + scripts/ + assets/ + references/ + schemas/ + | |
| # SKILL.md + CHANGELOG.md, mirroring a fresh `npx skills add` layout. | |
| staging="$(mktemp -d)" | |
| dest="$staging/bstack-${tag}" | |
| mkdir -p "$dest" | |
| # Copy the canonical skill payload; exclude .git, .github, tests | |
| # (downstream installs don't need CI workflow or development tests). | |
| tar --exclude='./.git' \ | |
| --exclude='./.github' \ | |
| --exclude='./tests' \ | |
| --exclude='./broomva.tech-worktrees' \ | |
| --exclude='./bstack-worktrees' \ | |
| --exclude='./.cache' \ | |
| -cf - . | tar -xf - -C "$dest" | |
| # Build the tarball in a deterministic order for reproducible | |
| # sha256 across runs (though we don't fail-close on this yet). | |
| ( cd "$staging" && tar --sort=name -czf "$tarball" "bstack-${tag}" ) | |
| mv "$staging/$tarball" "./$tarball" | |
| # sha256 sidecar — single canonical line, "<hash> <filename>" format. | |
| if command -v sha256sum >/dev/null 2>&1; then | |
| sha256sum "$tarball" > "${tarball}.sha256" | |
| else | |
| shasum -a 256 "$tarball" > "${tarball}.sha256" | |
| fi | |
| echo " packaged $tarball ($(wc -c < "$tarball") bytes)" | |
| echo " sha256: $(awk '{print $1}' "${tarball}.sha256")" | |
| # Upload as release assets. | |
| gh release upload "$tag" "$tarball" "${tarball}.sha256" --clobber | |
| echo " ✓ uploaded $tarball + sha256 to $tag" |