Skip to content

Commit da7a4df

Browse files
committed
fix(ci): release bump 안전장치 추가
1 parent 18cb510 commit da7a4df

14 files changed

Lines changed: 1256 additions & 40 deletions

.github/workflows/action-smoke.yml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,17 @@ jobs:
6666

6767
- name: Assert checked out release target
6868
shell: bash
69+
env:
70+
RELEASE_SHA: ${{ inputs.release_sha }}
71+
RELEASE_TAG: ${{ inputs.release_tag }}
6972
run: |
70-
if [[ -n "${{ inputs.release_sha }}" ]]; then
71-
test "$(git rev-parse HEAD)" = "${{ inputs.release_sha }}"
72-
git fetch --depth=1 origin "refs/tags/${{ inputs.release_tag }}:refs/tags/${{ inputs.release_tag }}"
73-
test "$(git rev-list -n 1 "${{ inputs.release_tag }}")" = "${{ inputs.release_sha }}"
73+
if [[ -n "$RELEASE_SHA" ]]; then
74+
test "$(git rev-parse HEAD)" = "$RELEASE_SHA"
75+
git fetch --depth=1 origin "refs/tags/${RELEASE_TAG}:refs/tags/${RELEASE_TAG}"
76+
test "$(git rev-list -n 1 "$RELEASE_TAG")" = "$RELEASE_SHA"
7477
else
7578
checked_ref="$(git describe --tags --exact-match HEAD)"
76-
test "$checked_ref" = "${{ inputs.release_tag }}"
79+
test "$checked_ref" = "$RELEASE_TAG"
7780
fi
7881
7982
# GitHub Actions does not allow dynamic expressions in step-level `uses:`.

.github/workflows/dev.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ on:
1616
- ".github/actions/marketplace-wrapper/action.yml"
1717
- ".github/workflows/action-smoke.yml"
1818
- ".github/workflows/dev.yml"
19+
- ".github/workflows/manual-release-bump.yml"
20+
- ".github/workflows/release-candidate.yml"
1921
- ".github/workflows/release.yml"
2022
- ".github/workflows/release-drafter.yml"
2123
- ".github/workflows/rust-release-binaries.yml"
@@ -42,6 +44,8 @@ on:
4244
- ".github/actions/marketplace-wrapper/action.yml"
4345
- ".github/workflows/action-smoke.yml"
4446
- ".github/workflows/dev.yml"
47+
- ".github/workflows/manual-release-bump.yml"
48+
- ".github/workflows/release-candidate.yml"
4549
- ".github/workflows/release.yml"
4650
- ".github/workflows/release-drafter.yml"
4751
- ".github/workflows/rust-release-binaries.yml"
@@ -221,4 +225,4 @@ jobs:
221225
run: node ./scripts/validate-rust-release-wiring.mjs
222226

