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,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 "=== 모든 파일 업로드 완료 ==="
|