diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..19f2334 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,235 @@ +name: release + +# Fires on every annotated tag matching v* (e.g. v0.3.0, v1.0.0). +# Builds a source tarball, attaches it + install.sh to a GH Release, +# publishes the MCP stdio server to npm, and opens an auto-bump PR +# against the Homebrew tap. See docs/releases.md for the contract. +# +# Required repo secrets (Settings → Secrets and variables → Actions): +# NPM_TOKEN — automation token scoped to publish under +# @sonpiaz/ on npm. Used by publish-mcp job. +# TODO(user): generate at npmjs.com/settings/ +# sonpiaz/tokens (type: Automation) and paste. +# HOMEBREW_TAP_PAT — fine-grained PAT scoped to +# sonpiaz/homebrew-tap only. Permissions: +# contents:write + pull-requests:write + +# metadata:read. 1-year expiry recommended. +# TODO(user): generate at +# github.com/settings/personal-access-tokens +# and paste here. +# GITHUB_TOKEN — built-in, sufficient for steps 1–4. + +on: + push: + tags: + - 'v*' + +permissions: + contents: write # gh release create + id-token: write # npm provenance (future) + +jobs: + release: + name: Build tarball + create GH Release + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + sha256: ${{ steps.tarball.outputs.sha256 }} + steps: + - name: Checkout (full history for ancestor check) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Verify tag is on main + # Refuse to release a tag cut from a feature branch. The + # workflow only ships from main. See docs/releases.md + # "Tag policy". + run: | + git fetch origin main --quiet + if ! git merge-base --is-ancestor "${GITHUB_SHA}" origin/main; then + echo "error: tag ${GITHUB_REF_NAME} is not on main (got ${GITHUB_SHA}); refusing to release" >&2 + exit 1 + fi + echo "OK: tag ${GITHUB_REF_NAME} is an ancestor of origin/main" + + - name: Resolve version from tag + id: version + run: | + VERSION="${GITHUB_REF_NAME#v}" + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "VERSION=${VERSION}" >> "$GITHUB_ENV" + echo "Releasing v${VERSION}" + + - name: Verify lib/version.sh matches tag + # Single source of truth check. If the bump-version PR was + # merged but lib/version.sh did not change, abort here so a + # mismatched tag never produces a release. + run: | + # shellcheck source=/dev/null + source lib/version.sh + if [[ "${WATCH_CLI_VERSION}" != "${VERSION}" ]]; then + echo "error: lib/version.sh says ${WATCH_CLI_VERSION} but tag is v${VERSION}" >&2 + echo "fix: bump WATCH_CLI_VERSION in lib/version.sh to ${VERSION}, or cut a new tag" >&2 + exit 1 + fi + echo "OK: lib/version.sh and tag both say ${VERSION}" + + - name: Build source tarball + id: tarball + run: | + git archive --format=tar.gz \ + --prefix="watch-cli-${VERSION}/" \ + -o "watch-cli.tar.gz" HEAD + SHA256="$(shasum -a 256 watch-cli.tar.gz | awk '{print $1}')" + echo "$SHA256" > watch-cli.tar.gz.sha256 + echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT" + echo "tarball: $(ls -lh watch-cli.tar.gz)" + echo "sha256: ${SHA256}" + + - name: Generate release notes + # Auto-draft from squashed PR titles since the previous tag. + # docs/releases.md "Release notes template" specifies the + # final shape; this seeds the draft and the maintainer + # hand-edits via the GH Release UI after the run. + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PREV_TAG="$(git describe --tags --abbrev=0 "${GITHUB_REF_NAME}^" 2>/dev/null || echo "")" + { + echo "## Highlights" + echo "- _(edit me before publish)_" + echo + echo "## What's new" + if [[ -n "$PREV_TAG" ]]; then + git log --merges --pretty=format:"- %s" "${PREV_TAG}..${GITHUB_REF_NAME}" \ + | sed 's/Merge pull request #\([0-9]*\) from [^ ]* /(#\1) /' \ + || echo "- _(no merge commits found between ${PREV_TAG} and ${GITHUB_REF_NAME})_" + else + echo "- Initial tagged release." + fi + echo + echo "## Schema version" + echo "Output schema: v1. See [output-schema.md](https://github.com/${GITHUB_REPOSITORY}/blob/v${VERSION}/docs/output-schema.md)." + echo + echo "## Install" + echo '```bash' + echo "brew tap sonpiaz/tap && brew install watch-cli # macOS" + echo "curl -fsSL https://github.com/${GITHUB_REPOSITORY}/releases/download/v${VERSION}/install.sh | bash" + echo "WATCH_CLI_VERSION=${VERSION} curl -fsSL | bash # pin" + echo '```' + } > release-notes.md + echo "── release-notes.md ──" + cat release-notes.md + + - name: Create GH Release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create "v${VERSION}" \ + --title "v${VERSION}" \ + --notes-file release-notes.md \ + watch-cli.tar.gz \ + watch-cli.tar.gz.sha256 \ + install.sh + + publish-mcp: + name: Publish @sonpiaz/watch-cli-mcp to npm + needs: release + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '20' + registry-url: 'https://registry.npmjs.org' + + - name: Sync mcp-server version to tag + # npm version --no-git-tag-version is idempotent if the + # version already matches; never errors on no-op. + working-directory: mcp-server + run: | + npm version --no-git-tag-version --allow-same-version "${{ needs.release.outputs.version }}" + echo "mcp-server/package.json version → $(jq -r .version package.json)" + + - name: Build + working-directory: mcp-server + run: | + npm ci + npm run build + + - name: Publish + working-directory: mcp-server + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm publish --access public + + bump-tap: + name: Open Homebrew tap bump PR + needs: release + runs-on: ubuntu-latest + steps: + - name: Checkout homebrew-tap + uses: actions/checkout@v4 + with: + repository: sonpiaz/homebrew-tap + token: ${{ secrets.HOMEBREW_TAP_PAT }} + path: homebrew-tap + + - name: Bump Formula/watch-cli.rb + # `url` resolves via v#{version} interpolation, so only + # `version` and `sha256` change between releases. See + # docs/homebrew.md "Auto-bump on release". + working-directory: homebrew-tap + run: | + VERSION="${{ needs.release.outputs.version }}" + SHA256="${{ needs.release.outputs.sha256 }}" + sed -i.bak \ + -e "s/^ version \".*\"$/ version \"${VERSION}\"/" \ + -e "s/^ sha256 \".*\"$/ sha256 \"${SHA256}\"/" \ + Formula/watch-cli.rb + rm Formula/watch-cli.rb.bak + echo "── Formula/watch-cli.rb (head) ──" + head -15 Formula/watch-cli.rb + + - name: Commit + push branch + working-directory: homebrew-tap + env: + GH_TOKEN: ${{ secrets.HOMEBREW_TAP_PAT }} + run: | + VERSION="${{ needs.release.outputs.version }}" + BRANCH="bump/watch-cli-${VERSION}" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git checkout -b "${BRANCH}" + git add Formula/watch-cli.rb + git commit -m "watch-cli ${VERSION}" + git push -u origin "${BRANCH}" + + - name: Open PR + working-directory: homebrew-tap + env: + GH_TOKEN: ${{ secrets.HOMEBREW_TAP_PAT }} + VERSION: ${{ needs.release.outputs.version }} + SHA256: ${{ needs.release.outputs.sha256 }} + run: | + cat > /tmp/pr-body.md < The Homebrew path requires the v0.3.0 release to be cut first. +> Until then, use the curl one-liner above — it auto-falls-back to +> `git clone` of `main` when no published release exists yet. + +Pin a specific version: + +```bash +WATCH_CLI_VERSION=0.3.0 curl -fsSL \ + https://github.com/sonpiaz/watch-cli/releases/download/v0.3.0/install.sh | bash ``` Or from a clone: diff --git a/bin/audio-q b/bin/audio-q index ef7cf86..451666f 100755 --- a/bin/audio-q +++ b/bin/audio-q @@ -36,7 +36,7 @@ while [[ $# -gt 0 ]]; do exit 0 ;; -V|--version) - echo "watch-cli v0.2.0" + echo "$WATCH_CLI_VERSION_STRING" exit 0 ;; *) diff --git a/bin/dl-video b/bin/dl-video index ca33631..8a733f4 100755 --- a/bin/dl-video +++ b/bin/dl-video @@ -16,6 +16,18 @@ set -uo pipefail +# Resolve symlinks so $ROOT_DIR points at the real install dir, not ~/.local. +SELF="${BASH_SOURCE[0]}" +while [ -L "$SELF" ]; do + SELF_DIR="$(cd "$(dirname "$SELF")" && pwd)" + SELF="$(readlink "$SELF")" + [[ $SELF != /* ]] && SELF="$SELF_DIR/$SELF" +done +SELF_DIR="$(cd "$(dirname "$SELF")" && pwd)" +ROOT_DIR="$(cd "$SELF_DIR/.." && pwd)" +# shellcheck source=../lib/version.sh +source "$ROOT_DIR/lib/version.sh" + URL="" OUTDIR="/tmp/dl-video" COOKIES_FILE="" @@ -35,7 +47,7 @@ while [[ $# -gt 0 ]]; do exit 0 ;; -V|--version) - echo "watch-cli v0.2.0" + echo "$WATCH_CLI_VERSION_STRING" exit 0 ;; *) diff --git a/bin/extract-frames b/bin/extract-frames index fbf80c1..52c23ae 100755 --- a/bin/extract-frames +++ b/bin/extract-frames @@ -12,6 +12,18 @@ set -uo pipefail +# Resolve symlinks so $ROOT_DIR points at the real install dir, not ~/.local. +SELF="${BASH_SOURCE[0]}" +while [ -L "$SELF" ]; do + SELF_DIR="$(cd "$(dirname "$SELF")" && pwd)" + SELF="$(readlink "$SELF")" + [[ $SELF != /* ]] && SELF="$SELF_DIR/$SELF" +done +SELF_DIR="$(cd "$(dirname "$SELF")" && pwd)" +ROOT_DIR="$(cd "$SELF_DIR/.." && pwd)" +# shellcheck source=../lib/version.sh +source "$ROOT_DIR/lib/version.sh" + VIDEO="" COUNT="8" OUTDIR_ARG="" @@ -23,7 +35,7 @@ while [[ $# -gt 0 ]]; do exit 0 ;; -V|--version) - echo "watch-cli v0.2.0" + echo "$WATCH_CLI_VERSION_STRING" exit 0 ;; *) diff --git a/bin/models b/bin/models index 6b8b841..1e6780b 100755 --- a/bin/models +++ b/bin/models @@ -34,7 +34,7 @@ while [[ $# -gt 0 ]]; do exit 0 ;; -V|--version) - echo "watch-cli v0.2.0" + echo "$WATCH_CLI_VERSION_STRING" exit 0 ;; -*) diff --git a/bin/transcribe b/bin/transcribe index 4a44042..85d40ec 100755 --- a/bin/transcribe +++ b/bin/transcribe @@ -53,7 +53,7 @@ while [[ $# -gt 0 ]]; do exit 0 ;; -V|--version) - echo "watch-cli v0.2.0" + echo "$WATCH_CLI_VERSION_STRING" exit 0 ;; *) diff --git a/bin/watch b/bin/watch index 5c498ac..ac57cfb 100755 --- a/bin/watch +++ b/bin/watch @@ -38,7 +38,21 @@ # success (frames OK, transcribe failed → exit 4) still emits a block. set -uo pipefail -VERSION="watch-cli v0.2.0" +# Resolve symlinks so $ROOT_DIR points at the real install dir, not ~/.local. +# Done up-front because --version needs to source lib/version.sh before +# arg parsing decides what to print. +SELF="${BASH_SOURCE[0]}" +while [ -L "$SELF" ]; do + SELF_DIR="$(cd "$(dirname "$SELF")" && pwd)" + SELF="$(readlink "$SELF")" + [[ $SELF != /* ]] && SELF="$SELF_DIR/$SELF" +done +SELF_DIR="$(cd "$(dirname "$SELF")" && pwd)" +ROOT_DIR="$(cd "$SELF_DIR/.." && pwd)" +# shellcheck source=../lib/version.sh +source "$ROOT_DIR/lib/version.sh" +# shellcheck source=../lib/health.sh +source "$ROOT_DIR/lib/health.sh" URL="" COUNT="8" @@ -73,7 +87,7 @@ while [[ $# -gt 0 ]]; do exit 0 ;; -V|--version) - echo "$VERSION" + echo "$WATCH_CLI_VERSION_STRING" exit 0 ;; *) @@ -87,18 +101,6 @@ while [[ $# -gt 0 ]]; do esac done -SELF="${BASH_SOURCE[0]}" -# Resolve symlinks so $ROOT_DIR points at the real install dir, not ~/.local. -while [ -L "$SELF" ]; do - SELF_DIR="$(cd "$(dirname "$SELF")" && pwd)" - SELF="$(readlink "$SELF")" - [[ $SELF != /* ]] && SELF="$SELF_DIR/$SELF" -done -SELF_DIR="$(cd "$(dirname "$SELF")" && pwd)" -ROOT_DIR="$(cd "$SELF_DIR/.." && pwd)" -# shellcheck source=../lib/health.sh -source "$ROOT_DIR/lib/health.sh" - # Pipe mode implies JSON output. A consumer reading JSONL doesn't want # text-block markers interleaved between objects. if [[ $PIPE -eq 1 ]]; then diff --git a/docs/homebrew.md b/docs/homebrew.md new file mode 100644 index 0000000..6ec0af1 --- /dev/null +++ b/docs/homebrew.md @@ -0,0 +1,348 @@ +# Homebrew tap + +This document is the contract for the watch-cli Homebrew tap repo, +the formula content, and the auto-bump workflow that updates the +formula on every watch-cli release. + +--- + +## Purpose + +Mac developers — watch-cli's primary audience — reach for `brew +install` first. The tool's dependencies (`yt-dlp`, `ffmpeg`, `jq`) +are already on Homebrew. Asking the same user to curl an arbitrary +`install.sh` for the wrapper is friction. A personal tap +(`sonpiaz/homebrew-tap`) is roughly 10× less work than upstreaming +to homebrew-core: no review queue, no popularity threshold, no audit +cycle, full control of release cadence. Homebrew-core submission is +explicitly out of scope for Phase 4. + +--- + +## Tap repo + +Name: **`sonpiaz/homebrew-tap`**. New public repo, implementer +creates as part of Phase 4: + +```bash +gh repo create sonpiaz/homebrew-tap \ + --public \ + --description "Homebrew formulas for Son Piaz projects" +``` + +`brew tap sonpiaz/tap` resolves to `github.com/sonpiaz/homebrew-tap` +by Homebrew convention. The `homebrew-` prefix is mandatory in the +repo name and must not appear in the user-facing tap name. + +Layout: + +``` +sonpiaz/homebrew-tap/ +├── Formula/ +│ └── watch-cli.rb +└── README.md +``` + +`Formula/` is required by Homebrew. Future formulas sit alongside +`watch-cli.rb`. Top-level `README.md` is a 2–3 paragraph blurb +pointing at each project's homepage — not a place to duplicate +watch-cli docs. + +--- + +## Formula spec + +File: `Formula/watch-cli.rb`. Ruby class extending `Formula`. The +sketch at the end of this section is what the implementer turns into +the real formula. + +### Metadata + +| Field | Value | +|---|---| +| `desc` | `"Turn any social video into an architecture diagram or working component"` (71 chars). Homebrew enforces `desc` under 80 chars via `brew style`. Tightened variant of the locked pitch in [`BRANDING.md`](../BRANDING.md) — preserves the U2 "video → concrete artifact" mapping (the must-keep half per BRANDING) while fitting the audit. | +| `homepage` | `"https://github.com/sonpiaz/watch-cli"` | +| `url` | `"https://github.com/sonpiaz/watch-cli/releases/download/v#{version}/watch-cli.tar.gz"` — `v#{version}` interpolation lets the auto-bump edit only `version` and `sha256`. | +| `sha256` | `""` — 64-hex tarball SHA, set by the bump PR. | +| `version` | `"0.3.0"` — first tap-tracked version, updated by the auto-bump PR. | +| `license` | `"MIT"` | + +### Dependencies + +```ruby +depends_on "yt-dlp" +depends_on "ffmpeg" +depends_on "jq" +``` + +Same deps as the curl installer minus `curl` and `python3` (Homebrew +assumes those on macOS). `whisper-cpp` is **not** a default +dependency — offline is an explicit `--with-local` opt-in (see +[`offline-mode.md`](offline-mode.md)); forcing a 1.62 GB model on +every brew install would punish the 95% of users on Kyma or BYOK. +Offline users run: + +```bash +brew install whisper-cpp +~/.watch-cli/install.sh --with-local +``` + +The `--with-local` flag stays the responsibility of `install.sh`; +the formula is a thin install of the bin scripts. + +### Install block + +```ruby +def install + # Bin scripts shell-source files under lib/. Move both into the + # formula prefix, then rewrite the bin scripts so that ROOT_DIR + # resolves to the prefix, not "$SELF_DIR/..". + libexec.install Dir["bin/*"] + pkgshare.install "lib" + pkgshare.install "prompts" if File.directory?("prompts") + + bin_files = Dir["#{libexec}/*"] + bin_files.each do |script| + inreplace script, %r{ROOT_DIR="\$\(cd "\$SELF_DIR/\.\." && pwd\)"}, + "ROOT_DIR=\"#{pkgshare}\"" + end + + bin.install_symlink bin_files +end +``` + +Bin scripts today resolve `ROOT_DIR="$(cd "$SELF_DIR/.." && pwd)"` +then `source "$ROOT_DIR/lib/...sh"`. Under Homebrew, the symlinked +binary at `$PREFIX/bin/watch` would resolve `$SELF_DIR/..` to +`$PREFIX` and look for `$PREFIX/lib/...` — wrong tree. The +`inreplace` pins `ROOT_DIR` to `pkgshare` so `source` lines find +their libraries. Real bin scripts live in `libexec`; +`bin.install_symlink` exposes them on `$PATH` (the homebrew-core +pattern for shell tools). `prompts/` is copied to `pkgshare` so the +README's "look in `prompts/`" line resolves to +`$(brew --prefix watch-cli)/share/watch-cli/prompts/`. + +The implementer must verify the rewrite on a fresh `brew install` +(see *Test plan*) — bin scripts can change shape across releases and +the regex must keep matching. + +### Test block + +```ruby +test do + system "#{bin}/watch", "--help" +end +``` + +`brew test watch-cli` runs after every install. The minimal test +asserts the binary exists, is executable, and exits 0 on `--help`. +`--help` over `--version` because it exercises lib sourcing — a +broken `ROOT_DIR` rewrite would fail to source `lib/env.sh` and the +help text would not print. + +### Sketch — full formula + +The implementer writes the real file; this sketch shows the shape. + +```ruby +class WatchCli < Formula + desc "Turn any social video into an architecture diagram or working component" + homepage "https://github.com/sonpiaz/watch-cli" + url "https://github.com/sonpiaz/watch-cli/releases/download/v#{version}/watch-cli.tar.gz" + sha256 "" + version "0.3.0" + license "MIT" + + depends_on "yt-dlp" + depends_on "ffmpeg" + depends_on "jq" + + def install + libexec.install Dir["bin/*"] + pkgshare.install "lib" + pkgshare.install "prompts" if File.directory?("prompts") + + bin_files = Dir["#{libexec}/*"] + bin_files.each do |script| + inreplace script, %r{ROOT_DIR="\$\(cd "\$SELF_DIR/\.\." && pwd\)"}, + "ROOT_DIR=\"#{pkgshare}\"" + end + bin.install_symlink bin_files + end + + test do + system "#{bin}/watch", "--help" + end +end +``` + +--- + +## Auto-bump on release + +Phase 4 wires the watch-cli release workflow (see +[`releases.md`](releases.md)) to update `Formula/watch-cli.rb` on +every `v*` tag push. The bump is a separate job in +`.github/workflows/release.yml` after tarball + npm publish. + +After the GH Release is created and the tarball SHA256 is known, the +job checks out `sonpiaz/homebrew-tap` via `HOMEBREW_TAP_PAT`, edits +only `version` and `sha256` (the `url` resolves via `v#{version}` +interpolation), and opens a PR: + +```bash +VERSION="${GITHUB_REF_NAME#v}" +SHA256=$(cat watch-cli.tar.gz.sha256) +sed -i.bak \ + -e "s/^ version \".*\"$/ version \"${VERSION}\"/" \ + -e "s/^ sha256 \".*\"$/ sha256 \"${SHA256}\"/" \ + Formula/watch-cli.rb +rm Formula/watch-cli.rb.bak +git commit -am "watch-cli ${VERSION}" +git push origin "bump/watch-cli-${VERSION}" +gh pr create --repo sonpiaz/homebrew-tap \ + --title "watch-cli ${VERSION}" \ + --body "Automated bump from watch-cli v${VERSION} release." +``` + +The PR is reviewed manually for the first few releases. Auto-merge +is intentionally not the Phase 4 default — human review catches +accidental breakage before users `brew upgrade`. + +### Required secret: `HOMEBREW_TAP_PAT` + +Fine-grained PAT scoped narrowly: resource owner `sonpiaz`, +repository access only `sonpiaz/homebrew-tap`, repository permissions +`Contents` read+write + `Pull requests` read+write + `Metadata` +read-only (auto-included), no account permissions. Add to the +**watch-cli** repo's Actions secrets as `HOMEBREW_TAP_PAT`. Recommend +1-year expiry with a calendar reminder for rotation. + +The default `GITHUB_TOKEN` cannot replace this PAT — it is scoped to +the workflow's own repo and cannot push to a different repository. + +--- + +## First-formula bootstrap + +The first release (v0.3.0) cannot rely on auto-bump — that job runs +after the release is created, but the formula must exist first. +Bootstrap: + +1. Create the tap repo (`gh repo create` above). +2. Cut watch-cli v0.3.0 per [`releases.md`](releases.md). This + produces `releases/download/v0.3.0/watch-cli.tar.gz` and its + SHA256. +3. Hand-write `Formula/watch-cli.rb` in the tap, filling + `version "0.3.0"` and the `sha256` from the release asset. + Commit directly to `main` of the tap (no PR — first commit). +4. Verify on a fresh machine per the test plan below. +5. Future releases (v0.3.1+) use the auto-bump. Never hand-edit + the formula after v0.3.0. + +If the auto-bump misfires later (PAT expired, format drift, +permission change), fall back to the hand-edit sequence — log the +issue and fix the workflow before the next release. + +--- + +## User install path + +The README install section gains a Homebrew block above the curl +one-liner: + +```bash +# macOS — Homebrew (recommended) +brew tap sonpiaz/tap +brew install watch-cli + +# Any OS — curl +curl -fsSL https://github.com/sonpiaz/watch-cli/releases/latest/download/install.sh | bash +``` + +Once v0.3.0 ships and the tap is verified, the curl form stays +documented for Linux + servers + CI but is no longer the lead on +macOS. Existing curl-installed users can keep `~/.watch-cli` or +migrate via `rm -rf ~/.watch-cli ~/.local/bin/watch && brew install +watch-cli`. + +--- + +## Linux note + +Linuxbrew works (the formula does not gate on `OS.mac?`) but is rare +in this audience. Linux users should keep using the curl installer +— well-tested on Ubuntu and Debian via the CI matrix. Phase 4 does +not optimize for Linuxbrew: no CI matrix, no docs, no support +commitment. Issues filed get "use the curl installer on Linux." + +--- + +## Test plan + +The implementer must verify on a clean macOS machine (or fresh +Homebrew in a container) before declaring the tap published. Any +failure blocks the v0.3.0 ship. + +1. **Tap discovery.** `brew tap sonpiaz/tap` succeeds, no warnings. +2. **Formula audit.** `brew audit --strict --online sonpiaz/tap` + and `brew style sonpiaz/tap` pass with no errors. (`brew style` + enforces `desc` length and Ruby formatting.) +3. **Install.** `brew install watch-cli` completes. Deps pulled + automatically. Tarball download matches the formula SHA256. +4. **Layout.** `which watch` → `$(brew --prefix)/bin/watch` + (symlink into `$(brew --prefix)/Cellar/watch-cli//libexec/`). + `$(brew --prefix)/share/watch-cli/lib/env.sh` exists. Grep the + installed script: `ROOT_DIR` rewritten to `pkgshare`. +5. **Smoke.** `watch --help` exits 0, prints help. +6. **`brew test`.** `brew test watch-cli` passes. +7. **End-to-end.** `watch https://www.youtube.com/watch?v=dQw4w9WgXcQ` + produces a valid v1 text block (or v1 JSON with `--format json`). + Transcribe reads `KYMA_API_KEY` from `~/.config/watch-cli/env` + (Homebrew does not touch that file). Second run is a cache hit, + < 2 seconds. +8. **Upgrade.** Hand-edit the tap formula to `0.3.1-test` against a + synthetic tarball on a branch; `brew upgrade + sonpiaz/tap/watch-cli` against the branch tap resolves cleanly. +9. **Clean uninstall.** `brew uninstall watch-cli` then `brew untap + sonpiaz/tap`. After both: `which watch` returns nothing, + `$(brew --prefix)/share/watch-cli/` is gone, + `~/.config/watch-cli/env` untouched. + +--- + +## Anti-patterns + +- **Submitting to homebrew-core in this phase.** Tap first, validate + over multiple releases, consider core only after the formula has + been stable for ≥ one major version. Homebrew-core has a + notability bar (~75 stars + months of stability) and a multi-week + review queue; the tap unblocks users today. +- **Bundling whisper.cpp or the ggml model.** Offline is an explicit + `--with-local` opt-in with a 1.62 GB model. Forcing it on every + brew install punishes the majority of users. +- **Vendoring `yt-dlp` or `ffmpeg`.** Both are first-class Homebrew + formulae with active security updates. `depends_on` keeps + watch-cli users on the same binaries as the rest of their + toolchain. Vendoring would freeze versions and put watch-cli on + the hook for upstream breakage in tools we explicitly compose. +- **Patching bin scripts inside the formula beyond the `ROOT_DIR` + rewrite.** A deeper patch belongs in the watch-cli repo, not the + formula. Formulae that diverge from upstream become + unmaintainable. +- **Auto-merging the bump PR by default.** Phase 4 keeps it manual. + Auto-merge can be enabled later once proven; a malformed release + would otherwise push to every user's next `brew upgrade`. + +--- + +## Cross-references + +- Release workflow that builds the tarball, computes SHA256, and + triggers the bump: [`releases.md`](releases.md). +- Output schema the installed binary satisfies (Homebrew ships the + binary, not a new schema): [`output-schema.md`](output-schema.md). +- Locked pitch and tone rules governing the `desc` line: + [`../BRANDING.md`](../BRANDING.md). +- Offline transcribe path users opt into separately from brew: + [`offline-mode.md`](offline-mode.md). diff --git a/docs/releases.md b/docs/releases.md new file mode 100644 index 0000000..20745cf --- /dev/null +++ b/docs/releases.md @@ -0,0 +1,366 @@ +# Releases + +This document is the source of truth for the watch-cli release +contract: semver policy, where the version number lives, what a tag +triggers, and how a consumer pins to a specific version. Every +downstream consumer (`install.sh`, the Homebrew tap, the +`@sonpiaz/watch-cli-mcp` npm package) reads a release artifact — a +tag, a tarball, a SHA256 — and those artifacts have a stability +promise. + +--- + +## Purpose + +Every install today hits `main` HEAD. `install.sh` clones the repo +from `main`, and the README's curl one-liner fetches +`raw.githubusercontent.com/sonpiaz/watch-cli/main/install.sh`. One bad +commit on `main` therefore breaks every machine that runs the +installer that minute, and there are zero published versions for +Homebrew or any other packager to pin against. Tagged releases give +downstream packagers a stable artifact and turn the v1 output schema +promise (see [`output-schema.md`](output-schema.md)) into a concrete +contract: "v1 means the tarball at tag v1.x.y." + +--- + +## Semver policy + +watch-cli follows semver with one pre-1.0 exception (see below). + +### Stable (post v1.0.0) + +| Bump | When | Examples | +|---|---|---| +| **Major** (X.0.0) | The v1 output schema in [`output-schema.md`](output-schema.md) changes incompatibly. A CLI flag is removed. A documented exit code changes meaning. | Removing `transcript` from JSON output; renaming `--cookies` to `--cookie-file`; changing the meaning of exit `4`. | +| **Minor** (0.X.0) | A new CLI flag, a new `bin/*` script, or a new `lib/*` helper ships. New optional JSON fields. New stderr tag tokens. | Adding `--no-audio`; adding `bin/cookies-export`; adding `transcribe_cost_usd` (already shipped). | +| **Patch** (0.0.X) | Bug fixes, doc updates, dependency version notes. No surface change. | Fixing a `stat` portability bug; correcting a README link; pinning yt-dlp recommendation in install.sh. | + +A "surface change" is anything an external script can observe: stdout +contents in `--format json` or text mode, stderr `tag=…` tokens, exit +codes, CLI flags, environment variables read, file paths written. The +v1 stability promise in `output-schema.md` is the authoritative list. + +### Pre-1.0 (current state) + +watch-cli is at **v0.2.0** as of the merge of Phase 3. While the major +is `0`, minor bumps are **allowed to contain breaking changes** — +standard pre-1.0 semver. This gives the foundation specs +(output-schema, exit-codes, offline-mode, releases, homebrew) one +final shakeout before freeze. `0.X.0` may rename a JSON field, change +an exit code, or rename a stderr tag if the change improves the +spec. `0.0.X` continues to mean "bug fix only" even pre-1.0. + +### Cutoff to v1.0.0 + +v1.0.0 is cut when all five of the following hold: + +1. All foundation sub-specs are signed off: `output-schema.md`, + `exit-codes.md`, `offline-mode.md`, `releases.md` (this file), + `homebrew.md`. +2. The output schema in v1 has at least one external consumer in + production. The MCP server in `mcp-server/` already meets this — it + returns the v1 JSON shape verbatim. +3. The Homebrew tap is published and one user has installed via brew + successfully (see [`homebrew.md`](homebrew.md)). +4. 30 days of green CI on `main` after the last spec sign-off, with + the full matrix passing on macOS and Ubuntu. +5. No open issue tagged `schema-breaking` or `exit-code-breaking`. + +Cutting v1.0.0 ends the breaking-change runway. After v1.0.0, any +schema or exit-code change requires a major bump and the v1 +compatibility flag promised in `output-schema.md` (`--format +json-v1` available for at least one major release after v2 ships). + +--- + +## Version source of truth + +Today the `VERSION` string lives in two places: the constant +`VERSION="watch-cli v0.2.0"` on line 41 of `bin/watch` (other bin +scripts do not carry it), and `WATCH_CLI_VERSION="0.2.0"` in +`lib/env.sh` for the user-agent string. + +Phase 4 consolidates into a single source — new file +`lib/version.sh`: + +```bash +# lib/version.sh +export WATCH_CLI_VERSION="0.3.0" +export WATCH_CLI_VERSION_STRING="watch-cli v${WATCH_CLI_VERSION}" +``` + +- Every `bin/*` script sources `lib/version.sh` near the top (after + `lib/env.sh`) and uses `$WATCH_CLI_VERSION_STRING` for `--version`. + The hardcoded `VERSION=` in `bin/watch` is removed. +- `lib/env.sh` sources `lib/version.sh` and uses `$WATCH_CLI_VERSION` + for the user-agent. The duplicate `WATCH_CLI_VERSION=` line in + `env.sh` is removed. + +Version becomes a one-line change at release time with no bin/lib +drift risk. + +### CI guardrail + +`.github/workflows/ci.yml` gains a step that compares the value of +`WATCH_CLI_VERSION` in `lib/version.sh` against the latest git tag +that matches `v*`. Mismatch on a `main` build fails CI with: + +```text +error: lib/version.sh says 0.3.0 but latest tag is v0.4.0 +fix: bump WATCH_CLI_VERSION in lib/version.sh to 0.4.0, or cut a new tag +``` + +The release workflow (below) re-checks the same invariant before +publishing. + +--- + +## Tag policy + +Tags name a single point on `main`. Format: `v`. No +zero-stripping, no `-rc.N` suffix, no `-alpha`. Pre-1.0 cadence is +small enough that direct tags suffice; release-candidate runways +get added after v1.0.0 if needed. + +### Cutting a release + +1. Open a PR that bumps `WATCH_CLI_VERSION` in `lib/version.sh`. + Title: `release: vX.Y.Z`. PR body lists user-facing changes + (these get promoted into the GH Release notes). +2. CI passes (lint, smoke, schema, exit-code tests). +3. Merge with a squash commit titled `release: vX.Y.Z`. +4. From `main` at the merged commit: + ```bash + git checkout main && git pull --rebase + git tag -a vX.Y.Z -m "vX.Y.Z" + git push origin vX.Y.Z + ``` +5. The release workflow fires on the tag push and creates the GH + Release, publishes the npm package, and bumps the Homebrew tap. + +Tags are cut from `main` only. The release workflow asserts the tag's +commit is an ancestor of `origin/main`. + +--- + +## Release notes template + +Auto-generated from squashed PR titles since the previous tag, then +human-edited before publish. + +```markdown +## Highlights +- One-line summary of the headline change. +- Up to three more bullets for anything an installed user would care + about (new flag, new platform, breaking change). + +## What's new + + +## Schema version +Output schema: v1 (no change since v0.3.0). See +[output-schema.md](https://github.com/sonpiaz/watch-cli/blob/vX.Y.Z/docs/output-schema.md). + +## Install + brew tap sonpiaz/tap && brew install watch-cli # macOS + curl -fsSL https://github.com/sonpiaz/watch-cli/releases/download/vX.Y.Z/install.sh | bash + WATCH_CLI_VERSION=X.Y.Z curl -fsSL | bash # pin + +## Acknowledgements +Thanks to for issues, patches, and review. +``` + +The "Schema version" line is required on every release. If the major +did not change, say so explicitly — downstream consumers grep for it +to confirm they do not need to re-validate their parser. + +--- + +## GitHub Actions release workflow + +File: `.github/workflows/release.yml`. Trigger: `on: push: tags: +['v*']`. Each step is required; failure aborts the release. + +### 1. Verify the tag + +Check out the tag's commit; assert the commit is an ancestor of +`origin/main`. If not, fail with `error: tag vX.Y.Z is not on main +(got ); refusing to release`. + +### 2. Verify the version + +Source `lib/version.sh`; assert `$WATCH_CLI_VERSION` equals the tag +minus the leading `v`. Mismatch fails with the CI guardrail message. + +### 3. Build the source tarball + +```bash +VERSION="${GITHUB_REF_NAME#v}" +git archive --format=tar.gz \ + --prefix="watch-cli-${VERSION}/" \ + -o "watch-cli.tar.gz" HEAD +sha256sum watch-cli.tar.gz | awk '{print $1}' > watch-cli.tar.gz.sha256 +``` + +Tarball name is `watch-cli.tar.gz` — fixed, not version-suffixed. +The Homebrew formula resolves the per-version URL via `v#{version}` +interpolation (see [`homebrew.md`](homebrew.md)). + +### 4. Create the GH Release + +```bash +gh release create "v${VERSION}" \ + --title "v${VERSION}" \ + --notes-file release-notes.md \ + watch-cli.tar.gz \ + watch-cli.tar.gz.sha256 \ + install.sh +``` + +Three assets attached: source tarball, SHA256 file, and a copy of +`install.sh` from the tagged commit (so the pin-a-version curl form +resolves cleanly). + +### 5. Publish the MCP server + +```bash +cd mcp-server +npm ci +npm run build +npm publish --access public +``` + +Requires `NPM_TOKEN` repo secret scoped to publish under `@sonpiaz/`. +`mcp-server/package.json` must already carry the correct version — +the version-bump PR includes a bump of `mcp-server/package.json` to +match. + +### 6. Bump the Homebrew tap + +Opens a PR against `sonpiaz/homebrew-tap` with the new `version` and +`sha256` in `Formula/watch-cli.rb`. Full mechanism in +[`homebrew.md`](homebrew.md). Requires the `HOMEBREW_TAP_PAT` repo +secret (fine-grained PAT scoped to `sonpiaz/homebrew-tap`, +contents:write + pull-requests:write). + +### Required repo secrets + +- `NPM_TOKEN` — step 5; publish under `@sonpiaz/` on npm. + **Setup (one-time, manual):** generate an Automation token at + https://www.npmjs.com/settings/sonpiaz/tokens, then paste into the + watch-cli repo at Settings → Secrets and variables → Actions → + New repository secret. Without this, the `publish-mcp` job fails + and no MCP server is published — the GH Release and tarball are + unaffected. +- `HOMEBREW_TAP_PAT` — step 6; push branches + open PRs in + `sonpiaz/homebrew-tap`. + **Setup (one-time, manual):** generate a fine-grained PAT at + https://github.com/settings/personal-access-tokens, scope: + resource owner `sonpiaz`, repo access only `sonpiaz/homebrew-tap`, + permissions `contents:write` + `pull-requests:write` + + `metadata:read`, 1-year expiry. Paste into the watch-cli repo at + Settings → Secrets and variables → Actions. Without this, the + `bump-tap` job fails and Homebrew users do not get an auto-bump + PR — the GH Release and npm publish are unaffected. +- `GITHUB_TOKEN` (built-in) is sufficient for steps 1–4. + +--- + +## Install URL strategy + +`install.sh` today clones `main` HEAD. Phase 4 changes it to fetch a +tagged release tarball. + +```bash +# default — latest +curl -fsSL https://github.com/sonpiaz/watch-cli/releases/latest/download/install.sh | bash + +# pin a specific version +WATCH_CLI_VERSION=0.3.0 curl -fsSL \ + https://github.com/sonpiaz/watch-cli/releases/download/v0.3.0/install.sh | bash +``` + +`releases/latest/download/` is a GitHub-managed redirect that +always resolves to the newest published release. The installer +fetches `watch-cli.tar.gz` from the same release and unpacks into +`$INSTALL_DIR` instead of cloning `main`. When `WATCH_CLI_VERSION` is +set, the installer fetches from +`releases/download/v${WATCH_CLI_VERSION}/watch-cli.tar.gz`. Mismatch +between URL and env variable aborts with `error: WATCH_CLI_VERSION=… +disagrees with installer for v…`. + +### What changes in `install.sh` + +The implementer (not this spec) edits the installer: + +- Replace `git clone "$REPO_URL" "$INSTALL_DIR"` with `curl -fsSL + | tar -xz -C "$INSTALL_DIR" --strip-components=1`. +- Replace the `git pull --rebase` update path with a version compare + against the latest release and a fresh tarball fetch if newer. +- Resolve `WATCH_CLI_VERSION` into the URL when set. +- Verify tarball SHA256 against `watch-cli.tar.gz.sha256` before + unpacking. Mismatch is fatal `exit 1` with stderr + `tag=tarball-checksum-mismatch`. + +The `git clone` path stays available for the README's "from a clone" +flow — a developer who clones the repo and runs `./install.sh` +locally still works. Only `curl | bash` uses the release tarball. + +--- + +## What the implementer must do before tagging v1.0.0 + +1. All five foundation specs merged and signed off: + `output-schema.md`, `exit-codes.md`, `offline-mode.md`, + `releases.md`, `homebrew.md`. +2. Homebrew tap published. `brew install sonpiaz/tap/watch-cli` + succeeds on a fresh macOS machine ([`homebrew.md`](homebrew.md)). +3. `@sonpiaz/watch-cli-mcp` is on npm at a version matching + watch-cli (MCP v1.0.0 ships when watch-cli v1.0.0 ships). +4. CI green on `main` for 30 days. No skipped jobs, no + `continue-on-error: true` on any required step. +5. No open issue tagged `schema-breaking` or `exit-code-breaking`. +6. v1.0.0 release notes call out the stability-promise from + `output-schema.md`: "Output schema is now frozen at v1. Any + future incompatible change ships as v2 with a one-major-version + `--format json-v1` compatibility flag." + +--- + +## Test plan + +The implementer must verify end to end before declaring done: + +1. **Local dry-run.** Run the workflow on a personal fork. Tag a + v0.3.0-rc local-only tag (or use + [`nektos/act`](https://github.com/nektos/act)); confirm steps 1–4 + produce a tarball and a GH Release on the fork. +2. **Tarball integrity.** Download `watch-cli.tar.gz`, verify SHA256 + against `watch-cli.tar.gz.sha256`, unpack and run + `./watch-cli-0.3.0/install.sh` on a clean container. `watch + --version` prints `watch-cli v0.3.0`. +3. **Pinned install.** Run the `WATCH_CLI_VERSION=0.3.0` curl form + on a second clean container. Same result. +4. **Latest install.** Run the `releases/latest/download/install.sh` + form. Resolves to v0.3.0. +5. **MCP server publish.** `@sonpiaz/watch-cli-mcp@0.3.0` is on npm, + `bin/watch-cli-mcp` works via `npx`. +6. **Homebrew bump.** A PR opens against `sonpiaz/homebrew-tap` + titled `watch-cli 0.3.0`; merging makes `brew upgrade watch-cli` + work. Full Homebrew test plan in [`homebrew.md`](homebrew.md). +7. **Version mismatch.** Push a tag `v0.3.1` without bumping + `lib/version.sh`. The workflow fails at step 2 with the + documented error; no release is created. + +--- + +## Cross-references + +- Output schema this release ships with: + [`output-schema.md`](output-schema.md). +- Homebrew tap and per-release formula bump: + [`homebrew.md`](homebrew.md). +- Exit codes the installer and the release workflow exit with on + failure: [`exit-codes.md`](exit-codes.md). diff --git a/install.sh b/install.sh index 2d50c0b..92a8e1f 100755 --- a/install.sh +++ b/install.sh @@ -1,10 +1,21 @@ #!/usr/bin/env bash # watch-cli installer. # Usage: -# curl -fsSL https://raw.githubusercontent.com/sonpiaz/watch-cli/main/install.sh | bash +# curl -fsSL https://github.com/sonpiaz/watch-cli/releases/latest/download/install.sh | bash +# or pin a version: +# WATCH_CLI_VERSION=0.3.0 curl -fsSL \ +# https://github.com/sonpiaz/watch-cli/releases/download/v0.3.0/install.sh | bash # or, from a clone: # ./install.sh # +# Install strategy: +# - Default: fetch the latest GH Release tarball +# (releases/latest/download/watch-cli.tar.gz) and unpack into +# $INSTALL_DIR. +# - WATCH_CLI_VERSION=X.Y.Z set: pin to that specific release. +# - Fallback: if no GH Releases exist yet (bootstrap window before +# v0.3.0 ships), git clone main HEAD into $INSTALL_DIR. +# # Flags: # --with-skill After install, drop SKILL.md into ~/.claude/skills/watch-cli/ # so Claude Code picks up the watch-cli skill on next start. @@ -23,6 +34,9 @@ INSTALL_DIR="${WATCH_CLI_HOME:-$HOME/.watch-cli}" BIN_LINK_DIR="${WATCH_CLI_BIN:-$HOME/.local/bin}" CLAUDE_SKILLS_DIR="${HOME}/.claude/skills" +# Optional: pin to a specific release. Empty → latest. +WATCH_CLI_VERSION="${WATCH_CLI_VERSION:-}" + WITH_SKILL=0 WITH_MCP=0 WITH_LOCAL=0 @@ -37,9 +51,15 @@ usage() { watch-cli installer Usage: - curl -fsSL https://raw.githubusercontent.com/sonpiaz/watch-cli/main/install.sh | bash + curl -fsSL https://github.com/sonpiaz/watch-cli/releases/latest/download/install.sh | bash + WATCH_CLI_VERSION=0.3.0 curl -fsSL \ + https://github.com/sonpiaz/watch-cli/releases/download/v0.3.0/install.sh | bash ./install.sh [--with-skill] [--with-mcp] [--with-local] +Env: + WATCH_CLI_VERSION Pin to a specific release (e.g. 0.3.0). + Unset → install the latest release. + Flags: --with-skill After install, copy SKILL.md into ~/.claude/skills/watch-cli/ so Claude Code picks up the watch-cli skill on next start. @@ -88,14 +108,87 @@ fi green "✓ Dependencies present (yt-dlp, ffmpeg, jq, curl, python3)" # ── Install/update repo ── +# Strategy: +# 1. If $WATCH_CLI_VERSION is set, fetch that exact release tarball. +# 2. Otherwise fetch releases/latest/download/watch-cli.tar.gz — +# a GH-managed redirect that always resolves to the newest tag. +# 3. If no GH Release exists yet (bootstrap window, HTTP 404), fall +# back to git clone of main. This branch is removed once v0.3.0 +# is live. +# 4. If $INSTALL_DIR is an existing git clone, keep using git pull +# (developer working from a clone, not the curl installer). +_resolve_tarball_url() { + if [[ -n "$WATCH_CLI_VERSION" ]]; then + echo "$REPO_URL/releases/download/v${WATCH_CLI_VERSION}/watch-cli.tar.gz" + else + echo "$REPO_URL/releases/latest/download/watch-cli.tar.gz" + fi +} + +_resolve_checksum_url() { + if [[ -n "$WATCH_CLI_VERSION" ]]; then + echo "$REPO_URL/releases/download/v${WATCH_CLI_VERSION}/watch-cli.tar.gz.sha256" + else + echo "$REPO_URL/releases/latest/download/watch-cli.tar.gz.sha256" + fi +} + +_install_from_tarball() { + local tarball_url checksum_url tmpdir tarball_path expected actual + tarball_url="$(_resolve_tarball_url)" + checksum_url="$(_resolve_checksum_url)" + + tmpdir="$(mktemp -d -t watch-cli-install.XXXXXX)" + trap 'rm -rf "$tmpdir"' RETURN + tarball_path="$tmpdir/watch-cli.tar.gz" + + yellow "Downloading $tarball_url …" + # -f makes curl fail on 4xx/5xx; -L follows the latest-redirect. + if ! curl -fsSL "$tarball_url" -o "$tarball_path"; then + return 1 + fi + + # Best-effort checksum verify. Asset is uploaded alongside the + # tarball; mismatch is fatal because a tampered tarball is worse + # than a bootstrap fallback. + if expected="$(curl -fsSL "$checksum_url" 2>/dev/null | awk '{print $1}')" \ + && [[ -n "$expected" ]]; then + actual="$(shasum -a 256 "$tarball_path" | awk '{print $1}')" + if [[ "$actual" != "$expected" ]]; then + red "[install] error: tarball-checksum-mismatch tag=tarball-checksum-mismatch" + echo " expected: $expected" + echo " actual: $actual" + return 1 + fi + green "✓ tarball SHA256 verified" + else + yellow "⚠ no checksum file on the release — skipping verify" + fi + + mkdir -p "$INSTALL_DIR" + # --strip-components=1 drops the top-level watch-cli-X.Y.Z/ dir + # from the git-archive output, so $INSTALL_DIR is the tree root. + tar -xzf "$tarball_path" -C "$INSTALL_DIR" --strip-components=1 +} + if [[ -d "$INSTALL_DIR/.git" ]]; then - yellow "Updating existing install at $INSTALL_DIR …" + yellow "Updating existing git clone at $INSTALL_DIR …" git -C "$INSTALL_DIR" pull --rebase --quiet + green "✓ watch-cli updated at $INSTALL_DIR" +elif _install_from_tarball; then + green "✓ watch-cli installed at $INSTALL_DIR (from release tarball)" else - yellow "Cloning watch-cli to $INSTALL_DIR …" + # Bootstrap fallback for the window before the first GH Release + # exists. Remove once v0.3.0 is shipped and the tarball is live. + yellow "Release tarball not available — falling back to git clone …" + if [[ -d "$INSTALL_DIR" && ! -d "$INSTALL_DIR/.git" ]]; then + # A previous tarball install left an INSTALL_DIR without .git; + # nuke so git clone has a clean target. + rm -rf "$INSTALL_DIR" + fi git clone --quiet "$REPO_URL" "$INSTALL_DIR" + green "✓ watch-cli installed at $INSTALL_DIR (from git clone)" fi -green "✓ watch-cli installed at $INSTALL_DIR" # ── Symlink bins ── mkdir -p "$BIN_LINK_DIR" diff --git a/lib/env.sh b/lib/env.sh index ec5d501..26823b3 100644 --- a/lib/env.sh +++ b/lib/env.sh @@ -31,9 +31,11 @@ fi [[ -n "${WATCH_CLI_ENV_LOADED:-}" ]] && return 0 export WATCH_CLI_ENV_LOADED=1 -# Version is sent in the User-Agent header so Kyma can attribute usage and -# detect which install needs an upgrade. Bump on every release. -export WATCH_CLI_VERSION="0.2.0" +# Version lives in lib/version.sh (single source of truth — see that file +# and docs/releases.md). The User-Agent below uses WATCH_CLI_VERSION so +# Kyma can attribute usage and detect which install needs an upgrade. +# shellcheck source=version.sh +source "$(dirname "${BASH_SOURCE[0]}")/version.sh" export WATCH_CLI_USER_AGENT="watch-cli/${WATCH_CLI_VERSION}" _load_dotenv() { diff --git a/lib/version.sh b/lib/version.sh new file mode 100644 index 0000000..5fdc358 --- /dev/null +++ b/lib/version.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Single source of truth for the watch-cli version. Sourced by every +# bin/* script (for --version) and by lib/env.sh (for the User-Agent +# header sent on Kyma calls). +# +# Release workflow (.github/workflows/release.yml) checks +# WATCH_CLI_VERSION here against the pushed git tag and refuses to +# publish on mismatch. CI on main runs the same check against the +# latest v* tag. See docs/releases.md for the full release contract. +# +# To cut a release: bump WATCH_CLI_VERSION below, open a release PR +# titled `release: vX.Y.Z`, merge, then tag the merge commit `vX.Y.Z` +# and push. The tag fires the release workflow. + +export WATCH_CLI_VERSION="0.3.0" +export WATCH_CLI_VERSION_STRING="watch-cli v${WATCH_CLI_VERSION}" diff --git a/mcp-server/package.json b/mcp-server/package.json index da5f9bf..7bb63bf 100644 --- a/mcp-server/package.json +++ b/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "@sonpiaz/watch-cli-mcp", - "version": "0.1.0", + "version": "0.3.0", "description": "Watch any social video → get an architecture diagram, working component, runnable notebook, or step-by-step cheat sheet — automatically. MCP stdio server for watch-cli.", "type": "module", "main": "dist/index.js",