Skip to content

Commit 58c3fca

Browse files
authored
ci: automate stable promotion pr preparation (#2917)
1 parent 84b5fbc commit 58c3fca

5 files changed

Lines changed: 199 additions & 11 deletions

File tree

.github/workflows/ci-behavior.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ permissions:
55

66
on:
77
pull_request:
8-
branches: [main]
8+
branches: [main, stable]
99
paths:
1010
- 'packages/superdoc/**'
1111
- 'packages/layout-engine/**'

.github/workflows/ci-superdoc.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ permissions:
55

66
on:
77
pull_request:
8-
branches: [main, 'release/**']
8+
branches: [main, stable, 'release/**']
99
paths-ignore:
1010
- 'apps/docs/**'
1111
- 'apps/mcp/**'
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
name: 🚀 Promote to stable
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
branch_name:
7+
description: Optional candidate branch name (defaults to merge/main-into-stable-YYYY-MM-DD)
8+
required: false
9+
type: string
10+
schedule:
11+
- cron: '0 7 * * *'
12+
13+
permissions:
14+
contents: write
15+
pull-requests: write
16+
17+
concurrency:
18+
group: promote-stable
19+
cancel-in-progress: false
20+
21+
jobs:
22+
prepare-stable-pr:
23+
runs-on: ubuntu-latest
24+
env:
25+
BASE_BRANCH: stable
26+
SOURCE_BRANCH: main
27+
steps:
28+
- name: Generate token
29+
id: generate_token
30+
uses: actions/create-github-app-token@v2
31+
with:
32+
app-id: ${{ secrets.APP_ID }}
33+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
34+
35+
- uses: actions/checkout@v6
36+
with:
37+
fetch-depth: 0
38+
token: ${{ steps.generate_token.outputs.token }}
39+
40+
- name: Prepare candidate branch
41+
id: prepare
42+
env:
43+
REQUESTED_BRANCH_NAME: ${{ inputs.branch_name }}
44+
run: |
45+
set -euo pipefail
46+
47+
git config user.name "github-actions[bot]"
48+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
49+
50+
git fetch origin "${BASE_BRANCH}" "${SOURCE_BRANCH}" --prune
51+
52+
DEFAULT_BRANCH_NAME="merge/main-into-stable-$(date -u +%Y-%m-%d)"
53+
BRANCH_NAME="${REQUESTED_BRANCH_NAME}"
54+
if [[ -z "${BRANCH_NAME}" ]]; then
55+
BRANCH_NAME="${DEFAULT_BRANCH_NAME}"
56+
fi
57+
58+
if git ls-remote --exit-code --heads origin "${BRANCH_NAME}" >/dev/null 2>&1; then
59+
echo "Remote branch already exists: ${BRANCH_NAME}"
60+
echo "Preserving the existing frozen candidate branch."
61+
echo "branch_name=${BRANCH_NAME}" >> "${GITHUB_OUTPUT}"
62+
echo "merge_status=existing" >> "${GITHUB_OUTPUT}"
63+
exit 0
64+
fi
65+
66+
git checkout -B "${BRANCH_NAME}" "origin/${BASE_BRANCH}"
67+
68+
if git merge --no-ff --no-edit "origin/${SOURCE_BRANCH}"; then
69+
MERGE_STATUS="clean"
70+
else
71+
MERGE_STATUS="conflicts"
72+
git add -A
73+
git commit -m "chore: merge main into stable (conflicts need resolution)"
74+
fi
75+
76+
if git diff --quiet "origin/${BASE_BRANCH}"...HEAD; then
77+
echo "No changes to promote from ${SOURCE_BRANCH} into ${BASE_BRANCH}."
78+
echo "branch_name=" >> "${GITHUB_OUTPUT}"
79+
echo "merge_status=noop" >> "${GITHUB_OUTPUT}"
80+
exit 0
81+
fi
82+
83+
git push origin "${BRANCH_NAME}"
84+
85+
echo "branch_name=${BRANCH_NAME}" >> "${GITHUB_OUTPUT}"
86+
echo "merge_status=${MERGE_STATUS}" >> "${GITHUB_OUTPUT}"
87+
88+
- name: Open pull request
89+
if: steps.prepare.outputs.branch_name != ''
90+
id: pr
91+
env:
92+
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
93+
BRANCH_NAME: ${{ steps.prepare.outputs.branch_name }}
94+
MERGE_STATUS: ${{ steps.prepare.outputs.merge_status }}
95+
run: |
96+
set -euo pipefail
97+
98+
if [[ "${MERGE_STATUS}" == "conflicts" ]]; then
99+
PR_TITLE="Merge main into stable (conflicts need resolution)"
100+
PR_BODY="$(cat <<EOF
101+
## Summary
102+
103+
- creates \`${BRANCH_NAME}\` from \`${BASE_BRANCH}\`
104+
- merges \`${SOURCE_BRANCH}\` into the candidate branch
105+
- commits the merge with conflict markers so a human can resolve the branch
106+
107+
## Next Step
108+
109+
Resolve the conflicts on \`${BRANCH_NAME}\`, push the fixes, and then merge this PR into \`${BASE_BRANCH}\`.
110+
111+
---
112+
_Auto-created by promote-stable workflow._
113+
EOF
114+
)"
115+
else
116+
PR_TITLE="Merge main into stable"
117+
PR_BODY="$(cat <<EOF
118+
## Summary
119+
120+
- creates \`${BRANCH_NAME}\` from \`${BASE_BRANCH}\`
121+
- merges \`${SOURCE_BRANCH}\` into the candidate branch
122+
- opens the promotion PR to \`${BASE_BRANCH}\`
123+
124+
---
125+
_Auto-created by promote-stable workflow._
126+
EOF
127+
)"
128+
fi
129+
130+
if [[ "${MERGE_STATUS}" == "existing" ]]; then
131+
PR_TITLE="Merge main into stable"
132+
PR_BODY="$(cat <<EOF
133+
## Summary
134+
135+
- reuses the existing candidate branch \`${BRANCH_NAME}\`
136+
- preserves the previously created frozen snapshot
137+
- ensures there is an open PR targeting \`${BASE_BRANCH}\`
138+
139+
---
140+
_Auto-created by promote-stable workflow._
141+
EOF
142+
)"
143+
fi
144+
145+
EXISTING_PR_URL="$(gh pr list \
146+
--repo "${GITHUB_REPOSITORY}" \
147+
--base "${BASE_BRANCH}" \
148+
--head "${BRANCH_NAME}" \
149+
--state open \
150+
--json url \
151+
--jq '.[0].url' 2>/dev/null || true)"
152+
153+
if [[ -n "${EXISTING_PR_URL}" ]]; then
154+
gh pr edit "${EXISTING_PR_URL}" \
155+
--repo "${GITHUB_REPOSITORY}" \
156+
--title "${PR_TITLE}" \
157+
--body "${PR_BODY}"
158+
echo "url=${EXISTING_PR_URL}" >> "${GITHUB_OUTPUT}"
159+
exit 0
160+
fi
161+
162+
PR_URL="$(gh pr create \
163+
--repo "${GITHUB_REPOSITORY}" \
164+
--base "${BASE_BRANCH}" \
165+
--head "${BRANCH_NAME}" \
166+
--title "${PR_TITLE}" \
167+
--body "${PR_BODY}")"
168+
169+
echo "url=${PR_URL}" >> "${GITHUB_OUTPUT}"
170+
171+
- name: Write workflow summary
172+
run: |
173+
{
174+
echo "### Promote to stable"
175+
echo
176+
echo "| Field | Value |"
177+
echo "| --- | --- |"
178+
echo "| Event | \`${{ github.event_name }}\` |"
179+
echo "| Source branch | \`${SOURCE_BRANCH}\` |"
180+
echo "| Base branch | \`${BASE_BRANCH}\` |"
181+
echo "| Candidate branch | \`${{ steps.prepare.outputs.branch_name || 'n/a' }}\` |"
182+
echo "| Merge status | \`${{ steps.prepare.outputs.merge_status || 'n/a' }}\` |"
183+
echo "| PR | ${{ steps.pr.outputs.url || 'n/a' }} |"
184+
} >> "${GITHUB_STEP_SUMMARY}"

.github/workflows/visual-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ permissions:
66

77
on:
88
pull_request:
9-
branches: [main]
9+
branches: [main, stable]
1010
paths:
1111
- 'packages/superdoc/**'
1212
- 'packages/layout-engine/**'

cicd.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,17 @@ main (next) → stable (latest) → X.x (maintenance)
6969

7070
#### 3. Promote to Stable (`promote-stable.yml`)
7171

72-
**Trigger**: Manual workflow dispatch
72+
**Trigger**: Manual workflow dispatch or daily schedule at `07:00 UTC`
7373

74-
**Input**: Optional tag to promote (defaults to latest from main)
74+
**Input**: Optional candidate branch name (defaults to `merge/main-into-stable-YYYY-MM-DD`)
7575

7676
**Actions**:
7777

78-
- Merges specified version to stable branch
79-
- Triggers automatic stable release
80-
- Updates npm @latest tag
78+
- Creates a fresh candidate branch from `stable`
79+
- Merges `main` into that branch
80+
- Opens a PR targeting `stable`
81+
- If the merge conflicts, commits the conflicted merge to the branch so a human can resolve it there
82+
- Merging that PR triggers the automatic stable release workflow
8183

8284
#### 4. Create Patch Branch (`create-patch.yml`)
8385

@@ -208,9 +210,11 @@ These skip semantic-release entirely — useful for re-publishing a failed platf
208210
### Scenario 2: Creating Stable Release
209211

210212
1. Run "Promote to Stable" workflow
211-
2. Merges main to stable
212-
3. Automatically publishes `1.1.0` as @latest
213-
4. Syncs back to main with version bump
213+
2. Review the generated PR from the candidate branch into `stable`
214+
3. If needed, resolve merge conflicts on the candidate branch
215+
4. Merge the PR into `stable`
216+
5. Automatically publishes `1.1.0` as @latest
217+
6. Syncs back to main with version bump
214218

215219
### Scenario 3: Hotfix to Current Stable
216220

0 commit comments

Comments
 (0)