Promote dev → master #155
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" |