From 7539c3aec99d7dc9ff9ff21c6faa24bf44c6050e Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Tue, 2 Jun 2026 03:39:22 +0300 Subject: [PATCH] chore(mirror): remove force_workspace reusable + smoke (R4 mothballed) R4 force_workspace is obsolete: Mergify's ignore_conflicts:true config already creates the backport workspace PR, resolves toward theirs, and applies backport-conflict for path-drift conflicts (canary-verified on VyOS-Networks/ipaddrcheck#26/#27). The reusable's 'No backport have been created' trigger never fires under that config. Source wrappers already removed across the 13 consumers; consumers.yaml schema removal follows. T8943 / IS-510. --- .../workflows/force-workspace-backport.yml | 393 ------------------ .../force-workspace-parser-smoke.yml | 114 ----- 2 files changed, 507 deletions(-) delete mode 100644 .github/workflows/force-workspace-backport.yml delete mode 100644 .github/workflows/force-workspace-parser-smoke.yml diff --git a/.github/workflows/force-workspace-backport.yml b/.github/workflows/force-workspace-backport.yml deleted file mode 100644 index ff88488..0000000 --- a/.github/workflows/force-workspace-backport.yml +++ /dev/null @@ -1,393 +0,0 @@ -name: force_workspace backport companion (reusable) - -on: - workflow_call: - inputs: - mergify_comment_id: - required: true - type: string - mergify_comment_body: - required: true - type: string - target_pr_number: - required: true - type: number - -permissions: - contents: write - pull-requests: write - -# Serialize force_workspace runs per target PR so two near-simultaneous triggers can't both pass -# the idempotency check before either PR exists (adversarial round-2 finding). Queue, don't cancel — -# the second run then sees the first's PR via idempotency and no-ops. -concurrency: - group: force-workspace-${{ github.repository }}-${{ inputs.target_pr_number }} - cancel-in-progress: false - -jobs: - parse-and-validate: - runs-on: ubuntu-latest - outputs: - sha: ${{ steps.parse.outputs.sha }} - target_branch: ${{ steps.parse.outputs.target_branch }} - consumer_force_workspace_enabled: ${{ steps.validate.outputs.enabled }} - consumer_backport_branches: ${{ steps.validate.outputs.consumer_backport_branches }} - skip: ${{ steps.idempotency.outputs.skip }} - steps: - # NOTE: no third-party action (e.g. Bullfrog) before the token-minting step — keeping - # untrusted actions out of jobs that handle secrets.APP_PRIVATE_KEY avoids a token-theft - # surface (adversarial-review finding). Egress-audit belongs on no-secret jobs only. - # get-token is a COMPOSITE action (v7) — called at step level; output is job-local. - - name: Mint App token - id: token - uses: vyos/.github/.github/actions/get-token@production - with: - owner: VyOS-Networks - client-id: ${{ vars.APP_CLIENT_ID }} - private-key: ${{ secrets.APP_PRIVATE_KEY }} - # F3 (provenance): re-verify the trigger inside the reusable (do NOT trust the wrapper's - # `if:` alone). Re-FETCH the comment by id with the App token (authoritative — don't trust - # the passed body either), assert author == mergify[bot] AND body contains the failure marker. - - name: Re-verify Mergify provenance + parse (anchored) - id: parse - env: - GH_TOKEN: ${{ steps.token.outputs.token }} - REPO: ${{ github.repository }} - COMMENT_ID: ${{ inputs.mergify_comment_id }} - run: | - set -euo pipefail - # Authoritative re-fetch of the comment (issue comments REST: PR comments are issue comments). - comment_json=$(gh api "repos/${REPO}/issues/comments/${COMMENT_ID}") - author=$(echo "$comment_json" | jq -r '.user.login') - if [ "$author" != "mergify[bot]" ]; then - echo "::error::provenance check failed — comment author is '$author', not 'mergify[bot]'; aborting" - exit 1 - fi - echo "$comment_json" | jq -r '.body' > /tmp/mergify_comment_body.txt - if ! grep -qF "No backport have been created" /tmp/mergify_comment_body.txt; then - echo "::error::provenance check failed — body lacks the 'No backport have been created' marker; aborting" - exit 1 - fi - # F5 (anchoring + ambiguity): extract the SHA + target branch, requiring EXACTLY ONE - # distinct candidate of each across the comment. The whitelist restricts branches to the - # active LTS vocabulary (equuleus/1.3 dropped 2026-06-01 — EOL; add a token when an LTS opens). - BODY="$(cat /tmp/mergify_comment_body.txt)" python3 - <<'PYEOF' >> "$GITHUB_OUTPUT" - import os, re, sys - body = os.environ["BODY"] - # Anchored to Mergify's REAL "No backport have been created" failure wording, captured verbatim - # from in-prod abort VyOS-Networks/vyos-1x#2116 (2026-05-15): - # #### ❌ No backport have been created - # * Backport to branch `circinus` failed due to conflicts - # Cherry-pick of 6df7ca2f61ce1aa8352e8dbb1fc6e9c5d6d42937 has failed: - # Branch name is wrapped in backticks; SHA is introduced by "Cherry-pick of <40hex>". - # (The earlier "(?:commit|Backport of)" / bare-"to branch" anchors never matched real output — - # caught at the Rollout 4 canary pre-flight; the smoke fixtures had mirrored the wrong wording.) - BRANCH_RE = r"to branch\s+`?(sagitta|circinus)`?\b" - # SHA anchored to the failed-cherry-pick sentence so an unrelated bare 40-hex elsewhere - # (URL, prior-PR reference) is NOT picked up (F5/anchoring). "Backport of " kept as a - # robustness alternate; the short 9-hex "commit 6df7ca2f6" line is correctly ignored ({40}). - SHA_RE = r"\b(?:[Cc]herry-pick of|Backport of)\s+(?:commit\s+)?([0-9a-f]{40})\b" - shas = sorted(set(re.findall(SHA_RE, body, re.IGNORECASE))) - branches = sorted({m.lower() for m in re.findall(BRANCH_RE, body, re.IGNORECASE)}) - if len(shas) != 1 or len(branches) != 1: - print(f"Parse failed/ambiguous — distinct SHAs={shas}, branches={branches} (require exactly 1 each)", file=sys.stderr) - print("\n".join(body.splitlines()[:20]), file=sys.stderr) - sys.exit(1) - print(f"sha={shas[0]}") - print(f"target_branch={branches[0]}") - print(f"Parsed: SHA={shas[0]}, target_branch={branches[0]}", file=sys.stderr) - PYEOF - - - name: Read consumers.yaml + validate - id: validate - env: - GH_TOKEN: ${{ steps.token.outputs.token }} - REPO: ${{ github.repository }} - run: | - set -euo pipefail - # Fetch consumers.yaml from VyOS-Networks/.github@production (relocated in Rollout 2 v5; - # private repo — the App token has Contents:read on it). - curl -sH "Authorization: token $GH_TOKEN" \ - "https://api.github.com/repos/VyOS-Networks/.github/contents/mirror/consumers.yaml?ref=production" \ - | jq -r '.content' | base64 -d > /tmp/consumers.yaml - - # Find the consumer entry where target == this repo. REPO is passed via env and read with - # os.environ — NOT interpolated into the python3 source (F6 hardening; no injection surface). - consumer=$(REPO="$REPO" python3 - <<'PYEOF' - import yaml, json, sys, os - repo = os.environ["REPO"] - d = yaml.safe_load(open('/tmp/consumers.yaml')) - for c in d['consumers']: - if c['target'] == repo: - print(json.dumps(c)) - sys.exit(0) - print('') - PYEOF - ) - if [ -z "$consumer" ]; then - echo "Repo $REPO not found in consumers.yaml; aborting" >&2 - exit 1 - fi - enabled=$(echo "$consumer" | jq -r '.force_workspace_enabled // false') - backport_branches=$(echo "$consumer" | jq -c '.backport_branches // ["sagitta","circinus"]') - echo "enabled=$enabled" >> $GITHUB_OUTPUT - # Output name matches the job-level `outputs.consumer_backport_branches` mapping. - echo "consumer_backport_branches=$backport_branches" >> $GITHUB_OUTPUT - - - name: Validate target branch is in allowlist - env: - TARGET_BRANCH: ${{ steps.parse.outputs.target_branch }} - ALLOWLIST: ${{ steps.validate.outputs.consumer_backport_branches }} - run: | - if ! echo "$ALLOWLIST" | jq -e --arg b "$TARGET_BRANCH" 'index($b) != null' >/dev/null; then - echo "target_branch $TARGET_BRANCH not in allowlist $ALLOWLIST; aborting" >&2 - exit 1 - fi - echo "target_branch validated against allowlist" - - - name: Idempotency check — does force-backport branch already exist? - id: idempotency - env: - GH_TOKEN: ${{ steps.token.outputs.token }} - REPO: ${{ github.repository }} - SHA: ${{ steps.parse.outputs.sha }} - TARGET: ${{ steps.parse.outputs.target_branch }} - run: | - set -euo pipefail - ref_name="force-backport/${SHA}/${TARGET}" - skip=false - # Idempotency: SKIP only when a PR for this deterministic head already exists (any state) — - # the work is done or in progress. A bare BRANCH with NO PR is NOT a skip: it is a failed - # prior run (pushed the branch, then errored before opening the PR). Treating branch-existence - # as a terminal skip would strand it forever (adversarial-review finding) — instead RECOVER - # (the push step force-overwrites the orphaned branch in our deterministic namespace, then the - # PR is opened). PR existence is checked across ALL states (a branch + its PR can diverge). - # Count only SAME-REPO PRs with that head — a fork/cross-repo PR could reuse the head - # branch name to poison this check and block recovery (adversarial round-2 finding). - existing_pr=$(gh pr list --repo "${REPO}" --head "${ref_name}" --state all \ - --json number,isCrossRepository --jq '[.[] | select(.isCrossRepository == false)] | length') - if [ "${existing_pr:-0}" -gt 0 ]; then - skip=true - echo "Idempotent skip: a PR with head ${ref_name} already exists (state all)" >&2 - # Singular `git/ref/heads/` → HTTP 404 when ABSENT (the plural `git/refs/...` would - # return [] with 200 for an absent ref — the prior always-skip bug). - elif gh api "repos/${REPO}/git/ref/heads/${ref_name}" >/dev/null 2>&1; then - echo "::warning::orphaned branch refs/heads/${ref_name} exists with NO PR (failed prior run) — recovering: force-overwrite + open PR" >&2 - fi - echo "skip=${skip}" >> "$GITHUB_OUTPUT" - [ "$skip" = "true" ] || echo "proceeding for ${ref_name}" - - cherry-pick-and-force-stage: - needs: [parse-and-validate] - # Gate on BOTH per-consumer opt-in AND idempotency check (skip if PR/branch already exists). - if: | - needs.parse-and-validate.outputs.consumer_force_workspace_enabled == 'true' - && needs.parse-and-validate.outputs.skip != 'true' - runs-on: ubuntu-latest - steps: - # NOTE: no third-party action before the token-minting step (token-theft-surface avoidance — - # adversarial-review finding). This job clones + force-pushes + opens a PR with the App token. - # Composite get-token (v7) — minted fresh in this job (outputs are job-local). - - name: Mint App token - id: token - uses: vyos/.github/.github/actions/get-token@production - with: - owner: VyOS-Networks - client-id: ${{ vars.APP_CLIENT_ID }} - private-key: ${{ secrets.APP_PRIVATE_KEY }} - - name: Clone target repo - env: - GH_TOKEN: ${{ steps.token.outputs.token }} - REPO: ${{ github.repository }} - run: | - git clone "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" repo - cd repo - git config user.name "vyos-bot[bot]" - git config user.email "vyos-bot[bot]@users.noreply.github.com" - - - name: Cherry-pick + force-stage unmerged paths (resolve toward source / "theirs") - id: forcestage - env: - SHA: ${{ needs.parse-and-validate.outputs.sha }} - TARGET: ${{ needs.parse-and-validate.outputs.target_branch }} - run: | - set -euo pipefail - cd repo - # Reachability (spec §4.2.8 step 3): refuse to force-stage a SHA that is not actually on - # the mirrored source branch — guards against a crafted/parsed SHA that isn't a real merge. - src_branch=$(git rev-parse --abbrev-ref origin/HEAD | sed 's@^origin/@@') - if ! git merge-base --is-ancestor "$SHA" "origin/${src_branch}"; then - echo "::error::SHA $SHA is not reachable from origin/${src_branch} (the mirrored source branch); refusing to force-stage" - exit 1 - fi - git checkout "$TARGET" - # --no-commit: never let Git auto-create a commit; staging is curated below. - if git cherry-pick --no-commit "$SHA"; then - # Unexpected CLEAN apply — the path-drift no longer reproduces (branch moved since - # Mergify's failure). Open no PR; leave the original Mergify failure for normal retry. - echo "::warning::cherry-pick of $SHA applied cleanly to $TARGET — no conflict to resolve; opening no force-staged PR" - git reset --hard HEAD; git cherry-pick --abort 2>/dev/null || true - echo "proceed=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - # Conflict path: classify + resolve each unmerged path toward the cherry-picked SOURCE - # ("theirs" in cherry-pick terms). The Python exits 2 (→ set -e → job fails RED = fail-closed, - # no push/PR) if ANY path is not deterministically resolvable. JSONL of decisions on success. - python3 - <<'PYEOF' > /tmp/decisions.jsonl - import subprocess, json, sys - # porcelain v2 unmerged record (NUL-terminated with -z): - # u

