Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions .github/scripts/generate-cli-notes.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env bash
Comment thread
aliasunder marked this conversation as resolved.
# List the commits since the previous CLI tag that change what a CLI user gets,
# one `- <subject>` line each, newest first.
# Usage: generate-cli-notes.sh <new-tag>
#
# A commit is listed when its diff under cli/ adds or removes a line that none
# of these rules skips:
# - blank lines
# - lines in files under cli/src/__tests__/
# - the "version" line of cli/package.json (the release bump)
# - in .ts files, lines that start, after any indent, with #, //, /* or *:
# comments, and the comment text of the .env templates in cli/src/env.ts.
# A # line shaped like a setting (# KEY=value) still counts: the templates
# ship optional settings commented out, for the user to uncomment.

set -euo pipefail

NEW_TAG="${1:?Usage: generate-cli-notes.sh <new-tag>}"
# --match keeps server tags (v0.55.0) out. With no earlier CLI tag, the range
# is every commit reachable from new-tag.
PREV_TAG=$(git describe --tags --abbrev=0 --match "cli-v*" "$NEW_TAG"^ 2> /dev/null || echo "")

if [ -n "$PREV_TAG" ]; then
RANGE="${PREV_TAG}..${NEW_TAG}"
else
RANGE="$NEW_TAG"
fi

