mirror of
https://github.com/block/buzz.git
synced 2026-11-09 19:02:00 +00:00
Merge main into webhook owner-ban check
Main added authenticate_member_post beside this branch's workflow_webhook_postgres_tests module declaration; keep both. Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Luke Tornquist <tornquist@squareup.com>
This commit is contained in:
@@ -217,7 +217,17 @@ 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",
|
||||
["scripts/test-mobile-gateway-recipes.sh"],
|
||||
["mobile"],
|
||||
],
|
||||
["desktop", ["desktop/src/main.tsx"], ["desktop"]],
|
||||
["Tauri", ["desktop/src-tauri/src/main.rs"], ["desktop", "desktop-rust"]],
|
||||
["relay", ["crates/buzz-relay/src/main.rs"], ["rust"]],
|
||||
@@ -232,7 +242,7 @@ const scenarios = [
|
||||
[".github/workflows/_ci-desktop.yml"],
|
||||
["desktop", "mobile", "rust"],
|
||||
],
|
||||
["Justfile", ["Justfile"], ["desktop", "rust"]],
|
||||
["Justfile", ["Justfile"], ["desktop", "mobile", "rust"]],
|
||||
["CI apt retry", ["scripts/ci-apt-retry.sh"], ["desktop", "rust"]],
|
||||
["integration services", ["docker-compose.yml"], ["rust"]],
|
||||
["CI integration images", ["docker-compose.ci.yml"], ["rust"]],
|
||||
|
||||
67
scripts/contracts/capture-solo-creation.mjs
Normal file
67
scripts/contracts/capture-solo-creation.mjs
Normal file
@@ -0,0 +1,67 @@
|
||||
// Capture the current desktop's production create call, without a relay write.
|
||||
// Usage: node --experimental-transform-types scripts/contracts/capture-solo-creation.mjs /path/to/buzz-app
|
||||
import { registerHooks } from "node:module";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { resolve } from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import assert from "node:assert/strict";
|
||||
const root = resolve(process.argv[2]);
|
||||
execFileSync("git", ["-C", root, "diff", "--exit-code", "HEAD", "--", "src"]);
|
||||
registerHooks({
|
||||
resolve(specifier, context, next) {
|
||||
try {
|
||||
return next(specifier, context);
|
||||
} catch (error) {
|
||||
if (specifier.startsWith(".") && !/\.[a-z]+$/.test(specifier))
|
||||
return next(`${specifier}.ts`, context);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
});
|
||||
const { createChannelWrites } = await import(
|
||||
pathToFileURL(`${root}/src/features/relay/channel-writes.ts`)
|
||||
);
|
||||
let creation;
|
||||
const outbox = {
|
||||
supports: () => true,
|
||||
send: (event) => {
|
||||
creation = event;
|
||||
return "captured";
|
||||
},
|
||||
};
|
||||
const reader = {};
|
||||
createChannelWrites(
|
||||
outbox,
|
||||
{},
|
||||
reader,
|
||||
new AbortController().signal,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
reader,
|
||||
true,
|
||||
).create("11111111-1111-4111-8111-111111111111", "Desktop Solo");
|
||||
assert.equal(creation.kind, 9007);
|
||||
assert.ok(
|
||||
creation.tags.some(([key, value]) => key === "t" && value === "buzz.solo"),
|
||||
);
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
producer: "https://github.com/block/buzz-app",
|
||||
revision: execFileSync("git", ["-C", root, "rev-parse", "HEAD"], {
|
||||
encoding: "utf8",
|
||||
}).trim(),
|
||||
creation,
|
||||
creator: "11".repeat(32),
|
||||
identity_tags: [
|
||||
["t", "stream"],
|
||||
["t", "buzz.solo"],
|
||||
["P", "11".repeat(32)],
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
58
scripts/mobile-release-source.sh
Normal file
58
scripts/mobile-release-source.sh
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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 ;;
|
||||
|
||||
37
scripts/mobile-test-shard.sh
Executable file
37
scripts/mobile-test-shard.sh
Executable file
@@ -0,0 +1,37 @@
|
||||
#!/bin/bash
|
||||
# Run one shard of the mobile Dart tests, split by file.
|
||||
#
|
||||
# `flutter test --total-shards` splits tests, so every shard still compiles
|
||||
# and loads every test file. Splitting files means each shard compiles only
|
||||
# its own. Files are balanced by line count, largest first, so the split is
|
||||
# deterministic and needs no timing history.
|
||||
#
|
||||
# Usage: mobile-test-shard.sh INDEX TOTAL [flutter test args...]
|
||||
set -euo pipefail
|
||||
index=$1
|
||||
total=$2
|
||||
shift 2
|
||||
cd "$(dirname "$0")/../mobile"
|
||||
|
||||
files=()
|
||||
while IFS= read -r file; do
|
||||
files+=("$file")
|
||||
done < <(
|
||||
find test -name '*_test.dart' -exec wc -l {} + |
|
||||
awk '$2 != "total" { print $1, $2 }' |
|
||||
sort -k1,1nr -k2,2 |
|
||||
awk -v index_="$index" -v total="$total" '
|
||||
{
|
||||
best = 0
|
||||
for (i = 1; i < total; i++) if (load[i] < load[best]) best = i
|
||||
load[best] += $1
|
||||
if (best == index_) print $2
|
||||
}'
|
||||
)
|
||||
|
||||
if [ ${#files[@]} -eq 0 ]; then
|
||||
echo "Shard $index of $total has no test files" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Shard $index of $total: ${#files[@]} test files"
|
||||
flutter test "$@" "${files[@]}"
|
||||
@@ -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'
|
||||
|
||||
@@ -16,8 +16,6 @@ BEGIN
|
||||
-- pgschema may copy parent triggers onto standalone children. Drop
|
||||
-- those copies before ATTACH; PostgreSQL recreates inherited parent
|
||||
-- triggers while attaching and rejects same-named child triggers.
|
||||
DROP TRIGGER IF EXISTS events_enqueue_push_match ON events_p_past;
|
||||
DROP TRIGGER IF EXISTS events_refresh_channel_ttl ON events_p_past;
|
||||
DROP TRIGGER IF EXISTS events_created_at_floor ON events_p_past;
|
||||
DROP TRIGGER IF EXISTS community_write_fence_events ON events_p_past;
|
||||
DROP TRIGGER IF EXISTS trg_events_guard_channel_roster_snapshot ON events_p_past;
|
||||
@@ -30,8 +28,6 @@ BEGIN
|
||||
WHERE inhparent = 'events'::regclass
|
||||
AND inhrelid = 'events_p2026_01'::regclass
|
||||
) THEN
|
||||
DROP TRIGGER IF EXISTS events_enqueue_push_match ON events_p2026_01;
|
||||
DROP TRIGGER IF EXISTS events_refresh_channel_ttl ON events_p2026_01;
|
||||
DROP TRIGGER IF EXISTS events_created_at_floor ON events_p2026_01;
|
||||
DROP TRIGGER IF EXISTS community_write_fence_events ON events_p2026_01;
|
||||
DROP TRIGGER IF EXISTS trg_events_guard_channel_roster_snapshot ON events_p2026_01;
|
||||
@@ -44,8 +40,6 @@ BEGIN
|
||||
WHERE inhparent = 'events'::regclass
|
||||
AND inhrelid = 'events_p2026_02'::regclass
|
||||
) THEN
|
||||
DROP TRIGGER IF EXISTS events_enqueue_push_match ON events_p2026_02;
|
||||
DROP TRIGGER IF EXISTS events_refresh_channel_ttl ON events_p2026_02;
|
||||
DROP TRIGGER IF EXISTS events_created_at_floor ON events_p2026_02;
|
||||
DROP TRIGGER IF EXISTS community_write_fence_events ON events_p2026_02;
|
||||
DROP TRIGGER IF EXISTS trg_events_guard_channel_roster_snapshot ON events_p2026_02;
|
||||
@@ -58,8 +52,6 @@ BEGIN
|
||||
WHERE inhparent = 'events'::regclass
|
||||
AND inhrelid = 'events_p2026_03'::regclass
|
||||
) THEN
|
||||
DROP TRIGGER IF EXISTS events_enqueue_push_match ON events_p2026_03;
|
||||
DROP TRIGGER IF EXISTS events_refresh_channel_ttl ON events_p2026_03;
|
||||
DROP TRIGGER IF EXISTS events_created_at_floor ON events_p2026_03;
|
||||
DROP TRIGGER IF EXISTS community_write_fence_events ON events_p2026_03;
|
||||
DROP TRIGGER IF EXISTS trg_events_guard_channel_roster_snapshot ON events_p2026_03;
|
||||
@@ -72,8 +64,6 @@ BEGIN
|
||||
WHERE inhparent = 'events'::regclass
|
||||
AND inhrelid = 'events_p2026_04'::regclass
|
||||
) THEN
|
||||
DROP TRIGGER IF EXISTS events_enqueue_push_match ON events_p2026_04;
|
||||
DROP TRIGGER IF EXISTS events_refresh_channel_ttl ON events_p2026_04;
|
||||
DROP TRIGGER IF EXISTS events_created_at_floor ON events_p2026_04;
|
||||
DROP TRIGGER IF EXISTS community_write_fence_events ON events_p2026_04;
|
||||
DROP TRIGGER IF EXISTS trg_events_guard_channel_roster_snapshot ON events_p2026_04;
|
||||
@@ -86,8 +76,6 @@ BEGIN
|
||||
WHERE inhparent = 'events'::regclass
|
||||
AND inhrelid = 'events_p2026_05'::regclass
|
||||
) THEN
|
||||
DROP TRIGGER IF EXISTS events_enqueue_push_match ON events_p2026_05;
|
||||
DROP TRIGGER IF EXISTS events_refresh_channel_ttl ON events_p2026_05;
|
||||
DROP TRIGGER IF EXISTS events_created_at_floor ON events_p2026_05;
|
||||
DROP TRIGGER IF EXISTS community_write_fence_events ON events_p2026_05;
|
||||
DROP TRIGGER IF EXISTS trg_events_guard_channel_roster_snapshot ON events_p2026_05;
|
||||
@@ -100,8 +88,6 @@ BEGIN
|
||||
WHERE inhparent = 'events'::regclass
|
||||
AND inhrelid = 'events_p2026_06'::regclass
|
||||
) THEN
|
||||
DROP TRIGGER IF EXISTS events_enqueue_push_match ON events_p2026_06;
|
||||
DROP TRIGGER IF EXISTS events_refresh_channel_ttl ON events_p2026_06;
|
||||
DROP TRIGGER IF EXISTS events_created_at_floor ON events_p2026_06;
|
||||
DROP TRIGGER IF EXISTS community_write_fence_events ON events_p2026_06;
|
||||
DROP TRIGGER IF EXISTS trg_events_guard_channel_roster_snapshot ON events_p2026_06;
|
||||
@@ -114,8 +100,6 @@ BEGIN
|
||||
WHERE inhparent = 'events'::regclass
|
||||
AND inhrelid = 'events_p_future'::regclass
|
||||
) THEN
|
||||
DROP TRIGGER IF EXISTS events_enqueue_push_match ON events_p_future;
|
||||
DROP TRIGGER IF EXISTS events_refresh_channel_ttl ON events_p_future;
|
||||
DROP TRIGGER IF EXISTS events_created_at_floor ON events_p_future;
|
||||
DROP TRIGGER IF EXISTS community_write_fence_events ON events_p_future;
|
||||
DROP TRIGGER IF EXISTS trg_events_guard_channel_roster_snapshot ON events_p_future;
|
||||
@@ -197,6 +181,55 @@ BEGIN
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- pgschema moves the partition key to the front of a partitioned table's
|
||||
-- primary key when it creates the table, so events gets
|
||||
-- PRIMARY KEY (created_at, community_id, id) instead of the declared
|
||||
-- (community_id, created_at, id). Every later plan then drops and re-adds
|
||||
-- events_pkey and each declared child's key, which fails once the children are
|
||||
-- attached because their keys are inherited. Rebuild the key on the parent in
|
||||
-- the declared order; PostgreSQL recreates each partition's key with it.
|
||||
-- delivery_log already declares its partition key first. This block is a
|
||||
-- verbatim copy of scripts/repair-events-pkey.sql; keep the two identical.
|
||||
DO $$
|
||||
DECLARE
|
||||
declared CONSTANT text := 'PRIMARY KEY (community_id, created_at, id)';
|
||||
BEGIN
|
||||
-- Also runs before pgschema apply, when events may not exist yet.
|
||||
IF to_regclass('events') IS NULL THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
IF (
|
||||
SELECT pg_get_constraintdef(oid) FROM pg_constraint
|
||||
WHERE conrelid = 'events'::regclass AND contype = 'p'
|
||||
) IS DISTINCT FROM declared THEN
|
||||
-- The rebuild locks events and every partition ACCESS EXCLUSIVE. Fail
|
||||
-- fast instead of queueing behind a relay that still uses them.
|
||||
BEGIN
|
||||
PERFORM set_config('lock_timeout', '10s', true);
|
||||
ALTER TABLE events DROP CONSTRAINT IF EXISTS events_pkey;
|
||||
ALTER TABLE events
|
||||
ADD CONSTRAINT events_pkey PRIMARY KEY (community_id, created_at, id);
|
||||
EXCEPTION WHEN lock_not_available THEN
|
||||
RAISE EXCEPTION 'timed out locking events to rebuild events_pkey'
|
||||
USING HINT = 'Stop any relay using this database, then rerun.';
|
||||
END;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE contype = 'p'
|
||||
AND conrelid IN (
|
||||
SELECT 'events'::regclass
|
||||
UNION ALL
|
||||
SELECT inhrelid FROM pg_inherits WHERE inhparent = 'events'::regclass
|
||||
)
|
||||
AND pg_get_constraintdef(oid) <> declared
|
||||
) THEN
|
||||
RAISE EXCEPTION 'events and its partitions must have %', declared;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- pgschema drops multi-column CHECK constraints. Restore the direct-action
|
||||
-- shape CHECK from schema/schema.sql (and migration 0055) verbatim.
|
||||
DO $$
|
||||
|
||||
56
scripts/repair-events-pkey.sql
Normal file
56
scripts/repair-events-pkey.sql
Normal file
@@ -0,0 +1,56 @@
|
||||
-- Rebuild events_pkey in its declared column order.
|
||||
--
|
||||
-- pgschema creates events with the partition key moved to the front of its
|
||||
-- primary key, and every later plan then tries to drop and re-add the inherited
|
||||
-- key on each partition, which fails (SQLSTATE 42P16).
|
||||
-- scripts/reconcile-schema-after-pgschema.sql repairs that after every apply
|
||||
-- and carries a verbatim copy of the block below. A reused database reconciled
|
||||
-- before that repair existed still fails its next apply, before the reconcile
|
||||
-- script can run, so scripts/start-relay-for-tests.sh, the one launcher that
|
||||
-- reuses its database, runs this file before pgschema apply. It does nothing on
|
||||
-- an empty database or one whose key is already in order.
|
||||
--
|
||||
-- Both copies are temporary. The reconcile block, and the test that keeps the
|
||||
-- copies identical, can go once pgschema creates the key in declared order
|
||||
-- (1.13.3 does). This file and the launcher step can go once no local database
|
||||
-- reconciled before the reconcile block existed remains.
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
declared CONSTANT text := 'PRIMARY KEY (community_id, created_at, id)';
|
||||
BEGIN
|
||||
-- Also runs before pgschema apply, when events may not exist yet.
|
||||
IF to_regclass('events') IS NULL THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
IF (
|
||||
SELECT pg_get_constraintdef(oid) FROM pg_constraint
|
||||
WHERE conrelid = 'events'::regclass AND contype = 'p'
|
||||
) IS DISTINCT FROM declared THEN
|
||||
-- The rebuild locks events and every partition ACCESS EXCLUSIVE. Fail
|
||||
-- fast instead of queueing behind a relay that still uses them.
|
||||
BEGIN
|
||||
PERFORM set_config('lock_timeout', '10s', true);
|
||||
ALTER TABLE events DROP CONSTRAINT IF EXISTS events_pkey;
|
||||
ALTER TABLE events
|
||||
ADD CONSTRAINT events_pkey PRIMARY KEY (community_id, created_at, id);
|
||||
EXCEPTION WHEN lock_not_available THEN
|
||||
RAISE EXCEPTION 'timed out locking events to rebuild events_pkey'
|
||||
USING HINT = 'Stop any relay using this database, then rerun.';
|
||||
END;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE contype = 'p'
|
||||
AND conrelid IN (
|
||||
SELECT 'events'::regclass
|
||||
UNION ALL
|
||||
SELECT inhrelid FROM pg_inherits WHERE inhparent = 'events'::regclass
|
||||
)
|
||||
AND pg_get_constraintdef(oid) <> declared
|
||||
) THEN
|
||||
RAISE EXCEPTION 'events and its partitions must have %', declared;
|
||||
END IF;
|
||||
END $$;
|
||||
@@ -87,6 +87,12 @@ run_unit_tests() {
|
||||
run_test_step "buzz-auth unit tests" \
|
||||
cargo test -p buzz-auth --lib -- --nocapture
|
||||
|
||||
# Mesh JWKS proxy-bypass regression: infra-free loopback I/O with
|
||||
# child-process environment isolation. --lib does not select its binary.
|
||||
# Keep this fallback in step with `just test-unit`.
|
||||
run_test_step "buzz-auth mesh JWKS proxy regression" \
|
||||
cargo test -p buzz-auth --test mesh_jwks_proxy -- --nocapture
|
||||
|
||||
# Cross-pod disconnect payload tests (NIP-FI and community archive fence;
|
||||
# infra-free). Mirrors `just test-unit`.
|
||||
run_test_step "buzz-pubsub conn_control tests" \
|
||||
@@ -95,6 +101,10 @@ run_unit_tests() {
|
||||
run_test_step "buzz-voice tests" \
|
||||
cargo test -p buzz-voice --lib -- --nocapture
|
||||
|
||||
# Inter-relay mesh crate (infra-free). Mirrors `just test-unit`.
|
||||
run_test_step "buzz-relay-mesh tests" \
|
||||
cargo test -p buzz-relay-mesh -- --nocapture
|
||||
|
||||
run_test_step "buzz-cli tests" \
|
||||
cargo test -p buzz-cli -- --nocapture
|
||||
|
||||
@@ -135,6 +145,15 @@ run_unit_tests() {
|
||||
run_test_step "buzz-admin community archive command tests" \
|
||||
cargo test -p buzz-admin communities -- --nocapture
|
||||
|
||||
run_test_step "buzz-admin partition audit outcome contract test" \
|
||||
cargo test -p buzz-admin tests::partition_audit_outcomes_keep_their_json_names_and_exit_codes -- --exact --nocapture
|
||||
|
||||
run_test_step "buzz-admin NIP-FI binding command tests" \
|
||||
cargo test -p buzz-admin nip_fi::tests -- --nocapture
|
||||
|
||||
run_test_step "buzz-admin user-deletion intake command tests" \
|
||||
cargo test -p buzz-admin user_deletions::tests -- --nocapture
|
||||
|
||||
# Multi-tenant conformance gate: independent replay checker + golden
|
||||
# fixtures (buzz-conformance). Pure in-process trace replay, no infra.
|
||||
run_test_step "buzz-conformance tests" \
|
||||
@@ -175,6 +194,9 @@ run_unit_tests() {
|
||||
run_test_step "buzz-relay push runtime tests" \
|
||||
cargo test -p buzz-relay --lib push_runtime:: -- --nocapture
|
||||
|
||||
run_test_step "buzz-relay operator listener tests" \
|
||||
cargo test -p buzz-relay --lib operator_listener::tests:: -- --nocapture
|
||||
|
||||
run_test_step "buzz-relay channel authorization tests" \
|
||||
cargo test -p buzz-relay --lib handlers::channel_authz:: -- --nocapture
|
||||
|
||||
@@ -184,6 +206,12 @@ run_unit_tests() {
|
||||
run_test_step "buzz-relay side-effects helper tests" \
|
||||
cargo test -p buzz-relay --lib handlers::side_effects::tests:: -- --nocapture
|
||||
|
||||
run_test_step "buzz-relay system channel scope tests" \
|
||||
cargo test -p buzz-relay --lib handlers::system_channels::tests:: -- --nocapture
|
||||
|
||||
run_test_step "buzz-relay NIP-11 document tests" \
|
||||
cargo test -p buzz-relay --lib nip11::tests:: -- --nocapture
|
||||
|
||||
run_test_step "buzz-relay storage snapshot tests" \
|
||||
cargo test -p buzz-relay --lib storage_sweep::tests:: -- --nocapture
|
||||
|
||||
@@ -212,6 +240,9 @@ run_unit_tests() {
|
||||
run_test_step "buzz-relay startup step tests" \
|
||||
cargo test -p buzz-relay --lib startup_steps::tests:: -- --nocapture
|
||||
|
||||
run_test_step "buzz-relay mesh boot tests" \
|
||||
cargo test -p buzz-relay --lib mesh_boot::tests:: -- --nocapture
|
||||
|
||||
# telemetry and config tests mutate process env. libtest runs one process
|
||||
# with many threads (nextest, used by `just test-unit`, isolates each test),
|
||||
# so run them single-threaded here. `--skip` keeps the substring filter
|
||||
@@ -227,6 +258,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" \
|
||||
@@ -239,7 +273,16 @@ run_unit_tests() {
|
||||
handlers::req::tests::revoke_then_replacement_keeps_replacement_whole \
|
||||
handlers::req::tests::claims_after_connection_cleanup_are_refused \
|
||||
handlers::req::tests::dropped_terminal_frame_cancels_connection \
|
||||
handlers::req::tests::revoke_dropped_terminal_frame_cancels_connection
|
||||
handlers::req::tests::revoke_dropped_terminal_frame_cancels_connection \
|
||||
handlers::req::tests::count_fallback_covers_filters_that_can_match_private_deletions \
|
||||
handlers::req::tests::group_state_t_and_creator_tags_reach_sql_only_for_group_state_kinds
|
||||
|
||||
# Infra-free bridge search extension parser tests, by exact name: the rest of
|
||||
# api::bridge::postgres_tests needs a database.
|
||||
run_test_step "buzz-relay bridge search extension tests" \
|
||||
cargo test -p buzz-relay --lib -- --exact --nocapture \
|
||||
api::bridge::postgres_tests::bridge_search_mode_extension_accepts_prefix_snake_or_camel_case \
|
||||
api::bridge::postgres_tests::bridge_search_sort_extension_selects_newest_only_when_named
|
||||
|
||||
run_test_step "buzz-relay readiness tests" \
|
||||
cargo test -p buzz-relay --lib readiness::tests:: -- --nocapture
|
||||
@@ -274,6 +317,9 @@ run_unit_tests() {
|
||||
run_test_step "buzz-relay API query parsing tests" \
|
||||
cargo test -p buzz-relay --lib api::parse_query_tests:: -- --nocapture
|
||||
|
||||
run_test_step "buzz-relay profile search request tests" \
|
||||
cargo test -p buzz-relay --lib api::profiles:: -- --nocapture
|
||||
|
||||
run_test_step "buzz-relay Git transport Off-mode precedence tests" \
|
||||
cargo test -p buzz-relay --lib api::git::transport::off_mode_precedence_tests:: -- --nocapture
|
||||
|
||||
@@ -286,6 +332,9 @@ run_unit_tests() {
|
||||
run_test_step "buzz-relay auth metrics contract tests" \
|
||||
cargo test -p buzz-relay --lib metrics::contract_tests:: -- --nocapture
|
||||
|
||||
run_test_step "buzz-relay partition alert series tests" \
|
||||
cargo test -p buzz-relay --lib metrics::partition_alert_tests:: -- --nocapture
|
||||
|
||||
local nip_fi_exact_tests=(
|
||||
audio::room::tests::roster_revisions_are_ordered_and_snapshot_is_authoritative
|
||||
connection::tests::auth_lifecycle_reconciles_every_terminal_and_never_leaks_gauge
|
||||
@@ -304,12 +353,26 @@ 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
|
||||
handlers::auth::tests::fi_invalid_nip42_proof_emits_terminal_evidence_rejected
|
||||
handlers::auth::tests::handle_auth_pairing_mismatch_runs_full_root_denial_path
|
||||
handlers::auth::tests::shadow_root_auth_records_pairing_denial
|
||||
handlers::auth::tests::shadow_root_auth_records_unavailable_owner_read
|
||||
handlers::ingest::postgres_tests::relay_admin_ban_maps_to_blocked_auth_failure
|
||||
handlers::ingest::postgres_tests::relay_admin_internal_maps_to_error_not_client_fault
|
||||
handlers::auth::tests::shadow_root_invalid_nip42_retires_without_record
|
||||
handlers::auth::tests::nip42_denial_class_separates_internal_failure_from_bad_evidence
|
||||
handlers::event::tests::fanout_access::owner_only_kinds_keep_only_the_owner
|
||||
@@ -322,6 +385,7 @@ run_unit_tests() {
|
||||
state::tests::conn_manager_disconnect_nip_fi_is_issuer_scoped
|
||||
state::tests::conn_manager_disconnect_nip_fi_sets_authorization_denied_reason
|
||||
state::tests::nip_fi_disconnect_audio_is_issuer_scoped
|
||||
state::tests::nip_fi_owner_disconnect_closes_owned_agent_sockets_on_root_and_audio
|
||||
state::tests::nip_fi_disconnect_closes_proven_audio_socket_and_sends_policy_close_reason
|
||||
state::tests::nip_fi_disconnect_closes_target_audio_only_and_preserves_collocated_peer
|
||||
state::tests::nip_fi_disconnect_does_not_close_different_pubkey_audio_socket
|
||||
|
||||
@@ -290,6 +290,8 @@ SELECT s.community_id, m.pubkey, m.display_name
|
||||
FROM seeded s JOIN seed_member m ON m.n = ANY (s.members)
|
||||
ON CONFLICT (community_id, pubkey) DO UPDATE SET display_name = EXCLUDED.display_name;
|
||||
|
||||
-- Raw rows bypass the NIP-43 member list; run `buzz-admin republish-members`
|
||||
-- per community to rebuild it.
|
||||
INSERT INTO relay_members (community_id, pubkey, role, added_by)
|
||||
SELECT s.community_id, encode(m.pubkey, 'hex'), 'member', NULL
|
||||
FROM seeded s JOIN seed_member m ON m.n = ANY (s.members)
|
||||
|
||||
@@ -103,6 +103,10 @@ export PGSCHEMA_PLAN_DB=buzz
|
||||
export PGSCHEMA_PLAN_USER=buzz
|
||||
export PGSCHEMA_PLAN_PASSWORD=buzz_dev
|
||||
|
||||
# This database persists across runs. One reconciled before the events_pkey
|
||||
# repair existed would fail apply, so repair it first (see the script).
|
||||
docker exec -i -e PGPASSWORD="${PGPASSWORD}" buzz-postgres \
|
||||
psql -U "${PGUSER}" -d "${PGDATABASE}" -v ON_ERROR_STOP=1 < scripts/repair-events-pkey.sql
|
||||
./bin/pgschema apply --file schema/schema.sql --auto-approve
|
||||
docker exec -i -e PGPASSWORD="${PGPASSWORD}" buzz-postgres \
|
||||
psql -U "${PGUSER}" -d "${PGDATABASE}" -v ON_ERROR_STOP=1 < scripts/reconcile-schema-after-pgschema.sql
|
||||
@@ -168,7 +172,7 @@ if [[ "${BUZZ_REQUIRE_RELAY_MEMBERSHIP:-}" == "true" ]]; then
|
||||
fi
|
||||
|
||||
nohup env \
|
||||
DATABASE_URL=postgres://buzz:buzz_dev@localhost:5432/buzz \
|
||||
DATABASE_URL="postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:${PGPORT}/${PGDATABASE}" \
|
||||
REDIS_URL=redis://localhost:6379 \
|
||||
RELAY_URL=ws://localhost:3000 \
|
||||
BUZZ_BIND_ADDR=0.0.0.0:3000 \
|
||||
|
||||
19
scripts/test-mobile-carousel.sh
Normal file
19
scripts/test-mobile-carousel.sh
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
# Test the actual UIKit carousel and Flutter bridge on a booted iOS simulator.
|
||||
# Usage: /bin/bash scripts/test-mobile-carousel.sh <simulator-udid>
|
||||
set -euo pipefail
|
||||
repo_root=$(cd "$(dirname "$0")/.." && pwd)
|
||||
device=${1:?Pass a booted iOS simulator UDID}
|
||||
flutter_root=$(sed -n 's/^FLUTTER_ROOT=//p' "$repo_root/mobile/ios/Flutter/Generated.xcconfig")
|
||||
frameworks="$flutter_root/bin/cache/artifacts/engine/ios/Flutter.xcframework/ios-arm64_x86_64-simulator"
|
||||
fixture=$(mktemp -d)
|
||||
trap 'rm -r "$fixture"' EXIT
|
||||
xcrun --sdk iphonesimulator swiftc \
|
||||
-sdk "$(xcrun --sdk iphonesimulator --show-sdk-path)" \
|
||||
-target "$(uname -m)-apple-ios15.0-simulator" \
|
||||
-F "$frameworks" -framework Flutter \
|
||||
-Xlinker -rpath -Xlinker "$frameworks" \
|
||||
"$repo_root/mobile/ios/Runner/IosMediaCarousel.swift" \
|
||||
"$repo_root/mobile/ios/NativeControlTests/IosMediaCarouselTests.swift" \
|
||||
-o "$fixture/carousel-tests"
|
||||
xcrun simctl spawn "$device" "$fixture/carousel-tests"
|
||||
@@ -15,7 +15,17 @@ cat > "$fixture/bin/flutter" <<'STUB'
|
||||
set -eu
|
||||
printf '%s\n' "$@" > "$CALL_LOG"
|
||||
STUB
|
||||
cat > "$fixture/bin/pgrep" <<'STUB'
|
||||
cat > "$fixture/bin/xcode-select" <<'STUB'
|
||||
#!/bin/bash
|
||||
[[ "$*" == "-p" ]] || exit 1
|
||||
printf '%s\n' "$SELECTED_DEVELOPER_DIR"
|
||||
STUB
|
||||
cat > "$fixture/bin/open" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$@" >> "$OPEN_LOG"
|
||||
[[ "$1" == -a && -d "$2" && "$OPEN_FAIL" == 0 ]]
|
||||
STUB
|
||||
cat > "$fixture/bin/sleep" <<'STUB'
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
STUB
|
||||
@@ -23,9 +33,14 @@ cat > "$fixture/scripts/mobile-worktree-overrides.sh" <<'STUB'
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
STUB
|
||||
chmod +x "$fixture/bin/flutter" "$fixture/bin/pgrep" "$fixture/scripts/mobile-worktree-overrides.sh"
|
||||
chmod +x "$fixture/bin/flutter" "$fixture/bin/xcode-select" "$fixture/bin/open" "$fixture/bin/sleep" "$fixture/scripts/mobile-worktree-overrides.sh"
|
||||
export PATH="$fixture/bin:$PATH"
|
||||
export CALL_LOG="$fixture/arguments"
|
||||
export OPEN_LOG="$fixture/open-calls"
|
||||
# Spaces also exercise quoting of the selected Xcode path.
|
||||
export SELECTED_DEVELOPER_DIR="$fixture/Xcode 27.app/Contents/Developer"
|
||||
export OPEN_FAIL=0
|
||||
mkdir -p "$SELECTED_DEVELOPER_DIR" "$SELECTED_DEVELOPER_DIR/../Applications/DeviceHub.app"
|
||||
unset BUZZ_PUSH_GATEWAY_URL
|
||||
|
||||
for recipe in mobile-dev mobile-build-android; do
|
||||
@@ -56,3 +71,33 @@ printf '%s\n' 'BUZZ_PUSH_GATEWAY_URL = https:/$()/push.example' > "$fixture/mobi
|
||||
printf '%s\n' run '--dart-define=BUZZ_PUSH_GATEWAY_URL=https://push.example' > "$fixture/expected"
|
||||
diff -u "$fixture/expected" "$CALL_LOG"
|
||||
printf 'PASS mobile-dev Xcode override\n'
|
||||
|
||||
# Both installations exist: selection must not depend on Launch Services order.
|
||||
xcode26="$fixture/Xcode 26.app/Contents/Developer"
|
||||
xcode27="$SELECTED_DEVELOPER_DIR"
|
||||
mkdir -p "$xcode26/Applications/Simulator.app"
|
||||
for scenario in selected-xcode26 selected-xcode27 launch-failure missing-app; do
|
||||
SELECTED_DEVELOPER_DIR="$xcode27"
|
||||
OPEN_FAIL=0
|
||||
case "$scenario" in
|
||||
selected-xcode26) SELECTED_DEVELOPER_DIR="$xcode26" ;;
|
||||
launch-failure) OPEN_FAIL=1 ;;
|
||||
missing-app) SELECTED_DEVELOPER_DIR="$fixture/Missing Xcode.app/Contents/Developer" ;;
|
||||
esac
|
||||
expected_app="$SELECTED_DEVELOPER_DIR/../Applications/DeviceHub.app"
|
||||
if [[ "$scenario" == selected-xcode26 || "$scenario" == missing-app ]]; then
|
||||
expected_app="$SELECTED_DEVELOPER_DIR/Applications/Simulator.app"
|
||||
fi
|
||||
rm -f "$CALL_LOG"
|
||||
: > "$OPEN_LOG"
|
||||
printf '%s\n' -a "$expected_app" > "$fixture/expected-open"
|
||||
if (cd "$fixture" && /bin/bash "$fixture/mobile-dev"); then
|
||||
[[ "$scenario" != launch-failure && "$scenario" != missing-app ]]
|
||||
diff -u "$fixture/expected" "$CALL_LOG"
|
||||
else
|
||||
[[ "$scenario" == launch-failure || "$scenario" == missing-app ]]
|
||||
[[ ! -e "$CALL_LOG" ]]
|
||||
fi
|
||||
diff -u "$fixture/expected-open" "$OPEN_LOG"
|
||||
printf 'PASS mobile-dev %s\n' "$scenario"
|
||||
done
|
||||
|
||||
21
scripts/test-mobile-me-agent-accessibility.sh
Executable file
21
scripts/test-mobile-me-agent-accessibility.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
# Exercise the production UIKit Me agent row on a booted iOS simulator.
|
||||
set -euo pipefail
|
||||
repo_root=$(cd "$(dirname "$0")/.." && pwd)
|
||||
device=${1:?Pass a booted iOS simulator UDID}
|
||||
flutter_root=$(sed -n 's/^FLUTTER_ROOT=//p' "$repo_root/mobile/ios/Flutter/Generated.xcconfig")
|
||||
frameworks="$flutter_root/bin/cache/artifacts/engine/ios/Flutter.xcframework/ios-arm64_x86_64-simulator"
|
||||
fixture=$(mktemp -d)
|
||||
trap 'rm -r "$fixture"' EXIT
|
||||
# Compile in one file to exercise the private production controller directly.
|
||||
cat "$repo_root/mobile/ios/Runner/MeAvatarLoader.swift" \
|
||||
"$repo_root/mobile/ios/Runner/NativeProfileTextEditor.swift" \
|
||||
"$repo_root/mobile/ios/NativeControlTests/MeAgentAccessibilityTests.swift" > "$fixture/test.swift"
|
||||
xcrun --sdk iphonesimulator swiftc -parse-as-library \
|
||||
-sdk "$(xcrun --sdk iphonesimulator --show-sdk-path)" \
|
||||
-target "$(uname -m)-apple-ios16.0-simulator" \
|
||||
-F "$frameworks" -framework Flutter \
|
||||
-Xlinker -rpath -Xlinker "$frameworks" \
|
||||
"$fixture/test.swift" \
|
||||
-o "$fixture/me-agent-tests"
|
||||
xcrun simctl spawn "$device" "$fixture/me-agent-tests"
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -44,6 +44,20 @@ grep -Fq "actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10" <<<"$merge_
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Staging auto-promotes the newest main-<ts>-<7> gateway image, so the gateway
|
||||
# must publish that tag and only from a source the relay CI qualified. Match
|
||||
# whole lines (-x) so a commented-out or otherwise inactive copy cannot satisfy
|
||||
# the contract after the live line regresses.
|
||||
gateway_merge_job=$(awk '/^ push-gateway-merge:/{f=1} f&&/^ [a-z][a-z_-]*:$/&&!/^ push-gateway-merge:/{exit} f' "$workflow")
|
||||
grep -Fxq " needs: [push-gateway-build, qualify]" <<<"$gateway_merge_job" || {
|
||||
echo "push gateway image must publish only after same-SHA CI qualification" >&2
|
||||
exit 1
|
||||
}
|
||||
grep -Fxq " type=raw,value=main-{{commit_date 'YYYYMMDDHHmmss'}}-{{sha}},enable=\${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}" <<<"$gateway_merge_job" || {
|
||||
echo "push gateway image must publish the sortable main-<commit UTC>-<7> tag on main pushes" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
select_run() {
|
||||
jq -r --arg source_sha aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -f "$selector" | jq -r '.id // empty'
|
||||
}
|
||||
|
||||
@@ -52,6 +52,15 @@ fi
|
||||
|
||||
grep -q 'verify-release-ref\.sh' "$repo_root/.github/workflows/release.yml"
|
||||
grep -q 'verify-release-ref\.sh' "$repo_root/.github/workflows/docker.yml"
|
||||
# Kargo's buzz-image and buzz-push-gateway-image warehouses select Lexical over
|
||||
# ^main-[0-9]{14}-[0-9a-f]{7}$; a drifted format stops Freight discovery
|
||||
# silently, so pin all three image legs (relay, relay-debug, push gateway).
|
||||
sortable_main_tag=" type=raw,value=main-{{commit_date 'YYYYMMDDHHmmss'}}-{{sha}},enable=\${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}"
|
||||
sortable_count="$(grep -Fxc "$sortable_main_tag" "$repo_root/.github/workflows/docker.yml" || true)"
|
||||
if [ "$sortable_count" != 3 ]; then
|
||||
echo "docker.yml must publish the sortable main-<UTC>-<sha7> tag on all three image legs (found $sortable_count)" >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -q 'test-release-ref-contract\.sh' "$repo_root/.github/workflows/ci.yml"
|
||||
"$repo_root/scripts/test-signed-canary-contract.sh"
|
||||
"$repo_root/scripts/test-desktop-release-cache-key.sh"
|
||||
|
||||
Reference in New Issue
Block a user