1- name : Promote SDKs
1+ name : Promote SDK changes
22
3- # Manually fast-forwards production main to the reviewed staging main. The
4- # ancestor check refuses divergent histories; this workflow never force-pushes.
3+ # Staging is the generator's integration history. Production `next` is the
4+ # developer-facing queue for the next release. This workflow combines the
5+ # latest released state with validated staging changes, then advances `next`.
6+ # Release automation maintains the single versioned PR from `next` to `main`.
57on :
8+ push :
9+ branches : [main]
610 workflow_dispatch : {}
711
812permissions :
1216 promote :
1317 if : github.repository == 'kernel/hypeman-ts-staging'
1418 runs-on : ${{ vars.STLC_RUNNER || 'ubuntu-latest' }}
15- environment : production
19+ concurrency :
20+ group : stlc-promote
21+ cancel-in-progress : true
1622 steps :
1723 - name : Check out staging
1824 uses : actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -29,33 +35,99 @@ jobs:
2935 owner : kernel
3036 repositories : hypeman-ts
3137 permission-contents : write
38+ permission-pull-requests : write
3239 permission-workflows : write
3340
34- - name : Fetch production main
41+ - name : Fetch production branches
42+ id : production
3543 env :
3644 GH_TOKEN : ${{ steps.production-token.outputs.token }}
3745 PRODUCTION_REPO : kernel/hypeman-ts
3846 run : |
39- git remote add production "https://x-access-token:${GH_TOKEN}@github.com/${PRODUCTION_REPO}.git"
47+ set -euo pipefail
48+ git remote add production \
49+ "https://x-access-token:${GH_TOKEN}@github.com/${PRODUCTION_REPO}.git"
4050 git fetch production main
41-
42- - name : Check whether production already has staging's content
43- id : diff
44- run : |
45- MERGED=$(git merge-tree --write-tree production/main origin/main) || MERGED=conflict
46- PRODUCTION_TREE=$(git rev-parse 'production/main^{tree}')
47- if [ "$MERGED" = "$PRODUCTION_TREE" ]; then
48- echo "Production already contains staging's content. Nothing to promote."
49- echo "synced=true" >> "$GITHUB_OUTPUT"
51+ if git ls-remote --exit-code --heads production next >/dev/null 2>&1; then
52+ git fetch production next
53+ echo "has_next=true" >> "$GITHUB_OUTPUT"
5054 else
51- echo "synced =false" >> "$GITHUB_OUTPUT"
55+ echo "has_next =false" >> "$GITHUB_OUTPUT"
5256 fi
5357
54- - name : Promote staging to production
55- if : steps.diff.outputs.synced == 'false'
58+ - name : Prepare the next release branch
59+ env :
60+ APP_SLUG : ${{ steps.production-token.outputs.app-slug }}
61+ GH_TOKEN : ${{ steps.production-token.outputs.token }}
62+ HAS_NEXT : ${{ steps.production.outputs.has_next }}
63+ PRODUCTION_REPO : kernel/hypeman-ts
5664 run : |
57- if ! git merge-base --is-ancestor production/main origin/main; then
58- echo "::error title=Promote blocked::production/main is not an ancestor of staging main. Back-sync production first."
65+ set -euo pipefail
66+ bot_id=$(gh api "/users/${APP_SLUG}[bot]" --jq .id)
67+ git config user.name "${APP_SLUG}[bot]"
68+ git config user.email "${bot_id}+${APP_SLUG}[bot]@users.noreply.github.com"
69+
70+ open_conflict_pr() {
71+ source_ref=$1
72+ source_name=$2
73+ advance_next=$3
74+ conflict_branch=stlc/promotion-conflict
75+
76+ git merge --abort
77+ existing=$(gh pr list --repo "$PRODUCTION_REPO" --base next \
78+ --head "$conflict_branch" --state open --json url --jq '.[0].url // ""')
79+ if [ -n "$existing" ]; then
80+ echo "::error title=SDK promotion blocked::Resolve the existing recovery PR: $existing"
81+ exit 1
82+ fi
83+
84+ if [ "$advance_next" = "true" ]; then
85+ git push production HEAD:refs/heads/next
86+ fi
87+ git push production "$source_ref:refs/heads/$conflict_branch" --force
88+
89+ body=$(mktemp)
90+ printf '%s\n' \
91+ '## SDK promotion conflict' \
92+ '' \
93+ "The automated promotion could not merge $source_name into the pending next release." \
94+ '' \
95+ 'Resolve the conflicts on this branch, validate the SDK, mark this PR ready, and merge it with a merge commit.' \
96+ '' \
97+ 'After merging, rerun the staging Promote SDK changes workflow to include any newer generated changes.' \
98+ > "$body"
99+ recovery_url=$(gh pr create --repo "$PRODUCTION_REPO" --draft \
100+ --base next --head "$conflict_branch" \
101+ --title 'chore: resolve SDK promotion conflict' --body-file "$body")
102+ echo "::error title=SDK promotion conflict::Resolve the recovery PR: $recovery_url"
59103 exit 1
104+ }
105+
106+ if [ "$HAS_NEXT" = "true" ]; then
107+ git checkout -B stlc/promote-next production/next
108+ else
109+ git checkout -B stlc/promote-next production/main
110+ fi
111+
112+ if ! git merge-base --is-ancestor production/main HEAD; then
113+ if ! git merge --no-edit production/main; then
114+ open_conflict_pr production/main 'production main' false
115+ fi
116+ fi
117+ if ! git merge-base --is-ancestor origin/main HEAD; then
118+ if ! git merge --no-edit origin/main; then
119+ open_conflict_pr origin/main 'validated staging changes' true
120+ fi
121+ fi
122+
123+ if [ "$HAS_NEXT" = "true" ]; then
124+ git merge-base --is-ancestor production/next HEAD
60125 fi
61- git push production origin/main:refs/heads/main
126+
127+ - name : Update the pending release
128+ env :
129+ GH_TOKEN : ${{ steps.production-token.outputs.token }}
130+ run : |
131+ set -euo pipefail
132+ git push production HEAD:refs/heads/next
133+ echo "Updated production next; the versioned release PR will be opened or refreshed."
0 commit comments