Skip to content

feat(release): publish forge-core, forge-skills, forge-plugins as Go library modules#153

Merged
initializ-mk merged 1 commit into
mainfrom
feat/library-module-releases
Jun 11, 2026
Merged

feat(release): publish forge-core, forge-skills, forge-plugins as Go library modules#153
initializ-mk merged 1 commit into
mainfrom
feat/library-module-releases

Conversation

@initializ-mk

Copy link
Copy Markdown
Contributor

Summary

Adds the release-pipeline change needed so external consumers (e.g. the Initializ platform) can embed Forge as a Go library — `go get github.com/initializ/forge/forge-core@vX.Y.Z` — instead of shelling out to the CLI.

This is step 1 of 3 in the multi-module library release plan we sketched:

  1. (this PR) Release pipeline learns to push path-prefixed Git tags for the three library modules per release, with the workspace-mode `replace` directives stripped at tag time.
  2. (next PR) Public API surface — `internal/` markers for the implementation details that aren't intended for embedders.
  3. (after that) Import smoke-test CI — a job that, after each release, scaffolds a tiny external module and runs `go get` + compile against the just-tagged libraries.

What blocks Initializ today

The library modules cross-reference each other via:

```go
// forge-core/go.mod
require github.com/initializ/forge/forge-skills v0.0.0
replace github.com/initializ/forge/forge-skills => ../forge-skills

// forge-plugins/go.mod
require github.com/initializ/forge/forge-core v0.0.0
replace github.com/initializ/forge/forge-core => ../forge-core
```

Correct for `go.work`-based local dev (workspace mode resolves to the local source tree). Broken for an external `go get` — the consumer sees `replace ../forge-skills`, can't find that path, and fails.

Strategy: ephemeral release commit

Tagging the library modules at the binary-release commit directly would publish the broken `go.mod` files verbatim. Instead, the new `scripts/release/tag-libraries.sh`:

  1. Checks out the binary-release tag (`vX.Y.Z`) in a detached worktree.
  2. Runs `go mod edit` to drop the `replace` directives and bump the `require` lines to the new version.
  3. Commits the rewrite as an ephemeral commit — single parent = the binary-release tag, never merged to main.
  4. Tags `forge-core/vX.Y.Z` and `forge-plugins/vX.Y.Z` at the ephemeral commit.
  5. Tags `forge-skills/vX.Y.Z` at the binary-release commit directly (leaf module, no rewrite needed).
  6. Pushes all three.

```
main ◀── workspace-mode go.mod files

├─ ... (regular commits)

└─ tagged: v0.15.0 ◀── flat binary-release tag (consumed by goreleaser)

└── release-libs(go.mod rewrites) ◀── forge-core/v0.15.0
forge-plugins/v0.15.0
```

Main is never touched. Workspace-mode dev keeps working unchanged — `replace ../forge-skills` stays on main.

End-to-end verification

  • `bash -n scripts/release/tag-libraries.sh` — syntax clean
  • `./scripts/release/tag-libraries.sh v0.14.1 --dry-run` — output matches expected actions
  • Manual `go mod edit` invocation on a detached worktree of v0.14.1 — `go.mod` files are clean:
    • `forge-core/go.mod`: `require github.com/initializ/forge/forge-skills v0.14.1`, no `replace` directive
    • `forge-plugins/go.mod`: `require github.com/initializ/forge/forge-core v0.14.1`, no `replace` directive

(Did not run end-to-end with real tag creation — sandbox correctly flagged that as a release-adjacent operation that needs explicit authorization. The first real run will happen on the v0.15.0 release tag push.)

Script safety

  • `--dry-run` prints every action without touching anything
  • `--no-push` tags locally without pushing (manual testing)
  • Refuses to run if any path-prefixed tag for the version already exists (no accidental overwrites)
  • Refuses to run if the working tree has uncommitted changes (no accidental capture of unrelated work)
  • Refuses to run if the binary-release tag doesn't exist locally
  • Validates the version string against `vMAJOR.MINOR.PATCH(-prerelease)?`
  • Uses `go mod edit` (not regex / sed) so future `go.mod` syntax additions don't break it

Files

  • `scripts/release/tag-libraries.sh` — the rewrite-and-tag script
  • `.github/workflows/release.yaml` — new `library-tags` job runs after `release` (the goreleaser job)
  • `docs/reference/library-modules.md` — full reference: three importable modules, path-prefixed tag scheme, ephemeral-commit mechanism with ASCII diagram, versioning policy (unified during v0.x), currently-stable public API surface, what workspace-mode dev still looks like
  • `README.md` — Library Modules row in the Operations doc table
  • `CONTRIBUTING.md` — Project Structure table gains a "Published as library?" column

