Check expected publication push URL
This commit is contained in:
9
check.go
9
check.go
@@ -6,12 +6,17 @@ import (
|
|||||||
|
|
||||||
internalbuild "gitea.maximumdirect.net/eric/ReleaseKit/internal/build"
|
internalbuild "gitea.maximumdirect.net/eric/ReleaseKit/internal/build"
|
||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate"
|
||||||
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/publish"
|
||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/validation"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/validation"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrInvalidVersion reports a version that is not stable vMAJOR.MINOR.PATCH.
|
// ErrInvalidVersion reports a version that is not stable vMAJOR.MINOR.PATCH.
|
||||||
var ErrInvalidVersion = candidate.ErrInvalidVersion
|
var ErrInvalidVersion = candidate.ErrInvalidVersion
|
||||||
|
|
||||||
|
// ErrUnexpectedPushURL reports that the effective publication push URL does
|
||||||
|
// not exactly match ReleaseRequest.ExpectedPushURL.
|
||||||
|
var ErrUnexpectedPushURL = publish.ErrUnexpectedPushURL
|
||||||
|
|
||||||
// Candidate identifies one committed local source revision selected for
|
// Candidate identifies one committed local source revision selected for
|
||||||
// validation.
|
// validation.
|
||||||
type Candidate struct {
|
type Candidate struct {
|
||||||
@@ -28,6 +33,10 @@ type ReleaseRequest struct {
|
|||||||
Product string
|
Product string
|
||||||
Version string
|
Version string
|
||||||
Remote string
|
Remote string
|
||||||
|
// ExpectedPushURL optionally requires Publish's resolved push URL to match
|
||||||
|
// this string exactly before validation or tag creation. Check ignores this
|
||||||
|
// publication-only expectation. A mismatch returns ErrUnexpectedPushURL.
|
||||||
|
ExpectedPushURL string
|
||||||
// Validation selects optional checks run against the captured candidate.
|
// Validation selects optional checks run against the captured candidate.
|
||||||
Validation ValidationSpec
|
Validation ValidationSpec
|
||||||
// AdditionalChecks optionally runs product checks after shared validation.
|
// AdditionalChecks optionally runs product checks after shared validation.
|
||||||
|
|||||||
@@ -112,15 +112,16 @@ type CheckCommand struct {
|
|||||||
Env map[string]string `json:"env,omitempty"`
|
Env map[string]string `json:"env,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Settings contains the product, remote, shared validation selections, and
|
// Settings contains the product, remote, publication expectation, shared
|
||||||
// check commands read from a releasekit.json profile.
|
// validation selections, and check commands read from a releasekit.json profile.
|
||||||
type Settings struct {
|
type Settings struct {
|
||||||
Product string `json:"product"`
|
Product string `json:"product"`
|
||||||
Remote string `json:"remote,omitempty"`
|
Remote string `json:"remote,omitempty"`
|
||||||
Checks []CheckCommand `json:"checks,omitempty"`
|
ExpectedPushURL string `json:"expected_push_url,omitempty"`
|
||||||
Validation *ValidationSettings `json:"validation,omitempty"`
|
Checks []CheckCommand `json:"checks,omitempty"`
|
||||||
Build *BuildSettings `json:"build,omitempty"`
|
Validation *ValidationSettings `json:"validation,omitempty"`
|
||||||
Delivery *DeliverySettings `json:"delivery,omitempty"`
|
Build *BuildSettings `json:"build,omitempty"`
|
||||||
|
Delivery *DeliverySettings `json:"delivery,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidationSettings selects shared candidate checks in a release profile.
|
// ValidationSettings selects shared candidate checks in a release profile.
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ func TestLoadSettingsDefaultsRemoteAndConfigPath(t *testing.T) {
|
|||||||
func TestLoadSettingsReadsGoValidation(t *testing.T) {
|
func TestLoadSettingsReadsGoValidation(t *testing.T) {
|
||||||
repoDir := t.TempDir()
|
repoDir := t.TempDir()
|
||||||
profilePath := filepath.Join(repoDir, "releasekit.json")
|
profilePath := filepath.Join(repoDir, "releasekit.json")
|
||||||
if err := os.WriteFile(profilePath, []byte(`{"product":"Example","validation":{"go":{"race":true,"full_vet":true,"compile_targets":["linux/amd64","linux/arm64","darwin/arm64"]}}}`), 0o600); err != nil {
|
if err := os.WriteFile(profilePath, []byte(`{"product":"Example","expected_push_url":"ssh://git.example/owner/repo.git","validation":{"go":{"race":true,"full_vet":true,"compile_targets":["linux/amd64","linux/arm64","darwin/arm64"]}}}`), 0o600); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,6 +158,9 @@ func TestLoadSettingsReadsGoValidation(t *testing.T) {
|
|||||||
if got, want := strings.Join(settings.Validation.Go.CompileTargets, ","), "linux/amd64,linux/arm64,darwin/arm64"; got != want {
|
if got, want := strings.Join(settings.Validation.Go.CompileTargets, ","), "linux/amd64,linux/arm64,darwin/arm64"; got != want {
|
||||||
t.Fatalf("compile targets = %q, want %q", got, want)
|
t.Fatalf("compile targets = %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
|
if settings.ExpectedPushURL != "ssh://git.example/owner/repo.git" {
|
||||||
|
t.Fatalf("expected push URL = %q", settings.ExpectedPushURL)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExampleProfileLoads(t *testing.T) {
|
func TestExampleProfileLoads(t *testing.T) {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
|
|||||||
}
|
}
|
||||||
request := candidateRequest(invocation, settings, stderr)
|
request := candidateRequest(invocation, settings, stderr)
|
||||||
if invocation.Command == cli.PublishAction {
|
if invocation.Command == cli.PublishAction {
|
||||||
|
request.ExpectedPushURL = settings.ExpectedPushURL
|
||||||
fmt.Fprintf(stderr, "validating and publishing %s\n", invocation.Version)
|
fmt.Fprintf(stderr, "validating and publishing %s\n", invocation.Version)
|
||||||
result, err := releasekit.Publish(ctx, request)
|
result, err := releasekit.Publish(ctx, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -114,7 +114,8 @@ func TestCheckCommandBindsConfiguredChecksToCapturedCandidate(t *testing.T) {
|
|||||||
|
|
||||||
func TestCandidateRequestMapsGoValidation(t *testing.T) {
|
func TestCandidateRequestMapsGoValidation(t *testing.T) {
|
||||||
request := candidateRequest(cli.Invocation{Repository: "repository", Version: "v1.2.3"}, cli.Settings{
|
request := candidateRequest(cli.Invocation{Repository: "repository", Version: "v1.2.3"}, cli.Settings{
|
||||||
Product: "Example Product",
|
Product: "Example Product",
|
||||||
|
ExpectedPushURL: "publication-only value",
|
||||||
Validation: &cli.ValidationSettings{
|
Validation: &cli.ValidationSettings{
|
||||||
Go: cli.GoValidationSettings{Race: true, FullVet: true, CompileTargets: []string{"linux/amd64", "darwin/arm64"}},
|
Go: cli.GoValidationSettings{Race: true, FullVet: true, CompileTargets: []string{"linux/amd64", "darwin/arm64"}},
|
||||||
},
|
},
|
||||||
@@ -122,6 +123,9 @@ func TestCandidateRequestMapsGoValidation(t *testing.T) {
|
|||||||
if !request.Validation.Go.Race || !request.Validation.Go.FullVet {
|
if !request.Validation.Go.Race || !request.Validation.Go.FullVet {
|
||||||
t.Fatalf("candidate request validation = %#v, want Go race and full vet enabled", request.Validation)
|
t.Fatalf("candidate request validation = %#v, want Go race and full vet enabled", request.Validation)
|
||||||
}
|
}
|
||||||
|
if request.ExpectedPushURL != "" {
|
||||||
|
t.Fatalf("candidate request carried publication-only expected push URL %q", request.ExpectedPushURL)
|
||||||
|
}
|
||||||
wantTargets := []releasekit.Target{{OS: "linux", Arch: "amd64"}, {OS: "darwin", Arch: "arm64"}}
|
wantTargets := []releasekit.Target{{OS: "linux", Arch: "amd64"}, {OS: "darwin", Arch: "arm64"}}
|
||||||
if len(request.Validation.Go.CompileTargets) != len(wantTargets) {
|
if len(request.Validation.Go.CompileTargets) != len(wantTargets) {
|
||||||
t.Fatalf("candidate request compile targets = %#v, want %#v", request.Validation.Go.CompileTargets, wantTargets)
|
t.Fatalf("candidate request compile targets = %#v, want %#v", request.Validation.Go.CompileTargets, wantTargets)
|
||||||
@@ -217,9 +221,11 @@ printf '%s %s\n' "$version" "$candidate" >> "$RELEASEKIT_COMPATIBILITY_LOG"
|
|||||||
cliGitRun(t, repository, "add", "docs/releases/v1.2.3.md", "scripts/check-release-note.sh")
|
cliGitRun(t, repository, "add", "docs/releases/v1.2.3.md", "scripts/check-release-note.sh")
|
||||||
cliGitRun(t, repository, "commit", "-m", "add compatibility note and check")
|
cliGitRun(t, repository, "commit", "-m", "add compatibility note and check")
|
||||||
checkLog := filepath.Join(t.TempDir(), "compatibility-check.log")
|
checkLog := filepath.Join(t.TempDir(), "compatibility-check.log")
|
||||||
|
remote := filepath.Join(t.TempDir(), "origin.git")
|
||||||
settings := cli.Settings{
|
settings := cli.Settings{
|
||||||
Product: "Example Product",
|
Product: "Example Product",
|
||||||
Remote: "origin",
|
Remote: "origin",
|
||||||
|
ExpectedPushURL: remote,
|
||||||
Checks: []cli.CheckCommand{{
|
Checks: []cli.CheckCommand{{
|
||||||
Args: []string{"sh", "scripts/check-release-note.sh"},
|
Args: []string{"sh", "scripts/check-release-note.sh"},
|
||||||
Env: map[string]string{"RELEASEKIT_COMPATIBILITY_LOG": checkLog},
|
Env: map[string]string{"RELEASEKIT_COMPATIBILITY_LOG": checkLog},
|
||||||
@@ -237,7 +243,6 @@ printf '%s %s\n' "$version" "$candidate" >> "$RELEASEKIT_COMPATIBILITY_LOG"
|
|||||||
}
|
}
|
||||||
cliGitRun(t, repository, "add", "releasekit.json")
|
cliGitRun(t, repository, "add", "releasekit.json")
|
||||||
cliGitRun(t, repository, "commit", "-m", "configure source release")
|
cliGitRun(t, repository, "commit", "-m", "configure source release")
|
||||||
remote := filepath.Join(t.TempDir(), "origin.git")
|
|
||||||
cliGitRun(t, repository, "init", "--bare", remote)
|
cliGitRun(t, repository, "init", "--bare", remote)
|
||||||
cliGitRun(t, repository, "remote", "add", "origin", remote)
|
cliGitRun(t, repository, "remote", "add", "origin", remote)
|
||||||
cliGitRun(t, repository, "push", "--set-upstream", "origin", "main")
|
cliGitRun(t, repository, "push", "--set-upstream", "origin", "main")
|
||||||
|
|||||||
@@ -71,6 +71,12 @@ after tests and full vet and before configured checks. Each failure is reported
|
|||||||
as `package-compile` with its selected target; this does not create build
|
as `package-compile` with its selected target; this does not create build
|
||||||
artifacts or execute target binaries.
|
artifacts or execute target binaries.
|
||||||
|
|
||||||
|
The optional profile field `expected_push_url` applies only to `publish`. A
|
||||||
|
nonempty value must exactly match the effective push URL resolved by Git;
|
||||||
|
mismatches fail during preflight before Go validation or tag creation. Git URL
|
||||||
|
rewrites are resolved first, and the push URL is used when fetch and push URLs
|
||||||
|
differ. `check` ignores this field.
|
||||||
|
|
||||||
The command returns status 0 for confirmed publication, 1 for a known
|
The command returns status 0 for confirmed publication, 1 for a known
|
||||||
publication failure or conflict, 2 for usage, profile, or invalid-version
|
publication failure or conflict, 2 for usage, profile, or invalid-version
|
||||||
errors, and 3 when the push outcome remains unknown. Unknown status must be
|
errors, and 3 when the push outcome remains unknown. Unknown status must be
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ The current profile fields are:
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `product` | Yes | Product name used by release operations. |
|
| `product` | Yes | Product name used by release operations. |
|
||||||
| `remote` | No | Selected Git remote; defaults to `origin`. |
|
| `remote` | No | Selected Git remote; defaults to `origin`. |
|
||||||
|
| `expected_push_url` | No | Exact effective push URL required by `publish`; ignored by local `check`. |
|
||||||
| `checks` | No | Ordered additional child commands, each with an argument vector and optional environment map. |
|
| `checks` | No | Ordered additional child commands, each with an argument vector and optional environment map. |
|
||||||
| `validation` | No | Optional shared checks selected by ReleaseKit. |
|
| `validation` | No | Optional shared checks selected by ReleaseKit. |
|
||||||
| `build` | No | Optional main package, binary name, and selected `GOOS/GOARCH` target strings. |
|
| `build` | No | Optional main package, binary name, and selected `GOOS/GOARCH` target strings. |
|
||||||
@@ -58,6 +59,13 @@ environment variable containing the Gitea token; it defaults to
|
|||||||
`RELEASEKIT_GITEA_TOKEN`. The CLI reads this variable only for `deliver` and
|
`RELEASEKIT_GITEA_TOKEN`. The CLI reads this variable only for `deliver` and
|
||||||
passes its value explicitly to the library operation.
|
passes its value explicitly to the library operation.
|
||||||
|
|
||||||
|
`expected_push_url`, when nonempty, is compared exactly with Git's resolved
|
||||||
|
effective push URL during `publish`, before candidate validation. It applies
|
||||||
|
to the push destination when fetch and push URLs differ, and Git URL rewrites
|
||||||
|
such as `insteadOf` are resolved before comparison. A mismatch fails preflight;
|
||||||
|
omitting the field keeps the selected remote's existing behavior. `check` is
|
||||||
|
local-only and ignores this publication expectation.
|
||||||
|
|
||||||
The `check` and `publish` commands run one
|
The `check` and `publish` commands run one
|
||||||
`go test -mod=readonly -count=1 ./...` suite with Go module and toolchain network
|
`go test -mod=readonly -count=1 ./...` suite with Go module and toolchain network
|
||||||
access disabled, adding `-race` when `validation.go.race` is true. When
|
access disabled, adding `-race` when `validation.go.race` is true. When
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ annotated source tag:
|
|||||||
|
|
||||||
```go
|
```go
|
||||||
result, err := releasekit.Publish(ctx, releasekit.ReleaseRequest{
|
result, err := releasekit.Publish(ctx, releasekit.ReleaseRequest{
|
||||||
RepositoryDir: "/work/project",
|
RepositoryDir: "/work/project",
|
||||||
Product: "Example",
|
Product: "Example",
|
||||||
Version: "v1.2.3",
|
Version: "v1.2.3",
|
||||||
|
ExpectedPushURL: "ssh://git.example/owner/example.git",
|
||||||
Validation: releasekit.ValidationSpec{
|
Validation: releasekit.ValidationSpec{
|
||||||
Go: releasekit.GoValidationSpec{Race: true, FullVet: true},
|
Go: releasekit.GoValidationSpec{Race: true, FullVet: true},
|
||||||
},
|
},
|
||||||
@@ -23,6 +24,12 @@ fmt.Println(result.Version, result.CandidateCommit, result.TagObject)
|
|||||||
|
|
||||||
Publication captures a clean candidate on `main`, validates its committed
|
Publication captures a clean candidate on `main`, validates its committed
|
||||||
release note, and checks the selected remote before running candidate checks.
|
release note, and checks the selected remote before running candidate checks.
|
||||||
|
Set `ExpectedPushURL` to require the effective push destination to match an
|
||||||
|
exact string. Git URL rewrites are resolved first, and the push URL is compared
|
||||||
|
when fetch and push URLs differ. A mismatch fails preflight before Go validation,
|
||||||
|
callbacks, or tag creation; detect it with
|
||||||
|
`errors.Is(err, releasekit.ErrUnexpectedPushURL)`. `Check` ignores this
|
||||||
|
publication-only field.
|
||||||
It runs one `go test -mod=readonly -count=1 ./...` suite with the available
|
It runs one `go test -mod=readonly -count=1 ./...` suite with the available
|
||||||
compatible local Go toolchain. Set `Validation.Go.Race` to run that suite with
|
compatible local Go toolchain. Set `Validation.Go.Race` to run that suite with
|
||||||
`-race`. Set `Validation.Go.FullVet` to run `go vet -mod=readonly ./...` after
|
`-race`. Set `Validation.Go.FullVet` to run `go vet -mod=readonly ./...` after
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ not a separate release matrix:
|
|||||||
| Runtime version lookup and injected application version | `buildinfo`, `internal/runtimeinfo`, `internal/versioncontract`, `internal/build` | `TestVersionInConsumerExecutable`, `TestVersion` |
|
| Runtime version lookup and injected application version | `buildinfo`, `internal/runtimeinfo`, `internal/versioncontract`, `internal/build` | `TestVersionInConsumerExecutable`, `TestVersion` |
|
||||||
| Candidate capture, shared validation, public diagnostics, additional checks, and rechecks | `releasekit.Check`, `releasekit.Publish`, `internal/candidate`, `internal/validation`, `cmd/releasekit/internal/cli` | `TestRunUsesOneSuiteWithSelectedRaceSetting`, `TestRunReportsRacePrerequisiteFailureWithoutFallback`, `TestRunExecutesRaceEnabledSuiteOnHost`, `TestRunCompilesTargetsAfterTestAndVetInSelectionOrder`, `TestRunAttributesTargetCompileFailureAndStops`, `TestRunCompilesSourceOnlyAndExecutablePackagesWithoutArtifacts`, `TestCandidateRequestMapsGoValidation`, `TestLoadSettingsReadsGoValidation`, `TestCheckUsesSelectedRaceSuiteBeforeAdditionalChecks`, `TestCheckCapturesCandidateAndCallsAdditionalChecksOnce`, `TestCheckReturnsPublicValidationError`, `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestPublishFailedStandardSuiteSkipsAdditionalChecksAndTagCreation`, `TestPublishAdditionalCheckFailurePreservesCauseAndPreventsTagCreation`, `TestCheckCommandBindsConfiguredChecksToCapturedCandidate`, `TestPublishCommandPublishesConfiguredCandidate`, `TestCandidateCommandsPresentValidationDiagnosticsOnce`, `TestDeliverPageOnlyCreatesAndReusesRelease` |
|
| Candidate capture, shared validation, public diagnostics, additional checks, and rechecks | `releasekit.Check`, `releasekit.Publish`, `internal/candidate`, `internal/validation`, `cmd/releasekit/internal/cli` | `TestRunUsesOneSuiteWithSelectedRaceSetting`, `TestRunReportsRacePrerequisiteFailureWithoutFallback`, `TestRunExecutesRaceEnabledSuiteOnHost`, `TestRunCompilesTargetsAfterTestAndVetInSelectionOrder`, `TestRunAttributesTargetCompileFailureAndStops`, `TestRunCompilesSourceOnlyAndExecutablePackagesWithoutArtifacts`, `TestCandidateRequestMapsGoValidation`, `TestLoadSettingsReadsGoValidation`, `TestCheckUsesSelectedRaceSuiteBeforeAdditionalChecks`, `TestCheckCapturesCandidateAndCallsAdditionalChecksOnce`, `TestCheckReturnsPublicValidationError`, `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestPublishFailedStandardSuiteSkipsAdditionalChecksAndTagCreation`, `TestPublishAdditionalCheckFailurePreservesCauseAndPreventsTagCreation`, `TestCheckCommandBindsConfiguredChecksToCapturedCandidate`, `TestPublishCommandPublishesConfiguredCandidate`, `TestCandidateCommandsPresentValidationDiagnosticsOnce`, `TestDeliverPageOnlyCreatesAndReusesRelease` |
|
||||||
| Shared Go settings for validation and builds | `internal/gotool`, `internal/process`, `internal/candidate`, `internal/build` | `TestDefaultValidationDoesNotInheritGOFLAGSRunFilter`, `TestNativeAndStaticCommandsUseOwnedBaseline`, `TestPersistentSettingsPreserveCachesAndNativeCTools`, `TestBuildIgnoresAmbientAndPersistentArchitectureTuning` |
|
| Shared Go settings for validation and builds | `internal/gotool`, `internal/process`, `internal/candidate`, `internal/build` | `TestDefaultValidationDoesNotInheritGOFLAGSRunFilter`, `TestNativeAndStaticCommandsUseOwnedBaseline`, `TestPersistentSettingsPreserveCachesAndNativeCTools`, `TestBuildIgnoresAmbientAndPersistentArchitectureTuning` |
|
||||||
| Authoritative endpoint and exact annotated tag publication | `releasekit.Publish`, `internal/publish`, `internal/git` | `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestResolveUsesPushURLAndQueriesWithoutMutation` |
|
| Authoritative endpoint and exact annotated tag publication | `releasekit.Publish`, `internal/publish`, `internal/git` | `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestPublishRejectsUnexpectedPushURLBeforeValidation`, `TestPublishComparesExpectedURLWithResolvedInsteadOfDestination`, `TestPublishExpectedPushURLUsesPushURLWhenFetchAndPushDiffer`, `TestCheckIgnoresExpectedPushURL`, `TestResolveUsesPushURLAndQueriesWithoutMutation` |
|
||||||
| Publication partial and uncertain recovery | `internal/publish`, `internal/git` | `TestPublishRejectedPushKeepsLocalTagAndReportsNotPublished`, `TestPublishLostAcknowledgmentConfirmsExactRemoteObject`, `TestPublishLostAcknowledgmentWithChangedEndpointRemainsUnknown`, `TestPublishUnavailableReconciliationRemainsUnknownAndPreservesPushError` |
|
| 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` |
|
| 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`, `TestDeliverBinaryConsumerWithBuildinfoAndSelectedTargets`, `TestDeliverPrebuiltArtifactsRemainCallerOwned`, `TestDeliverManagedBuildFailureCleansTemporaryOutputBeforeHostedWrites`, `TestDeliverManagedBuildCancellationCleansTemporaryOutput`, `TestDeliverBuildsTaggedArtifactsAndReusesAssets`, `TestDeliverRejectsTamperedAssetsAndStaleReceiptBeforeRemoteWrites` |
|
| Page-only and managed or prebuilt binary delivery | `releasekit.Deliver`, `internal/delivery`, `internal/gitea`, CLI | `TestDeliverPageOnlySkipsBuildAndHostedAssets`, `TestDeliverManagedBuildUploadsArtifactsAndCleansTemporaryOutput`, `TestDeliverBinaryConsumerWithBuildinfoAndSelectedTargets`, `TestDeliverPrebuiltArtifactsRemainCallerOwned`, `TestDeliverManagedBuildFailureCleansTemporaryOutputBeforeHostedWrites`, `TestDeliverManagedBuildCancellationCleansTemporaryOutput`, `TestDeliverBuildsTaggedArtifactsAndReusesAssets`, `TestDeliverRejectsTamperedAssetsAndStaleReceiptBeforeRemoteWrites` |
|
||||||
|
|||||||
@@ -295,6 +295,11 @@ Reuse the existing Go environment tests for controls already protected there.
|
|||||||
|
|
||||||
## Stage 4: Compare the optional expected push URL
|
## Stage 4: Compare the optional expected push URL
|
||||||
|
|
||||||
|
**Completion:** Complete. `ReleaseRequest.ExpectedPushURL` and profile
|
||||||
|
`expected_push_url` require an exact match with Git's resolved effective push
|
||||||
|
URL before validation. Mismatches return `ErrUnexpectedPushURL` with expected
|
||||||
|
and resolved values; `Check` ignores the publication-only expectation.
|
||||||
|
|
||||||
**Outcome:** Publication rejects a different effective destination before validation.
|
**Outcome:** Publication rejects a different effective destination before validation.
|
||||||
|
|
||||||
1. Add `ExpectedPushURL` and `expected_push_url`, with an empty value selecting
|
1. Add `ExpectedPushURL` and `expected_push_url`, with an empty value selecting
|
||||||
|
|||||||
@@ -36,11 +36,16 @@ const (
|
|||||||
|
|
||||||
const recoveryTimeout = 30 * time.Second
|
const recoveryTimeout = 30 * time.Second
|
||||||
|
|
||||||
|
// ErrUnexpectedPushURL reports that the resolved push URL differs from the
|
||||||
|
// caller's expected publication destination.
|
||||||
|
var ErrUnexpectedPushURL = errors.New("unexpected publication push URL")
|
||||||
|
|
||||||
type Request struct {
|
type Request struct {
|
||||||
RepositoryDir string
|
RepositoryDir string
|
||||||
Product string
|
Product string
|
||||||
Version string
|
Version string
|
||||||
Remote string
|
Remote string
|
||||||
|
ExpectedPushURL string
|
||||||
Validation validation.Settings
|
Validation validation.Settings
|
||||||
AdditionalChecks func(context.Context, candidate.Candidate) error
|
AdditionalChecks func(context.Context, candidate.Candidate) error
|
||||||
AfterValidate func(context.Context, candidate.Candidate) error
|
AfterValidate func(context.Context, candidate.Candidate) error
|
||||||
@@ -56,8 +61,9 @@ type Result struct {
|
|||||||
State State
|
State State
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish validates one captured candidate and pushes only its annotated tag
|
// Publish captures a candidate, resolves and checks the optional expected push
|
||||||
// object. It runs one standard Go suite, with the selected settings, then
|
// URL, then validates the candidate and pushes only its annotated tag object.
|
||||||
|
// It runs one standard Go suite, with the selected settings, then
|
||||||
// AdditionalChecks when supplied.
|
// AdditionalChecks when supplied.
|
||||||
// A failed push receives one bounded authoritative tag query. Exact
|
// A failed push receives one bounded authoritative tag query. Exact
|
||||||
// object equality confirms publication; absence and conflict are classified,
|
// object equality confirms publication; absence and conflict are classified,
|
||||||
@@ -87,6 +93,9 @@ func Publish(ctx context.Context, request Request) (Result, error) {
|
|||||||
return result, fmt.Errorf("resolve publication endpoint: %w", err)
|
return result, fmt.Errorf("resolve publication endpoint: %w", err)
|
||||||
}
|
}
|
||||||
result.Endpoint = endpoint.PushURL
|
result.Endpoint = endpoint.PushURL
|
||||||
|
if request.ExpectedPushURL != "" && request.ExpectedPushURL != endpoint.PushURL {
|
||||||
|
return result, fmt.Errorf("%w: expected %q, resolved %q", ErrUnexpectedPushURL, request.ExpectedPushURL, endpoint.PushURL)
|
||||||
|
}
|
||||||
if err := endpoint.Recheck(ctx, captured.RepositoryDir); err != nil {
|
if err := endpoint.Recheck(ctx, captured.RepositoryDir); err != nil {
|
||||||
return result, fmt.Errorf("pre-validation endpoint guard: %w", err)
|
return result, fmt.Errorf("pre-validation endpoint guard: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
28
publish.go
28
publish.go
@@ -48,21 +48,24 @@ type PublicationResult struct {
|
|||||||
State PublicationState // publication outcome for this invocation
|
State PublicationState // publication outcome for this invocation
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish always runs one offline Go test suite for the captured candidate.
|
// After publication preflight succeeds, Publish runs one offline Go test suite
|
||||||
// Validation.Go.Race enables the race detector for that suite, and
|
// for the captured candidate.
|
||||||
|
// Validation.Go.Race enables the race detector for that suite,
|
||||||
// Validation.Go.FullVet runs go vet afterward instead of test-time vet, and
|
// Validation.Go.FullVet runs go vet afterward instead of test-time vet, and
|
||||||
// Validation.Go.CompileTargets compiles all packages for each selected target
|
// Validation.Go.CompileTargets compiles all packages for each selected target
|
||||||
// with cgo disabled after tests and vet. Compilation produces no artifacts and
|
// with cgo disabled after tests and vet. Compilation produces no artifacts and
|
||||||
// does not execute target binaries. It then calls AdditionalChecks when
|
// does not execute target binaries. ExpectedPushURL, when nonempty, is compared
|
||||||
// supplied. It optionally builds selected
|
// exactly with the resolved effective push URL before validation; a mismatch
|
||||||
// targets in temporary storage outside the worktree and pushes the exact
|
// returns ErrUnexpectedPushURL. It then calls AdditionalChecks when supplied.
|
||||||
// annotated tag object to the selected remote. It returns a result with errors: preflight
|
// It optionally builds selected targets in temporary storage outside the
|
||||||
// failures are not_attempted, failed authoritative guards are conflict, and
|
// worktree and pushes the exact annotated tag object to the selected remote.
|
||||||
// errors after local tag creation but before push are not_published. A failed
|
// It returns a result with errors: preflight failures are not_attempted, failed
|
||||||
// push gets one bounded authoritative tag query: an exact object match confirms
|
// authoritative guards are conflict, and errors after local tag creation but
|
||||||
// publication, absence is not_published, a different object is conflict, and
|
// before push are not_published. A failed push gets one bounded authoritative
|
||||||
// an unreadable result is unknown. A nil error means publication is confirmed
|
// tag query: an exact object match confirms publication, absence is
|
||||||
// by the successful push or exact remote object match.
|
// not_published, a different object is conflict, and an unreadable result is
|
||||||
|
// unknown. A nil error means publication is confirmed by the successful push
|
||||||
|
// or exact remote object match.
|
||||||
func Publish(ctx context.Context, request ReleaseRequest) (PublicationResult, error) {
|
func Publish(ctx context.Context, request ReleaseRequest) (PublicationResult, error) {
|
||||||
candidateInput := candidateRequest(request)
|
candidateInput := candidateRequest(request)
|
||||||
result, err := publish.Publish(ctx, publish.Request{
|
result, err := publish.Publish(ctx, publish.Request{
|
||||||
@@ -70,6 +73,7 @@ func Publish(ctx context.Context, request ReleaseRequest) (PublicationResult, er
|
|||||||
Product: candidateInput.Product,
|
Product: candidateInput.Product,
|
||||||
Version: candidateInput.Version,
|
Version: candidateInput.Version,
|
||||||
Remote: request.Remote,
|
Remote: request.Remote,
|
||||||
|
ExpectedPushURL: request.ExpectedPushURL,
|
||||||
Validation: candidateInput.Validation,
|
Validation: candidateInput.Validation,
|
||||||
AdditionalChecks: candidateInput.AdditionalChecks,
|
AdditionalChecks: candidateInput.AdditionalChecks,
|
||||||
AfterValidate: candidateInput.AfterValidate,
|
AfterValidate: candidateInput.AfterValidate,
|
||||||
|
|||||||
@@ -94,6 +94,103 @@ func TestPublishPushesOnlyTheCapturedAnnotatedTag(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPublishRejectsUnexpectedPushURLBeforeValidation(t *testing.T) {
|
||||||
|
repo := newPublishFixture(t)
|
||||||
|
suiteRuns := filepath.Join(t.TempDir(), "suite-runs")
|
||||||
|
t.Setenv("RELEASEKIT_FIXTURE_SUITE_RUNS", suiteRuns)
|
||||||
|
expected := repo.bare + string(os.PathSeparator)
|
||||||
|
var callbacks int
|
||||||
|
result, err := releasekit.Publish(context.Background(), releasekit.ReleaseRequest{
|
||||||
|
RepositoryDir: repo.work,
|
||||||
|
Product: "Sample App",
|
||||||
|
Version: "v1.2.3",
|
||||||
|
ExpectedPushURL: expected,
|
||||||
|
AdditionalChecks: func(context.Context, releasekit.Candidate) error {
|
||||||
|
callbacks++
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if !errors.Is(err, releasekit.ErrUnexpectedPushURL) {
|
||||||
|
t.Fatalf("Publish() error = %v, want ErrUnexpectedPushURL", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), fmt.Sprintf("expected %q", expected)) || !strings.Contains(err.Error(), fmt.Sprintf("resolved %q", repo.bare)) {
|
||||||
|
t.Fatalf("Publish() error = %v, want expected and resolved URLs", err)
|
||||||
|
}
|
||||||
|
if result.State != releasekit.PublicationNotAttempted || result.Phase != releasekit.PublicationPreflight || result.Endpoint != repo.bare || result.TagObject != "" {
|
||||||
|
t.Fatalf("publication result = %#v, want preflight failure at resolved endpoint", result)
|
||||||
|
}
|
||||||
|
if callbacks != 0 {
|
||||||
|
t.Fatalf("additional checks ran %d times, want none", callbacks)
|
||||||
|
}
|
||||||
|
if _, statErr := os.Stat(suiteRuns); !errors.Is(statErr, os.ErrNotExist) {
|
||||||
|
t.Fatalf("standard-suite marker stat error = %v, want no suite run", statErr)
|
||||||
|
}
|
||||||
|
if got := gitText(t, repo.work, "tag", "--list", "v1.2.3"); got != "" {
|
||||||
|
t.Fatalf("local tag exists after URL mismatch: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPublishComparesExpectedURLWithResolvedInsteadOfDestination(t *testing.T) {
|
||||||
|
repo := newPublishFixture(t)
|
||||||
|
alias := "releasekit-expected-push:"
|
||||||
|
gitRun(t, repo.work, "config", "url."+repo.bare+".insteadOf", alias)
|
||||||
|
gitRun(t, repo.work, "remote", "set-url", "origin", alias)
|
||||||
|
|
||||||
|
result, err := releasekit.Publish(context.Background(), releasekit.ReleaseRequest{
|
||||||
|
RepositoryDir: repo.work,
|
||||||
|
Product: "Sample App",
|
||||||
|
Version: "v1.2.3",
|
||||||
|
ExpectedPushURL: repo.bare,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Publish() with the resolved insteadOf destination failed: %v", err)
|
||||||
|
}
|
||||||
|
if result.Endpoint != repo.bare || result.State != releasekit.PublicationPublished {
|
||||||
|
t.Fatalf("publication result = %#v, want resolved push URL and successful publication", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPublishExpectedPushURLUsesPushURLWhenFetchAndPushDiffer(t *testing.T) {
|
||||||
|
repo := newPublishFixture(t)
|
||||||
|
fetchURL := filepath.Join(t.TempDir(), "fetch-only.git")
|
||||||
|
gitRun(t, repo.work, "remote", "set-url", "origin", fetchURL)
|
||||||
|
gitRun(t, repo.work, "config", "remote.origin.pushurl", repo.bare)
|
||||||
|
|
||||||
|
result, err := releasekit.Publish(context.Background(), releasekit.ReleaseRequest{
|
||||||
|
RepositoryDir: repo.work,
|
||||||
|
Product: "Sample App",
|
||||||
|
Version: "v1.2.3",
|
||||||
|
ExpectedPushURL: fetchURL,
|
||||||
|
})
|
||||||
|
if !errors.Is(err, releasekit.ErrUnexpectedPushURL) {
|
||||||
|
t.Fatalf("Publish() error = %v, want ErrUnexpectedPushURL", err)
|
||||||
|
}
|
||||||
|
if result.Endpoint != repo.bare || !strings.Contains(err.Error(), fmt.Sprintf("resolved %q", repo.bare)) {
|
||||||
|
t.Fatalf("result = %#v, error = %v; want the effective push URL %q", result, err, repo.bare)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckIgnoresExpectedPushURL(t *testing.T) {
|
||||||
|
repo := newPublishFixture(t)
|
||||||
|
var callbacks int
|
||||||
|
_, err := releasekit.Check(context.Background(), releasekit.ReleaseRequest{
|
||||||
|
RepositoryDir: repo.work,
|
||||||
|
Product: "Sample App",
|
||||||
|
Version: "v1.2.3",
|
||||||
|
ExpectedPushURL: "unrelated destination",
|
||||||
|
AdditionalChecks: func(context.Context, releasekit.Candidate) error {
|
||||||
|
callbacks++
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Check() considered publication URL expectation: %v", err)
|
||||||
|
}
|
||||||
|
if callbacks != 1 {
|
||||||
|
t.Fatalf("additional checks ran %d times, want one", callbacks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCheckUsesSelectedRaceSuiteBeforeAdditionalChecks(t *testing.T) {
|
func TestCheckUsesSelectedRaceSuiteBeforeAdditionalChecks(t *testing.T) {
|
||||||
requireRacePrerequisites(t)
|
requireRacePrerequisites(t)
|
||||||
repo := newPublishFixture(t)
|
repo := newPublishFixture(t)
|
||||||
|
|||||||
Reference in New Issue
Block a user