25 Commits

Author SHA1 Message Date
2180aa868e Prepare the v0.9.0 release notes and retire completed roadmaps 2026-11-04 01:59:54 +00:00
bf4e2329ec Build delivery retries from clean tagged source 2026-11-04 01:52:26 +00:00
bd1c714935 Invoke the registered ReleaseKit package explicitly 2026-11-04 01:51:36 +00:00
f625674a68 Record follow up launcher review findings 2026-11-04 01:42:03 +00:00
9797f43dc4 Anchor smoke checks to the selected checkout 2026-11-04 01:25:50 +00:00
b5c6638538 Preserve replacement selectors during recovery 2026-11-04 01:25:34 +00:00
ec67496569 Align source and standalone tool instructions 2026-11-04 01:24:16 +00:00
d99fac23e9 Explain target variables in configured checks 2026-11-04 01:23:40 +00:00
a3c74188be Use external Go in recovery commands 2026-11-04 01:23:24 +00:00
7dcc9a37c7 Record that host native launchers are implemented 2026-11-04 01:22:27 +00:00
68ce3e5a3b Verify cross built workflow assets 2026-11-04 01:21:58 +00:00
b00bc41b65 Clarify the recovery command target scope 2026-11-04 01:21:28 +00:00
fcec6357ff Make the adoption smoke command runnable 2026-11-04 01:21:20 +00:00
13e1c21386 Record host launcher algorithm audit findings 2026-11-04 01:20:02 +00:00
80a610a5c2 Record host launcher integration findings 2026-11-04 01:10:34 +00:00
d02b9ffeb8 Record host native launcher review findings 2026-11-04 01:02:39 +00:00
0269648429 Document host-native release launchers 2026-11-04 00:54:44 +00:00
bd693ca31b Run recovery handoffs for the host target 2026-11-04 00:52:04 +00:00
d8ec9389e0 Prepare recovery tools for the host target 2026-11-04 00:50:59 +00:00
10028c4abb Use the host target for source-built release tools 2026-11-04 00:49:59 +00:00
8e897d7b3e Use the host target in consumer delivery workflows 2026-11-04 00:49:10 +00:00
a8ffa89011 Use the host target in the multi-module launcher 2026-11-04 00:47:38 +00:00
bf07bed2ae Plan host native release launchers 2026-11-04 00:43:57 +00:00
373f7a69cd Include recovery handoffs in the host launcher roadmap 2026-11-04 00:39:01 +00:00
b1ce5bf976 Plan host-native consumer launcher fixes 2026-11-03 15:48:37 +00:00
19 changed files with 370 additions and 83 deletions

View File

@@ -31,7 +31,9 @@ Run the offline Go test suite from the repository root with:
go test ./...
```
The version command prints ReleaseKit's version:
The version command prints ReleaseKit's version. For the `go run` examples
below, [select the Go host target](docs/consumers/adoption.md#select-the-go-host-target)
if your shell is set to cross-compile.
```sh
go run ./cmd/releasekit version

View File

