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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions .github/workflows/migration-tooling-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Quality gates for the major-scoped-channels migration tooling (epic #657 F5).
#
# Triggered on any PR that touches:
# - scripts/migrate-major-channels.sh
# - scripts/lib/ring-pins.sh (the shared pin model it builds on)
# - test/scripts/migrate-major-channels/**
#
# Gates (all must pass before merge):
# 1. shellcheck — static analysis for the migration helper + shared lib
# 2. bats — DRY_RUN v-tag planning, idempotency, and the guarded
# bare-tier retirement path
#
# Standard: the migration helper is DRY_RUN-aware and mutates only through the gh
# Git-refs API (never a direct push). See petry-projects/.github#698.

name: Migration Tooling Tests

on:
pull_request:
paths:
- 'scripts/migrate-major-channels.sh'
- 'scripts/lib/ring-pins.sh'
- 'test/scripts/migrate-major-channels/**'
- '.github/workflows/migration-tooling-tests.yml'
push:
branches:
- main
paths:
- 'scripts/migrate-major-channels.sh'
- 'scripts/lib/ring-pins.sh'
- 'test/scripts/migrate-major-channels/**'
- '.github/workflows/migration-tooling-tests.yml'

permissions:
contents: read

concurrency:
group: migration-tooling-tests-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: Lint and bats
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 1
persist-credentials: false

- name: Install bats, shellcheck, and jq
run: |
set -euo pipefail
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends bats shellcheck jq
Comment thread
don-petry marked this conversation as resolved.

- name: shellcheck
run: |
set -euo pipefail
shellcheck --severity=warning -x \
scripts/migrate-major-channels.sh \
scripts/lib/ring-pins.sh

- name: Run bats suite
run: |
bats --print-output-on-failure \
test/scripts/migrate-major-channels/
8 changes: 7 additions & 1 deletion .github/workflows/standards-deploy-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,21 @@ on:
paths:
- 'scripts/deploy-standard-workflows.sh'
- 'scripts/lib/standards-deploy.sh'
- 'scripts/lib/ring-pins.sh'
- 'test/scripts/deploy-standard-workflows/**'
- 'test/scripts/lib/standards-deploy.bats'
- 'test/scripts/lib/ring-pins.bats'
- '.github/workflows/standards-deploy-tests.yml'
push:
branches:
- main
paths:
- 'scripts/deploy-standard-workflows.sh'
- 'scripts/lib/standards-deploy.sh'
- 'scripts/lib/ring-pins.sh'
- 'test/scripts/deploy-standard-workflows/**'
- 'test/scripts/lib/standards-deploy.bats'
- 'test/scripts/lib/ring-pins.bats'
- '.github/workflows/standards-deploy-tests.yml'

permissions:
Expand Down Expand Up @@ -62,10 +66,12 @@ jobs:
set -euo pipefail
shellcheck --severity=warning -x \
scripts/deploy-standard-workflows.sh \
scripts/lib/standards-deploy.sh
scripts/lib/standards-deploy.sh \
scripts/lib/ring-pins.sh

- name: Run bats suite
run: |
bats --print-output-on-failure \
test/scripts/lib/standards-deploy.bats \
test/scripts/lib/ring-pins.bats \
test/scripts/deploy-standard-workflows/
74 changes: 70 additions & 4 deletions scripts/deploy-standard-workflows.sh
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,61 @@ is_already_compliant() {
while IFS= read -r ref; do
[[ -n "$ref" ]] && grep -qF "${prefix}@${ref}" <<< "$existing_content" && return 0
done < <(ring_accepted_refs "$base" "$repo")
# Transition to major-scoped channels (#657 F5): a stub already pinned to the
# repo's tier-correct `v<M>-<tier>` form (any major) is compliant too, so the
# sweep accepts both the bare and the v-form during migration. A WRONG-tier
# v-form is not accepted here and stays drift.
local existing_ref
existing_ref=$(grep -oE "@${base}/[^[:space:]\"']+" <<< "$existing_content" | head -1)
existing_ref="${existing_ref#@}"
if [[ -n "$existing_ref" ]] && ring_vform_tier_aligned "$existing_ref" "$base" "$repo"; then
return 0
fi
return 1
fi
fi

grep -qF "$expected_uses" <<< "$existing_content" && return 0 || return 1
}

# reusable_uses_of <template> -> the template's first reusable `uses:` value
# (org/repo/…/<base>-reusable.yml@<ref>), comment/CR stripped, or empty.
reusable_uses_of() {
grep -E '^[[:space:]]*uses:' "$1" | head -1 \
| sed 's/^[[:space:]]*uses:[[:space:]]*//' | sed 's/[[:space:]]*#.*//' | tr -d '\r'
}

# reusable_base_of <template> -> the ring channel base (e.g. `auto-rebase`) of the
# template's reusable, or empty if the template does not call a `-reusable.yml`.
reusable_base_of() {
local prefix; prefix="$(reusable_uses_of "$1")"; prefix="${prefix%@*}"
[[ "$prefix" =~ /([a-z0-9-]+)-reusable\.yml$ ]] && printf '%s' "${BASH_REMATCH[1]}"
return 0
}

# reusable_host_of <template> -> the org/repo that HOSTS the reusable (and thus
# carries its release tags), e.g. `petry-projects/.github-private` for dev-lead.
reusable_host_of() {
local prefix; prefix="$(reusable_uses_of "$1")"; prefix="${prefix%@*}"
cut -d/ -f1-2 <<< "$prefix"
return 0
}

# emit_ref_for <template> <repo> -> the channel ref a (re)deployed stub in <repo>
# should pin (#657 F5). For a ring-managed reusable it is the repo's tier channel,
# major-scoped `v<M>-<tier>` when the agent has a release, else the bare `<tier>`
# form. Empty for a non-ring template (deployed verbatim). Requires GH_TOKEN.
emit_ref_for() {
local template="$1" repo="$2" base host major
base="$(reusable_base_of "$template")"
[[ -z "$base" ]] && return 0
ring_is_ring_reusable "$base" || return 0
host="$(reusable_host_of "$template")"
major="$(ring_host_current_major "$host" "$base")"
ring_canonical_ref "$base" "$repo" "$major"
return 0
}

# Build the PR body for a batched stub deployment (one PR per repo).
batch_pr_body() {
local w lines=""
Expand All @@ -207,10 +255,14 @@ deploy_repo() {
if [[ "$opts_in_any" == "false" ]]; then skip "$repo (exempt)"; return; fi
fi

# Collect the drifted stubs for this repo (path/template pairs + names).
local -a paths=() templates=() names=()
local workflow template target_path raw existing_sha existing_content
# Collect the drifted stubs for this repo (path/template pairs + names). For
# ring-managed reusables the deployed template is REWRITTEN to pin the repo's
# tier channel (major-scoped `v<M>-<tier>` when the agent has a release) — the
# emit ref (#657 F5). Rewritten templates land in temp files cleaned up below.
local -a paths=() templates=() names=() emits=() tmpfiles=()
local workflow template target_path raw existing_sha existing_content emit deploy_template base
for workflow in "${WORKFLOWS[@]}"; do
Comment thread
don-petry marked this conversation as resolved.
base=""
if is_skipped_repo "$repo" && ! repo_opts_into "$repo" "$workflow"; then
skip "$repo/$workflow (exempt)"
continue
Expand All @@ -228,7 +280,15 @@ deploy_repo() {
skip "$repo/$target_path already compliant"
continue
fi
paths+=("$target_path"); templates+=("$template"); names+=("$workflow")
emit="$(emit_ref_for "$template" "$repo")"
deploy_template="$template"
if [[ -n "$emit" ]] && [[ "$DRY_RUN" != "true" ]]; then
base="$(reusable_base_of "$template")"
deploy_template="$(mktemp)"
ring_repin_uses "$base" "$emit" < "$template" > "$deploy_template"
tmpfiles+=("$deploy_template")
fi
paths+=("$target_path"); templates+=("$deploy_template"); names+=("$workflow"); emits+=("$emit")
done

[[ "${#names[@]}" -eq 0 ]] && return # nothing drifted for this repo
Expand All @@ -239,6 +299,10 @@ deploy_repo() {
local title="chore: sync ${n} org-standard workflow stub(s) from ${ORG}/.github"

if [[ "$DRY_RUN" == "true" ]]; then
local i
for (( i = 0; i < n; i++ )); do
[[ -n "${emits[i]}" ]] && dry "$repo/${names[i]} would pin @${emits[i]}"
done
dry "Would open PR for $repo (branch $branch) — ${n} stub(s): $list"
return
fi
Expand All @@ -251,6 +315,8 @@ deploy_repo() {
body=$(batch_pr_body "${names[@]}")
outcome=$(sd_deploy_files_via_pr "$ORG/$repo" "$branch" "$SYNC_LABEL" "$title" "$body" "${filepairs[@]}") || true

[[ "${#tmpfiles[@]}" -gt 0 ]] && rm -f "${tmpfiles[@]}"

case "$outcome" in
"OPENED "*) ok "$repo — opened ${outcome#OPENED } (${n} stub(s): $list)" ;;
"SKIP_PR_OPEN "*) skip "$repo — sync PR #${outcome#SKIP_PR_OPEN } already open" ;;
Expand Down
59 changes: 59 additions & 0 deletions scripts/lib/ring-pins.sh
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,62 @@ ring_legacy_csv() {
ring_accepted_refs "$1" "$2" | tail -n +2 | paste -sd, -
return 0
}

# ══ major-scoped channels tooling — F5 shared helpers (epic #657) ══════════════

# ring_highest_major <version>... -> the MAJOR of the highest strict semver among
# the arguments, or empty if none is a valid `[v]X.Y.Z`. Tolerates a leading `v`
# on a token; non-semver tokens are ignored. Pure; always returns 0.
ring_highest_major() {
local v major best=""
for v in "$@"; do
v="${v#v}"
[[ "$v" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]] || continue
major="${BASH_REMATCH[1]}"
if [ -z "$best" ] || [ "$major" -gt "$best" ]; then
best="$major"
fi
done
[ -n "$best" ] && printf '%s' "$best"
return 0
}

# ring_host_current_major <host-repo> <channel-base> -> the current major line for
# an agent, derived from its release tags `<base>/vX.Y.Z` on the host repo, or
# empty if the agent has no release (so callers fall back to the bare-tier form).
# gh-backed: reads matching-refs for `<base>/v` and feeds the semver tokens to
# ring_highest_major. Requires GH_TOKEN in the environment.
ring_host_current_major() {
local host="$1" base="$2" refs
refs="$(gh api "repos/${host}/git/matching-refs/tags/${base}/v" \
--jq '.[]?.ref' 2>/dev/null \
| sed -n "s#^refs/tags/${base}/v##p")" || {
echo "Warning: failed to fetch matching refs for ${host}/${base}" >&2
return 0
}
# shellcheck disable=SC2086
ring_highest_major $refs
return 0
}

# ring_repin_uses <channel-base> <newref> -> rewrite a workflow stub read on stdin
# so its reusable `uses:` ref (and any matching `agent_ref:`) points at <newref>.
# Only lines referencing THIS agent's reusable are touched; the trailing comment
# on a `uses:` line is preserved. Pure (sed only).
ring_repin_uses() {
local base="$1" newref="$2"
sed -E \
-e "s#^([[:space:]]*uses:[[:space:]]*petry-projects/[^@[:space:]]*/${base}-reusable\.yml)@[^[:space:]]+#\1@${newref}#" \
-e "s#^([[:space:]]*agent_ref:[[:space:]]*[\"']?)${base}/[^\"'[:space:]]+#\1${newref}#"
return 0
}

# ring_vform_tier_aligned <pinned-ref> <channel-base> <repo> -> 0 iff <pinned-ref>
# is a major-scoped v-form `<base>/v<M>-<tier>` whose tier matches <repo>'s ring
# tier (any major). A bare-tier ref (no `v<M>-`) or a wrong-tier v-form is not
# aligned. Pure.
ring_vform_tier_aligned() {
local ref="$1" base="$2" repo="$3" tier
tier="$(ring_tier_for_repo "$repo")"
[[ "$ref" =~ ^${base}/v[0-9]+-${tier}$ ]]
}
Loading
Loading