Recover publication against the captured endpoint
This commit is contained in:
@@ -61,7 +61,7 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
|
|||||||
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 {
|
||||||
fmt.Fprintf(stderr, "publication state=%s phase=%s version=%s commit=%s tag-object=%s: %v\n", result.State, result.Phase, result.Version, result.CandidateCommit, result.TagObject, err)
|
fmt.Fprintf(stderr, "publication state=%s phase=%s version=%s commit=%s tag-object=%s endpoint=%q: %v\n", result.State, result.Phase, result.Version, result.CandidateCommit, result.TagObject, result.Endpoint, err)
|
||||||
if errors.Is(err, releasekit.ErrInvalidVersion) {
|
if errors.Is(err, releasekit.ErrInvalidVersion) {
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -57,6 +59,10 @@ func TestReleaseWrapperPreservesNativeExitStatuses(t *testing.T) {
|
|||||||
cliGitRun(t, repository, "init", "--bare", remote)
|
cliGitRun(t, repository, "init", "--bare", remote)
|
||||||
cliGitRun(t, repository, "remote", "set-url", "origin", remote)
|
cliGitRun(t, repository, "remote", "set-url", "origin", remote)
|
||||||
cliGitRun(t, repository, "push", "origin", "HEAD:refs/heads/main")
|
cliGitRun(t, repository, "push", "origin", "HEAD:refs/heads/main")
|
||||||
|
fetch := filepath.Join(t.TempDir(), "fetch.git")
|
||||||
|
cliGitRun(t, repository, "init", "--bare", fetch)
|
||||||
|
cliGitRun(t, repository, "remote", "set-url", "origin", fetch)
|
||||||
|
cliGitRun(t, repository, "remote", "set-url", "--push", "origin", remote)
|
||||||
if mode.name == "rejected" {
|
if mode.name == "rejected" {
|
||||||
if err := os.WriteFile(filepath.Join(remote, "hooks/pre-receive"), []byte("#!/bin/sh\nexit 1\n"), 0755); err != nil {
|
if err := os.WriteFile(filepath.Join(remote, "hooks/pre-receive"), []byte("#!/bin/sh\nexit 1\n"), 0755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -67,7 +73,12 @@ func TestReleaseWrapperPreservesNativeExitStatuses(t *testing.T) {
|
|||||||
marker := filepath.Join(bin, "push-started")
|
marker := filepath.Join(bin, "push-started")
|
||||||
shim := `#!/bin/sh
|
shim := `#!/bin/sh
|
||||||
for arg do
|
for arg do
|
||||||
if [ "$arg" = push ]; then : > "$RELEASEKIT_TEST_PUSH_MARKER"; exit 1; fi
|
if [ "$arg" = push ]; then
|
||||||
|
"$RELEASEKIT_TEST_REAL_GIT" "$@" || exit 1
|
||||||
|
"$RELEASEKIT_TEST_REAL_GIT" remote set-url --push origin "$RELEASEKIT_TEST_MOVED_ENDPOINT"
|
||||||
|
: > "$RELEASEKIT_TEST_PUSH_MARKER"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
if [ -f "$RELEASEKIT_TEST_PUSH_MARKER" ]; then
|
if [ -f "$RELEASEKIT_TEST_PUSH_MARKER" ]; then
|
||||||
for arg do if [ "$arg" = ls-remote ]; then exit 128; fi; done
|
for arg do if [ "$arg" = ls-remote ]; then exit 128; fi; done
|
||||||
@@ -80,6 +91,7 @@ exec "$RELEASEKIT_TEST_REAL_GIT" "$@"
|
|||||||
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
|
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||||
t.Setenv("RELEASEKIT_TEST_REAL_GIT", realGit)
|
t.Setenv("RELEASEKIT_TEST_REAL_GIT", realGit)
|
||||||
t.Setenv("RELEASEKIT_TEST_PUSH_MARKER", marker)
|
t.Setenv("RELEASEKIT_TEST_PUSH_MARKER", marker)
|
||||||
|
t.Setenv("RELEASEKIT_TEST_MOVED_ENDPOINT", fetch)
|
||||||
}
|
}
|
||||||
command := exec.Command("./scripts/release", mode.version)
|
command := exec.Command("./scripts/release", mode.version)
|
||||||
command.Dir = repository
|
command.Dir = repository
|
||||||
@@ -95,6 +107,26 @@ exec "$RELEASEKIT_TEST_REAL_GIT" "$@"
|
|||||||
if status != mode.status {
|
if status != mode.status {
|
||||||
t.Fatalf("status=%d want=%d output=%s", status, mode.status, output)
|
t.Fatalf("status=%d want=%d output=%s", status, mode.status, output)
|
||||||
}
|
}
|
||||||
|
if mode.name == "unknown" {
|
||||||
|
if !strings.Contains(string(output), fmt.Sprintf("endpoint=%q", remote)) {
|
||||||
|
t.Fatalf("captured endpoint missing: %s", output)
|
||||||
|
}
|
||||||
|
query := exec.Command(realGit, "ls-remote", "--refs", "--", remote, "refs/tags/v1.2.3")
|
||||||
|
query.Dir = repository
|
||||||
|
found, err := query.Output()
|
||||||
|
if err != nil || len(found) == 0 {
|
||||||
|
t.Fatalf("captured endpoint lost published tag: %q %v", found, err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(found), cliGitOutput(t, repository, "rev-parse", "refs/tags/v1.2.3")) {
|
||||||
|
t.Fatalf("wrong recovered tag: %s", found)
|
||||||
|
}
|
||||||
|
fetchQuery := exec.Command(realGit, "ls-remote", "--refs", "origin", "refs/tags/v1.2.3")
|
||||||
|
fetchQuery.Dir = repository
|
||||||
|
stale, err := fetchQuery.Output()
|
||||||
|
if err != nil || len(stale) != 0 {
|
||||||
|
t.Fatalf("fetch fixture unexpectedly found tag: %q %v", stale, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ go run ./cmd/releasekit publish [--repo DIR] [--config FILE] VERSION
|
|||||||
It validates the candidate once, checks the authoritative remote, creates an
|
It validates the candidate once, checks the authoritative remote, creates an
|
||||||
annotated local tag, and pushes only its exact tag object. It prints the
|
annotated local tag, and pushes only its exact tag object. It prints the
|
||||||
confirmed version, candidate commit, and tag object on success. Validation
|
confirmed version, candidate commit, and tag object on success. Validation
|
||||||
progress and failed publication state are written to standard error. A signal
|
progress and failed publication state, including the captured publication
|
||||||
|
endpoint for recovery, are written to standard error. A signal
|
||||||
cancels the active operation; if it arrives during a push, the library's
|
cancels the active operation; if it arrives during a push, the library's
|
||||||
bounded reconciliation determines whether the exact tag was accepted.
|
bounded reconciliation determines whether the exact tag was accepted.
|
||||||
|
|
||||||
|
|||||||
@@ -77,13 +77,24 @@ A failed push triggers one exact authoritative tag query. An exact tag-object
|
|||||||
match confirms publication even when Git reported an error. A proven absent tag
|
match confirms publication even when Git reported an error. A proven absent tag
|
||||||
is `not_published`; a different object is a conflict; an unreadable query is
|
is `not_published`; a different object is a conflict; an unreadable query is
|
||||||
`unknown`. The command retains its local tag and reports the state, phase,
|
`unknown`. The command retains its local tag and reports the state, phase,
|
||||||
candidate commit, and tag object when available. Do not retry an unknown result
|
candidate commit, tag object, and captured publication endpoint when available.
|
||||||
until you resolve the authoritative ref:
|
Do not retry an unknown result until you resolve the authoritative ref at that
|
||||||
|
exact endpoint. Copy the reported endpoint value into the variable below using
|
||||||
|
normal shell quoting; retain the version and tag object from the same result.
|
||||||
|
Check that Git URL rewriting still resolves this value to itself before querying:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git ls-remote --refs origin "refs/tags/vX.Y.Z"
|
releasekit_endpoint='COPY_CAPTURED_ENDPOINT'
|
||||||
|
releasekit_version='vX.Y.Z'
|
||||||
|
test "$(git ls-remote --get-url -- "$releasekit_endpoint")" = "$releasekit_endpoint"
|
||||||
|
git ls-remote --refs -- "$releasekit_endpoint" "refs/tags/$releasekit_version"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
A failed URL-equality check or remote query leaves the outcome unresolved.
|
||||||
|
Correct transport configuration before continuing. The reported endpoint may
|
||||||
|
be different from `origin`'s fetch URL or its current push URL; querying either
|
||||||
|
of those does not establish absence at the captured endpoint.
|
||||||
|
|
||||||
Compare the returned object ID with the tag object reported by the command.
|
Compare the returned object ID with the tag object reported by the command.
|
||||||
An exact match confirms publication. If the ref is absent, the version was not
|
An exact match confirms publication. If the ref is absent, the version was not
|
||||||
published; only then may you deliberately remove the unpublished local tag
|
published; only then may you deliberately remove the unpublished local tag
|
||||||
@@ -91,9 +102,10 @@ with `git tag -d vX.Y.Z` before correcting the candidate or retrying. Do not
|
|||||||
move, overwrite, or delete a tag that exists remotely. If a different object
|
move, overwrite, or delete a tag that exists remotely. If a different object
|
||||||
uses the version, choose a new version after resolving the conflict.
|
uses the version, choose a new version after resolving the conflict.
|
||||||
|
|
||||||
If validation or a guard fails before the push, the local tag has not been
|
If validation or a guard fails before this invocation pushes, correct and
|
||||||
published. Correct and commit the candidate, publish `main`, and run the
|
commit the candidate, publish `main`, and run the command again. A retained
|
||||||
command again. The prepared Woodpecker workflow builds the CLI from the tagged
|
local tag still requires the authoritative-state check above before removal;
|
||||||
|
a concurrent publisher may already have published the version. The prepared Woodpecker workflow builds the CLI from the tagged
|
||||||
checkout and runs page-only delivery. To activate it after configuring the
|
checkout and runs page-only delivery. To activate it after configuring the
|
||||||
destination and secret, copy it to `.woodpecker/releasekit-source-delivery.yml`
|
destination and secret, copy it to `.woodpecker/releasekit-source-delivery.yml`
|
||||||
and add the Woodpecker repository secret `releasekit_gitea_token`. If page
|
and add the Woodpecker repository secret `releasekit_gitea_token`. If page
|
||||||
|
|||||||
@@ -323,6 +323,8 @@ and unresolved publication 3. The focused wrapper test passes.
|
|||||||
|
|
||||||
### R9 — High: Manual recovery inspects the fetch endpoint
|
### R9 — High: Manual recovery inspects the fetch endpoint
|
||||||
|
|
||||||
|
Status: Resolved.
|
||||||
|
|
||||||
Category: Release recovery correctness and documentation gap.
|
Category: Release recovery correctness and documentation gap.
|
||||||
|
|
||||||
**Evidence:** [The runbook](../release.md), around line 82, prescribes
|
**Evidence:** [The runbook](../release.md), around line 82, prescribes
|
||||||
@@ -347,6 +349,13 @@ local tag.
|
|||||||
fetch/push repositories and confirm that a published tag is recognized. Include
|
fetch/push repositories and confirm that a published tag is recognized. Include
|
||||||
endpoint changes after an uncertain push when verifying retained recovery facts.
|
endpoint changes after an uncertain push when verifying retained recovery facts.
|
||||||
|
|
||||||
|
**Resolution:** Failed publication output includes the captured endpoint. The
|
||||||
|
runbook checks URL resolution and queries that exact endpoint, retaining tags
|
||||||
|
on failed or unresolved checks. Local wrapper fixtures use separate fetch and
|
||||||
|
push repositories; a lost acknowledgment changes the configured push URL, and
|
||||||
|
recovery still finds the intended object at the captured endpoint while the
|
||||||
|
fetch endpoint reports absence. Focused publication/wrapper tests pass.
|
||||||
|
|
||||||
### R10 — Medium: The boundary checker bans valid command-internal composition
|
### R10 — Medium: The boundary checker bans valid command-internal composition
|
||||||
|
|
||||||
Category: Inadvertently strengthened architectural prohibition.
|
Category: Inadvertently strengthened architectural prohibition.
|
||||||
|
|||||||
Reference in New Issue
Block a user