From 93fd452aba00b89f7681d3d3475bc6967ecaaec5 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:10:41 -0400 Subject: [PATCH 1/8] ci(release): list only CLI changes in CLI release notes The CLI release notes listed every commit that touched cli/, including the release's own version bump, test-only changes, and commits that only reworded comments or the .env template comments in cli/src/env.ts. A commit is now listed only when its diff under cli/ adds or removes a line that is not blank and not a comment. Files under cli/src/__tests__/ and the "version" line of cli/package.json are not counted; in .ts files, lines starting with #, //, /*, * or */ are comments. The logic moves to .github/scripts/generate-cli-notes.sh, tested against real git. Co-Authored-By: Claude Opus 5.5 --- .github/scripts/generate-cli-notes.sh | 62 ++++++ .github/workflows/cli_release.yml | 11 +- CONTRIBUTING.md | 10 +- src/__tests__/generate-cli-notes.test.ts | 238 +++++++++++++++++++++++ 4 files changed, 311 insertions(+), 10 deletions(-) create mode 100755 .github/scripts/generate-cli-notes.sh create mode 100644 src/__tests__/generate-cli-notes.test.ts diff --git a/.github/scripts/generate-cli-notes.sh b/.github/scripts/generate-cli-notes.sh new file mode 100755 index 000000000..2bc9823c1 --- /dev/null +++ b/.github/scripts/generate-cli-notes.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# List the commits between two CLI tags that change what a CLI user gets, one +# `- ` line each, newest first. +# Usage: generate-cli-notes.sh [previous-tag] +# If previous-tag is omitted, uses the CLI tag before new-tag; with no earlier +# CLI tag, lists every commit reachable from new-tag. +# +# A commit is listed when its diff under cli/ adds or removes at least one line +# that is not blank and not a comment. Not counted: +# - files under cli/src/__tests__/ +# - the "version" line of cli/package.json (the release bump) +# - in .ts files, lines starting with #, //, /*, * or */ (the # lines are the +# .env template text in cli/src/env.ts) +# Every other non-blank line counts, cli/README.md included: npm shows it. + +set -euo pipefail + +NEW_TAG="${1:?Usage: generate-cli-notes.sh [previous-tag]}" +# --match restricts to CLI tags (cli-v0.16.0), so server tags (v0.55.0) never +# end the range. +PREV_TAG="${2:-$(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. Headers come +# before a file's first @@ line, so a content line such as "--- x" (a removed +# "-- x") is never read as a header. +has_counted_change() { + awk ' + /^diff --git / { in_hunk = 0; next } + /^@@/ { in_hunk = 1; next } + !in_hunk && /^--- a\// { path = substr($0, 7); next } + !in_hunk && /^\+\+\+ b\// { path = substr($0, 7); next } + !in_hunk { next } + /^[+-]/ { + content = substr($0, 2) + if (content ~ /^[ \t]*$/) next + if (path == "cli/package.json" && content ~ /^[ \t]*"version"[ \t]*:/) next + if (path ~ /\.ts$/ && content ~ /^[ \t]*(#|\/\/|\/\*|\*)/) next + found = 1 + exit + } + END { exit found ? 0 : 1 } + ' +} + +# Hex hashes only, so word splitting in the loop below is safe. +COMMIT_HASHES=$(git log --no-merges --format=%H "$RANGE" -- cli/) + +for commit_hash in $COMMIT_HASHES; do + # Fixed prefixes keep the header match independent of diff.noprefix and + # diff.mnemonicPrefix; --text keeps a binary change countable. + PATCH=$(git diff-tree -p --root --no-commit-id --no-renames --text -U0 \ + --src-prefix=a/ --dst-prefix=b/ "$commit_hash" -- cli/ ':(exclude)cli/src/__tests__/') + if has_counted_change <<< "$PATCH"; then + git show -s --format='- %s' "$commit_hash" + fi +done diff --git a/.github/workflows/cli_release.yml b/.github/workflows/cli_release.yml index fa466f070..7b848fc86 100644 --- a/.github/workflows/cli_release.yml +++ b/.github/workflows/cli_release.yml @@ -199,7 +199,8 @@ 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 + # list only commits that change what a CLI user gets (rules in + # generate-cli-notes.sh) — 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). @@ -213,14 +214,8 @@ jobs: 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/ + .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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7d2f798be..04b9a9169 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -254,8 +254,14 @@ 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/`, dependency bumps included, 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) + - comment lines in `.ts` files, including the `#` lines of the `.env` + templates in `cli/src/env.ts`; every line of `cli/README.md` counts See the [DEPLOY.md CI/CD section](./DEPLOY.md#cicd) for details on each workflow. diff --git a/src/__tests__/generate-cli-notes.test.ts b/src/__tests__/generate-cli-notes.test.ts new file mode 100644 index 000000000..cb93372ef --- /dev/null +++ b/src/__tests__/generate-cli-notes.test.ts @@ -0,0 +1,238 @@ +import { execFileSync, spawnSync } from "node:child_process" +import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs" +import { tmpdir } from "node:os" +import { dirname, join, resolve } from "node:path" + +import { describe, expect, it, onTestFinished } from "vitest" + +/** + * Behavioral spec for the CLI release-notes generator + * (.github/scripts/generate-cli-notes.sh): which commits between two CLI tags + * the notes list. The real script runs under `bash` with real git, in a fresh + * repository per test. + */ + +const SCRIPT_PATH = resolve(import.meta.dirname, "../../.github/scripts/generate-cli-notes.sh") + +const runnerPath = (): string => { + const path = process.env.PATH + + if (!path) throw new Error("the release-notes tests need a PATH") + return path +} + +const cliManifest = ({ version, zod }: { version: string; zod: string }): string => { + const manifest = { name: "vault-cortex", version, dependencies: { zod } } + return `${JSON.stringify(manifest, null, 2)}\n` +} + +/** Shaped like cli/src/env.ts: .env template text inside a template literal, + * where a line starting with # is a comment. */ +const envTemplateModule = ({ comment, setting }: { comment: string; setting: string }): string => { + return `export const TEMPLATE = \`\n${comment}\n${setting}\n\`\n` +} + +/** File contents by repository path; null deletes the file. */ +type FileChanges = Record + +type NotesRun = { status: number | null; stdout: string; stderr: string } + +type CliReleaseRepo = { + commit: (message: string, changes: FileChanges) => void + /** Tags HEAD as cli-v0.2.0 and runs the script for it. */ + generateNotes: () => NotesRun +} + +/** A repository whose first commit holds a small cli/ package and a server + * file, tagged firstCommitTag. */ +const createCliReleaseRepo = ({ + firstCommitTag = "cli-v0.1.0", +}: { firstCommitTag?: string } = {}): CliReleaseRepo => { + // realpath: macOS's tmpdir is a symlink. + const tempDir = realpathSync(mkdtempSync(join(tmpdir(), "generate-cli-notes-"))) + onTestFinished(() => rmSync(tempDir, { recursive: true, force: true })) + const repoDir = join(tempDir, "repo") + + // Built from scratch, so the runner's git config (signing, hooks, diff + // prefixes) never reaches git or the script. + const env = { + PATH: runnerPath(), + HOME: tempDir, + GIT_CONFIG_NOSYSTEM: "1", + GIT_AUTHOR_NAME: "Fixture", + GIT_AUTHOR_EMAIL: "fixture@example.com", + GIT_COMMITTER_NAME: "Fixture", + GIT_COMMITTER_EMAIL: "fixture@example.com", + } + const git = (...args: string[]): string => { + return execFileSync("git", ["-C", repoDir, ...args], { env, encoding: "utf8", stdio: "pipe" }) + } + + const commit = (message: string, changes: FileChanges): void => { + for (const [path, content] of Object.entries(changes)) { + const filePath = join(repoDir, path) + + if (content === null) { + rmSync(filePath) + continue + } + mkdirSync(dirname(filePath), { recursive: true }) + writeFileSync(filePath, content) + } + git("add", "--all") + git("commit", "--quiet", "--allow-empty", "--message", message) + } + + mkdirSync(repoDir) + git("init", "--quiet") + commit("feat: first release", { + "cli/package.json": cliManifest({ version: "0.1.0", zod: "^1.0.0" }), + "cli/README.md": "# vault-cortex\n", + "cli/src/env.ts": envTemplateModule({ + comment: "# Path to the vault", + setting: "VAULT_PATH=/vault", + }), + "cli/src/program.ts": "export const flag = 1\n", + "cli/src/__tests__/program.test.ts": "export const checks = 1\n", + "src/server.ts": "export const server = 1\n", + }) + git("tag", firstCommitTag) + + const generateNotes = (): NotesRun => { + git("tag", "cli-v0.2.0") + const result = spawnSync("bash", [SCRIPT_PATH, "cli-v0.2.0"], { + cwd: repoDir, + env, + encoding: "utf8", + }) + return { status: result.status, stdout: result.stdout, stderr: result.stderr } + } + + return { commit, generateNotes } +} + +/** A commit the notes always list, so a run that lists nothing cannot pass a + * test that expects another commit to be left out. */ +const commitListedCodeChange = (repo: CliReleaseRepo): void => { + repo.commit("feat: add a flag", { "cli/src/program.ts": "export const flag = 2\n" }) +} + +const LISTED_CODE_CHANGE_ONLY = { status: 0, stdout: "- feat: add a flag\n", stderr: "" } + +describe("generate-cli-notes.sh", () => { + describe("listed commits", () => { + it("lists a commit that changes code", () => { + const repo = createCliReleaseRepo() + commitListedCodeChange(repo) + + expect(repo.generateNotes()).toEqual(LISTED_CODE_CHANGE_ONLY) + }) + + it("lists a README change, headings included", () => { + const repo = createCliReleaseRepo() + repo.commit("docs(cli): add a usage heading", { + "cli/README.md": "# vault-cortex\n\n## Usage\n", + }) + + expect(repo.generateNotes()).toEqual({ + status: 0, + stdout: "- docs(cli): add a usage heading\n", + stderr: "", + }) + }) + + it("lists a dependency change in cli/package.json", () => { + const repo = createCliReleaseRepo() + repo.commit("chore(deps): bump zod", { + "cli/package.json": cliManifest({ version: "0.1.0", zod: "^1.1.0" }), + }) + + expect(repo.generateNotes()).toEqual({ + status: 0, + stdout: "- chore(deps): bump zod\n", + stderr: "", + }) + }) + + it("lists a commit that changes comment lines and a code line", () => { + const repo = createCliReleaseRepo() + repo.commit("fix(cli): rename the vault setting", { + "cli/src/env.ts": + "// The local template\n" + + envTemplateModule({ comment: "# Vault folder", setting: "VAULT_DIR=/vault" }), + }) + + expect(repo.generateNotes()).toEqual({ + status: 0, + stdout: "- fix(cli): rename the vault setting\n", + stderr: "", + }) + }) + + it("lists every commit from the first when no earlier CLI tag exists, ignoring server tags", () => { + const repo = createCliReleaseRepo({ firstCommitTag: "v0.1.0" }) + commitListedCodeChange(repo) + + expect(repo.generateNotes()).toEqual({ + status: 0, + stdout: "- feat: add a flag\n- feat: first release\n", + stderr: "", + }) + }) + }) + + describe("left-out commits", () => { + it("leaves out a .ts change made only of comment and blank lines", () => { + const repo = createCliReleaseRepo() + commitListedCodeChange(repo) + repo.commit("docs(cli): reword the template comments", { + "cli/src/env.ts": + "// Written to a new .env file\n" + + "/**\n * The local template.\n */\n" + + envTemplateModule({ + comment: "# Folder that holds your vault", + setting: "VAULT_PATH=/vault\n", + }), + }) + + expect(repo.generateNotes()).toEqual(LISTED_CODE_CHANGE_ONLY) + }) + + it("leaves out a commit that deletes a .ts file holding only comments", () => { + const repo = createCliReleaseRepo() + repo.commit("docs(cli): add a note file", { "cli/src/notes.ts": "// A note\n" }) + commitListedCodeChange(repo) + repo.commit("docs(cli): drop the note file", { "cli/src/notes.ts": null }) + + expect(repo.generateNotes()).toEqual(LISTED_CODE_CHANGE_ONLY) + }) + + it("leaves out a commit that changes only the version in cli/package.json", () => { + const repo = createCliReleaseRepo() + commitListedCodeChange(repo) + repo.commit("release(cli): v0.2.0", { + "cli/package.json": cliManifest({ version: "0.2.0", zod: "^1.0.0" }), + }) + + expect(repo.generateNotes()).toEqual(LISTED_CODE_CHANGE_ONLY) + }) + + it("leaves out a commit that changes only CLI tests", () => { + const repo = createCliReleaseRepo() + commitListedCodeChange(repo) + repo.commit("test(cli): cover the flag", { + "cli/src/__tests__/program.test.ts": "export const checks = 2\n", + }) + + expect(repo.generateNotes()).toEqual(LISTED_CODE_CHANGE_ONLY) + }) + + it("leaves out a commit that changes nothing under cli/", () => { + const repo = createCliReleaseRepo() + commitListedCodeChange(repo) + repo.commit("feat: add a server tool", { "src/server.ts": "export const server = 2\n" }) + + expect(repo.generateNotes()).toEqual(LISTED_CODE_CHANGE_ONLY) + }) + }) +}) From 7e1856719a6f2c4e544e0a718cb36f0d90c468bc Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:49:19 -0400 Subject: [PATCH 2/8] test(release): list the CLI release-notes script in the shell-lint test Co-Authored-By: Claude Opus 5.5 --- scripts/__tests__/lint-shell.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/__tests__/lint-shell.test.ts b/scripts/__tests__/lint-shell.test.ts index c82883b62..e168b428c 100644 --- a/scripts/__tests__/lint-shell.test.ts +++ b/scripts/__tests__/lint-shell.test.ts @@ -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", From 4e0a53ed1fa154c3d7932d3add6a66ce30347519 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:10:59 -0400 Subject: [PATCH 3/8] fix(review): read quoted and spaced file names in CLI notes headers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A file name with a space gets a trailing tab in the diff header, and a name with special characters is quoted, so neither matched the header pattern. The path then kept the previous file's value: a comment-only change to such a .ts file was listed, and a heading added to such an .md file after a .ts comment was read as a .ts comment and left out. The path now resets per file and the header pattern accepts both forms. Also lists generate-cli-notes.test.ts beside the other release-script tests in AGENTS.md, and says in CONTRIBUTING.md that in files other than .ts every non-blank line counts. Ship-Check: pr-review · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 --- .github/scripts/generate-cli-notes.sh | 12 +++++++++--- AGENTS.md | 5 +++-- CONTRIBUTING.md | 3 ++- src/__tests__/generate-cli-notes.test.ts | 25 ++++++++++++++++++++++++ 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/.github/scripts/generate-cli-notes.sh b/.github/scripts/generate-cli-notes.sh index 2bc9823c1..d69b4adf5 100755 --- a/.github/scripts/generate-cli-notes.sh +++ b/.github/scripts/generate-cli-notes.sh @@ -31,10 +31,16 @@ fi # "-- x") is never read as a header. has_counted_change() { awk ' - /^diff --git / { in_hunk = 0; next } + /^diff --git / { in_hunk = 0; path = ""; next } /^@@/ { in_hunk = 1; next } - !in_hunk && /^--- a\// { path = substr($0, 7); next } - !in_hunk && /^\+\+\+ b\// { path = substr($0, 7); next } + # Git wraps a name holding special characters in quotes, and ends the + # header with a tab when the name holds a space. + !in_hunk && /^(---|\+\+\+) "?[ab]\// { + path = $0 + sub(/^(---|\+\+\+) "?[ab]\//, "", path) + sub(/"?\t?$/, "", path) + next + } !in_hunk { next } /^[+-]/ { content = substr($0, 2) diff --git a/AGENTS.md b/AGENTS.md index 0a18c16ff..242f2ec73 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -957,10 +957,11 @@ Two naming layers — MCP (JSON wire format) and TypeScript (internal): `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 + `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 - the release-notes script, a temporary git repository, and name + the release-notes scripts, 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, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 04b9a9169..ef4c3f10c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -261,7 +261,8 @@ What each release's notes list: - in files under `cli/src/__tests__/` - the `"version"` line of `cli/package.json` (the release bump) - comment lines in `.ts` files, including the `#` lines of the `.env` - templates in `cli/src/env.ts`; every line of `cli/README.md` counts + templates in `cli/src/env.ts`; 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. diff --git a/src/__tests__/generate-cli-notes.test.ts b/src/__tests__/generate-cli-notes.test.ts index cb93372ef..d9f8ef13a 100644 --- a/src/__tests__/generate-cli-notes.test.ts +++ b/src/__tests__/generate-cli-notes.test.ts @@ -169,6 +169,20 @@ describe("generate-cli-notes.sh", () => { }) }) + it("lists a heading added to an .md file git quotes, after .ts comments in the same commit", () => { + const repo = createCliReleaseRepo() + repo.commit("docs(cli): add a guide", { + "cli/src/guide.ts": "// Points to the guide\n", + "cli/ünïcode.md": "# Guide\n", + }) + + expect(repo.generateNotes()).toEqual({ + status: 0, + stdout: "- docs(cli): add a guide\n", + stderr: "", + }) + }) + it("lists every commit from the first when no earlier CLI tag exists, ignoring server tags", () => { const repo = createCliReleaseRepo({ firstCommitTag: "v0.1.0" }) commitListedCodeChange(repo) @@ -198,6 +212,17 @@ describe("generate-cli-notes.sh", () => { expect(repo.generateNotes()).toEqual(LISTED_CODE_CHANGE_ONLY) }) + it.each([ + { label: "a space", path: "cli/src/env notes.ts" }, + { label: "non-ASCII characters", path: "cli/src/ünïcode.ts" }, + ])("leaves out a comment-only change to a .ts file whose name has $label", ({ path }) => { + const repo = createCliReleaseRepo() + commitListedCodeChange(repo) + repo.commit("docs(cli): add a comment file", { [path]: "// A note\n" }) + + expect(repo.generateNotes()).toEqual(LISTED_CODE_CHANGE_ONLY) + }) + it("leaves out a commit that deletes a .ts file holding only comments", () => { const repo = createCliReleaseRepo() repo.commit("docs(cli): add a note file", { "cli/src/notes.ts": "// A note\n" }) From af6b895b1723348211c7e9baeaab26ad4f258abb Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:22:18 -0400 Subject: [PATCH 4/8] style: clarify CLI release-notes rules and their docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - generate-cli-notes.sh: one pathspec shared by git log and diff-tree; drop prefix flags diff-tree already defaults to (plumbing ignores diff.noprefix); state why merges are skipped, why renames are off, that the .ts rule reads only how a line starts, and what the describe fallback is for - cli_release.yml: name --generate-notes, say how to create the release by hand, and when the existing-release skip is reached - CONTRIBUTING.md: CLI bullet puts the counting rule after the skip list and covers moved files and which dependency bumps count - AGENTS.md: list the shell-script test locations as items Ship-Check: code-quality · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 --- .github/scripts/generate-cli-notes.sh | 49 ++++++++++++++-------- .github/workflows/cli_release.yml | 16 +++++--- AGENTS.md | 58 ++++++++++++++------------- CONTRIBUTING.md | 14 ++++--- 4 files changed, 82 insertions(+), 55 deletions(-) diff --git a/.github/scripts/generate-cli-notes.sh b/.github/scripts/generate-cli-notes.sh index d69b4adf5..ace6c0517 100755 --- a/.github/scripts/generate-cli-notes.sh +++ b/.github/scripts/generate-cli-notes.sh @@ -5,19 +5,25 @@ # If previous-tag is omitted, uses the CLI tag before new-tag; with no earlier # CLI tag, lists every commit reachable from new-tag. # -# A commit is listed when its diff under cli/ adds or removes at least one line -# that is not blank and not a comment. Not counted: -# - files under cli/src/__tests__/ +# 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 starting with #, //, /*, * or */ (the # lines are the -# .env template text in cli/src/env.ts) -# Every other non-blank line counts, cli/README.md included: npm shows it. +# - in .ts files, lines that start, after any indent, with #, //, /*, * or */; +# the rule reads only how a line starts, so it skips comment lines, the .env +# template's # lines in cli/src/env.ts (they stay comments in the .env the +# CLI writes), and any code line that starts the same way +# In files other than .ts, cli/README.md included, every non-blank line +# counts: npm shows the README. set -euo pipefail NEW_TAG="${1:?Usage: generate-cli-notes.sh [previous-tag]}" # --match restricts to CLI tags (cli-v0.16.0), so server tags (v0.55.0) never -# end the range. +# end the range. Any describe failure leaves PREV_TAG empty: that is meant for +# a new-tag with no earlier CLI tag, and a new-tag git cannot resolve stops the +# script at git log below. PREV_TAG="${2:-$(git describe --tags --abbrev=0 --match "cli-v*" "$NEW_TAG"^ 2> /dev/null || echo "")}" if [ -n "$PREV_TAG" ]; then @@ -26,15 +32,17 @@ else RANGE="$NEW_TAG" fi -# Exits 0 when the patch on stdin adds or removes a counted line. Headers come -# before a file's first @@ line, so a content line such as "--- x" (a removed -# "-- x") is never read as a header. +# Exits 0 when the patch on stdin adds or removes a counted line. has_counted_change() { awk ' + # The headers of a file come 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 wraps a name holding special characters in quotes, and ends the - # header with a tab when the name holds a space. + # - Git wraps a name holding special characters in quotes, and ends the + # header with a tab when the name holds a space. + # - The /dev/null header of an added or deleted file fails the a/ or b/ + # match, so path comes from the other header. !in_hunk && /^(---|\+\+\+) "?[ab]\// { path = $0 sub(/^(---|\+\+\+) "?[ab]\//, "", path) @@ -54,14 +62,21 @@ has_counted_change() { ' } -# Hex hashes only, so word splitting in the loop below is safe. -COMMIT_HASHES=$(git log --no-merges --format=%H "$RANGE" -- cli/) +CLI_PATHSPEC=(cli/ ':(exclude)cli/src/__tests__/') + +# Hex hashes only, so word splitting in the loop below is safe. Merge commits +# are left out: the commits they merge are listed on their own. +COMMIT_HASHES=$(git log --no-merges --format=%H "$RANGE" -- "${CLI_PATHSPEC[@]}") for commit_hash in $COMMIT_HASHES; do - # Fixed prefixes keep the header match independent of diff.noprefix and - # diff.mnemonicPrefix; --text keeps a binary change countable. + # - --no-renames: a moved file shows as removed and re-added, so a move + # counts as a change. + # - --text: a binary file prints its content as lines, so its change can + # count. + # diff-tree is plumbing, so diff.noprefix and the prefix settings never + # change the a/ and b/ headers has_counted_change reads. PATCH=$(git diff-tree -p --root --no-commit-id --no-renames --text -U0 \ - --src-prefix=a/ --dst-prefix=b/ "$commit_hash" -- cli/ ':(exclude)cli/src/__tests__/') + "$commit_hash" -- "${CLI_PATHSPEC[@]}") if has_counted_change <<< "$PATCH"; then git show -s --format='- %s' "$commit_hash" fi diff --git a/.github/workflows/cli_release.yml b/.github/workflows/cli_release.yml index baeeb96eb..a8c58877f 100644 --- a/.github/workflows/cli_release.yml +++ b/.github/workflows/cli_release.yml @@ -219,18 +219,22 @@ jobs: echo "\`\`\`" } >> "$GITHUB_STEP_SUMMARY" - # --latest=false keeps the "Latest" badge on server releases. Notes - # list only commits that change what a CLI user gets (rules in - # generate-cli-notes.sh) — 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 diff --git a/AGENTS.md b/AGENTS.md index 242f2ec73..db7954c0a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -943,33 +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`, `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 - the release-notes scripts, 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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ef4c3f10c..067d339a7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -255,14 +255,18 @@ What each release's notes list: has deployers copy), or files under `.claude/`, `.devin/` or `.husky/` - **CLI releases** (`.github/scripts/generate-cli-notes.sh`): every commit - that changes `cli/`, dependency bumps included, except commits whose added - and removed lines under `cli/` are all: + 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) - - comment lines in `.ts` files, including the `#` lines of the `.env` - templates in `cli/src/env.ts`; in other files, `cli/README.md` included, - every non-blank line counts + - lines in `.ts` files that start, after any indent, with `#`, `//`, `/*` + or `*`: comment lines, and the `#` lines of the `.env` templates in + `cli/src/env.ts` + + In files other than `.ts`, `cli/README.md` included, every non-blank line + counts. A moved file counts as removing and re-adding all of its lines. See the [DEPLOY.md CI/CD section](./DEPLOY.md#cicd) for details on each workflow. From fec72abf1e66b0b1e784dbfab0d1378cd10b96d8 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:30:10 -0400 Subject: [PATCH 5/8] test(ci): leave out a commit that adds comments to two .ts files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The patch reader resets its hunk state at each file's diff line, so the next file's headers are not read as changed lines. No test covered a comment-only commit touching more than one file; removing the reset left every test passing while such a commit was listed in the CLI notes. Ship-Check: test-audit · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 --- src/__tests__/generate-cli-notes.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/__tests__/generate-cli-notes.test.ts b/src/__tests__/generate-cli-notes.test.ts index d9f8ef13a..4166cfa10 100644 --- a/src/__tests__/generate-cli-notes.test.ts +++ b/src/__tests__/generate-cli-notes.test.ts @@ -212,6 +212,19 @@ describe("generate-cli-notes.sh", () => { expect(repo.generateNotes()).toEqual(LISTED_CODE_CHANGE_ONLY) }) + it("leaves out a commit that adds comment lines to two .ts files", () => { + const repo = createCliReleaseRepo() + commitListedCodeChange(repo) + repo.commit("docs(cli): explain the template and the flag", { + "cli/src/env.ts": + "// Written to a new .env file\n" + + envTemplateModule({ comment: "# Path to the vault", setting: "VAULT_PATH=/vault" }), + "cli/src/program.ts": "// Read by the start command\nexport const flag = 2\n", + }) + + expect(repo.generateNotes()).toEqual(LISTED_CODE_CHANGE_ONLY) + }) + it.each([ { label: "a space", path: "cli/src/env notes.ts" }, { label: "non-ASCII characters", path: "cli/src/ünïcode.ts" }, From ba5f9a9b0c1c5dd58e1a2896ebcc513df86b549f Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:43:36 -0400 Subject: [PATCH 6/8] ci(release): keep CLI notes correct under user git config and CRLF MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - git log and git show pass --no-show-signature, so a log.showSignature setting no longer adds lines that break the hash loop (exit 128) or land in the notes when the release commands are run by hand. - A whitespace-only line in a CRLF file counts as blank. - The workflow runs the script with bash, like the server release scripts. - Comments and CONTRIBUTING.md state that the rules still apply with no earlier tag and to commits a merge brings in, and that code lines starting like a comment (the bin.ts shebang) are skipped too. Ship-Check: bug-check · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 --- .github/scripts/generate-cli-notes.sh | 17 ++++++++++------- .github/workflows/cli_release.yml | 2 +- CONTRIBUTING.md | 4 ++-- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/.github/scripts/generate-cli-notes.sh b/.github/scripts/generate-cli-notes.sh index ace6c0517..419c3bfef 100755 --- a/.github/scripts/generate-cli-notes.sh +++ b/.github/scripts/generate-cli-notes.sh @@ -3,7 +3,7 @@ # `- ` line each, newest first. # Usage: generate-cli-notes.sh [previous-tag] # If previous-tag is omitted, uses the CLI tag before new-tag; with no earlier -# CLI tag, lists every commit reachable from new-tag. +# CLI tag, checks every commit reachable from new-tag. # # A commit is listed when its diff under cli/ adds or removes a line that none # of these rules skips: @@ -12,7 +12,7 @@ # - the "version" line of cli/package.json (the release bump) # - in .ts files, lines that start, after any indent, with #, //, /*, * or */; # the rule reads only how a line starts, so it skips comment lines, the .env -# template's # lines in cli/src/env.ts (they stay comments in the .env the +# templates' # lines in cli/src/env.ts (they stay comments in the .env the # CLI writes), and any code line that starts the same way # In files other than .ts, cli/README.md included, every non-blank line # counts: npm shows the README. @@ -52,7 +52,7 @@ has_counted_change() { !in_hunk { next } /^[+-]/ { content = substr($0, 2) - if (content ~ /^[ \t]*$/) next + if (content ~ /^[ \t\r]*$/) next if (path == "cli/package.json" && content ~ /^[ \t]*"version"[ \t]*:/) next if (path ~ /\.ts$/ && content ~ /^[ \t]*(#|\/\/|\/\*|\*)/) next found = 1 @@ -64,9 +64,12 @@ has_counted_change() { CLI_PATHSPEC=(cli/ ':(exclude)cli/src/__tests__/') -# Hex hashes only, so word splitting in the loop below is safe. Merge commits -# are left out: the commits they merge are listed on their own. -COMMIT_HASHES=$(git log --no-merges --format=%H "$RANGE" -- "${CLI_PATHSPEC[@]}") +# Hex hashes only, so word splitting in the loop below is safe. +# - --no-merges: the commits a merge brings in are checked on their own. +# - --no-show-signature (here and on git show below): a log.showSignature +# setting would add signature lines to the output. +COMMIT_HASHES=$(git log --no-merges --no-show-signature --format=%H "$RANGE" \ + -- "${CLI_PATHSPEC[@]}") for commit_hash in $COMMIT_HASHES; do # - --no-renames: a moved file shows as removed and re-added, so a move @@ -78,6 +81,6 @@ for commit_hash in $COMMIT_HASHES; do PATCH=$(git diff-tree -p --root --no-commit-id --no-renames --text -U0 \ "$commit_hash" -- "${CLI_PATHSPEC[@]}") if has_counted_change <<< "$PATCH"; then - git show -s --format='- %s' "$commit_hash" + git show -s --no-show-signature --format='- %s' "$commit_hash" fi done diff --git a/.github/workflows/cli_release.yml b/.github/workflows/cli_release.yml index a8c58877f..414220b26 100644 --- a/.github/workflows/cli_release.yml +++ b/.github/workflows/cli_release.yml @@ -240,7 +240,7 @@ jobs: exit 0 fi { - .github/scripts/generate-cli-notes.sh "cli-v$NEXT" + 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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 067d339a7..d8d3c1b1c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -262,8 +262,8 @@ What each release's notes list: - 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 `*`: comment lines, and the `#` lines of the `.env` templates in - `cli/src/env.ts` + or `*`: comment lines, the `#` lines of the `.env` templates in + `cli/src/env.ts`, and any code line that starts the same way In files other than `.ts`, `cli/README.md` included, every non-blank line counts. A moved file counts as removing and re-adding all of its lines. From bdb123d8835e85623fb71078041ccb3a1fed0559 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:54:01 -0400 Subject: [PATCH 7/8] ci(release): list commits that add or rename a commented-out setting in CLI notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A # line shaped like a setting (# KEY=value, the key in capitals) in a .ts file now counts. The .env templates in cli/src/env.ts ship optional settings commented out for the user to uncomment, so a commit that adds or renames one changes what the CLI writes. Rewording a template's comment text is still left out. Ship-Check: bug-check · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 --- .github/scripts/generate-cli-notes.sh | 10 +++++++--- CONTRIBUTING.md | 7 +++++-- src/__tests__/generate-cli-notes.test.ts | 18 +++++++++++++++++- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/.github/scripts/generate-cli-notes.sh b/.github/scripts/generate-cli-notes.sh index 419c3bfef..e100ea68a 100755 --- a/.github/scripts/generate-cli-notes.sh +++ b/.github/scripts/generate-cli-notes.sh @@ -11,9 +11,11 @@ # - 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 */; -# the rule reads only how a line starts, so it skips comment lines, the .env -# templates' # lines in cli/src/env.ts (they stay comments in the .env the -# CLI writes), and any code line that starts the same way +# the rule reads only how a line starts, so it skips comment lines, the +# comment text of the .env templates in cli/src/env.ts, and any code line +# that starts the same way. A # line shaped like a setting (# KEY=value, +# the key in capitals) still counts: the templates ship optional settings +# commented out, for the user to uncomment. # In files other than .ts, cli/README.md included, every non-blank line # counts: npm shows the README. @@ -54,6 +56,8 @@ has_counted_change() { 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 found = 1 exit diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d8d3c1b1c..3c73c2b35 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -262,8 +262,11 @@ What each release's notes list: - 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 `*`: comment lines, the `#` lines of the `.env` templates in - `cli/src/env.ts`, and any code line that starts the same way + or `*`: comment lines, the comment text of the `.env` templates in + `cli/src/env.ts`, and any code line that starts the same way. A `#` line + shaped like a setting (`# KEY=value`, the key in capitals) still counts: + the templates ship optional settings commented out, for the user to + uncomment. In files other than `.ts`, `cli/README.md` included, every non-blank line counts. A moved file counts as removing and re-adding all of its lines. diff --git a/src/__tests__/generate-cli-notes.test.ts b/src/__tests__/generate-cli-notes.test.ts index 4166cfa10..af3d05d79 100644 --- a/src/__tests__/generate-cli-notes.test.ts +++ b/src/__tests__/generate-cli-notes.test.ts @@ -169,6 +169,22 @@ describe("generate-cli-notes.sh", () => { }) }) + it("lists a commented-out setting added to a .ts template", () => { + const repo = createCliReleaseRepo() + repo.commit("feat(cli): offer the log level setting", { + "cli/src/env.ts": envTemplateModule({ + comment: "# Path to the vault", + setting: "VAULT_PATH=/vault\n\n# How much the server logs\n# LOG_LEVEL=info", + }), + }) + + expect(repo.generateNotes()).toEqual({ + status: 0, + stdout: "- feat(cli): offer the log level setting\n", + stderr: "", + }) + }) + it("lists a heading added to an .md file git quotes, after .ts comments in the same commit", () => { const repo = createCliReleaseRepo() repo.commit("docs(cli): add a guide", { @@ -204,7 +220,7 @@ describe("generate-cli-notes.sh", () => { "// Written to a new .env file\n" + "/**\n * The local template.\n */\n" + envTemplateModule({ - comment: "# Folder that holds your vault", + comment: "# Folder that holds your vault (default: VAULT_PATH=/vault)", setting: "VAULT_PATH=/vault\n", }), }) From 23ec04dd6f0f0141e0b8377f1561606ebbbd2de4 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:58:46 -0400 Subject: [PATCH 8/8] ci(release): drop CLI notes options that change nothing --no-renames and --text change no output (diff-tree detects no renames unless asked, and cli/ has no binary files), --no-show-signature guards a git setting no runner has, and the optional previous-tag argument is never passed. The notes are identical for all 55 CLI tags before and after. Co-Authored-By: Claude Opus 5.5 --- .github/scripts/generate-cli-notes.sh | 61 +++++++++------------------ CONTRIBUTING.md | 14 +++--- 2 files changed, 27 insertions(+), 48 deletions(-) diff --git a/.github/scripts/generate-cli-notes.sh b/.github/scripts/generate-cli-notes.sh index e100ea68a..4c9202d0a 100755 --- a/.github/scripts/generate-cli-notes.sh +++ b/.github/scripts/generate-cli-notes.sh @@ -1,32 +1,24 @@ #!/usr/bin/env bash -# List the commits between two CLI tags that change what a CLI user gets, one -# `- ` line each, newest first. -# Usage: generate-cli-notes.sh [previous-tag] -# If previous-tag is omitted, uses the CLI tag before new-tag; with no earlier -# CLI tag, checks every commit reachable from new-tag. +# List the commits since the previous CLI tag that change what a CLI user gets, +# one `- ` line each, newest first. +# Usage: generate-cli-notes.sh # # 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 */; -# the rule reads only how a line starts, so it skips comment lines, the -# comment text of the .env templates in cli/src/env.ts, and any code line -# that starts the same way. A # line shaped like a setting (# KEY=value, -# the key in capitals) still counts: the templates ship optional settings -# commented out, for the user to uncomment. -# In files other than .ts, cli/README.md included, every non-blank line -# counts: npm shows the README. +# - 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 [previous-tag]}" -# --match restricts to CLI tags (cli-v0.16.0), so server tags (v0.55.0) never -# end the range. Any describe failure leaves PREV_TAG empty: that is meant for -# a new-tag with no earlier CLI tag, and a new-tag git cannot resolve stops the -# script at git log below. -PREV_TAG="${2:-$(git describe --tags --abbrev=0 --match "cli-v*" "$NEW_TAG"^ 2> /dev/null || echo "")}" +NEW_TAG="${1:?Usage: generate-cli-notes.sh }" +# --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}" @@ -37,14 +29,13 @@ fi # Exits 0 when the patch on stdin adds or removes a counted line. has_counted_change() { awk ' - # The headers of a file come before its first @@ line, so a content line - # such as "--- x" (a removed "-- x") is never read as a header. + # 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 wraps a name holding special characters in quotes, and ends the - # header with a tab when the name holds a space. - # - The /dev/null header of an added or deleted file fails the a/ or b/ - # match, so path comes from the other header. + # 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) @@ -68,23 +59,13 @@ has_counted_change() { CLI_PATHSPEC=(cli/ ':(exclude)cli/src/__tests__/') -# Hex hashes only, so word splitting in the loop below is safe. -# - --no-merges: the commits a merge brings in are checked on their own. -# - --no-show-signature (here and on git show below): a log.showSignature -# setting would add signature lines to the output. -COMMIT_HASHES=$(git log --no-merges --no-show-signature --format=%H "$RANGE" \ - -- "${CLI_PATHSPEC[@]}") +# 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 - # - --no-renames: a moved file shows as removed and re-added, so a move - # counts as a change. - # - --text: a binary file prints its content as lines, so its change can - # count. - # diff-tree is plumbing, so diff.noprefix and the prefix settings never - # change the a/ and b/ headers has_counted_change reads. - PATCH=$(git diff-tree -p --root --no-commit-id --no-renames --text -U0 \ - "$commit_hash" -- "${CLI_PATHSPEC[@]}") + PATCH=$(git diff-tree -p --root --no-commit-id -U0 "$commit_hash" -- "${CLI_PATHSPEC[@]}") if has_counted_change <<< "$PATCH"; then - git show -s --no-show-signature --format='- %s' "$commit_hash" + git show -s --format='- %s' "$commit_hash" fi done diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3c73c2b35..a16ff90ce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -262,14 +262,12 @@ What each release's notes list: - 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 `*`: comment lines, the comment text of the `.env` templates in - `cli/src/env.ts`, and any code line that starts the same way. A `#` line - shaped like a setting (`# KEY=value`, the key in capitals) still counts: - the templates ship optional settings commented out, for the user to - uncomment. - - In files other than `.ts`, `cli/README.md` included, every non-blank line - counts. A moved file counts as removing and re-adding all of its lines. + 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.