docs: keep a CHANGELOG.md, with one entry file per PR

Release notes were written by hand on GitHub at tag time, so the contents
of a stacked release had to be pieced together from every PR, and output
changes that scripts and agents rely on were easy to miss.

- CHANGELOG.md backfills every published release (0.2.0 to 0.4.0) from its
  GitHub notes, verbatim.
- A PR adds changes/unreleased/<branch-name>.md instead of editing
  CHANGELOG.md, so stacked PRs never conflict. changes/README.md gives the
  sections, including Output Changes for JSON, MCP and error-code changes.
- scripts/changelog.sh: `check` validates entries and CHANGELOG.md,
  `release X.Y.Z` folds the entries into a new section (ordered by PR), and
  `notes X.Y.Z` prints a section as GitHub release notes.
- The release workflow sets the GitHub notes from the tag's section.
- New flake check `changelog` runs scripts/changelog-test.sh; both scripts
  are shellchecked.
- Entries for 0.5.0: #73, #74, #75, #78, #79, #80, #81 and this PR.

Claude-Session: https://claude.ai/code/session_01RqPPVeVDrnLoP7NUZmLRxY
This commit is contained in:
Rasyidan Akbar F. 2026-11-04 19:48:58 +07:00
commit 809966925e
21 changed files with 942 additions and 13 deletions

View file

@ -100,11 +100,14 @@ jobs:
permissions:
contents: write
steps:
- name: Check out install script
- name: Check out install script and changelog
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref }}
sparse-checkout: install.sh
sparse-checkout: |
install.sh
CHANGELOG.md
scripts/changelog.sh
sparse-checkout-cone-mode: false
path: src
@ -136,3 +139,22 @@ jobs:
idx-mcp-darwin-x64
SHA256SUMS
src/install.sh
# The notes are the tag's CHANGELOG.md section. A tag from before
# CHANGELOG.md (or without its version in it) keeps the notes it has.
- name: Read release notes from CHANGELOG.md
id: notes
env:
CHANGELOG_ROOT: src
run: |
if [ -f src/scripts/changelog.sh ] && bash src/scripts/changelog.sh notes "$RELEASE_TAG" > notes.md; then
echo "found=true" >> "$GITHUB_OUTPUT"
else
echo "::warning::${RELEASE_TAG} has no CHANGELOG.md section at that tag; release notes left unchanged"
fi
- name: Publish release notes
if: steps.notes.outputs.found == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: gh release edit "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --notes-file notes.md

View file

