From adf8f1340eee3438e5eab695f883b0d4d11dd239 Mon Sep 17 00:00:00 2001 From: "Carlos D. Escobar-Valbuena" Date: Mon, 18 May 2026 09:15:44 -0500 Subject: [PATCH] feat(release): formalize OSS release infrastructure + bstack CLI dispatcher (0.2.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Foundation PR for the 0.2.x → 0.3.0 series. Adds CONTRIBUTING + RELEASE docs, CI workflows, a top-level bstack CLI dispatcher, and switches bstack-update-check to use the GitHub Releases API so dev-branch VERSION bumps no longer leak as "available upgrades" to downstream installs. Files: - NEW bin/bstack — top-level CLI dispatcher (doctor, validate, repair, bootstrap, onboard, revamp, upgrade, config, update-check, wave, release tag, version). Existing sub-binaries remain callable directly. - NEW CONTRIBUTING.md — contribution guide: branch/PR shape, Conventional Commits, primitive-promotion rule, local validation steps. - NEW RELEASE.md — semver policy (pre-1.0: minor = potentially breaking), release checklist using `bstack release tag`, retroactive-tag history, update-check transport docs. - NEW .github/workflows/ci.yml — shellcheck on scripts/*.sh and bin/*, JSON validation for assets/templates/*.snippet, bstack doctor --quiet on templated fixtures. - NEW .github/workflows/validate-release.yml — PR check: if VERSION changed, CHANGELOG.md must have a matching `## X.Y.Z` section and VERSION must monotonically increase. - EDIT bin/bstack-update-check — primary source is GitHub Releases API (/repos/broomva/bstack/releases/latest); raw VERSION on main is the fallback. New env vars: BSTACK_RELEASES_URL, BSTACK_REMOTE_URL. - EDIT VERSION → 0.2.2 - EDIT CHANGELOG.md — 0.2.2 entry Retroactive tags v0.2.0 (322ba23) and v0.2.1 (9170dd3) were pushed 2026-05-18 to give the new transport a stable anchor. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 67 +++++++++ .github/workflows/validate-release.yml | 64 ++++++++ CHANGELOG.md | 18 +++ CONTRIBUTING.md | 66 +++++++++ RELEASE.md | 99 +++++++++++++ VERSION | 2 +- bin/bstack | 197 +++++++++++++++++++++++++ bin/bstack-update-check | 34 ++++- 8 files changed, 541 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/validate-release.yml create mode 100644 CONTRIBUTING.md create mode 100644 RELEASE.md create mode 100755 bin/bstack diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..375c5b0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,67 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + lint: + name: Lint shell + JSON templates + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: ShellCheck (scripts + bin) + run: | + # Only lint files that have a shebang we recognize. + set -euo pipefail + mapfile -t targets < <( + find scripts bin -type f \( -name "*.sh" -o ! -name "*.*" \) 2>/dev/null \ + | xargs -I {} sh -c 'head -n1 "{}" | grep -q "^#!.*sh" && echo "{}"' \ + | sort -u + ) + if [ "${#targets[@]}" -eq 0 ]; then + echo "No shell scripts found to lint." + exit 0 + fi + printf ' • %s\n' "${targets[@]}" + # SC1091: don't follow sourced files; SC2155: declare-and-assign exit-code + # warnings — we accept these in bstack's defensive shell style. + shellcheck --severity=warning --exclude=SC1091,SC2155 "${targets[@]}" + + - name: Validate JSON templates + run: | + set -euo pipefail + shopt -s nullglob + fail=0 + for f in assets/templates/*.snippet; do + if ! jq -e . "$f" >/dev/null 2>&1; then + echo "::error file=$f::invalid JSON shape" + fail=1 + else + echo " • $f — valid JSON" + fi + done + exit "$fail" + + doctor: + name: bstack doctor (primitive-contract lint) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Run doctor against templates + run: | + set -euo pipefail + # doctor.sh expects CLAUDE.md / AGENTS.md / .control/policy.yaml in cwd. + # Stage the templated versions as a fixture so doctor lints what + # downstream installs would see after `bstack bootstrap`. + mkdir -p .control + cp assets/templates/CLAUDE.md.template ./CLAUDE.md 2>/dev/null || true + cp assets/templates/AGENTS.md.template ./AGENTS.md 2>/dev/null || true + cp assets/templates/policy.yaml.template .control/policy.yaml 2>/dev/null || true + bash scripts/doctor.sh --quiet diff --git a/.github/workflows/validate-release.yml b/.github/workflows/validate-release.yml new file mode 100644 index 0000000..f708e37 --- /dev/null +++ b/.github/workflows/validate-release.yml @@ -0,0 +1,64 @@ +name: Validate release + +on: + pull_request: + paths: + - VERSION + - CHANGELOG.md + +permissions: + contents: read + +jobs: + version-changelog-alignment: + name: VERSION ↔ CHANGELOG match + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Detect VERSION change + id: version + run: | + set -euo pipefail + base="${{ github.event.pull_request.base.sha }}" + if ! git diff --name-only "$base"...HEAD | grep -qx VERSION; then + echo "VERSION not modified in this PR — nothing to validate." + echo "changed=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + new="$(cat VERSION | tr -d '[:space:]')" + if ! printf '%s' "$new" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "::error file=VERSION::VERSION '$new' is not semver X.Y.Z" + exit 1 + fi + echo "changed=true" >> "$GITHUB_OUTPUT" + echo "new=$new" >> "$GITHUB_OUTPUT" + + - name: CHANGELOG has matching section + if: steps.version.outputs.changed == 'true' + run: | + set -euo pipefail + new="${{ steps.version.outputs.new }}" + if ! grep -qE "^## ${new}([[:space:]]|$)" CHANGELOG.md; then + echo "::error file=CHANGELOG.md::No '## ${new}' section found — every VERSION bump needs a matching CHANGELOG entry." + exit 1 + fi + echo " ✓ CHANGELOG.md has '## ${new}' section" + + - name: VERSION moves forward + if: steps.version.outputs.changed == 'true' + run: | + set -euo pipefail + base="${{ github.event.pull_request.base.sha }}" + new="${{ steps.version.outputs.new }}" + old="$(git show "$base:VERSION" 2>/dev/null | tr -d '[:space:]' || echo '0.0.0')" + # Lexicographic comparison works for left-padded semver but not generally. + # Use sort -V for correctness. + highest="$(printf '%s\n%s\n' "$old" "$new" | sort -V | tail -1)" + if [ "$highest" != "$new" ] || [ "$old" = "$new" ]; then + echo "::error file=VERSION::VERSION must increase. Previous: ${old}, this PR: ${new}." + exit 1 + fi + echo " ✓ VERSION ${old} → ${new}" diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ac6500..e09de17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 0.2.2 — 2026-05-18 + +### Release infrastructure + +First formal release with proper OSS tooling. Establishes the foundation that 0.2.3 (`bstack repair` merges hooks) and 0.3.0 (SessionStart auto-upgrade) build on. + +- **NEW** `bin/bstack` — top-level CLI dispatcher. Subcommands: `doctor`, `validate`, `repair`, `bootstrap`, `onboard`, `revamp`, `upgrade`, `config`, `update-check`, `wave`, `release tag`, `version`. Existing sub-binaries (`bstack-config`, `bstack-update-check`, `bstack-wave`) remain callable directly — the dispatcher is additive. `bstack release tag` is a maintainer helper that validates the tree, tags `vX.Y.Z`, pushes the tag, and creates the GitHub Release with the matching CHANGELOG section as notes. +- **NEW** `CONTRIBUTING.md` — contribution guide: branch/PR shape, Conventional Commits, primitive-promotion rule, local validation steps. +- **NEW** `RELEASE.md` — semver policy (pre-1.0: minor = potentially breaking), release checklist, retroactive-tag history, cadence guidance, update-check transport docs. +- **NEW** `.github/workflows/ci.yml` — shellcheck on `scripts/*.sh` and `bin/*`, JSON validation for `assets/templates/*.snippet`, `bstack doctor --quiet` on templated fixtures. +- **NEW** `.github/workflows/validate-release.yml` — PR check: if `VERSION` changed, `CHANGELOG.md` must have a matching `## X.Y.Z` section and the version must monotonically increase. +- **CHANGED** `bin/bstack-update-check` — primary source is now the GitHub Releases API (`/repos/broomva/bstack/releases/latest`), with raw `VERSION` on `main` as fallback. **This means dev-branch VERSION bumps no longer leak to downstream installs as "available upgrades"** — only tagged releases do. Two new env vars: `BSTACK_RELEASES_URL` (primary), `BSTACK_REMOTE_URL` (fallback, unchanged behavior). +- **HISTORY** `v0.2.0` and `v0.2.1` tags + GitHub Releases created retroactively on 2026-05-18 to give the update-check transport a stable anchor. + +### Migration + +None required. Existing installs continue to work — the API-first transport falls back to the raw `VERSION` URL on any failure, so behavior degrades gracefully. + ## 0.2.1 — 2026-05-16 ### Drop legacy fallback shims from the 0.2.0 renumber diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..decb591 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,66 @@ +# Contributing to bstack + +Thanks for opening a PR. bstack is the substrate that turns an agent-driven workspace into a self-operating system — every change to it propagates to every install. The contribution rules below exist to keep that propagation reliable. + +## Branch + PR shape + +- **Branch names**: `feat/`, `fix/`, `chore/`, `docs/`. +- **PR title**: Conventional Commits format (`feat:`, `fix:`, `chore:`, `docs:`, `refactor:`, `test:`). Examples in `git log --oneline`. +- **One concern per PR**. Mixing release infrastructure with a new feature makes both harder to revert. +- **Squash on merge**. Linear history. + +## Commit messages + +Conventional Commits, body explains the *why*. Existing commits are the reference: + +``` +feat(primitives): renumber so Wait=P9 — restore skill-name↔primitive-number alignment +fix(SKILL.md): compress description to ≤1024 chars per Agent Skill spec +chore(primitives): drop legacy fallback shims from the 0.2.0 renumber +``` + +## Local validation (before pushing) + +```bash +make bstack-check # validates skills + hooks + bridge + policy (if you have the workspace harness) +bash scripts/doctor.sh --quiet # primitive-contract compliance lint +shellcheck scripts/*.sh bin/* # shell hygiene +jq -e . assets/templates/*.snippet >/dev/null # template JSON shape +``` + +CI runs the same checks via `.github/workflows/ci.yml`. + +## Adding a new primitive (P21+) + +bstack's L3 stability budget says governance changes are rare and deliberate. Before adding a new `Pn`: + +1. Confirm rule-of-three: ≥ 3 independent instances of the failure mode the new primitive closes, each documented in `research/notes/` or an entity page. +2. The pattern must have: concrete mechanism, stated invariant, stated failure mode it prevents. +3. Add the row to `SKILL.md` §Primitives table. +4. Add the section to `assets/templates/AGENTS.md.template` §Primitives. +5. Update `references/primitives.md` Short-name index (must equal the new total count). +6. Update `scripts/doctor.sh` to lint the new row. +7. Bump VERSION minor and add a CHANGELOG entry — primitive additions are minor releases pre-1.0. + +## Adding a skill to the roster + +`SKILL.md` preamble has a `ROSTER=(...)` array of expected skill names. Add yours there. The skill itself lives in its own `broomva/` repo; bstack tracks installation status, not source. + +## Release + +See `RELEASE.md`. Short version: + +1. Bump `VERSION`. +2. Prepend a section to `CHANGELOG.md` matching the new version. +3. `validate-release.yml` confirms the two are aligned on the PR. +4. After merge, tag and create the GitHub Release (`gh release create vX.Y.Z`). + +## Style + +- **Shell**: `set -euo pipefail`, quote variables, `shellcheck`-clean. +- **Python**: PEP 8, type hints where useful, no global state. +- **Markdown**: agent-readable surfaces (SKILL.md, AGENTS.md, primitives.md) stay terse and structural. Human-readable docs (RELEASE.md, CONTRIBUTING.md) can be longer. + +## Questions + +Open a discussion in the repo or ping in the workspace channel where bstack is being used. PRs without context get bounced — paste the failure mode, the proposed fix, and the validation you ran. diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..96bfbd7 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,99 @@ +# Release process + +bstack ships as a skill installed via `npx skills add broomva/bstack` (vendored) or `git clone` (git install). Every release must be reachable to both install types and properly tagged so `bin/bstack-update-check` and downstream tooling can discover it. + +## Versioning policy (Semantic Versioning) + +bstack follows [SemVer 2.0](https://semver.org/) with the **pre-1.0 convention** that minor versions may carry breaking behavior changes: + +| Pre-1.0 (`0.x.y`) | Meaning | +|---|---| +| `0.x.0` (minor) | New primitives, new hooks wired by default, behavior-changing default flips. **May break existing installs** — document the migration in CHANGELOG. | +| `0.x.y` (patch) | Bug fixes, doc updates, additive non-default features, doctor lint additions. Safe to auto-upgrade. | + +Once 1.0.0 ships, the standard SemVer rules apply (major = breaking, minor = additive backwards-compatible, patch = fixes only). + +### Examples + +| Change | Bump | +|---|---| +| New primitive `P21` added to table + doctor lint | **Minor** — governance change | +| New optional hook in `settings.json.snippet` | **Minor** — installs that re-run `bstack repair` pick it up | +| `bstack-update-check` switches transport (raw VERSION → GitHub releases API) | **Patch** — internal mechanism, observable behavior unchanged | +| Default flip (`auto_upgrade` defaults to true) | **Minor** — silently changes behavior for existing users | +| Typo fix in CLAUDE.md.template | **Patch** | +| Remove legacy fallback shim that 0.x.y added | **Minor** — breaks pinned-to-shim installs even if "internal" | + +## Release checklist + +Use this checklist for every release. The CI workflow `validate-release.yml` enforces the VERSION ↔ CHANGELOG alignment automatically; the rest is human discipline. + +1. **PR opens with**: + - `VERSION` bumped to the new `X.Y.Z` + - `CHANGELOG.md` prepended with `## X.Y.Z — YYYY-MM-DD` section + - Any breaking changes documented under a `### Migration` subheading +2. **Validate locally** — `bash scripts/doctor.sh --quiet`, `shellcheck scripts/*.sh bin/*`, `jq -e . assets/templates/*.snippet`. +3. **CI passes** — `ci.yml` (lint) + `validate-release.yml` (version/changelog match). +4. **Reviewer approves** — at least one human or `pr-review-toolkit:code-reviewer` agent verdict. +5. **Merge to main** (squash). +6. **Tag + GitHub Release** — `bstack release tag` (≥ 0.2.2) wraps the manual sequence: + ```bash + git fetch origin && git checkout main && git pull --ff-only + bstack release tag # validates clean tree, on main, in sync; tags + pushes + creates Release + ``` + The dispatcher reads `VERSION`, picks the matching `## X.Y.Z` section out of `CHANGELOG.md` as the release notes, and uses the first `### ` heading inside that section as the release title. If `gh` is not installed the tag is still pushed and the command prints the manual `gh release create` invocation. + + Manual fallback (pre-0.2.2 installs, or if the dispatcher is unavailable): + ```bash + VERSION=$(cat VERSION) + git tag -a "v${VERSION}" -m "v${VERSION} — " + git push origin "v${VERSION}" + gh release create "v${VERSION}" --title "v${VERSION} — <title>" --notes-file <(awk "/^## ${VERSION}/{flag=1; next} /^## /{flag=0} flag" CHANGELOG.md) + ``` +7. **Downstream verification**: + - `bin/bstack-update-check --force` from any install should now emit `UPGRADE_AVAILABLE <old> <new>` within the cache TTL window. + - For git installs with `auto_upgrade=true`, the SessionStart hook (≥ 0.3.0) auto-pulls on next session. + +## Cadence + +bstack has no fixed release cadence. The triggers for a release are: + +- A new primitive earns its rule-of-three and gets promoted → **minor**. +- A behavior-changing default flip (anything that affects installs without their action) → **minor**. +- A bundle of fixes/docs is ready to ship → **patch**. +- A critical bug or security issue → **patch**, immediately. + +Avoid letting `main` accumulate more than 2-3 unreleased PRs — each unreleased PR is invisible to downstream installs. + +## Backporting + +bstack does not maintain release branches. If a fix on `main` is needed urgently on a pinned install, the downstream user pins to a tag and applies the fix locally. There is no `0.2.x` branch to backport to. + +## Retroactive tagging (history) + +The repo's first tagged release was **v0.2.0** (2026-05-16, commit `322ba23`). Earlier versions (`0.1.0`, etc.) referenced in `CHANGELOG.md` predate the release-infrastructure formalization and are not tagged. + +Tags `v0.2.0` and `v0.2.1` were created retroactively on 2026-05-18 as part of the v0.2.2 release-infrastructure work to give `bin/bstack-update-check` a stable anchor to compare against. + +## Update check transport + +`bin/bstack-update-check` (≥ 0.2.2) compares the local `VERSION` against: + +1. **Primary**: GitHub Releases API — `GET /repos/broomva/bstack/releases/latest`, read `.tag_name`, strip leading `v`. +2. **Fallback**: raw `VERSION` file on `main` (`https://raw.githubusercontent.com/broomva/bstack/main/VERSION`) — used when the API is unreachable or rate-limited. + +This separation means **development-branch VERSION bumps do not leak as available upgrades to downstream installs** — only tagged releases do. Bump `VERSION` freely on a feature branch; downstream sees nothing until the tag lands. + +## Disabling update checks + +Downstream users can disable update checks entirely: + +```bash +bstack-config set update_check false +``` + +Or snooze a specific version via the `/bstack-upgrade` interactive flow. + +## Questions + +See `CONTRIBUTING.md` for the contribution + PR shape. Cadence-or-policy questions belong in repo discussions; mechanical bugs in the release workflow are issues. diff --git a/VERSION b/VERSION index 0c62199..ee1372d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.1 +0.2.2 diff --git a/bin/bstack b/bin/bstack new file mode 100755 index 0000000..6bbac5b --- /dev/null +++ b/bin/bstack @@ -0,0 +1,197 @@ +#!/usr/bin/env bash +# bstack — top-level CLI dispatcher for the Broomva Stack. +# +# Subcommands delegate to existing sub-binaries (bstack-config, bstack- +# update-check, bstack-wave) and to scripts/*.sh. Standalone sub-binaries +# remain callable directly — the dispatcher is additive, never replacing. +# +# Usage: +# bstack <subcommand> [args...] +# bstack --help | -h | help +# bstack --version | -V | version +set -euo pipefail + +BSTACK_DIR="$(cd "$(dirname "$0")/.." && pwd)" +SCRIPTS_DIR="$BSTACK_DIR/scripts" +BIN_DIR="$BSTACK_DIR/bin" + +usage() { + cat <<'EOF' +bstack — Broomva Stack CLI + +Validation: + doctor [--quiet] Lint primitive-contract compliance. + validate Validate skill frontmatter health. + repair [--apply-all|--dry-run] Apply fixes for gaps surfaced by doctor. + +Lifecycle: + bootstrap Wire bstack into the current workspace. + onboard First-time setup wizard (4 questions). + revamp Full reconfiguration (idempotent). + upgrade Pull latest release into this install. + +Config + state: + config get|set|list Read/write ~/.bstack/config.yaml. + update-check [--force] Check for a newer tagged release. + version Print this install's VERSION. + +Orchestration: + wave <subcommand> P19 parallel sub-phase dispatch. + +Release (maintainers): + release tag Tag the current VERSION and create the + matching GitHub Release from CHANGELOG. + +Misc: + --help | -h | help This message. + --version | -V Print VERSION and exit. + +Examples: + bstack doctor --quiet + bstack config set auto_upgrade true + bstack wave dispatch plans/sub-a.md plans/sub-b.md + bstack release tag +EOF +} + +bstack_version() { + if [ -f "$BSTACK_DIR/VERSION" ]; then + cat "$BSTACK_DIR/VERSION" + else + echo "unknown" + return 1 + fi +} + +# `bstack upgrade` — non-interactive equivalent of the /bstack-upgrade flow. +# For git installs, runs the same git stash + fetch + reset --hard pattern +# the SKILL.md describes. For vendored installs, prints guidance. +bstack_upgrade() { + local upd + upd="$("$BIN_DIR/bstack-update-check" --force 2>/dev/null || true)" + case "$upd" in + JUST_UPGRADED*) + echo "Already on $(bstack_version) (just upgraded from $(echo "$upd" | awk '{print $2}'))." + return 0 + ;; + UPGRADE_AVAILABLE*) + local old new + old="$(echo "$upd" | awk '{print $2}')" + new="$(echo "$upd" | awk '{print $3}')" + if [ ! -d "$BSTACK_DIR/.git" ]; then + echo "Vendored install — v$new available. Re-run:" + echo " npx skills add -g broomva/bstack" + return 0 + fi + echo "Upgrading bstack v$old → v$new..." + ( + cd "$BSTACK_DIR" + git stash push -u -m "bstack upgrade $(date +%s)" 2>/dev/null || true + git fetch origin + git reset --hard origin/main + chmod +x bin/* scripts/* 2>/dev/null || true + ) + mkdir -p "$HOME/.bstack" + echo "$old" > "$HOME/.bstack/just-upgraded-from" + rm -f "$HOME/.bstack/last-update-check" "$HOME/.bstack/update-snoozed" + echo "Done. Running v$(bstack_version)." + ;; + *) + echo "Already on v$(bstack_version) (latest release)." + ;; + esac +} + +# `bstack release tag` — maintainer helper. Validates clean tree, reads +# VERSION, tags v$VERSION, pushes the tag, and creates a GitHub Release +# with the matching CHANGELOG section as body. +bstack_release_tag() { + cd "$BSTACK_DIR" + if [ -n "$(git status --porcelain)" ]; then + echo "release: working tree is dirty. Commit or stash first." >&2 + return 1 + fi + local current_branch + current_branch="$(git branch --show-current)" + if [ "$current_branch" != "main" ]; then + echo "release: must be on main (currently on '$current_branch')." >&2 + return 1 + fi + git fetch origin --tags --quiet + if [ "$(git rev-parse HEAD)" != "$(git rev-parse origin/main)" ]; then + echo "release: local main diverges from origin/main. Pull first." >&2 + return 1 + fi + local version tag + version="$(bstack_version)" + tag="v${version}" + if git rev-parse "$tag" >/dev/null 2>&1; then + echo "release: $tag already exists." >&2 + return 1 + fi + local notes_file + notes_file="$(mktemp)" + awk -v ver="$version" ' + $0 ~ "^## " ver "( |$)" { flag=1; next } + flag && /^## / { exit } + flag { print } + ' CHANGELOG.md > "$notes_file" + if [ ! -s "$notes_file" ]; then + echo "release: no '## $version' section found in CHANGELOG.md." >&2 + rm -f "$notes_file" + return 1 + fi + local title + title="$(awk '/^### / { sub(/^### /, ""); print; exit }' "$notes_file")" + [ -z "$title" ] && title="$tag" + echo "Tagging $tag..." + git tag -a "$tag" -m "$tag — $title" + git push origin "$tag" + if command -v gh >/dev/null 2>&1; then + echo "Creating GitHub Release..." + gh release create "$tag" --title "$tag — $title" --notes-file "$notes_file" + else + echo "gh not installed — tag pushed; create the GitHub Release manually:" + echo " gh release create $tag --notes-file $notes_file" + fi + rm -f "$notes_file" +} + +# ─── Dispatch ──────────────────────────────────────────────────────────── +if [ $# -eq 0 ]; then + usage + exit 0 +fi + +cmd="$1"; shift +case "$cmd" in + -h|--help|help) usage ;; + -V|--version|version) bstack_version ;; + doctor) exec bash "$SCRIPTS_DIR/doctor.sh" "$@" ;; + validate) exec bash "$SCRIPTS_DIR/validate.sh" "$@" ;; + repair) exec bash "$SCRIPTS_DIR/repair.sh" "$@" ;; + bootstrap) exec bash "$SCRIPTS_DIR/bootstrap.sh" "$@" ;; + onboard) exec bash "$SCRIPTS_DIR/onboard.sh" "$@" ;; + revamp) exec bash "$SCRIPTS_DIR/revamp.sh" "$@" ;; + upgrade) bstack_upgrade "$@" ;; + config) exec "$BIN_DIR/bstack-config" "$@" ;; + update-check) exec "$BIN_DIR/bstack-update-check" "$@" ;; + wave) exec "$BIN_DIR/bstack-wave" "$@" ;; + release) + sub="${1:-}" + [ $# -gt 0 ] && shift + case "$sub" in + tag) bstack_release_tag "$@" ;; + ""|-h|--help) + echo "Usage: bstack release tag" ;; + *) + echo "bstack release: unknown subcommand '$sub' (try: tag)" >&2 + exit 2 + ;; + esac + ;; + *) + echo "bstack: unknown subcommand '$cmd' — try 'bstack --help'." >&2 + exit 2 + ;; +esac diff --git a/bin/bstack-update-check b/bin/bstack-update-check index b1d82ef..1c72203 100755 --- a/bin/bstack-update-check +++ b/bin/bstack-update-check @@ -7,9 +7,17 @@ # (nothing) — up to date, snoozed, disabled, or check skipped # # Env overrides (for testing): -# BSTACK_DIR — override auto-detected bstack root -# BSTACK_REMOTE_URL — override remote VERSION URL -# BSTACK_STATE_DIR — override ~/.bstack state directory +# BSTACK_DIR — override auto-detected bstack root +# BSTACK_RELEASES_URL — override GitHub Releases API endpoint (primary) +# BSTACK_REMOTE_URL — override raw-VERSION fallback URL +# BSTACK_STATE_DIR — override ~/.bstack state directory +# +# Resolution order (≥ 0.2.2): +# 1. GitHub Releases API `/releases/latest` → strip leading `v` from +# tag_name. This is the canonical "what is the latest release" answer +# and ignores unreleased VERSION bumps on `main`. +# 2. Raw VERSION on `main` — fallback for offline, rate-limited, or +# pre-tagged installs. set -euo pipefail BSTACK_DIR="${BSTACK_DIR:-$(cd "$(dirname "$0")/.." && pwd)}" @@ -18,6 +26,7 @@ CACHE_FILE="$STATE_DIR/last-update-check" MARKER_FILE="$STATE_DIR/just-upgraded-from" SNOOZE_FILE="$STATE_DIR/update-snoozed" VERSION_FILE="$BSTACK_DIR/VERSION" +RELEASES_URL="${BSTACK_RELEASES_URL:-https://api.github.com/repos/broomva/bstack/releases/latest}" REMOTE_URL="${BSTACK_REMOTE_URL:-https://raw.githubusercontent.com/broomva/bstack/main/VERSION}" # ─── Force flag (busts cache for standalone /bstack-upgrade) ── @@ -134,9 +143,24 @@ fi # ─── Step 4: Slow path — fetch remote version ──────────────── mkdir -p "$STATE_DIR" +# Primary: GitHub Releases API. Captures only tagged releases — dev-branch +# VERSION bumps stay invisible until release. REMOTE="" -REMOTE="$(curl -sf --max-time 5 "$REMOTE_URL" 2>/dev/null || true)" -REMOTE="$(echo "$REMOTE" | tr -d '[:space:]')" +RELEASES_JSON="$(curl -sf --max-time 5 -H 'Accept: application/vnd.github+json' "$RELEASES_URL" 2>/dev/null || true)" +if [ -n "$RELEASES_JSON" ]; then + # Prefer jq when available; fall back to grep for portability. + if command -v jq >/dev/null 2>&1; then + REMOTE="$(printf '%s' "$RELEASES_JSON" | jq -r '.tag_name // empty' 2>/dev/null | sed 's/^v//')" + else + REMOTE="$(printf '%s' "$RELEASES_JSON" | grep -oE '"tag_name"[[:space:]]*:[[:space:]]*"[^"]+"' | head -1 | sed -E 's/.*"v?([0-9.]+)"/\1/')" + fi +fi + +# Fallback: raw VERSION on main (offline, rate-limited, or pre-tagged installs). +if ! echo "$REMOTE" | grep -qE '^[0-9]+\.[0-9.]+$'; then + REMOTE="$(curl -sf --max-time 5 "$REMOTE_URL" 2>/dev/null || true)" + REMOTE="$(echo "$REMOTE" | tr -d '[:space:]')" +fi if ! echo "$REMOTE" | grep -qE '^[0-9]+\.[0-9.]+$'; then echo "UP_TO_DATE $LOCAL" > "$CACHE_FILE"