diff --git a/cmd/releasekit/multi_module_woodpecker_test.go b/cmd/releasekit/multi_module_woodpecker_test.go new file mode 100644 index 0000000..8230801 --- /dev/null +++ b/cmd/releasekit/multi_module_woodpecker_test.go @@ -0,0 +1,401 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "io/fs" + "net/http" + "net/http/httptest" + "net/url" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit/internal/cli" +) + +func TestMultiModuleWoodpeckerDelivery(t *testing.T) { + root := cliRepositoryRoot(t) + example := filepath.Join(root, "examples", "multi-module") + contents, err := os.ReadFile(filepath.Join(example, "woodpecker.yml")) + if err != nil { + t.Fatal(err) + } + block, err := extractWoodpeckerCommand(string(contents), "deliver") + if err != nil { + t.Fatal(err) + } + command := strings.ReplaceAll(block, "$${", "${") + for _, eventTag := range []string{"refs/tags/v[01].*", "refs/tags/integrations/ckconfig/v*", "refs/tags/integrations/core/v2.*"} { + if !strings.Contains(string(contents), eventTag) { + t.Errorf("workflow tag filters omit %q", eventTag) + } + } + toolBinary := buildWorkflowCLI(t, root) + proxy := writeWorkflowToolProxy(t) + + t.Run("nested page delivery retries from tagged mapping", func(t *testing.T) { + const tag = "integrations/ckconfig/v1.2.3" + delivery := newMappedDeliveryFixture(t, tag) + repository := newWorkflowRepository(t, example) + writeMappedProfile(t, repository, "releasekit-ckconfig.json", cli.Settings{ + Product: "Example Consumer Integration", ModuleDir: "integrations/ckconfig", + Delivery: workflowDeliverySettings(delivery.URL), + }) + writeRepositoryFile(t, repository, "integrations/ckconfig/docs/releases/v1.2.3.md", "# Example Consumer Integration v1.2.3\n\nTagged integration note.\n") + writeRepositoryFile(t, repository, "integrations/ckconfig/go.mod", workflowModuleFile("example.com/multi-module-consumer/integrations/ckconfig", "v0.2.197")) + commit := tagAndPushWorkflowRepository(t, repository, tag) + delivery.commit = commit + env := multiModuleWorkflowEnvironment(t, repository, commit, tag, toolBinary, proxy) + 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) + } + if delivery.creates != 1 || delivery.requests == 0 || delivery.tag != tag { + t.Fatalf("page delivery fixture = %#v", delivery) + } + + cliGitRun(t, repository, "checkout", "main") + writeRepositoryFile(t, repository, "release-modules.tsv", "# changed after release\nroot\tv[01].*\t.\treleasekit-root.json\n") + writeRepositoryFile(t, repository, "integrations/ckconfig/go.mod", workflowModuleFile("example.com/multi-module-consumer/integrations/ckconfig", "v0.2.198")) + cliGitRun(t, repository, "add", ".") + cliGitRun(t, repository, "commit", "-m", "change default branch release selection") + cliGitRun(t, repository, "push", "origin", "main") + + output := runWoodpeckerBlock(t, command, repository, env) + if !strings.Contains(output, "tool-version=v0.2.197") || strings.Contains(output, "tool-version=v0.2.198") { + t.Fatalf("tag retry did not use its committed tool version: %q", output) + } + if delivery.creates != 1 || delivery.tag != tag { + t.Fatalf("tag retry did not reuse the tagged mapping: %#v", delivery) + } + }) + + t.Run("binary delivery from physical major module", func(t *testing.T) { + const tag = "integrations/core/v2.1.0" + 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"}}, + Delivery: workflowDeliverySettings(delivery.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") + prepareWorkflowModuleSum(t, repository, "integrations/core/v2", proxy) + commit := tagAndPushWorkflowRepository(t, repository, tag) + delivery.commit = commit + env := multiModuleWorkflowEnvironment(t, repository, commit, tag, toolBinary, proxy) + output := runWoodpeckerBlock(t, command, repository, env) + if !strings.Contains(output, "tool-version=v0.2.197") || !strings.Contains(output, "state complete") { + t.Fatalf("binary delivery output = %q", output) + } + if delivery.creates != 1 || delivery.uploads != 2 || delivery.tag != tag { + t.Fatalf("binary delivery fixture = %#v", delivery) + } + if _, ok := delivery.assets["core-v2.1.0-linux-amd64"]; !ok { + t.Fatalf("binary asset missing: %#v", delivery.assets) + } + if _, ok := delivery.assets["SHA256SUMS"]; !ok { + t.Fatalf("checksum asset missing: %#v", delivery.assets) + } + }) + + t.Run("binary delivery from virtual major module", func(t *testing.T) { + const tag = "integrations/core/v2.1.0" + delivery := newMappedDeliveryFixture(t, tag) + repository := newWorkflowRepository(t, example) + writeRepositoryFile(t, repository, "release-modules.tsv", strings.ReplaceAll(readWorkflowFile(t, repository, "release-modules.tsv"), "integrations/core/v2\treleasekit-core-v2.json", "integrations/core\treleasekit-core-v2.json")) + writeMappedProfile(t, repository, "releasekit-core-v2.json", cli.Settings{ + Product: "Example Consumer Core v2", ModuleDir: "integrations/core", + Delivery: workflowDeliverySettings(delivery.URL), + }) + writeRepositoryFile(t, repository, "integrations/core/go.mod", workflowModuleFile("example.com/multi-module-consumer/integrations/core/v2", "v0.2.197")) + writeRepositoryFile(t, repository, "integrations/core/main.go", readWorkflowFile(t, example, "integrations/core/v2/main.go")) + writeRepositoryFile(t, repository, "integrations/core/docs/releases/v2.1.0.md", "# Example Consumer Core v2 v2.1.0\n\nTagged core note.\n") + prepareWorkflowModuleSum(t, repository, "integrations/core", proxy) + commit := tagAndPushWorkflowRepository(t, repository, tag) + delivery.commit = commit + env := multiModuleWorkflowEnvironment(t, repository, commit, tag, toolBinary, proxy) + output := runWoodpeckerBlock(t, command, repository, env) + if !strings.Contains(output, "tool-version=v0.2.197") || !strings.Contains(output, "state complete") { + t.Fatalf("virtual-major delivery output = %q", output) + } + if delivery.creates != 1 || delivery.tag != tag { + t.Fatalf("virtual-major delivery fixture = %#v", delivery) + } + }) + + t.Run("inconsistent mapping fails exact event tag check", func(t *testing.T) { + const tag = "integrations/ckconfig/v2.1.0" + delivery := newMappedDeliveryFixture(t, tag) + repository := newWorkflowRepository(t, example) + mapping := readWorkflowFile(t, repository, "release-modules.tsv") + mapping = strings.Replace(mapping, + "ckconfig\tintegrations/ckconfig/v*\tintegrations/ckconfig\treleasekit-ckconfig.json", + "ckconfig\tintegrations/ckconfig/v*\tintegrations/core/v2\treleasekit-core-v2.json", 1) + writeRepositoryFile(t, repository, "release-modules.tsv", mapping) + writeMappedProfile(t, repository, "releasekit-core-v2.json", cli.Settings{ + Product: "Example Consumer Core v2", ModuleDir: "integrations/core/v2", + Delivery: workflowDeliverySettings(delivery.URL), + }) + writeRepositoryFile(t, repository, "integrations/core/v2/docs/releases/v2.1.0.md", "# Example Consumer Core v2 v2.1.0\n\nMismatched mapped note.\n") + commit := tagAndPushWorkflowRepository(t, repository, tag) + delivery.commit = commit + env := multiModuleWorkflowEnvironment(t, repository, commit, tag, toolBinary, proxy) + if output, err := executeWoodpeckerBlock(command, repository, env); err == nil { + t.Fatalf("inconsistent mapping succeeded: %s", output) + } else if !strings.Contains(output, tag) || !strings.Contains(output, "integrations/core/v2.1.0") { + t.Fatalf("inconsistent mapping did not fail the exact tag comparison: %s", output) + } + if delivery.requests != 0 { + t.Fatalf("exact event tag mismatch reached hosted delivery: %#v", delivery) + } + }) +} + +type mappedDeliveryFixture struct { + URL string + tag string + commit string + page bool + name string + body string + creates int + uploads int + requests int + assets map[string][]byte +} + +func newMappedDeliveryFixture(t *testing.T, tag string) *mappedDeliveryFixture { + t.Helper() + state := &mappedDeliveryFixture{tag: tag, assets: make(map[string][]byte)} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + state.requests++ + if r.Header.Get("Authorization") != "token fixture-token" { + t.Errorf("delivery authorization = %q", r.Header.Get("Authorization")) + } + apiRoot := "/api/v1/repos/owner/example/" + escapedTag := url.PathEscape(tag) + switch r.URL.EscapedPath() { + case apiRoot + "tags/" + escapedTag: + _ = json.NewEncoder(w).Encode(map[string]any{"name": tag, "commit": map[string]string{"sha": state.commit}}) + case apiRoot + "releases/tags/" + escapedTag: + if !state.page { + http.NotFound(w, r) + return + } + _ = json.NewEncoder(w).Encode(map[string]any{"id": 10, "html_url": serverURL(r) + "/releases/10", "tag_name": tag, "target_commitish": state.commit, "name": state.name, "body": state.body, "draft": false, "prerelease": false}) + case apiRoot + "releases": + var payload struct { + Tag string `json:"tag_name"` + Commit string `json:"target_commitish"` + Name string `json:"name"` + Body string `json:"body"` + } + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + t.Errorf("decode release create: %v", err) + } + if payload.Tag != tag || payload.Commit != state.commit { + t.Errorf("release payload = %#v", payload) + } + state.page = true + state.name = payload.Name + state.body = payload.Body + state.creates++ + _ = json.NewEncoder(w).Encode(map[string]any{"id": 10, "html_url": serverURL(r) + "/releases/10", "tag_name": tag, "target_commitish": state.commit, "name": payload.Name, "body": payload.Body, "draft": false, "prerelease": false}) + case apiRoot + "releases/10/assets": + if r.Method == http.MethodGet { + listing := make([]map[string]any, 0, len(state.assets)) + for name, content := range state.assets { + listing = append(listing, map[string]any{"id": len(listing) + 1, "name": name, "size": len(content), "browser_download_url": serverURL(r) + "/files/" + name}) + } + _ = json.NewEncoder(w).Encode(listing) + return + } + if err := r.ParseMultipartForm(1 << 20); err != nil { + t.Errorf("parse asset upload: %v", err) + http.Error(w, "multipart", http.StatusBadRequest) + return + } + name := r.URL.Query().Get("name") + file, _, err := r.FormFile("attachment") + if err != nil { + t.Errorf("read asset %q: %v", name, err) + http.Error(w, "attachment", http.StatusBadRequest) + return + } + content, err := io.ReadAll(file) + _ = file.Close() + if err != nil { + t.Errorf("read asset bytes: %v", err) + } + state.assets[name] = content + state.uploads++ + _ = json.NewEncoder(w).Encode(map[string]any{"id": state.uploads, "name": name, "size": len(content)}) + default: + if strings.HasPrefix(r.URL.Path, "/files/") { + name := strings.TrimPrefix(r.URL.Path, "/files/") + if content, ok := state.assets[name]; ok { + _, _ = w.Write(content) + return + } + http.NotFound(w, r) + return + } + t.Errorf("unexpected delivery request %s %s", r.Method, r.URL.EscapedPath()) + http.NotFound(w, r) + } + })) + t.Cleanup(server.Close) + state.URL = server.URL + return state +} + +func serverURL(r *http.Request) string { + return "http://" + r.Host +} + +func buildWorkflowCLI(t *testing.T, root string) string { + t.Helper() + path := filepath.Join(t.TempDir(), "releasekit") + command := exec.Command("go", "build", "-o", path, "./cmd/releasekit") + command.Dir = root + command.Env = append(os.Environ(), "GOTOOLCHAIN=local", "GOPROXY=off", "GOSUMDB=off") + if output, err := command.CombinedOutput(); err != nil { + t.Fatalf("build workflow CLI: %v\n%s", err, output) + } + return path +} + +func writeWorkflowToolProxy(t *testing.T) string { + t.Helper() + const module = "gitea.maximumdirect.net/eric/ReleaseKit" + const helper = "example.com/releasekit-helper" + proxyDir := t.TempDir() + moduleFile := "module " + module + "\n\ngo 1.25.0\n\nrequire " + helper + " v1.0.0\n" + for _, version := range []string{"v0.2.197", "v0.2.198"} { + main := fmt.Sprintf(`package main + +import ( + "fmt" + "os" + "os/exec" + helper "%s" +) + +func main() { + fmt.Println("tool-version=%s helper=" + helper.Version) + command := exec.Command(os.Getenv("RELEASEKIT_TEST_BINARY"), os.Args[1:]...) + command.Env = os.Environ() + command.Stdout = os.Stdout + command.Stderr = os.Stderr + if err := command.Run(); err != nil { + if exit, ok := err.(*exec.ExitError); ok { os.Exit(exit.ExitCode()) } + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} +`, helper, version) + files := map[string]string{ + "cmd/releasekit/main.go": main, + "buildinfo/version.go": "package buildinfo\nimport \"gitea.maximumdirect.net/eric/ReleaseKit/internal/runtimeinfo\"\nfunc Version() string { return runtimeinfo.Version() }\n", + "internal/runtimeinfo/version.go": "package runtimeinfo\nvar injectedVersion string\nfunc Version() string { if injectedVersion != \"\" { return injectedVersion }; return \"dev\" }\n", + } + writeModuleProxyVersion(t, proxyDir, module, version, moduleFile, files) + } + writeModuleProxyVersion(t, proxyDir, helper, "v1.0.0", "module "+helper+"\n\ngo 1.25.0\n", map[string]string{"helper.go": "package helper\nconst Version = \"tagged dependency\"\n"}) + return proxyDir +} + +func newWorkflowRepository(t *testing.T, example string) string { + t.Helper() + repository := filepath.Join(t.TempDir(), "multi module checkout") + if err := os.Mkdir(repository, 0o700); err != nil { + t.Fatal(err) + } + copyExampleTree(t, example, repository) + cliGitRun(t, repository, "init", "--initial-branch=main") + cliGitRun(t, repository, "config", "user.name", "ReleaseKit Test") + cliGitRun(t, repository, "config", "user.email", "releasekit@example.test") + cliGitRun(t, repository, "config", "commit.gpgsign", "false") + return repository +} + +func workflowModuleFile(module, version string) string { + return fmt.Sprintf("module %s\n\ngo 1.25.0\n\nrequire gitea.maximumdirect.net/eric/ReleaseKit %s\n", module, version) +} + +func workflowDeliverySettings(baseURL string) *cli.DeliverySettings { + return &cli.DeliverySettings{BaseURL: baseURL, Owner: "owner", Repository: "example", TokenEnv: "RELEASEKIT_GITEA_TOKEN"} +} + +func writeMappedProfile(t *testing.T, repository, name string, settings cli.Settings) { + t.Helper() + contents, err := json.MarshalIndent(settings, "", " ") + if err != nil { + t.Fatal(err) + } + writeRepositoryFile(t, repository, name, string(contents)+"\n") +} + +func tagAndPushWorkflowRepository(t *testing.T, repository, tag string) string { + t.Helper() + cliGitRun(t, repository, "add", "--all") + cliGitRun(t, repository, "commit", "-m", "prepare mapped delivery") + commit := cliGitOutput(t, repository, "rev-parse", "HEAD") + cliGitRun(t, repository, "tag", tag) + remote := filepath.Join(filepath.Dir(repository), "origin.git") + cliGitRun(t, repository, "init", "--bare", remote) + cliGitRun(t, repository, "remote", "add", "origin", remote) + cliGitRun(t, repository, "push", "origin", "HEAD:refs/heads/main", "refs/tags/"+tag) + return commit +} + +func multiModuleWorkflowEnvironment(t *testing.T, repository, commit, tag, binary, proxy string) map[string]string { + t.Helper() + goDir, goLog := installGoBootstrapShim(t) + environment := templateEnvironment(repository, commit, goDir, goLog) + environment["CI_COMMIT_TAG"] = tag + environment["RELEASEKIT_TEST_BINARY"] = binary + useSyntheticModuleProxy(t, environment, "file://"+filepath.ToSlash(proxy)) + cache := environment["GOMODCACHE"] + t.Cleanup(func() { + _ = filepath.WalkDir(cache, func(path string, entry fs.DirEntry, err error) error { + if err == nil && entry.IsDir() { + _ = os.Chmod(path, 0o700) + } + return nil + }) + }) + return environment +} + +func prepareWorkflowModuleSum(t *testing.T, repository, moduleDir, proxy string) { + t.Helper() + cache := t.TempDir() + t.Cleanup(func() { + _ = filepath.WalkDir(cache, func(path string, entry fs.DirEntry, err error) error { + if err == nil && entry.IsDir() { + _ = os.Chmod(path, 0o700) + } + return nil + }) + }) + command := exec.Command("go", "mod", "download", "gitea.maximumdirect.net/eric/ReleaseKit@v0.2.197") + command.Dir = filepath.Join(repository, filepath.FromSlash(moduleDir)) + command.Env = append(os.Environ(), "GOMODCACHE="+cache, "GOPROXY=file://"+filepath.ToSlash(proxy), "GOSUMDB=off", "GOWORK=off", "GOTOOLCHAIN=local", "GOFLAGS=-modcacherw") + if output, err := command.CombinedOutput(); err != nil { + t.Fatalf("prepare tagged module sums: %v\n%s", err, output) + } +} + +func readWorkflowFile(t *testing.T, repository, name string) string { + t.Helper() + contents, err := os.ReadFile(filepath.Join(repository, filepath.FromSlash(name))) + if err != nil { + t.Fatal(err) + } + return string(contents) +} diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 524abb2..542ea43 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -704,6 +704,18 @@ tag and verify retry still uses its original selection. Fail inconsistent mappin through the library's exact tag check. Isolate every synthetic dependency consumer with test-owned `GOMODCACHE`; use local HTTP fixtures and local Git remotes. +**Completed:** Added the multi-module Woodpecker workflow with full root and +nested tag filters, exact peeled-tag commit verification, and selection from the +verified checkout. Bootstrap uses the mapped physical module's committed Go +metadata and profile, then invokes prepared CI delivery with the expected module, +commit, and full tag. Added the binary core v2 example and documented committed +module checksums for clean binary delivery. Workflow harnesses cover nested +page-only and binary delivery, physical and virtual `/v2` module layouts, a retry +after default-branch mapping and tool changes, and an inconsistent mapping +rejected by exact event-tag validation. Tests use local Git remotes, local HTTP +delivery fixtures, a local module proxy, and test-owned `GOMODCACHE`. Verified +`go test ./...`, `go run ./internal/apicheck`, shell syntax, and `git diff --check`. + ## Stage 15: Complete consumer documentation and verify the whole cutover **Outcome:** The supported interface, examples, migration guidance, and behavioral diff --git a/examples/multi-module/README.md b/examples/multi-module/README.md index cafa773..630e473 100644 --- a/examples/multi-module/README.md +++ b/examples/multi-module/README.md @@ -33,6 +33,10 @@ go -C integrations/ckconfig mod download go -C integrations/core/v2 mod download ``` +Commit each generated `go.sum` with its module files. The binary delivery +profile prepares application dependencies in the tagged module before building; +the committed sums keep that prepared checkout clean for delivery verification. + Then select a module and semantic version with the shared launcher: ```sh @@ -53,3 +57,11 @@ If an integration depends on a sibling release, publish the dependency first, then publish the integration with the version that consumes it. For example, publish the core release before an integration that requires that release. 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 builds the ReleaseKit version selected there. It then +uses `ci-deliver --prepared` with the mapped profile and expected physical module. +The `core-v2` profile demonstrates binary delivery; the other profiles are +page-only. diff --git a/examples/multi-module/integrations/core/v2/main.go b/examples/multi-module/integrations/core/v2/main.go new file mode 100644 index 0000000..7911a24 --- /dev/null +++ b/examples/multi-module/integrations/core/v2/main.go @@ -0,0 +1,11 @@ +package main + +import ( + "fmt" + + "gitea.maximumdirect.net/eric/ReleaseKit/buildinfo" +) + +func main() { + fmt.Printf("core %s\n", buildinfo.Version()) +} diff --git a/examples/multi-module/releasekit-core-v2.json b/examples/multi-module/releasekit-core-v2.json index 5b3bf32..925ba8e 100644 --- a/examples/multi-module/releasekit-core-v2.json +++ b/examples/multi-module/releasekit-core-v2.json @@ -1,4 +1,9 @@ { "product": "Example Consumer Core v2", - "module_dir": "integrations/core/v2" + "module_dir": "integrations/core/v2", + "build": { + "main_package": ".", + "binary_name": "core", + "targets": ["linux/amd64"] + } } diff --git a/examples/multi-module/woodpecker.yml b/examples/multi-module/woodpecker.yml new file mode 100644 index 0000000..86467bb --- /dev/null +++ b/examples/multi-module/woodpecker.yml @@ -0,0 +1,63 @@ +when: + - event: tag + ref: + include: + - refs/tags/v[01].* + - refs/tags/integrations/ckconfig/v* + - refs/tags/integrations/core/v2.* + +steps: + - name: deliver + image: golang:1.26.8-bookworm + environment: + RELEASEKIT_GITEA_TOKEN: + from_secret: releasekit_gitea_token + commands: + - | + set -eu + test -n "$${CI_COMMIT_TAG}" + test -n "$${CI_COMMIT_SHA}" + git check-ref-format "refs/tags/$${CI_COMMIT_TAG}" + git fetch origin "refs/tags/$${CI_COMMIT_TAG}:refs/tags/$${CI_COMMIT_TAG}" + tag_commit=$(git rev-parse --verify "refs/tags/$${CI_COMMIT_TAG}^{commit}") + expected_commit=$(printf '%s' "$${CI_COMMIT_SHA}" | tr '[:upper:]' '[:lower:]') + test "$(printf '%s' "$${tag_commit}" | tr '[:upper:]' '[:lower:]')" = "$${expected_commit}" + git checkout --detach "$${tag_commit}" + test "$(git rev-parse HEAD | tr '[:upper:]' '[:lower:]')" = "$${expected_commit}" + test -z "$(git status --porcelain --untracked-files=all)" + + bootstrap_dir=$(mktemp -d "$${CI_WORKSPACE%/*}/.releasekit-bootstrap.XXXXXX") + trap 'rm -rf "$${bootstrap_dir}"' EXIT + selection_file="$${bootstrap_dir}/module-selection.tsv" + "$${CI_WORKSPACE}/select-module.sh" tag "$${CI_COMMIT_TAG}" > "$${selection_file}" + tab=$(printf '\t') + IFS="$${tab}" read -r selector tag_pattern module_dir profile_path < "$${selection_file}" + module_path="$${CI_WORKSPACE}" + if [ "$${module_dir}" != . ]; then + module_path="$${CI_WORKSPACE}/$${module_dir}" + fi + cp "$${module_path}/go.mod" "$${bootstrap_dir}/go.mod" + if [ -f "$${module_path}/go.sum" ]; then + cp "$${module_path}/go.sum" "$${bootstrap_dir}/go.sum" + else + : > "$${bootstrap_dir}/go.sum" + fi + cp "$${bootstrap_dir}/go.mod" "$${bootstrap_dir}/releasekit.mod" + cp "$${bootstrap_dir}/go.sum" "$${bootstrap_dir}/releasekit.sum" + + export GOWORK=off + export GOTOOLCHAIN=local + releasekit_module=gitea.maximumdirect.net/eric/ReleaseKit + ( + cd "$${module_path}" + go list -mod=mod -modfile="$${bootstrap_dir}/releasekit.mod" -m "$${releasekit_module}" + go build -mod=mod -modfile="$${bootstrap_dir}/releasekit.mod" \ + -o "$${bootstrap_dir}/releasekit" "$${releasekit_module}/cmd/releasekit" + ) + "$${bootstrap_dir}/releasekit" ci-deliver \ + --repo "$${CI_WORKSPACE}" \ + --config "$${profile_path}" \ + --expected-module "$${module_dir}" \ + --prepared \ + --commit "$${CI_COMMIT_SHA}" \ + "$${CI_COMMIT_TAG}"