mirror of
https://github.com/block/buzz.git
synced 2026-10-30 22:11:36 +00:00
chore(mobile): lighter-weight release process (#2144)
Signed-off-by: npub1sv749mw8zcmld4ygjx2mx2aqcn3zvtuj2nlmgatxgad3t9uweu9q5marze <833d52edc71637f6d4889195b32ba0c4e2262f9254ffb47566475b15978ecf0a@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1ux8n2yfs8qfvgd75s7kyhar2mztac355v6vmrz4juc9l3msw4pgstums9e <e18f3511303812c437d487ac4bf46ad897dc46946699b18ab2e60bf8ee0ea851@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1re830n24qhgstulznk5dxdluccspxkxxdq643lm4q3zwwwjgsuqqmcmdrm <1e4f17cd5505d105f3e29da8d337fcc6201358c6683558ff750444e73a488700@buzz.block.builderlab.xyz> Signed-off-by: npub102wg7q285p64ch2fjvstmf2ntn2sz3c4u5hmwatalc76mhsuauysftjtfj <7a9c8f0147a0755c5d499320bda5535cd5014715e52fb7757dfe3dadde1cef09@buzz.block.builderlab.xyz> Co-authored-by: npub1sv749mw8zcmld4ygjx2mx2aqcn3zvtuj2nlmgatxgad3t9uweu9q5marze <833d52edc71637f6d4889195b32ba0c4e2262f9254ffb47566475b15978ecf0a@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1ux8n2yfs8qfvgd75s7kyhar2mztac355v6vmrz4juc9l3msw4pgstums9e <e18f3511303812c437d487ac4bf46ad897dc46946699b18ab2e60bf8ee0ea851@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1re830n24qhgstulznk5dxdluccspxkxxdq643lm4q3zwwwjgsuqqmcmdrm <1e4f17cd5505d105f3e29da8d337fcc6201358c6683558ff750444e73a488700@buzz.block.builderlab.xyz> Co-authored-by: 7a9c8f0147a0755c5d499320bda5535cd5014715e52fb7757dfe3dadde1cef09@buzz.block.builderlab.xyz <7a9c8f0147a0755c5d499320bda5535cd5014715e52fb7757dfe3dadde1cef09@buzz.block.builderlab.xyz>
This commit is contained in:
169
scripts/mobile-release.sh
Executable file
169
scripts/mobile-release.sh
Executable file
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat >&2 <<'USAGE'
|
||||
usage:
|
||||
scripts/mobile-release.sh candidate X.Y.Z
|
||||
|
||||
candidate Publish the next immutable mobile-vX.Y.Z-rc.N candidate tag at the
|
||||
exact current commit of block/buzz's remote main branch.
|
||||
USAGE
|
||||
exit 2
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "Error: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# shellcheck source=scripts/release-rulesets.sh
|
||||
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/release-rulesets.sh"
|
||||
|
||||
require_gh_minimum_version() {
|
||||
local minimum="2.87.0" version
|
||||
|
||||
command -v gh >/dev/null 2>&1 || fail "gh >= $minimum is required"
|
||||
version="$(gh --version 2>/dev/null | awk 'NR == 1 { print $3 }')" || \
|
||||
fail "could not determine gh version (gh >= $minimum is required)"
|
||||
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || \
|
||||
fail "gh returned invalid version '$version'"
|
||||
if ! awk -v current="$version" -v minimum="$minimum" '
|
||||
BEGIN {
|
||||
split(current, c, ".")
|
||||
split(minimum, m, ".")
|
||||
for (i = 1; i <= 3; i++) {
|
||||
if ((c[i] + 0) > (m[i] + 0)) exit 0
|
||||
if ((c[i] + 0) < (m[i] + 0)) exit 1
|
||||
}
|
||||
exit 0
|
||||
}
|
||||
'; then
|
||||
fail "gh $version is too old; gh >= $minimum is required"
|
||||
fi
|
||||
}
|
||||
|
||||
require_clean_semver() {
|
||||
[[ "$1" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] || \
|
||||
fail "'$1' is not a mobile release version (expected X.Y.Z)"
|
||||
}
|
||||
|
||||
require_clean_tree() {
|
||||
git diff --quiet && git diff --cached --quiet && \
|
||||
[[ -z "$(git status --short --untracked-files=normal)" ]] || \
|
||||
fail "working tree is dirty; commit or stash changes first"
|
||||
}
|
||||
|
||||
require_annotated_tag() {
|
||||
local git_dir="$1" object="$2" label="$3"
|
||||
if [[ "$(git -C "$git_dir" cat-file -t "$object" 2>/dev/null || true)" != "tag" ]]; then
|
||||
echo "$label must be an annotated tag" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
remote_tag_commit_sha() {
|
||||
local ref="$1" line advertised_oid tmp fetched_oid commit
|
||||
line="$(git ls-remote --refs origin "$ref")" || return 1
|
||||
[[ -n "$line" && "$line" != *$'\n'* ]] || return 1
|
||||
advertised_oid="${line%%$'\t'*}"
|
||||
tmp="$(mktemp -d)"
|
||||
git -C "$tmp" init -q
|
||||
git -C "$tmp" remote add origin "$(git remote get-url origin)"
|
||||
if ! git -C "$tmp" fetch -q --depth 1 origin "$ref"; then
|
||||
rm -rf "$tmp"
|
||||
return 1
|
||||
fi
|
||||
fetched_oid="$(git -C "$tmp" rev-parse --verify FETCH_HEAD)"
|
||||
if [[ "$fetched_oid" != "$advertised_oid" ]]; then
|
||||
rm -rf "$tmp"
|
||||
fail "$ref moved while it was being resolved"
|
||||
fi
|
||||
if ! require_annotated_tag "$tmp" FETCH_HEAD "$ref"; then
|
||||
rm -rf "$tmp"
|
||||
return 1
|
||||
fi
|
||||
if ! commit="$(git -C "$tmp" rev-parse --verify 'FETCH_HEAD^{commit}')"; then
|
||||
rm -rf "$tmp"
|
||||
return 1
|
||||
fi
|
||||
rm -rf "$tmp"
|
||||
printf '%s' "$commit"
|
||||
}
|
||||
|
||||
remote_main_commit_sha() {
|
||||
local ref="refs/heads/main" line advertised_oid fetched_oid commit
|
||||
line="$(git ls-remote --refs origin "$ref")" || return 1
|
||||
[[ -n "$line" && "$line" != *$'\n'* ]] || return 1
|
||||
advertised_oid="${line%%$'\t'*}"
|
||||
git fetch -q --no-tags origin "$ref"
|
||||
fetched_oid="$(git rev-parse --verify FETCH_HEAD)"
|
||||
[[ "$fetched_oid" == "$advertised_oid" ]] || \
|
||||
fail "origin/main moved while it was being resolved"
|
||||
commit="$(git rev-parse --verify 'FETCH_HEAD^{commit}')" || return 1
|
||||
[[ "$commit" == "$advertised_oid" ]] || \
|
||||
fail "origin/main did not resolve directly to a commit"
|
||||
printf '%s' "$commit"
|
||||
}
|
||||
|
||||
command="${1:-}"
|
||||
case "$command" in
|
||||
candidate)
|
||||
[[ "$#" -eq 2 ]] || usage
|
||||
version="$2"
|
||||
require_clean_semver "$version"
|
||||
require_clean_tree
|
||||
require_canonical_repository || exit 1
|
||||
require_gh_minimum_version
|
||||
|
||||
local_head_sha="$(git rev-parse --verify 'HEAD^{commit}')" || fail "HEAD is not a commit"
|
||||
main_sha="$(remote_main_commit_sha)" || fail "origin/main does not exist"
|
||||
next=1
|
||||
if ! remote_tags="$(git ls-remote --refs --tags origin "refs/tags/mobile-v${version}-rc.*")"; then
|
||||
fail "could not list existing candidates for $version"
|
||||
fi
|
||||
while IFS=$'\t' read -r _ ref; do
|
||||
[[ "$ref" =~ ^refs/tags/mobile-v${version//./\.}-rc\.([1-9][0-9]*)$ ]] || continue
|
||||
number="${BASH_REMATCH[1]}"
|
||||
(( number >= next )) && next=$((number + 1))
|
||||
done <<< "$remote_tags"
|
||||
|
||||
tag="mobile-v${version}-rc.${next}"
|
||||
workflow="mobile-release-candidate.yml"
|
||||
if dispatch_output="$(gh workflow run "$workflow" \
|
||||
--repo block/buzz \
|
||||
--ref main \
|
||||
-f "version=$version" \
|
||||
-f "candidate_number=$next" \
|
||||
-f "target_sha=$main_sha" 2>&1)"; then
|
||||
:
|
||||
else
|
||||
if [[ "$dispatch_output" == *"does not have 'workflow_dispatch' trigger"* ]]; then
|
||||
fail "$workflow is not available on main yet; merge the release-process change before publishing a candidate"
|
||||
fi
|
||||
fail "could not dispatch App-backed publication for $tag: $dispatch_output"
|
||||
fi
|
||||
run_url="$(printf '%s\n' "$dispatch_output" | awk '/^https:\/\/github\.com\/block\/buzz\/actions\/runs\/[0-9]+$/ { if (found) exit 2; found = $0 } END { if (found) print found }')" || \
|
||||
fail "GitHub returned multiple workflow run URLs for one candidate dispatch"
|
||||
[[ -n "$run_url" ]] || \
|
||||
fail "GitHub accepted the candidate dispatch but returned no workflow run URL"
|
||||
run_id="${run_url##*/}"
|
||||
gh run watch "$run_id" --repo block/buzz --exit-status --compact || \
|
||||
fail "App-backed publication failed: $run_url"
|
||||
|
||||
current_main_sha="$(remote_main_commit_sha)" || fail "origin/main does not exist after publication"
|
||||
[[ "$current_main_sha" == "$main_sha" ]] || \
|
||||
fail "origin/main moved from requested commit $main_sha to $current_main_sha during publication"
|
||||
published_sha="$(remote_tag_commit_sha "refs/tags/$tag")" || \
|
||||
fail "publication completed without exact annotated candidate tag $tag"
|
||||
[[ "$published_sha" == "$main_sha" ]] || \
|
||||
fail "$tag resolved to $published_sha instead of requested commit $main_sha"
|
||||
if [[ "$local_head_sha" != "$main_sha" ]]; then
|
||||
echo "Note: local HEAD is $local_head_sha; candidate source is current origin/main $main_sha." >&2
|
||||
fi
|
||||
printf 'Published %s at origin/main commit %s through buzz-release-bot. Use this exact tag in Release Mobile.\n' \
|
||||
"$tag" "$main_sha"
|
||||
;;
|
||||
|
||||
*) usage ;;
|
||||
esac
|
||||
80
scripts/publish-mobile-release-candidate.sh
Executable file
80
scripts/publish-mobile-release-candidate.sh
Executable file
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
fail() {
|
||||
echo "Error: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# shellcheck source=scripts/release-rulesets.sh
|
||||
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/release-rulesets.sh"
|
||||
|
||||
[[ "$#" -eq 3 ]] || fail "usage: $0 X.Y.Z N COMMIT_SHA"
|
||||
version="$1"
|
||||
candidate_number="$2"
|
||||
target_sha="$3"
|
||||
repo="${GITHUB_REPOSITORY:-}"
|
||||
|
||||
[[ "$repo" == "block/buzz" ]] || fail "candidate publishing is restricted to block/buzz"
|
||||
[[ "$version" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] || \
|
||||
fail "'$version' is not a mobile release version (expected X.Y.Z)"
|
||||
[[ "$candidate_number" =~ ^[1-9][0-9]*$ ]] || \
|
||||
fail "'$candidate_number' is not a candidate number (expected N >= 1)"
|
||||
[[ "$target_sha" =~ ^[0-9a-f]{40}$ ]] || fail "'$target_sha' is not a full commit SHA"
|
||||
command -v gh >/dev/null 2>&1 || fail "gh is required"
|
||||
require_release_tag_ruleset || exit 1
|
||||
|
||||
main_sha="$(gh api "repos/$repo/git/ref/heads/main" --jq .object.sha)" || \
|
||||
fail "could not resolve $repo main"
|
||||
[[ "$main_sha" == "$target_sha" ]] || \
|
||||
fail "$repo main moved from requested commit $target_sha to $main_sha"
|
||||
commit_sha="$(gh api "repos/$repo/commits/$target_sha" --jq .sha)" || \
|
||||
fail "$target_sha is not a commit in $repo"
|
||||
[[ "$commit_sha" == "$target_sha" ]] || \
|
||||
fail "GitHub resolved requested commit $target_sha as $commit_sha"
|
||||
|
||||
refs="$(
|
||||
gh api --paginate "repos/$repo/git/matching-refs/tags/mobile-v${version}-rc." --jq '.[].ref'
|
||||
)" || fail "could not list existing candidates for $version"
|
||||
next=1
|
||||
while IFS= read -r ref; do
|
||||
[[ -n "$ref" ]] || continue
|
||||
[[ "$ref" =~ ^refs/tags/mobile-v${version//./\.}-rc\.([1-9][0-9]*)$ ]] || continue
|
||||
number="${BASH_REMATCH[1]}"
|
||||
(( number >= next )) && next=$((number + 1))
|
||||
done <<< "$refs"
|
||||
[[ "$candidate_number" -eq "$next" ]] || \
|
||||
fail "candidate sequence changed; expected rc.$candidate_number but next is rc.$next"
|
||||
|
||||
tag="mobile-v${version}-rc.${candidate_number}"
|
||||
message="Buzz Mobile $version release candidate $candidate_number"
|
||||
tag_object_sha="$(
|
||||
gh api --method POST "repos/$repo/git/tags" \
|
||||
-f tag="$tag" \
|
||||
-f message="$message" \
|
||||
-f object="$target_sha" \
|
||||
-f type=commit \
|
||||
--jq .sha
|
||||
)" || fail "could not create annotated tag object for $tag"
|
||||
[[ "$tag_object_sha" =~ ^[0-9a-f]{40}$ ]] || fail "GitHub returned an invalid tag object SHA"
|
||||
|
||||
gh api --method POST "repos/$repo/git/refs" \
|
||||
-f ref="refs/tags/$tag" \
|
||||
-f sha="$tag_object_sha" \
|
||||
--silent || fail "could not publish $tag"
|
||||
|
||||
published_type="$(gh api "repos/$repo/git/ref/tags/$tag" --jq .object.type)" || \
|
||||
fail "could not verify published tag $tag"
|
||||
published_object="$(gh api "repos/$repo/git/ref/tags/$tag" --jq .object.sha)" || \
|
||||
fail "could not verify published tag $tag"
|
||||
[[ "$published_type" == "tag" && "$published_object" == "$tag_object_sha" ]] || \
|
||||
fail "$tag does not reference the expected annotated tag object"
|
||||
|
||||
direct_type="$(gh api "repos/$repo/git/tags/$tag_object_sha" --jq .object.type)" || \
|
||||
fail "could not verify annotated tag object $tag_object_sha"
|
||||
direct_sha="$(gh api "repos/$repo/git/tags/$tag_object_sha" --jq .object.sha)" || \
|
||||
fail "could not verify annotated tag object $tag_object_sha"
|
||||
[[ "$direct_type" == "commit" && "$direct_sha" == "$target_sha" ]] || \
|
||||
fail "$tag does not point directly to requested commit $target_sha"
|
||||
|
||||
printf 'Published %s at %s through buzz-release-bot.\n' "$tag" "$target_sha"
|
||||
59
scripts/release-rulesets.sh
Executable file
59
scripts/release-rulesets.sh
Executable file
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
readonly RELEASE_TAG_RULESET_ID=14378754
|
||||
|
||||
fail_release_ruleset() {
|
||||
echo "Error: $*" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
require_canonical_repository() {
|
||||
local origin_url
|
||||
|
||||
origin_url="$(git config --get remote.origin.url 2>/dev/null)" || \
|
||||
fail_release_ruleset "origin is required and must point to block/buzz" || return 1
|
||||
case "$origin_url" in
|
||||
git@github.com:block/buzz.git|ssh://git@github.com/block/buzz.git|https://github.com/block/buzz.git|https://github.com/block/buzz)
|
||||
;;
|
||||
*)
|
||||
fail_release_ruleset "origin must point to canonical block/buzz, not '$origin_url'" || return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
require_release_tag_ruleset() {
|
||||
local ruleset_endpoint state can_bypass bypass_actors rule_types includes excludes
|
||||
|
||||
command -v gh >/dev/null 2>&1 || fail_release_ruleset "gh is required" || return 1
|
||||
ruleset_endpoint="repos/block/buzz/rulesets/$RELEASE_TAG_RULESET_ID"
|
||||
|
||||
state="$(gh api "$ruleset_endpoint" --jq .enforcement)" || \
|
||||
fail_release_ruleset "could not verify Release tag ruleset $RELEASE_TAG_RULESET_ID" || return 1
|
||||
[[ "$state" == "active" ]] || \
|
||||
fail_release_ruleset "Release tag ruleset $RELEASE_TAG_RULESET_ID is '$state'" || return 1
|
||||
|
||||
can_bypass="$(gh api "$ruleset_endpoint" --jq .current_user_can_bypass)" || \
|
||||
fail_release_ruleset "could not verify the release App's tag-ruleset bypass" || return 1
|
||||
[[ "$can_bypass" == "always" ]] || \
|
||||
fail_release_ruleset "release App cannot always bypass Release tag ruleset $RELEASE_TAG_RULESET_ID (reported '$can_bypass')" || return 1
|
||||
|
||||
bypass_actors="$(gh api "$ruleset_endpoint" --jq '[.bypass_actors[] | [.actor_type, (.actor_id | tostring), .bypass_mode] | join(":")] | sort | join(",")')" || \
|
||||
fail_release_ruleset "could not verify Release tag ruleset $RELEASE_TAG_RULESET_ID bypass actors" || return 1
|
||||
[[ "$bypass_actors" == "Integration:4349119:always" ]] || \
|
||||
fail_release_ruleset "Release tag ruleset $RELEASE_TAG_RULESET_ID has unexpected bypass actors: '$bypass_actors'" || return 1
|
||||
|
||||
rule_types="$(gh api "$ruleset_endpoint" --jq '[.rules[].type] | sort | join(",")')" || \
|
||||
fail_release_ruleset "could not verify Release tag ruleset $RELEASE_TAG_RULESET_ID rules" || return 1
|
||||
[[ "$rule_types" == "creation,deletion,non_fast_forward,update" ]] || \
|
||||
fail_release_ruleset "Release tag ruleset $RELEASE_TAG_RULESET_ID has unexpected rules: '$rule_types'" || return 1
|
||||
|
||||
includes="$(gh api "$ruleset_endpoint" --jq '[.conditions.ref_name.include[]] | sort | join(",")')" || \
|
||||
fail_release_ruleset "could not verify Release tag ruleset $RELEASE_TAG_RULESET_ID scope" || return 1
|
||||
[[ ",$includes," == *",refs/tags/mobile-v*,"* ]] || \
|
||||
fail_release_ruleset "Release tag ruleset $RELEASE_TAG_RULESET_ID does not include refs/tags/mobile-v*" || return 1
|
||||
|
||||
excludes="$(gh api "$ruleset_endpoint" --jq '[.conditions.ref_name.exclude[]] | sort | join(",")')" || \
|
||||
fail_release_ruleset "could not verify Release tag ruleset $RELEASE_TAG_RULESET_ID exclusions" || return 1
|
||||
[[ -z "$excludes" ]] || \
|
||||
fail_release_ruleset "Release tag ruleset $RELEASE_TAG_RULESET_ID has unexpected exclusions: '$excludes'" || return 1
|
||||
}
|
||||
173
scripts/test-mobile-release-candidate-publisher.sh
Executable file
173
scripts/test-mobile-release-candidate-publisher.sh
Executable file
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
publisher="$repo_root/scripts/publish-mobile-release-candidate.sh"
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
bin="$tmp/bin"
|
||||
mkdir -p "$bin"
|
||||
cat > "$bin/gh" <<'GH'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
record() {
|
||||
printf '%s\n' "$*" >> "$GH_CALLS"
|
||||
}
|
||||
|
||||
case "${1:-}:${2:-}" in
|
||||
api:repos/block/buzz/rulesets/14378754)
|
||||
case "$*" in
|
||||
*'.enforcement'*) printf '%s\n' "${GH_TAG_RULESET_STATE:-active}" ;;
|
||||
*'.current_user_can_bypass'*) printf '%s\n' "${GH_CURRENT_USER_CAN_BYPASS-always}" ;;
|
||||
*'.bypass_actors[]'*) printf '%s\n' "${GH_BYPASS_ACTORS:-Integration:4349119:always}" ;;
|
||||
*'[.rules[].type]'*) printf '%s\n' "${GH_TAG_RULE_TYPES:-creation,deletion,non_fast_forward,update}" ;;
|
||||
*'.conditions.ref_name.include[]'*) printf '%s\n' "${GH_TAG_INCLUDES:-refs/tags/mobile-v*}" ;;
|
||||
*'.conditions.ref_name.exclude[]'*) printf '%s\n' "${GH_TAG_EXCLUDES:-}" ;;
|
||||
*) exit 2 ;;
|
||||
esac
|
||||
;;
|
||||
api:repos/block/buzz/git/ref/heads/main) printf '%s\n' "$GH_TARGET_SHA" ;;
|
||||
api:repos/block/buzz/commits/*) printf '%s\n' "$GH_TARGET_SHA" ;;
|
||||
api:--paginate)
|
||||
[[ "$3" == "repos/block/buzz/git/matching-refs/tags/mobile-v1.2.3-rc." ]]
|
||||
printf '%s' "${GH_EXISTING_REFS:-}"
|
||||
;;
|
||||
api:--method)
|
||||
endpoint="$4"
|
||||
case "$endpoint" in
|
||||
repos/block/buzz/git/tags)
|
||||
record "$*"
|
||||
printf '%s\n' "$GH_TAG_OBJECT_SHA"
|
||||
;;
|
||||
repos/block/buzz/git/refs)
|
||||
record "$*"
|
||||
;;
|
||||
*) exit 2 ;;
|
||||
esac
|
||||
;;
|
||||
api:repos/block/buzz/git/ref/tags/mobile-v1.2.3-rc.*)
|
||||
if [[ "$*" == *'.object.type'* ]]; then
|
||||
printf '%s\n' "${GH_PUBLISHED_REF_TYPE:-tag}"
|
||||
else
|
||||
printf '%s\n' "${GH_PUBLISHED_REF_SHA:-$GH_TAG_OBJECT_SHA}"
|
||||
fi
|
||||
;;
|
||||
api:repos/block/buzz/git/tags/*)
|
||||
if [[ "$*" == *'.object.type'* ]]; then
|
||||
printf '%s\n' "${GH_ANNOTATED_TARGET_TYPE:-commit}"
|
||||
else
|
||||
printf '%s\n' "${GH_ANNOTATED_TARGET_SHA:-$GH_TARGET_SHA}"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "unexpected gh call: $*" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
GH
|
||||
chmod +x "$bin/gh"
|
||||
|
||||
export PATH="$bin:$PATH"
|
||||
export GH_CALLS="$tmp/calls"
|
||||
export GITHUB_REPOSITORY=block/buzz
|
||||
export GH_TARGET_SHA=1111111111111111111111111111111111111111
|
||||
export GH_TAG_OBJECT_SHA=2222222222222222222222222222222222222222
|
||||
|
||||
"$publisher" 1.2.3 1 "$GH_TARGET_SHA"
|
||||
grep -Fq -- '-f tag=mobile-v1.2.3-rc.1' "$GH_CALLS"
|
||||
grep -Fq -- '-f message=Buzz Mobile 1.2.3 release candidate 1' "$GH_CALLS"
|
||||
grep -Fq -- "-f object=$GH_TARGET_SHA" "$GH_CALLS"
|
||||
grep -Fq -- '-f type=commit' "$GH_CALLS"
|
||||
grep -Fq -- '-f ref=refs/tags/mobile-v1.2.3-rc.1' "$GH_CALLS"
|
||||
grep -Fq -- "-f sha=$GH_TAG_OBJECT_SHA" "$GH_CALLS"
|
||||
|
||||
if GH_CURRENT_USER_CAN_BYPASS=never "$publisher" 1.2.3 1 "$GH_TARGET_SHA" >/dev/null 2>&1; then
|
||||
echo "publisher accepted an App token without an always bypass" >&2
|
||||
exit 1
|
||||
fi
|
||||
if GH_CURRENT_USER_CAN_BYPASS='' "$publisher" 1.2.3 1 "$GH_TARGET_SHA" >/dev/null 2>&1; then
|
||||
echo "publisher accepted a ruleset response without an effective bypass" >&2
|
||||
exit 1
|
||||
fi
|
||||
if GH_BYPASS_ACTORS='Integration:4349119:always,Integration:9876543:always' \
|
||||
"$publisher" 1.2.3 1 "$GH_TARGET_SHA" >/dev/null 2>&1; then
|
||||
echo "publisher accepted an extra ruleset bypass actor" >&2
|
||||
exit 1
|
||||
fi
|
||||
if GH_BYPASS_ACTORS='Integration:9876543:always' \
|
||||
"$publisher" 1.2.3 1 "$GH_TARGET_SHA" >/dev/null 2>&1; then
|
||||
echo "publisher accepted a substituted ruleset bypass actor" >&2
|
||||
exit 1
|
||||
fi
|
||||
if GH_BYPASS_ACTORS='Integration:4349119:pull_request' \
|
||||
"$publisher" 1.2.3 1 "$GH_TARGET_SHA" >/dev/null 2>&1; then
|
||||
echo "publisher accepted a ruleset bypass actor with the wrong mode" >&2
|
||||
exit 1
|
||||
fi
|
||||
if GH_TAG_RULESET_STATE=disabled "$publisher" 1.2.3 1 "$GH_TARGET_SHA" >/dev/null 2>&1; then
|
||||
echo "publisher accepted disabled tag protection" >&2
|
||||
exit 1
|
||||
fi
|
||||
if GH_TAG_RULE_TYPES=creation "$publisher" 1.2.3 1 "$GH_TARGET_SHA" >/dev/null 2>&1; then
|
||||
echo "publisher accepted incomplete tag protection" >&2
|
||||
exit 1
|
||||
fi
|
||||
if GH_TAG_INCLUDES=refs/tags/v\* "$publisher" 1.2.3 1 "$GH_TARGET_SHA" >/dev/null 2>&1; then
|
||||
echo "publisher accepted a tag ruleset that excludes mobile candidates" >&2
|
||||
exit 1
|
||||
fi
|
||||
if GH_TAG_EXCLUDES=refs/tags/mobile-v0.0.0 "$publisher" 1.2.3 1 "$GH_TARGET_SHA" >/dev/null 2>&1; then
|
||||
echo "publisher accepted tag ruleset exclusions" >&2
|
||||
exit 1
|
||||
fi
|
||||
if GH_TARGET_SHA=3333333333333333333333333333333333333333 \
|
||||
"$publisher" 1.2.3 1 1111111111111111111111111111111111111111 >/dev/null 2>&1; then
|
||||
echo "publisher accepted a moved main branch" >&2
|
||||
exit 1
|
||||
fi
|
||||
if GH_EXISTING_REFS=$'refs/tags/mobile-v1.2.3-rc.1\n' \
|
||||
"$publisher" 1.2.3 1 "$GH_TARGET_SHA" >/dev/null 2>&1; then
|
||||
echo "publisher accepted a stale candidate number" >&2
|
||||
exit 1
|
||||
fi
|
||||
if GH_PUBLISHED_REF_TYPE=commit "$publisher" 1.2.3 1 "$GH_TARGET_SHA" >/dev/null 2>&1; then
|
||||
echo "publisher accepted a lightweight published tag" >&2
|
||||
exit 1
|
||||
fi
|
||||
if GH_PUBLISHED_REF_SHA=3333333333333333333333333333333333333333 \
|
||||
"$publisher" 1.2.3 1 "$GH_TARGET_SHA" >/dev/null 2>&1; then
|
||||
echo "publisher accepted the wrong annotated tag object" >&2
|
||||
exit 1
|
||||
fi
|
||||
if GH_ANNOTATED_TARGET_TYPE=tag "$publisher" 1.2.3 1 "$GH_TARGET_SHA" >/dev/null 2>&1; then
|
||||
echo "publisher accepted a nested annotated tag" >&2
|
||||
exit 1
|
||||
fi
|
||||
if GH_ANNOTATED_TARGET_SHA=3333333333333333333333333333333333333333 \
|
||||
"$publisher" 1.2.3 1 "$GH_TARGET_SHA" >/dev/null 2>&1; then
|
||||
echo "publisher accepted an annotated tag on the wrong commit" >&2
|
||||
exit 1
|
||||
fi
|
||||
if "$publisher" 01.2.3 1 "$GH_TARGET_SHA" >/dev/null 2>&1; then
|
||||
echo "publisher accepted a marketing version with a leading zero" >&2
|
||||
exit 1
|
||||
fi
|
||||
if "$publisher" 1.2.3 01 "$GH_TARGET_SHA" >/dev/null 2>&1; then
|
||||
echo "publisher accepted a candidate number with a leading zero" >&2
|
||||
exit 1
|
||||
fi
|
||||
if GH_EXISTING_REFS=$'refs/tags/mobile-v1.2.3-rc.1\nrefs/tags/mobile-v1.2.3-rc.7\nrefs/tags/mobile-v1.2.3-rc.08\nrefs/tags/mobile-v1.2.4-rc.99\n' \
|
||||
"$publisher" 1.2.3 8 "$GH_TARGET_SHA" >/dev/null; then
|
||||
:
|
||||
else
|
||||
echo "publisher did not sequence from the highest exact candidate" >&2
|
||||
exit 1
|
||||
fi
|
||||
if GITHUB_REPOSITORY=attacker/buzz "$publisher" 1.2.3 1 "$GH_TARGET_SHA" >/dev/null 2>&1; then
|
||||
echo "publisher accepted the wrong repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "mobile release candidate publisher contract passed"
|
||||
301
scripts/test-mobile-release-contract.sh
Executable file
301
scripts/test-mobile-release-contract.sh
Executable file
@@ -0,0 +1,301 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
script="$repo_root/scripts/mobile-release.sh"
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
remote="$tmp/remote.git"
|
||||
work="$tmp/work"
|
||||
operator="$tmp/operator"
|
||||
bin="$tmp/bin"
|
||||
canonical_origin="git@github.com:block/buzz.git"
|
||||
mkdir -p "$bin"
|
||||
|
||||
cat > "$bin/gh" <<'GH'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
case "${1:-}:${2:-}" in
|
||||
--version:*) printf 'gh version %s (test)\n' "${GH_VERSION:-2.94.0}" ;;
|
||||
api:repos/block/buzz/rulesets/14378754)
|
||||
case "$*" in
|
||||
*'.enforcement'*) printf '%s\n' "${GH_TAG_RULESET_STATE:-active}" ;;
|
||||
*'.current_user_can_bypass'*) printf '%s\n' "${GH_CURRENT_USER_CAN_BYPASS-always}" ;;
|
||||
*'[.rules[].type]'*) printf '%s\n' "${GH_TAG_RULE_TYPES:-creation,deletion,non_fast_forward,update}" ;;
|
||||
*'.conditions.ref_name.include[]'*) printf '%s\n' "${GH_TAG_INCLUDES:-refs/tags/mobile-v*}" ;;
|
||||
*'.conditions.ref_name.exclude[]'*) printf '%s\n' "${GH_TAG_EXCLUDES:-}" ;;
|
||||
*) exit 2 ;;
|
||||
esac
|
||||
;;
|
||||
workflow:run)
|
||||
if [[ "${GH_WORKFLOW_DISPATCH_FAIL:-}" == "1" ]]; then
|
||||
printf '%s\n' "${GH_WORKFLOW_DISPATCH_ERROR:-dispatch failed}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${GH_WORKFLOW_WRONG_URL:-}" == "1" ]]; then
|
||||
printf '%s\n' 'https://github.com/attacker/buzz/actions/runs/999'
|
||||
exit 0
|
||||
fi
|
||||
if [[ "${GH_WORKFLOW_EXTRA_URL:-}" == "1" ]]; then
|
||||
printf '%s\n' 'https://github.com/block/buzz/actions/runs/998'
|
||||
fi
|
||||
version=""
|
||||
number=""
|
||||
sha=""
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case "$1" in
|
||||
version=*) version="${1#version=}" ;;
|
||||
candidate_number=*) number="${1#candidate_number=}" ;;
|
||||
target_sha=*) sha="${1#target_sha=}" ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
[[ -n "$version" && -n "$number" && -n "$sha" ]]
|
||||
printf '%s\t%s\t%s\n' "$version" "$number" "$sha" >> "$GH_WORKFLOW_CAPTURE"
|
||||
if [[ "${GH_WORKFLOW_NO_URL:-}" == "1" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
printf 'https://github.com/block/buzz/actions/runs/%s\n' "$number"
|
||||
;;
|
||||
run:watch)
|
||||
[[ "${GH_WORKFLOW_FAIL:-}" != "1" ]] || exit 1
|
||||
number="$3"
|
||||
IFS=$'\t' read -r version expected sha < <(tail -n 1 "$GH_WORKFLOW_CAPTURE")
|
||||
[[ "$number" == "$expected" ]]
|
||||
if [[ "${GH_MAIN_MOVE_DURING_WORKFLOW:-}" == "1" ]]; then
|
||||
printf '%s\n' moved >> "$GH_WORKTREE/file"
|
||||
git -C "$GH_WORKTREE" commit -qam moved-during-publication
|
||||
git -C "$GH_WORKTREE" push -q origin main
|
||||
fi
|
||||
if [[ "${GH_TAG_VERIFY_LIGHTWEIGHT:-}" == "1" ]]; then
|
||||
git -C "$GH_WORKTREE" -c tag.gpgSign=false tag \
|
||||
"mobile-v${version}-rc.${expected}" "$sha"
|
||||
else
|
||||
git -C "$GH_WORKTREE" -c tag.gpgSign=false tag -a \
|
||||
-m "Buzz Mobile $version release candidate $expected" \
|
||||
"mobile-v${version}-rc.${expected}" "$sha"
|
||||
fi
|
||||
git -C "$GH_WORKTREE" -c core.hooksPath=/dev/null push -q \
|
||||
origin "refs/tags/mobile-v${version}-rc.${expected}"
|
||||
;;
|
||||
*) exit 2 ;;
|
||||
esac
|
||||
GH
|
||||
chmod +x "$bin/gh"
|
||||
|
||||
export PATH="$bin:$PATH"
|
||||
export GH_WORKFLOW_CAPTURE="$tmp/workflow-dispatches"
|
||||
export GH_WORKTREE="$work"
|
||||
|
||||
run_release() {
|
||||
local repo="$1"
|
||||
shift
|
||||
(
|
||||
cd "$repo"
|
||||
git config "url.file://$remote.insteadOf" "$canonical_origin"
|
||||
git config protocol.file.allow always
|
||||
"$script" "$@"
|
||||
)
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
assert_no_removed_mobile_release_behavior() {
|
||||
local status
|
||||
|
||||
grep -Eq 'gh[[:space:]]+release|mobile-release/|finalize' "$@" && \
|
||||
fail "removed branch/finalization/GitHub Release behavior remains"
|
||||
status="$?"
|
||||
[[ "$status" -eq 1 ]] || \
|
||||
fail "could not scan for removed branch/finalization/GitHub Release behavior"
|
||||
}
|
||||
|
||||
git init -q --bare "$remote"
|
||||
git init -q "$work"
|
||||
git -C "$work" config user.name test
|
||||
git -C "$work" config user.email test@example.com
|
||||
git -C "$work" remote add origin "$canonical_origin"
|
||||
git -C "$work" config "url.file://$remote.insteadOf" "$canonical_origin"
|
||||
git -C "$work" config protocol.file.allow always
|
||||
echo first > "$work/file"
|
||||
git -C "$work" add file
|
||||
git -C "$work" commit -qm first
|
||||
git -C "$work" branch -M main
|
||||
git --git-dir="$remote" symbolic-ref HEAD refs/heads/main
|
||||
git -C "$work" push -q -u origin main
|
||||
|
||||
# Candidate publication must work from a stale operator clone, warn about the
|
||||
# stale checkout, and target the exact current remote main commit.
|
||||
git -c "url.file://$remote.insteadOf=$canonical_origin" \
|
||||
-c protocol.file.allow=always clone -q "$canonical_origin" "$operator"
|
||||
git -C "$operator" config user.name test
|
||||
git -C "$operator" config user.email test@example.com
|
||||
echo remote-only >> "$work/file"
|
||||
git -C "$work" commit -qam remote-only
|
||||
git -C "$work" push -q origin main
|
||||
remote_main_sha="$(git --git-dir="$remote" rev-parse refs/heads/main)"
|
||||
if git -C "$operator" cat-file -e "$remote_main_sha^{commit}" 2>/dev/null; then
|
||||
fail "stale-clone fixture already contains the remote-only commit"
|
||||
fi
|
||||
run_release "$operator" candidate 1.2.3 > "$tmp/stale-output" 2> "$tmp/stale-error"
|
||||
grep -Fq "Note: local HEAD is $(git -C "$operator" rev-parse HEAD); candidate source is current origin/main $remote_main_sha." \
|
||||
"$tmp/stale-error"
|
||||
cat "$tmp/stale-output"
|
||||
[[ "$(git --git-dir="$remote" rev-parse 'refs/tags/mobile-v1.2.3-rc.1^{commit}')" == \
|
||||
"$remote_main_sha" ]]
|
||||
[[ "$(git --git-dir="$remote" cat-file -t refs/tags/mobile-v1.2.3-rc.1)" == tag ]]
|
||||
grep -Fq $'1.2.3\t1\t' "$GH_WORKFLOW_CAPTURE"
|
||||
|
||||
# Existing remote identities remain unchanged. Later candidates sequence
|
||||
# monotonically and target the then-current remote main commit.
|
||||
rc1_tag_oid="$(git --git-dir="$remote" rev-parse refs/tags/mobile-v1.2.3-rc.1)"
|
||||
echo newer >> "$work/file"
|
||||
git -C "$work" commit -qam newer
|
||||
git -C "$work" push -q origin main
|
||||
new_main_sha="$(git --git-dir="$remote" rev-parse refs/heads/main)"
|
||||
run_release "$operator" candidate 1.2.3
|
||||
[[ "$(git --git-dir="$remote" rev-parse refs/tags/mobile-v1.2.3-rc.1)" == "$rc1_tag_oid" ]]
|
||||
[[ "$(git --git-dir="$remote" rev-parse 'refs/tags/mobile-v1.2.3-rc.2^{commit}')" == \
|
||||
"$new_main_sha" ]]
|
||||
[[ "$(git --git-dir="$remote" cat-file -t refs/tags/mobile-v1.2.3-rc.2)" == tag ]]
|
||||
grep -Fq $'1.2.3\t2\t' "$GH_WORKFLOW_CAPTURE"
|
||||
|
||||
# Sequence from the highest exact remote RC even if there are gaps, and ignore
|
||||
# malformed or other-version tags.
|
||||
git -C "$work" -c tag.gpgSign=false tag -a -m gap mobile-v1.2.3-rc.7 "$new_main_sha"
|
||||
git -C "$work" -c tag.gpgSign=false tag -a -m malformed mobile-v1.2.3-rc.08 "$new_main_sha"
|
||||
git -C "$work" -c tag.gpgSign=false tag -a -m other mobile-v1.2.4-rc.99 "$new_main_sha"
|
||||
git -C "$work" push -q origin \
|
||||
refs/tags/mobile-v1.2.3-rc.7 refs/tags/mobile-v1.2.3-rc.08 \
|
||||
refs/tags/mobile-v1.2.4-rc.99
|
||||
run_release "$operator" candidate 1.2.3
|
||||
[[ "$(git --git-dir="$remote" rev-parse 'refs/tags/mobile-v1.2.3-rc.8^{commit}')" == \
|
||||
"$new_main_sha" ]]
|
||||
|
||||
# Failed or unattributable App-backed publication fails closed without creating
|
||||
# the expected candidate tag.
|
||||
if GH_WORKFLOW_DISPATCH_FAIL=1 run_release "$operator" candidate 9.9.7 >/dev/null 2>&1; then
|
||||
fail "candidate succeeded despite a rejected workflow dispatch"
|
||||
fi
|
||||
if git --git-dir="$remote" show-ref --verify --quiet refs/tags/mobile-v9.9.7-rc.1; then
|
||||
fail "rejected workflow dispatch created a candidate tag"
|
||||
fi
|
||||
if GH_WORKFLOW_DISPATCH_FAIL=1 \
|
||||
GH_WORKFLOW_DISPATCH_ERROR="does not have 'workflow_dispatch' trigger" \
|
||||
run_release "$operator" candidate 9.9.6 > "$tmp/missing-workflow-output" 2>&1; then
|
||||
fail "candidate succeeded without the publication workflow on main"
|
||||
fi
|
||||
grep -Fq 'merge the release-process change before publishing a candidate' \
|
||||
"$tmp/missing-workflow-output"
|
||||
if git --git-dir="$remote" show-ref --verify --quiet refs/tags/mobile-v9.9.6-rc.1; then
|
||||
fail "missing publication workflow created a candidate tag"
|
||||
fi
|
||||
if GH_WORKFLOW_FAIL=1 run_release "$operator" candidate 9.9.9 >/dev/null 2>&1; then
|
||||
fail "candidate succeeded despite a failed App-backed workflow"
|
||||
fi
|
||||
if git --git-dir="$remote" show-ref --verify --quiet refs/tags/mobile-v9.9.9-rc.1; then
|
||||
fail "failed App-backed workflow created a candidate tag"
|
||||
fi
|
||||
if GH_WORKFLOW_NO_URL=1 run_release "$operator" candidate 9.9.8 >/dev/null 2>&1; then
|
||||
fail "candidate succeeded without a workflow run URL"
|
||||
fi
|
||||
if git --git-dir="$remote" show-ref --verify --quiet refs/tags/mobile-v9.9.8-rc.1; then
|
||||
fail "URL-less dispatch created a candidate tag"
|
||||
fi
|
||||
if GH_WORKFLOW_WRONG_URL=1 run_release "$operator" candidate 9.9.5 >/dev/null 2>&1; then
|
||||
fail "candidate accepted a workflow run URL from another repository"
|
||||
fi
|
||||
if git --git-dir="$remote" show-ref --verify --quiet refs/tags/mobile-v9.9.5-rc.1; then
|
||||
fail "wrong-repository workflow URL created a candidate tag"
|
||||
fi
|
||||
if GH_WORKFLOW_EXTRA_URL=1 run_release "$operator" candidate 9.9.4 >/dev/null 2>&1; then
|
||||
fail "candidate accepted multiple workflow run URLs"
|
||||
fi
|
||||
if git --git-dir="$remote" show-ref --verify --quiet refs/tags/mobile-v9.9.4-rc.1; then
|
||||
fail "ambiguous workflow URLs created a candidate tag"
|
||||
fi
|
||||
if GH_TAG_VERIFY_LIGHTWEIGHT=1 run_release "$operator" candidate 9.9.2 >/dev/null 2>&1; then
|
||||
fail "candidate accepted a lightweight published tag"
|
||||
fi
|
||||
[[ "$(git --git-dir="$remote" cat-file -t refs/tags/mobile-v9.9.2-rc.1)" == commit ]]
|
||||
|
||||
# The publisher and operator both reject a main-tip race. The immutable tag may
|
||||
# already exist at the prior tip, but the operator must not report it as a
|
||||
# current-main candidate.
|
||||
pre_race_main_sha="$(git --git-dir="$remote" rev-parse refs/heads/main)"
|
||||
if GH_MAIN_MOVE_DURING_WORKFLOW=1 run_release "$operator" candidate 9.9.3 > "$tmp/main-race-output" 2>&1; then
|
||||
fail "candidate succeeded after main moved during publication"
|
||||
fi
|
||||
post_race_main_sha="$(git --git-dir="$remote" rev-parse refs/heads/main)"
|
||||
[[ "$pre_race_main_sha" != "$post_race_main_sha" ]]
|
||||
[[ "$(git --git-dir="$remote" rev-parse 'refs/tags/mobile-v9.9.3-rc.1^{commit}')" == \
|
||||
"$pre_race_main_sha" ]]
|
||||
grep -Fq "origin/main moved from requested commit $pre_race_main_sha to $post_race_main_sha during publication" \
|
||||
"$tmp/main-race-output"
|
||||
|
||||
# Publishing through a fork is rejected and unsupported gh versions fail before
|
||||
# dispatching any candidate publication.
|
||||
fork_operator="$tmp/fork-operator"
|
||||
git clone -q "$remote" "$fork_operator"
|
||||
git -C "$fork_operator" config user.name test
|
||||
git -C "$fork_operator" config user.email test@example.com
|
||||
if (cd "$fork_operator" && "$script" candidate 2.0.0 >/dev/null 2>&1); then
|
||||
fail "noncanonical origin was accepted"
|
||||
fi
|
||||
before_dispatches="$(wc -l < "$GH_WORKFLOW_CAPTURE")"
|
||||
if GH_VERSION=2.86.0 run_release "$operator" candidate 2.0.0 >/dev/null 2>&1; then
|
||||
fail "gh older than 2.87.0 was accepted"
|
||||
fi
|
||||
if GH_VERSION=2.9.0 run_release "$operator" candidate 2.0.0 >/dev/null 2>&1; then
|
||||
fail "numeric gh version comparison accepted 2.9.0 as at least 2.87.0"
|
||||
fi
|
||||
[[ "$(wc -l < "$GH_WORKFLOW_CAPTURE")" == "$before_dispatches" ]]
|
||||
|
||||
# Dirty trees and invalid marketing versions fail before publication.
|
||||
echo dirty > "$operator/untracked"
|
||||
if run_release "$operator" candidate 2.0.0 >/dev/null 2>&1; then
|
||||
fail "dirty operator tree was accepted"
|
||||
fi
|
||||
rm "$operator/untracked"
|
||||
if run_release "$operator" candidate 1.2 >/dev/null 2>&1; then
|
||||
fail "invalid marketing version was accepted"
|
||||
fi
|
||||
if run_release "$operator" candidate 01.2.3 >/dev/null 2>&1; then
|
||||
fail "marketing version with a leading zero was accepted"
|
||||
fi
|
||||
|
||||
# Mobile no longer has release branches, finalization, a stable alias, or a
|
||||
# GitHub Release call. Publication remains App-backed because the strict tag
|
||||
# ruleset denies direct human creation.
|
||||
if run_release "$operator" start 2.0.0 >/dev/null 2>&1; then
|
||||
fail "removed start command was accepted"
|
||||
fi
|
||||
if run_release "$operator" finalize 1.2.3-rc.2 >/dev/null 2>&1; then
|
||||
fail "removed finalize command was accepted"
|
||||
fi
|
||||
if git --git-dir="$remote" for-each-ref --format='%(refname)' refs/heads/mobile-release/ | grep -q .; then
|
||||
fail "mobile release branch was created"
|
||||
fi
|
||||
if git --git-dir="$remote" show-ref --verify --quiet refs/tags/mobile-v1.2.3; then
|
||||
fail "stable mobile tag alias was created"
|
||||
fi
|
||||
assert_no_removed_mobile_release_behavior \
|
||||
"$repo_root/scripts/mobile-release.sh" \
|
||||
"$repo_root/scripts/publish-mobile-release-candidate.sh" \
|
||||
"$repo_root/.github/workflows/mobile-release-candidate.yml"
|
||||
# Prove the negative contract itself is discriminating rather than merely
|
||||
# accepting a missing or broken search tool as "not found."
|
||||
printf '%s\n' 'gh release create forbidden' > "$tmp/forbidden-mobile-release-behavior"
|
||||
if (assert_no_removed_mobile_release_behavior "$tmp/forbidden-mobile-release-behavior") \
|
||||
>/dev/null 2>&1; then
|
||||
fail "removed-behavior assertion did not reject a forbidden GitHub Release call"
|
||||
fi
|
||||
grep -Fq 'version: 0.0.0+1' "$repo_root/mobile/pubspec.yaml"
|
||||
if grep -qE 'release-mobile|bump-mobile-version|get-current-mobile-version' "$repo_root/Justfile"; then
|
||||
fail "metadata-only mobile release recipe remains in Justfile"
|
||||
fi
|
||||
|
||||
echo "mobile release contract passed"
|
||||
@@ -12,7 +12,7 @@ git -C "$tmp" config user.email test@example.com
|
||||
echo first >"$tmp/file"
|
||||
git -C "$tmp" add file
|
||||
git -C "$tmp" commit -qm first
|
||||
git -C "$tmp" tag v1.2.3
|
||||
git -C "$tmp" tag -m "desktop release" v1.2.3
|
||||
|
||||
(
|
||||
cd "$tmp"
|
||||
@@ -37,7 +37,7 @@ if (
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git -C "$tmp" tag relay-v2.0.0
|
||||
git -C "$tmp" tag -m "relay release" relay-v2.0.0
|
||||
(
|
||||
cd "$tmp"
|
||||
GITHUB_REF=refs/tags/relay-v2.0.0 "$verify" relay-v 2.0.0
|
||||
|
||||
Reference in New Issue
Block a user