Add mapped multi-module delivery workflow
This commit is contained in:
401
cmd/releasekit/multi_module_woodpecker_test.go
Normal file
401
cmd/releasekit/multi_module_woodpecker_test.go
Normal file
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
11
examples/multi-module/integrations/core/v2/main.go
Normal file
11
examples/multi-module/integrations/core/v2/main.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Printf("core %s\n", buildinfo.Version())
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
|
||||
63
examples/multi-module/woodpecker.yml
Normal file
63
examples/multi-module/woodpecker.yml
Normal file
@@ -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}"
|
||||
Reference in New Issue
Block a user