Merge pinned main into invite revocation candidate

Signed-off-by: seiler <seiler@squareup.com>
This commit is contained in:
seiler
2026-10-19 22:30:54 +00:00
283 changed files with 26270 additions and 2363 deletions

View File

@@ -217,6 +217,11 @@ const scenarios = [
["mobile"],
],
["mobile lockfile", ["mobile/pubspec.lock"], ["mobile"]],
[
"mobile release source helper",
["scripts/mobile-release-source.sh"],
["mobile"],
],
["mobile release script", ["scripts/mobile-release.sh"], ["mobile"]],
[
"mobile gateway recipe tests",

View File

@@ -0,0 +1,58 @@
#!/usr/bin/env bash
# Validate an explicitly selected stable branch against the marketing version.
# main remains the default for releases without a stabilization branch.
require_mobile_release_source() {
local version="$1" branch="$2"
if [[ "$branch" == main ]]; then
return 0
fi
if [[ "$branch" != "mobile-v${version%.*}" ]]; then
echo "Source branch must be main or mobile-v${version%.*} for version $version" >&2
return 1
fi
}
# The repository administrator must configure this ruleset without bypass actors.
# GitHub hides that list from non-admin tokens; check the caller's effective
# bypass plus every public policy field on each publication.
require_mobile_release_branch_ruleset() {
local branch="$1" valid
[[ "$branch" != main ]] || return 0
valid="$(gh api repos/block/buzz/rulesets/19321162 --jq '
.target == "branch" and .enforcement == "active"
and .current_user_can_bypass == "never"
and (.conditions.ref_name.include | index("refs/heads/mobile-v*") != null)
and (.conditions.ref_name.exclude == [])
and ((has("bypass_actors") | not) or .bypass_actors == [])
and any(.rules[]; .type == "deletion")
and any(.rules[]; .type == "non_fast_forward")
and any(.rules[]; .type == "pull_request"
and .parameters.required_approving_review_count >= 1
and .parameters.dismiss_stale_reviews_on_push == true
and .parameters.require_last_push_approval == true
and .parameters.required_review_thread_resolution == true)
and any(.rules[]; .type == "required_status_checks"
and .parameters.strict_required_status_checks_policy == true
and ([.parameters.required_status_checks[].context] | index("Mobile") != null)
and ([.parameters.required_status_checks[].context] | index("DCO Check") != null))
')" || { echo "Could not verify mobile stable-branch protection" >&2; return 1; }
[[ "$valid" == true ]] || {
echo "Mobile stable-branch ruleset 19321162 must enforce review and checks on mobile-v* without bypass" >&2
return 1
}
}
# Compare immutable SHAs, not a mutable branch name. The selected branch may
# advance after this snapshot without changing the authorized candidate source.
require_mobile_release_commit_ancestor() {
local target_sha="$1" source_sha="$2" merge_base
merge_base="$(gh api "repos/block/buzz/compare/$target_sha...$source_sha" --jq .merge_base_commit.sha)" || {
echo "Could not verify candidate source ancestry" >&2
return 1
}
[[ "$merge_base" == "$target_sha" ]] || {
echo "Candidate commit $target_sha is not in selected source history at $source_sha" >&2
return 1
}
}

View File

@@ -4,10 +4,10 @@ set -euo pipefail
usage() {
cat >&2 <<'USAGE'
usage:
scripts/mobile-release.sh candidate X.Y.Z
scripts/mobile-release.sh candidate X.Y.Z [--source-branch mobile-vX.Y] [--commit FULL_SHA]
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.
selected commit in source history (defaults to the current source tip).
USAGE
exit 2
}
@@ -19,6 +19,8 @@ fail() {
# shellcheck source=scripts/release-rulesets.sh
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/release-rulesets.sh"
# shellcheck source=scripts/mobile-release-source.sh
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/mobile-release-source.sh"
require_gh_minimum_version() {
local minimum="2.87.0" version
@@ -91,33 +93,56 @@ remote_tag_commit_sha() {
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"
remote_source_commit_sha() {
local ref="refs/heads/$MOBILE_RELEASE_SOURCE_BRANCH" fetched_oid commit
# Fetch a single snapshot. A later branch advance does not invalidate it.
git fetch -q --no-tags origin "$ref" || return 1
fetched_oid="$(git rev-parse --verify FETCH_HEAD)" || return 1
commit="$(git rev-parse --verify 'FETCH_HEAD^{commit}')" || return 1
[[ "$commit" == "$advertised_oid" ]] || \
fail "origin/main did not resolve directly to a commit"
[[ "$commit" == "$fetched_oid" ]] || \
fail "origin/$MOBILE_RELEASE_SOURCE_BRANCH did not resolve directly to a commit"
printf '%s' "$commit"
}
command="${1:-}"
case "$command" in
candidate)
[[ "$#" -eq 2 ]] || usage
[[ "$#" -ge 2 ]] || usage
version="$2"
shift 2
MOBILE_RELEASE_SOURCE_BRANCH=main
requested_sha=""
source_option_seen=false
commit_option_seen=false
while [[ "$#" -gt 0 ]]; do
[[ "$#" -ge 2 ]] || usage
case "$1" in
--source-branch)
[[ "$source_option_seen" == false ]] || usage
source_option_seen=true
MOBILE_RELEASE_SOURCE_BRANCH="$2"
;;
--commit)
[[ "$commit_option_seen" == false ]] || usage
commit_option_seen=true
requested_sha="$2"
[[ "$requested_sha" =~ ^[0-9a-f]{40}$ ]] || fail "--commit requires a full commit SHA"
;;
*) usage ;;
esac
shift 2
done
require_clean_semver "$version"
require_mobile_release_source "$version" "$MOBILE_RELEASE_SOURCE_BRANCH" || exit 1
require_clean_tree
require_canonical_repository || exit 1
require_gh_minimum_version
require_mobile_release_branch_ruleset "$MOBILE_RELEASE_SOURCE_BRANCH" || exit 1
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"
source_tip="$(remote_source_commit_sha)" || fail "origin/$MOBILE_RELEASE_SOURCE_BRANCH does not exist"
source_sha="${requested_sha:-$source_tip}"
require_mobile_release_commit_ancestor "$source_sha" "$source_tip" || exit 1
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"
@@ -135,7 +160,8 @@ case "$command" in
--ref main \
-f "version=$version" \
-f "candidate_number=$next" \
-f "target_sha=$main_sha" 2>&1)"; then
-f "source_branch=$MOBILE_RELEASE_SOURCE_BRANCH" \
-f "target_sha=$source_sha" 2>&1)"; then
:
else
if [[ "$dispatch_output" == *"does not have 'workflow_dispatch' trigger"* ]]; then
@@ -151,18 +177,15 @@ case "$command" in
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
[[ "$published_sha" == "$source_sha" ]] || \
fail "$tag resolved to $published_sha instead of requested commit $source_sha"
if [[ "$local_head_sha" != "$source_sha" ]]; then
echo "Note: local HEAD is $local_head_sha; candidate source is pinned from origin/$MOBILE_RELEASE_SOURCE_BRANCH $source_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"
printf 'Published %s at pinned origin/%s commit %s through buzz-release-bot. Use this exact tag in Release Mobile.\n' \
"$tag" "$MOBILE_RELEASE_SOURCE_BRANCH" "$source_sha"
;;
*) usage ;;

