Summary
The origin template assumes — and actively encourages — a single Go module
("One Go module, many binaries" in AGENTS.md, the root go.mod comment, and
.skillrig-origin.toml). That holds nicely when every backing CLI is first-party code
authored in cmd/<tool>/. It breaks down the moment an org needs to inline a
substantial third-party CLI into the origin for supply-chain control (vendoring a fork
so releases are built from an audited, re-syncable copy instead of pulled from an upstream
we don't control).
We hit this concretely while inlining two upstream CLIs into our origin:
ankitpokhrel/jira-cli (our so0k fork) → cli/jira — module …/cli/jira, jira-v* stream, seeded 1.7.0
so0k/tfc-cli → cli/tfc — module …/cli/tfc, tfc-v* stream, seeded 0.0.6
Why "one Go module" doesn't work for inlined CLIs
Folding a large upstream program into the single root module would have forced us to:
- Rewrite 80+ import sites — every internal package path in the upstream tree gets
rewritten to the root module path. That mutates the source and makes future re-syncs a
manual merge.
- Merge two dependency trees into one
go.sum, resolving version conflicts between
the origin's deps and each CLI's deps. One transitive conflict bumps the whole origin.
- Bump the shared
go directive to the highest common denominator, dragging every
other binary along (jira needs go 1.25, tfc 1.24, the origin 1.23).
- Lose the verbatim, auditable source tree — the whole point of inlining. You want a
copy you can diff against upstream and re-sync; import-rewrite + go.sum merge destroys
that property.
So we went multi-module: a root go.work tying together the root module (cmd/oxid)
and cli/jira / cli/tfc, each keeping its own go.mod/go.sum/go directive and a
verbatim source tree (only the module path is rewritten). This works — but none of it is
covered by the template, and a couple of sharp edges cost us real time:
Sharp edge 1 — goreleaser monorepo.tag_prefix is Pro-only
The obvious fix for our prefixed tags (jira-v1.7.0) is goreleaser's
monorepo: { tag_prefix:, dir: }. But that stanza is a goreleaser Pro feature —
goreleaser check on the OSS binary rejects it outright:
yaml: unmarshal errors: line N: field monorepo not found in type config.Project
OSS goreleaser also can't parse a prefixed tag as semver
(failed to parse tag 'jira-v1.7.0' as semver). The OSS-compatible recipe we landed on,
per binary:
- per-tool config (
.goreleaser.<tool>.yaml) with builds[].dir: cli/<tool> (OSS supports
per-build dir), correct -X <module>/internal/...Version={{.Version}} ldflags, and
release.disable: true + changelog.disable: true (release-please owns the Release and
the CHANGELOG);
- in CI, derive the clean semver from the prefixed tag and feed it via
GORELEASER_CURRENT_TAG=v1.7.0, run goreleaser release --clean --skip=validate, then
attach the archives + checksums to the release-please-created Release with
gh release upload <prefixed-tag> dist/*.tar.gz dist/*_checksums.txt --clobber.
The template ships a single .goreleaser.yaml with no prefix handling at all, so it is
already subtly broken for the existing oxid-v* stream under OSS goreleaser — this isn't
only a multi-module issue.
Sharp edge 2 — go work sync silently un-vendors your deps
Per-binary release builds must use GOWORK=off so each binary builds from its own
pinned go.mod/go.sum — that pin is the supply-chain guarantee; the workspace is dev
convenience only. More importantly, running go work sync mutates the member modules'
go.mod to unify versions across the workspace: it bumped our vendored jira's
spf13/pflag from upstream v1.0.5 to v1.0.9 (to match tfc) without backfilling
go.sum, which (a) broke the per-module release build and (b) silently falsified the
"only the module path was rewritten" provenance claim. Vendored modules should be treated
as read-only by the workspace; go work sync is a footgun here.
What works fine as-is
- release-please: per-module
packages entries (cli/jira→jira, cli/tfc→tfc) with
include-component-in-tag + tag-separator: "-" → jira-v* / tfc-v* tags. Note the
component must not be a prefix of a skill component — we used jira (not jira-cli) so
the binary stream doesn't collide with the jira-cli-v* skill tags.
- mise: per-tool
[tools] entries with the per-stream tag_regex encoded in the key
(keys must be unique), anchored ^jira-v (not ^jira-) so it doesn't also match the
skill's jira-cli-v* tags.
Requests
- Document a supported multi-module /
go.work topology for vendored CLIs: the
cli/<tool>/ submodule layout, when to choose it over single-module, how to keep an
inlined fork verbatim + re-syncable, and the GOWORK=off-for-releases / don't-
go work sync-vendored-modules rules.
- Ship release plumbing that works for per-binary streams across separate module dirs:
the OSS-compatible goreleaser recipe above (and/or call out the Pro monorepo
requirement explicitly), per-module release-please packages, and anchored mise
tag_regex. Ideally fix the existing single-module template so its oxid-v* stream
actually releases under OSS goreleaser.
- Soften the "one Go module" language in
AGENTS.md / go.mod / .skillrig-origin.toml
so single-module is the default, not the only supported shape.
Concrete resolution for reference
Our origin now ships oxid (root module), jira (cli/jira), and tfc (cli/tfc),
tied by a root go.work. All three build and stamp version under
GOWORK=off … GORELEASER_CURRENT_TAG=vX.Y.Z goreleaser release --clean --skip=validate;
the inlined trees retain their upstream import paths (no rewrite of the 80+ upstream import
sites) and verbatim go.mod/go.sum. Happy to share the full diff if it helps shape the
template guidance.
Summary
The origin template assumes — and actively encourages — a single Go module
("One Go module, many binaries" in
AGENTS.md, the rootgo.modcomment, and.skillrig-origin.toml). That holds nicely when every backing CLI is first-party codeauthored in
cmd/<tool>/. It breaks down the moment an org needs to inline asubstantial third-party CLI into the origin for supply-chain control (vendoring a fork
so releases are built from an audited, re-syncable copy instead of pulled from an upstream
we don't control).
We hit this concretely while inlining two upstream CLIs into our origin:
ankitpokhrel/jira-cli(ourso0kfork) →cli/jira— module…/cli/jira,jira-v*stream, seeded1.7.0so0k/tfc-cli→cli/tfc— module…/cli/tfc,tfc-v*stream, seeded0.0.6Why "one Go module" doesn't work for inlined CLIs
Folding a large upstream program into the single root module would have forced us to:
rewritten to the root module path. That mutates the source and makes future re-syncs a
manual merge.
go.sum, resolving version conflicts betweenthe origin's deps and each CLI's deps. One transitive conflict bumps the whole origin.
godirective to the highest common denominator, dragging everyother binary along (jira needs go 1.25, tfc 1.24, the origin 1.23).
copy you can diff against upstream and re-sync; import-rewrite + go.sum merge destroys
that property.
So we went multi-module: a root
go.worktying together the root module (cmd/oxid)and
cli/jira/cli/tfc, each keeping its owngo.mod/go.sum/godirective and averbatim source tree (only the module path is rewritten). This works — but none of it is
covered by the template, and a couple of sharp edges cost us real time:
Sharp edge 1 — goreleaser
monorepo.tag_prefixis Pro-onlyThe obvious fix for our prefixed tags (
jira-v1.7.0) is goreleaser'smonorepo: { tag_prefix:, dir: }. But that stanza is a goreleaser Pro feature —goreleaser checkon the OSS binary rejects it outright:OSS goreleaser also can't parse a prefixed tag as semver
(
failed to parse tag 'jira-v1.7.0' as semver). The OSS-compatible recipe we landed on,per binary:
.goreleaser.<tool>.yaml) withbuilds[].dir: cli/<tool>(OSS supportsper-build
dir), correct-X <module>/internal/...Version={{.Version}}ldflags, andrelease.disable: true+changelog.disable: true(release-please owns the Release andthe CHANGELOG);
GORELEASER_CURRENT_TAG=v1.7.0, rungoreleaser release --clean --skip=validate, thenattach the archives + checksums to the release-please-created Release with
gh release upload <prefixed-tag> dist/*.tar.gz dist/*_checksums.txt --clobber.The template ships a single
.goreleaser.yamlwith no prefix handling at all, so it isalready subtly broken for the existing
oxid-v*stream under OSS goreleaser — this isn'tonly a multi-module issue.
Sharp edge 2 —
go work syncsilently un-vendors your depsPer-binary release builds must use
GOWORK=offso each binary builds from its ownpinned
go.mod/go.sum— that pin is the supply-chain guarantee; the workspace is devconvenience only. More importantly, running
go work syncmutates the member modules'go.modto unify versions across the workspace: it bumped our vendored jira'sspf13/pflagfrom upstreamv1.0.5tov1.0.9(to match tfc) without backfillinggo.sum, which (a) broke the per-module release build and (b) silently falsified the"only the module path was rewritten" provenance claim. Vendored modules should be treated
as read-only by the workspace;
go work syncis a footgun here.What works fine as-is
packagesentries (cli/jira→jira,cli/tfc→tfc) withinclude-component-in-tag+tag-separator: "-"→jira-v*/tfc-v*tags. Note thecomponent must not be a prefix of a skill component — we used
jira(notjira-cli) sothe binary stream doesn't collide with the
jira-cli-v*skill tags.[tools]entries with the per-streamtag_regexencoded in the key(keys must be unique), anchored
^jira-v(not^jira-) so it doesn't also match theskill's
jira-cli-v*tags.Requests
go.worktopology for vendored CLIs: thecli/<tool>/submodule layout, when to choose it over single-module, how to keep aninlined fork verbatim + re-syncable, and the
GOWORK=off-for-releases / don't-go work sync-vendored-modules rules.the OSS-compatible goreleaser recipe above (and/or call out the Pro
monoreporequirement explicitly), per-module release-please packages, and anchored mise
tag_regex. Ideally fix the existing single-module template so itsoxid-v*streamactually releases under OSS goreleaser.
AGENTS.md/go.mod/.skillrig-origin.tomlso single-module is the default, not the only supported shape.
Concrete resolution for reference
Our origin now ships
oxid(root module),jira(cli/jira), andtfc(cli/tfc),tied by a root
go.work. All three build and stamp version underGOWORK=off … GORELEASER_CURRENT_TAG=vX.Y.Z goreleaser release --clean --skip=validate;the inlined trees retain their upstream import paths (no rewrite of the 80+ upstream import
sites) and verbatim
go.mod/go.sum. Happy to share the full diff if it helps shape thetemplate guidance.