What ships on the next release

Once this lands and the next `vX.Y.Z` tag is pushed, the workflow automatically tags + pushes:

  • `forge-skills/vX.Y.Z`
  • `forge-core/vX.Y.Z`
  • `forge-plugins/vX.Y.Z`

Initializ can then put in their `go.mod`:

```go
require github.com/initializ/forge/forge-core v0.15.0
```

and the build resolves correctly via the Go module proxy.

Versioning

Unified during v0.x — all three library modules ship at the same version as the CLI on every release, even when only one of them changed. Simpler operational model, matches how the workspace evolves (changes routinely span modules). See docs/reference/library-modules.md § Versioning for the future v1.0 stability promise.

Out of scope

  • Retroactive tags for v0.14.1. Could be added if Initializ wants to consume the current release immediately. Run `./scripts/release/tag-libraries.sh v0.14.1` from a checkout with push access. Not done automatically here because the v0.14.1 commit doesn't go through the workflow.
  • `internal/` API markers — separate PR, tracked as step 2.
  • Smoke-test CI — separate PR, tracked as step 3.
  • Independent versioning — defer until one of the libraries justifies a different release cadence than the CLI. Today they move together.

Test plan

  • Script syntax check (`bash -n`)
  • Dry-run produces expected actions
  • `go mod edit` rewrites verified by hand on a detached worktree of v0.14.1
  • Doc cross-links resolve
  • First real run will happen on v0.15.0 release tag push — workflow will be exercised end-to-end then

…libraries

External consumers (e.g. the Initializ platform) need to embed the
agent runtime as a library, not shell out to the CLI:

  go get github.com/initializ/forge/forge-core@v0.15.0

Today that fails. The library modules cross-reference each other via
`replace ../forge-skills` (and `v0.0.0` placeholder require lines)
which is correct for workspace-mode local dev but unusable for a Go
module proxy fetch. The Initializ build sees `replace ../forge-skills`,
can't find the local path, and fails.

Path-prefixed Git tags are the standard Go multi-module convention
for this — `forge-skills/vX.Y.Z`, `forge-core/vX.Y.Z`,
`forge-plugins/vX.Y.Z` per release. But pointing those tags directly
at the binary-release commit would publish the broken go.mod files
verbatim. Need a tagged tree where `replace` is dropped and `require`
references real sibling versions.

This commit adds the "ephemeral release commit" pattern:

1. scripts/release/tag-libraries.sh — runs `go mod edit` on a detached
   worktree of the binary-release tag to drop `replace` directives and
   bump `require` lines to the new version. Commits the result as a
   throwaway commit (one parent = the binary tag, never merged to
   main). Tags forge-core/vX.Y.Z and forge-plugins/vX.Y.Z at that
   ephemeral commit; tags forge-skills/vX.Y.Z at the binary commit
   directly (leaf module, no rewrite needed). Pushes all three.

2. .github/workflows/release.yaml — new `library-tags` job that runs
   the script after goreleaser succeeds. Uses GITHUB_TOKEN and the
   workflow's `contents: write` permission to push tags.

3. docs/reference/library-modules.md — full reference covering the
   three importable modules + their roles, the path-prefixed tag
   scheme, the ephemeral-commit mechanism with an ASCII diagram, the
   versioning policy (unified during v0.x), the currently-stable
   public API surface, and what workspace-mode dev still looks like
   (unchanged — `replace` stays on main).

4. README.md — Library Modules row in the Operations doc table.

5. CONTRIBUTING.md — Project Structure table gains a "Published as
   library?" column and a sentence pointing at the new doc.

Main is untouched by every release. The workspace-mode `replace`
directives stay; main never has go.mod files with cross-module
`require vX.Y.Z` lines that would break local dev (workspace mode
can't satisfy `require X v0.15.0` from a local source tree).

Script supports --dry-run (verified) and --no-push for safe manual
testing. Refuses to overwrite existing path-prefixed tags so a hotfix
re-release can't accidentally clobber a published version.

Verified locally:
- bash -n syntax check passes
- --dry-run output matches expectations against v0.14.1
- `go mod edit` rewrites produce clean go.mod files (replace dropped,
  require bumped to v0.14.1)

Next release (v0.15.0) will be the first one that publishes library
tags. Initializ can then `go get github.com/initializ/forge/
forge-core@v0.15.0` and import the runtime directly.
@initializ-mk initializ-mk merged commit 48c355a into main Jun 11, 2026
10 checks passed
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