View File

@@ -8,11 +8,14 @@ fail() {
# shellcheck source=scripts/release-rulesets.sh
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/release-rulesets.sh"
# shellcheck source=scripts/mobile-release-source.sh
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/mobile-release-source.sh"
[[ "$#" -eq 3 ]] || fail "usage: $0 X.Y.Z N COMMIT_SHA"
[[ "$#" -eq 4 ]] || fail "usage: $0 X.Y.Z N COMMIT_SHA SOURCE_BRANCH"
version="$1"
candidate_number="$2"
target_sha="$3"
MOBILE_RELEASE_SOURCE_BRANCH="$4"
repo="${GITHUB_REPOSITORY:-}"
[[ "$repo" == "block/buzz" ]] || fail "candidate publishing is restricted to block/buzz"
@@ -21,17 +24,19 @@ repo="${GITHUB_REPOSITORY:-}"
[[ "$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"
require_mobile_release_source "$version" "$MOBILE_RELEASE_SOURCE_BRANCH" || exit 1
command -v gh >/dev/null 2>&1 || fail "gh is required"
require_release_tag_ruleset || exit 1
require_mobile_release_branch_ruleset "$MOBILE_RELEASE_SOURCE_BRANCH" || 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"
source_sha="$(gh api "repos/$repo/git/ref/heads/$MOBILE_RELEASE_SOURCE_BRANCH" --jq .object.sha)" || \
fail "could not resolve $repo $MOBILE_RELEASE_SOURCE_BRANCH"
[[ "$source_sha" =~ ^[0-9a-f]{40}$ ]] || fail "GitHub returned an invalid source commit 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"
require_mobile_release_commit_ancestor "$target_sha" "$source_sha" || exit 1
refs="$(
gh api --paginate "repos/$repo/git/matching-refs/tags/mobile-v${version}-rc." --jq '.[].ref'

View File

@@ -248,6 +248,9 @@ run_unit_tests() {
run_test_step "buzz-relay NIP-FI config tests" \
cargo test -p buzz-relay --lib nip_fi_config:: -- --nocapture
run_test_step "buzz-relay NIP-FI resolver tests" \
cargo test -p buzz-relay --lib nip_fi_resolver:: -- --nocapture
# Infra-free REQ subscription-lifecycle tests, by exact name: the rest of
# handlers::req needs a database.
run_test_step "buzz-relay REQ subscription lifecycle tests" \
@@ -340,6 +343,17 @@ run_unit_tests() {
connection::tests::expiry_notice_queued_on_ctrl_before_cancel
connection::tests::f3_root_outer_wrapper_delivers_denial_on_bootstrap_cancellation
connection::tests::f3_root_pre_built_expired_gate_terminates_connection
connection::terminal_close_delivery_tests::key_pairing_mismatch_closes_with_1008_on_real_socket
connection::terminal_close_delivery_tests::session_expiry_denial_closes_with_1008_on_real_socket
connection::terminal_close_delivery_tests::server_holds_socket_for_client_close_reply
connection::terminal_close_delivery_tests::server_finishes_promptly_when_client_answers_close
connection::terminal_close_delivery_tests::stalled_cleanup_does_not_hold_socket_open
connection::terminal_close_delivery_tests::bootstrap_drain_holds_socket_for_client_close_reply
connection::terminal_close_delivery_tests::stalled_expiry_barrier_does_not_hold_socket_open
connection::terminal_close_delivery_tests::blocked_frame_handler_does_not_hold_socket_open
connection::terminal_close_delivery_tests::refused_bootstrap_holds_connection_slot_until_socket_ends
connection::terminal_close_delivery_tests::stalled_bootstrap_holds_connection_slot
connection::terminal_close_delivery_tests::peer_wait_replies_to_client_initiated_close
handlers::auth::tests::b2_pre_cancelled_connection_never_becomes_authenticated
handlers::auth::tests::fi_ban_check_error_emits_terminal_authorization_unavailable
handlers::auth::tests::fi_root_authorization_denied_rows_emit_identical_frames

View File

@@ -17,6 +17,11 @@ record() {
}
case "${1:-}:${2:-}" in
api:repos/block/buzz/rulesets/19321162)
[[ "${GH_BRANCH_POLICY_FAIL:-}" != 1 ]] || exit 1
policy='{"target":"branch","enforcement":"active","current_user_can_bypass":"never","conditions":{"ref_name":{"include":["refs/heads/mobile-v*"],"exclude":[]}},"bypass_actors":[],"rules":[{"type":"deletion"},{"type":"non_fast_forward"},{"type":"pull_request","parameters":{"required_approving_review_count":1,"dismiss_stale_reviews_on_push":true,"require_last_push_approval":true,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"strict_required_status_checks_policy":true,"required_status_checks":[{"context":"Mobile"},{"context":"DCO Check"}]}}]}'
printf '%s\n' "${GH_BRANCH_POLICY:-$policy}" | jq -r "$4"
;;
api:repos/block/buzz/rulesets/14378754)
case "$*" in
*'.enforcement'*) printf '%s\n' "${GH_TAG_RULESET_STATE:-active}" ;;
@@ -28,7 +33,24 @@ case "${1:-}:${2:-}" in
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:repos/block/buzz/git/ref/heads/mobile-v1.2) printf '%s\n' "$GH_STABLE_SHA" ;;
api:repos/block/buzz/git/ref/heads/*) printf '%s\n' "$GH_MISMATCH_SHA" ;;
api:repos/block/buzz/compare/*)
[[ "${GH_COMPARE_FAIL:-}" != 1 ]] || exit 1
pair="${2##*/}"
base="${pair%%...*}"; tip="${pair##*...}"
if [[ -n "${GH_EXPECT_COMPARE:-}" ]]; then
[[ "$pair" == "$GH_EXPECT_COMPARE" ]] || exit 1
fi
if [[ -n "${GH_COMPARE_BASE_SHA:-}" ]]; then
printf '%s\n' "$GH_COMPARE_BASE_SHA"
elif [[ "$base" == "$tip" ]]; then
printf '%s\n' "$base"
else
printf '%s\n' 9999999999999999999999999999999999999999
fi
;;
api:repos/block/buzz/commits/*) printf '%s\n' "${2##*/}" ;;
api:--paginate)
[[ "$3" == "repos/block/buzz/git/matching-refs/tags/mobile-v1.2.3-rc." ]]
printf '%s' "${GH_EXISTING_REFS:-}"
@@ -72,9 +94,11 @@ export PATH="$bin:$PATH"
export GH_CALLS="$tmp/calls"
export GITHUB_REPOSITORY=block/buzz
export GH_TARGET_SHA=1111111111111111111111111111111111111111
export GH_STABLE_SHA=4444444444444444444444444444444444444444
export GH_MISMATCH_SHA=5555555555555555555555555555555555555555
export GH_TAG_OBJECT_SHA=2222222222222222222222222222222222222222
"$publisher" 1.2.3 1 "$GH_TARGET_SHA"
"$publisher" 1.2.3 1 "$GH_TARGET_SHA" main
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"
@@ -82,76 +106,161 @@ 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
if GH_CURRENT_USER_CAN_BYPASS=never "$publisher" 1.2.3 1 "$GH_TARGET_SHA" main >/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
if GH_CURRENT_USER_CAN_BYPASS='' "$publisher" 1.2.3 1 "$GH_TARGET_SHA" main >/dev/null 2>&1; then
echo "publisher accepted a ruleset response without an effective bypass" >&2
exit 1
fi
if GH_TAG_RULESET_STATE=disabled "$publisher" 1.2.3 1 "$GH_TARGET_SHA" >/dev/null 2>&1; then
if GH_TAG_RULESET_STATE=disabled "$publisher" 1.2.3 1 "$GH_TARGET_SHA" main >/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
if GH_TAG_RULE_TYPES=creation "$publisher" 1.2.3 1 "$GH_TARGET_SHA" main >/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
if GH_TAG_INCLUDES=refs/tags/v\* "$publisher" 1.2.3 1 "$GH_TARGET_SHA" main >/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
if GH_TAG_EXCLUDES=refs/tags/mobile-v0.0.0 "$publisher" 1.2.3 1 "$GH_TARGET_SHA" main >/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
GH_ANNOTATED_TARGET_SHA=1111111111111111111111111111111111111111 \
"$publisher" 1.2.3 1 1111111111111111111111111111111111111111 main >/dev/null 2>&1; then
echo "publisher accepted a commit outside main history" >&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
"$publisher" 1.2.3 1 "$GH_TARGET_SHA" main >/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
if GH_PUBLISHED_REF_TYPE=commit "$publisher" 1.2.3 1 "$GH_TARGET_SHA" main >/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
"$publisher" 1.2.3 1 "$GH_TARGET_SHA" main >/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
if GH_ANNOTATED_TARGET_TYPE=tag "$publisher" 1.2.3 1 "$GH_TARGET_SHA" main >/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
"$publisher" 1.2.3 1 "$GH_TARGET_SHA" main >/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
if "$publisher" 01.2.3 1 "$GH_TARGET_SHA" main >/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
if "$publisher" 1.2.3 01 "$GH_TARGET_SHA" main >/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
"$publisher" 1.2.3 8 "$GH_TARGET_SHA" main >/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
if GITHUB_REPOSITORY=attacker/buzz "$publisher" 1.2.3 1 "$GH_TARGET_SHA" main >/dev/null 2>&1; then
echo "publisher accepted the wrong repository" >&2
exit 1
fi
GH_ANNOTATED_TARGET_SHA="$GH_STABLE_SHA" "$publisher" 1.2.3 1 "$GH_STABLE_SHA" mobile-v1.2
# Old commits are accepted only when the immutable compare proves ancestry.
old_sha=6666666666666666666666666666666666666666
for source in main mobile-v1.2; do
tip="$GH_TARGET_SHA"
[[ "$source" != mobile-v1.2 ]] || tip="$GH_STABLE_SHA"
GH_COMPARE_BASE_SHA="$old_sha" GH_EXPECT_COMPARE="$old_sha...$tip" GH_ANNOTATED_TARGET_SHA="$old_sha" \
"$publisher" 1.2.3 1 "$old_sha" "$source"
before_calls="$(wc -l < "$GH_CALLS")"
for mode in unrelated unavailable; do
if GH_COMPARE_FAIL="$([[ "$mode" == unavailable ]] && echo 1 || echo 0)" \
GH_COMPARE_BASE_SHA="$GH_MISMATCH_SHA" GH_ANNOTATED_TARGET_SHA="$old_sha" \
"$publisher" 1.2.3 1 "$old_sha" "$source" >/dev/null 2>&1; then
echo "publisher accepted $mode ancestry for $source" >&2
exit 1
fi
done
[[ "$(wc -l < "$GH_CALLS")" == "$before_calls" ]]
done
for source in mobile-v1.3 mobile-v2.2 mobile-v01.2 arbitrary ''; do
if GH_ANNOTATED_TARGET_SHA="$GH_MISMATCH_SHA" "$publisher" 1.2.3 1 "$GH_MISMATCH_SHA" "$source" >/dev/null 2>&1; then
echo "publisher accepted mismatched source '$source'" >&2
exit 1
fi
done
# Mutate actual policy JSON so these tests exercise the production jq predicate.
python3 - "$publisher" <<'PYPOLICY'
import copy, json, os, subprocess, sys
policy = {'target': 'branch', 'enforcement': 'active', 'current_user_can_bypass': 'never', 'conditions': {'ref_name': {'include': ['refs/heads/mobile-v*'], 'exclude': []}}, 'bypass_actors': [], 'rules': [{'type': 'deletion'}, {'type': 'non_fast_forward'}, {'type': 'pull_request', 'parameters': {'required_approving_review_count': 1, 'dismiss_stale_reviews_on_push': True, 'require_last_push_approval': True, 'required_review_thread_resolution': True}}, {'type': 'required_status_checks', 'parameters': {'strict_required_status_checks_policy': True, 'required_status_checks': [{'context': 'Mobile'}, {'context': 'DCO Check'}]}}]}
mutations = {
"disabled": lambda p: p.update(enforcement="disabled"),
"wrong target": lambda p: p.update(target="tag"),
"wrong scope": lambda p: p["conditions"]["ref_name"].update(include=["refs/heads/mobile-release/*"]),
"excluded branch": lambda p: p["conditions"]["ref_name"].update(exclude=["refs/heads/mobile-v1.2"]),
"caller bypass": lambda p: p.update(current_user_can_bypass="always"),
"other bypass": lambda p: p.update(bypass_actors=[{"actor_id":1}]),
"no deletion guard": lambda p: p["rules"].pop(0),
"no force guard": lambda p: p["rules"].pop(1),
"no review": lambda p: p["rules"].pop(2),
"no checks": lambda p: p["rules"].pop(3),
"no approvals": lambda p: p["rules"][2]["parameters"].update(required_approving_review_count=0),
"stale approvals": lambda p: p["rules"][2]["parameters"].update(dismiss_stale_reviews_on_push=False),
"self approval": lambda p: p["rules"][2]["parameters"].update(require_last_push_approval=False),
"open threads": lambda p: p["rules"][2]["parameters"].update(required_review_thread_resolution=False),
"non-strict checks": lambda p: p["rules"][3]["parameters"].update(strict_required_status_checks_policy=False),
"missing mobile": lambda p: p["rules"][3]["parameters"]["required_status_checks"].pop(0),
"missing DCO": lambda p: p["rules"][3]["parameters"]["required_status_checks"].pop(1),
}
for name, mutate in mutations.items():
value = copy.deepcopy(policy)
mutate(value)
env = dict(os.environ, GH_BRANCH_POLICY=json.dumps(value), GH_ANNOTATED_TARGET_SHA=os.environ["GH_STABLE_SHA"])
before = open(env["GH_CALLS"]).read()
result = subprocess.run([sys.argv[1], "1.2.3", "1", env["GH_STABLE_SHA"], "mobile-v1.2"], env=env, capture_output=True, text=True)
assert result.returncode != 0 and "must enforce review" in result.stderr, (name, result.stderr)
assert open(env["GH_CALLS"]).read() == before, name
PYPOLICY
if GH_BRANCH_POLICY_FAIL=1 "$publisher" 1.2.3 1 "$GH_STABLE_SHA" mobile-v1.2 >/dev/null 2>&1; then
echo "publisher accepted unavailable branch policy" >&2
exit 1
fi
# Exercise the checked-in workflow command with its actual source input binding.
python3 - "$repo_root/.github/workflows/mobile-release-candidate.yml" "$tmp" <<'PYWORKFLOW'
import os
from pathlib import Path
import re
import subprocess
import sys
workflow = Path(sys.argv[1]).read_text()
step = workflow.split(" - name: Publish annotated candidate tag\n", 1)[1]
assert re.search(r"^ SOURCE_BRANCH: \$\{\{ inputs\.source_branch \}\}$", step, re.M), "source input is not bound to SOURCE_BRANCH"
command = re.search(r"^ run: (.+)$", step, re.M).group(1)
root = Path(sys.argv[2]) / "workflow"
(root / "scripts").mkdir(parents=True)
stub = root / "scripts/publish-mobile-release-candidate.sh"
stub.write_text('#!/usr/bin/env bash\nprintf "%s\\n" "$@"\n')
stub.chmod(0o755)
env = dict(os.environ, MOBILE_VERSION="1.2.3", CANDIDATE_NUMBER="1", TARGET_SHA="4" * 40, SOURCE_BRANCH="mobile-v1.2")
actual = subprocess.check_output(["bash", "-eu", "-c", command], cwd=root, env=env, text=True).splitlines()
assert actual == ["1.2.3", "1", "4" * 40, "mobile-v1.2"], actual
PYWORKFLOW
echo "mobile release candidate publisher contract passed"

