Skip to content

Commit 8100fbf

Browse files
authored
Merge pull request #17 from ImagingDataCommons/docs/contributing-changelog
docs: establish contribution, changelog, and release procedures
2 parents c387887 + d95af26 commit 8100fbf

8 files changed

Lines changed: 289 additions & 14 deletions

File tree

.github/workflows/promote.yml

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ name: Promote (test / prod)
77
# - Push a v* tag -> PROD: deploy the exact image TEST already validated, by digest,
88
# referencing TEST's registry directly (no rebuild, no copy). Gated by the `prod`
99
# Environment's required-reviewer rule. prod's deployer SA + Cloud Run service agent must
10-
# have read on TEST's Artifact Registry.
10+
# have read on TEST's Artifact Registry. The tag must equal "v" + pyproject.toml's version
11+
# (checked below): no rebuild happens here, so the version is whatever test baked in.
1112
#
1213
# For the prod path, test's registry coordinates come from repo-level Variables (test's project
1314
# isn't in prod's Environment): TEST_PROJECT_ID (required), TEST_REGION / TEST_AR_REPO (optional).
@@ -89,6 +90,24 @@ jobs:
8990
build_label: ${{ steps.r.outputs.sha }}
9091
source_image: ${{ steps.r.outputs.image }}
9192
steps:
93+
- uses: actions/checkout@v7
94+
95+
# The tag names the release, but the version the running server reports comes from
96+
# pyproject.toml, baked into the image at build time. Prod redeploys test's existing digest
97+
# and never rebuilds, so a tag that disagrees with the packaged version would deploy fine
98+
# and quietly serve the wrong version at /v3/version. Fail here, before the reviewer gate.
99+
- name: Verify the tag matches the packaged version
100+
env:
101+
TAG: ${{ github.ref_name }}
102+
run: |
103+
set -euo pipefail
104+
VERSION=$(python3 -c 'import tomllib; print(tomllib.load(open("pyproject.toml","rb"))["project"]["version"])')
105+
if [ "${TAG#v}" != "$VERSION" ]; then
106+
echo "::error::Tag '${TAG}' does not match pyproject.toml version '${VERSION}'. The version is baked in when test builds the image, so bump pyproject.toml, promote that commit to test, then tag it. See CONTRIBUTING.md -> Releasing."
107+
exit 1
108+
fi
109+
echo "Tag ${TAG} matches packaged version ${VERSION}."
110+
92111
- id: r
93112
env:
94113
TEST_PROJECT: ${{ vars.TEST_PROJECT_ID }}

