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,289 @@
1
+ # ===================================================================
2
+ # PROJECT-COMMON-RELEASE-PUBLISH.yaml
3
+ # Tag + GitHub Release publisher (dual-mode)
4
+ # ===================================================================
5
+ #
6
+ # Publishes a git tag (vX.Y.Z) and a GitHub Release for every release
7
+ # that lands on {{MAIN_BRANCH}}. Two branch modes, read from version.yml
8
+ # (metadata.template.branches.mode, default: pr-flow):
9
+ #
10
+ # - pr-flow: releases arrive as merge commits produced by
11
+ # AUTO-CHANGELOG-CONTROL. Only pushes whose head commit message matches
12
+ # the release-confirm pattern "chore(release):" proceed; anything else
13
+ # exits cleanly. Version + changelog were already confirmed in the PR.
14
+ #
15
+ # - trunk-based: every direct push to {{MAIN_BRANCH}} is a release.
16
+ # This job bumps the version (patch +1 and sync), summarizes the commits
17
+ # since the last tag with the ai-summary engine chain (user API ->
18
+ # GitHub Models -> rule-based fallback), updates CHANGELOG.json /
19
+ # CHANGELOG.md, and commits everything back with [skip ci].
20
+ #
21
+ # Common tail (both modes): recovery-aware idempotency guard (tag and
22
+ # release checked separately so a half-published release can be finished
23
+ # on re-run), tag push, release notes = changelog export (ai_notes) +
24
+ # GitHub generate-notes API (contributors/compare links), then
25
+ # gh release create.
26
+ #
27
+ # Release-confirm pattern: "chore(release):" — keep in sync with
28
+ # PROJECT-COMMON-AUTO-CHANGELOG-CONTROL and PROJECT-COMMON-VERSION-CONTROL.
29
+ #
30
+ # workflow_dispatch republishes the current version (recovery path when a
31
+ # previous run failed after the version was confirmed but before the
32
+ # release was created). The idempotency guard keeps it safe to re-run.
33
+ # ===================================================================
34
+
35
+ name: PROJECT-RELEASE-PUBLISH
36
+
37
+ concurrency:
38
+ group: release-publish
39
+ cancel-in-progress: false
40
+
41
+ on:
42
+ push:
43
+ branches: ["{{MAIN_BRANCH}}"]
44
+ workflow_dispatch:
45
+
46
+ permissions:
47
+ contents: write
48
+ models: read
49
+
50
+ jobs:
51
+ release:
52
+ name: Tag and publish GitHub Release
53
+ # Bot housekeeping commits never publish releases
54
+ if: "!contains(github.event.head_commit.message, '[skip ci]')"
55
+ runs-on: ubuntu-latest
56
+ steps:
57
+ - name: Checkout repository
58
+ uses: actions/checkout@v5
59
+ with:
60
+ token: ${{ secrets.GITHUB_TOKEN }}
61
+ fetch-depth: 0
62
+ ref: "{{MAIN_BRANCH}}"
63
+
64
+ - name: Git config
65
+ run: |
66
+ git config --local user.email "action@github.com"
67
+ git config --local user.name "GitHub Action"
68
+
69
+ - name: Detect branch mode
70
+ id: mode
71
+ run: |
72
+ # scoped to the metadata: -> template: -> branches: chain so an
73
+ # unrelated top-level "mode:" key can never hijack the value
74
+ MODE=$(python3 -c 'import re; t=open("version.yml",encoding="utf-8").read(); m=re.search(r"metadata:.*?template:.*?branches:.*?mode:\s*\"?([A-Za-z-]+)", t, re.S); print(m.group(1) if m else "pr-flow")' 2>/dev/null || echo "pr-flow")
75
+ echo "mode=$MODE" >> $GITHUB_OUTPUT
76
+ echo "branch mode: $MODE"
77
+
78
+ - name: Release gate
79
+ id: gate
80
+ run: |
81
+ MODE="${{ steps.mode.outputs.mode }}"
82
+ EVENT="${{ github.event_name }}"
83
+ HEAD_MSG=$(git log -1 --pretty=%B)
84
+ PROCEED=false
85
+
86
+ if [ "$EVENT" = "workflow_dispatch" ]; then
87
+ # manual republish of the current version
88
+ PROCEED=true
89
+ elif [ "$MODE" = "trunk-based" ]; then
90
+ # every non-[skip ci] push to {{MAIN_BRANCH}} is a release
91
+ PROCEED=true
92
+ elif printf '%s\n' "$HEAD_MSG" | grep -q "chore(release):"; then
93
+ # pr-flow: only release-confirm merges publish a release
94
+ # (pattern shared with AUTO-CHANGELOG-CONTROL / VERSION-CONTROL)
95
+ PROCEED=true
96
+ fi
97
+
98
+ echo "proceed=$PROCEED" >> $GITHUB_OUTPUT
99
+ if [ "$PROCEED" = "true" ]; then
100
+ echo "release gate open (mode=$MODE, event=$EVENT)"
101
+ else
102
+ echo "not a release-confirm push in pr-flow mode — skipping (clean exit)"
103
+ fi
104
+
105
+ - name: Trunk-based version bump + changelog
106
+ if: >-
107
+ steps.gate.outputs.proceed == 'true' &&
108
+ steps.mode.outputs.mode == 'trunk-based' &&
109
+ github.event_name == 'push'
110
+ env:
111
+ AI_API_KEY: ${{ secrets.AI_API_KEY }}
112
+ AI_API_BASE_URL: ${{ vars.AI_API_BASE_URL }}
113
+ AI_MODEL: ${{ vars.AI_MODEL }}
114
+ GITHUB_TOKEN: ${{ github.token }}
115
+ run: |
116
+ # Idempotency: on a re-run of a failed job the checked-out HEAD may
117
+ # already be this workflow's own docs commit — never bump twice.
118
+ # Jump straight to the common tail (tag + release for the current
119
+ # version).
120
+ HEAD_MSG=$(git log -1 --pretty=%s)
121
+ if printf '%s\n' "$HEAD_MSG" | grep -q "^chore(version): release v.* docs \[skip ci\]"; then
122
+ echo "HEAD is already this workflow's release docs commit — skipping re-increment (idempotent re-run)"
123
+ exit 0
124
+ fi
125
+
126
+ NEW_VERSION=$(python3 .github/scripts/version_manager.py increment | tail -n 1)
127
+ if [ -z "$NEW_VERSION" ]; then
128
+ echo "version bump failed"
129
+ exit 1
130
+ fi
131
+ python3 .github/scripts/version_manager.py sync
132
+ echo "release version: $NEW_VERSION"
133
+
134
+ # Commits since the last tag (fall back to full history on first release)
135
+ LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
136
+ if [ -n "$LAST_TAG" ]; then
137
+ echo "collecting commits since $LAST_TAG"
138
+ git log --pretty=%s "${LAST_TAG}..HEAD" > commits.txt
139
+ else
140
+ echo "no previous tag — collecting the full history"
141
+ git log --pretty=%s > commits.txt
142
+ fi
143
+
144
+ python3 .github/scripts/changelog_manager.py ai-summary \
145
+ --commits-file commits.txt \
146
+ --version "$NEW_VERSION" \
147
+ --output summary.md
148
+
149
+ # Same input channel as AUTO-CHANGELOG-CONTROL:
150
+ # update-from-summary reads ./pr_body.md
151
+ cp summary.md pr_body.md
152
+
153
+ # note: `grep | sed || echo default` never fires the default (the
154
+ # pipeline exits with sed's 0) — default via ${var:-} instead
155
+ PROJECT_TYPE=$(grep "^project_type:" version.yml 2>/dev/null | sed 's/project_type: *"\([^"]*\)".*/\1/')
156
+ PROJECT_TYPE=${PROJECT_TYPE:-unknown}
157
+ PROJECT_TYPES=$(grep "^project_types:" version.yml 2>/dev/null | sed -E 's/.*\[([^]]*)\].*/\1/' | tr -d '" ')
158
+ PROJECT_TYPES=${PROJECT_TYPES:-$PROJECT_TYPE}
159
+
160
+ export VERSION="$NEW_VERSION"
161
+ export PROJECT_TYPE PROJECT_TYPES
162
+ export TODAY=$(date '+%Y-%m-%d')
163
+ export TIMESTAMP=$(date '+%Y-%m-%dT%H:%M:%SZ')
164
+
165
+ python3 .github/scripts/changelog_manager.py update-from-summary
166
+ python3 .github/scripts/changelog_manager.py generate-md
167
+
168
+ rm -f pr_body.md summary.md commits.txt
169
+
170
+ git add -A
171
+ if git diff --staged --quiet; then
172
+ echo "no changes to commit"
173
+ else
174
+ git commit -m "chore(version): release v${NEW_VERSION} docs [skip ci]"
175
+
176
+ # Race-condition guard: pull-rebase then push (up to 3 retries)
177
+ MAX_RETRIES=3
178
+ RETRY_COUNT=0
179
+ PUSH_SUCCESS=false
180
+
181
+ while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
182
+ RETRY_COUNT=$((RETRY_COUNT + 1))
183
+ echo "push attempt $RETRY_COUNT/$MAX_RETRIES..."
184
+
185
+ if git push origin HEAD:{{MAIN_BRANCH}}; then
186
+ PUSH_SUCCESS=true
187
+ echo "release docs pushed"
188
+ break
189
+ else
190
+ echo "push failed, syncing with remote..."
191
+ if git pull --rebase origin {{MAIN_BRANCH}}; then
192
+ echo "rebase ok, retrying push..."
193
+ else
194
+ echo "rebase failed, manual conflict resolution required"
195
+ git rebase --abort 2>/dev/null || true
196
+ exit 1
197
+ fi
198
+ fi
199
+ done
200
+
201
+ if [ "$PUSH_SUCCESS" = false ]; then
202
+ echo "push failed after $MAX_RETRIES attempts"
203
+ exit 1
204
+ fi
205
+ fi
206
+
207
+ - name: Resolve version + idempotency guard
208
+ id: version
209
+ if: steps.gate.outputs.proceed == 'true'
210
+ env:
211
+ GH_TOKEN: ${{ github.token }}
212
+ run: |
213
+ VERSION=$(python3 .github/scripts/version_manager.py get | tail -n 1)
214
+ if [ -z "$VERSION" ]; then
215
+ echo "failed to read version"
216
+ exit 1
217
+ fi
218
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
219
+
220
+ git fetch --tags origin
221
+
222
+ # Recovery-aware idempotency: tag and release are checked
223
+ # separately, so a run that died between "tag pushed" and
224
+ # "release created" can be re-run and still publish the release.
225
+ if git rev-parse "v$VERSION" >/dev/null 2>&1; then
226
+ echo "tag_exists=true" >> $GITHUB_OUTPUT
227
+ echo "tag v$VERSION already exists — skipping tag push"
228
+ else
229
+ echo "tag_exists=false" >> $GITHUB_OUTPUT
230
+ fi
231
+
232
+ if gh release view "v$VERSION" >/dev/null 2>&1; then
233
+ echo "release_exists=true" >> $GITHUB_OUTPUT
234
+ echo "release v$VERSION already exists — nothing to publish"
235
+ else
236
+ echo "release_exists=false" >> $GITHUB_OUTPUT
237
+ echo "publishing release v$VERSION"
238
+ fi
239
+
240
+ - name: Create and push tag
241
+ if: steps.gate.outputs.proceed == 'true' && steps.version.outputs.tag_exists != 'true'
242
+ run: |
243
+ VERSION="${{ steps.version.outputs.version }}"
244
+ git tag "v$VERSION"
245
+ git push origin "v$VERSION"
246
+ echo "tag v$VERSION pushed"
247
+
248
+ - name: Build release notes (changelog export + GitHub generate-notes)
249
+ if: steps.gate.outputs.proceed == 'true' && steps.version.outputs.release_exists != 'true'
250
+ env:
251
+ GH_TOKEN: ${{ github.token }}
252
+ run: |
253
+ VERSION="${{ steps.version.outputs.version }}"
254
+
255
+ # AI/changelog notes for this version
256
+ python3 .github/scripts/changelog_manager.py export --version "$VERSION" --output ai_notes.md || true
257
+
258
+ # GitHub-generated notes (contributors, compare link). Deliberately
259
+ # via the API instead of --generate-notes so both sources can be
260
+ # concatenated into a single notes file.
261
+ gh api "repos/${GITHUB_REPOSITORY}/releases/generate-notes" \
262
+ -f tag_name="v$VERSION" --jq '.body' > gh_notes.md || true
263
+
264
+ {
265
+ if [ -s ai_notes.md ]; then
266
+ cat ai_notes.md
267
+ echo ""
268
+ fi
269
+ if [ -s gh_notes.md ]; then
270
+ echo "---"
271
+ echo ""
272
+ cat gh_notes.md
273
+ fi
274
+ } > notes.md
275
+
276
+ if [ ! -s notes.md ]; then
277
+ echo "Release v$VERSION" > notes.md
278
+ fi
279
+
280
+ echo "release notes assembled ($(wc -c < notes.md) bytes)"
281
+
282
+ - name: Create GitHub Release
283
+ if: steps.gate.outputs.proceed == 'true' && steps.version.outputs.release_exists != 'true'
284
+ env:
285
+ GH_TOKEN: ${{ github.token }}
286
+ run: |
287
+ VERSION="${{ steps.version.outputs.version }}"
288
+ gh release create "v$VERSION" --title "v$VERSION" --notes-file notes.md
289
+ echo "GitHub Release v$VERSION published"
@@ -0,0 +1,192 @@
1
+ # ===================================================================
2
+ # PROJECT-COMMON-VERSION-CONTROL.yaml
3
+ # Safety-net automatic version bump
4
+ # ===================================================================
5
+ #
6
+ # [Safety net] Only when a direct push (one that did not go through a
7
+ # release PR) lands on {{MAIN_BRANCH}} (production), this workflow bumps
8
+ # the patch version and syncs every version file.
9
+ #
10
+ # Normal releases ({{DEVELOP_BRANCH}} -> {{MAIN_BRANCH}} PR) get their
11
+ # version confirmed by AUTO-CHANGELOG-CONTROL before the merge. The merge
12
+ # commit carries the release-confirm pattern "chore(release):" and the
13
+ # push includes a version.yml change, so this workflow skips via its guard.
14
+ #
15
+ # In trunk-based mode (metadata.template.branches.mode: trunk-based)
16
+ # RELEASE-PUBLISH owns version bumping, so this safety net is skipped
17
+ # entirely to avoid double increments.
18
+ #
19
+ # Release-confirm pattern: "chore(release):" — keep in sync with
20
+ # PROJECT-COMMON-AUTO-CHANGELOG-CONTROL and PROJECT-COMMON-RELEASE-PUBLISH.
21
+ # ===================================================================
22
+
23
+ name: PROJECT-VERSION-CONTROL
24
+
25
+ # Prevent concurrent version bumps
26
+ concurrency:
27
+ group: version-increment
28
+ cancel-in-progress: false
29
+
30
+ on:
31
+ push:
32
+ branches: ["{{MAIN_BRANCH}}"]
33
+ paths-ignore:
34
+ - 'CHANGELOG.md'
35
+ - 'CHANGELOG.json'
36
+ - 'version.yml' # prevent infinite loop
37
+ workflow_dispatch:
38
+
39
+ permissions:
40
+ contents: write
41
+
42
+ jobs:
43
+ version-bump:
44
+ name: Safety-net version bump
45
+ runs-on: ubuntu-latest
46
+
47
+ steps:
48
+ - name: Checkout repository
49
+ uses: actions/checkout@v5
50
+ with:
51
+ token: ${{ secrets.GITHUB_TOKEN }}
52
+ fetch-depth: 0
53
+ ref: "{{MAIN_BRANCH}}"
54
+
55
+ - name: Release merge / mode guard
56
+ id: release_guard
57
+ run: |
58
+ SKIP="false"
59
+
60
+ # trunk-based mode: RELEASE-PUBLISH owns versioning — skip safety net
61
+ MODE=$(python3 -c 'import re; t=open("version.yml",encoding="utf-8").read(); m=re.search(r"^\s*mode:\s*\"?([A-Za-z-]+)", t, re.M); print(m.group(1) if m else "pr-flow")' 2>/dev/null || echo "pr-flow")
62
+ if [ "$MODE" = "trunk-based" ]; then
63
+ SKIP="true"
64
+ echo "trunk-based mode detected — RELEASE-PUBLISH owns version bumping, skipping"
65
+ fi
66
+
67
+ # release-confirm pattern: the merge commit of a release PR
68
+ HEAD_MSG=$(git log -1 --pretty=%B)
69
+ if echo "$HEAD_MSG" | grep -q "chore(release):"; then
70
+ SKIP="true"
71
+ echo "release-confirm commit detected (chore(release):) — skipping safety-net bump"
72
+ fi
73
+
74
+ # release merge detection: the release push includes a version.yml change
75
+ BEFORE="${{ github.event.before }}"
76
+ if [ "$BEFORE" = "0000000000000000000000000000000000000000" ] || ! git cat-file -e "$BEFORE" 2>/dev/null; then
77
+ RANGE="HEAD^..HEAD"
78
+ else
79
+ RANGE="$BEFORE..HEAD"
80
+ fi
81
+ if git diff --name-only $RANGE 2>/dev/null | grep -qx "version.yml"; then
82
+ SKIP="true"
83
+ echo "release merge detected (push contains a version.yml change) — skipping safety-net bump"
84
+ fi
85
+
86
+ echo "skip=$SKIP" >> $GITHUB_OUTPUT
87
+ if [ "$SKIP" = "false" ]; then
88
+ echo "direct push to {{MAIN_BRANCH}} detected — running safety-net version bump"
89
+ fi
90
+
91
+ - name: Read current version
92
+ id: current_version
93
+ if: steps.release_guard.outputs.skip != 'true'
94
+ run: |
95
+ CURRENT_VERSION=$(python3 .github/scripts/version_manager.py get | tail -n 1)
96
+ if [ -z "$CURRENT_VERSION" ]; then
97
+ echo "failed to read current version"
98
+ exit 1
99
+ fi
100
+ echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
101
+ echo "current version: $CURRENT_VERSION"
102
+
103
+ - name: Bump patch version and sync
104
+ id: version
105
+ if: steps.release_guard.outputs.skip != 'true'
106
+ run: |
107
+ NEW_VERSION=$(python3 .github/scripts/version_manager.py increment | tail -n 1)
108
+ if [ -z "$NEW_VERSION" ]; then
109
+ echo "version bump failed"
110
+ exit 1
111
+ fi
112
+ python3 .github/scripts/version_manager.py sync
113
+ echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
114
+ echo "new version: $NEW_VERSION"
115
+
116
+ - name: Read project type
117
+ id: project_info
118
+ if: steps.release_guard.outputs.skip != 'true'
119
+ run: |
120
+ if [ -f "version.yml" ]; then
121
+ PROJECT_TYPE=$(grep "^project_type:" version.yml | sed 's/project_type: *"\([^"]*\)".*/\1/' || echo "unknown")
122
+ PROJECT_TYPES=$(grep "^project_types:" version.yml 2>/dev/null | sed -E 's/.*\[([^]]*)\].*/\1/' | tr -d '" ' || echo "")
123
+ if [ -z "$PROJECT_TYPES" ]; then
124
+ PROJECT_TYPES="$PROJECT_TYPE"
125
+ fi
126
+ echo "project_type=$PROJECT_TYPE" >> $GITHUB_OUTPUT
127
+ echo "project_types=$PROJECT_TYPES" >> $GITHUB_OUTPUT
128
+ echo "project type: $PROJECT_TYPE (types: $PROJECT_TYPES)"
129
+ else
130
+ echo "project_type=unknown" >> $GITHUB_OUTPUT
131
+ echo "project_types=unknown" >> $GITHUB_OUTPUT
132
+ echo "version.yml not found"
133
+ fi
134
+
135
+ - name: Commit and push changes
136
+ if: steps.release_guard.outputs.skip != 'true'
137
+ run: |
138
+ git config --local user.email "action@github.com"
139
+ git config --local user.name "GitHub Action"
140
+
141
+ git add -A
142
+
143
+ if git diff --staged --quiet; then
144
+ echo "no changes to commit (already in sync)"
145
+ else
146
+ NEW_VERSION="${{ steps.version.outputs.new_version }}"
147
+ COMMIT_MSG="chore(version): bump to v${NEW_VERSION} [skip ci]"
148
+
149
+ if git commit -m "$COMMIT_MSG"; then
150
+ # Race-condition guard: pull-rebase then push (up to 3 retries)
151
+ MAX_RETRIES=3
152
+ RETRY_COUNT=0
153
+ PUSH_SUCCESS=false
154
+
155
+ while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
156
+ RETRY_COUNT=$((RETRY_COUNT + 1))
157
+ echo "push attempt $RETRY_COUNT/$MAX_RETRIES..."
158
+
159
+ if git push origin HEAD:{{MAIN_BRANCH}}; then
160
+ PUSH_SUCCESS=true
161
+ echo "version bump commit pushed"
162
+ break
163
+ else
164
+ echo "push failed, syncing with remote..."
165
+ if git pull --rebase origin {{MAIN_BRANCH}}; then
166
+ echo "rebase ok, retrying push..."
167
+ else
168
+ echo "rebase failed, manual conflict resolution required"
169
+ git rebase --abort 2>/dev/null || true
170
+ exit 1
171
+ fi
172
+ fi
173
+ done
174
+
175
+ if [ "$PUSH_SUCCESS" = false ]; then
176
+ echo "push failed after $MAX_RETRIES attempts"
177
+ exit 1
178
+ fi
179
+ else
180
+ echo "commit failed"
181
+ exit 1
182
+ fi
183
+ fi
184
+
185
+ - name: Summary
186
+ if: always()
187
+ run: |
188
+ echo "version-control workflow finished"
189
+ echo " skipped (release merge / trunk-based): ${{ steps.release_guard.outputs.skip }}"
190
+ echo " previous version: ${{ steps.current_version.outputs.current_version }}"
191
+ echo " new version: ${{ steps.version.outputs.new_version }}"
192
+ echo " branch: ${{ github.ref_name }}"
@@ -0,0 +1,209 @@
1
+ # ===================================================================
2
+ # PROJECT-COMMON-SECRET-FILE-UPLOAD.yaml
3
+ # GitHub Secret 파일 서버(SSH) 자동 업로드
4
+ # ===================================================================
5
+ #
6
+ # GitHub Secrets에 저장된 설정 파일들을 서버(SSH 접속 가능한 호스트)에
7
+ # 자동 업로드하여 변경 이력을 추적합니다.
8
+ # 타임스탬프 기반 백업으로 변경 히스토리를 보존합니다.
9
+ #
10
+ # ===================================================================
11
+ # 🤖 AI 에이전트 커스텀 가이드
12
+ # ===================================================================
13
+ # 이 워크플로우를 프로젝트에 맞게 수정하려면,
14
+ # 아래 프롬프트를 AI 에이전트에게 전달하세요:
15
+ #
16
+ # 📋 프롬프트:
17
+ # "PROJECT-COMMON-SECRET-FILE-UPLOAD.yaml을
18
+ # 프로젝트에 맞게 수정해줘.
19
+ # - 프로젝트명: {프로젝트명}
20
+ # - 역할(ROLE): {backend/frontend 등}
21
+ # - 서버 저장 경로: {서버 저장 경로}
22
+ # - SSH 포트: {포트번호}
23
+ # - 업로드할 GitHub Secrets:
24
+ # SECRET_KEY → 저장할_파일명.확장자
25
+ # 기존 파일 목록 섹션에 추가하고,
26
+ # 없는 Secret은 스킵하도록 해줘."
27
+ #
28
+ # 💡 예시:
29
+ # "- APPLICATION_PROD_YML → application-prod.yml
30
+ # - FIREBASE_KEY_JSON → firebase-admin-sdk.json
31
+ # - VERTEX_SA_KEY → vertex-sa-key.json
32
+ # - ENV_FILE → .env
33
+ # - ADMIN_YML → admin.yml"
34
+ # ===================================================================
35
+ #
36
+ # ===================================================================
37
+ # 🔑 필수 GitHub Secrets
38
+ # ===================================================================
39
+ # SERVER_HOST: 서버 호스트(SSH 접속 주소)
40
+ # SERVER_USER: SSH 사용자명
41
+ # SERVER_PASSWORD: SSH 비밀번호
42
+ # ===================================================================
43
+ #
44
+ # 🔧 환경변수 설정 (env 섹션에서 설정)
45
+ # ===================================================================
46
+ #
47
+ # 📦 프로젝트 설정:
48
+ # PROJECT_NAME: 프로젝트명 (서버 경로에 사용)
49
+ # ROLE: 역할 구분 (backend, frontend 등)
50
+ # SERVER_BASE_PATH: 서버 기본 경로 (기본: /volume1/projects, 예: AWS EC2 /home/ubuntu/projects)
51
+ #
52
+ # 🔌 SSH 연결:
53
+ # SSH_PORT: SSH 포트 (기본: 2022)
54
+ # ===================================================================
55
+
56
+ name: PROJECT-COMMON-SECRET-FILE-UPLOAD
57
+
58
+ # ===================================================================
59
+ # 트리거 설정
60
+ # ===================================================================
61
+ # {{MAIN_BRANCH}} 브랜치에 push할 때 자동 실행됩니다.
62
+ # workflow_dispatch로 수동 실행도 가능합니다.
63
+ on:
64
+ push:
65
+ branches:
66
+ - "{{DEVELOP_BRANCH}}"
67
+ workflow_dispatch:
68
+
69
+ concurrency:
70
+ group: secret-file-upload-${{ github.ref }}
71
+ cancel-in-progress: true
72
+
73
+ # ===================================================================
74
+ # 환경 변수 설정
75
+ # ===================================================================
76
+ env:
77
+ # 🔧 프로젝트 설정 - 프로젝트에 맞게 수정하세요
78
+ PROJECT_NAME: "my-project"
79
+ ROLE: "backend"
80
+ SERVER_BASE_PATH: "/volume1/projects"
81
+ SSH_PORT: "2022"
82
+
83
+ jobs:
84
+ upload-secret-files:
85
+ name: Secret 파일 업로드
86
+ runs-on: ubuntu-latest
87
+ # [skip ci] 커밋 메시지가 포함된 경우 실행하지 않음
88
+ if: "!contains(github.event.head_commit.message, '[skip ci]')"
89
+
90
+ steps:
91
+ - name: 타임스탬프 및 커밋 정보 생성
92
+ run: |
93
+ export TZ='Asia/Seoul'
94
+ TIMESTAMP=$(date '+%Y-%m-%d_%H-%M-%S')
95
+ echo "TIMESTAMP=$TIMESTAMP" >> $GITHUB_ENV
96
+ echo "BUILD_DATE=$(date '+%Y-%m-%d %H:%M')" >> $GITHUB_ENV
97
+ echo "SHORT_COMMIT_HASH=$(echo "${{ github.sha }}" | cut -c1-7)" >> $GITHUB_ENV
98
+ echo "생성된 타임스탬프: $TIMESTAMP"
99
+
100
+ - name: Secret 파일 업로드
101
+ uses: appleboy/ssh-action@v1.0.3
102
+ with:
103
+ host: ${{ secrets.SERVER_HOST }}
104
+ username: ${{ secrets.SERVER_USER }}
105
+ password: ${{ secrets.SERVER_PASSWORD }}
106
+ port: ${{ env.SSH_PORT }}
107
+ script: |
108
+ set -e
109
+
110
+ # GitHub Actions 환경변수 직접 설정
111
+ PROJECT_NAME="${{ env.PROJECT_NAME }}"
112
+ ROLE="${{ env.ROLE }}"
113
+ SERVER_BASE_PATH="${{ env.SERVER_BASE_PATH }}"
114
+ TIMESTAMP="${{ env.TIMESTAMP }}"
115
+ SHORT_COMMIT_HASH="${{ env.SHORT_COMMIT_HASH }}"
116
+ BUILD_DATE="${{ env.BUILD_DATE }}"
117
+
118
+ # 경로 설정
119
+ BASE_DIR="${SERVER_BASE_PATH}/${PROJECT_NAME}/github_secret/${ROLE}"
120
+
121
+ echo "=== Secret 파일 업로드 시작 ==="
122
+ echo "프로젝트: ${PROJECT_NAME} (${ROLE})"
123
+ echo "경로: ${BASE_DIR}"
124
+ echo "타임스탬프: ${TIMESTAMP}"
125
+
126
+ # 디렉토리 생성
127
+ export PW="${{ secrets.SERVER_PASSWORD }}"
128
+ echo "$PW" | sudo -S mkdir -p "${BASE_DIR}/${TIMESTAMP}"
129
+
130
+ # ============================================
131
+ # 📦 파일 업로드 섹션
132
+ # Secret이 비어있으면 자동 스킵됩니다.
133
+ # AI 에이전트로 이 섹션에 파일을 추가하세요.
134
+ # ============================================
135
+
136
+ # --- ENV_FILE → .env ---
137
+ cat << '__SUH_SECRET_CHECK_EOF__' > /tmp/_secret_check
138
+ ${{ secrets.ENV_FILE }}
139
+ __SUH_SECRET_CHECK_EOF__
140
+ if [ -s /tmp/_secret_check ] && [ -n "$(tr -d '[:space:]' < /tmp/_secret_check)" ]; then
141
+ echo "ENV_FILE → .env 업로드 중..."
142
+ cat << '__SUH_FILE_CONTENT_EOF__' | sudo tee "${BASE_DIR}/.env" > /dev/null
143
+ ${{ secrets.ENV_FILE }}
144
+ __SUH_FILE_CONTENT_EOF__
145
+ cat << '__SUH_FILE_CONTENT_EOF__' | sudo tee "${BASE_DIR}/${TIMESTAMP}/.env" > /dev/null
146
+ ${{ secrets.ENV_FILE }}
147
+ __SUH_FILE_CONTENT_EOF__
148
+ echo ".env 업로드 완료"
149
+ else
150
+ echo "ENV_FILE 스킵 (Secret 미설정)"
151
+ fi
152
+ rm -f /tmp/_secret_check
153
+
154
+ # --- APPLICATION_PROD_YML → application-prod.yml ---
155
+ cat << '__SUH_SECRET_CHECK_EOF__' > /tmp/_secret_check
156
+ ${{ secrets.APPLICATION_PROD_YML }}
157
+ __SUH_SECRET_CHECK_EOF__
158
+ if [ -s /tmp/_secret_check ] && [ -n "$(tr -d '[:space:]' < /tmp/_secret_check)" ]; then
159
+ echo "APPLICATION_PROD_YML → application-prod.yml 업로드 중..."
160
+ cat << '__SUH_FILE_CONTENT_EOF__' | sudo tee "${BASE_DIR}/application-prod.yml" > /dev/null
161
+ ${{ secrets.APPLICATION_PROD_YML }}
162
+ __SUH_FILE_CONTENT_EOF__
163
+ cat << '__SUH_FILE_CONTENT_EOF__' | sudo tee "${BASE_DIR}/${TIMESTAMP}/application-prod.yml" > /dev/null
164
+ ${{ secrets.APPLICATION_PROD_YML }}
165
+ __SUH_FILE_CONTENT_EOF__
166
+ echo "application-prod.yml 업로드 완료"
167
+ else
168
+ echo "APPLICATION_PROD_YML 스킵 (Secret 미설정)"
169
+ fi
170
+ rm -f /tmp/_secret_check
171
+
172
+ # ============================================
173
+ # 📋 메타데이터 JSON 생성
174
+ # ============================================
175
+ cat << EOF | sudo tee "${BASE_DIR}/${TIMESTAMP}/cicd-gitignore-file.json" > /dev/null
176
+ {
177
+ "build_info": {
178
+ "timestamp": "$TIMESTAMP",
179
+ "workflow": "Secret 파일 자동 업로드",
180
+ "run_id": "${{ github.run_id }}",
181
+ "run_number": "${{ github.run_number }}",
182
+ "event": "${{ github.event_name }}",
183
+ "repository": "${{ github.repository }}",
184
+ "branch": "${{ github.ref_name }}",
185
+ "commit_hash": "${{ github.sha }}",
186
+ "short_hash": "$SHORT_COMMIT_HASH",
187
+ "commit_url": "https://github.com/${{ github.repository }}/commit/${{ github.sha }}",
188
+ "actor": "${{ github.actor }}",
189
+ "build_date": "$BUILD_DATE"
190
+ },
191
+ "files": [
192
+ {
193
+ "secret_name": "ENV_FILE",
194
+ "file_name": ".env",
195
+ "last_updated": "$BUILD_DATE"
196
+ },
197
+ {
198
+ "secret_name": "APPLICATION_PROD_YML",
199
+ "file_name": "application-prod.yml",
200
+ "last_updated": "$BUILD_DATE"
201
+ }
202
+ ]
203
+ }
204
+ EOF
205
+
206
+ # 타임스탬프 인덱스 업데이트
207
+ echo "{\"last_updated\": \"${TIMESTAMP}\", \"commit\": \"${SHORT_COMMIT_HASH}\", \"actor\": \"${{ github.actor }}\", \"status\": \"completed\"}" | sudo tee "${BASE_DIR}/timestamp_index.json" > /dev/null
208
+
209
+ echo "=== 모든 파일 업로드 완료 ==="