Skip to content

Promote dev → master #155

Promote dev → master

Promote dev → master #155

name: Promote dev → master
on:
schedule:
- cron: "0 14 * * *" # UTC 14:00 = PRC 22:00 (2h before bump-electron stable run)
workflow_dispatch:
inputs:
force_skip_soak:
description: "Skip the 24h soak check. Only set true when promoting a sync-back / merge commit that is timestamp-fresh but content-stable."
type: boolean
default: false
concurrency:
group: promote-dev
cancel-in-progress: false
permissions:
contents: write
# `actions: write` is required by the final step's `gh workflow run docker-publish.yml`
# dispatch; the earlier CI status read needs only `actions: read`, but the
# broader scope subsumes it.
actions: write
checks: read
jobs:
promote:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Guard — dev branch must exist
id: dev_exists
run: |
if git ls-remote --exit-code --heads origin dev >/dev/null 2>&1; then
echo "ok=true" >> "$GITHUB_OUTPUT"
else
echo "::notice::dev branch does not exist yet, nothing to promote"
echo "ok=false" >> "$GITHUB_OUTPUT"
fi
- name: Fetch master and dev
if: steps.dev_exists.outputs.ok == 'true'
run: |
git fetch origin master:refs/remotes/origin/master --tags
git fetch origin dev:refs/remotes/origin/dev
- name: Check ahead-count
id: ahead
if: steps.dev_exists.outputs.ok == 'true'
run: |
AHEAD=$(git rev-list --count origin/master..origin/dev)
echo "ahead=$AHEAD" >> "$GITHUB_OUTPUT"
if [ "$AHEAD" -eq 0 ]; then
echo "::notice::dev is not ahead of master, nothing to promote"
else
echo "dev is $AHEAD commit(s) ahead of master"
fi
- name: Check fast-forward possible
id: ff
if: steps.ahead.outputs.ahead != '0'
run: |
if git merge-base --is-ancestor origin/master origin/dev; then
echo "ok=true" >> "$GITHUB_OUTPUT"
else
echo "ok=false" >> "$GITHUB_OUTPUT"
echo "::warning::master has commits not in dev — manual rebase needed (likely a hotfix landed directly on master)"
exit 1
fi
- name: Select promotion candidates (soak >= 24h, starvation-proof)
id: soak
if: steps.ff.outputs.ok == 'true'
env:
FORCE: ${{ inputs.force_skip_soak }}
run: |
# Old rule required dev HEAD itself to be >= 24h old; during active
# weeks every push reset the clock and master starved. The script
# instead emits the newest first-parent dev commits older than 24h
# (newest first) — fresh commits keep soaking and ride tomorrow.
CANDIDATES=$(bash .github/scripts/select-promote-candidate.sh)
if [ -z "$CANDIDATES" ]; then
echo "::notice::Soak: no dev commit is older than the 24h window yet"
echo "ok=false" >> "$GITHUB_OUTPUT"
else
echo "Candidates (newest eligible first):"
echo "$CANDIDATES"
echo "ok=true" >> "$GITHUB_OUTPUT"
{
echo "candidates<<EOF"
echo "$CANDIDATES"
echo "EOF"
} >> "$GITHUB_OUTPUT"
fi
- name: Pick first CI-green candidate
id: ci
if: steps.soak.outputs.ok == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CANDIDATES: ${{ steps.soak.outputs.candidates }}
run: |
# Gate on the latest CI - Quality gate run for this SHA. Do not scan
# commit check-runs: release retries and historical failures may
# share the same SHA and would permanently poison promotion.
PICKED=""
while read -r SHA; do
[ -z "$SHA" ] && continue
STATUS=$(bash .github/scripts/check-promote-ci.sh "$SHA" "${{ github.repository }}")
echo "$SHA → $STATUS"
if [ "$STATUS" = "green" ]; then
PICKED="$SHA"
break
fi
done <<< "$CANDIDATES"
if [ -n "$PICKED" ]; then
echo "ok=true" >> "$GITHUB_OUTPUT"
echo "dev_sha=$PICKED" >> "$GITHUB_OUTPUT"
else
echo "::notice::No soaked candidate has green CI, skipping promotion"
echo "ok=false" >> "$GITHUB_OUTPUT"
fi
- name: Fast-forward push dev → master
if: steps.ci.outputs.ok == 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
DEV_SHA="${{ steps.ci.outputs.dev_sha }}"
echo "Promoting $DEV_SHA to master"
git push origin "${DEV_SHA}:refs/heads/master"
echo "::notice::Promoted dev ($DEV_SHA) to master. bump-electron.yml will pick it up on the next 16:00 UTC tick."
- name: Notify promotion
if: steps.ci.outputs.ok == 'true'
env:
NOTIFY_WEBHOOK_URL: ${{ secrets.NOTIFY_WEBHOOK_URL }}
run: |
bash .github/scripts/notify-webhook.sh \
"🚀 Promoted dev (${{ steps.ci.outputs.dev_sha }}) to master — stable bump at 16:00 UTC"
- name: Trigger docker publish
# GITHUB_TOKEN-driven pushes do not fire downstream workflows (anti-recursion
# guard), so docker-publish.yml's `on: push: branches: [master]` would
# otherwise stay silent after every promotion. workflow_dispatch is one of
# the few events GITHUB_TOKEN is allowed to trigger.
if: steps.ci.outputs.ok == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh workflow run docker-publish.yml --ref master
# If the promote job fails (most commonly the ff-precondition check exiting 1
# because master drifted ahead of dev), open a tracking issue so the breakage
# is visible instead of silently piling up for weeks. Soak/CI-not-green are
# normal skips that keep the job green, so they do not trigger this.
notify-ff-failure:
needs: promote
if: failure()
runs-on: ubuntu-latest
permissions:
issues: write
# job-level permissions reset everything else to none; checkout (for
# notify-webhook.sh) needs explicit read even on a public repo clone
# via the default token.
contents: read
steps:
- name: Checkout (scripts only)
uses: actions/checkout@v4
- name: Notify promote failure
env:
NOTIFY_WEBHOOK_URL: ${{ secrets.NOTIFY_WEBHOOK_URL }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
bash .github/scripts/notify-webhook.sh "❌ promote dev→master job failed: $RUN_URL"
- name: Open tracking issue on promote failure
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
TITLE="⚠️ promote-dev→master 卡住:fast-forward 前提被破坏"
EXISTING=$(gh issue list --repo "$REPO" --state open \
--search "$TITLE in:title" --json number --jq '.[0].number // empty')
if [ -n "$EXISTING" ]; then
echo "Already tracked in #$EXISTING, not duplicating"
exit 0
fi
gh issue create --repo "$REPO" --title "$TITLE" --body \
"\`promote-dev-to-master.yml\` 失败:master 有 dev 没有的提交,ff-promote 链路中断,dev 无法晋升到 master。
常见近因:master 侧自动产物(changelog→README sync、version bump、promote merge commit)未回流 dev,逐周漂移。
恢复方法:\`git merge -s ours origin/master\` 把 master 记为已合并(树取 dev),走 PR 进 dev,即可恢复 ff 前提。
失败 run:$RUN_URL"