From 530d06747acf2a038eb4864791eb3b0a466357d8 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Mon, 1 Jun 2026 14:31:17 -0400 Subject: [PATCH] ci: prepare for 0.1.0 release via intuit/auto + add CHANGELOG/RELEASING MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire bids-utils into the intuit/auto release-on-PR-merge pipeline and document the workflow for maintainers + contributors. After this lands, v0.1.0 can be cut manually per RELEASING.md and every release from 0.1.1 onward is fully label-driven. What's in this commit --------------------- - `.autorc` — replace the bare `git-tag` + `conventional-commits` config with the standard label-based pipeline used by citeproc-py and dandi-cli: `git-tag`, `exec.afterRelease` for PyPI publishing (`python -m build && twine upload dist/*`), and `released` for per-PR ship comments / labels. `baseBranch: main`, `noVersionPrefix: false` so tags read as `v0.1.0` for `hatch-vcs`. Dropped `conventional-commits` — incompatible with the label-based bump workflow that the rest of the auto config now uses. - `.github/workflows/release.yml` — adapted from citeproc-py: triggers on `push: branches: [main]` (post-merge) plus `workflow_dispatch` (manual ship); downloads the latest `auto` Linux binary at runtime so no node install is needed; runs `auto shipit -vv --only-publish-with-release-label` on auto-push so releases happen ONLY for PRs that carried the `release` label; uses `secrets.GITHUB_TOKEN` (default `github-actions[bot]`) — the simplest baseline. PyPI publishing via `TWINE_PASSWORD=PYPI_TOKEN`. - `CHANGELOG.md` — initial file with a `v0.1.0` entry in intuit/auto's prepend-friendly format (`# v ()` heading, emoji section headers, `Authors:` block, `---` separator) so future releases prepend naturally. The 0.1.0 entry briefly summarises the shipped surface (commands, schema-driven migration, VCS integration, observability, integration-test fixtures) and flags pending work (Phase 2a/2b rename/edit-filename split, BIDS 2.0 migration, SC-003 perf verification). - `RELEASING.md` — full release runbook: TL;DR for 0.1.1+ flow, manual workflow_dispatch path, complete copy-pasteable v0.1.0 cutover procedure (Steps 0–5 covering prerequisites, repo labels, PYPI_TOKEN secret, tag + build + upload + GH release + smoke test + workflow sanity-check), plus optional upgrade paths (protected-branch plugin, PR label enforcement à la dandi-cli, PyPI Trusted Publishing). - `CONTRIBUTING.md` — new "Pull Requests and Release Labels" section explaining the label-based release flow for contributors: which bump label to pick (`major`/`minor`/`patch`), the auxiliary labels (`release`, `internal`, `documentation`, `tests`, `dependencies`, `performance`, `released`), and the aggregation rule for multi-PR releases. Points at RELEASING.md for the full procedure. (CLAUDE.md is a symlink to CONTRIBUTING.md, so both surfaces stay in sync.) Steps to run AFTER this commit lands on main -------------------------------------------- 1. Create release labels: `GH_TOKEN=$(git config hub.oauthtoken) intuit-auto create-labels` 2. Create the `PYPI_TOKEN` repo secret (project-scoped API token from ). 3. Cut v0.1.0 per RELEASING.md → "Cutting v0.1.0 — full runbook" (Steps 1–5). Co-Authored-By: Claude Code 2.1.126 / Claude Opus 4.7 (1M context) Entire-Checkpoint: da458a8b8922 --- .autorc | 20 +++- .github/workflows/release.yml | 48 +++++++++ CHANGELOG.md | 50 +++++++++ CONTRIBUTING.md | 44 ++++++++ RELEASING.md | 190 ++++++++++++++++++++++++++++++++++ 5 files changed, 348 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 CHANGELOG.md create mode 100644 RELEASING.md diff --git a/.autorc b/.autorc index 16cfcf7..d840dfb 100644 --- a/.autorc +++ b/.autorc @@ -1,6 +1,18 @@ { - "plugins": ["git-tag", "conventional-commits"], - "owner": "bids-standard", - "repo": "bids-utils", - "name": "bids-utils" + "baseBranch": "main", + "author": "auto ", + "noVersionPrefix": false, + "owner": "bids-standard", + "repo": "bids-utils", + "name": "bids-utils", + "plugins": [ + "git-tag", + [ + "exec", + { + "afterRelease": "python -m build && twine upload dist/*" + } + ], + "released" + ] } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..3d46edd --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,48 @@ +name: Auto-release on PR merge + +on: + # ATM, this is the closest trigger to a PR merging + push: + branches: + - main + workflow_dispatch: + +jobs: + auto-release: + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, 'ci skip') && !contains(github.event.head_commit.message, 'skip ci')" + steps: + - name: Checkout source + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: true + + - name: Download latest auto + run: | + auto_download_url="$(curl -fsSL https://api.github.com/repos/intuit/auto/releases/latest | jq -r '.assets[] | select(.name == "auto-linux.gz") | .browser_download_url')" + wget -O- "$auto_download_url" | gunzip > ~/auto + chmod a+x ~/auto + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install build & twine + run: python -m pip install --upgrade build twine + + - name: Create release + run: | + echo "@${{ github.actor }} is creating a release triggered by ${{ github.event_name }}" + if [ "${{ github.event_name }}" = workflow_dispatch ] + then opts= + else opts=--only-publish-with-release-label + fi + ~/auto shipit -vv $opts + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + +# vim:set sts=2: diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c425c0e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,50 @@ +# v0.1.0 (Mon Jun 01 2026) + +Initial public alpha release of `bids-utils` — a Python library and CLI for +structural manipulation of BIDS datasets, designed to be schema-driven +(via [`bidsschematools`][bst]) and VCS-aware (plain git, git-annex, and +DataLad). + +#### 🚀 What's in 0.1.0 + +- **CLI commands**: `rename`, `migrate`, `subject-rename`, `session-rename`, + `remove-subject`, `remove-session`, `remove-run`, `merge`, `split`, + `aggregate-metadata`, `segregate-metadata`, `audit-metadata`, `completion`. +- **Library API**: every CLI command maps to a public Python function with + `OperationResult` return values; suitable for scripted pipelines. +- **Migration engine**: applies BIDS 1.x intra-major deprecation rules + (`BasedOn`/`RawSources` → `Sources`, `ScanDate` → `_scans.tsv` `acq_time`, + `DCOffsetCorrection` → `SoftwareFilters`, `AcquisitionDuration` → + `FrameAcquisitionDuration`, deprecated coordinate-system labels, etc.) at + `safe` / `advisory` / `non-auto-fixable` tiers, with a schema-coverage + audit (`migrate --audit`) so missing rules surface as drift. +- **VCS integration**: detects DataLad → git-annex → git → plain + filesystem; uses `git mv` (or the annex equivalent) for moves; preserves + locked-annexed-symlink semantics during renames; configurable + `--annexed={error,get,skip,skip-warning}` policy for fetched content. +- **Observability**: `--dry-run=overview|detailed`, `--json` output mode, + exit codes (0 success / 1 partial / 2 refused), and per-source `Change` + entries in batch operations. +- **Test infrastructure**: per-test `git worktree` fixture pattern over the + `bids-examples` clone, plus an adversarial-suffix injection helper for + heudiconv `__dup-NN` / `+mine` / `--crap` / `_test` source filenames. + Mutating sweeps validate against the BIDS validator pre- and + post-operation across ~1700 cases. + +#### ⚠ Known limitations + +- The `rename --set` / `edit-filename` split (FR-039 / FR-040, atomic-batch + glob input per FR-043) is specified but **not yet implemented** — + scheduled for 0.2.0. Use `rename --set key=value FILE` (current form) for + entity edits until then. +- BIDS 2.0 migration is scaffolding-only — concrete rules are stubs. +- Performance optimization for >1000-subject datasets is unverified + (SC-003 deferred). + +#### Authors: 1 + +- Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) + +[bst]: https://github.com/bids-standard/bids-specification/tree/master/tools/schemacode + +--- diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bb9839a..364b4d6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,3 +41,47 @@ If any environment fails: - `click` — CLI framework (core dep) - `packaging` — version comparison for migration (core dep) - All version specs live in `pyproject.toml` (single source of truth) + +## Pull Requests and Release Labels + +Releases on this project are driven by [intuit/auto][auto] — version bumps +and PyPI uploads happen automatically when a PR is merged to `main`, based +on the labels applied to that PR. **Picking the right labels is part of +the review process.** + +### Bump labels (pick exactly one when you intend a release) + +| Label | Effect | Use for | +| --- | --- | --- | +| `major` | bumps `X.0.0` | Breaking changes to the CLI surface or library API | +| `minor` | bumps `0.X.0` | New features, new commands, backwards-compatible additions | +| `patch` | bumps `0.0.X` | Bug fixes, performance, internal refactors, docs that ship in a release | + +### Other labels + +| Label | Effect | Use for | +| --- | --- | --- | +| `release` | Required alongside `major`/`minor`/`patch` to actually publish on merge. Without it the workflow exits early. | The PR that should trigger a release | +| `internal` | No version bump (counted as "skip-release") | Test-only changes, CI tweaks, refactors that don't need a release | +| `documentation` | No version bump | Docs-only PRs (mkdocs / README / CHANGELOG fixups) | +| `tests` | No version bump | Pure test additions | +| `dependencies` | No version bump | Routine dep bumps (e.g. Dependabot) | +| `performance` | No version bump on its own — combine with a bump label if a perf change should ship | Performance-only changes | +| `released` | Applied automatically by `auto` after the release ships | (do not apply manually) | + +If multiple PRs are merged before a release fires, `auto` aggregates their +labels and bumps to the highest of (`major` > `minor` > `patch`). + +### What the maintainer does + +1. Apply the appropriate bump label and (if you want to release on merge) + the `release` label. +2. Merge to `main`. +3. The `release.yml` workflow runs `auto shipit`, which builds the + changelog entry, tags the release, creates the GitHub release, and + uploads to PyPI. No manual steps. + +See [RELEASING.md](RELEASING.md) for the full release procedure, including +the manual v0.1.0 cutover and one-time repo-settings tasks. + +[auto]: https://intuit.github.io/auto/ diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..899bccb --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,190 @@ +# Releasing bids-utils + +This project uses [`intuit/auto`][auto] for label-driven releases on PyPI. + +In normal operation **maintainers do not run any release command by hand** — +labels on merged PRs drive everything via `.github/workflows/release.yml`. +The exceptions are (a) the first release (`v0.1.0`) and (b) one-off manual +ships triggered through `workflow_dispatch`. Both are documented below. + +## TL;DR — releasing a new version (0.1.1 onward) + +1. On the PR you want to ship, apply **one bump label** (`major`, `minor`, or + `patch`) **and** the `release` label. +2. Merge the PR to `main`. +3. The `release.yml` workflow runs `auto shipit + --only-publish-with-release-label`, which: + - computes the new version from the bump label, + - prepends a new entry to `CHANGELOG.md`, + - creates the git tag (`vX.Y.Z`) and the GitHub release, + - runs `python -m build && twine upload dist/*` to publish to PyPI. +4. After the workflow succeeds, the merged PR is auto-commented with + "Released on `vX.Y.Z`" and labelled `released`. + +If multiple PRs are merged before a release fires, `auto` aggregates their +labels and bumps to the highest of (`major` > `minor` > `patch`). + +## Manual ship via workflow_dispatch + +Go to **Actions → Auto-release on PR merge → Run workflow** on GitHub. +This skips the `--only-publish-with-release-label` filter and releases +whatever's on `main` since the last tag. Useful for shipping a hot fix +where the bump label was forgotten. + +--- + +## Cutting v0.1.0 (initial release) — full runbook + +The first release is intentionally manual because `auto` needs a baseline +tag (`v0.1.0`) to start counting PR labels from. After 0.1.0 the +label-driven workflow takes over. + +### Step 0 — prerequisites (one-time, on the maintainer's machine) + +```bash +# Ensure local build tooling is available +pip install --upgrade build twine + +# Optional but recommended: install auto so you can dry-run locally +npm install -g auto # or use the binary download path from release.yml + +# Ensure your PyPI account has write access to the bids-utils project, +# OR that you have a project-scoped API token for it +``` + +### Step 1 — merge the release-pipeline branch + +Merge the `enh-auto-release` PR (containing `.autorc`, +`.github/workflows/release.yml`, `CHANGELOG.md`, `RELEASING.md`, +CONTRIBUTING.md updates) into `main`. The first release CANNOT proceed +until this is on `main`, because `release.yml` and the `auto` config are +what wire everything together. + +### Step 2 — create repo labels and the PyPI token (one-time, on GitHub side) + +```bash +# Create the auto release labels (major, minor, patch, release, ...) +GH_TOKEN=$(git config hub.oauthtoken) intuit-auto create-labels +``` + +Then on the GitHub web UI: + +1. Generate a project-scoped API token at + (scoped to `bids-utils`). +2. Add it as repo secret `PYPI_TOKEN` under + *Settings → Secrets and variables → Actions*. + +If you prefer OIDC-based publishing without a long-lived token, see +"Optional: PyPI Trusted Publishing" below — equivalent end-state, just a +different setup. + +### Step 3 — cut the tag and publish + +```bash +# 1. Make sure you're on main and clean +git checkout main +git pull --ff-only +git status # must be clean + +# 2. Run the full local test suite as a final gate +tox + +# 3. Sanity-check what hatch-vcs will produce AT the tag +# (without the tag, this shows a 0.1.devNN+gSHA dev version) +python -m build +ls dist/ +rm -rf dist/ # discard the dev-version artifacts; we'll rebuild at the tag + +# 4. Tag v0.1.0 and push the tag +git tag -a v0.1.0 -m "v0.1.0" +git push origin v0.1.0 + +# 5. Build the release artifacts from the tagged commit +python -m build +ls dist/ +# Expect: bids_utils-0.1.0-py3-none-any.whl AND bids_utils-0.1.0.tar.gz + +# 6. (Optional but recommended) twine check before upload +twine check dist/* + +# 7. Upload to PyPI +twine upload dist/* +# When prompted: +# Username: __token__ +# Password: + +# 8. Create the GitHub release with the CHANGELOG entry as the body +gh release create v0.1.0 --title "v0.1.0" \ + --notes "$(awk '/^# v0\.1\.0/,/^---$/' CHANGELOG.md | sed '$d')" +``` + +### Step 4 — sanity-check the release + +```bash +# Verify the artifact is up on PyPI +pip index versions bids-utils + +# Verify a fresh install works +python -m venv /tmp/bids-utils-smoke && \ + /tmp/bids-utils-smoke/bin/pip install bids-utils==0.1.0 && \ + /tmp/bids-utils-smoke/bin/bids-utils --help +``` + +### Step 5 — confirm the release.yml workflow is healthy + +After v0.1.0 is tagged, the next merge to `main` will trigger +`release.yml`. If that merged PR has no `release` label, the workflow +exits early (this is the `--only-publish-with-release-label` behaviour) +— this is the expected idle state and confirms the wiring is correct. + +To force a no-op smoke test of the workflow, use **Actions → Auto-release +on PR merge → Run workflow → main**: it should report "no PRs found to +release" or similar, NOT a permission error. + +If the workflow fails on the first auto-driven release attempt with +a 403/permission error from `github-actions[bot]`, see "Upgrading to +the protected-branch pattern" below. + +--- + +## Upgrading to the protected-branch pattern + +If branch protection on `main` rejects pushes from `github-actions[bot]`, +the auto-release will fail with a permission error. Switch from the +default-token flow to a bot-PAT flow: + +1. Create a dedicated bot account (e.g. `bidsbot`) with `write` access to + the repo. +2. Store its PAT as repo secret `PROTECTED_BRANCH_REVIEWER_TOKEN`. +3. Add `protected-branch` to the `plugins` list in `.autorc`. +4. Add `PROTECTED_BRANCH_REVIEWER_TOKEN: ${{ secrets.GH_TOKEN }}` to the + `env:` block of the release job, and `${{ secrets.GH_TOKEN }}` as + `GH_TOKEN` instead of `secrets.GITHUB_TOKEN`. + +See [dandi-cli's setup][dandi-release] for a reference. + +## Optional: PR label enforcement + +If you want CI to refuse a PR that's missing a bump label, copy +[dandi-cli's `labels.yml`][dandi-labels] into `.github/workflows/`. This +adds an `actions/github-script` check that fails the PR unless one of +`[major, minor, patch, performance, internal, documentation, tests, +dependencies]` is applied. + +## Optional: PyPI Trusted Publishing + +For projects that prefer OIDC-based publishing over a long-lived PyPI +token: + +1. On , + register `bids-standard/bids-utils` with workflow `release.yml` as a + Trusted Publisher. +2. Replace the `twine upload dist/*` portion of the `afterRelease` hook + (or the equivalent step in `release.yml`) with the + `pypa/gh-action-pypi-publish@release/v1` step. +3. Add `permissions: id-token: write` to the release job. +4. Drop the `PYPI_TOKEN` secret. + +[auto]: https://intuit.github.io/auto/ +[dandi-release]: https://github.com/dandi/dandi-cli/blob/master/.github/workflows/release.yml +[dandi-labels]: https://github.com/dandi/dandi-cli/blob/master/.github/workflows/labels.yml