# Exits 0 when the patch on stdin adds or removes a counted line.
has_counted_change() {
awk '
# A file header comes before its first @@ line, so a content line such as
# "--- x" (a removed "-- x") is never read as a header.
/^diff --git / { in_hunk = 0; path = ""; next }
/^@@/ { in_hunk = 1; next }
# Git quotes a name holding special characters and ends the header with a
# tab when the name holds a space. A /dev/null header (an added or deleted
# file) fails the match, so path comes from the other header.
!in_hunk && /^(---|\+\+\+) "?[ab]\// {
path = $0
sub(/^(---|\+\+\+) "?[ab]\//, "", path)
sub(/"?\t?$/, "", path)
next
}
Comment thread
aliasunder marked this conversation as resolved.
!in_hunk { next }
/^[+-]/ {
content = substr($0, 2)
if (content ~ /^[ \t\r]*$/) next
if (path == "cli/package.json" && content ~ /^[ \t]*"version"[ \t]*:/) next
# Before the # skip below, so a commented-out setting (# KEY=value) counts.
if (path ~ /\.ts$/ && content ~ /^[ \t]*#[ \t]*[A-Z][A-Z0-9_]*=/) { found = 1; exit }
if (path ~ /\.ts$/ && content ~ /^[ \t]*(#|\/\/|\/\*|\*)/) next
Comment thread
aliasunder marked this conversation as resolved.
found = 1
exit
}
END { exit found ? 0 : 1 }
'
}

CLI_PATHSPEC=(cli/ ':(exclude)cli/src/__tests__/')

# Hex hashes only, so word splitting in the loop below is safe. Merge commits
# are skipped: the commits a merge brings in are checked on their own.
COMMIT_HASHES=$(git log --no-merges --format=%H "$RANGE" -- "${CLI_PATHSPEC[@]}")

for commit_hash in $COMMIT_HASHES; do
Comment thread
aliasunder marked this conversation as resolved.
PATCH=$(git diff-tree -p --root --no-commit-id -U0 "$commit_hash" -- "${CLI_PATHSPEC[@]}")
if has_counted_change <<< "$PATCH"; then
git show -s --format='- %s' "$commit_hash"
fi
done
23 changes: 11 additions & 12 deletions .github/workflows/cli_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -220,29 +220,28 @@ jobs:
echo "\`\`\`"
} >> "$GITHUB_STEP_SUMMARY"

# --latest=false keeps the "Latest" badge on server releases. Notes
# list only cli/ commits — server commits interleave on main, so
# range-wide auto-notes would include unrelated work. If this step
# fails after a successful publish, create the release manually
# (re-running the workflow would refuse to republish the version).
# - --latest=false keeps the "Latest" badge on server releases.
# - Server commits interleave on main, so GitHub's --generate-notes
# would list them too; generate-cli-notes.sh lists only commits that
# change what a CLI user gets.
# - If this step fails after a successful publish, run its commands by
# hand with NEXT set to the version: re-running the workflow would
# refuse to republish it.
- name: Create GitHub release
if: inputs.bump != 'beta'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NEXT: ${{ steps.next.outputs.version }}
run: |
# "Refuse to republish an existing version" stops every repeat after
# a publish, so this skip is reached only for a release created by
# hand before the version reached npm.
if gh release view "cli-v$NEXT" > /dev/null 2>&1; then
echo "Release cli-v$NEXT already exists; skipping"
exit 0
fi
PREV=$(git describe --tags --match "cli-v*" --abbrev=0 "cli-v$NEXT"^ 2> /dev/null || true)
if [ -n "$PREV" ]; then
RANGE="$PREV..cli-v$NEXT"
else
RANGE="cli-v$NEXT"
fi
{
git log "$RANGE" --pretty="- %s" -- cli/
bash .github/scripts/generate-cli-notes.sh "cli-v$NEXT"
echo ""
echo "Published to npm: https://www.npmjs.com/package/vault-cortex/v/$NEXT"
} > /tmp/release-notes.md
Expand Down
57 changes: 31 additions & 26 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -943,32 +943,37 @@ Two naming layers — MCP (JSON wire format) and TypeScript (internal):
centralized test directory higher up the tree. Don't spawn a
standalone test file just to mock differently; use
`vi.mock(path, { spy: true })` to keep the real implementation.
**Exception — shell scripts outside vitest's include paths.** The
remote image's s6 init scripts under `rootfs/etc/s6-overlay/scripts/`
are the `vault-mcp` server's boot chain for the `:remote` target, the
Claude Code hook sits in `.claude/hooks/`, and the release scripts sit
in `.github/scripts/`. None is a TypeScript module, and vitest's
include paths (`src/`, `cli/src/`, `scripts/`) reach none of those
folders. The init-script tests live in
`src/vault-mcp/__tests__/` (`init-check-auth.test.ts`,
`init-obsidian-login.test.ts`, `init-first-sync.test.ts`,
`init-setup-user.test.ts`, `init-setup-vault.test.ts`,
`check-sync-vault.test.ts`, which covers the vault check
`init-setup-vault` runs, and `print-derived-env.test.ts`, which covers
the derivation that `init-derive-env` publishes), and the hook's and
release scripts' tests in `src/__tests__/` (`install-deps.test.ts`,
`generate-notes.test.ts`, `update-changelog.test.ts`). These script
tests run the real script under its own shell (`sh` for the init
scripts, `bash` for the others) with stub binaries on `PATH` or, for
the release-notes script, a temporary git repository, and name
the script they cover —
don't move them next to the scripts or widen vitest's include for
them. Whole-image behaviour (the init chain's ordering,
the `container_environment` files the chain publishes, the volume
layout, and the checks that stop the container) belongs in the
remote-boot test suite (`src/__tests__/docker/`, see "Remote image
boot tests — when to add"); the branches inside one script stay in
that script's test file.

**Exception — shell scripts outside vitest's include paths.** These
scripts are not TypeScript modules, and vitest's include paths (`src/`,
`cli/src/`, `scripts/`) reach none of their folders, so their tests
live in folders vitest reaches:
- the remote image's s6 init scripts in
`rootfs/etc/s6-overlay/scripts/` (the `vault-mcp` server's boot chain
for the `:remote` target) → `src/vault-mcp/__tests__/`:
`init-check-auth.test.ts`, `init-obsidian-login.test.ts`,
`init-first-sync.test.ts`, `init-setup-user.test.ts`,
`init-setup-vault.test.ts`, `check-sync-vault.test.ts` (the vault
check `init-setup-vault` runs), and `print-derived-env.test.ts` (the
derivation that `init-derive-env` publishes)
- the Claude Code hook in `.claude/hooks/` →
`src/__tests__/install-deps.test.ts`
- the release scripts in `.github/scripts/` → `src/__tests__/`:
`generate-notes.test.ts`, `generate-cli-notes.test.ts`,
`update-changelog.test.ts`

These script tests run the real script under its own shell (`sh` for
the init scripts, `bash` for the others) with stub binaries on `PATH`
or, for `generate-notes.sh` and `generate-cli-notes.sh`, a temporary
git repository, and name the script they cover — don't move them next
to the scripts or widen vitest's include for them.

Whole-image behaviour (the init chain's ordering, the
`container_environment` files the chain publishes, the volume layout,
and the checks that stop the container) belongs in the remote-boot test
suite (`src/__tests__/docker/`, see "Remote image boot tests — when to
add"); the branches inside one script stay in that script's test file.

- Separate `it()` blocks over callback-pattern `it.each` when
assertions are structurally different — `it.each` is for genuinely
identical assertion shapes (input → expected). Use labeled case objects
Expand Down
16 changes: 14 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,20 @@ What each release's notes list:
decides what the image build copies, or `.env.example`, which DEPLOY.md
has deployers copy), or files under `.claude/`,
`.devin/` or `.husky/`
- **CLI releases** (`.github/workflows/cli_release.yml`): every commit that
touches `cli/`, dependency bumps included (`deps-dev` too).
- **CLI releases** (`.github/scripts/generate-cli-notes.sh`): every commit
that changes `cli/`, including dependency bumps that edit
`cli/package.json`, except commits whose added and removed lines under
`cli/` are all:
- blank
- in files under `cli/src/__tests__/`
- the `"version"` line of `cli/package.json` (the release bump)
- lines in `.ts` files that start, after any indent, with `#`, `//`, `/*`
or `*`: comments, and the comment text of the `.env` templates in
`cli/src/env.ts`. A `#` line shaped like a setting (`# KEY=value`) still
counts: the templates ship optional settings commented out, for the user
to uncomment.

In other files, `cli/README.md` included, every non-blank line counts.

See the [DEPLOY.md CI/CD section](./DEPLOY.md#cicd) for details on each workflow.

Expand Down
1 change: 1 addition & 0 deletions scripts/__tests__/lint-shell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { describe, expect, it, onTestFinished } from "vitest"
/** Every shell script in the repo, sorted: what a run with no paths checks. */
const ALL_SHELL_SCRIPTS = [
".claude/hooks/install-deps.sh",
".github/scripts/generate-cli-notes.sh",
".github/scripts/generate-notes.sh",
".github/scripts/update-changelog.sh",
"cli/src/__tests__/integration/fixtures/docker",
Expand Down
Loading
Loading