- # The PATH is the FINAL field — after the three object-id hashes. split(" ", 10) keeps a - # space-containing path intact in parts[10] (the prior split(" ",8) grabbed "

"). - out = subprocess.check_output(["git", "status", "--porcelain=v2", "-z"]) - unresolved, decisions = [], [] - for entry in out.split(b"\x00"): - if not entry: - continue - s = entry.decode("utf-8", errors="replace") - if not s.startswith("u "): - continue - parts = s.split(" ", 10) - if len(parts) < 11: - unresolved.append(s) - decisions.append({"path": s, "xy_code": "?", "classification": "unparseable", "action": "none"}) - continue - xy, sub, m1, m2, m3, path = parts[1], parts[2], parts[3], parts[4], parts[5], parts[10] - # TYPE PRE-CHECK (XY alone is insufficient): submodule / gitlink / type-change → fail-closed. - modes = [m for m in (m1, m2, m3) if m != "000000"] - types = {m[:3] for m in modes} # 100=regular, 120=symlink, 160=gitlink - if sub[:1] == "S" or "160" in types or len(types) > 1: - unresolved.append(path) - decisions.append({"path": path, "xy_code": xy, "classification": "unresolved-type", "action": "none"}) - continue - # Per-XY resolution toward source ("theirs" = stage 3 = the picked commit). - if xy in ("UU", "AA"): # content conflict — take source's version, NOT the marker file - subprocess.check_call(["git", "checkout", "--theirs", "--", path]) - subprocess.check_call(["git", "add", "--", path]); action = "checkout --theirs + add" - elif xy == "UA": # added by them (source) → take it - subprocess.check_call(["git", "add", "--", path]); action = "add (source add)" - elif xy == "UD": # deleted by them (source) → delete - subprocess.check_call(["git", "rm", "--", path]); action = "rm (source delete)" - elif xy == "DU": # deleted by us (target), modified by source → take source's file - subprocess.check_call(["git", "add", "--", path]); action = "add (source modify)" - elif xy == "DD": # both deleted - subprocess.check_call(["git", "rm", "--", path]); action = "rm (both deleted)" - else: # AU (added by us, source has no opinion) + any unknown XY → fail-closed - unresolved.append(path) - decisions.append({"path": path, "xy_code": xy, "classification": "unresolved", "action": "none"}) - continue - decisions.append({"path": path, "xy_code": xy, "classification": "resolved", "action": action}) - for d in decisions: - print(json.dumps(d)) - if unresolved: - sys.stderr.write("::error::FAIL-CLOSED — force_workspace cannot deterministically resolve: %s. No PR opened; resolve the backport manually.\n" % unresolved) - sys.exit(2) - PYEOF - # Reached only when EVERY path resolved (python exited 0). - decisions_json=$(jq -s '.' /tmp/decisions.jsonl) - { echo "DECISIONS_JSON<> "$GITHUB_ENV" - if git diff --cached --quiet; then - echo "::error::no staged changes after resolution (unexpected); aborting" - exit 1 - fi - commit_msg=$(printf '%s\n\n%s\n\n%s\n' \ - "force-staged backport of $SHA to $TARGET" \ - "Mergify aborted on a path-drift conflict; auto-resolved by the force_workspace GHA companion toward the cherry-picked source (\"theirs\"). VERIFY each decision before merging." \ - "Decisions: $decisions_json") - git commit -m "$commit_msg" - echo "proceed=true" >> "$GITHUB_OUTPUT" - - - name: Push to deterministic branch - if: steps.forcestage.outputs.proceed == 'true' - env: - SHA: ${{ needs.parse-and-validate.outputs.sha }} - TARGET: ${{ needs.parse-and-validate.outputs.target_branch }} - run: | - cd repo - branch_name="force-backport/${SHA}/${TARGET}" - # --force: recover/overwrite an orphaned branch from a failed prior run. Safe — this - # deterministic force-backport// namespace is written exclusively by this bot. - git push --force origin "HEAD:refs/heads/${branch_name}" - - - name: Ensure required labels exist on the target repo - if: steps.forcestage.outputs.proceed == 'true' - env: - GH_TOKEN: ${{ steps.token.outputs.token }} - REPO: ${{ github.repository }} - run: | - set -euo pipefail - # `gh pr create --label X` fails if label X doesn't exist on the repo. - # Idempotently create the labels we need (no-op if already present). - for label_spec in "backport-conflict:ED4C2C:Auto-applied; PR has conflict requiring manual verification" \ - "force-staged:FBCA04:Auto-resolved by force_workspace GHA; verify each decision"; do - name="${label_spec%%:*}"; rest="${label_spec#*:}" - color="${rest%%:*}"; desc="${rest#*:}" - if ! gh api "repos/$REPO/labels/$name" >/dev/null 2>&1; then - gh api -X POST "repos/$REPO/labels" \ - -f "name=$name" -f "color=$color" -f "description=$desc" - echo "Created label: $name" - else - echo "Label already exists: $name" - fi - done - - - name: Open audit-trail PR - if: steps.forcestage.outputs.proceed == 'true' - env: - GH_TOKEN: ${{ steps.token.outputs.token }} - REPO: ${{ github.repository }} - SHA: ${{ needs.parse-and-validate.outputs.sha }} - TARGET: ${{ needs.parse-and-validate.outputs.target_branch }} - DECISIONS_JSON: ${{ env.DECISIONS_JSON }} - MERGIFY_COMMENT_ID: ${{ inputs.mergify_comment_id }} - TARGET_PR_NUMBER: ${{ inputs.target_pr_number }} - run: | - set -euo pipefail - cd repo - branch_name="force-backport/${SHA}/${TARGET}" - # F7 (hardening): render the audit trail as a FENCED JSON block, not inline backticks. - # A fenced ```json block cannot be broken out of by crafted path content (no inline-code-span - # termination, newlines are literal), and `jq .` already produces valid escaped JSON. - decisions_block=$(echo "$DECISIONS_JSON" | jq '.') - body=$(cat </dev/null || echo ""` pattern. - # --assignee is optional: only set when the triggering event provides a login (issue_comment). - assignee_flag=() - if [ -n "${{ github.event.issue.user.login }}" ]; then - assignee_flag=(--assignee "${{ github.event.issue.user.login }}") - fi - pr_number=$(gh pr create \ - --repo "$REPO" \ - --base "$TARGET" \ - --head "$branch_name" \ - --title "force-staged backport of $SHA to $TARGET" \ - --body "$body" \ - --label backport-conflict \ - --label force-staged \ - "${assignee_flag[@]}") - echo "Opened PR: $pr_number" diff --git a/.github/workflows/force-workspace-parser-smoke.yml b/.github/workflows/force-workspace-parser-smoke.yml deleted file mode 100644 index 4a1b9d9..0000000 --- a/.github/workflows/force-workspace-parser-smoke.yml +++ /dev/null @@ -1,114 +0,0 @@ -# .github/workflows/force-workspace-parser-smoke.yml -name: force_workspace parser smoke test - -on: - schedule: - - cron: '0 6 * * *' # Daily at 06:00 UTC - workflow_dispatch: - -permissions: - contents: read - -jobs: - smoke: - runs-on: ubuntu-latest - steps: - - name: Bullfrog Secure Runner - continue-on-error: true - uses: bullfrogsec/bullfrog@v0.8.4 - with: - egress-policy: audit - - name: Assert parser produces EXACT expected SHA + branch on fixtures - run: | - set -euo pipefail - # Use the EXACT same parser as force-workspace-backport.yml. - # This block must stay in lockstep with the parser there. - python3 - <<'PYEOF' - import re, sys - - # Fixtures: each is (body, expected_sha, expected_branch). expected_sha=None means the - # parser MUST REJECT (ambiguous, no anchored SHA, or multi-branch) — negative cases. - # - # POSITIVE fixtures use Mergify's REAL "No backport have been created" wording, captured - # verbatim from in-prod abort VyOS-Networks/vyos-1x#2116 (2026-05-15). The branch name is - # wrapped in backticks; the SHA is introduced by "Cherry-pick of <40hex> has failed:". - # If Mergify's failure-comment format ever drifts from this, THIS test goes red — which is - # the guard the prior (fictional-wording) fixtures failed to provide. - FIXTURES = [ - # Positive (real circinus abort, verbatim from #2116): - ("""#### ❌ No backport have been created - - * Backport to branch `circinus` failed due to conflicts - - Cherry-pick of 6df7ca2f61ce1aa8352e8dbb1fc6e9c5d6d42937 has failed: - You are currently cherry-picking commit 6df7ca2f6.""", - "6df7ca2f61ce1aa8352e8dbb1fc6e9c5d6d42937", "circinus"), - # Positive (real-wording sagitta variant): - ("""#### ❌ No backport have been created - - * Backport to branch `sagitta` failed due to conflicts - - Cherry-pick of abcdef1234567890abcdef1234567890abcdef12 has failed:""", - "abcdef1234567890abcdef1234567890abcdef12", "sagitta"), - # NEGATIVE: failure branch present but the only 40-hex is an UNRELATED bare SHA in a URL - # (no Cherry-pick-of/Backport-of anchor) → anchored SHA_RE finds 0 → must REJECT. - ("""No backport have been created. - * Backport to branch `sagitta` failed due to conflicts - See https://github.com/vyos/vyos-1x/commit/0000000000000000000000000000000000000000 for context.""", - None, None), - # NEGATIVE: two distinct anchored SHAs → ambiguous → must REJECT. - ("""Cherry-pick of 1111111111111111111111111111111111111111 has failed. - Cherry-pick of 2222222222222222222222222222222222222222 has failed. - * Backport to branch `circinus` failed due to conflicts""", - None, None), - # NEGATIVE: one comment naming TWO branches (e.g. `@Mergifyio backport sagitta circinus` - # where both fail) → two distinct branches → fail-closed REJECT (force_workspace acts on - # one branch per invocation; multi-branch is left to manual fallback — documented gap). - ("""#### ❌ No backport have been created - * Backport to branch `sagitta` failed due to conflicts - Cherry-pick of 6df7ca2f61ce1aa8352e8dbb1fc6e9c5d6d42937 has failed: - * Backport to branch `circinus` failed due to conflicts""", - None, None), - ] - - # Lockstep with force-workspace-backport.yml's parse step (keep byte-identical): - BRANCH_RE = r"to branch\s+`?(sagitta|circinus)`?\b" - SHA_RE = r"\b(?:[Cc]herry-pick of|Backport of)\s+(?:commit\s+)?([0-9a-f]{40})\b" - - # Lockstep with the reusable's parser: set-based with exactly-one-distinct assertion - # (anchored/ambiguity-safe). Keep identical to force-workspace-backport.yml's parse step. - failed = False - for body, expected_sha, expected_branch in FIXTURES: - shas = sorted(set(re.findall(SHA_RE, body, re.IGNORECASE))) - branches = sorted({m.lower() for m in re.findall(BRANCH_RE, body, re.IGNORECASE)}) - rejected = (len(shas) != 1 or len(branches) != 1) - if expected_sha is None: - # Negative fixture: the parser MUST reject (ambiguous / no anchored commit SHA). - if rejected: - print(f"OK (negative): correctly rejected — shas={shas} branches={branches}") - else: - print(f"FAIL (negative): expected rejection but extracted sha={shas} branch={branches}", file=sys.stderr) - failed = True - continue - if rejected: - print(f"FAIL: ambiguous/none — shas={shas} branches={branches}", file=sys.stderr) - failed = True - continue - if shas[0] != expected_sha: - print(f"FAIL: sha mismatch — expected {expected_sha}, got {shas[0]}", file=sys.stderr) - failed = True - continue - if branches[0] != expected_branch: - print(f"FAIL: branch mismatch — expected {expected_branch}, got {branches[0]}", file=sys.stderr) - failed = True - continue - print(f"OK: sha={shas[0]} branch={branches[0]}") - if failed: - sys.exit(1) - PYEOF - - # v7: Slack notify step REMOVED — SLACK_BOT_TOKEN / SLACK_VYOS_INFRA_CHANNEL_ID are not - # provisioned in either org (verified 2026-06-01). The smoke job's own red run status on - # `schedule` is the drift alert (visible in the Actions tab + any repo-level failure - # notifications). When the GHA-failure-notifications initiative provisions Slack secrets, - # wire a notify step here at that point (it would gate `if: failure() && secrets present`).