mirror of
https://github.com/block/buzz.git
synced 2026-11-06 09:21:45 +00:00
Mobile release candidates (`mobile-vX.Y.Z-rc.N`) are currently created
by the `mobile-release-candidate.yml` workflow, using the
`buzz-release-bot` App key that also serves the desktop, relay and chart
lanes. This PR takes mobile off that key. After it lands, the only way
to create a mobile candidate tag is for a person on the mobile release
team to push it.
### What changes
- `scripts/mobile-release.sh candidate` keeps all its existing checks:
version, source branch, clean tree, canonical origin, ancestry and next
rc number. It no longer dispatches a workflow. It now:
- verifies the mobile tag ruleset 26588677 (active, the expected rules,
scoped to `refs/tags/mobile-*` or `refs/tags/mobile-v*`, and bypassable
by the current user), and that the App-only general tag ruleset 14378754
excludes `refs/tags/mobile-*`;
- requires origin's effective push URLs to be exactly its fetch URL, and
fetches a pinned `--commit` missing from a shallow clone;
- creates the annotated tag locally and pushes exactly that ref
(`--no-follow-tags`, explicit refspec);
- confirms through the GitHub API that `block/buzz` holds that exact tag
object, then verifies the remote tag as before.
A rejected push removes the local tag only when origin lacks it, so a
retry starts clean. When the push reports failure but origin has the
tag, the script keeps the local tag and still runs the canonical GitHub
check, reporting success only if it passes. When GitHub can't confirm
the tag, the local tag is kept and the message says how to reconcile it.
- `scripts/release-rulesets.sh`: `require_mobile_release_tag_ruleset`
replaces the App-oriented check. `MOBILE_RELEASE_TAG_RULESET_ID` is
`26588677`.
- Removed `.github/workflows/mobile-release-candidate.yml`,
`scripts/publish-mobile-release-candidate.sh` and its test, and their CI
references. Updated `RELEASING.md` and the auto-tag workflow header
comment.
- The desktop, relay and chart auto-tagging is unchanged.
### Rollout
Tag ruleset 26588677 ("Restrict Mobile Tags") is active with the two
mobile release managers as bypass actors. The App (Integration 4349119)
is temporarily a bypass actor of 26588677 and 19321162 so the current
`main` workflow keeps working; it is removed from both right before this
merges. General tag ruleset 14378754 now excludes `refs/tags/mobile-*`;
without that, rulesets stack and it would reject release managers'
mobile tag pushes. The branch ruleset 19321162 now has the same bypass
actors, so `require_mobile_release_branch_ruleset` no longer requires
"no bypass"; it still checks the review and status check policy. Release
managers must be bypass actors of both rulesets.
### Before merge
- [x] Remove Integration 4349119 (`buzz-release-bot`) from the bypass
actors of rulesets 26588677 and 19321162.
### Validation
- `shellcheck -x scripts/release-rulesets.sh scripts/mobile-release.sh
scripts/mobile-release-source.sh
scripts/test-mobile-release-contract.sh`: clean.
- `scripts/test-mobile-release-contract.sh`: passes. It runs against a
local bare remote with a stubbed `gh` and covers:
- a successful annotated push at the pinned SHA;
- ruleset failures (not bypassable, inactive, wrong rules, wrong scope,
excludes, non-numeric ID), with `refs/tags/mobile-v*` also accepted as
the include, each failing before any tag is created;
- a push rejected by a pre-receive hook, which leaves no local tag and
allows a clean retry;
- `push.followTags=true` with a stray local annotated mobile tag
(exactly one ref published), a push that lands but exits non-zero (tag
kept), and a GitHub API outage after the push (tag kept, rerun publishes
the next rc);
- origin push URLs pointing at a fork (pushurl, a second pushurl,
pushInsteadOf, insteadOf), a shallow clone pinning a historical commit,
and 14378754 lacking the mobile exclusion;
- rc numbering, source branch rules and `--commit` pinning.
- `scripts/test-release-ref-contract.sh` and
`scripts/test-desktop-release-candidate.sh`: pass.
- Live read-only check against real GitHub as a bypass actor: sourced
`scripts/release-rulesets.sh` and `scripts/mobile-release-source.sh`,
then ran `require_mobile_release_tag_ruleset` (including the 14378754
exclusion check) and `require_mobile_release_branch_ruleset
mobile-v0.27`. Both returned success. No tag was created.
- No mobile app test: this is a script and workflow change only.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Signed-off-by: Tom Brow <tomb@squareup.com>
Co-authored-by: Tom Brow <tomb@squareup.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@localhost>
83 lines
5.1 KiB
Bash
Executable File
83 lines
5.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# Tag ruleset ("Restrict Mobile Tags") that protects mobile candidate tags. Its
|
|
# bypass actors are the mobile release managers, never the buzz-release-bot App:
|
|
# operators push candidate tags themselves.
|
|
readonly MOBILE_RELEASE_TAG_RULESET_ID=26588677
|
|
# General tag ruleset ("Release", ~ALL tags) whose only bypass actor is the
|
|
# App. Rulesets stack, so it must exclude mobile tags or operators can't push
|
|
# them even though they bypass the mobile tag ruleset.
|
|
readonly GENERAL_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
|
|
# Tags are pushed with `git push origin`, which honors remote.origin.pushurl
|
|
# and url.*.pushInsteadOf. Require exactly one effective push destination,
|
|
# the same canonical repository that origin fetches from.
|
|
# --all: git push sends to every configured push URL, not just the first.
|
|
[[ "$(git remote get-url --push --all origin)" == "$(git remote get-url origin)" ]] || \
|
|
fail_release_ruleset "origin pushes somewhere other than it fetches from; remove remote.origin.pushurl or pushInsteadOf so candidate tags reach canonical block/buzz" || return 1
|
|
}
|
|
|
|
require_mobile_release_tag_ruleset() {
|
|
local ruleset_endpoint state target can_bypass rule_types includes excludes
|
|
|
|
[[ "$MOBILE_RELEASE_TAG_RULESET_ID" =~ ^[0-9]+$ ]] || \
|
|
fail_release_ruleset "MOBILE_RELEASE_TAG_RULESET_ID in scripts/release-rulesets.sh must be numeric (got '$MOBILE_RELEASE_TAG_RULESET_ID')" || return 1
|
|
command -v gh >/dev/null 2>&1 || fail_release_ruleset "gh is required" || return 1
|
|
ruleset_endpoint="repos/block/buzz/rulesets/$MOBILE_RELEASE_TAG_RULESET_ID"
|
|
|
|
state="$(gh api "$ruleset_endpoint" --jq .enforcement)" || \
|
|
fail_release_ruleset "could not verify mobile tag ruleset $MOBILE_RELEASE_TAG_RULESET_ID" || return 1
|
|
[[ "$state" == "active" ]] || \
|
|
fail_release_ruleset "mobile tag ruleset $MOBILE_RELEASE_TAG_RULESET_ID is '$state'" || return 1
|
|
|
|
# Rulesets carry their target (branch, tag or push) independently of their
|
|
# ref patterns; a retargeted ruleset would leave candidate tags unprotected.
|
|
target="$(gh api "$ruleset_endpoint" --jq .target)" || \
|
|
fail_release_ruleset "could not verify mobile tag ruleset $MOBILE_RELEASE_TAG_RULESET_ID target" || return 1
|
|
[[ "$target" == "tag" ]] || \
|
|
fail_release_ruleset "mobile tag ruleset $MOBILE_RELEASE_TAG_RULESET_ID must target tags, not '$target'" || return 1
|
|
|
|
can_bypass="$(gh api "$ruleset_endpoint" --jq .current_user_can_bypass)" || \
|
|
fail_release_ruleset "could not verify your bypass of mobile tag ruleset $MOBILE_RELEASE_TAG_RULESET_ID" || return 1
|
|
[[ "$can_bypass" == "always" ]] || \
|
|
fail_release_ruleset "you cannot bypass mobile tag ruleset $MOBILE_RELEASE_TAG_RULESET_ID (reported '$can_bypass'); you must be a bypass actor of mobile tag ruleset $MOBILE_RELEASE_TAG_RULESET_ID (currently the mobile release managers)" || return 1
|
|
|
|
rule_types="$(gh api "$ruleset_endpoint" --jq '[.rules[].type] | sort | join(",")')" || \
|
|
fail_release_ruleset "could not verify mobile tag ruleset $MOBILE_RELEASE_TAG_RULESET_ID rules" || return 1
|
|
[[ "$rule_types" == "creation,deletion,non_fast_forward,update" ]] || \
|
|
fail_release_ruleset "mobile tag ruleset $MOBILE_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 mobile tag ruleset $MOBILE_RELEASE_TAG_RULESET_ID scope" || return 1
|
|
# Either pattern covers every mobile-vX.Y.Z-rc.N candidate tag.
|
|
[[ ",$includes," == *",refs/tags/mobile-*,"* || ",$includes," == *",refs/tags/mobile-v*,"* ]] || \
|
|
fail_release_ruleset "mobile tag ruleset $MOBILE_RELEASE_TAG_RULESET_ID must include refs/tags/mobile-* or refs/tags/mobile-v*" || return 1
|
|
|
|
excludes="$(gh api "$ruleset_endpoint" --jq '[.conditions.ref_name.exclude[]] | sort | join(",")')" || \
|
|
fail_release_ruleset "could not verify mobile tag ruleset $MOBILE_RELEASE_TAG_RULESET_ID exclusions" || return 1
|
|
[[ -z "$excludes" ]] || \
|
|
fail_release_ruleset "mobile tag ruleset $MOBILE_RELEASE_TAG_RULESET_ID has unexpected exclusions: '$excludes'" || return 1
|
|
|
|
excludes="$(gh api "repos/block/buzz/rulesets/$GENERAL_RELEASE_TAG_RULESET_ID" --jq '[.conditions.ref_name.exclude[]] | join(",")')" || \
|
|
fail_release_ruleset "could not verify general tag ruleset $GENERAL_RELEASE_TAG_RULESET_ID exclusions" || return 1
|
|
[[ ",$excludes," == *",refs/tags/mobile-*,"* ]] || \
|
|
fail_release_ruleset "general tag ruleset $GENERAL_RELEASE_TAG_RULESET_ID must exclude refs/tags/mobile-*, or it rejects your candidate tag push; ask a repository admin" || return 1
|
|
}
|