diff --git a/docs/release.md b/docs/release.md index 32cf771..1ab276e 100644 --- a/docs/release.md +++ b/docs/release.md @@ -180,10 +180,12 @@ Only when useful or requested, read back the remote tag or retrieve the exact version into a new module without a local `replace` directive. These checks are optional and non-gating. Do not poll CI or wait for module propagation. Private-host credentials must already be configured. For direct-origin -verification, use a temporary consumer module and check the exact module path +verification, set the version to the release just published (replace the sample +below), then use a temporary consumer module and check the exact module path and version reported by `go list -m`: ```sh +release_version=v0.5.0 consumer_dir=$(mktemp -d) ( cd "$consumer_dir" diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index 6122317..7c0a61e 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -1,5 +1,25 @@ # Release Workflow Review Findings +Status: both findings addressed. The original findings below are retained as +review history until normal release-preparation retirement. + +## Resolution and verification + +- Publication, fetch, and recovery now use the same validated named `origin`. + Pushes retain exact refspecs and disabled implicit tags, and override the + remote's mirror default for that invocation only. No new gate was introduced. +- A disposable-Git regression failed against the original publisher and passes + with the fix for normal publication and lost main/tag push responses. It also + checks that broad push defaults do not publish or delete unrelated refs and + that the rewrite destination receives no refs. +- Optional consumer verification explicitly initializes the selected version. + Executing the documented block with a local Go stub verified the exact module + argument with both an initially unset and an initially stale version variable, + without network retrieval. +- Focused publication tests passed. Final validation passed: Bash syntax, + whitespace review, and the Go 1.25.14 release checker (race suite including + documentation links, full vet, and both Linux target builds). + Reviewed implementation through `3d0093e` against the release roadmap and completed implementation plan. The ordinary Go 1.25.14 release checker passed (offline race suite, full vet, Linux amd64/arm64 builds), as did Bash syntax diff --git a/release_publish_test.go b/release_publish_test.go index 8794269..9de0e55 100644 --- a/release_publish_test.go +++ b/release_publish_test.go @@ -108,7 +108,8 @@ func (f *releaseFixture) run(version, behavior, validatorAction string) (string, if [ "${CONFIGKIT_GIT_BEHAVIOR-}" = main-unknown ] || [ "${CONFIGKIT_GIT_BEHAVIOR-}" = tag-unknown ]; then if [ "${1-}" = ls-remote ] && [ -f "$CONFIGKIT_GIT_MARKER" ]; then exit 37; fi fi -if [ "${1-}" = push ]; then +case "$*" in + push\ *|-c\ remote.origin.mirror=false\ push\ *) case "$*" in *refs/heads/main*) case "${CONFIGKIT_GIT_BEHAVIOR-}" in @@ -126,7 +127,8 @@ if [ "${1-}" = push ]; then tag-unknown) : > "$CONFIGKIT_GIT_MARKER"; exit 37 ;; esac ;; esac -fi + ;; +esac exec "$CONFIGKIT_REAL_GIT" "$@" ` if err := os.WriteFile(filepath.Join(bin, "git"), []byte(wrapper), 0700); err != nil { @@ -320,3 +322,38 @@ func TestReleasePublicationRecovery(t *testing.T) { }) } } + +func TestReleasePublicationUsesValidatedDestination(t *testing.T) { + for _, behavior := range []string{"success", "main-lost", "tag-lost"} { + t.Run(behavior, func(t *testing.T) { + f := newReleaseFixture(t) + other := filepath.Join(t.TempDir(), "other.git") + f.git("init", "-q", "--bare", other) + f.git("remote", "set-url", "--push", "origin", f.remote) + f.git("config", "url."+other+".pushInsteadOf", f.remote) + // These defaults must not broaden the explicit release refspecs when + // publishing through the validated named remote. + f.git("config", "remote.origin.mirror", "true") + f.git("config", "remote.origin.push", "refs/heads/*:refs/heads/*") + f.git("branch", "unrelated", f.base) + f.git("tag", "-a", "-m", "Unrelated", "v0.8.0", f.base) + f.git("--git-dir="+f.remote, "update-ref", "refs/heads/keep", f.base) + + out, err := f.run("v0.9.0", behavior, "") + if err != nil { + t.Fatalf("publish: %v\n%s", err, out) + } + if f.validations() != 1 || f.remoteRef("refs/heads/main") != f.candidate || + f.remoteRef("refs/tags/v0.9.0") != f.git("rev-parse", "refs/tags/v0.9.0") { + t.Fatal("publication did not reach the validated destination") + } + if f.remoteRef("refs/heads/keep") != f.base || + f.remoteRef("refs/heads/unrelated") != "" || f.remoteRef("refs/tags/v0.8.0") != "" { + t.Fatal("publication changed unrelated refs") + } + if refs := f.git("--git-dir="+other, "for-each-ref", "--format=%(refname)"); refs != "" { + t.Fatalf("publication reached the rewrite destination: %s", refs) + } + }) + } +} diff --git a/scripts/release.sh b/scripts/release.sh index adcea8e..740cd21 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -36,7 +36,7 @@ check_unused_version() { read_remote_ref() { local ref=$1 response result - if response=$(git ls-remote --refs --exit-code "$remote_url" "$ref" 2>/dev/null); then + if response=$(git ls-remote --refs --exit-code origin "$ref" 2>/dev/null); then [[ "$response" == *$'\t'"$ref" ]] || return 3 printf '%s\n' "${response%%$'\t'*}" else @@ -78,7 +78,7 @@ else ((result == 2)) && fail "authoritative main is absent" fail "cannot read authoritative main; publication status is unknown" fi -git fetch --no-tags "$remote_url" refs/heads/main >/dev/null || +git fetch --no-tags origin refs/heads/main >/dev/null || fail "cannot fetch authoritative main" fetched_main=$(git rev-parse --verify FETCH_HEAD) || fail "cannot inspect fetched main" git merge-base --is-ancestor "$fetched_main" "$candidate" || @@ -88,7 +88,10 @@ git merge-base --is-ancestor "$fetched_main" "$candidate" || check_candidate "after validation" check_unused_version -if git push --no-follow-tags "$remote_url" "$candidate:refs/heads/main"; then +# Use the named remote whose resolved destinations were checked above: a literal +# URL can apply pushInsteadOf even when origin's explicit push URL does not. +# Disable mirror defaults as well as implicit tags; publish only our refspec. +if git -c remote.origin.mirror=false push --no-follow-tags origin "$candidate:refs/heads/main"; then : else if observed_main=$(read_remote_ref refs/heads/main); then @@ -113,7 +116,7 @@ tag_type=$(git cat-file -t "refs/tags/$version") || tag_oid=$(git rev-parse --verify "refs/tags/$version") || fail "main is at $candidate; cannot identify local tag $version" -if git push --no-follow-tags "$remote_url" "refs/tags/$version:refs/tags/$version"; then +if git -c remote.origin.mirror=false push --no-follow-tags origin "refs/tags/$version:refs/tags/$version"; then : else if observed_tag=$(read_remote_ref "refs/tags/$version"); then