Add shared CI delivery command

This commit is contained in:
2026-10-06 19:26:54 +00:00
parent d970aac709
commit c6a5db8aeb
8 changed files with 338 additions and 26 deletions

View File

@@ -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`.

View File

@@ -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"`

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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...)

View File

@@ -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.

View File

@@ -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

View File

@@ -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.