CHANGELOG.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Changelog
2+
3+
All notable changes to this project are documented here.
4+
5+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
6+
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) with one house rule: the
7+
**MAJOR version is pinned to the served URL prefix**`/v3``3.y.z`. A breaking change to the
8+
REST or MCP contract means a new prefix (`/v4`) and a new major (`4.0.0`), never a silent break
9+
under `/v3`. See [CONTRIBUTING.md](CONTRIBUTING.md#versioning) for the full policy.
10+
11+
Entries describe **user-visible** change — endpoints, MCP tools, response shapes, configuration.
12+
Refactors, CI, and formatting land in the git history, not here.
13+
14+
## [Unreleased]
15+
16+
Nothing yet.
17+
18+
## [3.0.0b1] — unreleased
19+
20+
First public release of the v3 API: a rewrite that replaces the v1/v2 service with a single
21+
backend-agnostic core behind two thin adapters (REST + MCP), served from the `idc-index` Parquet
22+
index queried locally with DuckDB.
23+
24+
**This is a beta.** The `/v3` contract may still change in response to feedback before `3.0.0`.
25+
Pin to an exact version if you need stability. Legacy v1/v2 endpoints are unaffected — they are
26+
served by a different backend and v3 lives only under `/v3/*`.
27+
28+
### Added
29+
30+
- **REST API**, entirely under the `/v3` prefix: discovery (`/v3/version`, `/v3/stats`,
31+
`/v3/collections`, `/v3/analysis_results`, `/v3/attributes`, `/v3/tables`), cohort building,
32+
retrieval (manifests / cohort URLs, viewer URLs), citations and licenses, guarded SQL, and
33+
`/v3/health`. Interactive docs at `/v3/docs`; the bare domain redirects there.
34+
- **MCP server** over stdio (local) and streamable-http (hosted at `/mcp`), exposing the same
35+
capabilities as tools — `list_collections`, `get_collection`, `list_attributes`,
36+
`get_attribute_values`, `build_cohort`, `get_cohort_urls`, `download_cohort`, `get_viewer_url`,
37+
`run_sql`, `get_citations`, `get_licenses`, and the clinical/table introspection tools — plus
38+
an `idc://guide` resource describing the data model and workflow.
39+
- **Guarded SQL** (`POST /v3/sql`, `run_sql`): read-only DuckDB with external access and
40+
extension loading disabled, single-statement enforcement, a server row cap, and a timeout.
41+
See [SECURITY.md](SECURITY.md).
42+
- **Specialized indices** joinable to `index` on `SeriesInstanceUID` (`seg_index`, `ann_index`,
43+
`ct_index`, `mr_index`, `pt_index`, `sm_index`, …) and per-collection **clinical tables** under
44+
a `clinical` schema, both fetched from `idc-index` releases at build time.
45+
- **Software version reporting**, distinct from the IDC *data* version: `/v3/version` returns
46+
`api_version` (and `build`, when a deploy stamps `IDC_API_BUILD`); the same string appears in
47+
the OpenAPI `info.version` and the MCP `initialize` handshake (`serverInfo.version`).
48+
- **Structured audit logging** — one JSON line per REST request and MCP tool call.
49+
`IDC_API_SQL_LOG_MODE` selects how the guarded SQL query is rendered (`snippet` or `hash`).
50+
51+
[Unreleased]: https://github.com/ImagingDataCommons/IDC-REST-MCP/compare/v3.0.0b1...HEAD
52+
[3.0.0b1]: https://github.com/ImagingDataCommons/IDC-REST-MCP/releases/tag/v3.0.0b1

CLAUDE.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,20 @@ Docs are split by audience — keep them in their lanes:
6565
- **[README.md](README.md)** — kept **lean**: intro, status, install, run one-liners,
6666
deploy, and pointers. **Do not** add endpoint/tool reference or usage detail here — that goes
6767
in the user guide.
68+
- **[CONTRIBUTING.md](CONTRIBUTING.md)** — the **process**: branching, Conventional Commits, the
69+
changelog rule, and the versioning policy (SemVer with MAJOR pinned to the URL prefix,
70+
`/v3``3.y.z`). **Do not** restate setup/test commands or the architecture invariants here —
71+
it points at `dev/developer_guide.md` for those. The release *runbook* lives in
72+
`dev/deployment.md`, not here: it is a maintainer task, not a contributor one.
73+
- **[CHANGELOG.md](CHANGELOG.md)** — hand-curated, [Keep a Changelog](https://keepachangelog.com/)
74+
format. Describes **user-visible** change only (endpoints, MCP tools, response shapes, config
75+
vars, defaults) — never refactors, CI, or dependency bumps. Not generated from commits.
6876
- **`dev/`** — design & contributor docs: [architecture.md](dev/architecture.md),
6977
[api_v3_plan.md](dev/api_v3_plan.md) (design rationale + SQL threat model),
70-
[deployment.md](dev/deployment.md), [developer_guide.md](dev/developer_guide.md).
78+
[deployment.md](dev/deployment.md) (Cloud Run tiers **and** the release runbook),
79+
[developer_guide.md](dev/developer_guide.md) (setup, test, CI, the invariants, walkthroughs).
7180

7281
When you add or change a capability: update `docs/user-guide.md`; mirror any conceptual
7382
change into the `idc://guide` resource; check whether `INSTRUCTIONS` needs a one-line touch (it
74-
usually shouldn't, if it stays lean); keep `README.md` a pointer.
83+
usually shouldn't, if it stays lean); keep `README.md` a pointer. If the change is user-visible,
84+
add a `CHANGELOG.md` entry under `## [Unreleased]` in the same PR.

CONTRIBUTING.md

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# Contributing
2+
3+
Thanks for helping improve the IDC REST API + MCP server. This document is the **process**: how we
4+
branch, commit, changelog, and version.
5+
6+
The **code** is documented elsewhere, and this file does not repeat it:
7+
8+
| You want to | Read |
9+
|---|---|
10+
| Set up, run, and test the project | [dev/developer_guide.md](dev/developer_guide.md)*Setup*, *Run*, *Test* |
11+
| Know what CI will run against your PR | [dev/developer_guide.md](dev/developer_guide.md)*Continuous integration* |
12+
| Understand the invariants reviewers check | [dev/developer_guide.md](dev/developer_guide.md)*Conventions* |
13+
| Add a capability (model → service → REST → MCP → parity test) | [dev/developer_guide.md](dev/developer_guide.md)*Walkthrough* |
14+
| Understand *why* it's built this way | [dev/architecture.md](dev/architecture.md) |
15+
| Cut a release / deploy | [dev/deployment.md](dev/deployment.md)*Cutting a release* |
16+
17+
In short: `uv pip install -e ".[dev]"`, then make sure `ruff`, `bandit`, and `pytest` pass before
18+
you push — the developer guide has the exact commands CI uses.
19+
20+
Documentation is split by audience (user guide vs. the agent-facing `idc://guide` resource vs.
21+
the always-on MCP `INSTRUCTIONS` vs. `dev/`). Keep each in its lane — the conventions, and which
22+
file to touch when, are in [CLAUDE.md](CLAUDE.md#documentation-conventions).
23+
24+
## Branches
25+
26+
Branch off `main`, named `<type>/<short-slug>` using the same type vocabulary as commits:
27+
28+
```
29+
feat/cohort-size-estimate fix/mcp-trailing-slash
30+
docs/api-endpoint-examples ci/multi-tier-deploy
31+
```
32+
33+
Pull requests are merged with a merge commit, so the individual commits on your branch land in
34+
`main`'s history. Make them ones you'd want to read later.
35+
36+
## Commits
37+
38+
We follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/):
39+
40+
```
41+
<type>(<optional scope>): <imperative, lowercase summary>
42+
```
43+
44+
**Types:** `feat`, `fix`, `docs`, `test`, `refactor`, `perf`, `style`, `build`, `ci`, `chore`.
45+
**Scopes** in use: `rest`, `mcp`, `api`, `deploy`, `deps`.
46+
47+
```
48+
feat(rest): redirect the bare domain to the interactive docs
49+
fix(mcp): serve /mcp and /mcp/ directly instead of redirecting
50+
docs(api): add OpenAPI summaries/descriptions to every REST route
51+
```
52+
53+
This is a convention, not a CI gate — nothing will fail your build if you deviate. It exists so
54+
history stays skimmable and so the changelog is easy to assemble at release time. Dependabot's
55+
commits don't always conform; that's fine.
56+
57+
Mark a breaking change to the REST or MCP contract with a `!` (`feat(rest)!: …`) and a
58+
`BREAKING CHANGE:` footer. See [Versioning](#versioning) — such a change needs a new URL prefix,
59+
so it is a much bigger conversation than a commit message.
60+
61+
## Changelog
62+
63+
[CHANGELOG.md](CHANGELOG.md) is **hand-curated**, in [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
64+
format. It is not generated from commits: it describes what changed for *callers of the API*,
65+
which is a different thing from what changed in the tree.
66+
67+
**If your PR changes user-visible behavior, add an entry to `## [Unreleased]` in the same PR.**
68+
User-visible means an endpoint, an MCP tool or its description, a response shape, a configuration
69+
variable, or a default. Use the standard groupings — `Added`, `Changed`, `Deprecated`, `Removed`,
70+
`Fixed`, `Security` — and write for someone consuming the API, not someone reading the diff:
71+
72+
```markdown
73+
### Fixed
74+
- MCP: `/mcp` and `/mcp/` are both served directly; neither redirects.
75+
```
76+
77+
Refactors, test changes, CI, formatting, and dependency bumps do **not** get an entry. The git
78+
history already records them.
79+
80+
> While `3.0.0b1` is still unreleased, fixes to code that has never shipped are folded into that
81+
> release's `Added` section rather than listed under `Fixed` — there is no released behavior to
82+
> have fixed. Once the beta ships, use the groupings normally.
83+
84+
## Versioning
85+
86+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html), with one house rule:
87+
88+
**MAJOR is pinned to the served URL prefix.** `/v3``3.y.z`, always.
89+
90+
| Change | Version | URL |
91+
|---|---|---|
92+
| Add an endpoint, MCP tool, or optional field | MINOR — `3.1.0` | `/v3` |
93+
| Fix a bug without changing the contract | PATCH — `3.0.1` | `/v3` |
94+
| Break the REST or MCP contract | MAJOR — `4.0.0` | new prefix `/v4` |
95+
96+
So a breaking change is never a silent break under `/v3`: it is a new prefix served alongside the
97+
old one. This keeps `api_version` predictive of the URL, and matches the clean break v3 already
98+
made from v1/v2.
99+
100+
Pre-releases use [PEP 440](https://peps.python.org/pep-0440/) spelling so the Python package
101+
version and the git tag agree: `3.0.0b1` → tag `v3.0.0b1`; `3.0.0rc1` → tag `v3.0.0rc1`.
102+
103+
**The version lives in exactly one place: `version` in [pyproject.toml](pyproject.toml).**
104+
Everything else derives from it — `idc_api.__version__` and `core/version.py:package_version()`
105+
both read the installed distribution metadata. Never hardcode it a second time.
106+
107+
## Releasing
108+
109+
Cutting a release is a **maintainer** task that deploys to production. The runbook — including the
110+
v3 beta plan — lives with the deploy machinery it depends on:
111+
[dev/deployment.md § Cutting a release](dev/deployment.md#cutting-a-release).
112+
113+
Two things every contributor should know:
114+
115+
> [!IMPORTANT]
116+
> **Pushing a `v*` tag deploys to production.** [promote.yml](.github/workflows/promote.yml)
117+
> triggers on `push: tags: ["v*"]`, and that glob matches pre-release tags too — `v3.0.0b1` goes
118+
> to prod exactly like `v3.0.0`. Never create a `v*` tag for bookkeeping, and be careful with
119+
> `git push --tags`, which can fire a deploy from a stale local tag.
120+
121+
And you cannot tag a release without bumping `pyproject.toml` first: `promote.yml` asserts that
122+
the tag equals `"v"` + the packaged version, and fails before the reviewer gate if they disagree.
123+
124+
## Reporting security issues
125+
126+
Please don't open a public issue for a vulnerability — use GitHub's private reporting flow, as
127+
described in [SECURITY.md](SECURITY.md).

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ both.
2929
- [`dev/architecture.md`](dev/architecture.md) — internal design (one core, two adapters).
3030
- [`dev/api_v3_plan.md`](dev/api_v3_plan.md) — full design rationale + SQL threat model.
3131
- [`dev/deployment.md`](dev/deployment.md) — Cloud Run deployment.
32-
- [`dev/developer_guide.md`](dev/developer_guide.md) — contributing / adding capabilities.
32+
- [`dev/developer_guide.md`](dev/developer_guide.md) — codebase tour / adding capabilities.
33+
- [`CONTRIBUTING.md`](CONTRIBUTING.md) — branching, commits, changelog, versioning, releases.
34+
- [`CHANGELOG.md`](CHANGELOG.md) — what changed for callers, per release.
3335
- [`SECURITY.md`](SECURITY.md) — threat model, hardening in place, and how to report a
3436
vulnerability.
3537

dev/deployment.md

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -248,10 +248,64 @@ file — the first becomes the *trigger*, the second becomes an *Environment set
248248
| [promote.yml](../.github/workflows/promote.yml) (tag) | push a `v*` tag | deploy test's digest to **prod** (behind the required-reviewer gate) |
249249
| [deploy.yml](../.github/workflows/deploy.yml) | reusable (`workflow_call`) | the shared deploy job the callers invoke |
250250
251-
Prod deploys **test's** image, so a `v*` tag must point at a commit that was **already promoted to
252-
test** (its image exists in test's registry). Cut releases from a ref you've dispatched to test;
253-
tag a commit that never went through test and the prod deploy fails fast at the digest-resolve
254-
step.
251+
### Cutting a release
252+
253+
Versioning policy — what a version *number* means, and when to bump which part — is in
254+
[CONTRIBUTING.md](../CONTRIBUTING.md#versioning). This section is the mechanics.
255+
256+
> [!IMPORTANT]
257+
> **Pushing a `v*` tag deploys to production**, and the glob matches pre-release tags too —
258+
> `v3.0.0b1` goes to prod exactly like `v3.0.0`. Never create a `v*` tag for bookkeeping, and
259+
> beware `git push --tags` firing a deploy from a stale local tag.
260+
261+
Two constraints fall out of the security boundary above — prod runs test's exact bytes and never
262+
rebuilds:
263+
264+
1. **A `v*` tag must point at a commit already promoted to test** (its image exists in test's
265+
registry). Cut releases from a ref you've dispatched to test; tag a commit that never went
266+
through test and the prod deploy fails fast at the digest-resolve step.
267+
2. **The version bump must be its own commit, and it must go through test.** The version comes from
268+
the installed package metadata, baked into the image when *test* builds it; `IDC_API_BUILD` only
269+
stamps the git SHA on top. Tagging `v3.0.0` on the same commit that shipped as `3.0.0b1` would
270+
redeploy an image that still reports `3.0.0b1` at `/v3/version`. `promote.yml` guards this: on a
271+
`v*` tag it asserts `tag == "v" + pyproject version` and fails **before** the reviewer gate if
272+
they disagree.
273+
274+
#### Steps
275+
276+
1. **Bump and curate.** In one PR: set `version` in [pyproject.toml](../pyproject.toml), and in
277+
[CHANGELOG.md](../CHANGELOG.md) rename `## [Unreleased]` to `## [X.Y.Z] — YYYY-MM-DD`, open a
278+
fresh empty `[Unreleased]`, and update the link definitions at the foot of the file. Merge it.
279+
2. **Promote to test.** Run [promote.yml](../.github/workflows/promote.yml) via workflow dispatch
280+
against that merge commit. It builds the canonical image into test's registry and deploys
281+
`testing-api.canceridc.dev`.
282+
3. **Verify** against test — `/v3/health`, `/v3/version` (confirm it reports the version you just
283+
set), and the MCP handshake at `/mcp`.
284+
4. **Tag.** `git tag -a v3.0.0 -m "v3.0.0" <that commit> && git push origin v3.0.0`. This starts the
285+
prod deploy, which waits on the `prod` Environment's required-reviewer gate.
286+
5. **Approve** the deployment, then confirm `api.imaging.datacommons.cancer.gov/v3/version`.
287+
6. **Publish a GitHub Release** on the tag, with the changelog section as its body. Tick **"Set as a
288+
pre-release"** for `bN` / `rcN` tags.
289+
290+
#### The v3 beta
291+
292+
v3 ships to production as `3.0.0b1` before `3.0.0`.
293+
294+
The beta is **not** a traffic-safety measure — it can't be one. The prod load balancer routes only
295+
`/v3/*` to the `idc-api-v3` service (see [Shared-domain path routing](#shared-domain-path-routing-as-deployed--the-glob-gotcha)
296+
below); every other path falls through to the legacy ESP backend. No existing caller reaches v3, so
297+
shipping it cannot break them. What the beta buys is the freedom to **change the `/v3` contract in
298+
response to real usage** without spending a major version — which, under the versioning policy,
299+
would otherwise cost a whole new `/v4` prefix — plus an honest signal to early adopters that the
300+
surface may move. Exit the beta by tagging `v3.0.0` once the contract has held under real use.
301+
302+
Deliberately **not** doing a Cloud Run traffic split (`--tag beta --no-traffic` plus a percentage
303+
rollout) for this release. There is no incumbent v3 revision to canary against, and percentage
304+
splits are applied **per request**, not per session — an MCP streamable-http session could have its
305+
requests land on different revisions mid-conversation unless `--session-affinity` is enabled. If a
306+
canary becomes worthwhile for a later release (`3.1.0` onward, once v3 has consumers), use a
307+
**tagged revision** at zero traffic, which gets its own `beta---idc-api-v3-*.run.app` URL that
308+
testers opt into explicitly, rather than a percentage split of the live domain.
255309
256310
### One-time setup
257311

dev/developer_guide.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,17 @@ Fixtures live in [tests/conftest.py](../tests/conftest.py): `ctx` (the core
6464

6565
[`.github/workflows/ci.yml`](../.github/workflows/ci.yml) runs on pushes to `main`
6666
(and manual dispatch) and on PRs that touch `src/idc_api/**`, `tests/**`, `pyproject.toml`,
67-
or `uv.lock`. It installs locked deps with `uv sync --extra dev`, then runs `ruff check` and
68-
`pytest tests` on Python 3.11 and 3.12. CI needs no secrets or GCP; its first test run
67+
or `uv.lock`. It installs locked deps with `uv sync --extra dev`, then lints, scans, and tests on
68+
Python 3.11 and 3.12. CI needs no secrets or GCP; its first test run
6969
downloads the specialized indices (`IDC_API_INCLUDE_INDICES=all` by default — set `none` to run
70-
from bundled data only). Before pushing, run the same two commands locally:
70+
from bundled data only). Before pushing, run the same checks locally:
7171

7272
```bash
73-
uv run ruff check src tests
74-
uv run pytest tests -q
73+
uv run ruff check src tests # lint
74+
uv run ruff format --check src tests # formatting
75+
uv run bandit -q -r src/idc_api # static security lint
76+
uv run pip-audit # dependency CVEs
77+
uv run pytest tests -q # tests
7578
```
7679

7780
## Project layout

0 commit comments

Comments
 (0)