Skip to content

Upstream Stable Sync #154

Upstream Stable Sync

Upstream Stable Sync #154

Workflow file for this run

name: Upstream Stable Sync
on:
schedule:
- cron: '17 * * * *'
workflow_dispatch:
inputs:
upstream_tag:
description: 'Optional exact official stable tag; empty resolves latest stable'
required: false
default: ''
type: string
auto_promote:
description: 'For safe candidates only: merge checked PR, release and queue production approval'
required: true
default: true
type: boolean
permissions:
actions: write
contents: write
pull-requests: write
issues: write
concurrency:
group: upstream-stable-sync
cancel-in-progress: false
jobs:
sync:
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }}
UPSTREAM_REPOSITORY: Wei-Shaw/sub2api
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
fetch-depth: 0
ref: main
- name: Test the trusted risk classifier
run: |
python3 .github/scripts/test_upstream_risk.py
python3 .github/scripts/test_upstream_automation_contract.py
- name: Resolve latest stable release
id: release
env:
REQUESTED_TAG: ${{ inputs.upstream_tag }}
shell: bash
run: |
set -euo pipefail
if [[ -n "$REQUESTED_TAG" ]]; then
[[ "$REQUESTED_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]
payload=$(gh api "repos/${UPSTREAM_REPOSITORY}/releases/tags/${REQUESTED_TAG}")
else
payload=$(gh api "repos/${UPSTREAM_REPOSITORY}/releases/latest")
fi
tag=$(jq -er 'select(.draft == false and .prerelease == false) | .tag_name' <<<"$payload")
[[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]
current=$(tr -d '\r\n' < .downstream/upstream-base)
[[ "$current" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]
[[ "$(printf '%s\n%s\n' "$current" "$tag" | sort -V | head -n1)" == "$current" ]]
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "base=$current" >> "$GITHUB_OUTPUT"
[[ "$tag" == "$current" ]] && echo 'changed=false' >> "$GITHUB_OUTPUT" || echo 'changed=true' >> "$GITHUB_OUTPUT"
- name: Analyze and merge release into candidate branch
if: steps.release.outputs.changed == 'true'
id: candidate
shell: bash
run: |
set -euo pipefail
tag='${{ steps.release.outputs.tag }}'
base='${{ steps.release.outputs.base }}'
branch="sync/upstream-${tag#v}"
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
git remote add upstream "https://github.com/${UPSTREAM_REPOSITORY}.git" 2>/dev/null || true
git fetch upstream "refs/tags/${base}:refs/tags/${base}" "refs/tags/${tag}:refs/tags/${tag}" --force
python3 .github/scripts/upstream_risk.py \
--repo . --base "$base" --tag "$tag" \
--output .downstream/upstream-risk.json --github-output "$GITHUB_OUTPUT"
old_remote=$(git ls-remote --heads origin "refs/heads/${branch}" | awk '{print $1}')
if [[ -n "$old_remote" ]]; then
echo "Candidate $branch already exists; preserve manual resolutions and require Upstream Reviewed Promotion." >&2
printf '## Upstream %s candidate already exists\\n\\n' "$tag" > /tmp/upstream-conflict.md
printf 'The sync workflow did not overwrite the existing candidate branch.\\n\\n' >> /tmp/upstream-conflict.md
printf 'Inspect and resolve the existing candidate PR, then run Upstream Reviewed Promotion.\\n' >> /tmp/upstream-conflict.md
python3 -c 'import json; d=json.load(open(".downstream/upstream-risk.json")); d["merge_conflicts"]=["existing_candidate_branch"]; print(json.dumps(d,indent=2))' > /tmp/upstream-conflict.json
echo 'conflict=true' >> "$GITHUB_OUTPUT"
echo "branch=$branch" >> "$GITHUB_OUTPUT"
exit 0
fi
git switch -C "$branch" origin/main
set +e
git merge --no-ff --no-commit "$tag"
merge_rc=$?
set -e
if ((merge_rc != 0)); then
mapfile -t conflict_files < <(git diff --name-only --diff-filter=U | LC_ALL=C sort -u)
git merge --abort || true
{
printf '## Upstream %s requires manual conflict resolution\n\n' "$tag"
printf 'The hourly sync stopped without changing `main`.\n\n### Conflicted paths\n\n'
if ((${#conflict_files[@]} == 0)); then
printf -- '- Merge failed before Git reported conflicted paths.\n'
else
printf -- '- `%s`\n' "${conflict_files[@]}"
fi
} > /tmp/upstream-conflict.md
python3 -c 'import json,sys; d=json.load(open(".downstream/upstream-risk.json")); d["merge_conflicts"]=sys.argv[1:]; print(json.dumps(d,indent=2))' "${conflict_files[@]}" > /tmp/upstream-conflict.json
echo 'conflict=true' >> "$GITHUB_OUTPUT"
echo "branch=$branch" >> "$GITHUB_OUTPUT"
exit 0
fi
printf '%s\n' "$tag" > .downstream/upstream-base
git add .downstream/upstream-base .downstream/upstream-risk.json
git commit -m "chore: merge upstream ${tag}"
if [[ -n "$old_remote" ]]; then
git push --force-with-lease="refs/heads/${branch}:${old_remote}" origin "HEAD:refs/heads/${branch}"
else
git push origin "HEAD:refs/heads/${branch}"
fi
echo 'conflict=false' >> "$GITHUB_OUTPUT"
echo "branch=$branch" >> "$GITHUB_OUTPUT"
- name: Create or update conflict issue
if: steps.release.outputs.changed == 'true' && steps.candidate.outputs.conflict == 'true'
shell: bash
run: |
set -euo pipefail
tag='${{ steps.release.outputs.tag }}'
title="Upstream ${tag} merge conflicts"
gh label create --repo "$GITHUB_REPOSITORY" upstream-review-required --color B60205 --description 'Manual upstream review is required' --force
issue=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --search "${title} in:title" --json number,title --jq ".[] | select(.title == \"${title}\") | .number" | head -n1)
if [[ -n "$issue" ]]; then
gh issue edit "$issue" --repo "$GITHUB_REPOSITORY" --body-file /tmp/upstream-conflict.md --add-label upstream-review-required
else
gh issue create --repo "$GITHUB_REPOSITORY" --title "$title" --body-file /tmp/upstream-conflict.md --label upstream-review-required
fi
- name: Create or update candidate pull request
if: steps.release.outputs.changed == 'true' && steps.candidate.outputs.conflict == 'false'
env:
AUTO_PROMOTE: ${{ github.event_name == 'schedule' || inputs.auto_promote }}
shell: bash
run: |
set -euo pipefail
branch='${{ steps.candidate.outputs.branch }}'
tag='${{ steps.release.outputs.tag }}'
risk='${{ steps.candidate.outputs.risk_class }}'
overlap='${{ steps.candidate.outputs.overlap_file_count }}'
critical='${{ steps.candidate.outputs.critical_file_count }}'
gh label create --repo "$GITHUB_REPOSITORY" upstream-safe-candidate --color 0E8A16 --description 'Eligible for checked auto-merge' --force
gh label create --repo "$GITHUB_REPOSITORY" upstream-review-required --color B60205 --description 'Manual upstream review is required' --force
gh label create --repo "$GITHUB_REPOSITORY" upstream-reviewed --color 5319E7 --description 'Human upstream overlap review completed' --force
body=$(mktemp)
cat >"$body" <<EOF
Stable upstream candidate for **${tag}**.
- Risk class: \`${risk}\`
- Downstream overlap files: \`${overlap}\`
- Critical upstream paths: \`${critical}\`
- Full machine-readable report: [\`.downstream/upstream-risk.json\`](https://github.com/${GITHUB_REPOSITORY}/blob/${branch}/.downstream/upstream-risk.json)
Production remains protected by the \`production\` Environment. This PR cannot deploy directly.
EOF
pr=$(gh pr list --repo "$GITHUB_REPOSITORY" --state open --head "$branch" --json number --jq '.[0].number // empty')
if [[ -z "$pr" ]]; then
gh pr create --repo "$GITHUB_REPOSITORY" --base main --head "$branch" --title "chore: merge upstream ${tag}" --body-file "$body"
pr=$(gh pr list --repo "$GITHUB_REPOSITORY" --state open --head "$branch" --json number --jq '.[0].number')
else
gh pr edit "$pr" --repo "$GITHUB_REPOSITORY" --title "chore: merge upstream ${tag}" --body-file "$body"
fi
gh pr edit "$pr" --repo "$GITHUB_REPOSITORY" --remove-label upstream-safe-candidate --remove-label upstream-review-required 2>/dev/null || true
if [[ "$risk" == safe ]]; then
gh pr edit "$pr" --repo "$GITHUB_REPOSITORY" --add-label upstream-safe-candidate
gh pr merge "$pr" --repo "$GITHUB_REPOSITORY" --auto --merge
else
gh pr merge "$pr" --repo "$GITHUB_REPOSITORY" --disable-auto 2>/dev/null || true
gh pr edit "$pr" --repo "$GITHUB_REPOSITORY" --add-label upstream-review-required
fi
# Bot-created pushes cannot be relied on to trigger downstream workflows.
for workflow in backend-ci.yml security-scan.yml downstream-verify.yml; do
gh workflow run "$workflow" --repo "$GITHUB_REPOSITORY" --ref "$branch"
done
gh workflow run upstream-risk-gate.yml --repo "$GITHUB_REPOSITORY" --ref "$branch" -f pr_number="$pr"
if [[ "$risk" == safe && "$AUTO_PROMOTE" == true ]]; then
gh workflow run upstream-promote.yml --repo "$GITHUB_REPOSITORY" --ref main \
-f pr_number="$pr" -f confirmation=PROMOTE
else
echo "Candidate PR #$pr requires review; use Upstream Reviewed Promotion after resolution."
fi
- name: Upload conflict report
if: always() && steps.candidate.outputs.conflict == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: upstream-${{ steps.release.outputs.tag }}-merge-conflicts
path: |
/tmp/upstream-conflict.md
/tmp/upstream-conflict.json
if-no-files-found: error
retention-days: 30