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
|
through the library's exact tag check. Isolate every synthetic dependency consumer
|
||||||
with test-owned `GOMODCACHE`; use local HTTP fixtures and local Git remotes.
|
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
|
## Stage 15: Complete consumer documentation and verify the whole cutover
|
||||||
|
|
||||||
**Outcome:** The supported interface, examples, migration guidance, and behavioral
|
**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
|
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:
|
Then select a module and semantic version with the shared launcher:
|
||||||
|
|
||||||
```sh
|
```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,
|
then publish the integration with the version that consumes it. For example,
|
||||||
publish the core release before an integration that requires that release.
|
publish the core release before an integration that requires that release.
|
||||||
This declaration does not infer dependency order or rerun the standard suite.
|
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",
|
"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