From ee1c3b7440d02379a4da06391678a5e20022780d Mon Sep 17 00:00:00 2001 From: Rex Raphael Date: Thu, 6 Aug 2026 21:23:23 -0500 Subject: [PATCH 1/2] fix(release): build the CLI against this tree, not the last published root goreleaser builds `dir: ./cmd/forge`, which is a separate module pinning github.com/xraph/forge v1.8.2 -- its go.mod must stay free of replace directives for `go install ...@latest` to accept it. With no workspace it therefore compiled the CLI against the *released* root module rather than the tag being released, so every root API added since v1.8.2 failed as "undefined": plugins/client_diff.go:71:19: undefined: client.DiffSpecs plugins/client.go:401:23: undefined: client.PathFilter plugins/client.go:438:3: unknown field ReactQuery in GeneratorConfig Nothing had regressed. internal/client/filter.go (82cdebcc, Aug 3) and internal/client/diff.go (8bc90574, Aug 4) both postdate v1.9.2, the newest published root tag, so no released version carries them -- pinning forward does not help either, and was measured: against v1.9.2 the PathFilter errors clear and client_diff.go still does not build. go.yml's "Build CLI" job already solved this with a throwaway workspace, which is exactly why CI stayed green while the release broke -- the two jobs built the CLI differently. This moves the same workspace to where the release can see it, as .goreleaser.yml's first before-hook, so it applies to the real release and the dry run alike without touching the reusable workflow in xraph/workflows. GOWORK is pinned to $PWD because a bare `go work init` searches upward and refuses when the checkout sits inside another workspace. The dry-run job's "Validate module builds" step had the identical hole and would have reported a break the release no longer has, so it gets the same treatment, scoped by module path: the workfile stays outside the repo and everything other than cmd/forge and extensions/database still builds with GOWORK=off, so each module is checked standalone. Verified with goreleaser locally: the failure reproduces exactly before the change, and after it all five targets build -- including the three from the failed run (linux_amd64_v1, windows_amd64_v1, darwin_arm64_v8.0). The hooks leave no tracked file modified and go.work stays gitignored. Does NOT fix `go install github.com/xraph/forge/cmd/forge@latest` for users, who have no workspace. That needs a root tag cut at or after 8bc90574 and cmd/forge/go.mod pinned to it -- a release-ordering change, not a build one. --- .github/workflows/release.yml | 36 +++++++++++++++++++++++++++++++++-- .goreleaser.yml | 22 +++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b66769d9..051d8ec1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -398,8 +398,40 @@ jobs: - name: Validate module builds run: | - cd ${{ needs.detect.outputs.module_path }} - go build ./... + set -euo pipefail + + MODULE_PATH="${{ needs.detect.outputs.module_path }}" + + # cmd/forge and extensions/database pin the last PUBLISHED + # github.com/xraph/forge, because cmd/forge/go.mod has to stay free of + # replace directives for `go install ...@latest` to accept it. Built + # standalone they therefore compile against the released root module + # rather than this tree, and any root API this tag adds but has not + # published yet fails here as "undefined" -- a dry run reporting a + # break the real release does not have, since .goreleaser.yml's first + # before-hook sets up the same workspace for the build itself. + # + # The workfile lives OUTSIDE the repo on purpose: a go.work at the root + # would apply to every module, and the ones it does not list would fail + # with "directory prefix ... does not contain modules listed in + # go.work". Everything else is built with GOWORK=off so each module is + # still checked standalone. + case "$MODULE_PATH" in + cmd/forge|extensions/database) + WORKFILE="${RUNNER_TEMP:-/tmp}/forge-release.work" + rm -f "$WORKFILE" + GOWORK="$WORKFILE" go work init \ + "$GITHUB_WORKSPACE" \ + "$GITHUB_WORKSPACE/cmd/forge" \ + "$GITHUB_WORKSPACE/extensions/database" + cd "$MODULE_PATH" + GOWORK="$WORKFILE" go build ./... + ;; + *) + cd "$MODULE_PATH" + GOWORK=off go build ./... + ;; + esac - name: GoReleaser dry run (main/cli only) if: needs.detect.outputs.module_type == 'main' || needs.detect.outputs.module_type == 'cli' diff --git a/.goreleaser.yml b/.goreleaser.yml index bacafab7..e3ba1974 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -7,6 +7,28 @@ project_name: forge # Pre-build hooks (run in root module context) before: hooks: + # MUST be first: every hook below, and the build itself, resolves modules. + # + # `builds.dir` is ./cmd/forge, which is a separate module pinning the last + # PUBLISHED github.com/xraph/forge -- its go.mod has to stay free of replace + # directives for `go install ...@latest` to accept it (see the comment at the + # top of that file). Without a workspace the build therefore compiles the CLI + # against the *released* root module rather than this tree, so any root API a + # tag adds but has not published yet fails as "undefined". That is not + # hypothetical: it is how the release broke, with `client.DiffSpecs` and + # `client.PathFilter` undefined against a root module pinned several versions + # back. + # + # This is the same throwaway workspace the "Build CLI" job in go.yml creates, + # moved to where the *release* can see it -- that job builds the CLI + # correctly and the release did not, which is the whole reason the two + # disagreed. go.work is gitignored, so it never escapes the runner. + # + # GOWORK is pinned to this directory because a bare `go work init` searches + # upward and refuses with "go.work already exists" if the checkout happens to + # sit inside another workspace -- which a git worktree under the main clone + # does. Pinning it keeps the hook working wherever the tree is checked out. + - sh -c 'rm -f go.work go.work.sum && GOWORK="$PWD/go.work" go work init . ./cmd/forge ./extensions/database' - go mod tidy - go mod verify - sh -c "cd cmd/forge && go mod tidy" From 59c526df0d9731bf1bce9b490fd6a17b6fb6616c Mon Sep 17 00:00:00 2001 From: Rex Raphael Date: Thu, 6 Aug 2026 23:09:18 -0500 Subject: [PATCH 2/2] fix(release): scope GoReleaser's tag search to the CLI's own tags A release run failed before it built anything: git tag extensions/ai/v1.9.3 was not made against commit d07b870c This repository tags far more than the CLI -- every extension carries `extensions//vX.Y.Z` and the editor plugin carries `vscode-forge/vX.Y.Z`. GoReleaser takes the newest tag in the repo as "the release", so it picked up whichever extension was tagged last and then refused, because that tag belongs to a different commit than the one being released. `git.ignore_tags` restricts the search to the CLI's own `vX.Y.Z` tags. Verified: before, tag selection resolved to `current=extensions/ai/v1.9.3`; after, to `current=v1.9.3`, and the snapshot version stamps as `1.9.3-SNAPSHOT-...` instead of `extensions/ai/v1.9.3-SNAPSHOT-...`. Worth noting why this survived review: snapshot builds disable tag validation, so `goreleaser build --snapshot` reports the wrong tag as a skipped-pipe notice and carries on. Only a real release treats it as fatal. --- .goreleaser.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.goreleaser.yml b/.goreleaser.yml index e3ba1974..f622d86d 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -4,6 +4,7 @@ version: 2 project_name: forge + # Pre-build hooks (run in root module context) before: hooks: @@ -316,6 +317,21 @@ docker_manifests: git: tag_sort: -version:refname + # This repository tags far more than the CLI: every extension gets its own + # `extensions//vX.Y.Z`, and the editor plugin gets `vscode-forge/vX.Y.Z`. + # Without this, GoReleaser takes the newest tag in the whole repo as "the + # release", picks up whichever extension was tagged last, and refuses with + # + # git tag extensions/ai/v1.9.3 was not made against commit + # + # because that tag belongs to a different commit than the one being released. + # Snapshot builds hide it -- tag validation is disabled there -- so it only + # ever surfaces during a real release. Restricting the search to the CLI's own + # `vX.Y.Z` tags is what makes a snapshot and a release agree. + ignore_tags: + - 'extensions/*' + - 'vscode-forge/*' + # Metadata metadata: mod_timestamp: '{{ .CommitTimestamp }}' \ No newline at end of file