project-auto-wizard 0.15.0 → 0.16.0
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/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +2 -2
- package/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/payload/config/breaking-changes.json +31 -5
- package/payload/config/wizard-prompts.yml +0 -4
- package/payload/scripts/messages.py +162 -26
- package/payload/scripts/version_manager.py +69 -40
- package/payload/version.yml.template +6 -2
- package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +3 -0
- package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +64 -1
- package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +61 -12
- package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +46 -10
- package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +40 -11
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +24 -8
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +29 -10
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +57 -23
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +155 -72
- package/payload/workflows/flutter/PROJECT-FLUTTER-APP-BUILD-TRIGGER.yaml +49 -20
- package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +162 -62
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +184 -87
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +38 -15
- package/payload/workflows/go/PROJECT-GO-CI.yaml +14 -5
- package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +375 -120
- package/payload/workflows/go/PROJECT-GO-SIMPLE-CICD.yaml +9 -4
- package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +12 -4
- package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +375 -120
- package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +9 -4
- package/payload/workflows/react/PROJECT-REACT-CI.yaml +38 -9
- package/payload/workflows/react/PROJECT-REACT-CICD.yaml +19 -5
- package/payload/workflows/spring/PROJECT-SPRING-CI.yml +71 -35
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +9 -6
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +7 -4
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +425 -144
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +7 -4
- package/src/cli/args.js +32 -22
- package/src/commands/doctor.js +20 -0
- package/src/commands/dry-run.js +9 -1
- package/src/commands/full.js +9 -4
- package/src/commands/interactive.js +29 -30
- package/src/commands/status.js +19 -5
- package/src/context.js +4 -2
- package/src/core/copy/simple.js +31 -0
- package/src/core/copy/workflows.js +2 -2
- package/src/core/deploy-style.js +1 -1
- package/src/core/detect.js +3 -2
- package/src/core/options.js +88 -0
- package/src/core/release-options.js +6 -23
- package/src/core/types.js +15 -8
- package/src/core/version-yml.js +68 -26
- package/src/i18n/catalog/en/cli.js +3 -1
- package/src/i18n/catalog/en/commands.js +15 -0
- package/src/i18n/catalog/en/core2.js +3 -0
- package/src/i18n/catalog/en/ui.js +1 -0
- package/src/i18n/catalog/ko/cli.js +3 -1
- package/src/i18n/catalog/ko/commands.js +15 -0
- package/src/i18n/catalog/ko/core2.js +3 -0
- package/src/i18n/catalog/ko/ui.js +1 -0
- package/src/index.js +7 -3
- package/src/ui/env-plan.js +1 -1
- package/src/ui/prompts.js +9 -5
- package/src/ui/status-cards.js +2 -2
- package/payload/workflows/next/PROJECT-NEXT-CI.yaml +0 -241
- package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +0 -330
package/src/ui/prompts.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
import * as engine from "./readline-engine.js";
|
|
6
6
|
import { t } from "../i18n/index.js";
|
|
7
7
|
import { DEPLOY_STYLES, NO_DEPLOY_STYLE } from "../core/deploy-style.js";
|
|
8
|
-
import { TYPE_IDS } from "../core/types.js";
|
|
8
|
+
import { TYPE_IDS, typeInfo } from "../core/types.js";
|
|
9
|
+
import { askableOptions } from "../core/options.js";
|
|
9
10
|
import { ENV_MODES, DEFAULT_ENV_MODE, STORE_PLATFORMS, DEPLOY_MODES, DEFAULT_DEPLOY_MODE, deployModeWarning } from "../core/flutter-options.js";
|
|
10
11
|
|
|
11
12
|
export const CANCEL = engine.CANCEL;
|
|
@@ -47,8 +48,7 @@ export function editMenuOptions({ showFlutter = false, showOptions = false } = {
|
|
|
47
48
|
{ value: "branch", label: t("ui.prompts.edit.branch") },
|
|
48
49
|
];
|
|
49
50
|
if (showOptions) {
|
|
50
|
-
options.push({ value:
|
|
51
|
-
options.push({ value: "copilotAi", label: t("ui.prompts.edit.copilotAi") });
|
|
51
|
+
for (const o of askableOptions()) options.push({ value: o.name, label: t(o.ask.menuLabelKey) });
|
|
52
52
|
}
|
|
53
53
|
if (showFlutter) {
|
|
54
54
|
options.push({ value: "envMode", label: t("ui.prompts.edit.envMode") });
|
|
@@ -71,7 +71,7 @@ export const ALL_TYPES = TYPE_IDS;
|
|
|
71
71
|
export async function selectTypes(current = []) {
|
|
72
72
|
return engine.multiselect({
|
|
73
73
|
message: t("ui.prompts.types.select"),
|
|
74
|
-
options: ALL_TYPES.map((type) => ({ value: type, label: type })),
|
|
74
|
+
options: ALL_TYPES.map((type) => ({ value: type, label: type, hint: typeInfo(type).displayName })),
|
|
75
75
|
initialValues: current.length ? current : ["basic"],
|
|
76
76
|
required: true,
|
|
77
77
|
});
|
|
@@ -91,7 +91,11 @@ export async function confirmTypes({ types = [], markers = null } = {}) {
|
|
|
91
91
|
options: ALL_TYPES.map((type) => {
|
|
92
92
|
const marker = markers?.get?.(type);
|
|
93
93
|
// Only detected types get evidence - the rest are just listed as candidates.
|
|
94
|
-
return {
|
|
94
|
+
return {
|
|
95
|
+
value: type,
|
|
96
|
+
label: detected.has(type) && marker ? t("ui.prompts.types.withMarker", { type, marker }) : type,
|
|
97
|
+
hint: typeInfo(type).displayName,
|
|
98
|
+
};
|
|
95
99
|
}),
|
|
96
100
|
initialValues: types.length ? types : ["basic"],
|
|
97
101
|
required: true,
|
package/src/ui/status-cards.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { A, paint } from "./ansi.js";
|
|
4
4
|
import { t } from "../i18n/index.js";
|
|
5
5
|
import { DEFAULT_DEPLOY_MODE } from "../core/flutter-options.js";
|
|
6
|
+
import { askableOptions } from "../core/options.js";
|
|
6
7
|
|
|
7
8
|
const GUT = paint("│", A.gray);
|
|
8
9
|
const HEAD = paint("◆", A.cyan);
|
|
@@ -59,8 +60,7 @@ export function printAnalysisCard({ mode = "", modeLabel = "", types = [], versi
|
|
|
59
60
|
// Optional workflows - a saved value is used without asking, so show the current value before confirmation.
|
|
60
61
|
if (options) {
|
|
61
62
|
const onOff = (v) => (v ? paint(t("ui.status-cards.card.on"), A.green) : paint(t("ui.status-cards.card.off"), A.dim));
|
|
62
|
-
row(
|
|
63
|
-
row("🤖", "Copilot", onOff(options.copilotAi));
|
|
63
|
+
for (const o of askableOptions()) row(o.ask.cardIcon, t(o.ask.cardLabelKey), onOff(options[o.name]));
|
|
64
64
|
}
|
|
65
65
|
// Monorepo paths - shown when at least one entry is not the root
|
|
66
66
|
const nonRoot = [...paths.entries()].filter(([, p]) => p && p !== ".");
|
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
# project-auto-wizard:managed-workflow
|
|
2
|
-
# Next.js CI (build verification)
|
|
3
|
-
# On push/PR to the {{DEVELOP_BRANCH}} branch, verifies with npm ci, npm test (when a test script exists) and npm run build. Caches node_modules and .next/cache.
|
|
4
|
-
#
|
|
5
|
-
# GitHub Secrets
|
|
6
|
-
# ENV_FILE (optional): contents of the .env file. CI only verifies the build, so no deploy-related Secrets are needed
|
|
7
|
-
#
|
|
8
|
-
# Project settings (env section)
|
|
9
|
-
# PROJECT_NAME : project name
|
|
10
|
-
# NODE_VERSION : Node.js version (default: 20.15.0)
|
|
11
|
-
# ENV_FILE : environment file name (default: .env)
|
|
12
|
-
|
|
13
|
-
name: PROJECT-NEXT-CI
|
|
14
|
-
|
|
15
|
-
on:
|
|
16
|
-
push:
|
|
17
|
-
branches: ["{{DEVELOP_BRANCH}}"]
|
|
18
|
-
pull_request:
|
|
19
|
-
branches: ["{{DEVELOP_BRANCH}}"]
|
|
20
|
-
workflow_dispatch:
|
|
21
|
-
|
|
22
|
-
env:
|
|
23
|
-
PROJECT_NAME: "__PROJECT_NAME__" # @wizard ask:@repo
|
|
24
|
-
NODE_VERSION: "20.15.0"
|
|
25
|
-
ENV_FILE: ".env"
|
|
26
|
-
# Subdirectory of this project in a monorepo ('.' means always verify)
|
|
27
|
-
PROJECT_PATH: "." # @wizard auto:project-path
|
|
28
|
-
|
|
29
|
-
permissions:
|
|
30
|
-
contents: read
|
|
31
|
-
pull-requests: write
|
|
32
|
-
issues: write
|
|
33
|
-
|
|
34
|
-
jobs:
|
|
35
|
-
# Job: change detection (in a monorepo, checks whether this project's path changed)
|
|
36
|
-
changes:
|
|
37
|
-
name: Detect changes
|
|
38
|
-
runs-on: ubuntu-latest
|
|
39
|
-
permissions:
|
|
40
|
-
contents: read
|
|
41
|
-
pull-requests: read
|
|
42
|
-
outputs:
|
|
43
|
-
project: ${{ steps.filter.outputs.project }}
|
|
44
|
-
steps:
|
|
45
|
-
- name: Checkout repository
|
|
46
|
-
uses: actions/checkout@v7
|
|
47
|
-
|
|
48
|
-
- name: Check whether the project path changed
|
|
49
|
-
id: filter
|
|
50
|
-
if: ${{ github.event_name != 'workflow_dispatch' }}
|
|
51
|
-
uses: dorny/paths-filter@v4
|
|
52
|
-
with:
|
|
53
|
-
# A push only looks at this push's range (against the previous commit, 'before'). Left empty it would be the cumulative diff against the default branch.
|
|
54
|
-
# The first push of a new branch (before is 0) and PRs get an empty value -> default behavior (against the default branch / PR base)
|
|
55
|
-
base: ${{ github.event_name == 'push' && github.event.before != '0000000000000000000000000000000000000000' && github.event.before || '' }}
|
|
56
|
-
filters: |
|
|
57
|
-
project:
|
|
58
|
-
- '${{ env.PROJECT_PATH == '.' && '**' || format('{0}/**', env.PROJECT_PATH) }}'
|
|
59
|
-
|
|
60
|
-
build:
|
|
61
|
-
name: Build Next.js application
|
|
62
|
-
runs-on: ubuntu-latest
|
|
63
|
-
needs: changes
|
|
64
|
-
if: ${{ github.event_name == 'workflow_dispatch' || needs.changes.outputs.project == 'true' }}
|
|
65
|
-
# In a monorepo (--paths), build inside that subfolder
|
|
66
|
-
defaults:
|
|
67
|
-
run:
|
|
68
|
-
working-directory: ${{ env.PROJECT_PATH }}
|
|
69
|
-
|
|
70
|
-
steps:
|
|
71
|
-
- name: Checkout repository
|
|
72
|
-
uses: actions/checkout@v7
|
|
73
|
-
|
|
74
|
-
- name: Create environment file (${{ env.ENV_FILE }})
|
|
75
|
-
run: |
|
|
76
|
-
m() { (cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py get "$@"); }
|
|
77
|
-
echo -e "${{ secrets.ENV_FILE || secrets.ENV }}" > ${{ env.ENV_FILE }}
|
|
78
|
-
m cicd.env_file_created file="${{ env.ENV_FILE }}"
|
|
79
|
-
|
|
80
|
-
- name: Set up Node.js
|
|
81
|
-
uses: actions/setup-node@v7
|
|
82
|
-
with:
|
|
83
|
-
node-version: ${{ env.NODE_VERSION }}
|
|
84
|
-
cache: "npm"
|
|
85
|
-
cache-dependency-path: ${{ env.PROJECT_PATH }}/package-lock.json
|
|
86
|
-
|
|
87
|
-
- name: Cache npm dependencies
|
|
88
|
-
uses: actions/cache@v6
|
|
89
|
-
with:
|
|
90
|
-
path: ~/.npm
|
|
91
|
-
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
|
92
|
-
restore-keys: |
|
|
93
|
-
${{ runner.os }}-npm-
|
|
94
|
-
|
|
95
|
-
- name: Cache node_modules
|
|
96
|
-
id: node-modules-cache
|
|
97
|
-
uses: actions/cache@v6
|
|
98
|
-
with:
|
|
99
|
-
path: "**/node_modules"
|
|
100
|
-
key: ${{ runner.os }}-node-modules-${{ hashFiles('**/package-lock.json') }}
|
|
101
|
-
restore-keys: |
|
|
102
|
-
${{ runner.os }}-node-modules-
|
|
103
|
-
|
|
104
|
-
- name: Install dependencies
|
|
105
|
-
if: steps.node-modules-cache.outputs.cache-hit != 'true'
|
|
106
|
-
run: |
|
|
107
|
-
m() { (cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py get "$@"); }
|
|
108
|
-
m cibuild.node_deps_installing
|
|
109
|
-
npm ci
|
|
110
|
-
m cibuild.node_deps_done
|
|
111
|
-
|
|
112
|
-
# Cache the Next.js build (.next/cache)
|
|
113
|
-
# Caches the .next/cache folder for incremental builds
|
|
114
|
-
- name: Cache Next.js build
|
|
115
|
-
uses: actions/cache@v6
|
|
116
|
-
with:
|
|
117
|
-
path: ${{ env.PROJECT_PATH }}/.next/cache
|
|
118
|
-
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
|
|
119
|
-
restore-keys: |
|
|
120
|
-
${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
|
|
121
|
-
${{ runner.os }}-nextjs-
|
|
122
|
-
|
|
123
|
-
# Runs only when package.json has a test script (the npm init default "no test specified" counts as none)
|
|
124
|
-
- name: Run tests
|
|
125
|
-
run: |
|
|
126
|
-
m() { (cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py get "$@"); }
|
|
127
|
-
TEST_SCRIPT=$(node -p "(require('./package.json').scripts || {}).test || ''")
|
|
128
|
-
if [ -z "$TEST_SCRIPT" ] || echo "$TEST_SCRIPT" | grep -q "no test specified"; then
|
|
129
|
-
m cibuild.node_no_test_script
|
|
130
|
-
exit 0
|
|
131
|
-
fi
|
|
132
|
-
m cibuild.node_test_run script="$TEST_SCRIPT"
|
|
133
|
-
# CI=true: run watch-mode test runners (react-scripts, etc.) only once
|
|
134
|
-
CI=true npm test
|
|
135
|
-
|
|
136
|
-
- name: Build project
|
|
137
|
-
id: build
|
|
138
|
-
run: |
|
|
139
|
-
m() { (cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py get "$@"); }
|
|
140
|
-
m cibuild.node_build_start_next
|
|
141
|
-
m cibuild.node_build_project project="${{ env.PROJECT_NAME }}"
|
|
142
|
-
m cibuild.node_build_node node="${{ env.NODE_VERSION }}"
|
|
143
|
-
echo ""
|
|
144
|
-
|
|
145
|
-
# Run the build and save the log
|
|
146
|
-
npm run build > build_log.txt 2>&1 || {
|
|
147
|
-
echo "build_failed=true" >> $GITHUB_OUTPUT
|
|
148
|
-
echo "status=failed" >> $GITHUB_OUTPUT
|
|
149
|
-
echo ""
|
|
150
|
-
m cibuild.node_build_failed
|
|
151
|
-
exit 0
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
echo "build_failed=false" >> $GITHUB_OUTPUT
|
|
155
|
-
echo "status=success" >> $GITHUB_OUTPUT
|
|
156
|
-
m cibuild.node_build_ok
|
|
157
|
-
|
|
158
|
-
# Check the build output
|
|
159
|
-
if [ -d ".next" ]; then
|
|
160
|
-
echo ""
|
|
161
|
-
m cibuild.node_build_output
|
|
162
|
-
m cibuild.node_build_next_dir
|
|
163
|
-
du -sh .next/ || true
|
|
164
|
-
fi
|
|
165
|
-
|
|
166
|
-
- name: Print build error log
|
|
167
|
-
if: steps.build.outputs.build_failed == 'true'
|
|
168
|
-
run: |
|
|
169
|
-
m() { (cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py get "$@"); }
|
|
170
|
-
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
171
|
-
m cibuild.node_errlog_title
|
|
172
|
-
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
173
|
-
echo ""
|
|
174
|
-
|
|
175
|
-
if [ -f "build_log.txt" ]; then
|
|
176
|
-
tail -n 50 build_log.txt
|
|
177
|
-
else
|
|
178
|
-
m cibuild.node_errlog_missing
|
|
179
|
-
fi
|
|
180
|
-
|
|
181
|
-
echo ""
|
|
182
|
-
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
183
|
-
exit 1
|
|
184
|
-
|
|
185
|
-
- name: Upload build log
|
|
186
|
-
if: always()
|
|
187
|
-
uses: actions/upload-artifact@v7
|
|
188
|
-
with:
|
|
189
|
-
name: build-log
|
|
190
|
-
path: ${{ env.PROJECT_PATH }}/build_log.txt
|
|
191
|
-
retention-days: 7
|
|
192
|
-
|
|
193
|
-
- name: Build result summary
|
|
194
|
-
if: always()
|
|
195
|
-
run: |
|
|
196
|
-
m() { (cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py get "$@"); }
|
|
197
|
-
echo ""
|
|
198
|
-
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
199
|
-
if [ "${{ steps.build.outputs.status }}" = "success" ]; then
|
|
200
|
-
m cibuild.node_result_next_ok
|
|
201
|
-
else
|
|
202
|
-
m cibuild.node_result_next_fail
|
|
203
|
-
fi
|
|
204
|
-
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
205
|
-
echo ""
|
|
206
|
-
m cibuild.node_result_title
|
|
207
|
-
m cibuild.node_result_project project="${{ env.PROJECT_NAME }}"
|
|
208
|
-
m cibuild.node_result_node node="${{ env.NODE_VERSION }}"
|
|
209
|
-
m cibuild.node_result_status status="${{ steps.build.outputs.status }}"
|
|
210
|
-
m cibuild.node_result_branch branch="${{ github.ref_name }}"
|
|
211
|
-
m cibuild.node_result_commit sha="${{ github.sha }}"
|
|
212
|
-
m cibuild.node_result_time time="$(date '+%Y-%m-%d %H:%M:%S UTC')"
|
|
213
|
-
echo ""
|
|
214
|
-
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
215
|
-
|
|
216
|
-
# Job: CI Gate (the single job to register as a required check)
|
|
217
|
-
ci-gate:
|
|
218
|
-
name: CI Gate
|
|
219
|
-
if: ${{ always() }}
|
|
220
|
-
needs: [changes, build]
|
|
221
|
-
runs-on: ubuntu-latest
|
|
222
|
-
steps:
|
|
223
|
-
# Only the message catalog is needed here, so check out just that
|
|
224
|
-
- name: Checkout message catalog
|
|
225
|
-
uses: actions/checkout@v7
|
|
226
|
-
with:
|
|
227
|
-
sparse-checkout: |
|
|
228
|
-
.github/scripts/messages.py
|
|
229
|
-
version.yml
|
|
230
|
-
sparse-checkout-cone-mode: false
|
|
231
|
-
|
|
232
|
-
- name: Aggregate results
|
|
233
|
-
env:
|
|
234
|
-
RESULTS: ${{ toJSON(needs.*.result) }}
|
|
235
|
-
run: |
|
|
236
|
-
m() { (cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py get "$@"); }
|
|
237
|
-
m cibuild.gate_results results="$RESULTS"
|
|
238
|
-
if echo "$RESULTS" | grep -Eq '"(failure|cancelled)"'; then
|
|
239
|
-
m cibuild.gate_failed
|
|
240
|
-
exit 1
|
|
241
|
-
fi
|
|
@@ -1,330 +0,0 @@
|
|
|
1
|
-
# project-auto-wizard:managed-workflow
|
|
2
|
-
# Next.js CI/CD deployment
|
|
3
|
-
# Builds a Docker image and deploys it when the {{MAIN_BRANCH}} branch is pushed. Deployed as a Node.js runtime container to support SSR.
|
|
4
|
-
#
|
|
5
|
-
# GitHub Secrets
|
|
6
|
-
# ENV_FILE: contents of the .env file
|
|
7
|
-
# DOCKERHUB_USERNAME: DockerHub username
|
|
8
|
-
# DOCKERHUB_TOKEN: DockerHub access token
|
|
9
|
-
# SERVER_HOST: deploy server address
|
|
10
|
-
# SERVER_USER: SSH username
|
|
11
|
-
# SERVER_PASSWORD: SSH password (when SSH_AUTH_METHOD=password)
|
|
12
|
-
# SSH_KEY: contents of the SSH private key .pem (when SSH_AUTH_METHOD=key, e.g. AWS EC2)
|
|
13
|
-
# PROJECT_DEPLOY_PORT (optional): deploy port (default: 3000)
|
|
14
|
-
#
|
|
15
|
-
# Environment variables (set in the env section)
|
|
16
|
-
# SSH_AUTH_METHOD: SSH auth method password | key (default: password)
|
|
17
|
-
# SSH_PORT (default: 2022), SSH_COMMAND_TIMEOUT (default: 600s), SSH_CONNECTION_TIMEOUT (default: 60s)
|
|
18
|
-
|
|
19
|
-
name: PROJECT-NEXT-CICD
|
|
20
|
-
|
|
21
|
-
on:
|
|
22
|
-
push:
|
|
23
|
-
branches:
|
|
24
|
-
- "{{MAIN_BRANCH}}"
|
|
25
|
-
# @wizard paths-anchor (for a monorepo the integrator injects the paths filter here)
|
|
26
|
-
workflow_dispatch:
|
|
27
|
-
|
|
28
|
-
env:
|
|
29
|
-
PROJECT_NAME: "__PROJECT_NAME__" # @wizard ask:@repo
|
|
30
|
-
# Subdirectory of this project in a monorepo (--paths). Base for the build and Docker context ('.' means the repo root)
|
|
31
|
-
PROJECT_PATH: "." # @wizard auto:project-path
|
|
32
|
-
DOCKER_IMAGE_PREFIX: "nextjs-container"
|
|
33
|
-
NODE_VERSION: "20.15.0"
|
|
34
|
-
ENV_FILE_NAME: ".env"
|
|
35
|
-
|
|
36
|
-
# SSH auth method: "password" (Synology, generic servers) | "key" (AWS EC2, .pem)
|
|
37
|
-
# password -> uses the SERVER_PASSWORD secret
|
|
38
|
-
# key -> uses the SSH_KEY secret (the whole .pem file content as the secret value)
|
|
39
|
-
SSH_AUTH_METHOD: "password" # @wizard ask:password
|
|
40
|
-
|
|
41
|
-
SSH_PORT: "__SSH_PORT__" # @wizard ask:2022
|
|
42
|
-
SSH_COMMAND_TIMEOUT: "600s" # SSH command execution timeout
|
|
43
|
-
SSH_CONNECTION_TIMEOUT: "60s" # SSH connection timeout
|
|
44
|
-
|
|
45
|
-
jobs:
|
|
46
|
-
build:
|
|
47
|
-
name: Build Next.js application
|
|
48
|
-
runs-on: ubuntu-latest
|
|
49
|
-
# In a monorepo (--paths), build inside that subfolder
|
|
50
|
-
defaults:
|
|
51
|
-
run:
|
|
52
|
-
working-directory: ${{ env.PROJECT_PATH }}
|
|
53
|
-
|
|
54
|
-
steps:
|
|
55
|
-
- name: Checkout repository
|
|
56
|
-
uses: actions/checkout@v7
|
|
57
|
-
|
|
58
|
-
# If a required Secret or the Dockerfile is missing, report the missing item and stop
|
|
59
|
-
# instead of failing later with a third-party error such as docker login
|
|
60
|
-
- name: Deploy pre-check (Secrets and Dockerfile)
|
|
61
|
-
working-directory: ${{ env.PROJECT_PATH }}
|
|
62
|
-
env:
|
|
63
|
-
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
64
|
-
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
65
|
-
SERVER_HOST: ${{ secrets.SERVER_HOST }}
|
|
66
|
-
SERVER_USER: ${{ secrets.SERVER_USER }}
|
|
67
|
-
SERVER_PASSWORD: ${{ secrets.SERVER_PASSWORD }}
|
|
68
|
-
SSH_KEY: ${{ secrets.SSH_KEY }}
|
|
69
|
-
DOCKERFILE: Dockerfile
|
|
70
|
-
run: |
|
|
71
|
-
m() { (cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py get "$@"); }
|
|
72
|
-
MISSING=""
|
|
73
|
-
for NAME in DOCKERHUB_USERNAME DOCKERHUB_TOKEN SERVER_HOST SERVER_USER; do
|
|
74
|
-
[ -n "${!NAME}" ] || MISSING="$MISSING $NAME"
|
|
75
|
-
done
|
|
76
|
-
# SSH_AUTH_METHOD=key needs SSH_KEY, otherwise SERVER_PASSWORD is required
|
|
77
|
-
if [ "${SSH_AUTH_METHOD:-password}" = "key" ]; then
|
|
78
|
-
[ -n "$SSH_KEY" ] || MISSING="$MISSING SSH_KEY"
|
|
79
|
-
else
|
|
80
|
-
[ -n "$SERVER_PASSWORD" ] || MISSING="$MISSING SERVER_PASSWORD"
|
|
81
|
-
fi
|
|
82
|
-
FAILED=false
|
|
83
|
-
if [ -n "$MISSING" ]; then
|
|
84
|
-
echo "::error title=$(m cicd.precheck_secret_title)::$(m cicd.precheck_secret_body missing="$MISSING")"
|
|
85
|
-
FAILED=true
|
|
86
|
-
fi
|
|
87
|
-
if [ ! -f "$DOCKERFILE" ]; then
|
|
88
|
-
echo "::error title=$(m cicd.precheck_dockerfile_title)::$(m cicd.precheck_dockerfile_body path="${PROJECT_PATH}/${DOCKERFILE#./}")"
|
|
89
|
-
FAILED=true
|
|
90
|
-
fi
|
|
91
|
-
if [ "$FAILED" = "true" ]; then
|
|
92
|
-
exit 1
|
|
93
|
-
fi
|
|
94
|
-
m cicd.precheck_ok
|
|
95
|
-
|
|
96
|
-
- name: Set up Node.js
|
|
97
|
-
uses: actions/setup-node@v7
|
|
98
|
-
with:
|
|
99
|
-
node-version: ${{ env.NODE_VERSION }}
|
|
100
|
-
cache: 'npm'
|
|
101
|
-
cache-dependency-path: ${{ env.PROJECT_PATH }}/package-lock.json
|
|
102
|
-
|
|
103
|
-
- name: Create environment file (${{ env.ENV_FILE_NAME }})
|
|
104
|
-
run: |
|
|
105
|
-
m() { (cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py get "$@"); }
|
|
106
|
-
cat << 'EOF' > ./${{ env.ENV_FILE_NAME }}
|
|
107
|
-
${{ secrets.ENV_FILE || secrets.ENV }}
|
|
108
|
-
EOF
|
|
109
|
-
m cicd.env_file_created file="${{ env.ENV_FILE_NAME }}"
|
|
110
|
-
|
|
111
|
-
- name: Cache npm dependencies
|
|
112
|
-
uses: actions/cache@v6
|
|
113
|
-
with:
|
|
114
|
-
path: ~/.npm
|
|
115
|
-
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
|
116
|
-
restore-keys: |
|
|
117
|
-
${{ runner.os }}-npm-
|
|
118
|
-
|
|
119
|
-
- name: Cache node_modules
|
|
120
|
-
id: node-modules-cache
|
|
121
|
-
uses: actions/cache@v6
|
|
122
|
-
with:
|
|
123
|
-
path: "**/node_modules"
|
|
124
|
-
key: ${{ runner.os }}-node-modules-${{ hashFiles('**/package-lock.json') }}
|
|
125
|
-
restore-keys: |
|
|
126
|
-
${{ runner.os }}-node-modules-
|
|
127
|
-
|
|
128
|
-
- name: Install dependencies
|
|
129
|
-
if: steps.node-modules-cache.outputs.cache-hit != 'true'
|
|
130
|
-
run: npm ci
|
|
131
|
-
|
|
132
|
-
- name: Cache Next.js build
|
|
133
|
-
uses: actions/cache@v6
|
|
134
|
-
with:
|
|
135
|
-
path: ${{ env.PROJECT_PATH }}/.next/cache
|
|
136
|
-
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
|
|
137
|
-
restore-keys: |
|
|
138
|
-
${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
|
|
139
|
-
${{ runner.os }}-nextjs-
|
|
140
|
-
|
|
141
|
-
- name: Build with npm
|
|
142
|
-
run: npm run build
|
|
143
|
-
|
|
144
|
-
- name: Set up Docker Buildx
|
|
145
|
-
uses: docker/setup-buildx-action@v3
|
|
146
|
-
|
|
147
|
-
- name: Log in to DockerHub
|
|
148
|
-
uses: docker/login-action@v3
|
|
149
|
-
with:
|
|
150
|
-
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
151
|
-
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
152
|
-
|
|
153
|
-
- name: Cache Docker layers
|
|
154
|
-
uses: actions/cache@v6
|
|
155
|
-
with:
|
|
156
|
-
path: /tmp/.buildx-cache
|
|
157
|
-
key: ${{ runner.os }}-buildx-${{ hashFiles('Dockerfile') }}-${{ hashFiles('package-lock.json') }}
|
|
158
|
-
restore-keys: |
|
|
159
|
-
${{ runner.os }}-buildx-
|
|
160
|
-
|
|
161
|
-
- name: Build and push Docker image
|
|
162
|
-
uses: docker/build-push-action@v5
|
|
163
|
-
with:
|
|
164
|
-
context: ${{ env.PROJECT_PATH }}
|
|
165
|
-
file: ${{ env.PROJECT_PATH }}/Dockerfile
|
|
166
|
-
push: true
|
|
167
|
-
tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.PROJECT_NAME }}-${{ env.DOCKER_IMAGE_PREFIX }}:${{ github.ref_name }}
|
|
168
|
-
cache-from: type=local,src=/tmp/.buildx-cache
|
|
169
|
-
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
|
170
|
-
|
|
171
|
-
- name: Move Docker cache
|
|
172
|
-
run: |
|
|
173
|
-
rm -rf /tmp/.buildx-cache
|
|
174
|
-
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
|
175
|
-
|
|
176
|
-
deploy:
|
|
177
|
-
name: Deploy to remote server
|
|
178
|
-
needs: build
|
|
179
|
-
runs-on: ubuntu-latest
|
|
180
|
-
|
|
181
|
-
steps:
|
|
182
|
-
# The deploy script runs on the remote server, so the messages are resolved here
|
|
183
|
-
# and handed to it through env vars (PAW_M_*)
|
|
184
|
-
- name: Checkout message catalog
|
|
185
|
-
uses: actions/checkout@v7
|
|
186
|
-
with:
|
|
187
|
-
sparse-checkout: |
|
|
188
|
-
.github/scripts/messages.py
|
|
189
|
-
version.yml
|
|
190
|
-
sparse-checkout-cone-mode: false
|
|
191
|
-
|
|
192
|
-
- name: Load messages
|
|
193
|
-
run: |
|
|
194
|
-
python3 .github/scripts/messages.py dump cicd. | PYTHONIOENCODING=utf-8 python3 -c 'import json,sys; [print("PAW_M_" + k.replace(".", "_") + "=" + v) for k, v in json.load(sys.stdin).items()]' >> "$GITHUB_ENV"
|
|
195
|
-
|
|
196
|
-
- name: Deploy
|
|
197
|
-
uses: appleboy/ssh-action@v1.0.3
|
|
198
|
-
env:
|
|
199
|
-
SSH_AUTH_METHOD: ${{ env.SSH_AUTH_METHOD }}
|
|
200
|
-
with:
|
|
201
|
-
host: ${{ secrets.SERVER_HOST }}
|
|
202
|
-
username: ${{ secrets.SERVER_USER }}
|
|
203
|
-
password: ${{ secrets.SERVER_PASSWORD }}
|
|
204
|
-
key: ${{ secrets.SSH_KEY }}
|
|
205
|
-
port: ${{ env.SSH_PORT }}
|
|
206
|
-
command_timeout: ${{ env.SSH_COMMAND_TIMEOUT }}
|
|
207
|
-
timeout: ${{ env.SSH_CONNECTION_TIMEOUT }}
|
|
208
|
-
envs: SSH_AUTH_METHOD
|
|
209
|
-
script: |
|
|
210
|
-
set -e
|
|
211
|
-
|
|
212
|
-
# Fill the {name} placeholders of a message template (usage: m "template" name=value ...)
|
|
213
|
-
m() { local s="$1" kv; shift; for kv in "$@"; do s="${s//\{${kv%%=*}\}/${kv#*=}}"; done; printf '%s\n' "$s"; }
|
|
214
|
-
|
|
215
|
-
# Set deploy environment variables
|
|
216
|
-
m "${{ env.PAW_M_cicd_setup_env }}"
|
|
217
|
-
export PATH=$PATH:/usr/local/bin
|
|
218
|
-
export PW=${{ secrets.SERVER_PASSWORD }}
|
|
219
|
-
|
|
220
|
-
# sudo abstraction per SSH auth method
|
|
221
|
-
# password mode: echo $PW | sudo -S (servers that need a sudo password, e.g. Synology)
|
|
222
|
-
# key mode: plain sudo (passwordless-sudo servers, e.g. AWS EC2)
|
|
223
|
-
SSH_AUTH_METHOD="${SSH_AUTH_METHOD:-password}"
|
|
224
|
-
if [ "${SSH_AUTH_METHOD}" = "key" ]; then
|
|
225
|
-
SUDO() { sudo "$@"; }
|
|
226
|
-
else
|
|
227
|
-
SUDO() { echo "$PW" | sudo -S "$@"; }
|
|
228
|
-
fi
|
|
229
|
-
m "${{ env.PAW_M_cicd_ssh_auth }}" method="${SSH_AUTH_METHOD}"
|
|
230
|
-
|
|
231
|
-
BRANCH=${{ github.ref_name }}
|
|
232
|
-
PROJECT_NAME="${{ env.PROJECT_NAME }}"
|
|
233
|
-
|
|
234
|
-
# Per-branch port and container name
|
|
235
|
-
PORT=3000
|
|
236
|
-
CONTAINER_NAME="${PROJECT_NAME}-nextjs"
|
|
237
|
-
|
|
238
|
-
if [ "$BRANCH" == "{{MAIN_BRANCH}}" ]; then
|
|
239
|
-
PORT=${{ secrets.PROJECT_DEPLOY_PORT || '3000' }}
|
|
240
|
-
CONTAINER_NAME="${PROJECT_NAME}-nextjs-deploy"
|
|
241
|
-
m "${{ env.PAW_M_cicd_deploy_start }}" branch="$BRANCH"
|
|
242
|
-
else
|
|
243
|
-
m "${{ env.PAW_M_cicd_branch_unsupported }}" branch="$BRANCH"
|
|
244
|
-
exit 1
|
|
245
|
-
fi
|
|
246
|
-
|
|
247
|
-
m "${{ env.PAW_M_cicd_config_title }}"
|
|
248
|
-
m "${{ env.PAW_M_cicd_config_branch }}" branch="$BRANCH"
|
|
249
|
-
m "${{ env.PAW_M_cicd_config_project }}" project="$PROJECT_NAME"
|
|
250
|
-
m "${{ env.PAW_M_cicd_config_container }}" container="$CONTAINER_NAME"
|
|
251
|
-
m "${{ env.PAW_M_cicd_config_port }}" port="$PORT"
|
|
252
|
-
m "${{ env.PAW_M_cicd_config_image }}" image="${{ secrets.DOCKERHUB_USERNAME }}/${PROJECT_NAME}-${{ env.DOCKER_IMAGE_PREFIX }}:${BRANCH}"
|
|
253
|
-
|
|
254
|
-
# Pull the Docker image
|
|
255
|
-
m "${{ env.PAW_M_cicd_pull_image_short }}"
|
|
256
|
-
SUDO docker pull ${{ secrets.DOCKERHUB_USERNAME }}/${PROJECT_NAME}-${{ env.DOCKER_IMAGE_PREFIX }}:${BRANCH}
|
|
257
|
-
|
|
258
|
-
# Clean up the existing container
|
|
259
|
-
m "${{ env.PAW_M_cicd_container_check }}" container="$CONTAINER_NAME"
|
|
260
|
-
|
|
261
|
-
if SUDO docker ps -a --format '{{.Names}}' | grep -Eq "^${CONTAINER_NAME}\$"; then
|
|
262
|
-
m "${{ env.PAW_M_cicd_container_exists }}" container="$CONTAINER_NAME"
|
|
263
|
-
SUDO docker rm -f $CONTAINER_NAME
|
|
264
|
-
m "${{ env.PAW_M_cicd_container_removed }}" container="$CONTAINER_NAME"
|
|
265
|
-
else
|
|
266
|
-
m "${{ env.PAW_M_cicd_container_none }}" container="$CONTAINER_NAME"
|
|
267
|
-
fi
|
|
268
|
-
|
|
269
|
-
# Start the new container
|
|
270
|
-
m "${{ env.PAW_M_cicd_container_run }}" container="$CONTAINER_NAME"
|
|
271
|
-
|
|
272
|
-
# Next.js needs a Node.js server for SSR
|
|
273
|
-
# With standalone mode, run it with node server.js
|
|
274
|
-
SUDO docker run -d \
|
|
275
|
-
-p ${PORT}:3000 \
|
|
276
|
-
--name $CONTAINER_NAME \
|
|
277
|
-
-e TZ=Asia/Seoul \
|
|
278
|
-
-e NODE_ENV=production \
|
|
279
|
-
-v /etc/localtime:/etc/localtime:ro \
|
|
280
|
-
--restart unless-stopped \
|
|
281
|
-
${{ secrets.DOCKERHUB_USERNAME }}/${PROJECT_NAME}-${{ env.DOCKER_IMAGE_PREFIX }}:${BRANCH}
|
|
282
|
-
|
|
283
|
-
# Confirm the deployment
|
|
284
|
-
m "${{ env.PAW_M_cicd_deploy_done }}"
|
|
285
|
-
echo ""
|
|
286
|
-
m "${{ env.PAW_M_cicd_summary_title }}"
|
|
287
|
-
m "${{ env.PAW_M_cicd_summary_project }}" project="$PROJECT_NAME"
|
|
288
|
-
m "${{ env.PAW_M_cicd_summary_branch }}" branch="$BRANCH"
|
|
289
|
-
m "${{ env.PAW_M_cicd_summary_container }}" container="$CONTAINER_NAME"
|
|
290
|
-
m "${{ env.PAW_M_cicd_summary_port }}" port="$PORT"
|
|
291
|
-
m "${{ env.PAW_M_cicd_summary_time }}" time="$(date '+%Y-%m-%d %H:%M:%S')"
|
|
292
|
-
echo ""
|
|
293
|
-
m "${{ env.PAW_M_cicd_summary_url }}" url="http://${{ secrets.SERVER_HOST }}:${PORT}"
|
|
294
|
-
|
|
295
|
-
# Next.js Dockerfile example (standalone mode)
|
|
296
|
-
#
|
|
297
|
-
# # Stage 1: Dependencies
|
|
298
|
-
# FROM node:20-alpine AS deps
|
|
299
|
-
# WORKDIR /app
|
|
300
|
-
# COPY package*.json ./
|
|
301
|
-
# RUN npm ci --only=production
|
|
302
|
-
#
|
|
303
|
-
# # Stage 2: Builder
|
|
304
|
-
# FROM node:20-alpine AS builder
|
|
305
|
-
# WORKDIR /app
|
|
306
|
-
# COPY --from=deps /app/node_modules ./node_modules
|
|
307
|
-
# COPY . .
|
|
308
|
-
# RUN npm run build
|
|
309
|
-
#
|
|
310
|
-
# # Stage 3: Runner
|
|
311
|
-
# FROM node:20-alpine AS runner
|
|
312
|
-
# WORKDIR /app
|
|
313
|
-
# ENV NODE_ENV production
|
|
314
|
-
#
|
|
315
|
-
# RUN addgroup --system --gid 1001 nodejs
|
|
316
|
-
# RUN adduser --system --uid 1001 nextjs
|
|
317
|
-
#
|
|
318
|
-
# COPY --from=builder /app/public ./public
|
|
319
|
-
# COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
|
320
|
-
# COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
|
321
|
-
#
|
|
322
|
-
# USER nextjs
|
|
323
|
-
# EXPOSE 3000
|
|
324
|
-
# ENV PORT 3000
|
|
325
|
-
# CMD ["node", "server.js"]
|
|
326
|
-
#
|
|
327
|
-
# Add the standalone setting to next.config.js:
|
|
328
|
-
# module.exports = {
|
|
329
|
-
# output: 'standalone',
|
|
330
|
-
# }
|