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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +139 -0
  3. package/bin/project-auto-wizard.js +19 -0
  4. package/package.json +36 -0
  5. package/payload/coderabbit.yaml +19 -0
  6. package/payload/config/breaking-changes.json +1 -0
  7. package/payload/config/wizard-prompts.yml +62 -0
  8. package/payload/scripts/changelog_manager.py +726 -0
  9. package/payload/scripts/version_manager.py +617 -0
  10. package/payload/version.yml.template +48 -0
  11. package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +303 -0
  12. package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +293 -0
  13. package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +289 -0
  14. package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +192 -0
  15. package/payload/workflows/common/secret-backup/PROJECT-COMMON-SECRET-FILE-UPLOAD.yaml +209 -0
  16. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +591 -0
  17. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +700 -0
  18. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +308 -0
  19. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +992 -0
  20. package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +689 -0
  21. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +987 -0
  22. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +471 -0
  23. package/payload/workflows/flutter/PROJECT-FLUTTER-SUH-LAB-APP-BUILD-TRIGGER.yaml +500 -0
  24. package/payload/workflows/next/PROJECT-NEXT-CI.yaml +185 -0
  25. package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +271 -0
  26. package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +81 -0
  27. package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +2191 -0
  28. package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +386 -0
  29. package/payload/workflows/react/PROJECT-REACT-CI.yaml +194 -0
  30. package/payload/workflows/react/PROJECT-REACT-CICD.yaml +255 -0
  31. package/payload/workflows/spring/PROJECT-SPRING-GITHUB-PACKAGES-PUBLISH.yml +84 -0
  32. package/payload/workflows/spring/nexus/PROJECT-SPRING-NEXUS-CI.yml +316 -0
  33. package/payload/workflows/spring/nexus/PROJECT-SPRING-NEXUS-PUBLISH.yml +58 -0
  34. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +535 -0
  35. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +437 -0
  36. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +2277 -0
  37. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +425 -0
  38. package/src/cli/args.js +95 -0
  39. package/src/cli/help.js +27 -0
  40. package/src/commands/full.js +53 -0
  41. package/src/commands/interactive.js +276 -0
  42. package/src/commands/revert.js +60 -0
  43. package/src/commands/version.js +31 -0
  44. package/src/commands/workflows.js +49 -0
  45. package/src/context.js +26 -0
  46. package/src/core/assets.js +47 -0
  47. package/src/core/branches.js +60 -0
  48. package/src/core/branding.js +15 -0
  49. package/src/core/breaking-check.js +65 -0
  50. package/src/core/breaking.js +26 -0
  51. package/src/core/copy/coderabbit.js +26 -0
  52. package/src/core/copy/gitignore.js +55 -0
  53. package/src/core/copy/readme.js +30 -0
  54. package/src/core/copy/simple.js +23 -0
  55. package/src/core/copy/workflows.js +233 -0
  56. package/src/core/detect-fs.js +106 -0
  57. package/src/core/detect.js +62 -0
  58. package/src/core/fsutil.js +46 -0
  59. package/src/core/options-ask.js +99 -0
  60. package/src/core/paths-resolve.js +261 -0
  61. package/src/core/paths.js +16 -0
  62. package/src/core/version-yml.js +190 -0
  63. package/src/core/wizard-env.js +101 -0
  64. package/src/core/wizard-labels.js +107 -0
  65. package/src/index.js +162 -0
  66. package/src/ui/ansi.js +26 -0
  67. package/src/ui/banner.js +29 -0
  68. package/src/ui/env-plan.js +207 -0
  69. package/src/ui/prompts.js +99 -0
  70. package/src/ui/readline-engine.js +257 -0
  71. package/src/ui/status-cards.js +56 -0
  72. 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