Add reusable CI preparation operations
This commit is contained in:
@@ -7,6 +7,37 @@ The operation accepts historical lightweight and annotated tags, including a
|
||||
detached checkout, when the checked-out `HEAD` identifies the selected tag
|
||||
commit. It does not require the old remote `main` to match.
|
||||
|
||||
## CI preparation
|
||||
|
||||
Use `PrepareTaggedCheckout` when a disposable CI checkout must be verified
|
||||
against the commit that triggered the job. It fetches only the selected tag
|
||||
from the chosen remote (default `origin`), compares the peeled tag commit with
|
||||
the expected commit, and checks out that commit detached. Fetch or checkout
|
||||
conflicts are returned so the caller can replace or repair the workspace.
|
||||
Cancellation is returned to the caller; the operation does not publish a tag
|
||||
or hosted content.
|
||||
|
||||
```go
|
||||
err := releasekit.PrepareTaggedCheckout(ctx, releasekit.TaggedCheckoutRequest{
|
||||
RepositoryDir: repositoryDir,
|
||||
Version: version,
|
||||
ExpectedCommit: ciCommit,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
For binary profiles, call `PrepareBuildDependencies(ctx, repositoryDir)` after
|
||||
checkout preparation and before delivery. It runs `go mod download` with
|
||||
workspace discovery disabled and the available compatible local Go toolchain.
|
||||
The operation may use the network and updates the Go module cache while
|
||||
preserving the caller's proxy, private-module, credential, and cache settings.
|
||||
If checksums are missing, Go may update `go.sum`; see the
|
||||
[Go Modules Reference](https://go.dev/ref/mod#go-mod-download). Cancellation
|
||||
and command errors are returned. Page-only profiles do not need dependency
|
||||
preparation.
|
||||
|
||||
## Command line
|
||||
|
||||
Add a Gitea destination to the profile:
|
||||
|
||||
@@ -6,7 +6,8 @@ two Go facades and a command-line interface:
|
||||
- The root `releasekit` package is the orchestration facade. `Check` validates
|
||||
one local candidate, `Publish` validates and publishes its exact annotated
|
||||
source tag, `Build` compiles selected application targets, and `Deliver`
|
||||
delivers a tagged note page and any declared assets.
|
||||
delivers a tagged note page and any declared assets. `PrepareTaggedCheckout`
|
||||
and `PrepareBuildDependencies` provide explicit local CI preparation.
|
||||
- `buildinfo` is the small runtime facade. `Version` reports the injected
|
||||
application version, the main-module version when available, or `dev`.
|
||||
- `cmd/releasekit` is the executable interface. Its `version`, `check`,
|
||||
@@ -39,11 +40,12 @@ these implemented packages:
|
||||
| `internal/build` | Candidate-build coordination, selected Go target compilation, version injection, native smoke check, and artifact digests. |
|
||||
| `internal/candidate` | Stable-version, note, repository-state, candidate capture, and post-validation checks. |
|
||||
| `internal/delivery` | Tagged-source and release-note checks, managed builds in disposable storage outside the checkout, prebuilt artifact validation, manifest assembly, and hosted delivery coordination. |
|
||||
| `internal/git` | Git command boundary, push-destination resolution, exact ref inspection, and tag operations, including captured-endpoint verification during recovery. |
|
||||
| `internal/git` | Git command boundary, push-destination resolution, exact ref inspection, tagged checkout preparation, and tag operations, including captured-endpoint verification during recovery. |
|
||||
| `internal/gitea` | Gitea API transport, hosted release pages, asset inspection, manifests, and uploads. |
|
||||
| `internal/gotool` | Effective Go configuration discovery and shared native-validation and static-build environments; see [Go command environments](gotool.md). |
|
||||
| `internal/gotool` | Effective Go configuration discovery, shared native-validation and static-build environments, and network-capable dependency preparation; see [Go command environments](gotool.md). |
|
||||
| `internal/tempdir` | Disposable system temporary storage outside source worktrees. |
|
||||
| `internal/process` | Cancellable child-process execution with per-command environment overlays and removals. |
|
||||
| `internal/preparation` | Coordination for expected-commit tag checkout and Go module dependency preparation. |
|
||||
| `internal/publish` | Publication guards, one validation run, annotated tag creation, push, and recovery state. |
|
||||
| `internal/runtimeinfo` | Private version lookup used by the runtime facade. |
|
||||
| `internal/versioncontract` | Shared stable-version rules and linker-symbol identity. |
|
||||
@@ -70,6 +72,7 @@ not a separate release matrix:
|
||||
| Publication partial and uncertain recovery | `internal/publish`, `internal/git` | `TestPublishRejectedPushKeepsLocalTagAndReportsNotPublished`, `TestPublishLostAcknowledgmentConfirmsExactRemoteObject`, `TestPublishLostAcknowledgmentWithChangedEndpointRemainsUnknown`, `TestPublishUnavailableReconciliationRemainsUnknownAndPreservesPushError` |
|
||||
| Cross-builds, version smoke checks, empty target sets, and partial outputs | `releasekit.Build`, `internal/build`, `buildinfo` | `TestBuildCreatesSelectedTargetsAndInjectsVersion`, `TestBuildWithNoTargetsDoesNotInvokeCompiler`, `TestBuildReturnsPartialArtifactsAfterTargetFailure` |
|
||||
| Page-only and managed or prebuilt binary delivery | `releasekit.Deliver`, `internal/delivery`, `internal/gitea`, CLI | `TestDeliverPageOnlySkipsBuildAndHostedAssets`, `TestDeliverManagedBuildUploadsArtifactsAndCleansTemporaryOutput`, `TestDeliverPrebuiltArtifactsRemainCallerOwned`, `TestDeliverManagedBuildFailureCleansTemporaryOutputBeforeHostedWrites`, `TestDeliverManagedBuildCancellationCleansTemporaryOutput`, `TestDeliverBuildsTaggedArtifactsAndReusesAssets`, `TestDeliverRejectsTamperedAssetsAndStaleReceiptBeforeRemoteWrites` |
|
||||
| Tagged checkout and build dependency preparation | `releasekit.PrepareTaggedCheckout`, `releasekit.PrepareBuildDependencies`, `internal/preparation`, `internal/git`, `internal/gotool` | `TestPrepareTaggedCheckoutFetchesExpectedCommitAndDetaches`, `TestPrepareTaggedCheckoutRejectsWrongCommitBeforeCheckout`, `TestPrepareTaggedCheckoutReportsLocalCheckoutConflict`, `TestPrepareTaggedCheckoutReportsConflictingLocalTag`, `TestPrepareBuildDependenciesPreservesPreparationEnvironment`, `TestPrepareBuildDependenciesPropagatesCommandFailure`, `TestPrepareBuildDependenciesPropagatesCancellation` |
|
||||
| Page, manifest, asset comparison, and additive retry outcomes | `internal/gitea` | `TestEnsureReleaseCreatesMissingPageFromCommittedNote`, `TestInspectAssetsDetectsSameNameAndSizeWithDifferentBytes`, `TestUploadMissingPartialFailureThenRerunOnlyMissingAssets` |
|
||||
| CLI profiles and prepared Woodpecker sequences | `cmd/releasekit`, `cmd/releasekit/internal/cli`, `examples/woodpecker/` | `TestLoadSettingsRejectsInvalidProfiles`, `TestWoodpeckerDeliveryCommandSequences`, `TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout` |
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# ReleaseKit audit implementation plan
|
||||
|
||||
Status: Stages 1–7 complete; Stage 8 next.
|
||||
Status: Stages 1–8 complete; Stage 9 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.
|
||||
@@ -412,6 +412,18 @@ uses the repository and preparation environment and propagates failure and
|
||||
cancellation. Keep default tests offline with a local module cache or a small
|
||||
Go-command fixture; no live module server is needed.
|
||||
|
||||
**Completed:** The root facade now exposes `PrepareTaggedCheckout` and
|
||||
`PrepareBuildDependencies`. Checkout preparation fetches an explicit tag
|
||||
refspec, compares the peeled commit with the expected full object ID, and
|
||||
detaches only after a match; tests cover default and selected remotes, wrong
|
||||
commits, conflicting local edits and tags, and input rejection before fetch.
|
||||
Dependency preparation runs `go mod download` with workspace discovery off and
|
||||
the local Go toolchain, preserving caller proxy, private-module, and cache
|
||||
settings; subprocess fixtures verify repository selection, failure details,
|
||||
and cancellation without network access. GoDoc, the delivery consumer guide,
|
||||
and the internal inventory describe effects and ownership. The full offline
|
||||
suite, API boundary check, and documentation whitespace checks pass.
|
||||
|
||||
## Stage 9 — Add the shared CI delivery command
|
||||
|
||||
**Covers:** the shared command portion of F6.
|
||||
|
||||
37
internal/git/prepare.go
Normal file
37
internal/git/prepare.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PrepareTaggedCheckout fetches one explicit tag ref, checks its peeled commit
|
||||
// against expectedCommit, and checks out that commit detached.
|
||||
func PrepareTaggedCheckout(ctx context.Context, repositoryDir, version, remote, expectedCommit string) error {
|
||||
if !validVersionRef(version) || !validObjectID(expectedCommit) {
|
||||
return fmt.Errorf("invalid tagged checkout identity %q at %q", expectedCommit, version)
|
||||
}
|
||||
ref := "refs/tags/" + version
|
||||
if _, err := run(ctx, repositoryDir, nil, "fetch", "--no-tags", remote, ref+":"+ref); err != nil {
|
||||
return fmt.Errorf("fetch tag %s from remote %s: %w", version, remote, err)
|
||||
}
|
||||
source, err := InspectTaggedSource(ctx, repositoryDir, version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !strings.EqualFold(source.TagCommit, expectedCommit) {
|
||||
return fmt.Errorf("tag %s resolves to commit %s, expected %s", version, source.TagCommit, expectedCommit)
|
||||
}
|
||||
if _, err := run(ctx, repositoryDir, nil, "checkout", "--detach", source.TagCommit); err != nil {
|
||||
return fmt.Errorf("check out tag %s at %s: %w", version, source.TagCommit, err)
|
||||
}
|
||||
head, err := run(ctx, repositoryDir, nil, "rev-parse", "--verify", "HEAD^{commit}")
|
||||
if err != nil {
|
||||
return fmt.Errorf("verify prepared checkout HEAD: %w", err)
|
||||
}
|
||||
if !strings.EqualFold(strings.TrimSpace(head), expectedCommit) {
|
||||
return fmt.Errorf("prepared checkout HEAD is %s, expected %s", strings.TrimSpace(head), expectedCommit)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -89,6 +89,30 @@ func New(ctx context.Context, directory string) (*Tool, error) {
|
||||
return tool, nil
|
||||
}
|
||||
|
||||
// PrepareBuildDependencies downloads module dependencies with the available
|
||||
// local Go toolchain. It preserves caller module-network settings and caches,
|
||||
// while disabling workspace discovery and automatic toolchain downloads.
|
||||
func PrepareBuildDependencies(ctx context.Context, directory string) error {
|
||||
captured := &combinedWriter{}
|
||||
err := process.Run(ctx, process.Invocation{
|
||||
Args: []string{"go", "mod", "download"},
|
||||
Dir: directory,
|
||||
Env: map[string]string{
|
||||
"GOWORK": "off",
|
||||
"GOTOOLCHAIN": "local",
|
||||
},
|
||||
Stdout: captured,
|
||||
Stderr: captured,
|
||||
})
|
||||
if err != nil {
|
||||
if output := strings.TrimSpace(string(captured.Bytes())); output != "" {
|
||||
return fmt.Errorf("go mod download: %w: %s", err, output)
|
||||
}
|
||||
return fmt.Errorf("go mod download: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HostTarget reports the Go command's host operating system and architecture.
|
||||
func (tool *Tool) HostTarget() Target {
|
||||
return tool.host
|
||||
|
||||
@@ -3,13 +3,167 @@ package gotool
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type goPreparationRecord struct {
|
||||
Arguments []string
|
||||
Directory string
|
||||
Workspace string
|
||||
Toolchain string
|
||||
Proxy string
|
||||
Private string
|
||||
NoSumDB string
|
||||
ModuleCache string
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if os.Getenv("RELEASEKIT_GO_FIXTURE") == "1" {
|
||||
os.Exit(runGoPreparationFixture())
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func runGoPreparationFixture() int {
|
||||
record := goPreparationRecord{
|
||||
Arguments: os.Args[1:], Directory: mustGetwd(),
|
||||
Workspace: os.Getenv("GOWORK"), Toolchain: os.Getenv("GOTOOLCHAIN"),
|
||||
Proxy: os.Getenv("GOPROXY"), Private: os.Getenv("GOPRIVATE"),
|
||||
NoSumDB: os.Getenv("GONOSUMDB"), ModuleCache: os.Getenv("GOMODCACHE"),
|
||||
}
|
||||
contents, err := json.Marshal(record)
|
||||
if err != nil {
|
||||
return 10
|
||||
}
|
||||
if err := os.WriteFile(os.Getenv("RELEASEKIT_GO_FIXTURE_RECORD"), contents, 0o600); err != nil {
|
||||
return 11
|
||||
}
|
||||
switch os.Getenv("RELEASEKIT_GO_FIXTURE_MODE") {
|
||||
case "fail":
|
||||
fmt.Fprintln(os.Stderr, "fixture dependency failure")
|
||||
return 23
|
||||
case "wait":
|
||||
for {
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func mustGetwd() string {
|
||||
directory, err := os.Getwd()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return directory
|
||||
}
|
||||
|
||||
func installGoPreparationFixture(t *testing.T) (repository, recordPath string) {
|
||||
t.Helper()
|
||||
directory := t.TempDir()
|
||||
repository = t.TempDir()
|
||||
executable, err := os.Executable()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
contents, err := os.ReadFile(executable)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
name := "go"
|
||||
if runtime.GOOS == "windows" {
|
||||
name += ".exe"
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(directory, name), contents, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
recordPath = filepath.Join(t.TempDir(), "go-command.json")
|
||||
t.Setenv("PATH", directory+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
t.Setenv("RELEASEKIT_GO_FIXTURE", "1")
|
||||
t.Setenv("RELEASEKIT_GO_FIXTURE_RECORD", recordPath)
|
||||
return repository, recordPath
|
||||
}
|
||||
|
||||
func TestPrepareBuildDependenciesPreservesPreparationEnvironment(t *testing.T) {
|
||||
repository, recordPath := installGoPreparationFixture(t)
|
||||
moduleCache := filepath.Join(t.TempDir(), "module-cache")
|
||||
t.Setenv("GOWORK", filepath.Join(t.TempDir(), "go.work"))
|
||||
t.Setenv("GOTOOLCHAIN", "auto")
|
||||
t.Setenv("GOPROXY", "https://proxy.example.invalid")
|
||||
t.Setenv("GOPRIVATE", "example.invalid/private")
|
||||
t.Setenv("GONOSUMDB", "example.invalid/private")
|
||||
t.Setenv("GOMODCACHE", moduleCache)
|
||||
|
||||
if err := PrepareBuildDependencies(context.Background(), repository); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
contents, err := os.ReadFile(recordPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var record goPreparationRecord
|
||||
if err := json.Unmarshal(contents, &record); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Join(record.Arguments, " ") != "mod download" || record.Directory != repository {
|
||||
t.Fatalf("Go invocation = %#v", record)
|
||||
}
|
||||
if record.Workspace != "off" || record.Toolchain != "local" {
|
||||
t.Fatalf("preparation Go settings = %#v", record)
|
||||
}
|
||||
if record.Proxy != "https://proxy.example.invalid" || record.Private != "example.invalid/private" || record.NoSumDB != "example.invalid/private" || record.ModuleCache != moduleCache {
|
||||
t.Fatalf("caller dependency settings were not preserved: %#v", record)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareBuildDependenciesPropagatesCommandFailure(t *testing.T) {
|
||||
repository, _ := installGoPreparationFixture(t)
|
||||
t.Setenv("RELEASEKIT_GO_FIXTURE_MODE", "fail")
|
||||
err := PrepareBuildDependencies(context.Background(), repository)
|
||||
var exitError *exec.ExitError
|
||||
if !errors.As(err, &exitError) || exitError.ExitCode() != 23 || !strings.Contains(err.Error(), "fixture dependency failure") {
|
||||
t.Fatalf("PrepareBuildDependencies() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareBuildDependenciesPropagatesCancellation(t *testing.T) {
|
||||
repository, recordPath := installGoPreparationFixture(t)
|
||||
t.Setenv("RELEASEKIT_GO_FIXTURE_MODE", "wait")
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- PrepareBuildDependencies(ctx, repository) }()
|
||||
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for {
|
||||
if _, err := os.Stat(recordPath); err == nil {
|
||||
break
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatal("Go fixture did not start")
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
cancel()
|
||||
select {
|
||||
case err := <-done:
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("PrepareBuildDependencies() error = %v; want cancellation", err)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("Go fixture did not stop after cancellation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNativeAndStaticCommandsUseOwnedBaseline(t *testing.T) {
|
||||
clearGoSettings(t)
|
||||
t.Setenv("GOENV", "off")
|
||||
|
||||
43
internal/preparation/preparation.go
Normal file
43
internal/preparation/preparation.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// Package preparation coordinates explicit local checkout and Go module
|
||||
// preparation operations for consumers and CI adapters.
|
||||
package preparation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/versioncontract"
|
||||
)
|
||||
|
||||
type TaggedCheckoutRequest struct {
|
||||
RepositoryDir string
|
||||
Version string
|
||||
Remote string
|
||||
ExpectedCommit string
|
||||
}
|
||||
|
||||
func PrepareTaggedCheckout(ctx context.Context, request TaggedCheckoutRequest) error {
|
||||
if !versioncontract.ValidStableVersion(request.Version) {
|
||||
return fmt.Errorf("%w: %q", candidate.ErrInvalidVersion, request.Version)
|
||||
}
|
||||
if strings.TrimSpace(request.ExpectedCommit) == "" {
|
||||
return errors.New("expected commit is required")
|
||||
}
|
||||
remote := request.Remote
|
||||
if remote == "" {
|
||||
remote = "origin"
|
||||
}
|
||||
return git.PrepareTaggedCheckout(ctx, request.RepositoryDir, request.Version, remote, request.ExpectedCommit)
|
||||
}
|
||||
|
||||
func PrepareBuildDependencies(ctx context.Context, repositoryDir string) error {
|
||||
if strings.TrimSpace(repositoryDir) == "" {
|
||||
return errors.New("repository directory is required")
|
||||
}
|
||||
return gotool.PrepareBuildDependencies(ctx, repositoryDir)
|
||||
}
|
||||
40
preparation.go
Normal file
40
preparation.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package releasekit
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
internalpreparation "gitea.maximumdirect.net/eric/ReleaseKit/internal/preparation"
|
||||
)
|
||||
|
||||
// TaggedCheckoutRequest selects a stable tag and the full Git object ID
|
||||
// expected by an external build or CI event. An empty Remote selects origin.
|
||||
type TaggedCheckoutRequest struct {
|
||||
RepositoryDir string
|
||||
Version string
|
||||
Remote string
|
||||
ExpectedCommit string
|
||||
}
|
||||
|
||||
// PrepareTaggedCheckout fetches Version from Remote, verifies that its peeled
|
||||
// commit matches ExpectedCommit, and checks out that commit detached. This
|
||||
// operation mutates the selected local repository; use it for disposable
|
||||
// checkouts where tag-fetch and checkout conflicts can be resolved by the
|
||||
// caller. It does not publish a tag or hosted content.
|
||||
func PrepareTaggedCheckout(ctx context.Context, request TaggedCheckoutRequest) error {
|
||||
return internalpreparation.PrepareTaggedCheckout(ctx, internalpreparation.TaggedCheckoutRequest{
|
||||
RepositoryDir: request.RepositoryDir,
|
||||
Version: request.Version,
|
||||
Remote: request.Remote,
|
||||
ExpectedCommit: request.ExpectedCommit,
|
||||
})
|
||||
}
|
||||
|
||||
// PrepareBuildDependencies runs `go mod download` in repositoryDir with
|
||||
// workspace discovery disabled and the locally available compatible Go
|
||||
// toolchain selected. It may access module networks and updates the Go module
|
||||
// cache using the caller's Go proxy, private-module, credential, and cache
|
||||
// settings. If required checksums are missing, the Go command may update
|
||||
// go.sum. Cancellation and Go command failures are returned to the caller.
|
||||
func PrepareBuildDependencies(ctx context.Context, repositoryDir string) error {
|
||||
return internalpreparation.PrepareBuildDependencies(ctx, repositoryDir)
|
||||
}
|
||||
163
preparation_test.go
Normal file
163
preparation_test.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package releasekit_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
releasekit "gitea.maximumdirect.net/eric/ReleaseKit"
|
||||
)
|
||||
|
||||
func TestPrepareTaggedCheckoutFetchesExpectedCommitAndDetaches(t *testing.T) {
|
||||
repository := newTaggedDeliveryConsumer(t)
|
||||
tagCommit := publishPreparationTag(t, repository)
|
||||
advancePreparationCheckout(t, repository, "later local commit")
|
||||
|
||||
if err := releasekit.PrepareTaggedCheckout(context.Background(), releasekit.TaggedCheckoutRequest{
|
||||
RepositoryDir: repository,
|
||||
Version: "v1.2.3",
|
||||
ExpectedCommit: tagCommit,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if head := buildGit(t, repository, "rev-parse", "HEAD"); head != tagCommit {
|
||||
t.Fatalf("prepared HEAD = %s, want %s", head, tagCommit)
|
||||
}
|
||||
if branch := buildGit(t, repository, "rev-parse", "--abbrev-ref", "HEAD"); branch != "HEAD" {
|
||||
t.Fatalf("prepared checkout branch = %q, want detached HEAD", branch)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareTaggedCheckoutRejectsWrongCommitBeforeCheckout(t *testing.T) {
|
||||
repository := newTaggedDeliveryConsumer(t)
|
||||
tagCommit := publishPreparationTag(t, repository)
|
||||
currentCommit := advancePreparationCheckout(t, repository, "later local commit")
|
||||
remote := buildGit(t, repository, "remote", "get-url", "origin")
|
||||
buildGit(t, repository, "remote", "add", "candidate", remote)
|
||||
|
||||
err := releasekit.PrepareTaggedCheckout(context.Background(), releasekit.TaggedCheckoutRequest{
|
||||
RepositoryDir: repository,
|
||||
Version: "v1.2.3",
|
||||
Remote: "candidate",
|
||||
ExpectedCommit: currentCommit,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("PrepareTaggedCheckout() succeeded for a tag that resolves to another commit")
|
||||
}
|
||||
if head := buildGit(t, repository, "rev-parse", "HEAD"); head != currentCommit {
|
||||
t.Fatalf("wrong-tag rejection changed HEAD to %s, want %s (tag commit %s)", head, currentCommit, tagCommit)
|
||||
}
|
||||
if branch := buildGit(t, repository, "rev-parse", "--abbrev-ref", "HEAD"); branch == "HEAD" {
|
||||
t.Fatal("wrong-tag rejection detached the checkout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareTaggedCheckoutReportsLocalCheckoutConflict(t *testing.T) {
|
||||
repository := newTaggedDeliveryConsumer(t)
|
||||
tagCommit := publishPreparationTag(t, repository)
|
||||
advancePreparationCheckout(t, repository, "later committed content")
|
||||
path := filepath.Join(repository, "cmd/app/main.go")
|
||||
if err := os.WriteFile(path, []byte("uncommitted conflicting content"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
currentCommit := buildGit(t, repository, "rev-parse", "HEAD")
|
||||
|
||||
err := releasekit.PrepareTaggedCheckout(context.Background(), releasekit.TaggedCheckoutRequest{
|
||||
RepositoryDir: repository,
|
||||
Version: "v1.2.3",
|
||||
ExpectedCommit: tagCommit,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("PrepareTaggedCheckout() succeeded despite a conflicting local edit")
|
||||
}
|
||||
if head := buildGit(t, repository, "rev-parse", "HEAD"); head != currentCommit {
|
||||
t.Fatalf("conflicting checkout changed HEAD to %s, want %s", head, currentCommit)
|
||||
}
|
||||
contents, err := os.ReadFile(path)
|
||||
if err != nil || string(contents) != "uncommitted conflicting content" {
|
||||
t.Fatalf("conflicting local file = %q, %v", contents, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareTaggedCheckoutReportsConflictingLocalTag(t *testing.T) {
|
||||
repository := newTaggedDeliveryConsumer(t)
|
||||
publishPreparationTag(t, repository)
|
||||
currentCommit := advancePreparationCheckout(t, repository, "later local commit")
|
||||
buildGit(t, repository, "tag", "v1.2.3")
|
||||
|
||||
err := releasekit.PrepareTaggedCheckout(context.Background(), releasekit.TaggedCheckoutRequest{
|
||||
RepositoryDir: repository,
|
||||
Version: "v1.2.3",
|
||||
ExpectedCommit: buildGit(t, repository, "rev-parse", "HEAD~1"),
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("PrepareTaggedCheckout() replaced a conflicting local tag")
|
||||
}
|
||||
if head := buildGit(t, repository, "rev-parse", "HEAD"); head != currentCommit {
|
||||
t.Fatalf("conflicting tag fetch changed HEAD to %s, want %s", head, currentCommit)
|
||||
}
|
||||
if tagCommit := buildGit(t, repository, "rev-parse", "refs/tags/v1.2.3^{commit}"); tagCommit != currentCommit {
|
||||
t.Fatalf("conflicting local tag changed to %s, want %s", tagCommit, currentCommit)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareTaggedCheckoutValidatesInputsBeforeFetching(t *testing.T) {
|
||||
repository := newTaggedDeliveryConsumer(t)
|
||||
publishPreparationTag(t, repository)
|
||||
for _, request := range []releasekit.TaggedCheckoutRequest{
|
||||
{RepositoryDir: repository, Version: "not-a-version", ExpectedCommit: buildGit(t, repository, "rev-parse", "HEAD")},
|
||||
{RepositoryDir: repository, Version: "v1.2.3"},
|
||||
{RepositoryDir: repository, Version: "v1.2.3", ExpectedCommit: "not-an-object-id"},
|
||||
} {
|
||||
err := releasekit.PrepareTaggedCheckout(context.Background(), request)
|
||||
if err == nil {
|
||||
t.Fatalf("PrepareTaggedCheckout(%#v) succeeded", request)
|
||||
}
|
||||
if _, err := buildGitOutput(repository, "show-ref", "--verify", "refs/tags/v1.2.3"); err == nil {
|
||||
t.Fatal("input validation fetched the tag before rejecting the request")
|
||||
}
|
||||
}
|
||||
if err := releasekit.PrepareTaggedCheckout(context.Background(), releasekit.TaggedCheckoutRequest{
|
||||
RepositoryDir: repository, Version: "invalid", ExpectedCommit: "bad",
|
||||
}); !errors.Is(err, releasekit.ErrInvalidVersion) {
|
||||
t.Fatalf("invalid-version error = %v, want ErrInvalidVersion", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareBuildDependenciesRequiresRepositoryDirectory(t *testing.T) {
|
||||
if err := releasekit.PrepareBuildDependencies(context.Background(), " "); err == nil {
|
||||
t.Fatal("PrepareBuildDependencies() accepted an empty repository directory")
|
||||
}
|
||||
}
|
||||
|
||||
func publishPreparationTag(t *testing.T, repository string) string {
|
||||
t.Helper()
|
||||
remoteDir := t.TempDir()
|
||||
remote := filepath.Join(remoteDir, "origin.git")
|
||||
buildGit(t, remoteDir, "init", "--bare", remote)
|
||||
buildGit(t, repository, "remote", "add", "origin", remote)
|
||||
commit := buildGit(t, repository, "rev-parse", "refs/tags/v1.2.3^{commit}")
|
||||
buildGit(t, repository, "push", "origin", "refs/tags/v1.2.3")
|
||||
buildGit(t, repository, "tag", "-d", "v1.2.3")
|
||||
return commit
|
||||
}
|
||||
|
||||
func advancePreparationCheckout(t *testing.T, repository, source string) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(repository, "cmd/app/main.go")
|
||||
if err := os.WriteFile(path, []byte("package main\n// "+source+"\nfunc main() {}\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
buildGit(t, repository, "add", "cmd/app/main.go")
|
||||
buildGit(t, repository, "commit", "-m", source)
|
||||
return buildGit(t, repository, "rev-parse", "HEAD")
|
||||
}
|
||||
|
||||
func buildGitOutput(repository string, args ...string) ([]byte, error) {
|
||||
command := exec.Command("git", args...)
|
||||
command.Dir = repository
|
||||
return command.Output()
|
||||
}
|
||||
Reference in New Issue
Block a user