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..f622d86d 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -4,9 +4,32 @@ version: 2 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" @@ -294,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