@@ -142,16 +142,17 @@ func selectedPublicationRecoveryCommand(ctx context.Context, result releasekit.P
if err != nil {
return "", err
}
if len(args) > 0 {
command := "env GOWORK=off GOTOOLCHAIN=local go"
for _, arg := range args {
if args != nil {
prefix := "env go -C " + shellQuote(git.DisplayDiagnostic(args.Directory))
command := "(export GOWORK=off GOTOOLCHAIN=local && releasekit_host_os=$(" + prefix + " env GOHOSTOS) && releasekit_host_arch=$(" + prefix + " env GOHOSTARCH) && GOOS=\"$releasekit_host_os\" GOARCH=\"$releasekit_host_arch\" " + prefix
for _, arg := range args.Args {
command += " " + shellQuote(git.DisplayDiagnostic(arg))
}
path, err := filepath.Abs(git.DisplayDiagnostic(result.RecordPath))
if err != nil {
return "", err
}
return command + " reconcile --record " + shellQuote(path), nil
return command + " reconcile --record " + shellQuote(path) + ")", nil
}
}
return publicationRecoveryCommand(result.RecordPath)

View File

@@ -8,6 +8,7 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"testing"
@@ -81,6 +82,11 @@ func TestMultiModuleLauncherUsesSelectedModuleAndProfile(t *testing.T) {
shim := `#!/bin/sh
set -eu
printf 'cwd=%s args=%s\n' "$PWD" "$*" >> "$RELEASEKIT_TEST_GO_LOG"
if [ "$1" = env ]; then
case "$2" in GOHOSTOS) printf '%s\n' "$RELEASEKIT_TEST_HOST_OS" ;; GOHOSTARCH) printf '%s\n' "$RELEASEKIT_TEST_HOST_ARCH" ;; esac
exit 0
fi
case " $* " in *" -deps "*) printf 'target=%s/%s\n' "$GOOS" "$GOARCH" >> "$RELEASEKIT_TEST_GO_LOG" ;; esac
modfile=
for argument in "$@"; do
case "$argument" in -modfile=*) modfile=${argument#-modfile=} ;; esac
@@ -98,6 +104,11 @@ fi
t.Setenv("RELEASEKIT_MODULE_MAP", mapPath)
t.Setenv("RELEASEKIT_TEST_GO_LOG", goLog)
t.Setenv("RELEASEKIT_TEST_TOOL_ARGS", argsLog)
t.Setenv("RELEASEKIT_TEST_HOST_OS", runtime.GOOS)
t.Setenv("RELEASEKIT_TEST_HOST_ARCH", runtime.GOARCH)
foreignOS, foreignArch := foreignTarget()
t.Setenv("GOOS", foreignOS)
t.Setenv("GOARCH", foreignArch)
launcher := filepath.Join(repository, "release.sh")
command := exec.Command(launcher, "with space", "v1.2.4")
var stdout, stderr bytes.Buffer
@@ -115,6 +126,9 @@ fi
if !strings.Contains(string(goCommands), "cwd="+wantModule) || !strings.Contains(string(goCommands), "v0.2.299") || strings.Contains(string(goCommands), "v0.2.197") {
t.Fatalf("tool bootstrap did not use selected module metadata: %s", goCommands)
}
if !strings.Contains(string(goCommands), "target="+runtime.GOOS+"/"+runtime.GOARCH) {
t.Fatalf("dependency inspection did not use host target: %s", goCommands)
}
toolArgs, err := os.ReadFile(argsLog)
if err != nil {
t.Fatal(err)
@@ -134,7 +148,11 @@ func TestMultiModuleLauncherRunsToolFromSelectedDependency(t *testing.T) {
repository := newCLIRepository(t)
copyExampleTree(t, example, repository)
writeRepositoryFile(t, repository, "go.mod", "module example.com/multi-module-consumer\n\ngo 1.25.0\n\nrequire go.maximumdirect.net/releasekit v0.2.197\n")
writeRepositoryFile(t, repository, "integrations/ckconfig/go.mod", "module example.com/multi-module-consumer/integrations/ckconfig\n\ngo 1.25.0\n\nrequire go.maximumdirect.net/releasekit v0.2.198\n")
const otherModule = "example.com/other-tool"
other := t.TempDir()
writeRepositoryFile(t, other, "go.mod", "module "+otherModule+"\n\ngo 1.25.0\n")
writeRepositoryFile(t, other, "cmd/releasekit/main.go", "package main\nfunc main() { println(\"unexpected tool ran\") }\n")
writeRepositoryFile(t, repository, "integrations/ckconfig/go.mod", "module example.com/multi-module-consumer/integrations/ckconfig\n\ngo 1.25.0\n\nrequire go.maximumdirect.net/releasekit v0.2.198\nrequire "+otherModule+" v0.0.0\n\nreplace "+otherModule+" => "+strconv.Quote(other)+"\n\ntool "+otherModule+"/cmd/releasekit\n")
cliGitRun(t, repository, "add", ".")
cliGitRun(t, repository, "commit", "-m", "prepare selected tool module")
@@ -152,6 +170,9 @@ func TestMultiModuleLauncherRunsToolFromSelectedDependency(t *testing.T) {
t.Setenv("GOPROXY", "file://"+filepath.ToSlash(proxy))
t.Setenv("GOSUMDB", "off")
t.Setenv("GOTOOLCHAIN", "local")
foreignOS, foreignArch := foreignTarget()
t.Setenv("GOOS", foreignOS)
t.Setenv("GOARCH", foreignArch)
moduleBefore, err := os.ReadFile(filepath.Join(repository, "integrations", "ckconfig", "go.mod"))
if err != nil {
t.Fatal(err)
@@ -183,6 +204,9 @@ func TestMultiModuleLauncherRunsToolFromSelectedDependency(t *testing.T) {
if strings.Contains(string(output), "tool-version=v0.2.197") {
t.Fatalf("launcher ran the root-selected ReleaseKit version: %s", output)
}
if strings.Contains(string(output), "unexpected tool ran") {
t.Fatalf("launcher ran the other tool with the same name: %s", output)
}
moduleAfter, err := os.ReadFile(filepath.Join(repository, "integrations", "ckconfig", "go.mod"))
if err != nil || !bytes.Equal(moduleAfter, moduleBefore) {
t.Fatalf("selected module go.mod changed: got %q want %q error=%v", moduleAfter, moduleBefore, err)
@@ -203,6 +227,17 @@ func TestMultiModuleLauncherRunsToolFromSelectedDependency(t *testing.T) {
}
}
func foreignTarget() (string, string) {
osName, arch := "windows", "arm64"
if runtime.GOOS == osName {
osName = "linux"
}
if runtime.GOARCH == arch {
arch = "amd64"
}
return osName, arch
}
func runModuleSelector(t *testing.T, helper, mode, value, mapPath string) string {
t.Helper()
command := exec.Command("sh", helper, mode, value)

View File

@@ -1,6 +1,8 @@
package main
import (
"bytes"
"debug/elf"
"encoding/json"
"fmt"
"io"
@@ -11,6 +13,7 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
@@ -55,6 +58,7 @@ func TestMultiModuleWoodpeckerDelivery(t *testing.T) {
if output := runWoodpeckerBlock(t, command, repository, env); !strings.Contains(output, "state complete") || !strings.Contains(output, "tool-version=v0.2.197") {
t.Fatalf("tagged page delivery output = %q", output)
}
assertGoSelection(t, repository, env["RELEASEKIT_GO_LOG"])
if delivery.snapshot().creates != 1 || delivery.snapshot().requests == 0 || delivery.snapshot().tag != tag {
t.Fatalf("page delivery fixture = %#v", delivery.snapshot())
}
@@ -77,11 +81,15 @@ func TestMultiModuleWoodpeckerDelivery(t *testing.T) {
t.Run("binary delivery from physical major module", func(t *testing.T) {
const tag = "integrations/core/v2.1.0"
arch, machine := "arm64", elf.EM_AARCH64
if runtime.GOOS == "linux" && runtime.GOARCH == arch {
arch, machine = "amd64", elf.EM_X86_64
}
delivery := newMappedDeliveryFixture(t, tag)
repository := newWorkflowRepository(t, example)
writeMappedProfile(t, repository, "releasekit-core-v2.json", cli.Settings{
Product: "Example Consumer Core v2", ModuleDir: "integrations/core/v2",
Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "core", Targets: []string{"linux/amd64"}},
Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "core", Targets: []string{"linux/" + arch}},
Delivery: workflowDeliverySettings(delivery.snapshot().URL),
})
writeRepositoryFile(t, repository, "integrations/core/v2/docs/releases/v2.1.0.md", "# Example Consumer Core v2 v2.1.0\n\nTagged core note.\n")
@@ -97,8 +105,17 @@ func TestMultiModuleWoodpeckerDelivery(t *testing.T) {
if observed.creates != 1 || observed.uploads != 2 || observed.tag != tag {
t.Fatalf("binary delivery fixture = %#v", observed)
}
if _, ok := observed.assets["core-v2.1.0-linux-amd64"]; !ok {
t.Fatalf("binary asset missing: %#v", observed.assets)
name := "core-v2.1.0-linux-" + arch
asset, ok := observed.assets[name]
if !ok {
t.Fatalf("binary asset %q missing", name)
}
binary, err := elf.NewFile(bytes.NewReader(asset))
if err != nil {
t.Fatalf("read uploaded %s as ELF: %v", name, err)
}
if binary.Machine != machine {
t.Fatalf("uploaded %s machine = %s, want %s", name, binary.Machine, machine)
}
if _, ok := observed.assets["SHA256SUMS"]; !ok {
t.Fatalf("checksum asset missing: %#v", observed.assets)
@@ -400,6 +417,7 @@ func multiModuleWorkflowEnvironment(t *testing.T, repository, commit, tag, binar
t.Helper()
goDir, goLog := installGoBootstrapShim(t)
environment := templateEnvironment(repository, commit, goDir, goLog)
environment["GOOS"], environment["GOARCH"] = foreignTarget()
environment["CI_COMMIT_TAG"] = tag
environment["RELEASEKIT_TEST_BINARY"] = binary
useSyntheticModuleProxy(t, environment, "file://"+filepath.ToSlash(proxy))

View File

@@ -15,6 +15,8 @@ import (
"testing"
"time"
"golang.org/x/mod/modfile"
"go.maximumdirect.net/releasekit/cmd/releasekit/internal/cli"
"go.maximumdirect.net/releasekit/internal/gotool"
"go.maximumdirect.net/releasekit/internal/publish"
@@ -57,8 +59,8 @@ func TestVendoredAPIOnlyPublicationRecovery(t *testing.T) {
if err != nil {
t.Fatal(err)
}
launcher := exec.Command(goBinary, append(launcherArgs, "publish", "--repo", repo, "v1.2.3")...)
launcher.Dir = t.TempDir()
launcher := exec.Command(goBinary, append(launcherArgs.Args, "publish", "--repo", repo, "v1.2.3")...)
launcher.Dir = launcherArgs.Directory
publicationOutput, err := launcher.CombinedOutput()
var exitError *exec.ExitError
if !errors.As(err, &exitError) || exitError.ExitCode() != 1 {
@@ -98,12 +100,29 @@ func TestRecoveryToolPreservesVersionedReplacement(t *testing.T) {
if err := os.WriteFile(filepath.Join(module, "go.mod"), []byte(data), 0600); err != nil {
t.Fatal(err)
}
shimDir := t.TempDir()
logPath := filepath.Join(t.TempDir(), "target")
realGo := filepath.Join(runtime.GOROOT(), "bin", "go")
shim := "#!/bin/sh\ncase \" $* \" in *\" -deps \"*) printf '%s/%s\\n' \"$GOOS\" \"$GOARCH\" > \"$RECOVERY_TARGET_LOG\" ;; esac\nexec \"$RECOVERY_REAL_GO\" \"$@\"\n"
if err := os.WriteFile(filepath.Join(shimDir, "go"), []byte(shim), 0700); err != nil {
t.Fatal(err)
}
t.Setenv("RECOVERY_TARGET_LOG", logPath)
t.Setenv("RECOVERY_REAL_GO", realGo)
t.Setenv("PATH", shimDir+string(os.PathListSeparator)+os.Getenv("PATH"))
foreignOS, foreignArch := foreignTarget()
t.Setenv("GOOS", foreignOS)
t.Setenv("GOARCH", foreignArch)
args, err := gotool.RecoveryTool(t.Context(), module, t.TempDir())
if err != nil {
t.Fatal(err)
}
command := exec.Command(filepath.Join(runtime.GOROOT(), "bin", "go"), args...)
command.Dir = t.TempDir()
if target, err := os.ReadFile(logPath); err != nil || strings.TrimSpace(string(target)) != runtime.GOOS+"/"+runtime.GOARCH {
t.Fatalf("recovery preparation target = %q, %v", target, err)
}
command := exec.Command(realGo, args.Args...)
command.Dir = args.Directory
command.Env = setEnvironment(setEnvironment(os.Environ(), "GOOS", runtime.GOOS), "GOARCH", runtime.GOARCH)
output, err := command.CombinedOutput()
if err != nil || !strings.Contains(string(output), "tool-version=v0.2.198 helper=tool-only dependency") {
t.Fatalf("versioned recovery selection: %v: %s", err, output)
@@ -119,8 +138,8 @@ func TestRecoveryToolSupportsReleaseKitSourceModule(t *testing.T) {
if err != nil {
t.Fatal(err)
}
command := exec.Command(filepath.Join(runtime.GOROOT(), "bin", "go"), append(args, "version")...)
command.Dir = t.TempDir()
command := exec.Command(filepath.Join(runtime.GOROOT(), "bin", "go"), append(args.Args, "version")...)
command.Dir = args.Directory
output, err := command.CombinedOutput()
if err != nil || !strings.HasPrefix(string(output), "releasekit ") {
t.Fatalf("source module recovery entry point: %v: %s", err, output)
@@ -142,16 +161,25 @@ func TestColdGoToolRecoverySurvivesLauncherExit(t *testing.T) {
if err := os.Symlink(source, selectedSource); err != nil {
t.Fatal(err)
}
if !registered {
relative, err := filepath.Rel(repo, selectedSource)
const toolModule = "go.maximumdirect.net/releasekit"
const toolVersion = "v0.0.0"
module := "module example.com/cli-candidate\n\ngo 1.26.0\n\nrequire " + toolModule + " " + toolVersion + "\n"
var wildcardSource string
if registered {
module += "\nreplace " + toolModule + " => " + strconv.Quote(selectedSource) + "\n"
module += "\ntool " + toolModule + "/cmd/releasekit\n"
} else {
wildcardSource = t.TempDir()
writeRepositoryFile(t, wildcardSource, "go.mod", "module "+toolModule+"\n\ngo 1.26.0\n")
wildcard, err := filepath.Rel(repo, wildcardSource)
if err != nil {
t.Fatal(err)
}
selectedSource = relative
}
module := "module example.com/cli-candidate\n\ngo 1.26.0\n\nrequire go.maximumdirect.net/releasekit v0.0.0\n\nreplace go.maximumdirect.net/releasekit => " + strconv.Quote(selectedSource) + "\n"
if registered {
module += "\ntool go.maximumdirect.net/releasekit/cmd/releasekit\n"
specific, err := filepath.Rel(repo, selectedSource)
if err != nil {
t.Fatal(err)
}
module += "\nreplace (\n" + toolModule + " => " + strconv.Quote(wildcard) + "\n" + toolModule + " " + toolVersion + " => " + strconv.Quote(specific) + "\n)\n"
}
writeRepositoryFile(t, repo, "go.mod", module)
sums, err := os.ReadFile(filepath.Join(source, "go.sum"))
@@ -234,12 +262,39 @@ func TestColdGoToolRecoverySurvivesLauncherExit(t *testing.T) {
if !strings.Contains(command, "go ") || strings.Contains(command, "/exe/releasekit") {
t.Fatalf("unstable handoff: %s", command)
}
if !registered && !strings.Contains(command, "publication-recovery") {
t.Fatalf("API-only handoff lacks durable metadata: %s", command)
if !registered {
store := filepath.Join(repo, ".git", "releasekit", "publication-recovery")
entries, err := os.ReadDir(store)
if err != nil || len(entries) != 1 {
t.Fatalf("API-only recovery modules = %v, %v", entries, err)
}
dir := filepath.Join(store, entries[0].Name())
if !strings.Contains(command, shellQuote(dir)) {
t.Fatalf("API-only handoff lacks durable metadata: %s", command)
}
path := filepath.Join(dir, "go.mod")
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
prepared, err := modfile.Parse(path, data, nil)
if err != nil {
t.Fatal(err)
}
paths := map[string]string{}
for _, replacement := range prepared.Replace {
if replacement.Old.Path == toolModule {
paths[replacement.Old.Version] = replacement.New.Path
}
}
if len(paths) != 2 || paths[""] != wildcardSource || paths[toolVersion] != selectedSource {
t.Fatalf("durable recovery lost replacement selection: %v", paths)
}
}
// Execute from another directory after the original driver and the
// API-only launcher's temporary files are gone.
evidence, err := publish.LoadEvidence(outputLineValue(text, "publication record saved: "))
originalRecord := outputLineValue(text, "publication record saved: ")
evidence, err := publish.LoadEvidence(originalRecord)
if err != nil {
t.Fatal(err)
}
@@ -260,10 +315,13 @@ func TestColdGoToolRecoverySurvivesLauncherExit(t *testing.T) {
}
selected := command
if scenario.record != "" {
selected += " --record " + shellQuote(scenario.record)
selected = strings.Replace(selected, shellQuote(originalRecord), shellQuote(scenario.record), 1)
}
recovery := exec.Command("sh", "-c", selected)
foreignOS, foreignArch := foreignTarget()
goFunction := "go() { GOOS=" + shellQuote(foreignOS) + " GOARCH=" + shellQuote(foreignArch) + " command go \"$@\"; }\n"
recovery := exec.Command("sh", "-c", goFunction+selected)
recovery.Dir = t.TempDir()
recovery.Env = setEnvironment(setEnvironment(os.Environ(), "GOOS", foreignOS), "GOARCH", foreignArch)
recovered, err := recovery.CombinedOutput()
if scenario.status == 0 && err != nil || scenario.status != 0 && (!errors.As(err, &exitError) || exitError.ExitCode() != scenario.status) {
t.Fatalf("recovery status want %d: %v: %s", scenario.status, err, recovered)

View File

@@ -46,6 +46,7 @@ func TestWoodpeckerTagDeliveryTemplate(t *testing.T) {
goDir, goLog := installGoBootstrapShim(t)
env := templateEnvironment(repository, commit, goDir, goLog)
env["GOOS"], env["GOARCH"] = foreignTarget()
moduleBefore := readWoodpeckerFixtureFile(t, filepath.Join(repository, "go.mod"))
output := runWoodpeckerBlock(t, command, repository, env)
if !strings.Contains(output, "state complete") {
@@ -77,6 +78,7 @@ func TestWoodpeckerTagDeliveryTemplate(t *testing.T) {
goDir, goLog := installGoBootstrapShim(t)
env := templateEnvironment(repository, commit, goDir, goLog)
env["GOOS"], env["GOARCH"] = foreignTarget()
moduleBefore := readWoodpeckerFixtureFile(t, filepath.Join(repository, "go.mod"))
sumBefore := readWoodpeckerFixtureFile(t, filepath.Join(repository, "go.sum"))
for attempt := 0; attempt < 2; attempt++ {
@@ -108,6 +110,7 @@ func TestWoodpeckerTagDeliveryTemplate(t *testing.T) {
unrelated := cliGitOutput(t, repository, "rev-parse", "HEAD")
goDir, goLog := installControlledGoBootstrapShim(t)
env := templateEnvironment(repository, unrelated, goDir, goLog)
env["GOOS"], env["GOARCH"] = foreignTarget()
if output, err := executeWoodpeckerBlock(command, repository, env); err == nil {
t.Fatalf("wrong tag succeeded: %s", output)
} else if !strings.Contains(output, commit) || !strings.Contains(output, unrelated) {
@@ -123,6 +126,7 @@ func TestWoodpeckerTagDeliveryTemplate(t *testing.T) {
writeRepositoryFile(t, repository, "go.mod", "module example.com/different-tool-selection\n\ngo 1.25.0\n")
goDir, goLog := installControlledGoBootstrapShim(t)
env := templateEnvironment(repository, commit, goDir, goLog)
env["GOOS"], env["GOARCH"] = foreignTarget()
if output, err := executeWoodpeckerBlock(command, repository, env); err == nil {
t.Fatalf("dirty module succeeded: %s", output)
}
@@ -153,6 +157,7 @@ func TestWoodpeckerTagDeliveryTemplate(t *testing.T) {
goDir, goLog := installControlledGoBootstrapShim(t)
toolArgs := filepath.Join(t.TempDir(), "tool-arguments")
env := templateEnvironment(repository, strings.ToUpper(eventID), goDir, goLog)
env["GOOS"], env["GOARCH"] = foreignTarget()
env["CI_COMMIT_TAG"] = event.tag
env["RELEASEKIT_TOOL_ARGS_LOG"] = toolArgs
env["RELEASEKIT_TOOL_EXIT"] = "37"
@@ -225,6 +230,7 @@ func TestWoodpeckerTagDeliveryUsesTaggedModuleVersion(t *testing.T) {
moduleBefore := readWoodpeckerFixtureFile(t, filepath.Join(repository, "go.mod"))
commit := oldCommit
env := templateEnvironment(repository, commit, goDir, goLog)
env["GOOS"], env["GOARCH"] = foreignTarget()
useSyntheticModuleProxy(t, env, "file://"+filepath.ToSlash(proxy))
sumLog := filepath.Join(t.TempDir(), "tool-only-sums")
env["RELEASEKIT_SUM_LOG"] = sumLog
@@ -242,6 +248,7 @@ func TestWoodpeckerTagDeliveryUsesTaggedModuleVersion(t *testing.T) {
const module = "go.maximumdirect.net/releasekit"
writeRepositoryFile(t, repository, "go.mod", "module example.com/cli-candidate\n\ngo 1.25.0\n\nrequire "+module+" v0.2.197\n\ntool "+module+"/cmd/releasekit\n")
env := templateEnvironment(repository, "", goDir, goLog)
env["GOOS"], env["GOARCH"] = foreignTarget()
useSyntheticModuleProxy(t, env, "file://"+filepath.ToSlash(proxy))
runWoodpeckerBlock(t, "go mod tidy\ngo mod vendor", repository, env)
cliGitRun(t, repository, "add", "go.mod", "go.sum", "vendor")
@@ -267,6 +274,7 @@ func TestWoodpeckerTagDeliveryUsesTaggedModuleVersion(t *testing.T) {
commit := pushTaggedCLIRepository(t, repository)
moduleBefore := readWoodpeckerFixtureFile(t, filepath.Join(repository, "go.mod"))
env := templateEnvironment(repository, commit, goDir, goLog)
env["GOOS"], env["GOARCH"] = foreignTarget()
useSyntheticModuleProxy(t, env, "file://"+filepath.ToSlash(proxy))
sumLog := filepath.Join(t.TempDir(), "tool-only-sums")
env["RELEASEKIT_SUM_LOG"] = sumLog
@@ -313,6 +321,7 @@ func TestWoodpeckerSourceDeliveryTemplateSelfBootstraps(t *testing.T) {
commit := pushTaggedCLIRepository(t, repository)
goDir, goLog := installGoBootstrapShim(t)
env := templateEnvironment(repository, commit, goDir, goLog)
env["GOOS"], env["GOARCH"] = foreignTarget()
useSyntheticModuleProxy(t, env, proxy.URL)
proxyRequests.Store(0)
env["CI_COMMIT_SHA"] = strings.Repeat("0", len(commit))
@@ -335,6 +344,9 @@ func TestWoodpeckerSourceDeliveryTemplateSelfBootstraps(t *testing.T) {
t.Fatalf("self-bootstrapped command arguments = %q", output)
}
log := readWoodpeckerFixtureFile(t, goLog)
if !strings.Contains(log, "build-target="+runtime.GOOS+"/"+runtime.GOARCH) {
t.Fatalf("source build did not use host target: %s", log)
}
if !strings.Contains(log, "GOWORK=off GOTOOLCHAIN=local") || !strings.Contains(log, "build -mod=readonly -o ") || !strings.Contains(log, "./cmd/releasekit") {
t.Fatalf("self-bootstrap Go command = %q", log)
}
@@ -418,6 +430,8 @@ func installGoBootstrapShim(t *testing.T) (string, string) {
goLog := filepath.Join(shimDir, "go-commands")
shim := `#!/bin/sh
printf 'cwd=%s GOWORK=%s GOTOOLCHAIN=%s %s\n' "$PWD" "$GOWORK" "$GOTOOLCHAIN" "$*" >> "$RELEASEKIT_GO_LOG"
case " $* " in *" -deps "*) printf 'target=%s/%s\n' "$GOOS" "$GOARCH" >> "$RELEASEKIT_GO_LOG" ;; esac
if [ "$1" = build ]; then printf 'build-target=%s/%s\n' "$GOOS" "$GOARCH" >> "$RELEASEKIT_GO_LOG"; fi
"$RELEASEKIT_REAL_GO" "$@"
status=$?
if [ "$status" -eq 0 ] && [ "$1" = "tool" ] && [ -n "${RELEASEKIT_SUM_LOG:-}" ]; then
@@ -442,6 +456,8 @@ func installControlledGoBootstrapShim(t *testing.T) (string, string) {
shim := `#!/bin/sh
set -eu
printf 'cwd=%s GOWORK=%s GOTOOLCHAIN=%s %s\n' "$PWD" "$GOWORK" "$GOTOOLCHAIN" "$*" >> "$RELEASEKIT_GO_LOG"
if [ "$1" = env ]; then exec "$RELEASEKIT_REAL_GO" "$@"; fi
case " $* " in *" -deps "*) printf 'target=%s/%s\n' "$GOOS" "$GOARCH" >> "$RELEASEKIT_GO_LOG" ;; esac
modfile=
for argument in "$@"; do
case "$argument" in -modfile=*) modfile=${argument#-modfile=} ;; esac
@@ -644,13 +660,16 @@ func assertModuleBootstrap(t *testing.T, repository, goLog, moduleBefore string,
func assertGoSelection(t *testing.T, repository, goLog string) {
t.Helper()
log := readWoodpeckerFixtureFile(t, goLog)
if !strings.Contains(log, "target="+runtime.GOOS+"/"+runtime.GOARCH) {
t.Fatalf("dependency inspection did not use host target: %s", log)
}
list := "list -mod=mod -modfile="
tool := "tool -modfile="
module := "go.maximumdirect.net/releasekit"
if !strings.Contains(log, "GOWORK=off GOTOOLCHAIN=local") || !strings.Contains(log, list) || !strings.Contains(log, "-m "+module) {
t.Fatalf("Go module selection command is missing required settings: %q", log)
}
if !strings.Contains(log, tool) || !strings.Contains(log, " releasekit ci-deliver ") || strings.Contains(log, "@v0.2.0") {
if !strings.Contains(log, tool) || !strings.Contains(log, " "+module+"/cmd/releasekit ci-deliver ") || strings.Contains(log, "@v0.2.0") {
t.Fatalf("Go tool did not run the module-selected ReleaseKit: %q", log)
}
if strings.Index(log, list) > strings.Index(log, tool) {

View File

@@ -4,6 +4,8 @@ ReleaseKit currently provides eight commands. The exit statuses below belong
to the native executable. `go run` examples are convenient for source execution,
but the Go driver maps a nonzero child exit status to 1. Use the built executable
or `./scripts/release` for publication when distinct statuses are needed.
For the `go run` examples, [select the Go host target](consumers/adoption.md#select-the-go-host-target)
before running from a shell set to cross-compile.
Print the executable version:
@@ -154,7 +156,9 @@ uses a persistent native executable when available. Recognized temporary Go-buil
executables provide no native fallback after their driver exits. Command
preparation has its own bounded context after a failed publication, so publication
cancellation permits preparing the later invocation. Each entry point preserves
the recovery exit status.
the recovery exit status. Run a generated Go-tool handoff in a POSIX shell.
When it runs, it sets `GOOS` and `GOARCH` to the selected Go toolchain's
`GOHOSTOS` and `GOHOSTARCH`.
If the selected entry point becomes unavailable, use another supported ReleaseKit
CLI with the saved record.
@@ -338,11 +342,12 @@ status 2. Final delivery uses the same statuses as `deliver`: 0 for complete,
state, including when cancellation prevents reconciliation.
The maintained module-based tag workflow verifies and checks out the event tag
before resolving the consumer module. It runs `go tool releasekit` for the version
already selected by that tagged `go.mod`, using temporary alternate module files
outside the source checkout. This tool bootstrap is separate from `ci-deliver`'s
application dependency preparation for binary profiles. ReleaseKit's own
source workflow self-bootstraps `./cmd/releasekit`; consumers without a declared
before resolving the consumer module. It invokes the ReleaseKit tool by its full
package path for the version already selected by that tagged `go.mod`, using
temporary alternate module files outside the source checkout. This tool bootstrap
is separate from `ci-deliver`'s application dependency preparation for binary
profiles. ReleaseKit's own source workflow self-bootstraps `./cmd/releasekit`;
consumers without a declared
ReleaseKit module dependency can use the separately installed-tool command.
See the [delivery guide](consumers/delivery.md#woodpecker-tag-workflows) for
compatible Go environments, retries, and preparation details.

View File

@@ -12,7 +12,6 @@ From the consumer module's root, select the ReleaseKit version to adopt:
: "${RELEASEKIT_TOOL_VERSION:?Set the released ReleaseKit version to adopt}"
go get -tool "go.maximumdirect.net/releasekit/cmd/releasekit@${RELEASEKIT_TOOL_VERSION}"
go mod tidy
go tool releasekit version
```
Select a release that implements the API, profile, and workflow you are adopting.
@@ -33,18 +32,32 @@ participate in dependency version selection and can raise the consumer module's
Go minimum. See the
[Go tool dependency reference](https://go.dev/ref/mod#go-mod-file-tool).
Run `go tool releasekit` from within the consumer module to use its selected
version. Go builds and caches the executable and preserves its exit status.
A launcher invoked from that module can forward commands directly:
### Select the Go host target
Invoke the ReleaseKit tool by its full package path from within the consumer
module to use its selected version. Go builds and caches the executable and
preserves its exit status. The full path distinguishes tools with the same
short name. A launcher can forward commands directly:
```sh
#!/bin/sh
exec go tool releasekit "$@"
releasekit_host_os=$(go env GOHOSTOS) || exit $?
releasekit_host_arch=$(go env GOHOSTARCH) || exit $?
export GOOS="$releasekit_host_os" GOARCH="$releasekit_host_arch"
exec go tool go.maximumdirect.net/releasekit/cmd/releasekit "$@"
```
Save this launcher as `releasekit` and run `sh ./releasekit version` from the
selected module to verify registration. It preserves the consumer's workspace
and toolchain settings while selecting the host for the executable.
For a launcher callable from other directories, first change to its consumer
module directory. Follow the [CLI contract](../cli.md) for commands and exit
statuses, and the [configuration reference](../config.md) for the release profile.
statuses.
Configured product checks inherit the launcher's host `GOOS` and `GOARCH`.
If a check needs an application target, set it in that check's `env` map or
command. The [configuration reference](../config.md) describes
check environments and the separate target selection for built-in builds.
For consumers using vendoring, register the CLI before running `go mod vendor`
so the vendor tree includes its dependencies. `go tool` follows Go's normal
@@ -102,12 +115,16 @@ Go module identity. The Go consumer minimum remains 1.26.0.
## CI adoption
The maintained [consumer tag-delivery workflow](delivery.md#woodpecker-tag-workflows)
supports both tool and ordinary package dependencies. It runs `go tool releasekit`
with temporary module metadata derived from the tagged consumer's module
selection, so the committed module files also govern CI delivery and retries.
supports both tool and ordinary package dependencies. It runs the ReleaseKit
tool by its full package path with temporary metadata derived from the tagged
module, so the committed module files also govern CI delivery and retries.
Registering the CLI as a tool is sufficient for
consumers that use ReleaseKit only for maintainer commands.
Update copied launchers and workflow templates separately when upgrading the
module dependency. ReleaseKit tests the generic bootstrap; consumer wiring tests
can focus on selectors and profile mapping. Profiles continue to select
application build targets.
Upgrade the module-selected tool and workflow together. For a tag created before
that upgrade, retry with the workflow matching the tool recorded at that tag.
Keep its immutable module files and tag intact. See the

View File

@@ -293,11 +293,13 @@ identify an annotated tag object or a commit. The template requires the resolved
commits to match, checks out that commit detached, and confirms the source tree
is clean. It then copies the tagged `go.mod` and any `go.sum` to a temporary
directory outside the checkout. `go list -m` confirms that the module selects
ReleaseKit. The template adds its CLI tool declaration to that temporary copy
ReleaseKit. The template selects the host Go target before preparing and running
the CLI. The template adds its CLI tool declaration to that temporary copy
and prepares tool dependencies with `go list -deps`. It then uses
`go tool -modfile=... releasekit` to build, cache, and run the selected tool with
`CI_WORKSPACE`, the resolved event commit, and `CI_COMMIT_TAG`. The tagged module
files stay unchanged, including for consumers with an ordinary package
`go tool -modfile=... go.maximumdirect.net/releasekit/cmd/releasekit` to build,
cache, and run the selected tool with `CI_WORKSPACE`, the resolved event commit,
and `CI_COMMIT_TAG`. The tagged module files stay unchanged, including for
consumers with an ordinary package
dependency. A cleanup trap removes the temporary metadata; Go manages the
cached executable. The command preserves the tool's exit status.
@@ -336,10 +338,21 @@ and application artifact compilation retain their separate environments.
The normal Go module and build caches are sufficient. Cache mounts may be added
as deployment configuration when they help the runner; they are not needed by
the template. A separately installed executable can be installed with
`go install go.maximumdirect.net/releasekit/cmd/releasekit@PIN`, then run
with the same arguments. With the event object available in the CI checkout,
resolve it to its source commit before invocation:
the template. For a standalone installation, replace `PIN` with the desired
version and [select the Go host target](adoption.md#select-the-go-host-target)
for `go install`:
```sh
(
releasekit_host_os=$(go env GOHOSTOS) || exit $?
releasekit_host_arch=$(go env GOHOSTARCH) || exit $?
export GOOS="$releasekit_host_os" GOARCH="$releasekit_host_arch"
go install go.maximumdirect.net/releasekit/cmd/releasekit@PIN
)
```
With the event object available in the CI checkout, resolve it to its source
commit before invoking the installed executable:
```sh
expected_commit=$(git -C "$CI_WORKSPACE" rev-parse --verify "${CI_COMMIT_SHA}^{commit}")

View File

@@ -12,9 +12,14 @@ For CLI publication recovery, the package selects a registered module tool or
prepares a durable private module root for an API-only consumer. Go's module
graph query includes transitive dependencies; preparation retains module
requirements, versioned replacements, and checksums. Local replacements resolve
against their original module directory before relocation. The original main
module remains a local replacement, including when releasing ReleaseKit itself.
against their original module directory before relocation. Each relocation
updates the directive's destination and retains its path and version selector.
The original main module remains a local replacement, including when releasing
ReleaseKit itself.
The private root provides tool resolution independently of consumer vendoring.
API-only preparation discovers the host target for dependency inspection. The
returned invocation carries its directory and tool arguments; the CLI's later
shell command selects the host again when it runs.
The CLI formats the later invocation using a separate bounded context after
publication failure. It falls back to a persistent native executable when module
selection is unavailable or preparation fails, and excludes recognized temporary

View File

@@ -188,19 +188,35 @@ Woodpecker workflow builds the CLI from the tagged checkout, then uses
destination and secret, copy it to `.woodpecker/releasekit-source-delivery.yml`
and add the Woodpecker repository secret `releasekit_gitea_token`. If page
delivery fails or is uncertain, inspect the hosted release and retry the same
immutable version from its tag:
immutable version from its tag. The retry requires a clean tagged checkout
before preparing or building the tool:
```sh
git fetch origin "refs/tags/vX.Y.Z:refs/tags/vX.Y.Z"
git checkout --detach "vX.Y.Z"
GOTOOLCHAIN=local go mod download
mkdir -p .releasekit/bin
GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off go build -o .releasekit/bin/releasekit ./cmd/releasekit
.releasekit/bin/releasekit deliver "vX.Y.Z"
(
set -e
version=vX.Y.Z
tag_ref="refs/tags/$version"
git fetch origin "$tag_ref:$tag_ref"
tag_commit=$(git rev-parse --verify "$tag_ref^{commit}")
git checkout --detach "$tag_commit"
worktree_status=$(git status --porcelain --untracked-files=all)
if [ -n "$worktree_status" ]; then
printf '%s\n' 'Delivery retry requires a clean tagged checkout.' >&2
exit 1
fi
releasekit_host_os=$(GOTOOLCHAIN=local go env GOHOSTOS)
releasekit_host_arch=$(GOTOOLCHAIN=local go env GOHOSTARCH)
export GOOS="$releasekit_host_os" GOARCH="$releasekit_host_arch"
GOTOOLCHAIN=local go mod download
mkdir -p .releasekit/bin
GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off go build -o .releasekit/bin/releasekit ./cmd/releasekit
.releasekit/bin/releasekit deliver "$version"
)
```
For an older tag whose CLI predates `token_file`, supply the token through its
environment variable when retrying that tagged tool:
For an older tag whose CLI predates `token_file`, build the tagged executable
as above, then supply the token through its environment variable when retrying:
```sh
RELEASEKIT_GITEA_TOKEN="$(cat .releasekit/gitea-token)" \

41
docs/releases/v0.9.0.md Normal file
View File

@@ -0,0 +1,41 @@
# ReleaseKit v0.9.0
ReleaseKit's maintained launchers and generated Go recovery commands now
prepare and run the release tool for the Go host when inherited `GOOS` and
`GOARCH` select another target. Release profiles still select the targets for
application builds.
## Launcher, delivery, and recovery fixes
- Local launchers and Woodpecker templates select the host before preparing
tool dependencies or compiling the CLI. Module-based launchers invoke
ReleaseKit by its full Go package path, so another registered tool named
`releasekit` does not make the invocation ambiguous.
- Generated Go recovery commands select the host when run in a POSIX shell
and find the Go executable through `PATH`. Recovery for API-only consumers
preserves both wildcard and version-specific module replacements when
relocating relative paths, fixing a panic after a failed publication.
- The manual hosted-page retry stops if fetching or checking out the tag fails,
or if local edits remain. It checks the tagged checkout before downloading
dependencies or building the CLI.
## Compatibility and upgrade
The module path remains `go.maximumdirect.net/releasekit` and the Go consumer
minimum remains 1.26.0. Public Go APIs, release profiles, and publication
record formats are unchanged.
Update copied launchers and CI templates to get the host and tool selection
fixes. Updating the module dependency alone does not update those files.
Configured product checks inherit the launcher's host target; if a check
needs an application target, set it explicitly in the check's `env` map
or command.
See the [adoption guide](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.9.0/docs/consumers/adoption.md),
[delivery guide](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.9.0/docs/consumers/delivery.md),
and [maintainer runbook](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.9.0/docs/release.md)
for updated recipes.
[COPYRIGHT](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.9.0/COPYRIGHT) |
[LICENSE](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.9.0/LICENSE) |
[ATTRIBUTION](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.9.0/docs/oss/attribution.md)

View File

@@ -73,9 +73,9 @@ Then select a module and semantic version with the shared launcher:
The launcher copies the selected module's `go.mod` and `go.sum` to temporary
alternate module files outside the checkout, resolves ReleaseKit from that
module's local dependency selection, adds the CLI tool declaration to that
temporary copy, and prepares its dependencies. It uses `go tool releasekit`
with the alternate module file to invoke `publish` with the repository profile
and `--expected-module` so the library can verify that the profile agrees with
temporary copy, and prepares its dependencies. It invokes the tool by its full
package path with the alternate module file to run `publish` with the repository
profile and `--expected-module` so the library can verify that the profile agrees with
the selected table row. Go builds and caches the executable and preserves its
exit status; the launcher removes the temporary metadata on exit. As with any
`publish` invocation, commit the selected module's release note and prepare its
@@ -89,9 +89,9 @@ This declaration does not infer dependency order or rerun the standard suite.
The maintained [Woodpecker example](woodpecker.yml) uses the same map and
selector for root, `ckconfig`, and major-version tags. It verifies and detaches
the literal event tag before selecting the module, copies that module's metadata
outside the checkout, and runs the ReleaseKit version selected there through
`go tool releasekit`. It then uses `ci-deliver --prepared` with the mapped profile
and expected physical module.
outside the checkout, and runs the selected ReleaseKit tool by its full package
path. It passes `ci-deliver --prepared`, the mapped profile, and the expected
physical module to the tool.
The `core-v2` profile demonstrates binary delivery; the other profiles are
page-only. Before enabling the workflow, configure each selected profile's
`delivery` object with `"forge": "gitea"`, its `base_url`, owner, and repository,
@@ -126,9 +126,27 @@ source and each selected product's module-local release note, then commit the
fixture and run the source-selected tool without publishing:
```sh
go tool releasekit check --config releasekit-root.json v1.2.3
go -C integrations/ckconfig tool releasekit check --config releasekit-ckconfig.json v1.2.3
go -C integrations/core/v2 tool releasekit check --config releasekit-core-v2.json v2.1.0
(
cd . || exit $?
releasekit_host_os=$(go env GOHOSTOS) || exit $?
releasekit_host_arch=$(go env GOHOSTARCH) || exit $?
export GOOS="$releasekit_host_os" GOARCH="$releasekit_host_arch"
go tool go.maximumdirect.net/releasekit/cmd/releasekit check --config releasekit-root.json v1.2.3
)
(
cd ./integrations/ckconfig || exit $?
releasekit_host_os=$(go env GOHOSTOS) || exit $?
releasekit_host_arch=$(go env GOHOSTARCH) || exit $?
export GOOS="$releasekit_host_os" GOARCH="$releasekit_host_arch"
go tool go.maximumdirect.net/releasekit/cmd/releasekit check --config releasekit-ckconfig.json v1.2.3
)
(
cd ./integrations/core/v2 || exit $?
releasekit_host_os=$(go env GOHOSTOS) || exit $?
releasekit_host_arch=$(go env GOHOSTARCH) || exit $?
export GOOS="$releasekit_host_os" GOARCH="$releasekit_host_arch"
go tool go.maximumdirect.net/releasekit/cmd/releasekit check --config releasekit-core-v2.json v2.1.0
)
```
These commands use the repository-root profile paths even when invoked from a

View File

@@ -36,10 +36,13 @@ releasekit_module=go.maximumdirect.net/releasekit
cd "$module_path"
export GOWORK=off
export GOTOOLCHAIN=local
releasekit_host_os=$(go env GOHOSTOS) || exit $?
releasekit_host_arch=$(go env GOHOSTARCH) || exit $?
export GOOS="$releasekit_host_os" GOARCH="$releasekit_host_arch"
go list -mod=mod -modfile="$bootstrap_dir/releasekit.mod" -m "$releasekit_module"
go mod edit -modfile="$bootstrap_dir/releasekit.mod" -tool="$releasekit_module/cmd/releasekit"
go list -mod=mod -modfile="$bootstrap_dir/releasekit.mod" -deps "$releasekit_module/cmd/releasekit" > /dev/null
go tool -modfile="$bootstrap_dir/releasekit.mod" releasekit publish \
go tool -modfile="$bootstrap_dir/releasekit.mod" "$releasekit_module/cmd/releasekit" publish \
--repo "$repository_dir" \
--config "$profile_path" \
--expected-module "$module_dir" \

View File

@@ -52,10 +52,13 @@ steps:
releasekit_module=go.maximumdirect.net/releasekit
(
cd "$${module_path}"
releasekit_host_os=$(go env GOHOSTOS) || exit $?
releasekit_host_arch=$(go env GOHOSTARCH) || exit $?
export GOOS="$${releasekit_host_os}" GOARCH="$${releasekit_host_arch}"
go list -mod=mod -modfile="$${bootstrap_dir}/releasekit.mod" -m "$${releasekit_module}"
go mod edit -modfile="$${bootstrap_dir}/releasekit.mod" -tool="$${releasekit_module}/cmd/releasekit"
go list -mod=mod -modfile="$${bootstrap_dir}/releasekit.mod" -deps "$${releasekit_module}/cmd/releasekit" > /dev/null
go tool -modfile="$${bootstrap_dir}/releasekit.mod" releasekit ci-deliver \
go tool -modfile="$${bootstrap_dir}/releasekit.mod" "$${releasekit_module}/cmd/releasekit" ci-deliver \
--repo "$${CI_WORKSPACE}" \
--config "$${profile_path}" \
--expected-module "$${module_dir}" \

View File

@@ -30,6 +30,9 @@ steps:
trap 'rm -rf "$${bootstrap_dir}"' EXIT
export GOWORK=off
export GOTOOLCHAIN=local
releasekit_host_os=$(go env GOHOSTOS) || exit $?
releasekit_host_arch=$(go env GOHOSTARCH) || exit $?
export GOOS="$${releasekit_host_os}" GOARCH="$${releasekit_host_arch}"
go build -mod=readonly -o "$${bootstrap_dir}/releasekit" ./cmd/releasekit
"$${bootstrap_dir}/releasekit" ci-deliver \
--repo "$${CI_WORKSPACE}" \

View File

@@ -41,11 +41,14 @@ steps:
export GOWORK=off
export GOTOOLCHAIN=local
releasekit_host_os=$(go env GOHOSTOS) || exit $?
releasekit_host_arch=$(go env GOHOSTARCH) || exit $?
export GOOS="$${releasekit_host_os}" GOARCH="$${releasekit_host_arch}"
releasekit_module=go.maximumdirect.net/releasekit
go list -mod=mod -modfile="$${bootstrap_dir}/releasekit.mod" -m "$${releasekit_module}"
go mod edit -modfile="$${bootstrap_dir}/releasekit.mod" -tool="$${releasekit_module}/cmd/releasekit"
go list -mod=mod -modfile="$${bootstrap_dir}/releasekit.mod" -deps "$${releasekit_module}/cmd/releasekit" > /dev/null
go tool -modfile="$${bootstrap_dir}/releasekit.mod" releasekit ci-deliver \
go tool -modfile="$${bootstrap_dir}/releasekit.mod" "$${releasekit_module}/cmd/releasekit" ci-deliver \
--repo "$${CI_WORKSPACE}" \
--prepared \
--commit "$${expected_commit}" \

View File

@@ -43,10 +43,16 @@ func TemporaryExecutable(path string) bool {
return err == nil
}
// RecoveryTool returns module-selected Go arguments for a later invocation.
// An empty result means this module does not select ReleaseKit. API-only modules
// RecoveryInvocation describes a module-selected Go tool invocation.
type RecoveryInvocation struct {
Directory string
Args []string
}
// RecoveryTool returns a module-selected Go invocation for later use.
// A nil result means this module does not select ReleaseKit. API-only modules
// receive a private module root that outlives launcher-owned bootstraps.
func RecoveryTool(ctx context.Context, moduleDir, recordDir string) ([]string, error) {
func RecoveryTool(ctx context.Context, moduleDir, recordDir string) (*RecoveryInvocation, error) {
path := filepath.Join(moduleDir, "go.mod")
data, err := os.ReadFile(path)
if err != nil {
@@ -58,7 +64,7 @@ func RecoveryTool(ctx context.Context, moduleDir, recordDir string) ([]string, e
}
for _, tool := range module.Tool {
if tool.Path == releaseKitCommand {
return []string{"-C", moduleDir, "tool", "-modfile=", releaseKitCommand}, nil
return &RecoveryInvocation{Directory: moduleDir, Args: []string{"tool", "-modfile=", releaseKitCommand}}, nil
}
}
// Ask Go about the selected module graph, including transitive dependencies.
@@ -94,9 +100,11 @@ func RecoveryTool(ctx context.Context, moduleDir, recordDir string) ([]string, e
// replacements relative to the directory that originally owned go.mod.
for _, replacement := range module.Replace {
if replacement.New.Version == "" && !filepath.IsAbs(replacement.New.Path) {
if err := module.AddReplace(replacement.Old.Path, replacement.Old.Version, filepath.Join(moduleDir, replacement.New.Path), ""); err != nil {
return nil, err
}
// A local replacement ends in its path token. Edit that token on
// the parsed directive so a wildcard does not absorb versioned ones.
replacement.New.Path = filepath.Join(moduleDir, replacement.New.Path)
tokens := replacement.Syntax.Token
tokens[len(tokens)-1] = modfile.AutoQuote(replacement.New.Path)
}
}
// The original main module's source remains local if a dependency refers
@@ -140,15 +148,31 @@ func RecoveryTool(ctx context.Context, moduleDir, recordDir string) ([]string, e
return nil, err
}
// Prepare the private graph and sums before the later tool invocation.
environment := map[string]string{"GOWORK": "off", "GOTOOLCHAIN": "local"}
hostJSON, err := discover(ctx, directory, environment, "go", "env", "-json", "GOHOSTOS", "GOHOSTARCH")
if err != nil {
return nil, fmt.Errorf("prepare recovery tool: %w", err)
}
var host struct {
GOHOSTOS string
GOHOSTARCH string
}
if err := json.Unmarshal(hostJSON, &host); err != nil {
return nil, fmt.Errorf("prepare recovery tool: decode host target: %w", err)
}
if host.GOHOSTOS == "" || host.GOHOSTARCH == "" {
return nil, fmt.Errorf("prepare recovery tool: missing host target")
}
environment["GOOS"], environment["GOARCH"] = host.GOHOSTOS, host.GOHOSTARCH
captured := &combinedWriter{mode: captureStatus}
err = process.Run(ctx, process.Invocation{
Args: []string{"go", "list", "-mod=mod", "-modfile=", "-deps", releaseKitCommand},
Dir: directory, Env: map[string]string{"GOWORK": "off", "GOTOOLCHAIN": "local"},
Dir: directory, Env: environment,
Stdout: captured, Stderr: captured,
})
if err != nil {
return nil, fmt.Errorf("prepare recovery tool: %w: %s", err, diagnosticText(captured.take(), captured.omitted()))
}
complete = true
return []string{"-C", directory, "tool", "-modfile=", releaseKitCommand}, nil
return &RecoveryInvocation{Directory: directory, Args: []string{"tool", "-modfile=", releaseKitCommand}}, nil
}

View File

@@ -6,6 +6,9 @@ if [ "$#" -ne 1 ]; then
exit 2
fi
releasekit_host_os=$(GOTOOLCHAIN=local go env GOHOSTOS) || exit $?
releasekit_host_arch=$(GOTOOLCHAIN=local go env GOHOSTARCH) || exit $?
export GOOS="$releasekit_host_os" GOARCH="$releasekit_host_arch"
mkdir -p .releasekit/bin
GOTOOLCHAIN=local go build -o .releasekit/bin/releasekit ./cmd/releasekit
exec .releasekit/bin/releasekit publish "$1"