Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 100 additions & 4 deletions .github/workflows/auto-version.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,18 @@ jobs:
echo "version=${next}" >> "$GITHUB_OUTPUT"
echo "tag=${tag}" >> "$GITHUB_OUTPUT"

- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
cache: true
cache-dependency-path: go.sum

- name: Update source version
shell: bash
env:
VERSION: ${{ steps.version.outputs.version }}
TAG: ${{ steps.version.outputs.tag }}
run: |
set -euo pipefail
sed -i -E "s/^var version = \"[^\"]+\"/var version = \"${VERSION}\"/" cmd/bomly/main.go
Expand All @@ -119,7 +127,7 @@ jobs:

git diff -- cmd/bomly/main.go npm/package.json server.json

- name: Commit version bump and tag
- name: Commit version bump (commit A)
shell: bash
env:
TAG: ${{ steps.version.outputs.tag }}
Expand All @@ -130,11 +138,99 @@ jobs:
git config user.email "${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com"

git add cmd/bomly/main.go npm/package.json server.json
git commit -m "chore(release): ${TAG} [skip ci]"
git tag -a "${TAG}" -m "Release ${TAG}"
if git diff --cached --quiet; then
# Rerun after a partial failure: the bump commit is already on
# main, so fall through to the (idempotent) later phases.
echo "version bump for ${TAG} already committed on main; skipping commit A"
else
git commit -m "chore(release): ${TAG} [skip ci]"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate generated pins before creating release tags

The release commit now changes component requirements, but its [skip ci] marker suppresses the push workflow containing .github/workflows/ci.yml's pinned-build job. The subsequent release validation runs make test with the committed workspace enabled, so it can mask an invalid or incomplete root-module pin; GOWORK=off is not exercised until GoReleaser runs after immutable tags have already been pushed. Remove the skip marker or run an explicit workspace-off build before tagging.

AGENTS.md reference: AGENTS.md:L84-L85

Useful? React with 👍 / 👎.

git push origin HEAD:main
fi

- name: Resolve component anchor commit (commit A)
id: anchor
shell: bash
env:
TAG: ${{ steps.version.outputs.tag }}
run: |
set -euo pipefail
# Component tags anchor at commit A, the version bump. On the first
# run that is HEAD. On a rerun after a partial failure, main may
# already carry the pin commit B; the component tags pushed before
# the failure then define A (they all point at it).
anchor="$(git ls-remote --tags origin 'refs/tags/components/*' |
{ grep -F "/${TAG}^{}" || true; } | awk '{print $1; exit}')"
if [[ -z "${anchor}" ]]; then
anchor="$(git rev-parse HEAD)"
fi
echo "commit A (component tag anchor): ${anchor}"
echo "commit=${anchor}" >> "$GITHUB_OUTPUT"

git push origin HEAD:main
- name: Tag component modules at commit A
shell: bash
env:
TAG: ${{ steps.version.outputs.tag }}
ANCHOR: ${{ steps.anchor.outputs.commit }}
run: |
set -euo pipefail
# Lockstep: every component module is tagged at the version-bump
# commit A, making `go get <module>@${TAG}` resolvable before the
# root go.mod pins it. The script is idempotent (remote tags at the
# release commit are skipped; a remote tag anywhere else aborts), so
# a rerun after a partial failure completes the tag set.
./scripts/release-components.sh tag --version "${TAG}" --commit "${ANCHOR}"

- name: Pin component modules (commit B)
shell: bash
env:
TAG: ${{ steps.version.outputs.tag }}
run: |
set -euo pipefail
# Pin the root go.mod/go.sum to the component tags just pushed and
# commit the result (commit B). The root tag lands on commit B, so
# every root tag carries resolvable pins and no replace directives —
# remote `go install .../cmd/bomly@latest` keeps working. With no
# component modules (or on a rerun that already pinned), this is a
# clean no-op.
./scripts/release-components.sh pin --version "${TAG}"
git add go.mod go.sum
if git diff --cached --quiet; then
echo "component pins for ${TAG} already in place; skipping commit B"
else
git commit -m "chore(release): pin components ${TAG} [skip ci]"
git push origin HEAD:main
fi

