Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions internal/releasecheck/publisher_tag_binding_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package releasecheck

import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)

// GitHub clears a release's tag when an update omits tag_name. Model that
// mutation before returning the response: checking the response is too late
// once a newly published release has become immutable.
func TestPublisherPatchesPreserveEachBoundReleaseTag(t *testing.T) {
publish := readReleaseFile(t, "../../scripts/publish-release.sh")
start := strings.Index(publish, "initial_release_state() {")
end := strings.Index(publish, "\nrequire_remote_tag_object() {")
if start < 0 || end <= start {
t.Fatal("could not isolate bound publisher mutation functions")
}
for _, tc := range []struct {
name, initial, invoke, want string
}{
{
name: "publish candidate",
initial: "v2.10.9 true false false 12345",
invoke: "publish_bound_release",
want: "v2.10.9 false false true 12345 false",
},
{
name: "restore mutable candidate to draft",
initial: "v2.10.9 false false false 12345",
invoke: "secure_failed_publication_state",
want: "v2.10.9 true false false 12345 false",
},
{
name: "promote candidate to Latest",
initial: "v2.10.9 false false true 12345",
invoke: "set_latest_by_release_id 12345 v2.10.9 true",
want: "v2.10.9 false false true 12345 true",
},
{
name: "restore previous Latest with its own tag",
initial: "v2.10.8 false false true 12344",
invoke: "set_latest_by_release_id 12344 v2.10.8 true",
want: "v2.10.8 false false true 12344 true",
},
{
name: "clear candidate Latest",
initial: "v2.10.9 false false true 12345",
invoke: "set_latest_by_release_id 12345 v2.10.9 false",
want: "v2.10.9 false false true 12345 false",
},
} {
t.Run(tc.name, func(t *testing.T) {
dir := t.TempDir()
script := filepath.Join(dir, "patch-tag.sh")
body := `#!/bin/bash
set -Eeuo pipefail
TAG=v2.10.9
REPO=mock/repo
EXPECTED_RELEASE_ID=12345
expected_prerelease=false
MAX_RELEASE_VERSION_BYTES=128
printf '%s\n' "$TEST_INITIAL" > "$TEST_STATE/release"
gh_with_timeout() {
local tag draft prerelease immutable id endpoint field latest=unchanged
read -r tag draft prerelease immutable id < "$TEST_STATE/release"
if [[ "$1" == api && "$2" == "repos/${REPO}/releases/${id}" ]]; then
cat "$TEST_STATE/release"
return 0
fi
[[ "$1" == api && "$2" == --method && "$3" == PATCH ]] || return 90
endpoint=$4
[[ "$endpoint" == "repos/${REPO}/releases/${id}" ]] || return 91
shift 4
# Match the real API's omission behavior, not the desired caller behavior.
tag=untagged-fixture
while (( $# )); do
case "$1" in
-f|-F)
field=$2
case "$field" in
tag_name=*) tag=${field#*=} ;;
draft=true) draft=true; immutable=false ;;
draft=false) draft=false; immutable=true ;;
prerelease=*) prerelease=${field#*=} ;;
make_latest=*) latest=${field#*=} ;;
*) return 92 ;;
esac
shift 2
;;
--jq) shift 2 ;;
*) return 93 ;;
esac
done
printf '%s %s %s %s %s\n' "$tag" "$draft" "$prerelease" "$immutable" "$id" > "$TEST_STATE/release"
printf '%s %s %s %s %s %s\n' "$tag" "$draft" "$prerelease" "$immutable" "$id" "$latest" >> "$TEST_STATE/mutations"
cat "$TEST_STATE/release"
}
` + publish[start:end] + "\n" + tc.invoke + "\n"
if err := os.WriteFile(script, []byte(body), 0o700); err != nil {
t.Fatal(err)
}
cmd := exec.Command("/bin/bash", "-p", script)
cmd.Env = append(os.Environ(), "TEST_STATE="+dir, "TEST_INITIAL="+tc.initial)
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("bound release tag was lost during PATCH: %v\n%s", err, out)
}
mutations, err := os.ReadFile(filepath.Join(dir, "mutations"))
if err != nil || string(mutations) != tc.want+"\n" {
t.Fatalf("mutations = %q, %v; want exactly %q", mutations, err, tc.want+"\n")
}
})
}
}
3 changes: 3 additions & 0 deletions scripts/publish-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,7 @@ require_draft() {
publish_bound_release() {
local state actual_tag actual_draft actual_prerelease actual_immutable actual_id extra
state="$(gh_with_timeout api --method PATCH "repos/${REPO}/releases/${EXPECTED_RELEASE_ID}" \
-f "tag_name=${TAG}" \
-F draft=false -F "prerelease=${expected_prerelease}" -f make_latest=false \
--jq '. | (.tag_name|tostring) + " " + (.draft|tostring) + " " + (.prerelease|tostring) + " " + (.immutable|tostring) + " " + (.id|tostring)')" \
|| return 1
Expand Down Expand Up @@ -685,6 +686,7 @@ secure_failed_publication_state() {
fi

rollback_state="$(gh_with_timeout api --method PATCH "repos/${REPO}/releases/${EXPECTED_RELEASE_ID}" \
-f "tag_name=${TAG}" \
-F draft=true -F "prerelease=${expected_prerelease}" -f make_latest=false \
--jq '. | (.tag_name|tostring) + " " + (.draft|tostring) + " " + (.prerelease|tostring) + " " + (.immutable|tostring) + " " + (.id|tostring)')" \
|| { echo "could not return mutable Release $EXPECTED_RELEASE_ID to draft" >&2; return 1; }
Expand Down Expand Up @@ -719,6 +721,7 @@ set_latest_by_release_id() {
&& ( "$make_latest" == true || "$make_latest" == false ) ]] \
|| { echo "invalid bound Release identity for Latest update" >&2; return 1; }
state="$(gh_with_timeout api --method PATCH "repos/${REPO}/releases/${release_id}" \
-f "tag_name=${tag}" \
-f "make_latest=${make_latest}" \
--jq '. | (.tag_name|tostring) + " " + (.draft|tostring) + " " + (.prerelease|tostring) + " " + (.immutable|tostring) + " " + (.id|tostring)')" \
|| return 1
Expand Down
Loading