llmenv 2.0.5

Universal scope-aware environment for AI coding agents
Documentation
# Releasing llmenv

Releases are **tag-triggered**. Pushing a `v*` tag to GitHub fires
[`.github/workflows/release.yml`](.github/workflows/release.yml), which does all
the publishing work:

- builds the cross-platform binaries (Linux x86_64, macOS x86_64, macOS arm64)
  with SHA-256 checksums and SLSA provenance,
- publishes the crate to [crates.io]https://crates.io/crates/llmenv
  (`publish-crate` job),
- creates the GitHub Release with the binaries attached,
- bumps the Homebrew formula in `phaedrus1992/homebrew-tap`.

[`cargo-release`](https://github.com/crate-ci/cargo-release) owns only the local
prep: bumping the version and rolling `CHANGELOG.md` into a single commit. It is
configured (in [`release.toml`](release.toml)) to **not** publish, tag, or push
— see [Why cargo-release does so little](#why-cargo-release-does-so-little).

## Branch strategy

Feature development happens on `main`. Each major.minor version gets a long-lived
`release/X.X.x` branch (created from the release tag) for managing bug fixes
without picking up new feature work.

**Backport policy** — fixes are applied (when feasible) to:

| Branch | Description |
|--------|-------------|
| `release/X.X.x` | Current major.minor — always patched |
| `release/X.(X-1).x` | Previous minor of the current major — always patched |
| `release/(X-1).Y.x` | Last minor branch of the previous major — always patched |

Fix in the **oldest applicable branch** first, then merge forward through the
chain to carry the fix (and its CHANGELOG entry) into newer branches
automatically. Only skip a backport when the fix does not apply to an older
branch — document the skip in the PR description.

**Example:** a fix that applies to 1.0, 1.1, and main:
1. Land fix on `release/1.0.x`
2. Merge `release/1.0.x``release/1.1.x`
3. Merge `release/1.1.x``main`

The CHANGELOG entry written on `release/1.0.x` propagates forward via the merges
while it still lives under `## [Unreleased]` — no cherry-picking needed. Once a
branch **cuts a release**, that entry is frozen under a versioned heading, and
the cross-listing rule below takes over.

**Do not manually cherry-pick or re-apply the fix to newer branches.** The
`forward-merge-release` workflow does this automatically after every push to a
release branch. If it fails (conflict, skipped branch), resolve the conflict
in the merge PR it opens — don't work around it by applying the change twice.

### Forward-merged fixes appear in every release that ships them

A fix lands once (on the oldest branch) but **ships in a separate release on
every branch it reaches**. Each of those releases is a distinct version with its
own changelog section, and a user reading any one of them must see the fix that
shipped in it. So:

> **Rule:** When a release inherits a fix via forward-merge, that fix's entry
> must appear under that release's version heading too — referencing the oldest
> version it was first fixed in.

This is *not* the duplicate-entry case the workflow avoids. Forward-merge keeps
the entry flowing **while it sits in `[Unreleased]`**. The gap appears later:
the older branch cuts its release first (entry freezes under, say, `[1.0.13]`),
then weeks later the newer branch cuts *its* release — and the fix, long since
merged into its code, is invisible in the new version's section because the
entry froze on the older branch.

When cutting a release on a newer branch, check what forward-merged in since its
last tag (`git log --no-merges <lasttag>..HEAD`) and add an entry for any
user-facing fix that originated downstream, attributing the origin:

```markdown
## [2.0.4]

### Fixed

- Fix `llmenv plugin-sync` dropping object-form marketplace sources
  (originally fixed in 1.0.13)
```

The `(originally fixed in X.Y.Z)` back-reference tells the user the fix is not
new behavior unique to this line — it is the same fix that shipped earlier on an
older release line, now also in this version. Reference the **oldest** version
that carried it, not the immediately-preceding branch.

**No automation enforces this — so check on every changelog edit.** Nothing
triggers a docs update when a forward-merge lands, so the cross-listing can only
be caught by hand. Make it a reflex: **any time you modify `CHANGELOG.md`** (not
only when cutting a release), first reconcile against what has forward-merged in:

```bash
# What landed since this branch's last tag, and from where?
git log --no-merges <last-tag>..HEAD
# What user-facing entries exist on the older line that aren't here yet?
git show origin/release/<older>.x:CHANGELOG.md
```

Add any missing user-facing fix to the appropriate section with its
`(originally fixed in X.Y.Z)` back-reference before finishing your edit. A
changelog edit that ignores an unlisted forward-merged fix is incomplete.

### Creating a release branch

After tagging a new major.minor (e.g. `v1.1.0`), branch immediately from that
tag so the branch starts at exactly what was released:

```bash
git checkout -b release/1.1.x v1.1.0
git push -u origin release/1.1.x
```

### Cutting a patch release from a release branch

```bash
git switch release/1.1.x && git pull
cargo release patch --workspace            # dry-run preview
cargo release patch --workspace --execute  # bump all crates + roll CHANGELOG + commit
git push -u origin HEAD
gh pr create --base release/1.1.x --fill
# After merge, tag the merged commit:
git switch release/1.1.x && git pull
git tag -a "v1.1.1" -m "v1.1.1"
git push origin "v1.1.1"
```

After the patch tag is pushed, merge forward into the next release branch (or
`main`) so the fix and its CHANGELOG entry propagate.

## One-time setup

```bash
cargo install cargo-release@1.1.2
```

Repo prerequisites (already in place, listed so they are not forgotten):

- **`CARGO_REGISTRY_TOKEN`** secret in the repo settings — the `publish-crate`
  job reads it. Without it, the tag build fails at publish.
- **`HOMEBREW_TAP_TOKEN`** secret — used by the `update-homebrew` job.
- The crate name **`llmenv`** must be owned by the publishing account on
  crates.io. The first publish claims it; confirm it is available beforehand.

## Cutting a release

`main` is protected (PR-only), so the version-bump commit lands through a PR and
the tag is cut on the merged commit.

### 1. Prepare the bump on a branch

```bash
git switch main && git pull
git switch -c release/<next-version>
cargo release <patch|minor|major> --workspace            # dry-run preview (default)
cargo release <patch|minor|major> --workspace --execute  # apply: bump all crates + roll CHANGELOG, commit
```

`cargo release --workspace` bumps all workspace crates to the same version,
turns the `[Unreleased]` CHANGELOG section into a dated `[<version>]` section,
re-seeds a fresh `[Unreleased]` + compare link, and makes one
`chore(release): <version>` commit. The `--workspace` flag is required — without
it only the root crate is bumped, leaving sub-crates at the old version.

### 2. PR and merge

```bash
git push -u origin HEAD
gh pr create --fill
# ... review, then merge to main
```

### 3. Tag the merged commit

After the PR merges, tag the resulting `main` commit and push the tag. **This is
what triggers the release.**

```bash
git switch main && git pull
git tag -a "v<version>" -m "v<version>"
git push origin "v<version>"
```

### 4. Watch the release

```bash
gh run watch
```

CI publishes to crates.io, creates the GitHub Release, and updates Homebrew. The
crates.io publish runs exactly once per tag — re-pushing an existing tag will
fail at publish because that version already exists on the registry.

## Why cargo-release does so little

`cargo-release` is fully capable of tagging, pushing, and publishing. We disable
all three deliberately:

- **`publish = false`** — crates.io publishing is owned by the `publish-crate`
  job in `release.yml`. If `cargo release` also published, every release would
  attempt to publish twice.
- **`tag = false` / `push = false`**`main` is protected, so the bump commit
  must go through a PR. Tagging on the prep branch would point the tag at a
  commit that is not on `main` after merge. Cutting the tag on the merged commit
  (step 3) keeps the `v*` tag pointing at exactly what shipped.

If branch protection is ever lifted for the maintainer, `release.toml` can be
switched to `tag = true` / `push = true` for a single-command release.

## Security of the release trigger

The release is fired by the `v*` tag, and `release.yml` hands `CARGO_REGISTRY_TOKEN`
and `HOMEBREW_TAP_TOKEN` to whatever commit that tag points at. Two protections
keep an attacker from pushing a malicious tag straight to a publish:

- **`main` is branch-protected** so release content lands through review.
- **Add a tag protection rule for `v*`** (repo Settings → Tags) so only
  maintainers can create release tags.

If either protection is ever lifted, **rotate both secrets** — a contributor who
can push a `v*` tag or an arbitrary `main` commit can otherwise publish under the
project's crates.io and Homebrew credentials.

## The 1.0.0 release

1.0.0 is already prepared on `main`: `Cargo.toml` is at `1.0.0` and `CHANGELOG.md`
has a dated `[1.0.0]` section. To publish it, run **step 3** above with
`v1.0.0` — no `cargo release` run is needed for this first release. Every release
after 1.0.0 uses the full flow.