- name: Tag root module and verify lockstep
shell: bash
env:
TAG: ${{ steps.version.outputs.tag }}
ANCHOR: ${{ steps.anchor.outputs.commit }}
run: |
set -euo pipefail
head_commit="$(git rev-parse HEAD)"
# Lockstep invariant: commit B only touches the root go.mod/go.sum,
# so the components/ tree must be identical between the component
# tags (commit A) and the root tag (commit B). Component module zips
# contain only their own subtree, so tree identity means the tags
# describe exactly the code shipping in this release.
if [[ "${ANCHOR}" != "${head_commit}" ]]; then
if ! git diff --quiet "${ANCHOR}" "${head_commit}" -- components/; then
echo "components/ tree changed between commit A (${ANCHOR}) and commit B (${head_commit}); lockstep invariant violated" >&2
exit 1
fi
fi
if existing="$(git rev-parse -q --verify "refs/tags/${TAG}^{commit}")"; then
if [[ "${existing}" != "${head_commit}" ]]; then
echo "tag ${TAG} already exists at ${existing}, not ${head_commit}" >&2
exit 1
fi
echo "root tag ${TAG} already exists at ${head_commit}; pushing"
else
git tag -a "${TAG}" -m "Release ${TAG}"
fi
git push origin "${TAG}"
./scripts/release-components.sh verify --version "${TAG}"
Comment on lines 232 to +233

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Verify lockstep before triggering the release

Pushing the root tag here immediately triggers .github/workflows/release.yml through its push.tags: "v*.*.*" trigger, so the Release jobs can start before the following verify command succeeds; the later explicit workflow_dispatch does not provide the intended gate. If verification detects a missing or mismatched component tag, publishing may already be running against the inconsistent, immutable root tag. Verify against the prospective root commit before pushing, or rely solely on the post-verification dispatch.

AGENTS.md reference: AGENTS.md:L84-L87

Useful? React with 👍 / 👎.


