project-auto-wizard 0.13.3 → 0.14.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.md +1 -1
- package/bin/project-auto-wizard.js +18 -4
- package/package.json +2 -2
- package/payload/config/breaking-changes.json +28 -14
- package/payload/config/wizard-prompts.yml +102 -46
- package/payload/flutter-app/android/fastlane/Fastfile.playstore +44 -32
- package/payload/flutter-app/ios/ExportOptions.plist +8 -8
- package/payload/flutter-app/ios/fastlane/Fastfile +57 -43
- package/payload/scripts/changelog_manager.py +296 -257
- package/payload/scripts/issue_helper.py +54 -50
- package/payload/scripts/messages.py +2279 -0
- package/payload/scripts/truncate_release_notes.py +7 -6
- package/payload/scripts/version_manager.py +64 -61
- package/payload/version.yml.template +2 -1
- package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +17 -12
- package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +59 -34
- package/payload/workflows/common/PROJECT-COMMON-ISSUE-HELPER.yaml +17 -15
- package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +17 -10
- package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +54 -44
- package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +24 -17
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +194 -184
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +226 -213
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +46 -42
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +334 -241
- package/payload/workflows/flutter/PROJECT-FLUTTER-APP-BUILD-TRIGGER.yaml +152 -128
- package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +246 -188
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +353 -238
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +96 -85
- package/payload/workflows/go/PROJECT-GO-CI.yaml +47 -36
- package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +945 -508
- package/payload/workflows/go/PROJECT-GO-SIMPLE-CICD.yaml +155 -133
- package/payload/workflows/next/PROJECT-NEXT-CI.yaml +76 -60
- package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +97 -77
- package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +62 -47
- package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +942 -505
- package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +148 -126
- package/payload/workflows/react/PROJECT-REACT-CI.yaml +86 -70
- package/payload/workflows/react/PROJECT-REACT-CICD.yaml +112 -92
- package/payload/workflows/spring/PROJECT-SPRING-CI.yml +112 -82
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +180 -150
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +161 -131
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +970 -549
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +193 -162
- package/src/cli/args.js +98 -52
- package/src/cli/help.js +8 -41
- package/src/commands/doctor.js +85 -89
- package/src/commands/dry-run.js +63 -43
- package/src/commands/full.js +79 -77
- package/src/commands/install-settings.js +16 -15
- package/src/commands/interactive-flutter.js +20 -19
- package/src/commands/interactive.js +135 -132
- package/src/commands/purge.js +47 -42
- package/src/commands/status.js +36 -36
- package/src/commands/uninstall.js +43 -41
- package/src/context.js +12 -11
- package/src/core/assets.js +12 -11
- package/src/core/baseline.js +27 -27
- package/src/core/branches.js +29 -27
- package/src/core/branding.js +7 -6
- package/src/core/breaking-check.js +22 -21
- package/src/core/breaking.js +15 -8
- package/src/core/copy/app-files.js +5 -4
- package/src/core/copy/flutter-app.js +8 -8
- package/src/core/copy/gitignore.js +58 -50
- package/src/core/copy/readme.js +45 -35
- package/src/core/copy/simple.js +16 -15
- package/src/core/copy/workflows.js +131 -124
- package/src/core/deploy-style.js +51 -48
- package/src/core/detect-fs.js +57 -57
- package/src/core/detect.js +67 -65
- package/src/core/errors.js +2 -2
- package/src/core/flutter-doctor.js +17 -16
- package/src/core/flutter-hooks.js +33 -32
- package/src/core/flutter-options.js +34 -33
- package/src/core/fsutil.js +12 -12
- package/src/core/installed-stores.js +4 -4
- package/src/core/logger.js +42 -40
- package/src/core/paths-resolve.js +88 -86
- package/src/core/paths.js +10 -10
- package/src/core/release-options.js +8 -8
- package/src/core/removal-exec.js +14 -13
- package/src/core/removal-plan.js +37 -37
- package/src/core/types.js +47 -47
- package/src/core/verify.js +44 -38
- package/src/core/version-yml.js +76 -68
- package/src/core/wizard-env.js +46 -46
- package/src/core/wizard-labels.js +63 -41
- package/src/i18n/catalog/en/cli.js +50 -0
- package/src/i18n/catalog/en/commands.js +187 -0
- package/src/i18n/catalog/en/copy.js +19 -0
- package/src/i18n/catalog/en/core.js +74 -0
- package/src/i18n/catalog/en/core2.js +15 -0
- package/src/i18n/catalog/en/ui.js +221 -0
- package/src/i18n/catalog/en.js +12 -0
- package/src/i18n/catalog/index.js +5 -0
- package/src/i18n/catalog/ko/cli.js +50 -0
- package/src/i18n/catalog/ko/commands.js +187 -0
- package/src/i18n/catalog/ko/copy.js +19 -0
- package/src/i18n/catalog/ko/core.js +74 -0
- package/src/i18n/catalog/ko/core2.js +15 -0
- package/src/i18n/catalog/ko/ui.js +221 -0
- package/src/i18n/catalog/ko.js +10 -0
- package/src/i18n/index.js +44 -0
- package/src/i18n/languages.js +9 -0
- package/src/index.js +128 -94
- package/src/ui/ansi.js +7 -7
- package/src/ui/banner.js +7 -6
- package/src/ui/env-plan.js +82 -81
- package/src/ui/prompts.js +97 -122
- package/src/ui/readline-engine.js +59 -58
- package/src/ui/status-cards.js +37 -31
- package/src/ui/summary.js +89 -88
|
@@ -1,66 +1,66 @@
|
|
|
1
1
|
# project-auto-wizard:managed-workflow
|
|
2
|
-
# Spring Boot
|
|
2
|
+
# Spring Boot zero-downtime deploy (Nginx Blue-Green, SSH + Docker)
|
|
3
3
|
#
|
|
4
|
-
# deploy
|
|
5
|
-
#
|
|
4
|
+
# On a push to the deploy branch, builds a Docker image and
|
|
5
|
+
# deploys it Blue-Green with no downtime by toggling the proxy_pass port in the nginx config.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
7
|
+
# This workflow is the zero-downtime deploy option.
|
|
8
|
+
# The default deploy is PROJECT-SPRING-SIMPLE-CICD.yaml.
|
|
9
|
+
# To switch, uncomment push.branches below and comment out the push trigger in SIMPLE-CICD.
|
|
10
10
|
#
|
|
11
|
-
#
|
|
12
|
-
# APPLICATION_PROD_YML (
|
|
13
|
-
# DOCKERHUB_USERNAME: DockerHub
|
|
14
|
-
# DOCKERHUB_TOKEN: DockerHub
|
|
15
|
-
# SERVER_HOST:
|
|
16
|
-
# SERVER_USER: SSH
|
|
17
|
-
# SERVER_PASSWORD: SSH
|
|
18
|
-
# SSH_KEY: SSH
|
|
11
|
+
# Required GitHub Secrets
|
|
12
|
+
# APPLICATION_PROD_YML (optional): contents of application-prod.yml
|
|
13
|
+
# DOCKERHUB_USERNAME: DockerHub username
|
|
14
|
+
# DOCKERHUB_TOKEN: DockerHub access token
|
|
15
|
+
# SERVER_HOST: deployment server address
|
|
16
|
+
# SERVER_USER: SSH username
|
|
17
|
+
# SERVER_PASSWORD: SSH password (when SSH_AUTH_METHOD=password)
|
|
18
|
+
# SSH_KEY: contents of the SSH private key .pem (when SSH_AUTH_METHOD=key, e.g. AWS EC2)
|
|
19
19
|
#
|
|
20
|
-
#
|
|
20
|
+
# Environment variables (set in the env section)
|
|
21
21
|
#
|
|
22
|
-
#
|
|
23
|
-
# PROJECT_NAME:
|
|
24
|
-
# SERVICE_DOMAIN:
|
|
25
|
-
# CONTAINER_INTERNAL_PORT: Spring Boot
|
|
26
|
-
# BLUE_PORT:
|
|
27
|
-
# GREEN_PORT:
|
|
22
|
+
# Ports and containers:
|
|
23
|
+
# PROJECT_NAME: project name (container/image name)
|
|
24
|
+
# SERVICE_DOMAIN: external domain (must match the nginx server_name)
|
|
25
|
+
# CONTAINER_INTERNAL_PORT: Spring Boot internal port (default: 8080)
|
|
26
|
+
# BLUE_PORT: host port exposed by Blue (default: 8080)
|
|
27
|
+
# GREEN_PORT: host port exposed by Green (default: 8081)
|
|
28
28
|
#
|
|
29
29
|
# Nginx:
|
|
30
|
-
# NGINX_RP_CONF: nginx
|
|
31
|
-
# NGINX_BACKUP_DIR: nginx config
|
|
32
|
-
# NGINX_BACKUP_KEEP:
|
|
30
|
+
# NGINX_RP_CONF: path of the nginx reverse proxy config file
|
|
31
|
+
# NGINX_BACKUP_DIR: directory for nginx config backups
|
|
32
|
+
# NGINX_BACKUP_KEEP: number of backups to keep (default: 10)
|
|
33
33
|
#
|
|
34
|
-
#
|
|
35
|
-
# ENABLE_VOLUME_MOUNT:
|
|
36
|
-
# VOLUME_HOST_PATH:
|
|
37
|
-
# VOLUME_CONTAINER_PATH:
|
|
34
|
+
# Volume/storage:
|
|
35
|
+
# ENABLE_VOLUME_MOUNT: whether to mount a volume (true/false)
|
|
36
|
+
# VOLUME_HOST_PATH: host volume path
|
|
37
|
+
# VOLUME_CONTAINER_PATH: mount path inside the container
|
|
38
38
|
#
|
|
39
|
-
# SSH
|
|
40
|
-
# SSH_PORT: SSH
|
|
41
|
-
# SSH_COMMAND_TIMEOUT:
|
|
42
|
-
# SSH_CONNECTION_TIMEOUT:
|
|
39
|
+
# SSH connection:
|
|
40
|
+
# SSH_PORT: SSH port (default: 2022)
|
|
41
|
+
# SSH_COMMAND_TIMEOUT: command timeout (default: 600s)
|
|
42
|
+
# SSH_CONNECTION_TIMEOUT: connection timeout (default: 60s)
|
|
43
43
|
#
|
|
44
|
-
#
|
|
45
|
-
# HEALTHCHECK_PATH: HTTP
|
|
46
|
-
# HEALTHCHECK_MAX_RETRIES:
|
|
47
|
-
# HEALTHCHECK_RETRY_INTERVAL:
|
|
48
|
-
# DOMAIN_CHECK_RETRIES:
|
|
49
|
-
# DOMAIN_CHECK_INTERVAL:
|
|
50
|
-
# IN_FLIGHT_WAIT:
|
|
44
|
+
# Health check:
|
|
45
|
+
# HEALTHCHECK_PATH: HTTP path (default: /actuator/health, fallback: /)
|
|
46
|
+
# HEALTHCHECK_MAX_RETRIES: max container health check attempts (default: 120)
|
|
47
|
+
# HEALTHCHECK_RETRY_INTERVAL: retry interval in seconds (default: 1)
|
|
48
|
+
# DOMAIN_CHECK_RETRIES: domain check attempts after the nginx reload (default: 10)
|
|
49
|
+
# DOMAIN_CHECK_INTERVAL: domain check interval in seconds (default: 2)
|
|
50
|
+
# IN_FLIGHT_WAIT: seconds to wait before removing the old container (default: 5)
|
|
51
51
|
#
|
|
52
|
-
#
|
|
53
|
-
# -
|
|
54
|
-
# -
|
|
52
|
+
# Prerequisites:
|
|
53
|
+
# - nginx is installed on the server and a sites-enabled config exists
|
|
54
|
+
# - the server block of that config contains these lines:
|
|
55
55
|
# server_name ${SERVICE_DOMAIN};
|
|
56
|
-
# proxy_pass http://127.0.0.1:${BLUE_PORT}; #
|
|
57
|
-
# - SSH
|
|
56
|
+
# proxy_pass http://127.0.0.1:${BLUE_PORT}; # or ${GREEN_PORT}
|
|
57
|
+
# - the SSH user can run nginx -t / reload and docker commands via sudo
|
|
58
58
|
|
|
59
59
|
name: PROJECT-SPRING-NONSTOP-NGINX-CICD
|
|
60
60
|
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
61
|
+
# Trigger settings
|
|
62
|
+
# The automatic trigger on a push to the deploy branch is commented out.
|
|
63
|
+
# To switch to this workflow, uncomment push.branches below.
|
|
64
64
|
on:
|
|
65
65
|
# push:
|
|
66
66
|
# branches:
|
|
@@ -68,51 +68,51 @@ on:
|
|
|
68
68
|
workflow_dispatch:
|
|
69
69
|
|
|
70
70
|
env:
|
|
71
|
-
#
|
|
71
|
+
# Project settings - change to your actual project name
|
|
72
72
|
PROJECT_NAME: "__PROJECT_NAME__" # @wizard ask:@repo
|
|
73
|
-
#
|
|
73
|
+
# Subdirectory of this project in a monorepo (--paths). Base for the build and Docker context ('.' = repo root)
|
|
74
74
|
PROJECT_PATH: "." # @wizard auto:project-path
|
|
75
|
-
#
|
|
76
|
-
#
|
|
75
|
+
# In a monorepo (PROJECT_PATH is not '.') '-spring' is appended to image, container and domain names
|
|
76
|
+
# so they do not collide with services of other types in the same repo (e.g. my-app-spring)
|
|
77
77
|
SERVICE_DOMAIN: "__SERVICE_DOMAIN__" # @wizard ask:example.com
|
|
78
78
|
|
|
79
|
-
# Spring Boot
|
|
79
|
+
# Spring Boot settings
|
|
80
80
|
SPRING_PROFILE: "prod"
|
|
81
81
|
JAVA_VERSION: "__JAVA_VERSION__" # @wizard ask:@jdk
|
|
82
82
|
GRADLE_OPTS: "-Dspring.profiles.active=prod"
|
|
83
83
|
|
|
84
|
-
#
|
|
84
|
+
# Path settings
|
|
85
85
|
APPLICATION_YML_DIR: "__APPLICATION_YML_DIR__" # @wizard auto:spring-app-yml-dir
|
|
86
|
-
DOCKERFILE_PATH: "./Dockerfile" #
|
|
86
|
+
DOCKERFILE_PATH: "./Dockerfile" # relative to PROJECT_PATH
|
|
87
87
|
|
|
88
|
-
#
|
|
89
|
-
# BLUE/GREEN
|
|
88
|
+
# Port and container settings
|
|
89
|
+
# BLUE/GREEN are the host ports of the two containers that alternate on the server - the deploy fails if they clash with another service.
|
|
90
90
|
CONTAINER_INTERNAL_PORT: "8080"
|
|
91
91
|
BLUE_PORT: "__BLUE_PORT__" # @wizard ask:8080
|
|
92
92
|
GREEN_PORT: "__GREEN_PORT__" # @wizard ask:8081
|
|
93
93
|
|
|
94
|
-
# Nginx
|
|
95
|
-
# NGINX_RP_CONF
|
|
94
|
+
# Nginx settings
|
|
95
|
+
# If the NGINX_RP_CONF path is wrong, the zero-downtime switch (proxy_pass port toggle) cannot work at all.
|
|
96
96
|
NGINX_RP_CONF: "__NGINX_RP_CONF__" # @wizard ask:/etc/nginx/sites-enabled/__PROJECT_NAME__.conf
|
|
97
97
|
NGINX_BACKUP_DIR: "__NGINX_BACKUP_DIR__" # @wizard ask:/volume1/projects/nginx/backups
|
|
98
98
|
NGINX_BACKUP_KEEP: "10"
|
|
99
99
|
|
|
100
|
-
#
|
|
100
|
+
# Volume mount settings
|
|
101
101
|
ENABLE_VOLUME_MOUNT: "false" # @wizard ask:false
|
|
102
102
|
VOLUME_HOST_PATH: "/volume1/projects/__PROJECT_NAME__" # @wizard ask:/volume1/projects/__PROJECT_NAME__
|
|
103
103
|
VOLUME_CONTAINER_PATH: "/app" # @wizard ask:/app
|
|
104
104
|
|
|
105
|
-
# SSH
|
|
106
|
-
# password
|
|
107
|
-
# key
|
|
105
|
+
# SSH auth method: "password" (Synology / generic servers) | "key" (AWS EC2 / .pem)
|
|
106
|
+
# password -> uses the SERVER_PASSWORD secret
|
|
107
|
+
# key -> uses the SSH_KEY secret (the full .pem contents as the secret value)
|
|
108
108
|
SSH_AUTH_METHOD: "password" # @wizard ask:password
|
|
109
109
|
|
|
110
|
-
# SSH
|
|
110
|
+
# SSH connection settings
|
|
111
111
|
SSH_PORT: "__SSH_PORT__" # @wizard ask:2022
|
|
112
112
|
SSH_COMMAND_TIMEOUT: "600s"
|
|
113
113
|
SSH_CONNECTION_TIMEOUT: "60s"
|
|
114
114
|
|
|
115
|
-
#
|
|
115
|
+
# Health check settings
|
|
116
116
|
HEALTHCHECK_PATH: "/actuator/health"
|
|
117
117
|
HEALTHCHECK_MAX_RETRIES: "120"
|
|
118
118
|
HEALTHCHECK_RETRY_INTERVAL: "1"
|
|
@@ -121,17 +121,17 @@ env:
|
|
|
121
121
|
IN_FLIGHT_WAIT: "5"
|
|
122
122
|
|
|
123
123
|
jobs:
|
|
124
|
-
#
|
|
124
|
+
# Build job
|
|
125
125
|
build:
|
|
126
|
-
name: Spring Boot
|
|
126
|
+
name: Build Spring Boot application
|
|
127
127
|
runs-on: ubuntu-latest
|
|
128
128
|
|
|
129
129
|
steps:
|
|
130
|
-
- name:
|
|
130
|
+
- name: Checkout code
|
|
131
131
|
uses: actions/checkout@v7
|
|
132
132
|
|
|
133
|
-
#
|
|
134
|
-
- name:
|
|
133
|
+
# If a required Secret or the Dockerfile is missing, name it and stop instead of failing later in a third-party step such as docker login
|
|
134
|
+
- name: Deploy pre-check (Secrets and Dockerfile)
|
|
135
135
|
working-directory: ${{ env.PROJECT_PATH }}
|
|
136
136
|
env:
|
|
137
137
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
@@ -142,11 +142,12 @@ jobs:
|
|
|
142
142
|
SSH_KEY: ${{ secrets.SSH_KEY }}
|
|
143
143
|
DOCKERFILE: ${{ env.DOCKERFILE_PATH }}
|
|
144
144
|
run: |
|
|
145
|
+
m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
|
|
145
146
|
MISSING=""
|
|
146
147
|
for NAME in DOCKERHUB_USERNAME DOCKERHUB_TOKEN SERVER_HOST SERVER_USER; do
|
|
147
148
|
[ -n "${!NAME}" ] || MISSING="$MISSING $NAME"
|
|
148
149
|
done
|
|
149
|
-
# SSH_AUTH_METHOD=key
|
|
150
|
+
# SSH_AUTH_METHOD=key needs SSH_KEY, anything else needs SERVER_PASSWORD
|
|
150
151
|
if [ "${SSH_AUTH_METHOD:-password}" = "key" ]; then
|
|
151
152
|
[ -n "$SSH_KEY" ] || MISSING="$MISSING SSH_KEY"
|
|
152
153
|
else
|
|
@@ -154,45 +155,46 @@ jobs:
|
|
|
154
155
|
fi
|
|
155
156
|
FAILED=false
|
|
156
157
|
if [ -n "$MISSING" ]; then
|
|
157
|
-
echo "::error title
|
|
158
|
+
echo "::error title=$(m cicd.precheck_secret_title)::$(m cicd.precheck_secret_body missing="$MISSING")"
|
|
158
159
|
FAILED=true
|
|
159
160
|
fi
|
|
160
161
|
if [ ! -f "$DOCKERFILE" ]; then
|
|
161
|
-
echo "::error title=
|
|
162
|
+
echo "::error title=$(m cicd.precheck_dockerfile_title)::$(m cicd.precheck_dockerfile_body path="${PROJECT_PATH}/${DOCKERFILE#./}")"
|
|
162
163
|
FAILED=true
|
|
163
164
|
fi
|
|
164
165
|
if [ "$FAILED" = "true" ]; then
|
|
165
166
|
exit 1
|
|
166
167
|
fi
|
|
167
|
-
|
|
168
|
+
m cicd.precheck_ok
|
|
168
169
|
|
|
169
|
-
- name: Java
|
|
170
|
+
- name: Set up Java
|
|
170
171
|
uses: actions/setup-java@v5
|
|
171
172
|
with:
|
|
172
173
|
java-version: ${{ env.JAVA_VERSION }}
|
|
173
174
|
distribution: 'temurin'
|
|
174
175
|
cache: 'gradle'
|
|
175
176
|
|
|
176
|
-
- name: Gradle
|
|
177
|
+
- name: Make Gradle wrapper executable
|
|
177
178
|
working-directory: ${{ env.PROJECT_PATH }}
|
|
178
179
|
run: chmod +x gradlew
|
|
179
180
|
|
|
180
|
-
- name: application-prod.yml
|
|
181
|
+
- name: Generate application-prod.yml
|
|
181
182
|
run: |
|
|
183
|
+
m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
|
|
182
184
|
mkdir -p ${{ env.APPLICATION_YML_DIR }}
|
|
183
185
|
cat << 'EOF' > ./${{ env.APPLICATION_YML_DIR }}/application-prod.yml
|
|
184
186
|
${{ secrets.APPLICATION_PROD_YML }}
|
|
185
187
|
EOF
|
|
186
|
-
|
|
188
|
+
m wf_spring_deploy.prod_yml_created dir="${{ env.APPLICATION_YML_DIR }}"
|
|
187
189
|
|
|
188
190
|
- name: Build with Gradle
|
|
189
191
|
working-directory: ${{ env.PROJECT_PATH }}
|
|
190
192
|
run: ./gradlew clean build -x test ${{ env.GRADLE_OPTS }}
|
|
191
193
|
|
|
192
|
-
- name: Docker
|
|
194
|
+
- name: Set up Docker Buildx
|
|
193
195
|
uses: docker/setup-buildx-action@v3
|
|
194
196
|
|
|
195
|
-
- name: DockerHub
|
|
197
|
+
- name: Log in to DockerHub
|
|
196
198
|
uses: docker/login-action@v3
|
|
197
199
|
with:
|
|
198
200
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
@@ -206,7 +208,7 @@ jobs:
|
|
|
206
208
|
restore-keys: |
|
|
207
209
|
${{ runner.os }}-buildx-
|
|
208
210
|
|
|
209
|
-
- name:
|
|
211
|
+
- name: Build and push Docker image
|
|
210
212
|
uses: docker/build-push-action@v5
|
|
211
213
|
with:
|
|
212
214
|
context: ${{ env.PROJECT_PATH }}
|
|
@@ -221,16 +223,34 @@ jobs:
|
|
|
221
223
|
rm -rf /tmp/.buildx-cache
|
|
222
224
|
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
|
223
225
|
|
|
224
|
-
#
|
|
226
|
+
# Deploy job (Blue-Green, zero downtime)
|
|
225
227
|
deploy:
|
|
226
|
-
name: Blue-Green
|
|
228
|
+
name: Blue-Green zero-downtime deploy
|
|
227
229
|
needs: build
|
|
228
230
|
runs-on: ubuntu-latest
|
|
229
231
|
|
|
230
232
|
steps:
|
|
233
|
+
# The deploy script runs on the server, which has no messages.py:
|
|
234
|
+
# render the message catalog here and pass it along as a base64 env var
|
|
235
|
+
- name: Checkout message catalog
|
|
236
|
+
uses: actions/checkout@v7
|
|
237
|
+
with:
|
|
238
|
+
sparse-checkout: |
|
|
239
|
+
.github/scripts/messages.py
|
|
240
|
+
version.yml
|
|
241
|
+
sparse-checkout-cone-mode: false
|
|
242
|
+
|
|
243
|
+
- name: Load messages
|
|
244
|
+
run: |
|
|
245
|
+
MSGS="$(python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" dump wf_spring_deploy. \
|
|
246
|
+
| python3 -c 'import json, shlex, sys; d = json.load(sys.stdin); print("\n".join("M_%s=%s" % (k.replace(".", "_"), shlex.quote(v)) for k, v in d.items()))' \
|
|
247
|
+
| base64 | tr -d '\n')"
|
|
248
|
+
echo "PAW_MSG_B64=$MSGS" >> "$GITHUB_ENV"
|
|
249
|
+
|
|
231
250
|
- name: Deploy
|
|
232
251
|
uses: appleboy/ssh-action@v1.0.3
|
|
233
252
|
env:
|
|
253
|
+
PAW_MSG_B64: ${{ env.PAW_MSG_B64 }}
|
|
234
254
|
PROJECT_NAME: ${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}
|
|
235
255
|
SERVICE_DOMAIN: ${{ env.SERVICE_DOMAIN }}
|
|
236
256
|
SPRING_PROFILE: ${{ env.SPRING_PROFILE }}
|
|
@@ -261,49 +281,59 @@ jobs:
|
|
|
261
281
|
port: ${{ env.SSH_PORT }}
|
|
262
282
|
command_timeout: ${{ env.SSH_COMMAND_TIMEOUT }}
|
|
263
283
|
timeout: ${{ env.SSH_CONNECTION_TIMEOUT }}
|
|
264
|
-
envs: PROJECT_NAME,SERVICE_DOMAIN,SPRING_PROFILE,CONTAINER_INTERNAL_PORT,BLUE_PORT,GREEN_PORT,ENABLE_VOLUME_MOUNT,VOLUME_HOST_PATH,VOLUME_CONTAINER_PATH,NGINX_RP_CONF,NGINX_BACKUP_DIR,NGINX_BACKUP_KEEP,HEALTHCHECK_MAX_RETRIES,HEALTHCHECK_RETRY_INTERVAL,HEALTHCHECK_PATH,DOMAIN_CHECK_RETRIES,DOMAIN_CHECK_INTERVAL,IN_FLIGHT_WAIT,BRANCH_REF,DOCKERHUB_USERNAME,SERVER_PASSWORD,SSH_AUTH_METHOD
|
|
284
|
+
envs: PROJECT_NAME,SERVICE_DOMAIN,SPRING_PROFILE,CONTAINER_INTERNAL_PORT,BLUE_PORT,GREEN_PORT,ENABLE_VOLUME_MOUNT,VOLUME_HOST_PATH,VOLUME_CONTAINER_PATH,NGINX_RP_CONF,NGINX_BACKUP_DIR,NGINX_BACKUP_KEEP,HEALTHCHECK_MAX_RETRIES,HEALTHCHECK_RETRY_INTERVAL,HEALTHCHECK_PATH,DOMAIN_CHECK_RETRIES,DOMAIN_CHECK_INTERVAL,IN_FLIGHT_WAIT,BRANCH_REF,DOCKERHUB_USERNAME,SERVER_PASSWORD,SSH_AUTH_METHOD,PAW_MSG_B64
|
|
265
285
|
script: |
|
|
266
286
|
set -e
|
|
267
287
|
export PATH=$PATH:/usr/local/bin
|
|
268
288
|
export PW="${SERVER_PASSWORD}"
|
|
269
289
|
|
|
270
|
-
#
|
|
290
|
+
# Messages arrive as base64 shell assignments (the server has no messages.py)
|
|
291
|
+
eval "$(printf '%s' "$PAW_MSG_B64" | base64 -d)"
|
|
292
|
+
m() {
|
|
293
|
+
local _v="M_wf_spring_deploy_$1" _kv
|
|
294
|
+
local _s="${!_v}"
|
|
295
|
+
shift
|
|
296
|
+
for _kv in "$@"; do _s=${_s//\{${_kv%%=*}\}/${_kv#*=}}; done
|
|
297
|
+
printf '%s\n' "$_s"
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
# Abstract sudo by SSH auth method
|
|
271
301
|
SSH_AUTH_METHOD="${SSH_AUTH_METHOD:-password}"
|
|
272
302
|
if [ "${SSH_AUTH_METHOD}" = "key" ]; then
|
|
273
303
|
SUDO() { sudo "$@"; }
|
|
274
304
|
else
|
|
275
305
|
SUDO() { echo "$PW" | sudo -S "$@"; }
|
|
276
306
|
fi
|
|
277
|
-
|
|
307
|
+
m ssh_auth_method method="${SSH_AUTH_METHOD}"
|
|
278
308
|
|
|
279
309
|
IMAGE="${DOCKERHUB_USERNAME}/${PROJECT_NAME}:${BRANCH_REF}"
|
|
280
310
|
BLUE_CONTAINER_NAME="${PROJECT_NAME}-blue"
|
|
281
311
|
GREEN_CONTAINER_NAME="${PROJECT_NAME}-green"
|
|
282
312
|
|
|
283
313
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
284
|
-
|
|
314
|
+
m nginx_deploy_start
|
|
285
315
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
316
|
+
m banner_project name="${PROJECT_NAME}"
|
|
317
|
+
m banner_domain domain="${SERVICE_DOMAIN}"
|
|
318
|
+
m banner_image image="${IMAGE}"
|
|
289
319
|
echo "🎨 Blue=${BLUE_PORT} | Green=${GREEN_PORT}"
|
|
290
320
|
echo "🔧 Nginx config: ${NGINX_RP_CONF}"
|
|
291
321
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
292
322
|
|
|
293
|
-
# 1.
|
|
294
|
-
|
|
323
|
+
# 1. Pull the latest image
|
|
324
|
+
m image_pull image="${IMAGE}"
|
|
295
325
|
SUDO docker pull "${IMAGE}"
|
|
296
326
|
|
|
297
|
-
# 2. nginx
|
|
298
|
-
|
|
327
|
+
# 2. Back up the nginx config
|
|
328
|
+
m nginx_backup_dir dir="${NGINX_BACKUP_DIR}"
|
|
299
329
|
SUDO mkdir -p "${NGINX_BACKUP_DIR}"
|
|
300
330
|
|
|
301
331
|
BACKUP_FILE="${NGINX_BACKUP_DIR}/server.ReverseProxy.conf.$(date +%Y%m%d_%H%M%S).bak"
|
|
302
|
-
|
|
332
|
+
m nginx_backup_file file="${BACKUP_FILE}"
|
|
303
333
|
SUDO cp "${NGINX_RP_CONF}" "${BACKUP_FILE}"
|
|
304
334
|
|
|
305
|
-
# 3.
|
|
306
|
-
|
|
335
|
+
# 3. Determine the active port (parse proxy_pass in the nginx config)
|
|
336
|
+
m nginx_find_active
|
|
307
337
|
ACTIVE_PORT=$(
|
|
308
338
|
SUDO awk -v dom="${SERVICE_DOMAIN}" '
|
|
309
339
|
BEGIN {
|
|
@@ -337,10 +367,10 @@ jobs:
|
|
|
337
367
|
)
|
|
338
368
|
|
|
339
369
|
if [ -z "${ACTIVE_PORT}" ]; then
|
|
340
|
-
|
|
370
|
+
m nginx_active_missing blue="${BLUE_PORT}"
|
|
341
371
|
ACTIVE_PORT="${BLUE_PORT}"
|
|
342
372
|
fi
|
|
343
|
-
|
|
373
|
+
m nginx_active_port port="${ACTIVE_PORT}"
|
|
344
374
|
|
|
345
375
|
if [ "${ACTIVE_PORT}" = "${BLUE_PORT}" ]; then
|
|
346
376
|
NEW_PORT="${GREEN_PORT}"
|
|
@@ -352,30 +382,30 @@ jobs:
|
|
|
352
382
|
OLD_CONTAINER_NAME="${GREEN_CONTAINER_NAME}"
|
|
353
383
|
fi
|
|
354
384
|
|
|
355
|
-
|
|
356
|
-
|
|
385
|
+
m nginx_new_deploy port="${NEW_PORT}" name="${NEW_CONTAINER_NAME}"
|
|
386
|
+
m nginx_old_container name="${OLD_CONTAINER_NAME}"
|
|
357
387
|
|
|
358
|
-
# 4.
|
|
388
|
+
# 4. Clean up a leftover container in the same (idle) slot
|
|
359
389
|
if SUDO docker ps -a --format '{{.Names}}' | grep -Eq "^${NEW_CONTAINER_NAME}\$"; then
|
|
360
|
-
|
|
390
|
+
m idle_slot_cleanup name="${NEW_CONTAINER_NAME}"
|
|
361
391
|
SUDO docker rm -f "${NEW_CONTAINER_NAME}"
|
|
362
392
|
fi
|
|
363
393
|
|
|
364
|
-
# 5.
|
|
394
|
+
# 5. Build the volume mount option
|
|
365
395
|
VOLUME_MOUNT_OPTION="-v /etc/localtime:/etc/localtime:ro"
|
|
366
396
|
if [ "${ENABLE_VOLUME_MOUNT}" = "true" ]; then
|
|
367
397
|
if [ ! -d "${VOLUME_HOST_PATH}" ]; then
|
|
368
|
-
|
|
398
|
+
m volume_mkdir path="${VOLUME_HOST_PATH}"
|
|
369
399
|
SUDO mkdir -p "${VOLUME_HOST_PATH}"
|
|
370
400
|
fi
|
|
371
401
|
VOLUME_MOUNT_OPTION="${VOLUME_MOUNT_OPTION} -v ${VOLUME_HOST_PATH}:${VOLUME_CONTAINER_PATH}"
|
|
372
|
-
|
|
402
|
+
m volume_enabled host="${VOLUME_HOST_PATH}" container="${VOLUME_CONTAINER_PATH}"
|
|
373
403
|
else
|
|
374
|
-
|
|
404
|
+
m volume_disabled
|
|
375
405
|
fi
|
|
376
406
|
|
|
377
|
-
# 6.
|
|
378
|
-
|
|
407
|
+
# 6. Start the new container
|
|
408
|
+
m nginx_new_container_start name="${NEW_CONTAINER_NAME}" port="${NEW_PORT}"
|
|
379
409
|
SUDO docker run -d \
|
|
380
410
|
-p ${NEW_PORT}:${CONTAINER_INTERNAL_PORT} \
|
|
381
411
|
--name "${NEW_CONTAINER_NAME}" \
|
|
@@ -384,30 +414,30 @@ jobs:
|
|
|
384
414
|
${VOLUME_MOUNT_OPTION} \
|
|
385
415
|
"${IMAGE}"
|
|
386
416
|
|
|
387
|
-
# 7.
|
|
388
|
-
|
|
417
|
+
# 7. Container health check (call the local port directly)
|
|
418
|
+
m nginx_hc_start retries="${HEALTHCHECK_MAX_RETRIES}" interval="${HEALTHCHECK_RETRY_INTERVAL}"
|
|
389
419
|
HEALTHCHECK_SUCCESS=false
|
|
390
420
|
for i in $(seq 1 ${HEALTHCHECK_MAX_RETRIES}); do
|
|
391
421
|
if curl -fsS "http://127.0.0.1:${NEW_PORT}${HEALTHCHECK_PATH}" >/dev/null 2>&1 || \
|
|
392
422
|
curl -fsS "http://127.0.0.1:${NEW_PORT}/" >/dev/null 2>&1 ; then
|
|
393
|
-
|
|
423
|
+
m nginx_hc_ok i="${i}"
|
|
394
424
|
HEALTHCHECK_SUCCESS=true
|
|
395
425
|
break
|
|
396
426
|
fi
|
|
397
|
-
|
|
427
|
+
m nginx_hc_progress i="${i}" max="${HEALTHCHECK_MAX_RETRIES}"
|
|
398
428
|
sleep ${HEALTHCHECK_RETRY_INTERVAL}
|
|
399
429
|
done
|
|
400
430
|
|
|
401
431
|
if [ "${HEALTHCHECK_SUCCESS}" != "true" ]; then
|
|
402
|
-
|
|
432
|
+
m nginx_hc_fail
|
|
403
433
|
SUDO docker logs --tail 200 "${NEW_CONTAINER_NAME}" || true
|
|
404
|
-
|
|
434
|
+
m nginx_new_cleanup name="${NEW_CONTAINER_NAME}"
|
|
405
435
|
SUDO docker rm -f "${NEW_CONTAINER_NAME}" || true
|
|
406
436
|
exit 1
|
|
407
437
|
fi
|
|
408
438
|
|
|
409
|
-
# 8. nginx config
|
|
410
|
-
|
|
439
|
+
# 8. Toggle the nginx config port (replace the proxy_pass port)
|
|
440
|
+
m nginx_toggle old="${ACTIVE_PORT}" new="${NEW_PORT}"
|
|
411
441
|
TMP_FILE="$(mktemp)"
|
|
412
442
|
SUDO awk -v dom="${SERVICE_DOMAIN}" -v new_port="${NEW_PORT}" '
|
|
413
443
|
BEGIN {
|
|
@@ -440,97 +470,97 @@ jobs:
|
|
|
440
470
|
}
|
|
441
471
|
' "${NGINX_RP_CONF}" > "${TMP_FILE}"
|
|
442
472
|
|
|
443
|
-
# 9. nginx
|
|
444
|
-
|
|
473
|
+
# 9. Validate the nginx config and reload (restore the backup on failure)
|
|
474
|
+
m nginx_apply
|
|
445
475
|
SUDO cp "${TMP_FILE}" "${NGINX_RP_CONF}"
|
|
446
476
|
if ! (SUDO nginx -t); then
|
|
447
|
-
|
|
477
|
+
m nginx_test_fail
|
|
448
478
|
SUDO cp "${BACKUP_FILE}" "${NGINX_RP_CONF}"
|
|
449
479
|
SUDO rm -f "${BACKUP_FILE}"
|
|
450
480
|
rm -f "${TMP_FILE}"
|
|
451
|
-
|
|
481
|
+
m nginx_new_cleanup name="${NEW_CONTAINER_NAME}"
|
|
452
482
|
SUDO docker rm -f "${NEW_CONTAINER_NAME}" || true
|
|
453
483
|
exit 1
|
|
454
484
|
fi
|
|
455
|
-
|
|
485
|
+
m nginx_test_ok
|
|
456
486
|
rm -f "${TMP_FILE}"
|
|
457
487
|
|
|
458
488
|
echo "🔄 nginx reload"
|
|
459
489
|
if ! (SUDO systemctl reload nginx); then
|
|
460
|
-
|
|
490
|
+
m nginx_reload_fail
|
|
461
491
|
if ! (SUDO systemctl restart nginx); then
|
|
462
|
-
|
|
492
|
+
m nginx_restart_fail
|
|
463
493
|
SUDO cp "${BACKUP_FILE}" "${NGINX_RP_CONF}"
|
|
464
494
|
SUDO systemctl restart nginx
|
|
465
495
|
SUDO rm -f "${BACKUP_FILE}"
|
|
466
|
-
|
|
496
|
+
m nginx_new_cleanup name="${NEW_CONTAINER_NAME}"
|
|
467
497
|
SUDO docker rm -f "${NEW_CONTAINER_NAME}" || true
|
|
468
498
|
exit 1
|
|
469
499
|
fi
|
|
470
500
|
fi
|
|
471
501
|
|
|
472
|
-
# 10.
|
|
473
|
-
|
|
502
|
+
# 10. Verify domain access
|
|
503
|
+
m nginx_domain_check domain="${SERVICE_DOMAIN}"
|
|
474
504
|
DOMAIN_OK=false
|
|
475
505
|
for i in $(seq 1 ${DOMAIN_CHECK_RETRIES}); do
|
|
476
506
|
if curl -fsS "https://${SERVICE_DOMAIN}${HEALTHCHECK_PATH}" >/dev/null 2>&1 || \
|
|
477
507
|
curl -fsS "https://${SERVICE_DOMAIN}/" >/dev/null 2>&1 ; then
|
|
478
|
-
|
|
508
|
+
m nginx_domain_ok i="${i}"
|
|
479
509
|
DOMAIN_OK=true
|
|
480
510
|
break
|
|
481
511
|
fi
|
|
482
|
-
|
|
512
|
+
m nginx_domain_progress i="${i}" max="${DOMAIN_CHECK_RETRIES}"
|
|
483
513
|
sleep ${DOMAIN_CHECK_INTERVAL}
|
|
484
514
|
done
|
|
485
515
|
|
|
486
516
|
if [ "${DOMAIN_OK}" != "true" ]; then
|
|
487
|
-
|
|
517
|
+
m nginx_domain_fail
|
|
488
518
|
fi
|
|
489
519
|
|
|
490
|
-
# 11.
|
|
491
|
-
|
|
520
|
+
# 11. Remove the previous container
|
|
521
|
+
m inflight_wait sec="${IN_FLIGHT_WAIT}"
|
|
492
522
|
sleep ${IN_FLIGHT_WAIT}
|
|
493
523
|
|
|
494
524
|
if SUDO docker ps -a --format '{{.Names}}' | grep -Eq "^${OLD_CONTAINER_NAME}\$"; then
|
|
495
|
-
|
|
525
|
+
m old_removed name="${OLD_CONTAINER_NAME}"
|
|
496
526
|
SUDO docker rm -f "${OLD_CONTAINER_NAME}" || true
|
|
497
527
|
else
|
|
498
|
-
|
|
528
|
+
m old_absent name="${OLD_CONTAINER_NAME}"
|
|
499
529
|
fi
|
|
500
530
|
|
|
501
|
-
# 12. dangling
|
|
502
|
-
|
|
531
|
+
# 12. Clean up dangling images and keep only recent backups
|
|
532
|
+
m prune_dangling
|
|
503
533
|
SUDO docker image prune -af
|
|
504
534
|
|
|
505
|
-
|
|
535
|
+
m nginx_backup_keep keep="${NGINX_BACKUP_KEEP}"
|
|
506
536
|
KEEP_PLUS_ONE=$((NGINX_BACKUP_KEEP + 1))
|
|
507
537
|
SUDO /bin/bash -c "ls -1t '${NGINX_BACKUP_DIR}'/server.ReverseProxy.conf.*.bak 2>/dev/null | tail -n +${KEEP_PLUS_ONE} | xargs -r rm -f"
|
|
508
538
|
|
|
509
539
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
510
|
-
|
|
511
|
-
|
|
540
|
+
m nginx_done name="${NEW_CONTAINER_NAME}" port="${NEW_PORT}"
|
|
541
|
+
m banner_domain domain="https://${SERVICE_DOMAIN}"
|
|
512
542
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
513
543
|
|
|
514
|
-
#
|
|
544
|
+
# Usage
|
|
515
545
|
#
|
|
516
|
-
# 1) GitHub Secrets
|
|
546
|
+
# 1) Register the GitHub Secrets:
|
|
517
547
|
# APPLICATION_PROD_YML, DOCKERHUB_USERNAME, DOCKERHUB_TOKEN,
|
|
518
548
|
# SERVER_HOST, SERVER_USER, SERVER_PASSWORD
|
|
519
549
|
#
|
|
520
|
-
# 2)
|
|
550
|
+
# 2) Edit the project-specific settings in this workflow's env block:
|
|
521
551
|
# - PROJECT_NAME, SERVICE_DOMAIN
|
|
522
552
|
# - APPLICATION_YML_DIR, CONTAINER_INTERNAL_PORT
|
|
523
|
-
# - BLUE_PORT, GREEN_PORT (
|
|
553
|
+
# - BLUE_PORT, GREEN_PORT (two different host ports)
|
|
524
554
|
# - NGINX_RP_CONF, NGINX_BACKUP_DIR
|
|
525
555
|
# - ENABLE_VOLUME_MOUNT, VOLUME_HOST_PATH, VOLUME_CONTAINER_PATH
|
|
526
556
|
#
|
|
527
|
-
# 3)
|
|
528
|
-
# - nginx
|
|
529
|
-
# -
|
|
557
|
+
# 3) Server preparation:
|
|
558
|
+
# - nginx installed and a sites-enabled config present
|
|
559
|
+
# - the server block of that config contains these lines:
|
|
530
560
|
# server_name ${SERVICE_DOMAIN};
|
|
531
|
-
# proxy_pass http://127.0.0.1:${BLUE_PORT}; #
|
|
532
|
-
# - SSH
|
|
561
|
+
# proxy_pass http://127.0.0.1:${BLUE_PORT}; # or ${GREEN_PORT}
|
|
562
|
+
# - the SSH user can run nginx -t / reload and docker commands via sudo
|
|
533
563
|
#
|
|
534
|
-
# 4)
|
|
535
|
-
# -
|
|
536
|
-
# - PROJECT-SPRING-SIMPLE-CICD.yaml
|
|
564
|
+
# 4) Switch to this workflow:
|
|
565
|
+
# - uncomment push.branches in this file
|
|
566
|
+
# - comment out the push trigger in PROJECT-SPRING-SIMPLE-CICD.yaml
|