@ -35,6 +35,7 @@ Docs:
- `docs/DESIGN.md`: idx-core / idx-mcp design and parity gates
- `docs/IDX_NEWS.md`: news sources research (IDX disclosures, Google News, Yahoo) and a proposed publisher design
- `ROADMAP.md`: open work
- `CHANGELOG.md` / `changes/README.md`: released changes / how a PR adds its entry
## Rules
1. Keep data access provider-driven; no ad hoc fetches in `cli/`.
@ -49,6 +50,9 @@ Docs:
7. Output parity is a contract: never edit golden files to make a change pass.
After changing CLI behavior, run the live check and report its summary;
report IDX `403`s explicitly rather than counting them as passes.
8. A PR that changes what users or agents see adds
`changes/unreleased/<branch-name>.md` (format in `changes/README.md`);
`Output Changes` lists any change to JSON, MCP results or error codes.
## Verify
```bash

295
CHANGELOG.md Normal file
View file

@ -0,0 +1,295 @@
# Changelog
Notable changes to `idx` (idx-cli), `idx-mcp` and the `idx-core` library, newest
first. Versions follow [Semantic Versioning](https://semver.org); before 1.0, a
minor version (0.4.0, 0.5.0) adds features and a patch version (0.3.3) fixes
them. Each version's section is also its GitHub release notes.
Changes that are merged but not yet released live in
[`changes/unreleased/`](changes/unreleased/), one file per pull request, and are
folded in here at release time. See [`changes/README.md`](changes/README.md).
## [0.4.0] - 2026-10-19
Broker summaries, bandarmology and foreign flow, from public data with no login or key.
### New Features
- `idx stocks broker` and the MCP tool `get_broker_summary` show a stock's top 10 buying and selling brokers over a day or a date range, with totals. You can filter by investor side and board. The data comes from Indo Premier (IPOT). (#62, #63)
- `idx stocks bandar` and `analyze_bandar` read broker concentration as buy minus sell value for the top 1, 3, 5 and 10 brokers, and give a signal from big accumulation to big distribution. Several symbols make a ranked screen that `stocks screen` can pipe into. The thresholds are heuristics. (#64)
- `idx stocks flow` and `get_foreign_flow` show foreign buying, selling and net value session by session. The data comes from IPOT by default; `--source idx` uses IDX's trading summary for exact share counts. (#65, #68)
- `--investor foreign|domestic` now reads one investor side in `broker` and `bandar`, with that side's buy and sell totals. (#67)
### Bug Fixes
- Quotes and IDX screen rows now carry `behind_session`: true when the last trade is from before the latest IDX session. The quote table adds a note for these quotes, so a provider still showing the previous close after the session is no longer passed off as the latest price. (#71, #50)
### Improvements
- IPOT broker pages are parsed with html5ever and accepted only in IPOT's exact markup. A page that differs is rejected with an error and never misread. (#70)
- IPOT pages for dates other than those requested are rejected before caching, and so are IDX trading summaries with no rows for the requested session. (#70)
- Bandar readings use checked arithmetic, and the table says "No trades." only when no broker traded. (#70)
- Flow and broker data share caches. Past sessions are cached for days and today for 5 minutes, and offline `latest` lookups skip uncached days. (#62, #65, #68)
- The `analyze_bandar` `top` parameter lists its allowed values (1, 3, 5, 10) in the MCP schema. (#70)
### Notes
- `stocks flow --source idx` needs curl-impersonate. The default IPOT source does not.
- New dependency: `scraper` 0.27 (html5ever). Servo's `selectors`, `cssparser` and `dtoa-short` are MPL-2.0 and used unmodified.
- JSON output for quotes and screens adds the `behind_session` field; no existing field changed.
**Full Changelog**: https://github.com/0xrsydn/idx-cli/compare/v0.3.3...v0.4.0
## [0.3.3] - 2026-10-18
**Upgrade from 0.3.2**: ownership `changes` could report the wrong direction for investors with several accounts, and invalid dates were answered instead of rejected.
### Bug Fixes
- `ownership changes` and the `get_ownership_changes` tool compare an investor's total stake per ticker: KREN's KRESNA PRIMA INVEST, which added a second account, was reported as a decrease (944 → 401 bps) instead of an increase (944 → 1345). A corrected KSEI file for a month now replaces the original instead of mixing with it, and a file carrying several report dates compares each date on its own rows. (#53)
- `get_ownership_changes` rejects dates that are not imported releases, are reversed, or are malformed; an empty `from` used to report every holder as new. (#53)
- Investor search finds aliases, the raw names KSEI prints: `PT ASTRA INTERNATIONAL TBK` now finds ASTRA INTERNATIONAL. An empty investor name is an error instead of an arbitrary match. (#53)
- `ownership graph` reaches every connected holder (holders whose id was a prefix of another's were dropped) and no longer slows down exponentially with depth. (#53)
- `ownership sync` replaces the database in a single step, so an interrupted sync can no longer leave it missing; a snapshot whose holdings are missing is rejected before it replaces good data. A backup left by an interrupted older sync is restored, not overwritten. (#54)
- Requests to MSN and Yahoo give up after 30 seconds instead of hanging, and ownership commands wait at most 30 seconds for another process's lock, so a stalled call cannot block every MCP tool. Downloads stop at 256 MiB as they arrive, including compressed ones. (#55)
- Escape sequences in provider text (news titles, insights, errors, names) are neutralized before reaching the terminal. Descriptions and URLs with non-ASCII characters no longer crash table output. (#56)
- In JSON mode, argument errors and warnings are JSON, so stderr stays parseable. `idx config` honors `-o json`; piped output stays plain text. (#56)
- A "healthy" fundamental rating needs valuation and risk data; negative equity counts as high risk. (#57)
- `install.sh` installs the newest release even when the releases API answers on one line. (#58)
- `cache clear` removes symlinks without following them out of the cache. (#56)
### Improvements
- `zip` moves off a yanked release, to 9.0.4. (#52, #51)
- Price data outside any plausible range is treated as missing instead of overflowing. (#55)
- The snapshot publisher handles report titles with line breaks, checks its manifest before upload, and copies its database safely. (#58)
- Release workflow actions are pinned to commit SHAs, and only the publishing job can write to releases. (#58)
- Help text covers config keys and output, `ownership changes` date rules, and alias search. (#56)
### Testing
- Every fix has a regression test that fails on 0.3.2. The agent smoke test passes in Claude Code, Codex, pi, Hermes Agent, and opencode 1 and 2 through `npx -y idx-mcp`, ten cases each. Found by a full-repo audit with Codex (gpt-6.1-sol, xhigh) and re-reviewed until clean; Pullfrog's PR review found four more, fixed here.
**Full Changelog**: https://github.com/0xrsydn/idx-cli/compare/v0.3.2...v0.3.3
## [0.3.2] - 2026-10-16
**Upgrade from 0.3.1** if you run idx-mcp through `npx`, Hermes Agent, pnpm, or bun: 0.3.1 does not start there on npm 12.
### New Features
- News items carry `relevance`: `company` items about this ticker come first, newest first, then `market` items with general market news. The `stocks news` table gains an `ABOUT` column, and the `get_company` news section returns the same labels. (#47)
- `get_ownership_changes` pages through large diffs with `offset` / `next_offset` and returns a `summary` over every row (counts by change type and the tickers with the most changes), so agents can answer overview questions without fetching every row. Rows are ordered by largest change first. (#48)
### Bug Fixes
- `npx -y idx-mcp` and `npx idx-cli` work on npm 12, pnpm 10+, and bun, which skip install scripts: the launcher now downloads the binary on first run instead of exiting with `installed binary not found`, which MCP clients reported as "Connection closed". This affected every Hermes Agent user. (#41, #42)
- A first run with a read-only package directory, such as a root-owned `sudo npm install -g` on npm 12, installs the binary to a per-user cache instead of failing with `EACCES`. (#46)
- Stopping the launcher during the first-run download (`SIGTERM`, `SIGHUP`, or `SIGINT` sent to its PID) stops the installer, removes the partial file, and exits by the same signal. (#46)
- A local `IDX_BINARY_URL` binary is verified against `IDX_BINARY_SHA256` after it is copied, so the installed bytes are the verified ones. (#46)
- The user-cache fallback runs only from an absolute cache root, so an invalid `XDG_CACHE_HOME` or an empty `HOME` can never make the launcher run a binary from the current directory. (#46)
- News is ordered by publish time rather than by timestamp text, so fractional seconds and time-zone offsets no longer move older items ahead. (#47)
### Improvements
- `get_ownership_changes` returns at most 300 rows per page (previously 2000, about 200 KB), which no longer overflows agents' tool output. The over-limit error points to `offset` and `summary`. (#48)
- News is fetched wider (at least 50 items), so this company's articles further down MSN's feed are found. MSN's stock tags are combined with the ticker and company name to find them. (#47)
### Testing
- The agent smoke test runs Claude Code, Codex, pi, Hermes Agent, and opencode 1 and 2 against `npx -y idx-mcp`, each from a cold npm cache in an empty directory. Each agent must install the binary itself, and ten cases call every MCP tool except `ownership_sync`, which needs `--allow-sync`. All six harnesses pass on this release. (#44)
- The npm smoke test covers installs without postinstall, read-only installs, and a planted binary that must never run. (#46)
**Full Changelog**: https://github.com/0xrsydn/idx-cli/compare/v0.3.1...v0.3.2
## [0.3.1] - 2026-10-15
Security and data-safety fixes for 0.3.0. **Upgrade from 0.3.0**; 0.3.0 is yanked on crates.io and deprecated on npm.
### Security
- **Path traversal via ticker symbols.** A ticker such as `../../x` was used directly in cache file names, so idx could read or delete `.json` files outside its cache directory. Through `idx-mcp`, a single tool call from a prompt-injected agent was enough. Tickers are now validated (`invalid ticker symbol`, exit 2) and cache file names are encoded.
### Ownership database
- A cross-process lock (`<db>.lock`) now guards the ownership database: sync installs, imports and entity edits take it exclusively, reads take it shared. Before, a sync could replace the database under an active import or reader and lose data.
- Sync downloads without holding the lock, and downloads now time out (15 s connect, 10 min total) instead of hanging.
- A symlinked `ownership.db_path` keeps its symlink: sync updates the real file and shares its lock.
- A rebuilt snapshot never replaces a local database that holds more releases than it does.
### MCP
- Input limits: at most 20 symbols per call (duplicates collapsed), caps on `limit`, at most 4 concurrent tool calls. The tool schemas state the limits.
### Robustness
- Atomic cache writes; per-client Yahoo cookie files.
- Size caps on ZIP entries and XLSX workbooks; two parser inputs that could crash or overflow now return errors.
### Other
- Minimum supported Rust version is now 1.89 (for std file locks).
- `tests/agents/agent-smoke.sh`: opt-in smoke test that drives real coding agents (Claude Code, Codex, pi, Hermes, opencode) against idx-mcp.
## [0.3.0] - 2026-10-15
### Highlights
- **New: `idx-mcp`, an MCP server.** It serves the same market, financial and ownership data to MCP clients over stdio, through 11 tools: `get_quotes`, `get_price_history`, `screen_stocks`, `get_company`, `analyze_stock`, `compare_stocks`, `get_shareholders`, `get_investor_holdings`, `search_investors`, `get_ownership_changes`, `ownership_status`.
- Install with `claude mcp add idx -- npx -y idx-mcp`, `cargo install idx-mcp`, or `nix profile install github:0xrsydn/idx-cli#idx-mcp`.
- Live results come as `{data, source, fetched_at, stale}`.
- Errors use the CLI's `{error, code, message}` body.
- The server shares idx's config, cache and ownership database.
- `--allow-sync` adds an `ownership_sync` tool.
- **Agent-native CLI output.** idx now prints JSON when stdout is not a terminal, and tables when it is. Agents and scripts get structured data without flags. JSON is compact when piped. `-o jsonl` prints one record per line. No color codes appear when piped.
- **`idx-core` library.** All data access, caching and analysis now live in a separate crate shared by `idx` and `idx-mcp`.
### Breaking changes
- **Default output when piped is JSON.** Use `-o table`, `IDX_OUTPUT=table`, or `general.output = "table"` to get tables back. A config created by `idx config init` before 0.3 pins `output = "table"`; run `idx config set general.output auto` to switch to the new behavior.
- **Exit codes by error class.** Before 0.3, every error exited with 1.
| Exit | Meaning |
| --- | --- |
| 2 | invalid input or config |
| 3 | not found, no data, or no cached data offline |
| 4 | upstream unavailable; retry later |
| 5 | unsupported |
| 1 | other |
### CLI
- `stocks quote -` and `stocks compare -` read symbols from stdin, separated by whitespace or commas. Example: `idx stocks screen --filter high-dividend | jq -r '.[].symbol' | idx stocks compare -`.
- Financial statement line items come out in a stable order in JSON.
- Fixed: tables with colored cells (signals, change types) had a ragged right border in terminals.
### Ownership
- Fixed: the IDX workbook stores the share code `TRUE` (TRINITI DINAMIK) as a boolean cell, so its holders were imported under ticker `1`.
- The published snapshot for 2026-09-30 has been rebuilt. The rebuild also corrects the bogus August→September changes for `TRUE` and `1`.
- With 0.3, `idx ownership sync` picks up the rebuild automatically.
- On 0.2.x, run `idx ownership sync --force` once.
- Sync now updates a database that is still byte-identical to an earlier build of the same snapshot. The manifest gains an optional `replaces_sqlite_sha256` field, which older clients ignore. A database you imported or edited locally still needs `--force`.
### Distribution
- Release assets add `idx-mcp-<platform>` binaries next to `idx-<platform>`, all listed in `SHA256SUMS`.
- New npm package `idx-mcp`, alongside `idx-cli`. New crates `idx-core` and `idx-mcp`.
- Rust: `idx` builds with 1.85+, and `idx-mcp` needs 1.88+.
### Verification
- 143 golden outputs (every command in table and JSON, plus error cases) are unchanged except for the documented output-default and exit-code changes.
- Live A/B checks against 0.2.4 match.
- Each MCP tool's data equals the CLI's JSON.
## [0.2.4] - 2026-09-26
### Highlights
- **Ownership data is current again.** Since June 2026 IDX publishes the monthly "Pemegang Saham di Atas 1%" list as **XLSX** on the [Data Kepemilikan Saham](https://www.idx.co.id/id/perusahaan-tercatat/data-kepemilikan-saham/) page instead of a PDF announcement. `idx ownership discover` finds it there and `idx ownership import` reads it. The published snapshot (`idx ownership sync`) now covers **2026-06-30, 2026-07-31 and 2026-08-31**.
- **New install channels:** `curl -fsSL https://raw.githubusercontent.com/0xrsydn/idx-cli/main/install.sh | sh` and npm (`npx idx-cli`, `npm i -g idx-cli`). Both use prebuilt binaries with SHA-256 verification.
- **Sync accepts larger snapshots.** v0.2.3 and earlier cap snapshot downloads at 10 MiB. This version accepts up to the manifest's size (at most 1 GiB), which leaves room for more months of history later.
### Ownership
- XLSX parser for the above-1% holder register (#25). Only the exact 12-column layout is accepted, so the daily above-5% and investor-type workbooks are rejected instead of mis-imported. Both the Feb/Mar 2026 `INVESTOR_TYPE` and the later `INVESTOR_CLASSIFICATION` headers are handled.
- Discovery merges the announcement API with the data page (#26). Downloads retry across curl-impersonate browser profiles, since IDX's Cloudflare rejects `curl_chrome142` on the new page.
- `ownership import --url` accepts `.xlsx`.
- Snapshot sync: the download is bounded by the manifest's `size_bytes`, and a body larger than advertised is rejected (#31).
### Snapshot publishing (maintainers)
- `build-latest-ownership-snapshot.sh --history <n>` includes earlier months (#27). `--max-snapshot-bytes` (default 10 MiB) refuses snapshots that released clients can't download (#31).
- Idempotent, packaged publisher: `nix build .#ownership-publisher` gives `idx-ownership-publish` and `idx-ownership-freshness`. Every run ends in one `RESULT:` line (#29). Upload verification and partial-publish recovery (#30).
### Distribution
- Release workflow builds static musl Linux (x64, arm64) and macOS (arm64, x64) binaries, plus `SHA256SUMS` and `install.sh` (#22, #24).
- npm wrapper with checksum-verified postinstall (#22). pnpm 10+ and bun need `--allow-build` / `--trust`.
### Compatibility
- The snapshot manifest is still schema v1. v0.2.3 clients keep syncing as long as snapshots stay at or below 10 MiB, which the publisher enforces.
- Discover JSON gains `format` and `as_of_date` fields; existing fields are unchanged.
## [0.2.3] - 2026-08-01
### Fixes
- handles the new spaced-header KSEI PDF layout (SHARE CODE, INVESTOR CLASSIFICATION) that broke schema classification
- normalizes spaced header text before schema classification so both underscore (SHARE_CODE) and spaced layouts parse
- adds INVESTOR_CLASSIFICATION to schema markers and header labels
- satisfies clippy 1.97 lints that failed the ubuntu CI step (filter, sort_by_key(Reverse), ?)
### Scope
- published from commit `603c0e4` (main, after PR #21 merge)
- adds a test fixture from the live June 2026 KSEI PDF and a spaced-header test case
- all 222 tests pass (143 unit + 79 integration), clippy + fmt clean
- snapshot data asset unchanged: latest KSEI above-1% report is still as_of 2026-05-29; the parser fix enables future June/July imports once IDX publishes them
## [0.2.2] - 2026-04-06
### Fixes
- handles April 2026 KSEI PDFs that omit zero-valued scrip columns on some holder rows
- defaults missing component share fields to zero while still requiring total shares
- restores successful import and sync against the live April 2026 KSEI ownership file format
### Scope
- published from commit `04eaa75`
- includes the `0.2.2` version bump that was already shipped to crates.io
- this release object backfills the missing GitHub tag/release for that published crate version
## [0.2.1] - 2026-04-06
### Highlights
- adds `idx ownership discover` to locate the latest supported IDX above-1 shareholder register PDF
- adds `idx ownership import --url <pdf-url>` for direct remote import
- adds `idx ownership sync` for maintained snapshot-based bootstrap and refresh
- adds local archive ZIP/TXT ingest as a fallback backstop path
- expands smoke coverage and fixture-backed ownership tests
### Notes
- `stocks` remains MSN-first, with Yahoo retained for history fallback
- this release is being backfilled on GitHub so release objects match the existing tag history
## [0.2.0] - 2026-03-07
### 🔍 Ownership Intelligence Module
First-of-its-kind local ownership data for all IDX-listed stocks. Import KSEI's ≥1% shareholder registry, query cross-holdings, track concentration — all offline from a single SQLite file.
#### New Commands
```bash
# Import KSEI ownership PDF (monthly release from idx.co.id)
idx ownership import --file ownership_202603.pdf
idx ownership import --url https://idx.co.id/...pdf
# Query any ticker's shareholders
idx ownership ticker BBCA
idx ownership ticker GOTO --source ksei
# Who else does this investor hold?
idx ownership entity "GOVERNMENT OF NORWAY"
# Fuzzy search entities
idx ownership search "salim"
# Cross-holding analysis
idx ownership cross-holders --top 20
idx ownership concentration --by hhi --top 10
# Ownership graph traversal
idx ownership graph BBCA --depth 2
idx ownership graph BBCA --format dot # Graphviz export
# Diff between monthly releases
idx ownership changes --from 2026-01-27 --to 2026-02-27
# Entity resolution tools
idx ownership resolve unresolved
idx ownership resolve map "PT ASTRA INTL" "ASTRA INTERNATIONAL"
idx ownership resolve merge 42 99
```
#### Highlights
- **KSEI PDF parser** — mutool stext + quick-xml, 99.2% accuracy across 73-page PDFs
- **7,256 ownership rows** across **955 tickers** from a single import
- **FTS5 trigram search** on 5,175 entities
- **Graph traversal** with recursive CTEs (ASCII tree + Graphviz DOT)
- **Entity resolution CLI** — manual mapping + merge for deduplication
- **SQLite-backed** — single `ownership.db` file, zero infrastructure
- **Feature-gated** — `--features ownership` (default-on)
#### Also in this release
- Hybrid history provider: MSN default + Yahoo fallback (`--history-provider auto|yahoo|msn`)
- Bing Finance institutional ownership API client (5 endpoints, ready for integration)
- `docs/ARCHITECTURE.md` and `docs/CONVENTIONS.md`
#### Stats
- 82 tests passing (unit + integration)
- QA validated: 41/41 pass across 20 sample tickers
## [0.1.0] - 2026-03-05
The first tagged version: quotes, history, fundamentals and screens from MSN and Yahoo. It was published without release notes.

View file

@ -193,6 +193,7 @@ User docs, MCP setup per agent, and agent-readable `llms.txt`:
- [docs/OWNERSHIP_PUBLISH.md](docs/OWNERSHIP_PUBLISH.md): maintainer snapshot publishing
- [docs/SMOKE.md](docs/SMOKE.md): test layout and the live provider drift check
- [ROADMAP.md](ROADMAP.md): planned work
- [CHANGELOG.md](CHANGELOG.md): what each release changed; unreleased entries in [changes/](changes/README.md)
## Development

39
changes/README.md Normal file
View file

@ -0,0 +1,39 @@
# Changelog entries
Every pull request that changes what users or agents see adds one file here,
`changes/unreleased/<branch-name>.md`; for example, `fix/mcp-warnings` adds
`changes/unreleased/mcp-warnings.md`. Each PR has its own file, so stacked PRs
never conflict over the changelog. Internal refactors, CI-only and test-only
changes need no entry.
An entry holds `### <section>` headings with one bullet per change. Write each
bullet as one sentence for the user, saying what changed for them, and end it
with the PR number:
```markdown
### Bug Fixes
- `ownership import` reads IDX's register from October 2026 on, which adds a `CONTROLLER_OR_AFFILIATE` column. (#79)
```
The sections, in the order a release lists them:
| Section | For |
| --- | --- |
| `Security` | vulnerabilities, with what an attacker could do |
| `New Features` | new commands, tools, flags and providers |
| `Bug Fixes` | wrong output, crashes, failures |
| `Improvements` | speed, robustness, help text, distribution |
| `Output Changes` | any change to JSON fields, MCP results, error codes or exit codes that scripts and agents read; start a breaking one with **Breaking:** |
| `Docs` | user-facing documentation |
| `Testing` | what was verified, when a release needs to say so |
| `Notes` | dependencies, requirements, known limits |
A release may add `changes/unreleased/summary.md`: a short paragraph with no
headings that opens its notes, such as "**Upgrade from 0.3.2**: ...".
`scripts/changelog.sh check` (part of `nix flake check`) validates the entries.
At release time, `scripts/changelog.sh release X.Y.Z` folds them into a new
section of [`CHANGELOG.md`](../CHANGELOG.md) and deletes them; edit the section
if needed and commit it with the version bump. The release workflow publishes
that section as the GitHub release notes.

View file

View file

@ -0,0 +1,3 @@
### Docs
- `CHANGELOG.md` records every release since 0.2.0, and each release's GitHub notes come from it. (#82)

View file

@ -0,0 +1,3 @@
### Improvements
- CI and the release build macOS binaries on `macos-15` runners, since GitHub retired `macos-14`. (#75)

View file

@ -0,0 +1,7 @@
### Output Changes
- MCP answers and errors carry `warnings` when idx worked around something the CLI warns about on stderr: history that fell back to Yahoo, a stale cache entry and the failure behind it, or a shareholder movements query cut short. (#81)
### Improvements
- The stale-cache warning names the failure that caused it, for example `network failed (provider unavailable), serving stale cache for BBCA.JK`. (#81)

View file

@ -0,0 +1,3 @@
### Bug Fixes
- `ownership import` reads IDX's above-1% register from October 2026 on, which adds a `CONTROLLER_OR_AFFILIATE` column; 0.4.0 rejects these files with `PARSEERROR`. Any other unknown column is still rejected. (#79)

View file

@ -0,0 +1,5 @@
### New Features
- `idx stocks actions` and `get_corporate_actions` list one stock's corporate actions from your Stockbit session, newest first: cash and stock dividends, bonus shares, rights issues, warrants, splits and reverse splits, shareholders' meetings, public exposes and tender offers. `--kind` picks kinds, and `--kind ipo` adds the stock's IPO. (#78)
- `idx stocks calendar` and `get_corporate_calendar` list the market's actions dated in a range, oldest first: by default today and the next 30 days, at most 366 days. (#78)
- An action's `date` is its cum date where there is an entitlement (dividends, bonuses, rights, splits), else the event's first day. Where Stockbit gives only an ex date, the table marks it `(ex)`. Dividends paid in another currency carry `currency`. (#78)

View file

@ -0,0 +1,11 @@
### New Features
- `idx stockbit login`, `status` and `logout` sign in with your own Stockbit account. On a desktop, idx opens a browser window on Stockbit's sign-in page; over SSH or with `--no-browser`, a page on 127.0.0.1 takes the session from your own browser. idx never sees your password, stores the session with mode 0600, and refreshes it under a lock so concurrent processes never spend it twice. (#74)
- `idx stocks orderbook` and `get_orderbook` show a stock's order book: the depth ladder, OHLC, value, trades and foreign flow, and whether the book is live. (#74)
- With a session, `--provider stockbit` quotes come from Stockbit's session API, and price history from 5 days to 1 year uses Stockbit's daily charts (`--history-provider stockbit`, or `auto` under the stockbit provider). If Stockbit refuses the session, `auto` falls back to Yahoo with a warning. (#74)
- MCP: `stockbit_login` starts a sign-in and answers at once, and `stockbit_status` reports it: `sign_in.state` (waiting, signed_in or failed), plus `sign_in.warning` while it waits, for example when Stockbit refuses the session. (#74)
### Notes
- New dependency in idx-core: `base64` 0.22. (#74)
- Google sign-in does not work in idx's browser window; sign in with your password there, or use the local page (`idx stockbit login --no-browser`). (#74)

View file

@ -0,0 +1,7 @@
### New Features
- `idx ownership movements` and `get_shareholder_movements` list reported changes in insiders' and major holders' stakes from your Stockbit session, newest first. `IDX` rows are insider disclosures with the trade price; `KSEI` rows are holders at 5% and above, with a price Stockbit estimates. Stockbit filters by date, holder, action and source, and `--pages` reads up to 10 pages of 50 rows. If a later page fails, you get the rows read before it, with `incomplete` saying why. (#80)
### Improvements
- The `ownership` commands honour `--offline` and `--no-cache`; only `movements` uses them. (#80)

View file

@ -0,0 +1,14 @@
### New Features
- `idx stocks intraday` and the MCP tool `get_intraday` show today's session by the minute from Stockbit's public page, for stocks and the `IHSG` and `LQ45` indices: the quote, the best bid and offer while the market is open, and a sparkline. No login is needed. (#73)
- `--provider stockbit` (or `IDX_PROVIDER=stockbit`, or `general.provider`) serves quotes from Stockbit, with bid, offer and market status. History stays on Yahoo, and the MSN-only commands answer `UNSUPPORTED`. (#73)
### Output Changes
- New error code `UPSTREAMBLOCKED` (exit 4): a site's CDN refused idx, so the request is not retried. (#73)
- Under the stockbit provider, quotes carry `market_status` (`open` or `close`), `bid` and `offer` (while the market is open) and `session_note`. MSN and Yahoo output is unchanged. (#73, #74)
- `stocks intraday` marks a chart from before the latest session with `behind_session`, as quotes do. (#73)
### Notes
- Stockbit's terms allow personal, non-commercial use, and its data is delayed at least 10 minutes. idx sends at most one request a second across all its processes. See the [Stockbit page](https://idx.rasyidanaf.com/cli/stockbit/). (#73)

View file

@ -0,0 +1 @@
Stockbit data: intraday and quotes without a login, and with your own Stockbit session the order book, daily history, corporate actions and shareholder movements. **Upgrade from 0.4.0** to import IDX's ownership register from October 2026 on.

View file

@ -129,3 +129,6 @@ let conn = Connection::open(path)
- Push via `nix develop --command git push` (for prek hooks)
- Branch naming: `feat/<name>`, `fix/<name>`, `docs/<name>`
- Commit messages: conventional commits (`feat:`, `fix:`, `refactor:`, `docs:`)
- Changelog: a PR that changes what users or agents see adds one entry file,
`changes/unreleased/<branch-name>.md`, rather than editing `CHANGELOG.md`, so
stacked PRs never conflict. See [changes/README.md](../changes/README.md).

View file

@ -116,17 +116,22 @@ Release order:
1. Bump the version together in `Cargo.toml` (workspace version and the
`idx-core` dependency pin), `package.json`, and `npm/idx-mcp/package.json`.
A version number can never be republished.
2. Push the tag, or run the workflow for an existing tag.
3. Publish the crates from the tagged commit, dependency first:
2. In the same release PR, run `scripts/changelog.sh release X.Y.Z` (the date
defaults to today, UTC). It folds `changes/unreleased/` into a new
`CHANGELOG.md` section in the fixed section order, ends it with the
`**Full Changelog**` compare link, and deletes the entries. Add a
`changes/unreleased/summary.md` first for a one-line opener, such as an
upgrade note when a fix is urgent. Edit the section by hand if needed;
`scripts/changelog.sh notes X.Y.Z` prints it as GitHub will show it.
3. Push the tag, or run the workflow for an existing tag. The workflow
attaches the binaries and sets the release notes to the tag's
`CHANGELOG.md` section; a tag whose `CHANGELOG.md` lacks its version keeps
the notes it has.
4. Publish the crates from the tagged commit, dependency first:
`cargo publish -p idx-core`, then `-p idx-cli` and `-p idx-mcp`.
4. Confirm the release has all eight binaries plus `SHA256SUMS`, then write
its notes (`gh release edit vX.Y.Z --notes-file <file>`; the workflow
creates the release without them). Open with a one-line upgrade note when
a fix is urgent, then `## New Features`, `## Bug Fixes`, `## Improvements`
(and `## Testing` or `## Docs` when useful): one user-facing sentence per
item, ending with the PR or issue numbers, e.g. `(#46)`. Omit empty
sections, and end with
`**Full Changelog**: https://github.com/0xrsydn/idx-cli/compare/<previous>...<this>`.
5. Run `npm publish` in the repo root and in `npm/idx-mcp/` from the tagged
5. Confirm the release has all eight binaries plus `SHA256SUMS` and the notes.
To fix the notes later, edit `CHANGELOG.md` and run
`gh release edit vX.Y.Z --notes-file <(scripts/changelog.sh notes X.Y.Z)`.
6. Run `npm publish` in the repo root and in `npm/idx-mcp/` from the tagged
commit. The account has two-factor auth, so npm asks for a browser
approval or `--otp=<code>`.

View file

@ -13,6 +13,7 @@ offline with pinned tools:
| `packaged-cli` | the wrapped Nix release binary emits JSON, generates completions, and returns a JSON `CACHEMISS` error with exit 3 offline |
| `ownership-publisher`, `publisher-test` | packaged publisher entry point and its regression scenarios |
| `install-script-sh`, `install-script-dash`, `shellcheck` | installer regressions and shell lint |
| `changelog` | `CHANGELOG.md` and `changes/unreleased/` entries are well formed; `scripts/changelog.sh` folds and extracts them (`scripts/changelog-test.sh`) |
`tests/cli.rs` is the behavioral suite: it runs the real `idx` binary against
fixture-backed mock providers (`IDX_USE_MOCK_PROVIDER=1`) and covers output

View file

@ -152,6 +152,15 @@
INSTALL_SH_SHELL=${shell} bash scripts/install-sh-test.sh
touch "$out"
'';
changelogSrc = lib.fileset.toSource {
root = ./.;
fileset = lib.fileset.unions [
./CHANGELOG.md
./changes
./scripts/changelog.sh
./scripts/changelog-test.sh
];
};
publisherTestSrc = lib.fileset.toSource {
root = ./.;
fileset = lib.fileset.unions [
@ -303,6 +312,17 @@
diff -u ${./web/src/content/docs/mcp/tools.md} tools.md
touch "$out"
'';
# CHANGELOG.md and changes/unreleased/ entries are well formed, and
# scripts/changelog.sh folds and extracts them as releases expect.
changelog = pkgs.runCommand "idx-changelog" {
nativeBuildInputs = with pkgs; [ bash coreutils diffutils findutils gawk gnugrep ];
} ''
cp -r ${changelogSrc}/. .
chmod -R u+w .
export HOME="$TMPDIR"
bash scripts/changelog-test.sh
touch "$out"
'';
install-script-sh = installerCheck "sh";
install-script-dash = installerCheck "dash";
shellcheck = pkgs.runCommand "idx-shellcheck" {
@ -311,6 +331,7 @@
shellcheck -s sh ${installerSrc}/install.sh
shellcheck ${installerSrc}/scripts/install-sh-test.sh
shellcheck ${./scripts/live-smoke.sh}
shellcheck ${changelogSrc}/scripts/changelog.sh ${changelogSrc}/scripts/changelog-test.sh
shellcheck ${./tests/agents/agent-smoke.sh}
shellcheck ${./web/scripts/gen-cli-reference.sh} ${./web/scripts/gen-mcp-tools.sh}
touch "$out"

223
scripts/changelog-test.sh Normal file
View file

@ -0,0 +1,223 @@
#!/usr/bin/env bash
# Tests scripts/changelog.sh against a scratch checkout, then checks the real
# CHANGELOG.md and entries.
set -Eeuo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
script="${repo_root}/scripts/changelog.sh"
work="$(mktemp -d "${TMPDIR:-/tmp}/idx-changelog-test.XXXXXX")"
trap 'rm -rf "$work"' EXIT
failures=0
fail() {
printf 'FAIL: %s\n' "$*" >&2
failures=$((failures + 1))
}
changelog() {
CHANGELOG_ROOT="$work" bash "$script" "$@"
}
# A checkout with one released version and no entries.
reset() {
rm -rf "${work:?}"/*
mkdir -p "$work/changes/unreleased"
cat >"$work/CHANGELOG.md" <<'EOF'
# Changelog
Intro.
## [0.4.0] - 2026-10-19
### Bug Fixes
- An old fix. (#1)
EOF
}
entry() {
cat >"$work/changes/unreleased/$1"
}
expect_check_error() {
local what="$1" pattern="$2" err
if err="$(changelog check 2>&1)"; then
fail "check accepted ${what}"
elif ! grep -q -- "$pattern" <<<"$err"; then
fail "check on ${what} said: ${err}"
fi
}
# --- check -----------------------------------------------------------------
reset
changelog check >/dev/null || fail "check refused a clean checkout"
entry bad.md <<'EOF'
### Fixes
- x (#2)
EOF
expect_check_error "an unknown section" 'unknown section "Fixes"'
reset
entry bad.md <<'EOF'
- a bullet before any heading (#2)
### Bug Fixes
- x (#2)
EOF
expect_check_error "text before a heading" "text before the first"
reset
entry bad.md <<'EOF'
### Bug Fixes
### New Features
- x (#2)
EOF
expect_check_error "an empty section" 'section "Bug Fixes" is empty'
reset
entry bad.md <<'EOF'
## Bug Fixes
- x (#2)
EOF
expect_check_error "a level-2 heading" 'use "### <section>" headings only'
reset
entry summary.md <<'EOF'
### Not here
EOF
expect_check_error "a heading in the summary" "the summary takes no headings"
reset
printf '\n## [0.5.0] - 2026-11-05\n\n- newer below older\n' >>"$work/CHANGELOG.md"
expect_check_error "versions out of order" "is not older than the version above it"
reset
printf '\n## 0.3.0\n' >>"$work/CHANGELOG.md"
expect_check_error "a malformed version heading" 'version headings read'
# --- release -----------------------------------------------------------------
# b-feature.md cites a lower PR than a-fix.md, so it comes first.
reset
entry b-feature.md <<'EOF'
### New Features
- Feature B, which spans
two lines. (#9)
### Output Changes
- **Breaking:** field `x` is gone. (#9)
EOF
entry a-fix.md <<'EOF'
### Bug Fixes
- Fix A. (#10)
### New Features
- Feature A, with an example:
```bash
# a comment, not a heading
idx stocks quote BBCA
```
EOF
entry summary.md <<'EOF'
**Upgrade from 0.4.0**: it fixes A.
EOF
changelog release 0.5.0 2026-11-05 >/dev/null || fail "release failed"
expected="$work/expected.md"
cat >"$expected" <<'EOF'
# Changelog
Intro.
## [0.5.0] - 2026-11-05
**Upgrade from 0.4.0**: it fixes A.
### New Features
- Feature B, which spans
two lines. (#9)
- Feature A, with an example:
```bash
# a comment, not a heading
idx stocks quote BBCA
```
### Bug Fixes
- Fix A. (#10)
### Output Changes
- **Breaking:** field `x` is gone. (#9)
**Full Changelog**: https://github.com/0xrsydn/idx-cli/compare/v0.4.0...v0.5.0
## [0.4.0] - 2026-10-19
### Bug Fixes
- An old fix. (#1)
EOF
diff -u "$expected" "$work/CHANGELOG.md" || fail "release wrote a different CHANGELOG.md"
if find "$work/changes/unreleased" -name '*.md' | grep -q .; then
fail "release left entries behind"
fi
changelog check >/dev/null || fail "check refused the released CHANGELOG.md"
entry again.md <<'EOF'
### Bug Fixes
- x (#12)
EOF
if changelog release 0.5.0 2026-11-06 >/dev/null 2>&1; then
fail "release accepted a version already released"
fi
rm "$work/changes/unreleased/again.md"
if changelog release 0.5.1 >/dev/null 2>&1; then
fail "release accepted no entries"
fi
# --- notes -------------------------------------------------------------------
notes="$(changelog notes v0.5.0)"
[[ "$(head -1 <<<"$notes")" == "**Upgrade from 0.4.0**: it fixes A." ]] ||
fail "notes do not open with the summary: $(head -1 <<<"$notes")"
grep -qx '## New Features' <<<"$notes" || fail "notes did not raise section headings"
grep -qx ' # a comment, not a heading' <<<"$notes" || fail "notes changed a code block"
grep -q '0.4.0\] -' <<<"$notes" && fail "notes ran into the next version"
[[ "$(tail -1 <<<"$notes")" == "**Full Changelog**: https://github.com/0xrsydn/idx-cli/compare/v0.4.0...v0.5.0" ]] ||
fail "notes do not end with the compare link"
[[ "$(changelog notes 0.4.0)" == "## Bug Fixes"$'\n\n'"- An old fix. (#1)" ]] ||
fail "notes for the oldest version: $(changelog notes 0.4.0)"
if changelog notes 0.9.9 >/dev/null 2>&1; then
fail "notes accepted a version not in CHANGELOG.md"
fi
# --- the real changelog --------------------------------------------------------
bash "$script" check >/dev/null || fail "the repository's changelog entries do not pass check"
while read -r version; do
[[ -n "$(bash "$script" notes "$version")" ]] || fail "no notes for ${version}"
done < <(awk '/^## \[[0-9]/ { print substr($2, 2, length($2) - 2) }' "${repo_root}/CHANGELOG.md")
if ((failures > 0)); then
printf '%d changelog test(s) failed\n' "$failures" >&2
exit 1
fi
printf 'changelog tests passed\n'

261
scripts/changelog.sh Executable file
View file

@ -0,0 +1,261 @@
#!/usr/bin/env bash
# CHANGELOG.md and the entries waiting for the next release.
#
# A pull request that changes what users or agents see adds one entry file,
# changes/unreleased/<branch-name>.md (see changes/README.md). At release time
# the entries are folded into CHANGELOG.md, and the release workflow publishes
# that version's section as the GitHub release notes.
#
# scripts/changelog.sh check # validate entries and CHANGELOG.md
# scripts/changelog.sh release 0.5.0 [YYYY-MM-DD] # fold entries into a new section
# scripts/changelog.sh notes 0.5.0 # print a version's release notes
#
# CHANGELOG_ROOT points at another checkout (default: this repository).
set -Eeuo pipefail
root="${CHANGELOG_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
changelog="${root}/CHANGELOG.md"
unreleased="${root}/changes/unreleased"
repo_url="https://github.com/0xrsydn/idx-cli"
# Sections in the order a release lists them.
sections=(
"Security"
"New Features"
"Bug Fixes"
"Improvements"
"Output Changes"
"Docs"
"Testing"
"Notes"
)
die() {
printf 'changelog: %s\n' "$*" >&2
exit 1
}
usage() {
sed -n '2,13p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
}
# Entry files in the order a release lists them: by the lowest PR number an
# entry cites, so the release reads in the order the work landed, then by
# name. The summary is left out; it opens the release instead.
entries() {
[[ -d "$unreleased" ]] || return 0
local file pr
find "$unreleased" -maxdepth 1 -type f -name '*.md' ! -name 'summary.md' |
while IFS= read -r file; do
pr="$(grep -o '(#[0-9][0-9]*' "$file" | tr -d '(#' | sort -n | head -1 || true)"
printf '%09d\t%s\n' "$((10#${pr:-999999999}))" "$file"
done | LC_ALL=C sort | cut -f2-
}
section_names() {
local IFS='|'
printf '%s' "${sections[*]}"
}
check_entry() {
local file="$1"
awk -v file="$file" -v names="$(section_names)" '
BEGIN { n = split(names, list, "|"); for (i = 1; i <= n; i++) allowed[list[i]] = 1 }
/^```/ { fence = !fence }
fence { if (current != "") body[current]++; next }
/^#/ {
if ($0 !~ /^### /) { printf "%s:%d: use \"### <section>\" headings only\n", file, NR; bad = 1; next }
current = substr($0, 5)
if (!(current in allowed)) {
printf "%s:%d: unknown section \"%s\" (use one of: %s)\n", file, NR, current, names; bad = 1
}
if (current in seen) { printf "%s:%d: section \"%s\" appears twice\n", file, NR, current; bad = 1 }
seen[current] = 1; order[++count] = current
next
}
/[^[:space:]]/ {
if (current == "") { printf "%s:%d: text before the first \"### <section>\" heading\n", file, NR; bad = 1; next }
body[current]++
}
END {
if (count == 0) { printf "%s: no \"### <section>\" heading\n", file; bad = 1 }
for (i = 1; i <= count; i++) if (!(order[i] in body)) {
printf "%s: section \"%s\" is empty\n", file, order[i]; bad = 1
}
exit bad
}
' "$file" >&2
}
check_summary() {
local file="$1"
awk -v file="$file" '
/^```/ { fence = !fence }
!fence && /^#/ { printf "%s:%d: the summary takes no headings\n", file, NR; bad = 1 }
END { exit bad }
' "$file" >&2
}
check_changelog() {
[[ -f "$changelog" ]] || die "missing ${changelog}"
awk -v file="$changelog" '
/^```/ { fence = !fence }
fence || !/^## / { next }
{
if ($0 !~ /^## \[[0-9]+\.[0-9]+\.[0-9]+\] - [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$/) {
printf "%s:%d: version headings read \"## [X.Y.Z] - YYYY-MM-DD\"\n", file, NR; bad = 1; next
}
version = substr($2, 2, length($2) - 2)
if (version in seen) { printf "%s:%d: %s appears twice\n", file, NR, version; bad = 1 }
seen[version] = 1
split(version, v, ".")
key = sprintf("%06d%06d%06d", v[1], v[2], v[3])
if (last != "" && key >= last) {
printf "%s:%d: %s is not older than the version above it\n", file, NR, version; bad = 1
}
last = key
}
END { exit bad }
' "$changelog" >&2
}
cmd_check() {
local failed=0 file
check_changelog || failed=1
while IFS= read -r file; do
check_entry "$file" || failed=1
done < <(entries)
if [[ -f "${unreleased}/summary.md" ]]; then
check_summary "${unreleased}/summary.md" || failed=1
fi
((failed == 0)) || exit 1
printf 'changelog: ok (%s unreleased entries)\n' "$(entries | wc -l | tr -d ' ')"
}
# The newest version in CHANGELOG.md, if any.
latest_version() {
awk '/^## \[[0-9]/ { print substr($2, 2, length($2) - 2); exit }' "$changelog"
}
# Drops leading and trailing blank lines.
trim() {
awk '
{ lines[++n] = $0 }
END {
first = 1; while (first <= n && lines[first] ~ /^[[:space:]]*$/) first++
last = n; while (last >= first && lines[last] ~ /^[[:space:]]*$/) last--
for (i = first; i <= last; i++) print lines[i]
}
'
}
# One section's lines across every entry, in entry order, each entry's part
# trimmed so the bullets read as one list.
section_body() {
local name="$1"
shift
awk -v want="$name" '
function flush( first, last, i) {
first = 1; while (first <= n && buf[first] ~ /^[[:space:]]*$/) first++
last = n; while (last >= first && buf[last] ~ /^[[:space:]]*$/) last--
for (i = first; i <= last; i++) print buf[i]
n = 0
}
FNR == 1 { flush(); keep = 0; fence = 0 }
{
if ($0 ~ /^```/) { fence = !fence; if (keep) buf[++n] = $0; next }
if (!fence && $0 ~ /^### /) { flush(); keep = (substr($0, 5) == want); next }
if (keep) buf[++n] = $0
}
END { flush() }
' "$@"
}
cmd_release() {
local version="${1:-}" date="${2:-}"
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || die "release takes a version such as 0.5.0"
if [[ -z "$date" ]]; then
date="$(date -u +%Y-%m-%d)"
fi
[[ "$date" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || die "the date reads YYYY-MM-DD"
cmd_check >/dev/null
if grep -q "^## \[${version//./\\.}\] " "$changelog"; then
die "${version} is already in CHANGELOG.md"
fi
local files=()
local file
while IFS= read -r file; do
files+=("$file")
done < <(entries)
((${#files[@]} > 0)) || die "no entries in changes/unreleased/ to release"
local previous section body
previous="$(latest_version)"
local out
out="$(mktemp "${TMPDIR:-/tmp}/changelog.XXXXXX")"
{
printf '## [%s] - %s\n\n' "$version" "$date"
if [[ -f "${unreleased}/summary.md" ]]; then
trim <"${unreleased}/summary.md"
printf '\n'
fi
for section in "${sections[@]}"; do
body="$(section_body "$section" "${files[@]}")"
[[ -n "$body" ]] || continue
printf '### %s\n\n%s\n\n' "$section" "$body"
done
if [[ -n "$previous" ]]; then
printf '**Full Changelog**: %s/compare/v%s...v%s\n\n' "$repo_url" "$previous" "$version"
fi
} >"$out"
# The new section goes above the newest version (or at the end).
awk -v section="$out" '
!done && /^## \[/ { while ((getline line < section) > 0) print line; done = 1 }
{ print }
END { if (!done) { print ""; while ((getline line < section) > 0) print line } }
' "$changelog" >"${out}.full"
mv "${out}.full" "$changelog"
rm -f "$out" "${files[@]}" "${unreleased}/summary.md"
printf 'changelog: %s released from %d entries; review CHANGELOG.md, then commit\n' \
"$version" "${#files[@]}"
}
# A version's section without its heading, headings raised one level to
# match GitHub release notes (### New Features -> ## New Features).
cmd_notes() {
local version="${1:-}"
version="${version#v}"
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || die "notes takes a version such as 0.5.0"
[[ -f "$changelog" ]] || die "missing ${changelog}"
awk -v want="$version" '
/^```/ && inside { fence = !fence }
!fence && /^## \[/ {
if (inside) exit
if (substr($2, 2, length($2) - 2) == want) { inside = 1; found = 1; next }
}
inside {
if (!fence && /^###/) sub(/^#/, "")
lines[++n] = $0
}
END {
if (!found) exit 3
first = 1; while (first <= n && lines[first] ~ /^[[:space:]]*$/) first++
last = n; while (last >= first && lines[last] ~ /^[[:space:]]*$/) last--
for (i = first; i <= last; i++) print lines[i]
}
' "$changelog" || {
local status=$?
((status == 3)) && die "${version} is not in CHANGELOG.md"
exit "$status"
}
}
case "${1:-}" in
check) shift; cmd_check "$@" ;;
release) shift; cmd_release "$@" ;;
notes) shift; cmd_notes "$@" ;;
-h | --help | help) usage ;;
*) usage >&2; exit 2 ;;
esac