feat: show models in status, improve pre-push hook

- Move model info from --help to `qmd status` with live HuggingFace
  links derived from actual configured URIs
- Pre-push hook: handle non-interactive shells gracefully, resolve
  annotated tags correctly for CI checks
This commit is contained in:
Tobi Lutke 2026-02-16 09:08:28 -04:00
parent 8dd6cdcebf
commit 63f3b68559
No known key found for this signature in database
4 changed files with 91 additions and 67 deletions

View File

@ -2,6 +2,15 @@
## [Unreleased]
### Changes
- CLI: `qmd status` now shows models with full HuggingFace links instead of
static names in `--help`. Model info is derived from the actual configured
URIs so it stays accurate if models change.
- Release tooling: pre-push hook handles non-interactive shells (CI, editors)
gracefully — warnings auto-proceed instead of hanging on a tty prompt.
Annotated tags now resolve correctly for CI checks.
## [1.0.5] - 2026-02-16
The npm package now ships compiled JavaScript instead of raw TypeScript,

View File

@ -8,7 +8,28 @@ set -euo pipefail
# 2. CHANGELOG.md has a "## [{version}] - {date}" entry
# 3. CI passed upstream on GitHub for the tagged commit
#
# Installed automatically by: bun install (via prepare script)
# In non-interactive shells (no tty), warnings auto-proceed instead of
# prompting. Hard failures (version mismatch, missing changelog) always block.
# Detect if we can prompt
CAN_PROMPT=false
if [[ -t 0 ]] || [[ -e /dev/tty ]]; then
# Verify /dev/tty is actually usable
if echo -n "" > /dev/tty 2>/dev/null; then
CAN_PROMPT=true
fi
fi
prompt_or_continue() {
local msg="$1"
if $CAN_PROMPT; then
read -p "$msg [y/N] " -n 1 -r </dev/tty
echo ""
[[ $REPLY =~ ^[Yy]$ ]] || exit 1
else
echo "$msg — auto-proceeding (non-interactive)"
fi
}
while read -r local_ref local_sha remote_ref remote_sha; do
# Only validate v* tag pushes
@ -34,7 +55,7 @@ while read -r local_ref local_sha remote_ref remote_sha; do
echo "Run: jq --arg v '$VERSION' '.version = \$v' package.json > tmp && mv tmp package.json"
exit 1
fi
echo " package.json version: $PKG_VERSION"
echo " package.json version: $PKG_VERSION"
# --- 2. CHANGELOG.md must have an entry for this version ---
if [[ ! -f CHANGELOG.md ]]; then
@ -51,17 +72,17 @@ while read -r local_ref local_sha remote_ref remote_sha; do
echo "Write the changelog entry first. See CLAUDE.md for guidelines."
exit 1
fi
echo " CHANGELOG.md: entry found"
echo " CHANGELOG.md: entry found"
# --- 3. CI must have passed on GitHub for this commit ---
COMMIT=$(git rev-parse "$TAG" 2>/dev/null || git rev-parse HEAD)
# Resolve annotated tag to its underlying commit
COMMIT=$(git rev-list -n 1 "$TAG" 2>/dev/null || git rev-parse HEAD)
if ! command -v gh &>/dev/null; then
echo " CI check: skipped (gh CLI not installed)"
continue
fi
# Check GitHub Actions check runs
CHECK_JSON=$(gh api "repos/{owner}/{repo}/commits/$COMMIT/check-runs" 2>/dev/null || echo "")
if [[ -z "$CHECK_JSON" ]]; then
@ -72,17 +93,13 @@ while read -r local_ref local_sha remote_ref remote_sha; do
TOTAL=$(echo "$CHECK_JSON" | jq -r '.total_count // 0' 2>/dev/null || echo "0")
if [[ "$TOTAL" -eq 0 ]] 2>/dev/null; then
# No checks found — commit may not be pushed yet
MAIN_SHA=$(git rev-parse origin/main 2>/dev/null || echo "")
if [[ "$COMMIT" != "$MAIN_SHA" ]]; then
echo ""
echo "WARNING: No CI runs found for $COMMIT and it's not the tip of origin/main."
echo "Push the commit to main first and wait for CI to pass."
read -p "Push tag anyway? [y/N] " -n 1 -r </dev/tty
echo ""
[[ $REPLY =~ ^[Yy]$ ]] || exit 1
echo "WARNING: No CI runs found for $COMMIT"
prompt_or_continue "Push tag anyway?"
else
echo " CI check: no runs found (commit is on main)"
echo " CI check: no runs yet (commit is tip of origin/main)"
fi
else
FAILED=$(echo "$CHECK_JSON" | jq '[.check_runs // [] | .[] | select(.conclusion == "failure")] | length' 2>/dev/null || echo "0")
@ -98,15 +115,13 @@ while read -r local_ref local_sha remote_ref remote_sha; do
if [[ "$PENDING" -gt 0 ]] 2>/dev/null; then
echo ""
echo "WARNING: CI still running for commit $COMMIT ($PENDING pending)"
read -p "Push tag anyway? [y/N] " -n 1 -r </dev/tty
echo ""
[[ $REPLY =~ ^[Yy]$ ]] || exit 1
prompt_or_continue "Push tag anyway?"
else
echo " CI check: all passed"
echo " CI check: all passed ✓"
fi
fi
echo "All checks passed for $TAG"
echo "All checks passed for $TAG"
done
exit 0

