project-auto-wizard 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +139 -0
- package/bin/project-auto-wizard.js +19 -0
- package/package.json +36 -0
- package/payload/coderabbit.yaml +19 -0
- package/payload/config/breaking-changes.json +1 -0
- package/payload/config/wizard-prompts.yml +62 -0
- package/payload/scripts/changelog_manager.py +726 -0
- package/payload/scripts/version_manager.py +617 -0
- package/payload/version.yml.template +48 -0
- package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +303 -0
- package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +293 -0
- package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +289 -0
- package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +192 -0
- package/payload/workflows/common/secret-backup/PROJECT-COMMON-SECRET-FILE-UPLOAD.yaml +209 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +591 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +700 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +308 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +992 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +689 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +987 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +471 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-SUH-LAB-APP-BUILD-TRIGGER.yaml +500 -0
- package/payload/workflows/next/PROJECT-NEXT-CI.yaml +185 -0
- package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +271 -0
- package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +81 -0
- package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +2191 -0
- package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +386 -0
- package/payload/workflows/react/PROJECT-REACT-CI.yaml +194 -0
- package/payload/workflows/react/PROJECT-REACT-CICD.yaml +255 -0
- package/payload/workflows/spring/PROJECT-SPRING-GITHUB-PACKAGES-PUBLISH.yml +84 -0
- package/payload/workflows/spring/nexus/PROJECT-SPRING-NEXUS-CI.yml +316 -0
- package/payload/workflows/spring/nexus/PROJECT-SPRING-NEXUS-PUBLISH.yml +58 -0
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +535 -0
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +437 -0
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +2277 -0
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +425 -0
- package/src/cli/args.js +95 -0
- package/src/cli/help.js +27 -0
- package/src/commands/full.js +53 -0
- package/src/commands/interactive.js +276 -0
- package/src/commands/revert.js +60 -0
- package/src/commands/version.js +31 -0
- package/src/commands/workflows.js +49 -0
- package/src/context.js +26 -0
- package/src/core/assets.js +47 -0
- package/src/core/branches.js +60 -0
- package/src/core/branding.js +15 -0
- package/src/core/breaking-check.js +65 -0
- package/src/core/breaking.js +26 -0
- package/src/core/copy/coderabbit.js +26 -0
- package/src/core/copy/gitignore.js +55 -0
- package/src/core/copy/readme.js +30 -0
- package/src/core/copy/simple.js +23 -0
- package/src/core/copy/workflows.js +233 -0
- package/src/core/detect-fs.js +106 -0
- package/src/core/detect.js +62 -0
- package/src/core/fsutil.js +46 -0
- package/src/core/options-ask.js +99 -0
- package/src/core/paths-resolve.js +261 -0
- package/src/core/paths.js +16 -0
- package/src/core/version-yml.js +190 -0
- package/src/core/wizard-env.js +101 -0
- package/src/core/wizard-labels.js +107 -0
- package/src/index.js +162 -0
- package/src/ui/ansi.js +26 -0
- package/src/ui/banner.js +29 -0
- package/src/ui/env-plan.js +207 -0
- package/src/ui/prompts.js +99 -0
- package/src/ui/readline-engine.js +257 -0
- package/src/ui/status-cards.js +56 -0
- package/src/ui/summary.js +127 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# ===================================================================
|
|
2
|
+
# PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml
|
|
3
|
+
# Release PR pipeline: version confirm + AI changelog + automerge
|
|
4
|
+
# ===================================================================
|
|
5
|
+
#
|
|
6
|
+
# Runs on release PRs ({{DEVELOP_BRANCH}} -> {{MAIN_BRANCH}}) and:
|
|
7
|
+
# 1. Confirms the release version (patch +1 and sync of every version file)
|
|
8
|
+
# 2. Acquires a release summary:
|
|
9
|
+
# - If version.yml metadata.template.options.coderabbit is true:
|
|
10
|
+
# requests a CodeRabbit summary comment once and polls the PR body
|
|
11
|
+
# (30s x 10 polls = 5 minutes max)
|
|
12
|
+
# - Otherwise (or on poll timeout): generates the summary locally with
|
|
13
|
+
# changelog_manager.py ai-summary (engine chain: user API key ->
|
|
14
|
+
# GitHub Models -> rule-based fallback; always succeeds)
|
|
15
|
+
# 3. Updates CHANGELOG.json / CHANGELOG.md and commits to the PR head
|
|
16
|
+
# branch with [skip ci]
|
|
17
|
+
# 4. Enables automerge (gh pr merge --auto --merge)
|
|
18
|
+
#
|
|
19
|
+
# The merge-commit subject carries the release-confirm pattern
|
|
20
|
+
# "chore(release):" — keep in sync with PROJECT-COMMON-VERSION-CONTROL
|
|
21
|
+
# and PROJECT-COMMON-RELEASE-PUBLISH.
|
|
22
|
+
#
|
|
23
|
+
# Optional secrets/vars:
|
|
24
|
+
# - secrets.WORKFLOW_PAT: PAT used for the merge. Without it the merge is
|
|
25
|
+
# performed with GITHUB_TOKEN, and GitHub will NOT trigger downstream
|
|
26
|
+
# {{MAIN_BRANCH}}-push workflows (e.g. RELEASE-PUBLISH) for that merge.
|
|
27
|
+
# - secrets.AI_API_KEY (+ vars.AI_API_BASE_URL / vars.AI_MODEL): preferred
|
|
28
|
+
# summary engine. Falls back to GitHub Models, then to rule-based.
|
|
29
|
+
# ===================================================================
|
|
30
|
+
|
|
31
|
+
name: PROJECT-AUTO-CHANGELOG-CONTROL
|
|
32
|
+
|
|
33
|
+
concurrency:
|
|
34
|
+
group: auto-changelog-${{ github.event.pull_request.number || github.run_id }}
|
|
35
|
+
cancel-in-progress: false
|
|
36
|
+
|
|
37
|
+
# No workflow_dispatch: this pipeline needs a live PR context and would be
|
|
38
|
+
# a dead lever without one. The manual recovery path is
|
|
39
|
+
# PROJECT-RELEASE-PUBLISH's workflow_dispatch.
|
|
40
|
+
on:
|
|
41
|
+
pull_request:
|
|
42
|
+
types: [opened, synchronize, reopened]
|
|
43
|
+
branches: ["{{MAIN_BRANCH}}"]
|
|
44
|
+
|
|
45
|
+
permissions:
|
|
46
|
+
contents: write
|
|
47
|
+
pull-requests: write
|
|
48
|
+
models: read
|
|
49
|
+
|
|
50
|
+
jobs:
|
|
51
|
+
changelog-and-merge:
|
|
52
|
+
name: Version confirm + changelog + automerge
|
|
53
|
+
# Guard: only release PRs whose head is the develop branch of THIS
|
|
54
|
+
# repository enter the pipeline (protects against accidental feature
|
|
55
|
+
# PRs opened against production and against fork PRs with a spoofed
|
|
56
|
+
# "develop" head ref).
|
|
57
|
+
if: >-
|
|
58
|
+
github.event_name == 'pull_request' &&
|
|
59
|
+
github.event.pull_request.head.repo.full_name == github.repository &&
|
|
60
|
+
github.event.pull_request.head.ref == '{{DEVELOP_BRANCH}}'
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
steps:
|
|
63
|
+
- name: Checkout PR head
|
|
64
|
+
uses: actions/checkout@v5
|
|
65
|
+
with:
|
|
66
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
67
|
+
fetch-depth: 0
|
|
68
|
+
ref: ${{ github.event.pull_request.head.ref }}
|
|
69
|
+
|
|
70
|
+
- name: Git config
|
|
71
|
+
run: |
|
|
72
|
+
git config --local user.email "action@github.com"
|
|
73
|
+
git config --local user.name "GitHub Action"
|
|
74
|
+
|
|
75
|
+
- name: Read coderabbit option from version.yml
|
|
76
|
+
id: options
|
|
77
|
+
run: |
|
|
78
|
+
# scoped to the metadata: -> template: -> options: chain so an
|
|
79
|
+
# unrelated "coderabbit:" key can never hijack the value
|
|
80
|
+
CODERABBIT=$(python3 -c 'import re; t=open("version.yml",encoding="utf-8").read(); m=re.search(r"metadata:.*?template:.*?options:.*?coderabbit:\s*\"?(true|false)", t, re.S); print(m.group(1) if m else "false")' 2>/dev/null || echo "false")
|
|
81
|
+
echo "coderabbit=$CODERABBIT" >> $GITHUB_OUTPUT
|
|
82
|
+
echo "coderabbit option: $CODERABBIT"
|
|
83
|
+
|
|
84
|
+
- name: Request CodeRabbit summary (PR-body-init protection)
|
|
85
|
+
id: coderabbit_request
|
|
86
|
+
if: steps.options.outputs.coderabbit == 'true'
|
|
87
|
+
run: |
|
|
88
|
+
PR_NUMBER=${{ github.event.pull_request.number }}
|
|
89
|
+
|
|
90
|
+
# The PR body can lag right after open (API replication) —
|
|
91
|
+
# retry the read up to 3 times before deciding.
|
|
92
|
+
CURRENT_BODY=""
|
|
93
|
+
for attempt in 1 2 3; do
|
|
94
|
+
CURRENT_BODY=$(curl -s -H "Authorization: token ${{ github.token }}" \
|
|
95
|
+
"https://api.github.com/repos/${{ github.repository }}/pulls/${PR_NUMBER}" | \
|
|
96
|
+
jq -r '.body // ""')
|
|
97
|
+
if [ -n "$CURRENT_BODY" ] && [ "$CURRENT_BODY" != "null" ]; then
|
|
98
|
+
echo "[try $attempt] body length=$(printf '%s' "$CURRENT_BODY" | wc -c)"
|
|
99
|
+
break
|
|
100
|
+
fi
|
|
101
|
+
echo "[try $attempt] body empty — possible replication lag, retrying"
|
|
102
|
+
sleep 2
|
|
103
|
+
done
|
|
104
|
+
|
|
105
|
+
if printf '%s\n' "$CURRENT_BODY" | grep -q "Summary by CodeRabbit"; then
|
|
106
|
+
# Protection: a summary already exists — never wipe it, never re-request
|
|
107
|
+
echo "Summary by CodeRabbit already present — skipping request"
|
|
108
|
+
echo "already_found=true" >> $GITHUB_OUTPUT
|
|
109
|
+
else
|
|
110
|
+
echo "already_found=false" >> $GITHUB_OUTPUT
|
|
111
|
+
echo "requesting CodeRabbit summary on PR #$PR_NUMBER"
|
|
112
|
+
curl -s -H "Authorization: token ${{ github.token }}" \
|
|
113
|
+
-H "Content-Type: application/json" \
|
|
114
|
+
-X POST \
|
|
115
|
+
-d '{"body": "@coderabbitai summary"}' \
|
|
116
|
+
"https://api.github.com/repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" > /dev/null
|
|
117
|
+
echo "CodeRabbit summary requested"
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
- name: Poll PR body for CodeRabbit summary (30s x 10 polls = 5 minutes max)
|
|
121
|
+
id: poll
|
|
122
|
+
if: steps.options.outputs.coderabbit == 'true'
|
|
123
|
+
run: |
|
|
124
|
+
PR_NUMBER=${{ github.event.pull_request.number }}
|
|
125
|
+
|
|
126
|
+
if [ "${{ steps.coderabbit_request.outputs.already_found }}" = "true" ]; then
|
|
127
|
+
echo "summary already present — skipping poll"
|
|
128
|
+
curl -s -H "Authorization: token ${{ github.token }}" \
|
|
129
|
+
"https://api.github.com/repos/${{ github.repository }}/pulls/${PR_NUMBER}" | \
|
|
130
|
+
jq -r '.body // ""' > pr_body.md
|
|
131
|
+
echo "summary_found=true" >> $GITHUB_OUTPUT
|
|
132
|
+
exit 0
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
MAX_POLLS=10
|
|
136
|
+
POLL_INTERVAL=30
|
|
137
|
+
FOUND=false
|
|
138
|
+
|
|
139
|
+
for i in $(seq 1 $MAX_POLLS); do
|
|
140
|
+
echo "[$i/$MAX_POLLS] checking PR body... ($(date '+%H:%M:%S'))"
|
|
141
|
+
|
|
142
|
+
PR_BODY=$(curl -s -H "Authorization: token ${{ github.token }}" \
|
|
143
|
+
"https://api.github.com/repos/${{ github.repository }}/pulls/${PR_NUMBER}" | \
|
|
144
|
+
jq -r '.body // ""')
|
|
145
|
+
|
|
146
|
+
if [ -n "$PR_BODY" ] && [ "$PR_BODY" != "null" ] && printf '%s\n' "$PR_BODY" | grep -q "Summary by CodeRabbit"; then
|
|
147
|
+
echo "CodeRabbit summary found"
|
|
148
|
+
printf '%s\n' "$PR_BODY" > pr_body.md
|
|
149
|
+
FOUND=true
|
|
150
|
+
break
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
if [ "$i" -lt "$MAX_POLLS" ]; then
|
|
154
|
+
sleep $POLL_INTERVAL
|
|
155
|
+
fi
|
|
156
|
+
done
|
|
157
|
+
|
|
158
|
+
if [ "$FOUND" = "true" ]; then
|
|
159
|
+
echo "summary_found=true" >> $GITHUB_OUTPUT
|
|
160
|
+
else
|
|
161
|
+
echo "no CodeRabbit summary after 5 minutes — falling back to ai-summary"
|
|
162
|
+
echo "summary_found=false" >> $GITHUB_OUTPUT
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
- name: Confirm release version (patch +1 and sync)
|
|
166
|
+
id: bump
|
|
167
|
+
run: |
|
|
168
|
+
# Idempotency: if the PR head is already this workflow's own
|
|
169
|
+
# version-confirm commit (re-run / synchronize retrigger), do not
|
|
170
|
+
# bump a second time — reuse the confirmed version.
|
|
171
|
+
HEAD_MSG=$(git log -1 --pretty=%s)
|
|
172
|
+
if printf '%s\n' "$HEAD_MSG" | grep -q "^chore(version): confirm v.* \[skip ci\]"; then
|
|
173
|
+
NEW_VERSION=$(python3 .github/scripts/version_manager.py get | tail -n 1)
|
|
174
|
+
echo "version already confirmed by a previous run — reusing $NEW_VERSION (no re-increment)"
|
|
175
|
+
else
|
|
176
|
+
NEW_VERSION=$(python3 .github/scripts/version_manager.py increment | tail -n 1)
|
|
177
|
+
if [ -z "$NEW_VERSION" ]; then
|
|
178
|
+
echo "version bump failed"
|
|
179
|
+
exit 1
|
|
180
|
+
fi
|
|
181
|
+
python3 .github/scripts/version_manager.py sync
|
|
182
|
+
echo "release version confirmed: $NEW_VERSION"
|
|
183
|
+
fi
|
|
184
|
+
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
|
185
|
+
|
|
186
|
+
- name: Generate summary with the AI engine chain (fallback path)
|
|
187
|
+
if: steps.options.outputs.coderabbit != 'true' || steps.poll.outputs.summary_found != 'true'
|
|
188
|
+
env:
|
|
189
|
+
# PR title goes through the env block — never inline-interpolate
|
|
190
|
+
# untrusted text into the shell string (quote injection).
|
|
191
|
+
PR_TITLE: ${{ github.event.pull_request.title }}
|
|
192
|
+
AI_API_KEY: ${{ secrets.AI_API_KEY }}
|
|
193
|
+
AI_API_BASE_URL: ${{ vars.AI_API_BASE_URL }}
|
|
194
|
+
AI_MODEL: ${{ vars.AI_MODEL }}
|
|
195
|
+
GITHUB_TOKEN: ${{ github.token }}
|
|
196
|
+
run: |
|
|
197
|
+
VERSION=$(python3 .github/scripts/version_manager.py get | tail -n 1)
|
|
198
|
+
|
|
199
|
+
git fetch origin {{MAIN_BRANCH}}
|
|
200
|
+
git log --pretty=%s "origin/{{MAIN_BRANCH}}..HEAD" > commits.txt
|
|
201
|
+
echo "commits to summarize: $(wc -l < commits.txt)"
|
|
202
|
+
|
|
203
|
+
python3 .github/scripts/changelog_manager.py ai-summary \
|
|
204
|
+
--commits-file commits.txt \
|
|
205
|
+
--version "$VERSION" \
|
|
206
|
+
--output summary.md \
|
|
207
|
+
--pr-title "$PR_TITLE"
|
|
208
|
+
|
|
209
|
+
# Feed the generated summary through the same channel the
|
|
210
|
+
# CodeRabbit path uses: update-from-summary reads ./pr_body.md
|
|
211
|
+
cp summary.md pr_body.md
|
|
212
|
+
|
|
213
|
+
- name: Update CHANGELOG from summary
|
|
214
|
+
run: |
|
|
215
|
+
# note: `grep | sed || echo default` never fires the default (the
|
|
216
|
+
# pipeline exits with sed's 0) — default via ${var:-} instead
|
|
217
|
+
PROJECT_TYPE=$(grep "^project_type:" version.yml 2>/dev/null | sed 's/project_type: *"\([^"]*\)".*/\1/')
|
|
218
|
+
PROJECT_TYPE=${PROJECT_TYPE:-unknown}
|
|
219
|
+
PROJECT_TYPES=$(grep "^project_types:" version.yml 2>/dev/null | sed -E 's/.*\[([^]]*)\].*/\1/' | tr -d '" ')
|
|
220
|
+
PROJECT_TYPES=${PROJECT_TYPES:-$PROJECT_TYPE}
|
|
221
|
+
|
|
222
|
+
export VERSION="${{ steps.bump.outputs.new_version }}"
|
|
223
|
+
export PROJECT_TYPE PROJECT_TYPES
|
|
224
|
+
export TODAY=$(date '+%Y-%m-%d')
|
|
225
|
+
export PR_NUMBER="${{ github.event.pull_request.number }}"
|
|
226
|
+
export TIMESTAMP=$(date '+%Y-%m-%dT%H:%M:%SZ')
|
|
227
|
+
|
|
228
|
+
python3 .github/scripts/changelog_manager.py update-from-summary
|
|
229
|
+
python3 .github/scripts/changelog_manager.py generate-md
|
|
230
|
+
|
|
231
|
+
- name: Commit release docs to the PR head branch
|
|
232
|
+
run: |
|
|
233
|
+
HEAD_BRANCH="${{ github.event.pull_request.head.ref }}"
|
|
234
|
+
VERSION="${{ steps.bump.outputs.new_version }}"
|
|
235
|
+
|
|
236
|
+
# Working files must not land in the repo
|
|
237
|
+
rm -f pr_body.md summary.md commits.txt
|
|
238
|
+
|
|
239
|
+
git add -A
|
|
240
|
+
|
|
241
|
+
if git diff --staged --quiet; then
|
|
242
|
+
echo "no changes to commit"
|
|
243
|
+
else
|
|
244
|
+
git commit -m "chore(version): confirm v${VERSION} and update release docs [skip ci]"
|
|
245
|
+
|
|
246
|
+
# Race-condition guard: pull-rebase then push (up to 3 retries)
|
|
247
|
+
MAX_RETRIES=3
|
|
248
|
+
RETRY_COUNT=0
|
|
249
|
+
PUSH_SUCCESS=false
|
|
250
|
+
|
|
251
|
+
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
|
|
252
|
+
RETRY_COUNT=$((RETRY_COUNT + 1))
|
|
253
|
+
echo "push attempt $RETRY_COUNT/$MAX_RETRIES..."
|
|
254
|
+
|
|
255
|
+
if git push origin HEAD:$HEAD_BRANCH; then
|
|
256
|
+
PUSH_SUCCESS=true
|
|
257
|
+
echo "release docs pushed to $HEAD_BRANCH"
|
|
258
|
+
break
|
|
259
|
+
else
|
|
260
|
+
echo "push failed, syncing with remote..."
|
|
261
|
+
if git pull --rebase origin $HEAD_BRANCH; then
|
|
262
|
+
echo "rebase ok, retrying push..."
|
|
263
|
+
else
|
|
264
|
+
echo "rebase failed, manual conflict resolution required"
|
|
265
|
+
git rebase --abort 2>/dev/null || true
|
|
266
|
+
exit 1
|
|
267
|
+
fi
|
|
268
|
+
fi
|
|
269
|
+
done
|
|
270
|
+
|
|
271
|
+
if [ "$PUSH_SUCCESS" = false ]; then
|
|
272
|
+
echo "push failed after $MAX_RETRIES attempts"
|
|
273
|
+
exit 1
|
|
274
|
+
fi
|
|
275
|
+
fi
|
|
276
|
+
|
|
277
|
+
- name: Enable automerge
|
|
278
|
+
env:
|
|
279
|
+
# With a PAT the merge push triggers downstream workflows
|
|
280
|
+
# (RELEASE-PUBLISH etc.); GITHUB_TOKEN merges do not.
|
|
281
|
+
GH_TOKEN: ${{ secrets.WORKFLOW_PAT || github.token }}
|
|
282
|
+
run: |
|
|
283
|
+
PR_NUMBER=${{ github.event.pull_request.number }}
|
|
284
|
+
VERSION="${{ steps.bump.outputs.new_version }}"
|
|
285
|
+
|
|
286
|
+
# Merge-commit subject carries the release-confirm pattern —
|
|
287
|
+
# VERSION-CONTROL skips on it and RELEASE-PUBLISH proceeds on it.
|
|
288
|
+
SUBJECT="chore(release): v${VERSION} (PR #${PR_NUMBER})"
|
|
289
|
+
|
|
290
|
+
if gh pr merge "$PR_NUMBER" --auto --merge --subject "$SUBJECT"; then
|
|
291
|
+
echo "automerge enabled: $SUBJECT"
|
|
292
|
+
elif gh pr merge "$PR_NUMBER" --merge --subject "$SUBJECT"; then
|
|
293
|
+
echo "automerge unavailable — merged directly: $SUBJECT"
|
|
294
|
+
else
|
|
295
|
+
# Both attempts failed — diagnose instead of pretending success.
|
|
296
|
+
ALLOW_MERGE_COMMIT=$(gh api "repos/${GITHUB_REPOSITORY}" --jq '.allow_merge_commit' 2>/dev/null || echo "unknown")
|
|
297
|
+
if [ "$ALLOW_MERGE_COMMIT" = "false" ]; then
|
|
298
|
+
echo "::error::이 레포는 merge commit이 비활성화되어 있습니다 — Settings에서 'Allow merge commits'를 켜세요; 릴리스 감지는 merge commit subject를 사용합니다"
|
|
299
|
+
else
|
|
300
|
+
echo "::error::PR merge failed (allow_merge_commit=$ALLOW_MERGE_COMMIT) — check branch protection rules, required checks, and token permissions"
|
|
301
|
+
fi
|
|
302
|
+
exit 1
|
|
303
|
+
fi
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# ===================================================================
|
|
2
|
+
# PROJECT-COMMON-README-VERSION-UPDATE.yaml
|
|
3
|
+
# Automatic README version update workflow
|
|
4
|
+
# ===================================================================
|
|
5
|
+
#
|
|
6
|
+
# On every push to {{MAIN_BRANCH}} this workflow reads the latest version
|
|
7
|
+
# via version_manager.py and rewrites the version section of README.md.
|
|
8
|
+
#
|
|
9
|
+
# ===================================================================
|
|
10
|
+
# How it works:
|
|
11
|
+
# 1. Triggered by a push to {{MAIN_BRANCH}}
|
|
12
|
+
# 2. Reads the latest version via version_manager.py
|
|
13
|
+
# 3. Updates the version line in README.md
|
|
14
|
+
# 4. Commits and pushes the change ([skip ci])
|
|
15
|
+
#
|
|
16
|
+
# Supported version line formats (case-insensitive, flexible spacing):
|
|
17
|
+
# - ## 최신 버전 : v1.0.0 (2025-08-15)
|
|
18
|
+
# - ## 최신버전 : v1.0.0 (2025-08-15)
|
|
19
|
+
# - ## Current Version : v1.0.0 (2025-08-15)
|
|
20
|
+
# - ## Recent Version : v1.0.0 (2025-08-15)
|
|
21
|
+
# - ## Latest Version : v1.0.0
|
|
22
|
+
# - ## Version : v1.0.0
|
|
23
|
+
# - ## 버전 : v1.0.0
|
|
24
|
+
#
|
|
25
|
+
# The marker comment <!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->
|
|
26
|
+
# is added automatically above the version line.
|
|
27
|
+
#
|
|
28
|
+
# Unsupported formats (will fail):
|
|
29
|
+
# - markdown bold (**) inside the version line
|
|
30
|
+
# - missing colon
|
|
31
|
+
# - regex special characters (*, [, ], ^, $) in the version line
|
|
32
|
+
#
|
|
33
|
+
# Environment variables:
|
|
34
|
+
# - SHOW_DATE: whether to append the date (true/false, default: true)
|
|
35
|
+
# * true: v1.0.0 (2025-08-16)
|
|
36
|
+
# * false: v1.0.0
|
|
37
|
+
#
|
|
38
|
+
# Note (safety-net path): on a direct push to {{MAIN_BRANCH}} this
|
|
39
|
+
# workflow reads the pre-bump version — PROJECT-VERSION-CONTROL bumps
|
|
40
|
+
# afterwards with a [skip ci] commit that does not re-trigger this
|
|
41
|
+
# workflow, so the README stays one version behind until the next
|
|
42
|
+
# release/push. Run this workflow manually (workflow_dispatch) to catch up.
|
|
43
|
+
# ===================================================================
|
|
44
|
+
|
|
45
|
+
name: README VERSION UPDATE
|
|
46
|
+
|
|
47
|
+
env:
|
|
48
|
+
# Whether to show the date next to the version (true/false)
|
|
49
|
+
SHOW_DATE: true
|
|
50
|
+
|
|
51
|
+
concurrency:
|
|
52
|
+
group: readme-version-update
|
|
53
|
+
cancel-in-progress: false
|
|
54
|
+
|
|
55
|
+
on:
|
|
56
|
+
push:
|
|
57
|
+
branches: ["{{MAIN_BRANCH}}"]
|
|
58
|
+
workflow_dispatch:
|
|
59
|
+
|
|
60
|
+
permissions:
|
|
61
|
+
contents: write
|
|
62
|
+
pull-requests: write
|
|
63
|
+
|
|
64
|
+
jobs:
|
|
65
|
+
update-readme:
|
|
66
|
+
name: Update README version
|
|
67
|
+
runs-on: ubuntu-latest
|
|
68
|
+
steps:
|
|
69
|
+
- name: Checkout repository
|
|
70
|
+
uses: actions/checkout@v5
|
|
71
|
+
with:
|
|
72
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
73
|
+
fetch-depth: 0
|
|
74
|
+
ref: "{{MAIN_BRANCH}}"
|
|
75
|
+
|
|
76
|
+
- name: Git config
|
|
77
|
+
run: |
|
|
78
|
+
git config --local user.email "action@github.com"
|
|
79
|
+
git config --local user.name "GitHub Action"
|
|
80
|
+
|
|
81
|
+
- name: Check README.md exists
|
|
82
|
+
id: precheck
|
|
83
|
+
run: |
|
|
84
|
+
if [ -f README.md ]; then
|
|
85
|
+
echo "readme_exists=true" >> $GITHUB_OUTPUT
|
|
86
|
+
else
|
|
87
|
+
echo "readme_exists=false" >> $GITHUB_OUTPUT
|
|
88
|
+
echo "README.md not found — nothing to update, skipping"
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
- name: Read latest version via version_manager.py
|
|
92
|
+
id: version_info
|
|
93
|
+
if: steps.precheck.outputs.readme_exists == 'true'
|
|
94
|
+
run: |
|
|
95
|
+
LATEST_VERSION=$(python3 .github/scripts/version_manager.py get | tail -n 1)
|
|
96
|
+
# Guard: an empty version must never reach the README rewrite —
|
|
97
|
+
# it would commit a corrupted "v (date)" line.
|
|
98
|
+
if [ -z "$LATEST_VERSION" ]; then
|
|
99
|
+
echo "failed to read the current version"
|
|
100
|
+
exit 1
|
|
101
|
+
fi
|
|
102
|
+
RELEASE_DATE=$(date '+%Y-%m-%d')
|
|
103
|
+
|
|
104
|
+
echo "latest_version=$LATEST_VERSION" >> $GITHUB_OUTPUT
|
|
105
|
+
echo "release_date=$RELEASE_DATE" >> $GITHUB_OUTPUT
|
|
106
|
+
|
|
107
|
+
- name: Update version line in README.md
|
|
108
|
+
if: steps.precheck.outputs.readme_exists == 'true'
|
|
109
|
+
run: |
|
|
110
|
+
LATEST_VERSION="${{ steps.version_info.outputs.latest_version }}"
|
|
111
|
+
RELEASE_DATE="${{ steps.version_info.outputs.release_date }}"
|
|
112
|
+
SHOW_DATE="${{ env.SHOW_DATE }}"
|
|
113
|
+
|
|
114
|
+
# ---------------------------------------------------------
|
|
115
|
+
# Error message helper
|
|
116
|
+
# ---------------------------------------------------------
|
|
117
|
+
print_error_message() {
|
|
118
|
+
local error_type="$1"
|
|
119
|
+
local version_line="$2"
|
|
120
|
+
local header_part="$3"
|
|
121
|
+
local version_text="$4"
|
|
122
|
+
|
|
123
|
+
echo ""
|
|
124
|
+
echo "README.md version update failed"
|
|
125
|
+
echo " error type: $error_type"
|
|
126
|
+
echo " current version line: $version_line"
|
|
127
|
+
echo " extracted header: $header_part"
|
|
128
|
+
echo " new version text: $version_text"
|
|
129
|
+
echo ""
|
|
130
|
+
echo "Make sure the README version line matches one of:"
|
|
131
|
+
echo " ## 최신 버전 : v1.0.0 (2025-08-15)"
|
|
132
|
+
echo " ## Current Version : v1.0.0"
|
|
133
|
+
echo " ## Version : v1.0.0"
|
|
134
|
+
echo "Notes:"
|
|
135
|
+
echo " - no markdown bold (**) characters"
|
|
136
|
+
echo " - no regex special characters (*, [, ], ^, $)"
|
|
137
|
+
echo " - the version must come right after the colon (:)"
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if [ "$SHOW_DATE" = "true" ]; then
|
|
141
|
+
VERSION_TEXT="v${LATEST_VERSION} (${RELEASE_DATE})"
|
|
142
|
+
echo "version text with date: $VERSION_TEXT"
|
|
143
|
+
else
|
|
144
|
+
VERSION_TEXT="v${LATEST_VERSION}"
|
|
145
|
+
echo "version text: $VERSION_TEXT"
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
# Patterns for recognized version lines (case-insensitive, flexible spacing)
|
|
149
|
+
VERSION_PATTERNS=(
|
|
150
|
+
"^##[[:space:]]*최신[[:space:]]*버전[[:space:]]*:"
|
|
151
|
+
"^##[[:space:]]*최신버전[[:space:]]*:"
|
|
152
|
+
"^##[[:space:]]*[Cc]urrent[[:space:]]*[Vv]ersion[[:space:]]*:"
|
|
153
|
+
"^##[[:space:]]*[Rr]ecent[[:space:]]*[Vv]ersion[[:space:]]*:"
|
|
154
|
+
"^##[[:space:]]*[Vv]ersion[[:space:]]*:"
|
|
155
|
+
"^##[[:space:]]*버전[[:space:]]*:"
|
|
156
|
+
"^##[[:space:]]*[Ll]atest[[:space:]]*[Vv]ersion[[:space:]]*:"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
VERSION_LINE=""
|
|
160
|
+
PATTERN_FOUND=""
|
|
161
|
+
|
|
162
|
+
for pattern in "${VERSION_PATTERNS[@]}"; do
|
|
163
|
+
if grep -qi "$pattern" README.md; then
|
|
164
|
+
VERSION_LINE=$(grep -i "$pattern" README.md | head -n 1)
|
|
165
|
+
PATTERN_FOUND="$pattern"
|
|
166
|
+
echo "version line found: $VERSION_LINE"
|
|
167
|
+
break
|
|
168
|
+
fi
|
|
169
|
+
done
|
|
170
|
+
|
|
171
|
+
# No version line yet — insert a default one
|
|
172
|
+
if [ -z "$VERSION_LINE" ]; then
|
|
173
|
+
echo "no version line found — inserting a new one"
|
|
174
|
+
|
|
175
|
+
if grep -q "^# " README.md; then
|
|
176
|
+
# right after the first title
|
|
177
|
+
sed -i '/^# /a\\n<!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->\n## Latest Version : '"$VERSION_TEXT" README.md
|
|
178
|
+
else
|
|
179
|
+
# at the very top of the file
|
|
180
|
+
sed -i '1i\<!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->\n## Latest Version : '"$VERSION_TEXT"'\n' README.md
|
|
181
|
+
fi
|
|
182
|
+
echo "default version line inserted: $VERSION_TEXT"
|
|
183
|
+
else
|
|
184
|
+
# Update the existing version line
|
|
185
|
+
|
|
186
|
+
# ---------------------------------------------------------
|
|
187
|
+
# Validate the line format
|
|
188
|
+
# ---------------------------------------------------------
|
|
189
|
+
if [[ "$VERSION_LINE" =~ \*\* ]]; then
|
|
190
|
+
echo ""
|
|
191
|
+
echo "warning: the version line contains markdown bold (**) characters"
|
|
192
|
+
echo " current line: $VERSION_LINE"
|
|
193
|
+
echo " this may break the version update"
|
|
194
|
+
echo ""
|
|
195
|
+
fi
|
|
196
|
+
|
|
197
|
+
# Make sure the AUTO-VERSION-SECTION marker sits above the line
|
|
198
|
+
LINE_NUMBER=$(grep -n -i "$PATTERN_FOUND" README.md | head -n 1 | cut -d: -f1)
|
|
199
|
+
PREV_LINE_NUMBER=$((LINE_NUMBER - 1))
|
|
200
|
+
|
|
201
|
+
if [ $PREV_LINE_NUMBER -gt 0 ]; then
|
|
202
|
+
PREV_LINE=$(sed -n "${PREV_LINE_NUMBER}p" README.md)
|
|
203
|
+
if [[ "$PREV_LINE" != *"AUTO-VERSION-SECTION"* ]]; then
|
|
204
|
+
sed -i "${PREV_LINE_NUMBER}a\\<!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->" README.md
|
|
205
|
+
echo "auto-update marker comment added"
|
|
206
|
+
LINE_NUMBER=$((LINE_NUMBER + 1))
|
|
207
|
+
fi
|
|
208
|
+
else
|
|
209
|
+
sed -i '1i\<!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->' README.md
|
|
210
|
+
echo "auto-update marker comment added"
|
|
211
|
+
LINE_NUMBER=$((LINE_NUMBER + 1))
|
|
212
|
+
fi
|
|
213
|
+
|
|
214
|
+
# ---------------------------------------------------------
|
|
215
|
+
# Extract the header part (up to the colon)
|
|
216
|
+
# ---------------------------------------------------------
|
|
217
|
+
HEADER_PART=$(printf '%s\n' "$VERSION_LINE" | sed 's/\(.*:\).*/\1 /')
|
|
218
|
+
|
|
219
|
+
if [ -z "$HEADER_PART" ] || [[ "$HEADER_PART" == "$VERSION_LINE" ]]; then
|
|
220
|
+
print_error_message "header extraction failed - no colon (:) found" "$VERSION_LINE" "$HEADER_PART" "$VERSION_TEXT"
|
|
221
|
+
exit 1
|
|
222
|
+
fi
|
|
223
|
+
|
|
224
|
+
# ---------------------------------------------------------
|
|
225
|
+
# Replace by line number (avoids regex special-char issues)
|
|
226
|
+
# ---------------------------------------------------------
|
|
227
|
+
NEW_LINE="${HEADER_PART}${VERSION_TEXT}"
|
|
228
|
+
|
|
229
|
+
# Escape sed replacement metacharacters (backslash, & and the
|
|
230
|
+
# | delimiter) coming from README content so they cannot inject
|
|
231
|
+
# into the replacement expression
|
|
232
|
+
ESCAPED_LINE=$(printf '%s' "$NEW_LINE" | sed 's/[&\\|]/\\&/g')
|
|
233
|
+
|
|
234
|
+
if ! sed -i "${LINE_NUMBER}s|.*|${ESCAPED_LINE}|" README.md 2>/tmp/sed_error.log; then
|
|
235
|
+
SED_ERROR=$(cat /tmp/sed_error.log 2>/dev/null || echo "unknown sed error")
|
|
236
|
+
print_error_message "sed command failed - $SED_ERROR" "$VERSION_LINE" "$HEADER_PART" "$VERSION_TEXT"
|
|
237
|
+
exit 1
|
|
238
|
+
fi
|
|
239
|
+
|
|
240
|
+
# Verify the replacement
|
|
241
|
+
UPDATED_LINE=$(sed -n "${LINE_NUMBER}p" README.md)
|
|
242
|
+
if [[ "$UPDATED_LINE" != *"$VERSION_TEXT"* ]]; then
|
|
243
|
+
print_error_message "version replacement verification failed" "$VERSION_LINE" "$HEADER_PART" "$VERSION_TEXT"
|
|
244
|
+
echo " updated line: $UPDATED_LINE"
|
|
245
|
+
exit 1
|
|
246
|
+
fi
|
|
247
|
+
|
|
248
|
+
echo "version line updated: $VERSION_TEXT"
|
|
249
|
+
fi
|
|
250
|
+
|
|
251
|
+
- name: Commit and push changes
|
|
252
|
+
if: steps.precheck.outputs.readme_exists == 'true'
|
|
253
|
+
run: |
|
|
254
|
+
git add README.md
|
|
255
|
+
|
|
256
|
+
if git diff --staged --quiet; then
|
|
257
|
+
echo "no changes in README.md"
|
|
258
|
+
else
|
|
259
|
+
VERSION="${{ steps.version_info.outputs.latest_version }}"
|
|
260
|
+
|
|
261
|
+
git commit -m "docs(readme): update version to v$VERSION [skip ci]"
|
|
262
|
+
|
|
263
|
+
# Race-condition guard: pull-rebase then push (up to 3 retries)
|
|
264
|
+
MAX_RETRIES=3
|
|
265
|
+
RETRY_COUNT=0
|
|
266
|
+
PUSH_SUCCESS=false
|
|
267
|
+
|
|
268
|
+
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
|
|
269
|
+
RETRY_COUNT=$((RETRY_COUNT + 1))
|
|
270
|
+
echo "push attempt $RETRY_COUNT/$MAX_RETRIES..."
|
|
271
|
+
|
|
272
|
+
if git push origin HEAD:{{MAIN_BRANCH}}; then
|
|
273
|
+
PUSH_SUCCESS=true
|
|
274
|
+
echo "README.md version update pushed"
|
|
275
|
+
break
|
|
276
|
+
else
|
|
277
|
+
echo "push failed, syncing with remote..."
|
|
278
|
+
|
|
279
|
+
if git pull --rebase origin {{MAIN_BRANCH}}; then
|
|
280
|
+
echo "rebase ok, retrying push..."
|
|
281
|
+
else
|
|
282
|
+
echo "rebase failed, manual conflict resolution required"
|
|
283
|
+
git rebase --abort 2>/dev/null || true
|
|
284
|
+
exit 1
|
|
285
|
+
fi
|
|
286
|
+
fi
|
|
287
|
+
done
|
|
288
|
+
|
|
289
|
+
if [ "$PUSH_SUCCESS" = false ]; then
|
|
290
|
+
echo "push failed after $MAX_RETRIES attempts"
|
|
291
|
+
exit 1
|
|
292
|
+
fi
|
|
293
|
+
fi
|