Skip to content

feat(release): opt into linux-musl artifacts via shared workflow - #48

Merged
brettdavies merged 2 commits into
devfrom
feat/ship-musl-artifacts
May 4, 2026
Merged

feat(release): opt into linux-musl artifacts via shared workflow#48
brettdavies merged 2 commits into
devfrom
feat/ship-musl-artifacts

Conversation

@brettdavies

@brettdavies brettdavies commented May 4, 2026

Copy link
Copy Markdown
Owner

Summary

Opts agentnative-cli into the two new shared-workflow inputs that landed in brettdavies/.github PR #18, so future
releases ship Alpine/musl artifacts. No source changes; the binary is identical to v0.3.0.

After this PR merges (and ships via a release/v0.3.1 cherry-pick to main), every release will produce 7 archives
instead of 5, adding agentnative-x86_64-unknown-linux-musl.tar.gz and
agentnative-aarch64-unknown-linux-musl.tar.gz. Alpine and other musl-libc-host distros gain a first-class install path
beyond cargo install and the existing glibc Linux artifact.

Bundled: a small RELEASES.md polish that was already dirty in the working tree (cliff.toml chore-skip warning + "never
hand-written" rule under Releasing dev to main), plus the necessary 5 → 7 target table update.

Changelog

Added

  • Ship x86_64- and aarch64-unknown-linux-musl static binaries on every release. Statically linked against musl libc,
    so they run on Alpine and other musl-libc-host distros without glibc, and on every glibc distro too.

Documentation

  • Document the cliff.toml chore-skip footgun and the "CHANGELOG is generated, never hand-written" rule in
    RELEASES.md under Releasing dev to main. Adds a new review step (renumbered to 9) and tightens the existing "PRs
    and changelog generation" section.

Type of Change

  • feat: New feature (non-breaking change which adds functionality)

Related Issues/Stories

Testing

  • Manual testing completed
  • All tests passing

Test Summary:

  • Unit tests: n/a (workflow opt-in only; no Rust source change)
  • Integration tests: n/a
  • Coverage: n/a

End-to-end validation completed in the throwaway spike/musl-build workflow before either upstream or this PR opened:

  • cross build --release --locked --target x86_64-unknown-linux-musl: green, ~1m45s, statically linked
  • cross build --release --locked --target aarch64-unknown-linux-musl: green, ~1m45s, statically linked
  • file target/.../release/anc matched statically linked for both arches
  • x86_64 host smoke test (anc --version): anc 0.3.0
  • Belt-and-suspenders: docker run --rm --pull always -v $PWD/target/.../release:/musl-bin:ro alpine:latest /musl-bin/anc --version: anc 0.3.0. Confirms exec compatibility with musl-libc host (Alpine), not just static
    linkage.

actionlint clean on the modified release.yml. The pre-push hook (scripts/hooks/pre-push: fmt, clippy -Dwarnings,
test, cargo-deny, Windows compat) passed.

Files Modified

Modified:

  • .github/workflows/release.yml: passes linux_musl_required: true and linux_musl_verify_alpine: true to the
    shared rust-release.yml workflow.
  • RELEASES.md: updates the release-pipeline table from 5 → 7 targets (naming the two musl rows and the two opt-in
    inputs), and bundles a separate cliff.toml chore-skip + "never hand-written" docs improvement that was already dirty
    in the working tree.

Created:

  • None.

Renamed:

  • None.

Deleted:

  • None.

Key Features

  • Alpine support: musl-static binaries run on Alpine without glibc, and on every glibc distro too.
  • Belt-and-suspenders Alpine exec verification in CI catches dynamic-link surprises (dlopen, NSS, locale) that pass
    the static-link file assertion.
  • Hard-fail behavior (linux_musl_required: true) means a musl regression blocks the release, not a silent artifact
    omission.

Benefits

  • Closes the install gap for Alpine users; the existing Homebrew bottle is glibc-linux only, and cargo install
    requires the full Rust toolchain. Now: wget + tar xf + run.
  • Distroless / scratch containers gain a usable artifact too.
  • The Alpine exec check turns a class of "ship now, find out later in a user bug report" failures into "fail at CI
    time."

Breaking Changes

  • No breaking changes

Deployment Notes

  • Standard release flow per RELEASES.md

After this PR squash-merges to dev, ship via the standard cherry-pick:

  1. Branch release/v0.3.1 from origin/main.
  2. Cherry-pick this commit; bump Cargo.toml 0.3.0 → 0.3.1; cargo update -p agentnative.
  3. Run ./scripts/generate-completions.sh, bash scripts/sync-skill-fixture.sh, ./scripts/generate-changelog.sh.
    Cross-check the generated CHANGELOG section against this PR's ## Changelog bullets per the cliff.toml chore-skip
    warning.
  4. Triple-diff verify, push, open release: v0.3.1 PR.
  5. After merge, annotated tag and push triggers the release pipeline (now with musl).
  6. After the release publishes, run ./scripts/sync-dev-after-release.sh v0.3.1 && git push origin dev per the new
    convention.

The throwaway spike/musl-build branch in this repo can be pruned (locally + remotely) any time after this PR opens; it
served its purpose and was never merged anywhere.

Checklist

  • Code follows project conventions and style guidelines
  • Commit messages follow Conventional Commits
  • Self-review of code completed
  • Tests added/updated and passing (validated via spike workflow before upstream PR opened)
  • No new warnings or errors introduced (actionlint clean; pre-push hook clean)
  • Changes are backward compatible

Pass the new dot-github rust-release.yml inputs that landed in #18 over
there:

  linux_musl_required: true       — block release on musl build failure
  linux_musl_verify_alpine: true  — exec the x86_64-musl binary inside
                                    alpine:latest after build to catch
                                    dlopen/NSS/locale surprises that pass
                                    the static-link `file` assertion

Both inputs were spike-validated end-to-end at spike/musl-build before
the upstream PR landed: cross builds for both musl arches succeeded
(~1m45s each, statically linked), x86_64 host smoke test passed, and the
binary executed cleanly inside alpine:latest (`anc 0.3.0`).

Also bundled (carried in the dirty working tree across the session;
related RELEASES.md polish):

  - Update RELEASES.md release-pipeline table from 5 → 7 targets, naming
    both musl rows and the two opt-in inputs.
  - Document the cliff.toml chore-skip footgun and the
    'CHANGELOG is generated, never hand-written' rule under `Releasing
    dev to main`. Adds a new step 9 (review CHANGELOG) and renumbers the
    push step to 10. Tightens the existing 'PRs and changelog
    generation' section to point at the new rule.
Upstream agentnative-site `dev` advanced from 0.1.0 to 0.2.0 since
this fixture was last synced. The drift-check workflow caught it on
PR #48. No host map change — only the version string differs — so the
generated Rust map (SkillHost / KNOWN_HOSTS / resolve_host) is
unaffected, but keeping the committed fixture in sync with upstream
prevents drift-check from failing on every subsequent PR.
@brettdavies
brettdavies merged commit 7298473 into dev May 4, 2026
7 checks passed
@brettdavies
brettdavies deleted the feat/ship-musl-artifacts branch May 4, 2026 18:24
@brettdavies brettdavies mentioned this pull request May 4, 2026
10 tasks
brettdavies added a commit that referenced this pull request May 4, 2026
## Summary

Releases v0.3.1, an artifact-set release: ships
`x86_64-unknown-linux-musl` and `aarch64-unknown-linux-musl` static
binaries on every release. No source change versus v0.3.0 — the binary
is identical; only the artifact set is wider.

Consolidates one dev commit:

- **#48 — `feat(release): opt into linux-musl artifacts via shared
workflow`** — passes `linux_musl_required: true` and
`linux_musl_verify_alpine: true` to the shared `rust-release.yml@main`
(those inputs landed in
brettdavies/.github#18). Bundled with the inputs change is a
`RELEASES.md` polish: 5 → 7 target table update plus a
separate cliff.toml chore-skip + "never hand-written" rule that was
pre-existing dirty in the working tree.

Triple-diff verification before opening: A is exactly the 6 expected
ship-surface files; B contains only release-prep
files (Cargo.toml, Cargo.lock, CHANGELOG.md) plus the always-divergent
completions/; no guarded engineering-doc paths
leaked.

## Changelog

### Added

- Ship `x86_64-` and `aarch64-unknown-linux-musl` static binaries on
every release. Statically linked against musl libc,
so they run on Alpine and other musl-libc-host distros without glibc,
and on every glibc distro too.

### Documentation

- Document the `cliff.toml` chore-skip footgun and the "CHANGELOG is
generated, never hand-written" rule in
  `RELEASES.md` under `Releasing dev to main`.
- Update the release-pipeline target table from 5 → 7 targets, naming
the two musl rows and the two opt-in inputs
  (`linux_musl_required`, `linux_musl_verify_alpine`).

## Type of Change

- [x] `feat`: New feature (non-breaking change which adds functionality)

## Related Issues/Stories

- Story: n/a
- Issue: n/a
- Architecture: n/a
- Related PRs: #48 (this release's only consolidated dev commit),
brettdavies/.github#17 (shared-workflow musl matrix
  rows + soft-fail), brettdavies/.github#18 (release of #17 to main)

## Testing

- [x] Manual testing completed
- [x] All tests passing

**Test Summary:**

- Unit tests: pass on dev as part of #48 CI (fmt, clippy, test, audit,
package check, drift-check)
- Integration tests: n/a (no source change vs v0.3.0)
- Coverage: unchanged

End-to-end musl validation completed pre-merge in the throwaway
`spike/musl-build` workflow:

- `cross build --release --locked --target x86_64-unknown-linux-musl` —
green, statically linked
- `cross build --release --locked --target aarch64-unknown-linux-musl` —
green, statically linked
- `file target/.../release/anc` matched `statically linked` for both
arches
- x86_64 host smoke test (`anc --version`) — `anc 0.3.0`
- Belt-and-suspenders: `docker run --rm --pull always -v
".../release:/musl-bin:ro" alpine:latest /musl-bin/anc
  --version` — `anc 0.3.0`

The `release` pipeline (now 7 targets) re-runs all of the above on the
v0.3.1 tag push, so the binaries shipped to the
GitHub Release will be the freshly-built ones, not the spike's.

## Files Modified

**Modified:**

- `.github/workflows/release.yml` — passes the two new shared-workflow
inputs.
- `Cargo.toml` — version 0.3.0 → 0.3.1.
- `Cargo.lock` — `cargo update -p agentnative` regen.
- `CHANGELOG.md` — `scripts/generate-changelog.sh` extracted #48's `##
Changelog` section into a v0.3.1 entry.
Cross-checked: cliff.toml chore-skip didn't drop anything (commit
subject was `feat`, not `chore`).
- `RELEASES.md` — release-pipeline table 5 → 7 targets + bundled
cliff.toml/never-hand-written documentation.
- `src/skill_install/skill.json` — fixture refresh from upstream
agentnative-site dev (caught by drift-check on #48;
  brought along here).

**Created:**

- None.

**Renamed:**

- None.

**Deleted:**

- None.

## Breaking Changes

- [x] No breaking changes

The binary surface is unchanged. New consumers on Alpine gain an install
path; existing consumers see no difference.

## Deployment Notes

After this PR squash-merges to `main`:

```bash
git checkout main && git pull
git tag -a -m "Release v0.3.1" v0.3.1
git push origin main --tags
```

The tag push triggers the now-7-target release pipeline. After the
GitHub Release reaches `published` (homebrew dispatch
and finalize-release flip), backport per the new convention:

```bash
./scripts/sync-dev-after-release.sh v0.3.1
git push origin dev
```

The throwaway `spike/musl-build` branch (local + remote) can be pruned
any time after this PR opens; it served its
purpose.

## Checklist

- [x] Code follows project conventions and style guidelines
- [x] Commit messages follow Conventional Commits
- [x] Self-review of code completed (PR #48 review applies; release-prep
commits are
  bump/completions/CHANGELOG/skill-fixture as scripted)
- [x] Tests added/updated and passing (validated via spike + #48 CI)
- [x] No new warnings or errors introduced
- [x] Changes are backward compatible
brettdavies added a commit to brettdavies/.github that referenced this pull request May 4, 2026
## Summary

Fixes a silent matrix-expansion failure introduced in #17/#18 that bit
agentnative-cli's first attempt at v0.3.1
(release run
[25336702699](https://github.com/brettdavies/agentnative-cli/actions/runs/25336702699)).

The build job's `continue-on-error: ${{ matrix.soft_fail &&
!inputs.linux_musl_required }}` evaluated to `null` for
non-musl matrix rows when the caller passed `linux_musl_required: true`
— `matrix.soft_fail` was undefined for those
rows, `null && false` returns `null`, and GHA refuses non-boolean
job-level booleans by silently dropping the entire
build matrix from the run. The absence cascaded: `publish-crate` `needs:
build` skipped, `release` and `homebrew`
skipped behind it, the v0.3.1 tag pushed but no crate published /
release created / formula dispatched.

Belt-and-suspenders fix:

- **Belt**: normalize the expression to `matrix.soft_fail == true` (and
the same on the post-build annotation step's
  `if:`), so missing-field rows evaluate to `false` instead of `null`.
- **Suspenders**: set `soft_fail: false` explicitly on every non-musl
row, so the matrix is self-documenting (every
row's stance is grep-able) and the implicit-default footgun stops being
a footgun.

Either fix alone closes the immediate bug. Both together prevent the
class — a future matrix row added without
`soft_fail` won't reintroduce this failure.

## Changelog

### Fixed

- Fix silent build-job matrix expansion failure when the caller passed
`linux_musl_required: true`. The expression
`matrix.soft_fail && !inputs.linux_musl_required` returned `null` for
matrix rows without `soft_fail`, which GHA
refuses as a job-level boolean — the entire build matrix dropped from
the run and `publish-crate` / `release` /
  `homebrew` skipped behind it.

## Type of Change

- [x] `fix`: Bug fix (non-breaking change which fixes an issue)

## Related Issues/Stories

- Story: n/a
- Issue: n/a
- Architecture: n/a
- Related PRs: #17 (introduced the matrix rows + expression), #18
(released #17 to main), brettdavies/agentnative-cli#48
(consumer opt-in PR), brettdavies/agentnative-cli#49 (consumer release
PR — the v0.3.1 tag has been deleted from
  agentnative-cli; will be re-pushed after this fix lands on main)

## Testing

- [x] Manual testing completed
- [x] All tests passing

**Test Summary:**

- Unit tests: n/a (workflow-only repo)
- Integration tests: n/a
- Coverage: n/a

`actionlint` clean on the modified `rust-release.yml`. The fix will be
exercised in real conditions when agentnative-cli
re-pushes its v0.3.1 tag after this lands on main — at that point all 7
matrix rows should instantiate (5 hard-fail, 2
soft-fail with `linux_musl_required: true` flipping them to hard-fail).

The original spike validation (brettdavies/agentnative-cli
`spike/musl-build`, 2026-05-04) confirmed the underlying musl
build paths work; this PR addresses only the orchestration glue.

## Files Modified

**Modified:**

- `.github/workflows/rust-release.yml` — Adds explicit `soft_fail:
false` to the 5 non-musl matrix rows
(x86_64-linux-gnu, aarch64-linux-gnu, aarch64-apple-darwin,
x86_64-apple-darwin, x86_64-pc-windows-msvc). Updates the
build-job `continue-on-error` expression and the post-build annotation
step's `if:` to compare `matrix.soft_fail ==
true`. Adds a comment to the matrix block explaining the constraint and
citing the failed run as the canary.

**Created:**

- None.

**Renamed:**

- None.

**Deleted:**

- None.

## Breaking Changes

- [x] No breaking changes

Existing consumers see no behavior change. Consumers passing
`linux_musl_required: false` (default) saw working
soft-fail behavior before this fix because `matrix.soft_fail && true`
returned the matrix-row value (or null for
non-musl rows, but with `continue-on-error: null` GHA seemingly
tolerated it for some expansion paths — only the
`linux_musl_required: true` path triggered the silent drop). The fix
makes both paths reliable.

## Deployment Notes

- [x] No special deployment steps required

After merge to dev → cherry-pick to main, agentnative-cli will re-tag
v0.3.1 and push. Pipeline should run clean: 7
matrix rows instantiate, 5 hard-fail, 2 musl rows hard-fail (because
`linux_musl_required: true` flips their stance). On
a successful musl build the alpine-verify step runs as before.

## Checklist

- [x] Code follows project conventions and style guidelines
- [x] Commit messages follow Conventional Commits
- [x] Self-review of code completed
- [x] Tests added/updated and passing (workflow-only; `actionlint`
clean)
- [x] No new warnings or errors introduced
- [x] Changes are backward compatible
brettdavies added a commit to brettdavies/.github that referenced this pull request May 4, 2026
## Summary

Consolidates two fix commits to main:

- **#20 — `fix(rust-release): normalize build matrix soft_fail
evaluation`** — silent matrix-expansion failure in the
build job's `continue-on-error` expression when callers pass
`linux_musl_required: true`. Bit agentnative-cli's first
attempt at v0.3.1 ([failed
run](https://github.com/brettdavies/agentnative-cli/actions/runs/25336702699))
— tag
pushed, no crate published, no release created. Tag has been deleted;
will be re-pushed after this lands.
Belt-and-suspenders fix: normalize the expression to `matrix.soft_fail
== true` AND set `soft_fail: false` explicitly
  on every non-musl matrix row.

- **#19 — `ci(lint): gate dev pushes and fail on actionlint findings`**
— bundled because it landed on dev between #20
and the cut of this release branch. Single-file change to
`.github/workflows/lint.yml` hardening this repo's own CI:
  actionlint now runs on dev pushes and fails the workflow on findings.

Triple-diff verification clean: A has exactly the two expected files; B
is empty (no non-doc paths on dev missing from
this release); no guarded paths leaked.

## Changelog

### Fixed

- Fix silent build-job matrix expansion failure when the caller passed
`linux_musl_required: true`. The expression
`matrix.soft_fail && !inputs.linux_musl_required` returned `null` for
matrix rows without `soft_fail`, which GHA
refuses as a job-level boolean — the entire build matrix dropped from
the run and `publish-crate` / `release` /
  `homebrew` skipped behind it.

### Changed

- `.github/workflows/lint.yml` (this repo's own CI) now runs actionlint
on `push: dev` and fails the workflow on
  actionlint findings, raising the bar for what reaches dev.

## Type of Change

- [x] `fix`: Bug fix (non-breaking change which fixes an issue)

## Related Issues/Stories

- Story: n/a
- Issue: n/a
- Architecture: n/a
- Related PRs: #20 (the rust-release fix), #19 (the lint hardening), #18
(the release that introduced the soft_fail
bug), brettdavies/agentnative-cli#48 (consumer opt-in),
brettdavies/agentnative-cli#49 (consumer release — v0.3.1 tag
  deleted, will be re-pushed after this lands)

## Testing

- [x] Manual testing completed
- [x] All tests passing

**Test Summary:**

- Unit tests: n/a (workflow-only repo)
- Integration tests: n/a
- Coverage: n/a

`actionlint` clean on both modified workflow files. The rust-release
fix's real-world validation comes when
agentnative-cli re-pushes its v0.3.1 tag after this lands — at that
point all 7 matrix rows should instantiate, 5
hard-fail and 2 musl rows hard-fail (because `linux_musl_required: true`
flips them).

## Files Modified

**Modified:**

- `.github/workflows/rust-release.yml` — see #20.
- `.github/workflows/lint.yml` — see #19.

**Created:**

- None.

**Renamed:**

- None.

**Deleted:**

- None.

## Breaking Changes

- [x] No breaking changes

## Deployment Notes

- [x] No special deployment steps required

After this lands on main, agentnative-cli re-tags v0.3.1 and pushes.
Pipeline should run clean.

## Checklist

- [x] Code follows project conventions and style guidelines
- [x] Commit messages follow Conventional Commits
- [x] Self-review of code completed (PR #20 and #19 reviews apply)
- [x] Tests added/updated and passing (`actionlint` clean on both files)
- [x] No new warnings or errors introduced
- [x] Changes are backward compatible
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant