diff --git a/README.md b/README.md index 9959110..69310ea 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,10 @@ ReleaseKit is an opinionated Go library and executable for a shared release process. The Go module, root `releasekit` facade, `buildinfo` runtime package, local candidate check, source-tag publication API, selected-target build API, -tagged-source delivery API, and `version`, `check`, `publish`, `build`, and -`deliver` commands are implemented. ReleaseKit's source-only maintainer -procedure and prepared Woodpecker workflow for hosted note-page delivery are -documented. ReleaseKit does not declare binary deliverables. The +tagged-source delivery API, and the `version`, `check`, `publish`, `build`, +`deliver`, and `ci-deliver` commands are implemented. ReleaseKit's source-only +maintainer procedure and prepared Woodpecker workflow for hosted note-page +delivery are documented. ReleaseKit does not declare binary deliverables. The [product roadmap](docs/roadmap/releasekit.md) records remaining adoption and deployment work. @@ -44,8 +44,8 @@ failure recovery. remaining release and deployment work. - [Contributor guide](docs/development.md): orientation, validation, and selective policy reading. -- [CLI contract](docs/cli.md): the `version`, `check`, `publish`, `build`, and - `deliver` commands. +- [CLI contract](docs/cli.md): the `version`, `check`, `publish`, `build`, + `deliver`, and `ci-deliver` commands. - [Configuration](docs/config.md): profile fields and child-command settings. - [Runtime version guidance](docs/consumers/runtime-version.md): how applications read their embedded version through `buildinfo`. diff --git a/cmd/releasekit/internal/cli/config.go b/cmd/releasekit/internal/cli/config.go index 8c6863b..40cd251 100644 --- a/cmd/releasekit/internal/cli/config.go +++ b/cmd/releasekit/internal/cli/config.go @@ -16,16 +16,17 @@ import ( "gitea.maximumdirect.net/eric/ReleaseKit/internal/process" ) -const Usage = "usage: releasekit version\n releasekit check [--repo DIR] [--config FILE] VERSION\n releasekit publish [--repo DIR] [--config FILE] VERSION\n releasekit build [--repo DIR] [--config FILE] [--output DIR] VERSION\n releasekit deliver [--repo DIR] [--config FILE] [--assets DIR] VERSION" +const Usage = "usage: releasekit version\n releasekit check [--repo DIR] [--config FILE] VERSION\n releasekit publish [--repo DIR] [--config FILE] VERSION\n releasekit build [--repo DIR] [--config FILE] [--output DIR] VERSION\n releasekit deliver [--repo DIR] [--config FILE] [--assets DIR] VERSION\n releasekit ci-deliver [--repo DIR] [--config FILE] [--remote NAME] --commit OID VERSION" type Command string const ( - VersionCommand Command = "version" - CheckAction Command = "check" - PublishAction Command = "publish" - BuildAction Command = "build" - DeliveryAction Command = "deliver" + VersionCommand Command = "version" + CheckAction Command = "check" + PublishAction Command = "publish" + BuildAction Command = "build" + DeliveryAction Command = "deliver" + CIDeliveryAction Command = "ci-deliver" ) // Invocation contains parsed command settings owned by the CLI adapter. @@ -35,6 +36,8 @@ type Invocation struct { ConfigPath string OutputDir string AssetsDir string + Remote string + Commit string Version string } @@ -49,7 +52,7 @@ func Parse(args []string) (Invocation, error) { return Invocation{}, errors.New("version takes no arguments") } return Invocation{Command: VersionCommand}, nil - case string(CheckAction), string(PublishAction), string(BuildAction), string(DeliveryAction): + case string(CheckAction), string(PublishAction), string(BuildAction), string(DeliveryAction), string(CIDeliveryAction): action := Command(args[0]) flags := flag.NewFlagSet(string(action), flag.ContinueOnError) flags.SetOutput(io.Discard) @@ -63,18 +66,46 @@ func Parse(args []string) (Invocation, error) { if action == DeliveryAction { flags.StringVar(&assetsDir, "assets", "", "verified build assets directory") } + var remote *string + expectedCommit := "" + if action == CIDeliveryAction { + remote = flags.String("remote", "origin", "Git remote") + flags.StringVar(&expectedCommit, "commit", "", "expected tagged commit") + } if err := flags.Parse(args[1:]); err != nil { return Invocation{}, err } if flags.NArg() != 1 { return Invocation{}, fmt.Errorf("%s requires one version argument", action) } - return Invocation{Command: action, Repository: *repository, ConfigPath: *configPath, OutputDir: outputDir, AssetsDir: assetsDir, Version: flags.Arg(0)}, nil + if action == CIDeliveryAction && expectedCommit == "" { + return Invocation{}, errors.New("ci-deliver requires --commit OID") + } + if action == CIDeliveryAction && !validGitObjectID(expectedCommit) { + return Invocation{}, errors.New("ci-deliver --commit must be a full Git object ID") + } + parsedRemote := "" + if remote != nil { + parsedRemote = *remote + } + return Invocation{Command: action, Repository: *repository, ConfigPath: *configPath, OutputDir: outputDir, AssetsDir: assetsDir, Remote: parsedRemote, Commit: expectedCommit, Version: flags.Arg(0)}, nil default: return Invocation{}, fmt.Errorf("unknown command %q", args[0]) } } +func validGitObjectID(value string) bool { + if len(value) != 40 && len(value) != 64 { + return false + } + for _, char := range value { + if (char < '0' || char > '9') && (char < 'a' || char > 'f') && (char < 'A' || char > 'F') { + return false + } + } + return true +} + // CheckCommand contains one direct child-process invocation from a profile. type CheckCommand struct { Args []string `json:"args"` diff --git a/cmd/releasekit/internal/cli/config_test.go b/cmd/releasekit/internal/cli/config_test.go index 78ef582..e126649 100644 --- a/cmd/releasekit/internal/cli/config_test.go +++ b/cmd/releasekit/internal/cli/config_test.go @@ -47,9 +47,29 @@ func TestParse(t *testing.T) { if deliver.Command != DeliveryAction || deliver.Repository != "candidate" || deliver.ConfigPath != "releasekit.json" || deliver.AssetsDir != "assets with spaces" || deliver.Version != "v1.2.6" { t.Fatalf("Parse(deliver) = %#v", deliver) } + ciDelivery, err := Parse([]string{"ci-deliver", "--repo", "candidate", "--config", "profiles/release config.json", "--remote", "upstream", "--commit", "0123456789abcdef0123456789abcdef01234567", "v1.2.7"}) + if err != nil { + t.Fatal(err) + } + if ciDelivery.Command != CIDeliveryAction || ciDelivery.Repository != "candidate" || ciDelivery.ConfigPath != "profiles/release config.json" || ciDelivery.Remote != "upstream" || ciDelivery.Commit != "0123456789abcdef0123456789abcdef01234567" || ciDelivery.Version != "v1.2.7" { + t.Fatalf("Parse(ci-deliver) = %#v", ciDelivery) + } + ciDefault, err := Parse([]string{"ci-deliver", "--commit", "0123456789abcdef0123456789abcdef01234567", "v1.2.8"}) + if err != nil || ciDefault.Remote != "origin" { + t.Fatalf("Parse(ci-deliver defaults) = %#v, error = %v", ciDefault, err) + } + if _, err := Parse([]string{"ci-deliver", "v1.2.3"}); err == nil || !strings.Contains(err.Error(), "--commit") { + t.Fatalf("Parse(ci-deliver without commit) error = %v, want required commit", err) + } + if _, err := Parse([]string{"ci-deliver", "--commit", "abc123", "v1.2.3"}); err == nil || !strings.Contains(err.Error(), "full Git object ID") { + t.Fatalf("Parse(ci-deliver with abbreviated commit) error = %v, want full object ID error", err) + } if _, err := Parse([]string{"check", "v1.2.3", "--repo", "repository"}); err == nil { t.Fatal("Parse accepted a flag after the version") } + if _, err := Parse([]string{"ci-deliver", "--commit", "0123456789abcdef0123456789abcdef01234567", "v1.2.3", "--repo", "repository"}); err == nil { + t.Fatal("Parse accepted a ci-deliver flag after the version") + } if _, err := Parse(nil); err == nil || !strings.Contains(err.Error(), "command is required") { t.Fatalf("Parse(nil) error = %v, want missing-command error", err) } diff --git a/cmd/releasekit/main.go b/cmd/releasekit/main.go index 404073c..eed6aaf 100644 --- a/cmd/releasekit/main.go +++ b/cmd/releasekit/main.go @@ -143,6 +143,8 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int { return 0 case cli.DeliveryAction: return runDelivery(ctx, invocation, stdout, stderr) + case cli.CIDeliveryAction: + return runCIDelivery(ctx, invocation, stdout, stderr) } return 2 } @@ -156,13 +158,54 @@ func runDelivery(ctx context.Context, invocation cli.Invocation, stdout, stderr return deliverWithSettings(ctx, invocation, settings, stdout, stderr) } +func runCIDelivery(ctx context.Context, invocation cli.Invocation, stdout, stderr io.Writer) int { + err := releasekit.PrepareTaggedCheckout(ctx, releasekit.TaggedCheckoutRequest{ + RepositoryDir: invocation.Repository, + Version: invocation.Version, + Remote: invocation.Remote, + ExpectedCommit: invocation.Commit, + }) + if err != nil { + fmt.Fprintf(stderr, "prepare tagged checkout: %v\n", err) + if errors.Is(err, releasekit.ErrInvalidVersion) { + return 2 + } + return 1 + } + + settings, err := cli.LoadSettings(invocation.Repository, invocation.ConfigPath) + if err != nil { + fmt.Fprintf(stderr, "load profile: %v\n", err) + return 2 + } + destination, status := deliveryDestination(settings, stderr) + if status != 0 { + return status + } + if settings.Build != nil && len(settings.Build.Targets) != 0 { + if err := releasekit.PrepareBuildDependencies(ctx, invocation.Repository); err != nil { + fmt.Fprintf(stderr, "prepare build dependencies: %v\n", err) + return 1 + } + } + return deliverWithDestination(ctx, invocation, settings, destination, stdout, stderr) +} + // deliverWithSettings adapts prepared CLI state to the public delivery facade. // CI entry points can reuse it after preparing the checkout and loading its profile. func deliverWithSettings(ctx context.Context, invocation cli.Invocation, settings cli.Settings, stdout, stderr io.Writer) int { + destination, status := deliveryDestination(settings, stderr) + if status != 0 { + return status + } + return deliverWithDestination(ctx, invocation, settings, destination, stdout, stderr) +} + +func deliveryDestination(settings cli.Settings, stderr io.Writer) (releasekit.GiteaDestination, int) { delivery := settings.Delivery if delivery == nil || strings.TrimSpace(delivery.BaseURL) == "" || strings.TrimSpace(delivery.Owner) == "" || strings.TrimSpace(delivery.Repository) == "" { fmt.Fprintln(stderr, "deliver requires base_url, owner, and repository in the profile delivery settings") - return 2 + return releasekit.GiteaDestination{}, 2 } tokenEnv := delivery.TokenEnv if strings.TrimSpace(tokenEnv) == "" { @@ -171,9 +214,12 @@ func deliverWithSettings(ctx context.Context, invocation cli.Invocation, setting token, ok := os.LookupEnv(tokenEnv) if !ok || strings.TrimSpace(token) == "" { fmt.Fprintf(stderr, "deliver requires a non-empty token in environment variable %s\n", tokenEnv) - return 2 + return releasekit.GiteaDestination{}, 2 } + return releasekit.GiteaDestination{BaseURL: delivery.BaseURL, Owner: delivery.Owner, Repository: delivery.Repository, Token: token}, 0 +} +func deliverWithDestination(ctx context.Context, invocation cli.Invocation, settings cli.Settings, destination releasekit.GiteaDestination, stdout, stderr io.Writer) int { hasTargets := settings.Build != nil && len(settings.Build.Targets) != 0 if invocation.AssetsDir != "" && !hasTargets { fmt.Fprintln(stderr, "--assets requires build targets in the profile") @@ -207,12 +253,10 @@ func deliverWithSettings(ctx context.Context, invocation cli.Invocation, setting RepositoryDir: invocation.Repository, Product: settings.Product, Version: invocation.Version, - Destination: releasekit.GiteaDestination{ - BaseURL: delivery.BaseURL, Owner: delivery.Owner, Repository: delivery.Repository, Token: token, - }, - Build: buildSpec, - Prebuilt: prebuilt, - Output: stderr, + Destination: destination, + Build: buildSpec, + Prebuilt: prebuilt, + Output: stderr, }) if err != nil { fmt.Fprintf(stderr, "delivery state=%s version=%s commit=%s url=%s: %v\n", result.State, result.Version, result.CandidateCommit, result.ReleaseURL, err) diff --git a/cmd/releasekit/main_test.go b/cmd/releasekit/main_test.go index 005841e..b78b3ef 100644 --- a/cmd/releasekit/main_test.go +++ b/cmd/releasekit/main_test.go @@ -325,6 +325,145 @@ func TestDeliverBuildsTaggedArtifactsAndReusesAssets(t *testing.T) { } } +func TestCIDeliverLoadsTaggedProfileAndSkipsUnavailablePageOnlyDependencies(t *testing.T) { + repository := newCLIRepository(t) + writeRepositoryFile(t, repository, "go.mod", "module example.com/cli-candidate\n\ngo 1.25.0\n\nrequire example.com/unavailable/module v1.2.3\n") + cliGitRun(t, repository, "add", "go.mod") + cliGitRun(t, repository, "commit", "-m", "declare unavailable dependency") + + state := &deliveryServerState{} + server := newDeliveryServer(t, state) + defer server.Close() + writeDeliveryProfileAt(t, repository, "profiles/ci-delivery.json", cli.Settings{ + Product: "Example Product", + Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, + }) + expectedCommit := pushTaggedCLIRepository(t, repository) + state.commit = expectedCommit + writeDeliveryProfileAt(t, repository, "profiles/ci-delivery.json", cli.Settings{Product: "Later Branch Profile"}) + + t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") + t.Setenv("GOPROXY", "off") + t.Setenv("GOSUMDB", "off") + var stdout, stderr bytes.Buffer + args := []string{"ci-deliver", "--repo", repository, "--config", "profiles/ci-delivery.json", "--commit", expectedCommit, "v1.2.3"} + if status := run(context.Background(), args, &stdout, &stderr); status != 0 { + t.Fatalf("run() status=%d stdout=%q stderr=%q", status, stdout.String(), stderr.String()) + } + if state.releaseCreates != 1 || state.apiRequests == 0 { + t.Fatalf("page delivery did not reach the server: creates=%d requests=%d", state.releaseCreates, state.apiRequests) + } + if got := cliGitOutput(t, repository, "rev-parse", "HEAD"); got != expectedCommit { + t.Fatalf("prepared HEAD = %s, want %s", got, expectedCommit) + } +} + +func TestCIDeliverRejectsWrongCommitBeforeGoOrHostedRequests(t *testing.T) { + repository := newCLIRepository(t) + writeRepositoryFile(t, repository, "go.mod", "module example.com/cli-candidate\n\ngo 1.25.0\n\nrequire example.com/unavailable/module v1.2.3\n") + cliGitRun(t, repository, "add", "go.mod") + cliGitRun(t, repository, "commit", "-m", "declare unavailable dependency") + state := &deliveryServerState{} + server := newDeliveryServer(t, state) + defer server.Close() + writeDeliveryProfile(t, repository, cli.Settings{ + Product: "Example Product", + Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}}, + Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, + }) + expectedCommit := pushTaggedCLIRepository(t, repository) + state.commit = expectedCommit + + shimDir := t.TempDir() + markerPath := filepath.Join(shimDir, "go-invoked") + goShim := "#!/bin/sh\n: > \"$RELEASEKIT_GO_MARKER\"\nexit 99\n" + if err := os.WriteFile(filepath.Join(shimDir, "go"), []byte(goShim), 0o700); err != nil { + t.Fatal(err) + } + t.Setenv("RELEASEKIT_GO_MARKER", markerPath) + t.Setenv("PATH", shimDir+string(os.PathListSeparator)+os.Getenv("PATH")) + t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") + var stdout, stderr bytes.Buffer + wrongCommit := strings.Repeat("0", 40) + args := []string{"ci-deliver", "--repo", repository, "--commit", wrongCommit, "v1.2.3"} + if status := run(context.Background(), args, &stdout, &stderr); status != 1 { + t.Fatalf("run() status=%d stdout=%q stderr=%q, want preparation failure", status, stdout.String(), stderr.String()) + } + if !strings.Contains(stderr.String(), "resolves to commit") { + t.Fatalf("stderr=%q, want expected-commit mismatch", stderr.String()) + } + if _, err := os.Stat(markerPath); !os.IsNotExist(err) { + t.Fatalf("Go was invoked before commit agreement: %v", err) + } + if state.apiRequests != 0 { + t.Fatalf("hosted API requests = %d, want 0", state.apiRequests) + } +} + +func TestCIDeliverPreparesDependenciesAndBuildsConfiguredTargets(t *testing.T) { + repository := newDeliveryBuildRepository(t) + state := &deliveryServerState{} + server := newDeliveryServer(t, state) + defer server.Close() + writeDeliveryProfile(t, repository, cli.Settings{ + Product: "Example Product", + Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)}}, + Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, + }) + expectedCommit := pushTaggedCLIRepository(t, repository) + state.commit = expectedCommit + t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") + var stdout, stderr bytes.Buffer + args := []string{"ci-deliver", "--repo", repository, "--commit", expectedCommit, "v1.2.3"} + if status := run(context.Background(), args, &stdout, &stderr); status != 0 { + t.Fatalf("run() status=%d stdout=%q stderr=%q", status, stdout.String(), stderr.String()) + } + if state.releaseCreates != 1 || state.assetUploads != 2 { + t.Fatalf("delivery counts = creates:%d uploads:%d", state.releaseCreates, state.assetUploads) + } + if _, ok := state.assets["helper-v1.2.3-"+runtime.GOOS+"-"+runtime.GOARCH]; !ok { + t.Fatalf("binary not uploaded: %#v", state.assets) + } +} + +func TestCIDeliverValidatesCredentialsBeforePreparingDependencies(t *testing.T) { + repository := newDeliveryBuildRepository(t) + state := &deliveryServerState{} + server := newDeliveryServer(t, state) + defer server.Close() + writeDeliveryProfile(t, repository, cli.Settings{ + Product: "Example Product", + Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}}, + Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, + }) + expectedCommit := pushTaggedCLIRepository(t, repository) + state.commit = expectedCommit + + shimDir := t.TempDir() + markerPath := filepath.Join(shimDir, "go-invoked") + goShim := "#!/bin/sh\n: > \"$RELEASEKIT_GO_MARKER\"\nexit 99\n" + if err := os.WriteFile(filepath.Join(shimDir, "go"), []byte(goShim), 0o700); err != nil { + t.Fatal(err) + } + t.Setenv("RELEASEKIT_GO_MARKER", markerPath) + t.Setenv("PATH", shimDir+string(os.PathListSeparator)+os.Getenv("PATH")) + t.Setenv("RELEASEKIT_GITEA_TOKEN", "") + var stdout, stderr bytes.Buffer + args := []string{"ci-deliver", "--repo", repository, "--commit", expectedCommit, "v1.2.3"} + if status := run(context.Background(), args, &stdout, &stderr); status != 2 { + t.Fatalf("run() status=%d stdout=%q stderr=%q, want credential error", status, stdout.String(), stderr.String()) + } + if !strings.Contains(stderr.String(), "non-empty token") { + t.Fatalf("stderr=%q, want missing-credential error", stderr.String()) + } + if _, err := os.Stat(markerPath); !os.IsNotExist(err) { + t.Fatalf("Go was invoked before credential validation: %v", err) + } + if state.apiRequests != 0 { + t.Fatalf("hosted API requests = %d, want 0", state.apiRequests) + } +} + func TestDeliverRejectsInvalidTaggedSourceBeforeManagedBuild(t *testing.T) { repository := newDeliveryBuildRepository(t) state := &deliveryServerState{} @@ -701,15 +840,23 @@ func writeDeliveryRelease(w http.ResponseWriter, serverURL string, state *delive } func writeDeliveryProfile(t *testing.T, repository string, settings cli.Settings) { + writeDeliveryProfileAt(t, repository, "releasekit.json", settings) +} + +func writeDeliveryProfileAt(t *testing.T, repository, name string, settings cli.Settings) { t.Helper() contents, err := json.Marshal(settings) if err != nil { t.Fatal(err) } - if err := os.WriteFile(filepath.Join(repository, "releasekit.json"), contents, 0o600); err != nil { + path := filepath.Join(repository, filepath.FromSlash(name)) + if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { t.Fatal(err) } - cliGitRun(t, repository, "add", "releasekit.json") + if err := os.WriteFile(path, contents, 0o600); err != nil { + t.Fatal(err) + } + cliGitRun(t, repository, "add", name) cliGitRun(t, repository, "commit", "-m", "configure delivery") } @@ -779,6 +926,28 @@ func cliGitRun(t *testing.T, repository string, args ...string) { } } +func writeRepositoryFile(t *testing.T, repository, name, contents string) { + t.Helper() + path := filepath.Join(repository, filepath.FromSlash(name)) + if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(contents), 0o600); err != nil { + t.Fatal(err) + } +} + +func pushTaggedCLIRepository(t *testing.T, repository string) string { + t.Helper() + cliGitRun(t, repository, "tag", "v1.2.3") + commit := cliGitOutput(t, repository, "rev-parse", "v1.2.3^{commit}") + remote := filepath.Join(t.TempDir(), "origin.git") + cliGitRun(t, repository, "init", "--bare", remote) + cliGitRun(t, repository, "remote", "add", "origin", remote) + cliGitRun(t, repository, "push", "origin", "refs/tags/v1.2.3") + return commit +} + func cliGitOutput(t *testing.T, repository string, args ...string) string { t.Helper() commandArgs := append([]string{"git"}, args...) diff --git a/docs/cli.md b/docs/cli.md index d29ec96..f652942 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -1,6 +1,6 @@ # Command line -ReleaseKit currently provides five commands. The exit statuses below belong +ReleaseKit currently provides six 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. @@ -125,3 +125,30 @@ artifacts and retry with `--assets DIR`. Matching content is kept and only known missing assets are added. See the [delivery consumer guide](consumers/delivery.md) and [Gitea integration guide](integrations/gitea.md). + +Prepare a tagged CI checkout and deliver it: + +```sh +releasekit ci-deliver [--repo DIR] [--config FILE] [--remote NAME] --commit OID VERSION +``` + +`ci-deliver` fetches `VERSION` from the selected Git remote (default `origin`), +requires the tag's peeled commit to match the required full `--commit` object +ID, and checks out that commit detached. It then loads the profile from the +prepared checkout, validates delivery settings and the configured token, and +downloads application dependencies only when the profile selects build targets. +Binary profiles are built and delivered through the same facade path as +`deliver`; source-only profiles skip dependency downloads and binary builds. +`--config` follows the existing profile path rules: relative paths are resolved +from `--repo`, and absolute paths are used as-is. This command explicitly +prepares and changes the selected checkout. Ordinary `deliver` continues to +operate on already prepared source without fetching, checking out, or +downloading dependencies. + +In Woodpecker, pass `CI_WORKSPACE`, `CI_COMMIT_SHA`, and `CI_COMMIT_TAG` as the +repository, expected commit, and version arguments. The CLI does not read +provider-specific environment variable names. Checkout and dependency +preparation failures return status 1; usage, profile, credential, and invalid +version errors return status 2. Final delivery uses the same statuses as +`deliver`: 0 for complete, 1 for known failure, conflict, or partial delivery, +and 3 for uncertain remote state. diff --git a/docs/config.md b/docs/config.md index a5d96ff..e68eb97 100644 --- a/docs/config.md +++ b/docs/config.md @@ -48,6 +48,17 @@ retained for retries or when build and delivery credentials need separate invocations. Ordinary `deliver` does not download application dependencies or change the checkout. +`ci-deliver` uses the same profile after preparing the selected tag in the +checkout. It checks out the tag only after its peeled commit matches the +provider-supplied commit ID, then loads the profile and validates delivery +settings and credentials. It runs `go mod download` only when the profile has +selected build targets; source-only delivery does not need application +dependencies. This preparation may access the network and may update +`go.sum`. The command takes CI values as arguments, so profile settings remain +independent of Woodpecker environment variable names. See the +[CI command contract](cli.md#prepare-a-tagged-ci-checkout-and-deliver-it) for +the invocation and exit statuses. + ReleaseKit's source-only profile is [releasekit.json](../releasekit.json). See the [source-only profile](../examples/releasekit.json) and diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 9905533..10d6c77 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # ReleaseKit audit implementation plan -Status: Stages 1–8 complete; Stage 9 next. +Status: Stages 1–9 complete; Stage 10 next. Baseline: `e4be7fa` (2026-10-06). The [audit](audit.md) records the evidence, product goals, and selected direction. This document owns implementation order and completion criteria for that work. @@ -460,6 +460,16 @@ a wrong CI commit fails before application builds or hosted writes, and a source-only module with unavailable dependencies still reaches page delivery. Reuse facade tests for the individual preparation and delivery operations. +**Completed:** `ci-deliver` now prepares the tagged checkout against the +provider-supplied commit, loads the profile from that checkout, validates its +delivery settings and credential, downloads dependencies only for selected +binary targets, and finishes through the existing delivery facade. Local +Git/HTTP fixtures cover source-only and binary delivery, relative custom profile +loading, commit mismatch before Go or hosted requests, and unavailable +source-only dependencies. CLI and configuration guidance describe preparation, +provider arguments, effects, and exit statuses. The full offline suite, API +boundary check, and documentation whitespace checks pass. + ## Stage 10 — Consolidate and exercise maintained CI templates **Covers:** installation, workflow duplication, and artifact-testing in F6.