View File

@ -16,35 +16,38 @@ Cut a release, validate the changelog, and ensure git hooks are installed.
When the user triggers `/release <version>`:
1. **Install hooks** — run `scripts/install-hooks.sh` (idempotent)
2. **Check for unstaged work** — run `git status`. If there are valuable
unstaged/uncommitted changes that belong in this release, commit them
first (use the commit skill or make well-formed commits directly).
3. **Validate changelog** — check if `## [Unreleased]` has content.
If empty or missing, **write the changelog now** (see below).
4. **Preview** — show the user what will be released (unreleased content +
minor series rollup via `scripts/extract-changelog.sh`)
5. **Ask for confirmation** — do NOT proceed without explicit user approval
6. **Run `scripts/release.sh <version>`** — renames `[Unreleased]`, bumps
version, commits, tags
7. **Remind** — tell the user to `git push origin main --tags`
1. **Gather context** — run `skills/release/scripts/release-context.sh <version>`.
This silently installs git hooks and prints everything needed: version info,
working directory status, commits since last release, files changed, current
`[Unreleased]` content, and the previous release entry for style reference.
2. **Commit outstanding work** — if the context shows staged, modified, or
untracked files that belong in this release, commit them first. Use the
/commit skill or make well-formed commits directly.
3. **Write the changelog** — if `[Unreleased]` is empty, write it now using
the commits and file changes from the context output. Follow the changelog
standard below. Re-run the context script after committing if needed.
4. **Cut the release** — run `scripts/release.sh <version>`. This renames
`[Unreleased]``[X.Y.Z] - date`, inserts a fresh `[Unreleased]`,
bumps `package.json`, commits, and tags.
5. **Show the final changelog** — print the full `[Unreleased]` +
minor series rollup via `scripts/extract-changelog.sh <version>`.
Ask the user to confirm before pushing.
6. **Push** — after explicit confirmation, run `git push origin main --tags`.
7. **Watch CI** — after the push, start a background dispatch to watch the
publish workflow. Use `interactive_shell` in dispatch mode with:
```
gh run watch $(gh run list --workflow=publish.yml --limit=1 --json databaseId --jq '.[0].databaseId') --exit-status
```
The agent will be notified when CI completes and should report the result.
If any step fails, stop and explain. Never force-push or skip validation.
### Writing the changelog from git history
If `[Unreleased]` is empty when the release is triggered, populate it before
continuing:
1. Find the last release tag: `git describe --tags --abbrev=0`
2. Review commits since then: `git log <tag>..HEAD --oneline`
3. Read the diffs for anything non-obvious: `git log <tag>..HEAD --stat`
4. Write changelog entries following the standard below — group by theme,
explain the why, include numbers, credit contributors.
5. Add optional prose highlights (1-4 sentences) if the release warrants it.
6. Write the entries into `## [Unreleased]` in CHANGELOG.md.
7. Commit the changelog update, then continue with the release.
## Changelog Standard
The changelog lives in `CHANGELOG.md` and follows [Keep a Changelog](https://keepachangelog.com/) conventions.
@ -54,18 +57,15 @@ The changelog lives in `CHANGELOG.md` and follows [Keep a Changelog](https://kee
- `## [Unreleased]` — accumulates entries between releases
- `## [X.Y.Z] - YYYY-MM-DD` — released versions
The release script renames `[Unreleased]``[X.Y.Z] - date` and inserts a
fresh empty `[Unreleased]` section automatically.
### Structure of a release entry
Each version entry has two parts:
**1. Highlights (optional, 1-4 sentences of prose)**
Immediately after the version heading, before any `###` section. This is the
elevator pitch — what would you tell someone in 30 seconds? Only include for
releases with significant changes. Skip for small patches.
Immediately after the version heading, before any `###` section. The elevator
pitch — what would you tell someone in 30 seconds? Only for significant
releases; skip for small patches.
```markdown
## [1.1.0] - 2026-03-01
@ -110,8 +110,7 @@ through parallel contexts. GPU auto-detection replaces the unreliable
## GitHub Release Notes
Each GitHub release includes the full changelog for the **minor series** back
to x.x.0. Releasing v1.2.3 includes entries for 1.2.3, 1.2.2, 1.2.1, and
1.2.0. The `scripts/extract-changelog.sh` script handles this, and the
to x.x.0. The `scripts/extract-changelog.sh` script handles this, and the
publish workflow (`publish.yml`) calls it to populate the GitHub release.
## Git Hooks
@ -120,15 +119,8 @@ The pre-push hook (`scripts/pre-push`) blocks `v*` tag pushes unless:
1. `package.json` version matches the tag
2. `CHANGELOG.md` has a `## [X.Y.Z] - date` entry for the version
3. CI passed on GitHub for the tagged commit
3. CI passed on GitHub (warns in non-interactive shells, blocks in terminals)
Run `skills/release/scripts/install-hooks.sh` to install (also runs
automatically via `bun install` prepare script).
## Scripts
- [`scripts/install-hooks.sh`](scripts/install-hooks.sh) — install/update git hooks
- Project scripts used during release:
- `scripts/release.sh` — rename [Unreleased], bump version, commit, tag
- `scripts/extract-changelog.sh` — extract minor series notes for GitHub release
- `scripts/pre-push` — pre-push validation hook
Hooks are installed silently by the context script. They can also be installed
manually via `skills/release/scripts/install-hooks.sh` or automatically via
`bun install` (prepare script).

View File

@ -365,6 +365,19 @@ async function showStatus(): Promise<void> {
console.log(`\n${c.dim}No collections. Run 'qmd collection add .' to index markdown files.${c.reset}`);
}
// Models
{
// hf:org/repo/file.gguf → https://huggingface.co/org/repo
const hfLink = (uri: string) => {
const match = uri.match(/^hf:([^/]+\/[^/]+)\//);
return match ? `https://huggingface.co/${match[1]}` : uri;
};
console.log(`\n${c.bold}Models${c.reset}`);
console.log(` Embedding: ${hfLink(DEFAULT_EMBED_MODEL_URI)}`);
console.log(` Reranking: ${hfLink(DEFAULT_RERANK_MODEL_URI)}`);
console.log(` Generation: ${hfLink(DEFAULT_GENERATE_MODEL_URI)}`);
}
// Device / GPU info
try {
const llm = getDefaultLlamaCpp();
@ -2182,11 +2195,6 @@ function showHelp(): void {
console.log(" --max-bytes <num> - Skip files larger than N bytes (default: 10240)");
console.log(" --json/--csv/--md/--xml/--files - Output format (same as search)");
console.log("");
console.log("Models (auto-downloaded from HuggingFace):");
console.log(" Embedding: embeddinggemma-300M-Q8_0");
console.log(" Reranking: qwen3-reranker-0.6b-q8_0");
console.log(" Generation: Qwen3-0.6B-Q8_0");
console.log("");
console.log(`Index: ${getDbPath()}`);
}