223227
- name: Run focused release wiring test
224-
run: node --test test/release-workflow-context.test.js test/github-action-wiring.test.js test/release-plan.test.js test/npm-error-classifiers.test.js
228+
run: node --test test/release-workflow-context.test.js test/github-action-wiring.test.js test/release-plan.test.js test/release-candidate-metadata.test.js test/release-helpers.test.js test/npm-error-classifiers.test.js test/bump-release-version.test.js
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
name: Manual Release Bump
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
tag:
7+
description: Target release tag or version, for example v0.1.1 or 0.1.1-alpha.2
8+
required: true
9+
type: string
10+
dry_run:
11+
description: Validate and prepare the bump without pushing a branch or opening a PR
12+
required: false
13+
default: false
14+
type: boolean
15+
16+
permissions:
17+
actions: write
18+
contents: write
19+
issues: write
20+
pull-requests: write
21+
22+
concurrency:
23+
group: manual-release-bump-master-${{ startsWith(inputs.tag, 'v') && inputs.tag || format('v{0}', inputs.tag) }}
24+
cancel-in-progress: false
25+
26+
jobs:
27+
bump_release_version:
28+
runs-on: ubuntu-latest
29+
env:
30+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
31+
32+
steps:
33+
- name: Check out repository
34+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
35+
with:
36+
ref: master
37+
fetch-depth: 0
38+
39+
- name: Set up Node.js
40+
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
41+
with:
42+
node-version: "24"
43+
44+
- name: Resolve bump metadata
45+
id: meta
46+
shell: bash
47+
env:
48+
INPUT_TAG: ${{ inputs.tag }}
49+
run: |
50+
tag="$INPUT_TAG"
51+
if [[ "$tag" != v* ]]; then
52+
tag="v$tag"
53+
fi
54+
55+
if ! [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
56+
echo "tag must look like v1.2.3 or v1.2.3-alpha.1"
57+
exit 1
58+
fi
59+
60+
current_version=$(node -p "require('./package.json').version")
61+
expected_version="${tag#v}"
62+
branch=$(node --input-type=module -e "import { createManualBumpBranchName } from './scripts/bump-release-version.mjs'; console.log(createManualBumpBranchName(process.argv[1]));" "$tag")
63+
64+
{
65+
echo "currentVersion=$current_version"
66+
echo "version=$expected_version"
67+
echo "tag=$tag"
68+
echo "branch=$branch"
69+
echo "title=chore: ${expected_version} 수동 bump"
70+
} >> "$GITHUB_OUTPUT"
71+
72+
- name: Apply version bump
73+
env:
74+
RELEASE_TAG: ${{ steps.meta.outputs.tag }}
75+
run: node ./scripts/bump-release-version.mjs "$RELEASE_TAG"
76+
77+
- name: Verify changed files before package verification
78+
shell: bash
79+
run: |
80+
node --input-type=module <<'NODE'
81+
import { execFileSync } from "node:child_process";
82+
import { packageManifestPaths } from "./scripts/bump-release-version.mjs";
83+
84+
const changedFiles = execFileSync("git", ["diff", "--name-only"], { encoding: "utf8" })
85+
.trim()
86+
.split("\n")
87+
.filter(Boolean)
88+
.sort();
89+
const expectedFiles = [...packageManifestPaths].sort();
90+
91+
if (JSON.stringify(changedFiles) !== JSON.stringify(expectedFiles)) {
92+
throw new Error(`Unexpected changed files: ${changedFiles.join(", ")}`);
93+
}
94+
NODE
95+
96+
- name: Verify package
97+
run: npm test
98+
99+
- name: Verify changed files after package verification
100+
shell: bash
101+
run: |
102+
node --input-type=module <<'NODE'
103+
import { execFileSync } from "node:child_process";
104+
import { packageManifestPaths } from "./scripts/bump-release-version.mjs";
105+
106+
const changedFiles = execFileSync("git", ["diff", "--name-only"], { encoding: "utf8" })
107+
.trim()
108+
.split("\n")
109+
.filter(Boolean)
110+
.sort();
111+
const expectedFiles = [...packageManifestPaths].sort();
112+
113+
if (JSON.stringify(changedFiles) !== JSON.stringify(expectedFiles)) {
114+
throw new Error(`Unexpected changed files after verification: ${changedFiles.join(", ")}`);
115+
}
116+
NODE
117+
118+
- name: Stop after dry run
119+
if: ${{ inputs.dry_run }}
120+
run: echo "Dry run requested; skipping branch push and PR creation."
121+
122+
- name: Configure git author
123+
if: ${{ !inputs.dry_run }}
124+
run: |
125+
git config user.name "github-actions[bot]"
126+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
127+
128+
- name: Ensure skip-changelog label exists
129+
if: ${{ !inputs.dry_run }}
130+
shell: bash
131+
run: |
132+
if gh label list --search "skip-changelog" --json name --jq 'map(select(.name == "skip-changelog")) | length > 0' | grep -q true; then
133+
exit 0
134+
fi
135+
136+
gh label create "skip-changelog" \
137+
--color "6E7781" \
138+
--description "Exclude automated release prep PRs from release notes."
139+
140+
- name: Resolve existing open PR
141+
if: ${{ !inputs.dry_run }}
142+
id: existing_pr
143+
shell: bash
144+
env:
145+
BUMP_BRANCH: ${{ steps.meta.outputs.branch }}
146+
EXPECTED_VERSION: ${{ steps.meta.outputs.version }}
147+
run: |
148+
pr_json=$(gh pr list \
149+
--head "$BUMP_BRANCH" \
150+
--state open \
151+
--json number,url,baseRefName,labels)
152+
153+
pr_number=$(printf '%s' "$pr_json" | jq -r '.[0].number // empty')
154+
pr_url=$(printf '%s' "$pr_json" | jq -r '.[0].url // empty')
155+
pr_base=$(printf '%s' "$pr_json" | jq -r '.[0].baseRefName // empty')
156+
has_skip_changelog=$(printf '%s' "$pr_json" | jq -r 'if (.[0].labels // []) | map(.name) | index("skip-changelog") then "true" else "false" end')
157+
158+
if [ -n "$pr_number" ] && [ "$pr_base" != "master" ]; then
159+
echo "Existing PR $pr_url targets base '$pr_base', expected 'master'."
160+
exit 1
161+
fi
162+
163+
if [ -n "$pr_number" ]; then
164+
git fetch origin \
165+
"refs/heads/master:refs/remotes/origin/master" \
166+
"refs/heads/${BUMP_BRANCH}:refs/remotes/origin/${BUMP_BRANCH}"
167+
168+
# shellcheck disable=SC2016
169+
node --input-type=module -e '
170+
import { execFileSync } from "node:child_process";
171+
import { packageManifestPaths } from "./scripts/bump-release-version.mjs";
172+
173+
const branch = process.argv[1];
174+
const expectedVersion = process.argv[2];
175+
const diffFiles = execFileSync(
176+
"git",
177+
["diff", "--name-only", `refs/remotes/origin/master...refs/remotes/origin/${branch}`],
178+
{ encoding: "utf8" },
179+
)
180+
.trim()
181+
.split("\n")
182+
.filter(Boolean)
183+
.sort();
184+
const expectedFiles = [...packageManifestPaths].sort();
185+
186+
if (JSON.stringify(diffFiles) !== JSON.stringify(expectedFiles)) {
187+
throw new Error(`Existing PR is not a manifest-only bump: ${diffFiles.join(", ")}`);
188+
}
189+
190+
for (const relativePath of packageManifestPaths) {
191+
const manifest = JSON.parse(
192+
execFileSync("git", ["show", `refs/remotes/origin/${branch}:${relativePath}`], {
193+
encoding: "utf8",
194+
}),
195+
);
196+
197+
if (manifest.version !== expectedVersion) {
198+
throw new Error(`${relativePath} version mismatch: ${manifest.version}`);
199+
}
200+
}
201+
' "$BUMP_BRANCH" "$EXPECTED_VERSION"
202+
fi
203+
204+
if [ -n "$pr_number" ] && [ "$has_skip_changelog" != "true" ]; then
205+
gh pr edit "$pr_number" --add-label skip-changelog
206+
fi
207+
208+
{
209+
echo "number=$pr_number"
210+
echo "url=$pr_url"
211+
} >> "$GITHUB_OUTPUT"
212+
213+
- name: Prepare bump branch
214+
if: ${{ !inputs.dry_run && steps.existing_pr.outputs.number == '' }}
215+
shell: bash
216+
env:
217+
BUMP_BRANCH: ${{ steps.meta.outputs.branch }}
218+
BUMP_TITLE: ${{ steps.meta.outputs.title }}
219+
run: |
220+
lease_args=()
221+
222+
if git ls-remote --exit-code --heads origin "$BUMP_BRANCH" >/dev/null 2>&1; then
223+
echo "Remote branch $BUMP_BRANCH already exists; refreshing it with the verified bump commit."
224+
git fetch origin "refs/heads/${BUMP_BRANCH}:refs/remotes/origin/${BUMP_BRANCH}"
225+
lease_args+=(--force-with-lease="refs/heads/${BUMP_BRANCH}:$(git rev-parse "refs/remotes/origin/${BUMP_BRANCH}")")
226+
fi
227+
228+
git switch -C "$BUMP_BRANCH"
229+
git add package.json npm/*/package.json
230+
git commit -m "$BUMP_TITLE"
231+
git push "${lease_args[@]}" -u origin "$BUMP_BRANCH"
232+
233+
- name: Dispatch CI for bump branch
234+
if: ${{ !inputs.dry_run }}
235+
shell: bash
236+
env:
237+
BUMP_BRANCH: ${{ steps.meta.outputs.branch }}
238+
run: gh workflow run dev.yml --ref "$BUMP_BRANCH"
239+
240+
- name: Create or reuse draft PR
241+
if: ${{ !inputs.dry_run }}
242+
shell: bash
243+
env:
244+
BUMP_BRANCH: ${{ steps.meta.outputs.branch }}
245+
BUMP_TITLE: ${{ steps.meta.outputs.title }}
246+
CURRENT_VERSION: ${{ steps.meta.outputs.currentVersion }}
247+
DEFAULT_GH_TOKEN: ${{ github.token }}
248+
EXISTING_PR_URL: ${{ steps.existing_pr.outputs.url }}
249+
PR_CREATE_TOKEN: ${{ secrets.MAXIMUS_RELEASE_BOT_TOKEN }}
250+
RELEASE_TAG: ${{ steps.meta.outputs.tag }}
251+
RELEASE_VERSION: ${{ steps.meta.outputs.version }}
252+
run: |
253+
printf '%s\n' \
254+
"## 배경 / Background" \
255+
"" \
256+
"- manual release bump workflow로 \`${CURRENT_VERSION}\`에서 \`${RELEASE_VERSION}\`으로 승격합니다." \
257+
"- 실제 tag / publish는 이 PR merge 이후 별도 \`release.yml\` workflow에서 진행합니다." \
258+
"" \
259+
"## 변경 사항 / Changes" \
260+
"" \
261+
"- root wrapper와 platform package manifest version을 \`${RELEASE_VERSION}\`으로 맞췄습니다." \
262+
"- root optionalDependencies도 같은 버전으로 정렬했습니다." \
263+
"" \
264+
"## 검증 / Verification" \
265+
"" \
266+
"- \`node ./scripts/bump-release-version.mjs ${RELEASE_TAG}\`" \
267+
"- \`npm test\`" \
268+
"- bump branch에 대해 \`maximus-dev\` workflow를 dispatch했습니다." \
269+
"" \
270+
"## 리스크 / Risks" \
271+
"" \
272+
"- 실제 tag / publish는 아직 실행하지 않았습니다." \
273+
"- merge 후 \`Release Candidate Core Verification\` 과 같은 commit의 \`maximus-dev\` workflow 결과를 확인한 뒤 tag를 생성해야 합니다." \
274+
> pr-body.md
275+
276+
if [ -n "$PR_CREATE_TOKEN" ]; then
277+
export GH_TOKEN="$PR_CREATE_TOKEN"
278+
token_source="MAXIMUS_RELEASE_BOT_TOKEN"
279+
else
280+
export GH_TOKEN="$DEFAULT_GH_TOKEN"
281+
token_source="github.token"
282+
fi
283+
284+
if [ -n "$EXISTING_PR_URL" ]; then
285+
echo "Reusing existing PR: $EXISTING_PR_URL"
286+
exit 0
287+
fi
288+
289+
if pr_url=$(gh pr create \
290+
--draft \
291+
--base master \
292+
--head "$BUMP_BRANCH" \
293+
--title "$BUMP_TITLE" \
294+
--body-file pr-body.md \
295+
--label skip-changelog 2>/tmp/maximus-gh-pr-create-error.txt); then
296+
echo "Created draft PR: $pr_url"
297+
exit 0
298+
fi
299+
300+
compare_url="https://github.com/${GITHUB_REPOSITORY}/compare/master...${BUMP_BRANCH}?expand=1"
301+
302+
{
303+
echo "### Manual release bump failed to create a PR"
304+
echo
305+
echo "The bump branch was pushed, but the workflow could not open a draft PR."
306+
echo
307+
echo "- branch: \`${BUMP_BRANCH}\`"
308+
echo "- title: \`${BUMP_TITLE}\`"
309+
echo "- token source: \`$token_source\`"
310+
echo "- compare URL: $compare_url"
311+
echo
312+
echo "Either enable repository Actions permission to create pull requests, or configure \`MAXIMUS_RELEASE_BOT_TOKEN\` with pull request creation permission."
313+
} >> "$GITHUB_STEP_SUMMARY"
314+
315+
cat /tmp/maximus-gh-pr-create-error.txt
316+
echo "::error::Unable to create a draft PR for $BUMP_BRANCH. See the step summary for required repository or token changes."
317+
exit 1

0 commit comments

Comments
 (0)