- name: Start release workflow
env:
Expand Down
15 changes: 8 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,12 @@ jobs:
id: pins
shell: bash
# A wave PR lands component modules consumed through the committed
# go.work; the root go.mod cannot require them until the release train
# tags them (make release-components ARGS=--apply) and the pin-bump PR
# merges. Until every component module under components/ is required
# by the root go.mod, a workspace-off build cannot succeed by design,
# so skip the pinned build instead of failing it.
# go.work; the root go.mod cannot require them until the automated
# release train in auto-version.yml tags them at the version-bump
# commit and pushes the pin commit. Until every component module under
# components/ is required by the root go.mod, a workspace-off build
# cannot succeed by design, so skip the pinned build instead of
# failing it.
run: |
set -euo pipefail
missing=0
Expand Down Expand Up @@ -155,8 +156,8 @@ jobs:
# component module under components/ is workspace-only (consumed via
# the committed go.work, not yet pinned in the root go.mod), root tidy
# cannot resolve the component import paths, so the root check is
# skipped until the release train tags the components and the
# pin-bump PR lands.
# skipped until auto-version.yml's automated release train tags the
# components and pushes the pin commit.
run: |
set -euo pipefail
skip=0
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ Runtime preparation is owned by `internal/engine`: build the filtered registry o
- Extracted components will live under `components/<kind>/<name>/` as separate Go modules with their own `go.mod`, tagged per module as `components/<kind>/<name>/vX.Y.Z`.
- The committed `go.work` puts the repo in workspace mode for local development; waves add `use ./components/...` entries. Release and pinned builds run with `GOWORK=off` (GoReleaser sets it explicitly; CI's `pinned-build` job verifies the module pins alone still build on pushes to `main`).
- Each extraction wave lands as **one atomic PR**: move the code into its component module, add the `use` entry, and keep the root module compiling in the same change.
- `scripts/release-components.sh` (also `make release-components`) is the release train: component modules version in lockstep with the CLI — after a CLI release tag exists, the script tags every component module at that same version (idempotent; unchanged modules get empty releases by design); `--apply` (ARGS="--apply") creates and pushes the tags and prints the root `go get` pin bumps for the follow-up PR.
- The release train is fully automated in `.github/workflows/auto-version.yml` and is a two-commit flow: version-bump commit A on `main` → every `components/<kind>/<name>/vX.Y.Z` tag at A (so `go get <module>@vX.Y.Z` resolves) → root `go.mod`/`go.sum` pin commit B (`[skip ci]`) → root `vX.Y.Z` tag at B → release dispatch. The two commits exist so the root `go.mod` never needs `replace` directives: every root tag carries resolvable pins, preserving remote `go install .../cmd/bomly@latest` — the Go equivalent of Maven parent-version inheritance (one authoritative version; automation propagates it). Lockstep invariant: the `components/` tree is identical between A and B (commit B touches only root `go.mod`/`go.sum`), so unchanged modules get empty releases by design. `scripts/release-components.sh` (also `make release-components` for the dry run) provides the `tag` / `pin` / `verify` subcommands the workflow uses; run them manually only to recover from a partial failure.

### Package Boundaries

Expand Down Expand Up @@ -277,7 +277,7 @@ Any new user-visible feature needs a smoke case under `test/smoke/` — follow t

## Release

Draft releases are created automatically after merges to `main` from commit prefixes: `feat:` → minor, other → patch, `type!:`/`BREAKING CHANGE:` → major, `[skip release]` → none. Squash titles count. Publishing runs GoReleaser with signed checksums and SLSA provenance; see `dev-docs/RELEASE_CHECKLIST.md`.
Draft releases are created automatically after merges to `main` from commit prefixes: `feat:` → minor, other → patch, `type!:`/`BREAKING CHANGE:` → major, `[skip release]` → none. Squash titles count. The `Auto Version` workflow runs the whole lockstep release train (version-bump commit, component tags, pin commit, root tag, release dispatch). Publishing runs GoReleaser with signed checksums and SLSA provenance; see `dev-docs/RELEASE_CHECKLIST.md`.

## Reference Docs

Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ Runtime preparation is owned by `internal/engine`: build the filtered registry o
- Extracted components will live under `components/<kind>/<name>/` as separate Go modules with their own `go.mod`, tagged per module as `components/<kind>/<name>/vX.Y.Z`.
- The committed `go.work` puts the repo in workspace mode for local development; waves add `use ./components/...` entries. Release and pinned builds run with `GOWORK=off` (GoReleaser sets it explicitly; CI's `pinned-build` job verifies the module pins alone still build on pushes to `main`).
- Each extraction wave lands as **one atomic PR**: move the code into its component module, add the `use` entry, and keep the root module compiling in the same change.
- `scripts/release-components.sh` (also `make release-components`) is the release train: component modules version in lockstep with the CLI — after a CLI release tag exists, the script tags every component module at that same version (idempotent; unchanged modules get empty releases by design); `--apply` (ARGS="--apply") creates and pushes the tags and prints the root `go get` pin bumps for the follow-up PR.
- The release train is fully automated in `.github/workflows/auto-version.yml` and is a two-commit flow: version-bump commit A on `main` → every `components/<kind>/<name>/vX.Y.Z` tag at A (so `go get <module>@vX.Y.Z` resolves) → root `go.mod`/`go.sum` pin commit B (`[skip ci]`) → root `vX.Y.Z` tag at B → release dispatch. The two commits exist so the root `go.mod` never needs `replace` directives: every root tag carries resolvable pins, preserving remote `go install .../cmd/bomly@latest` — the Go equivalent of Maven parent-version inheritance (one authoritative version; automation propagates it). Lockstep invariant: the `components/` tree is identical between A and B (commit B touches only root `go.mod`/`go.sum`), so unchanged modules get empty releases by design. `scripts/release-components.sh` (also `make release-components` for the dry run) provides the `tag` / `pin` / `verify` subcommands the workflow uses; run them manually only to recover from a partial failure.

### Package Boundaries

Expand Down Expand Up @@ -277,7 +277,7 @@ Any new user-visible feature needs a smoke case under `test/smoke/` — follow t

## Release

Draft releases are created automatically after merges to `main` from commit prefixes: `feat:` → minor, other → patch, `type!:`/`BREAKING CHANGE:` → major, `[skip release]` → none. Squash titles count. Publishing runs GoReleaser with signed checksums and SLSA provenance; see `dev-docs/RELEASE_CHECKLIST.md`.
Draft releases are created automatically after merges to `main` from commit prefixes: `feat:` → minor, other → patch, `type!:`/`BREAKING CHANGE:` → major, `[skip release]` → none. Squash titles count. The `Auto Version` workflow runs the whole lockstep release train (version-bump commit, component tags, pin commit, root tag, release dispatch). Publishing runs GoReleaser with signed checksums and SLSA provenance; see `dev-docs/RELEASE_CHECKLIST.md`.

## Reference Docs

Expand Down
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ smoke:
fuzz:
FUZZTIME="$(FUZZTIME)" scripts/run-fuzz.sh

# Dry-run the component release train (pass ARGS="--apply" to tag and push).
# Dry-run the component release train at the latest release tag. The train
# itself is fully automated in .github/workflows/auto-version.yml; the script's
# `tag`, `pin`, and `verify` subcommands (ARGS="tag --version vX.Y.Z --commit
# <sha>", ARGS="pin --version vX.Y.Z", ARGS="verify --version vX.Y.Z") exist
# for recovery after a partial failure.
release-components:
./scripts/release-components.sh $(if $(ARGS),$(ARGS),)

Expand Down
39 changes: 32 additions & 7 deletions dev-docs/CI.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ its own minimal CI in its own repository.
| `Fuzz` | Nightly schedule, manual dispatch | Native Go fuzzing over the `scripts/run-fuzz.sh` target list; uploads minimized failures as artifacts |
| `CodeQL` | Pull requests, pushes, schedule | Static analysis for Go and JavaScript |
| `SBOM Interoperability` | Schedule, manual dispatch | Binary-driven SBOM export/ingest checks against third-party tools |
| `Auto Version` | Pushes to `main` | Computes the next semver from the squash-commit prefix and creates a draft release tag |
| `Auto Version` | Manual dispatch (from `main`) | Fully automated lockstep release train: version-bump commit, component tags, pin commit, root tag, `Release` dispatch |
| `Release` | Release tags | GoReleaser build/publish with signed checksums and SLSA provenance |
| `Scorecard`, `Dependency Review`, `Bomly Guard` | Various | Supply-chain posture checks and dogfooding |

Expand Down Expand Up @@ -53,9 +53,34 @@ regression seed.

## Releases

Merges to `main` create draft releases automatically from conventional-commit
prefixes (`feat:` → minor, other → patch, `type!:`/`BREAKING CHANGE:` → major,
`[skip release]` → none; squash titles count). Publishing runs GoReleaser —
archives, Linux packages, Homebrew/Scoop/WinGet manifests, signed checksums,
and SLSA provenance. See `dev-docs/RELEASE_CHECKLIST.md` and
`docs/INSTALLATION.md` for the artifacts users receive.
Releasing is one `Auto Version` dispatch from `main` (choose `patch`,
`minor`, or `major`). The workflow runs the whole lockstep release train as
a two-commit flow:

1. Version-bump commit **A** on `main` — `cmd/bomly/main.go`, the npm
wrapper, and the MCP Registry versions. No root tag yet.
2. Every `components/<kind>/<name>/vX.Y.Z` tag at commit A, making
`go get <module>@vX.Y.Z` resolvable.
3. Pin commit **B** on `main` (`[skip ci]`) — the root `go.mod`/`go.sum`
pinned to the just-tagged component versions (`GOWORK=off`).
4. The root tag `vX.Y.Z` at commit B, then a `Release` dispatch.

The two commits exist because the root `go.mod` carries no `replace`
directives: every root tag has resolvable pins, so remote
`go install github.com/bomly-dev/bomly-cli/cmd/bomly@latest` keeps working —
the Go equivalent of Maven parent-version inheritance (one authoritative
version, propagated by automation). The lockstep invariant is tree identity:
commit B touches only the root `go.mod`/`go.sum`, so the `components/` tree
is identical between A and B, and component module zips (which contain only
their own subtree) describe exactly the code shipping in the CLI release.
The workflow asserts this before tagging the root, and
`scripts/release-components.sh verify` re-checks it against origin.

Every phase is idempotent, so rerunning a partially failed `Auto Version`
completes the train; `scripts/release-components.sh` exposes the same
`tag` / `pin` / `verify` subcommands for manual recovery.

Publishing runs GoReleaser — archives, Linux packages,
Homebrew/Scoop/WinGet manifests, signed checksums, and SLSA provenance. See
`dev-docs/RELEASE_CHECKLIST.md` and `docs/INSTALLATION.md` for the artifacts
users receive.
34 changes: 34 additions & 0 deletions dev-docs/RELEASE_CHECKLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,41 @@ Use this checklist when publishing a tagged Bomly CLI release.
## Release workflow

- Run `Auto Version` from `main`, choosing `patch`, `minor`, or `major`.
- `Auto Version` performs the whole lockstep release train in one dispatch,
as a two-commit flow:
1. Version-bump commit **A** on `main`: `cmd/bomly/main.go` plus the npm
wrapper and MCP Registry versions. No root tag yet.
2. Every component module tag (`components/<kind>/<name>/vX.Y.Z`) at
commit A. From here, `go get <module>@vX.Y.Z` resolves.
3. Pin commit **B** on `main` (`[skip ci]`): the root `go.mod`/`go.sum`
pinned to the just-tagged component versions (`GOWORK=off go get` +
`go mod tidy`).
4. The root tag `vX.Y.Z` at commit B, then a `Release` dispatch.
- Why two commits: the root `go.mod` carries **no `replace` directives**, so
every root tag has resolvable pins and remote
`go install github.com/bomly-dev/bomly-cli/cmd/bomly@latest` keeps
working. The component tags must exist before the pins can resolve — the
Go equivalent of Maven parent-version inheritance: one authoritative
version, propagated by automation.
- Lockstep invariant (asserted by the workflow and by `verify`): the
`components/` tree is identical between commits A and B — commit B touches
only the root `go.mod`/`go.sum`, and component module zips contain only
their own subtree, so the component tags describe exactly the code that
ships in the CLI release.
- Every phase is idempotent: rerunning a partially failed `Auto Version`
completes the missing pieces (component tags already on origin at commit A
are skipped; a tag on origin at any other commit aborts the run and must
be resolved on origin first; an already-present pin skips commit B). For
manual recovery, use
`./scripts/release-components.sh tag --version vX.Y.Z --commit <A>`,
`./scripts/release-components.sh pin --version vX.Y.Z` (then commit), and
check the invariant with
`./scripts/release-components.sh verify --version vX.Y.Z`. If the run
failed after the root tag was pushed, dispatch `Release` manually:
`gh workflow run release.yml --ref vX.Y.Z`.
- Wait for `Release` to finish.
- Run `./scripts/release-components.sh verify --version vX.Y.Z` if you want an
explicit post-release lockstep check (the workflow already ran it).
- Review the published GitHub release:
- `bomly` archives exist for Linux, macOS, and Windows on `amd64` and `arm64`.
- `bomly-lite` archives exist for the same platforms.
Expand Down
Loading