View File

@@ -17,6 +17,11 @@ cat > "$bin/gh" <<'GH'
set -euo pipefail
case "${1:-}:${2:-}" in
--version:*) printf 'gh version %s (test)\n' "${GH_VERSION:-2.94.0}" ;;
api:repos/block/buzz/rulesets/19321162)
[[ "${GH_BRANCH_POLICY_FAIL:-}" != 1 ]] || exit 1
policy='{"target":"branch","enforcement":"active","current_user_can_bypass":"never","conditions":{"ref_name":{"include":["refs/heads/mobile-v*"],"exclude":[]}},"bypass_actors":[],"rules":[{"type":"deletion"},{"type":"non_fast_forward"},{"type":"pull_request","parameters":{"required_approving_review_count":1,"dismiss_stale_reviews_on_push":true,"require_last_push_approval":true,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"strict_required_status_checks_policy":true,"required_status_checks":[{"context":"Mobile"},{"context":"DCO Check"}]}}]}'
printf '%s\n' "${GH_BRANCH_POLICY:-$policy}" | jq -r "$4"
;;
api:repos/block/buzz/rulesets/14378754)
case "$*" in
*'.enforcement'*) printf '%s\n' "${GH_TAG_RULESET_STATE:-active}" ;;
@@ -27,7 +32,13 @@ case "${1:-}:${2:-}" in
*) exit 2 ;;
esac
;;
api:repos/block/buzz/compare/*)
[[ "${GH_COMPARE_FAIL:-}" != 1 ]] || exit 1
pair="${2##*/}"
git --git-dir="$GH_REMOTE" merge-base "${pair%%...*}" "${pair##*...}"
;;
workflow:run)
printf '%s\n' attempted >> "$GH_WORKFLOW_ATTEMPTS"
if [[ "${GH_WORKFLOW_DISPATCH_FAIL:-}" == "1" ]]; then
printf '%s\n' "${GH_WORKFLOW_DISPATCH_ERROR:-dispatch failed}" >&2
exit 1
@@ -42,15 +53,18 @@ case "${1:-}:${2:-}" in
version=""
number=""
sha=""
source_branch=""
while [[ "$#" -gt 0 ]]; do
case "$1" in
version=*) version="${1#version=}" ;;
candidate_number=*) number="${1#candidate_number=}" ;;
source_branch=*) source_branch="${1#source_branch=}" ;;
target_sha=*) sha="${1#target_sha=}" ;;
esac
shift
done
[[ -n "$version" && -n "$number" && -n "$sha" ]]
[[ -n "$version" && -n "$number" && -n "$sha" && -n "$source_branch" ]] || exit 1
git --git-dir="$GH_REMOTE" merge-base --is-ancestor "$sha" "refs/heads/$source_branch" || exit 1
printf '%s\t%s\t%s\n' "$version" "$number" "$sha" >> "$GH_WORKFLOW_CAPTURE"
if [[ "${GH_WORKFLOW_NO_URL:-}" == "1" ]]; then
exit 0
@@ -85,7 +99,9 @@ chmod +x "$bin/gh"
export PATH="$bin:$PATH"
export GH_WORKFLOW_CAPTURE="$tmp/workflow-dispatches"
export GH_WORKFLOW_ATTEMPTS="$tmp/workflow-attempts"
export GH_WORKTREE="$work"
export GH_REMOTE="$remote"
run_release() {
local repo="$1"
@@ -106,7 +122,7 @@ fail() {
assert_no_removed_mobile_release_behavior() {
local status
grep -Eq 'gh[[:space:]]+release|mobile-release/|finalize' "$@" && \
grep -Eq 'gh[[:space:]]+release|finalize' "$@" && \
fail "removed branch/finalization/GitHub Release behavior remains"
status="$?"
[[ "$status" -eq 1 ]] || \
@@ -141,7 +157,7 @@ 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." \
grep -Fq "Note: local HEAD is $(git -C "$operator" rev-parse HEAD); candidate source is pinned from 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}')" == \
@@ -149,6 +165,37 @@ cat "$tmp/stale-output"
[[ "$(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"
# A stable branch may diverge from main, including an empty release-only commit.
git -C "$work" switch -q -c mobile-v1.2
git -C "$work" commit -q --allow-empty -m stable-only
git -C "$work" push -q origin mobile-v1.2
stable_sha="$(git -C "$work" rev-parse HEAD)"
git -C "$work" switch -q main
if GH_BRANCH_POLICY_FAIL=1 run_release "$operator" candidate 1.2.4 --source-branch mobile-v1.2 > "$tmp/policy-error" 2>&1; then
fail "operator accepted unavailable stable-branch policy"
fi
grep -Fq 'Could not verify mobile stable-branch protection' "$tmp/policy-error"
if GH_BRANCH_POLICY='{}' run_release "$operator" candidate 1.2.4 --source-branch mobile-v1.2 > "$tmp/policy-error" 2>&1; then
fail "operator accepted missing stable-branch policy"
fi
run_release "$operator" candidate 1.2.4 --source-branch mobile-v1.2
[[ "$(git --git-dir="$remote" rev-parse 'refs/tags/mobile-v1.2.4-rc.1^{commit}')" == "$stable_sha" ]]
# Existing mismatched refs must be rejected by the guard, not missing-ref errors.
for source in mobile-v1.3 mobile-v2.2 mobile-v01.2 arbitrary; do
git -C "$work" switch -q -c "$source"
git -C "$work" commit -q --allow-empty -m "$source"
git -C "$work" push -q origin "$source"
done
git -C "$work" switch -q main
for source in mobile-v1.3 mobile-v2.2 mobile-v01.2 arbitrary ''; do
if run_release "$operator" candidate 1.2.4 --source-branch "$source" >/dev/null 2>&1; then
fail "accepted mismatched source '$source'"
fi
done
if run_release "$operator" candidate 8.8.0 --source-branch mobile-v8.8 >/dev/null 2>&1; then
fail "missing stable branch fell back to main"
fi
# 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)"
@@ -222,19 +269,40 @@ if GH_TAG_VERIFY_LIGHTWEIGHT=1 run_release "$operator" candidate 9.9.2 >/dev/nul
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.
# Advancing main during publication must not invalidate the pinned tag.
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
GH_MAIN_MOVE_DURING_WORKFLOW=1 run_release "$operator" candidate 9.9.3 > "$tmp/main-race-output" 2>&1
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"
[[ "$(git --git-dir="$remote" rev-parse 'refs/tags/mobile-v9.9.3-rc.1^{commit}')" == "$pre_race_main_sha" ]]
grep -Fq 'Published mobile-v9.9.3-rc.1' "$tmp/main-race-output"
# Explicit historical source remains pinned even when the source branch advances.
run_release "$operator" candidate 7.7.0 --commit "$remote_main_sha"
[[ "$(git --git-dir="$remote" rev-parse 'refs/tags/mobile-v7.7.0-rc.1^{commit}')" == "$remote_main_sha" ]]
run_release "$operator" candidate 1.2.5 --commit "$remote_main_sha" --source-branch mobile-v1.2
[[ "$(git --git-dir="$remote" rev-parse 'refs/tags/mobile-v1.2.5-rc.1^{commit}')" == "$remote_main_sha" ]]
before_attempts="$(wc -l < "$GH_WORKFLOW_ATTEMPTS")"
before_dispatches="$(wc -l < "$GH_WORKFLOW_CAPTURE")"
if run_release "$operator" candidate 7.7.1 --commit "$stable_sha" >/dev/null 2>&1; then
fail "accepted a commit outside main history"
fi
if run_release "$operator" candidate 1.2.6 --source-branch mobile-v1.2 --commit "$new_main_sha" >/dev/null 2>&1; then
fail "accepted a main-only commit outside stable history"
fi
for invalid_sha in main HEAD deadbeef 9999999999999999999999999999999999999999 ''; do
if run_release "$operator" candidate 7.7.1 --commit "$invalid_sha" >/dev/null 2>&1; then
fail "accepted invalid or absent commit '$invalid_sha'"
fi
done
if GH_COMPARE_FAIL=1 run_release "$operator" candidate 7.7.1 --commit "$remote_main_sha" >/dev/null 2>&1; then
fail "accepted unavailable ancestry evidence"
fi
if run_release "$operator" candidate 7.7.1 --commit "$remote_main_sha" --commit "$new_main_sha" >/dev/null 2>&1; then
fail "accepted duplicate commit options"
fi
[[ "$(wc -l < "$GH_WORKFLOW_ATTEMPTS")" == "$before_attempts" ]]
[[ "$(wc -l < "$GH_WORKFLOW_CAPTURE")" == "$before_dispatches" ]]
# Publishing through a fork is rejected and unsupported gh versions fail before
# dispatching any candidate publication.