project-auto-wizard 0.13.2 → 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,72 +1,72 @@
|
|
|
1
1
|
# project-auto-wizard:managed-workflow
|
|
2
2
|
# Python/FastAPI PR Preview (SSH + Docker + Traefik)
|
|
3
|
-
#
|
|
4
|
-
#
|
|
3
|
+
# Manages the preview environment automatically through PR or issue comment commands.
|
|
4
|
+
# Routing is dynamic via the Traefik reverse proxy.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
# ENV_FILE: .env
|
|
8
|
-
# DOCKERHUB_USERNAME: Docker Hub
|
|
9
|
-
# DOCKERHUB_TOKEN: Docker Hub
|
|
10
|
-
# SERVER_HOST:
|
|
11
|
-
# SERVER_USER: SSH
|
|
12
|
-
# SERVER_PASSWORD: SSH
|
|
13
|
-
# SSH_KEY: SSH
|
|
6
|
+
# Required GitHub Secrets
|
|
7
|
+
# ENV_FILE: full contents of the .env file
|
|
8
|
+
# DOCKERHUB_USERNAME: Docker Hub username
|
|
9
|
+
# DOCKERHUB_TOKEN: Docker Hub access token
|
|
10
|
+
# SERVER_HOST: server host (SSH address)
|
|
11
|
+
# SERVER_USER: SSH username
|
|
12
|
+
# SERVER_PASSWORD: SSH password (when SSH_AUTH_METHOD=password)
|
|
13
|
+
# SSH_KEY: contents of the SSH private key .pem (when SSH_AUTH_METHOD=key, e.g. AWS EC2)
|
|
14
14
|
#
|
|
15
|
-
#
|
|
16
|
-
# /wizard server build -
|
|
17
|
-
# /wizard server destroy -
|
|
18
|
-
# /wizard server status -
|
|
15
|
+
# Usage
|
|
16
|
+
# /wizard server build - Build and deploy the preview
|
|
17
|
+
# /wizard server destroy - Delete the preview environment
|
|
18
|
+
# /wizard server status - Check the current status
|
|
19
19
|
#
|
|
20
|
-
#
|
|
21
|
-
# -
|
|
22
|
-
# -
|
|
20
|
+
# Resource naming
|
|
21
|
+
# - Container: {PROJECT_NAME}-pr-{number}
|
|
22
|
+
# - Domain: {PROJECT_NAME}-pr-{number}.pr.example.com
|
|
23
23
|
#
|
|
24
|
-
#
|
|
25
|
-
# - Traefik
|
|
26
|
-
# -
|
|
24
|
+
# Prerequisites
|
|
25
|
+
# - Traefik container running (traefik-network)
|
|
26
|
+
# - Wildcard DNS: *.pr.example.com → server
|
|
27
27
|
|
|
28
28
|
name: PROJECT-PYTHON-PR-PREVIEW
|
|
29
29
|
|
|
30
|
-
#
|
|
30
|
+
# Project settings: when reusing this in another project, edit only this section
|
|
31
31
|
env:
|
|
32
|
-
#
|
|
32
|
+
# Unique project identifier (used in container, image and domain names)
|
|
33
33
|
PROJECT_NAME: "__PROJECT_NAME__" # @wizard ask:@repo
|
|
34
|
-
#
|
|
34
|
+
# Subdirectory of this project in a monorepo (--paths). Base for the build and Docker context ('.' means the repo root)
|
|
35
35
|
PROJECT_PATH: "." # @wizard auto:project-path
|
|
36
|
-
#
|
|
37
|
-
#
|
|
36
|
+
# In a monorepo (PROJECT_PATH is not '.') '-python' is appended to image, container and domain names
|
|
37
|
+
# so they do not collide with services of other types in the same repo (e.g. my-app-python)
|
|
38
38
|
|
|
39
|
-
# Docker
|
|
40
|
-
DOCKERFILE_PATH: './Dockerfile' #
|
|
39
|
+
# Docker settings
|
|
40
|
+
DOCKERFILE_PATH: './Dockerfile' # path relative to PROJECT_PATH
|
|
41
41
|
INTERNAL_PORT: '8000'
|
|
42
42
|
|
|
43
|
-
#
|
|
43
|
+
# Volume mount settings (edit per project; leave empty to skip mounting)
|
|
44
44
|
PROJECT_TARGET_DIR: "__PROJECT_TARGET_DIR__" # @wizard ask:/volume1/projects/__PROJECT_NAME__
|
|
45
45
|
PROJECT_MNT_DIR: "__PROJECT_MNT_DIR__" # @wizard ask:/mnt/__PROJECT_NAME__
|
|
46
46
|
|
|
47
|
-
# Traefik &
|
|
47
|
+
# Traefik & preview domain settings (do not change after the environment is set up)
|
|
48
48
|
TRAEFIK_NETWORK: traefik-network
|
|
49
49
|
PREVIEW_DOMAIN_SUFFIX: "__PREVIEW_DOMAIN_SUFFIX__" # @wizard ask:pr.example.com
|
|
50
50
|
PREVIEW_PORT: '8079'
|
|
51
51
|
|
|
52
|
-
# SSH
|
|
53
|
-
# password → SERVER_PASSWORD secret
|
|
54
|
-
# key → SSH_KEY secret
|
|
52
|
+
# SSH auth method: "password" (Synology / regular servers) | "key" (AWS EC2 / .pem)
|
|
53
|
+
# password → uses the SERVER_PASSWORD secret
|
|
54
|
+
# key → uses the SSH_KEY secret (store the whole .pem contents as the secret value)
|
|
55
55
|
SSH_AUTH_METHOD: "password" # @wizard ask:password
|
|
56
56
|
|
|
57
|
-
# SSH
|
|
57
|
+
# SSH port (do not change after the server port is set up)
|
|
58
58
|
SSH_PORT: "__SSH_PORT__" # @wizard ask:2022
|
|
59
59
|
|
|
60
|
-
# Issue Helper
|
|
61
|
-
#
|
|
60
|
+
# Issue Helper comment marker (edit per project)
|
|
61
|
+
# Used to extract the branch name from an issue comment
|
|
62
62
|
ISSUE_HELPER_MARKER: '<!-- project-auto-wizard issue helper -->'
|
|
63
63
|
|
|
64
|
-
# Health
|
|
65
|
-
# - HEALTH_CHECK_PATH: HTTP
|
|
66
|
-
# - HEALTH_CHECK_LOG_PATTERN:
|
|
67
|
-
# - API_DOCS_PATH: API
|
|
64
|
+
# Health check settings (edit per project)
|
|
65
|
+
# - HEALTH_CHECK_PATH: HTTP health check path (empty skips the HTTP check)
|
|
66
|
+
# - HEALTH_CHECK_LOG_PATTERN: log pattern match (fallback when HTTP fails)
|
|
67
|
+
# - API_DOCS_PATH: API docs URL (empty hides it from the deploy comment)
|
|
68
68
|
#
|
|
69
|
-
#
|
|
69
|
+
# Default examples per framework:
|
|
70
70
|
# FastAPI: '/docs', 'Uvicorn running on|Application startup complete'
|
|
71
71
|
# Spring Boot: '/actuator/health', 'Started.*Application|Tomcat started on port'
|
|
72
72
|
# Express: '/health', 'Server listening on port'
|
|
@@ -76,11 +76,11 @@ env:
|
|
|
76
76
|
|
|
77
77
|
on:
|
|
78
78
|
issue_comment:
|
|
79
|
-
types: [created] # PR
|
|
79
|
+
types: [created] # PR comments + issue comments
|
|
80
80
|
issues:
|
|
81
|
-
types: [closed] #
|
|
81
|
+
types: [closed] # destroy when the issue is closed
|
|
82
82
|
pull_request:
|
|
83
|
-
types: [closed] # PR
|
|
83
|
+
types: [closed] # destroy when the PR is closed
|
|
84
84
|
|
|
85
85
|
permissions:
|
|
86
86
|
contents: read
|
|
@@ -88,9 +88,9 @@ permissions:
|
|
|
88
88
|
issues: write
|
|
89
89
|
|
|
90
90
|
jobs:
|
|
91
|
-
# Job 1:
|
|
91
|
+
# Job 1: parse the command (supports both PR and issue comments)
|
|
92
92
|
check-command:
|
|
93
|
-
name:
|
|
93
|
+
name: Check command
|
|
94
94
|
if: github.event_name == 'issue_comment'
|
|
95
95
|
runs-on: ubuntu-latest
|
|
96
96
|
outputs:
|
|
@@ -100,7 +100,28 @@ jobs:
|
|
|
100
100
|
custom_branch: ${{ steps.parse.outputs.custom_branch }}
|
|
101
101
|
is_custom_branch: ${{ steps.parse.outputs.is_custom_branch }}
|
|
102
102
|
steps:
|
|
103
|
-
|
|
103
|
+
# Messages come from the same revision as this workflow (the PR merge commit on pull_request, otherwise the
|
|
104
|
+
# default branch) so they exist even before the default branch has them, and before the PR branch is checked out
|
|
105
|
+
- name: Checkout messages
|
|
106
|
+
uses: actions/checkout@v7
|
|
107
|
+
with:
|
|
108
|
+
ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
|
|
109
|
+
sparse-checkout: |
|
|
110
|
+
.github/scripts/messages.py
|
|
111
|
+
version.yml
|
|
112
|
+
sparse-checkout-cone-mode: false
|
|
113
|
+
|
|
114
|
+
- name: Load messages
|
|
115
|
+
run: |
|
|
116
|
+
# Keep a copy outside the workspace: checking out the PR branch later replaces its files
|
|
117
|
+
mkdir -p "$RUNNER_TEMP/paw"
|
|
118
|
+
cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
|
|
119
|
+
{
|
|
120
|
+
echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
|
|
121
|
+
echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
|
|
122
|
+
} >> "$GITHUB_ENV"
|
|
123
|
+
|
|
124
|
+
- name: Add 👀 reaction to comment
|
|
104
125
|
if: contains(github.event.comment.body, '/wizard') && contains(github.event.comment.body, 'server')
|
|
105
126
|
uses: actions/github-script@v9
|
|
106
127
|
with:
|
|
@@ -113,31 +134,33 @@ jobs:
|
|
|
113
134
|
content: 'eyes'
|
|
114
135
|
});
|
|
115
136
|
|
|
116
|
-
- name:
|
|
137
|
+
- name: Parse command
|
|
117
138
|
id: parse
|
|
118
139
|
env:
|
|
119
140
|
COMMENT: ${{ github.event.comment.body }}
|
|
120
141
|
IS_PR: ${{ github.event.issue.pull_request != null }}
|
|
121
142
|
run: |
|
|
143
|
+
m() { python3 "$RUNNER_TEMP/paw/messages.py" get "$@"; }
|
|
144
|
+
|
|
122
145
|
if [[ "$IS_PR" == "true" ]]; then
|
|
123
146
|
echo "is_pr=true" >> $GITHUB_OUTPUT
|
|
124
|
-
|
|
147
|
+
m wf_preview.cmd_from_pr
|
|
125
148
|
else
|
|
126
149
|
echo "is_pr=false" >> $GITHUB_OUTPUT
|
|
127
|
-
|
|
150
|
+
m wf_preview.cmd_from_issue
|
|
128
151
|
fi
|
|
129
152
|
|
|
130
|
-
#
|
|
153
|
+
# Format: /wizard server <command> [branch]
|
|
131
154
|
if [[ "$COMMENT" =~ /wizard[[:space:]]+server[[:space:]]+build([[:space:]]+([^[:space:]]+))? ]]; then
|
|
132
155
|
echo "command=build" >> $GITHUB_OUTPUT
|
|
133
156
|
echo "is_valid=true" >> $GITHUB_OUTPUT
|
|
134
157
|
if [[ -n "${BASH_REMATCH[2]}" ]]; then
|
|
135
158
|
echo "custom_branch=${BASH_REMATCH[2]}" >> $GITHUB_OUTPUT
|
|
136
159
|
echo "is_custom_branch=true" >> $GITHUB_OUTPUT
|
|
137
|
-
|
|
160
|
+
m wf_preview.cmd_detected_build_branch branch="${BASH_REMATCH[2]}"
|
|
138
161
|
else
|
|
139
162
|
echo "is_custom_branch=false" >> $GITHUB_OUTPUT
|
|
140
|
-
|
|
163
|
+
m wf_preview.cmd_detected_build
|
|
141
164
|
fi
|
|
142
165
|
elif [[ "$COMMENT" =~ /wizard[[:space:]]+server[[:space:]]+destroy([[:space:]]+([^[:space:]]+))? ]]; then
|
|
143
166
|
echo "command=destroy" >> $GITHUB_OUTPUT
|
|
@@ -145,10 +168,10 @@ jobs:
|
|
|
145
168
|
if [[ -n "${BASH_REMATCH[2]}" ]]; then
|
|
146
169
|
echo "custom_branch=${BASH_REMATCH[2]}" >> $GITHUB_OUTPUT
|
|
147
170
|
echo "is_custom_branch=true" >> $GITHUB_OUTPUT
|
|
148
|
-
|
|
171
|
+
m wf_preview.cmd_detected_destroy_branch branch="${BASH_REMATCH[2]}"
|
|
149
172
|
else
|
|
150
173
|
echo "is_custom_branch=false" >> $GITHUB_OUTPUT
|
|
151
|
-
|
|
174
|
+
m wf_preview.cmd_detected_destroy
|
|
152
175
|
fi
|
|
153
176
|
elif [[ "$COMMENT" =~ /wizard[[:space:]]+server[[:space:]]+status([[:space:]]+([^[:space:]]+))? ]]; then
|
|
154
177
|
echo "command=status" >> $GITHUB_OUTPUT
|
|
@@ -156,19 +179,19 @@ jobs:
|
|
|
156
179
|
if [[ -n "${BASH_REMATCH[2]}" ]]; then
|
|
157
180
|
echo "custom_branch=${BASH_REMATCH[2]}" >> $GITHUB_OUTPUT
|
|
158
181
|
echo "is_custom_branch=true" >> $GITHUB_OUTPUT
|
|
159
|
-
|
|
182
|
+
m wf_preview.cmd_detected_status_branch branch="${BASH_REMATCH[2]}"
|
|
160
183
|
else
|
|
161
184
|
echo "is_custom_branch=false" >> $GITHUB_OUTPUT
|
|
162
|
-
|
|
185
|
+
m wf_preview.cmd_detected_status
|
|
163
186
|
fi
|
|
164
187
|
else
|
|
165
188
|
echo "is_valid=false" >> $GITHUB_OUTPUT
|
|
166
|
-
|
|
189
|
+
m wf_preview.cmd_not_wizard
|
|
167
190
|
fi
|
|
168
191
|
|
|
169
|
-
# Job 2-1:
|
|
192
|
+
# Job 2-1: build & deploy (triggered from a PR comment)
|
|
170
193
|
build-preview-pr:
|
|
171
|
-
name: Preview
|
|
194
|
+
name: Preview build & deploy (PR)
|
|
172
195
|
needs: check-command
|
|
173
196
|
if: |
|
|
174
197
|
needs.check-command.outputs.is_valid == 'true' &&
|
|
@@ -177,7 +200,51 @@ jobs:
|
|
|
177
200
|
needs.check-command.outputs.is_custom_branch != 'true'
|
|
178
201
|
runs-on: ubuntu-latest
|
|
179
202
|
steps:
|
|
180
|
-
|
|
203
|
+
# Messages come from the same revision as this workflow (the PR merge commit on pull_request, otherwise the
|
|
204
|
+
# default branch) so they exist even before the default branch has them, and before the PR branch is checked out
|
|
205
|
+
- name: Checkout messages
|
|
206
|
+
uses: actions/checkout@v7
|
|
207
|
+
with:
|
|
208
|
+
ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
|
|
209
|
+
sparse-checkout: |
|
|
210
|
+
.github/scripts/messages.py
|
|
211
|
+
version.yml
|
|
212
|
+
sparse-checkout-cone-mode: false
|
|
213
|
+
|
|
214
|
+
- name: Load messages
|
|
215
|
+
run: |
|
|
216
|
+
# Keep a copy outside the workspace: checking out the PR branch later replaces its files
|
|
217
|
+
mkdir -p "$RUNNER_TEMP/paw"
|
|
218
|
+
cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
|
|
219
|
+
{
|
|
220
|
+
echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
|
|
221
|
+
echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
|
|
222
|
+
} >> "$GITHUB_ENV"
|
|
223
|
+
# The deploy script runs on the remote server, where messages.py does not exist:
|
|
224
|
+
# ship the messages it needs as a small shell function (m KEY name=value ...)
|
|
225
|
+
python3 - >> "$GITHUB_ENV" <<'PY'
|
|
226
|
+
import json, os, shlex, subprocess
|
|
227
|
+
script = os.path.join(os.environ["RUNNER_TEMP"], "paw", "messages.py")
|
|
228
|
+
msgs = json.loads(subprocess.check_output(["python3", script, "dump", "wf_preview.r_"], text=True))
|
|
229
|
+
head = ["m() {", ' case "$1" in']
|
|
230
|
+
for key, text in msgs.items():
|
|
231
|
+
head.append(" %s) _t=%s ;;" % (key, shlex.quote(text)))
|
|
232
|
+
tail = r''' *) _t="$1" ;;
|
|
233
|
+
esac
|
|
234
|
+
shift
|
|
235
|
+
while [ "$#" -gt 0 ]; do
|
|
236
|
+
_v=$(printf '%s' "${1#*=}" | sed 's/[\\&|]/\\&/g')
|
|
237
|
+
_t=$(printf '%s' "$_t" | sed "s|{${1%%=*}}|$_v|g")
|
|
238
|
+
shift
|
|
239
|
+
done
|
|
240
|
+
printf '%s\n' "$_t"
|
|
241
|
+
}'''
|
|
242
|
+
print("PAW_REMOTE<<PAW_EOF")
|
|
243
|
+
print("\n".join(head) + "\n" + tail)
|
|
244
|
+
print("PAW_EOF")
|
|
245
|
+
PY
|
|
246
|
+
|
|
247
|
+
- name: Get PR info
|
|
181
248
|
id: pr
|
|
182
249
|
uses: actions/github-script@v9
|
|
183
250
|
with:
|
|
@@ -191,28 +258,31 @@ jobs:
|
|
|
191
258
|
core.setOutput('sha', pr.data.head.sha.substring(0, 7));
|
|
192
259
|
return pr.data.number;
|
|
193
260
|
|
|
194
|
-
#
|
|
195
|
-
- name:
|
|
261
|
+
# Progress comment system
|
|
262
|
+
- name: Create progress comment
|
|
196
263
|
id: progress
|
|
197
264
|
uses: actions/github-script@v9
|
|
198
265
|
with:
|
|
199
266
|
script: |
|
|
267
|
+
const M = JSON.parse(process.env.PAW_MSG);
|
|
268
|
+
const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
|
|
269
|
+
|
|
200
270
|
const prNumber = context.issue.number;
|
|
201
271
|
const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
|
202
272
|
const startTime = Date.now();
|
|
203
273
|
|
|
204
274
|
const body = [
|
|
205
|
-
'
|
|
275
|
+
m('wf_preview.title_pr_building'),
|
|
206
276
|
'',
|
|
207
|
-
'
|
|
277
|
+
m('wf_preview.tbl_steps'),
|
|
208
278
|
'|------|------|----------|',
|
|
209
|
-
'
|
|
210
|
-
'
|
|
279
|
+
m('wf_preview.row_docker_progress'),
|
|
280
|
+
m('wf_preview.row_deploy_wait'),
|
|
211
281
|
'',
|
|
212
|
-
|
|
282
|
+
m('wf_preview.link_logs', {runUrl}),
|
|
213
283
|
'',
|
|
214
284
|
'---',
|
|
215
|
-
'
|
|
285
|
+
m('wf_preview.footer_progress')
|
|
216
286
|
].join('\n');
|
|
217
287
|
|
|
218
288
|
const { data: comment } = await github.rest.issues.createComment({
|
|
@@ -226,13 +296,13 @@ jobs:
|
|
|
226
296
|
core.setOutput('start_time', startTime);
|
|
227
297
|
core.setOutput('docker_start', startTime);
|
|
228
298
|
|
|
229
|
-
- name:
|
|
299
|
+
- name: Checkout code
|
|
230
300
|
uses: actions/checkout@v7
|
|
231
301
|
with:
|
|
232
302
|
ref: ${{ steps.pr.outputs.ref }}
|
|
233
303
|
|
|
234
|
-
#
|
|
235
|
-
- name:
|
|
304
|
+
# If a required secret or the Dockerfile is missing, report the missing items and stop instead of surfacing a third-party error such as a docker login failure
|
|
305
|
+
- name: Deploy pre-check (Secrets and Dockerfile)
|
|
236
306
|
working-directory: ${{ env.PROJECT_PATH }}
|
|
237
307
|
env:
|
|
238
308
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
@@ -243,11 +313,13 @@ jobs:
|
|
|
243
313
|
SSH_KEY: ${{ secrets.SSH_KEY }}
|
|
244
314
|
DOCKERFILE: ${{ env.DOCKERFILE_PATH }}
|
|
245
315
|
run: |
|
|
316
|
+
m() { python3 "$RUNNER_TEMP/paw/messages.py" get "$@"; }
|
|
317
|
+
|
|
246
318
|
MISSING=""
|
|
247
319
|
for NAME in DOCKERHUB_USERNAME DOCKERHUB_TOKEN SERVER_HOST SERVER_USER; do
|
|
248
320
|
[ -n "${!NAME}" ] || MISSING="$MISSING $NAME"
|
|
249
321
|
done
|
|
250
|
-
# SSH_AUTH_METHOD=key
|
|
322
|
+
# SSH_KEY is required when SSH_AUTH_METHOD=key, otherwise SERVER_PASSWORD
|
|
251
323
|
if [ "${SSH_AUTH_METHOD:-password}" = "key" ]; then
|
|
252
324
|
[ -n "$SSH_KEY" ] || MISSING="$MISSING SSH_KEY"
|
|
253
325
|
else
|
|
@@ -255,36 +327,36 @@ jobs:
|
|
|
255
327
|
fi
|
|
256
328
|
FAILED=false
|
|
257
329
|
if [ -n "$MISSING" ]; then
|
|
258
|
-
echo "::error title
|
|
330
|
+
echo "::error title=$(m wf_preview.precheck_secret_title)::$(m wf_preview.precheck_secret_body missing="$MISSING")"
|
|
259
331
|
FAILED=true
|
|
260
332
|
fi
|
|
261
333
|
if [ ! -f "$DOCKERFILE" ]; then
|
|
262
|
-
echo "::error title=
|
|
334
|
+
echo "::error title=$(m wf_preview.precheck_dockerfile_title)::$(m wf_preview.precheck_dockerfile_body path="${PROJECT_PATH}/${DOCKERFILE#./}")"
|
|
263
335
|
FAILED=true
|
|
264
336
|
fi
|
|
265
337
|
if [ "$FAILED" = "true" ]; then
|
|
266
338
|
exit 1
|
|
267
339
|
fi
|
|
268
|
-
|
|
340
|
+
m wf_preview.precheck_ok
|
|
269
341
|
|
|
270
|
-
#
|
|
271
|
-
- name: "[
|
|
342
|
+
# Create the environment file (project-specific section)
|
|
343
|
+
- name: "[Required] Create .env file"
|
|
272
344
|
working-directory: ${{ env.PROJECT_PATH }}
|
|
273
345
|
run: |
|
|
274
346
|
cat << 'EOF' > .env
|
|
275
347
|
${{ secrets.ENV_FILE }}
|
|
276
348
|
EOF
|
|
277
349
|
|
|
278
|
-
- name: Docker
|
|
350
|
+
- name: Log in to Docker Hub
|
|
279
351
|
uses: docker/login-action@v3
|
|
280
352
|
with:
|
|
281
353
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
282
354
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
283
355
|
|
|
284
|
-
- name: Docker Buildx
|
|
356
|
+
- name: Set up Docker Buildx
|
|
285
357
|
uses: docker/setup-buildx-action@v3
|
|
286
358
|
|
|
287
|
-
- name:
|
|
359
|
+
- name: Build & push Docker image
|
|
288
360
|
uses: docker/build-push-action@v5
|
|
289
361
|
with:
|
|
290
362
|
context: ${{ env.PROJECT_PATH }}
|
|
@@ -294,32 +366,35 @@ jobs:
|
|
|
294
366
|
cache-from: type=gha
|
|
295
367
|
cache-to: type=gha,mode=max
|
|
296
368
|
|
|
297
|
-
- name:
|
|
369
|
+
- name: Progress - Docker done
|
|
298
370
|
id: docker_progress
|
|
299
371
|
uses: actions/github-script@v9
|
|
300
372
|
with:
|
|
301
373
|
script: |
|
|
374
|
+
const M = JSON.parse(process.env.PAW_MSG);
|
|
375
|
+
const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
|
|
376
|
+
|
|
302
377
|
const commentId = ${{ steps.progress.outputs.comment_id }};
|
|
303
378
|
const dockerStart = ${{ steps.progress.outputs.docker_start }};
|
|
304
379
|
const now = Date.now();
|
|
305
380
|
const dockerElapsed = now - dockerStart;
|
|
306
381
|
const minutes = Math.floor(dockerElapsed / 60000);
|
|
307
382
|
const seconds = Math.floor((dockerElapsed % 60000) / 1000);
|
|
308
|
-
const dockerDuration = minutes > 0 ?
|
|
383
|
+
const dockerDuration = minutes > 0 ? m('wf_preview.dur_min_sec', {minutes, seconds}) : m('wf_preview.dur_sec', {seconds});
|
|
309
384
|
const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
|
310
385
|
|
|
311
386
|
const body = [
|
|
312
|
-
'
|
|
387
|
+
m('wf_preview.title_pr_building'),
|
|
313
388
|
'',
|
|
314
|
-
'
|
|
389
|
+
m('wf_preview.tbl_steps'),
|
|
315
390
|
'|------|------|----------|',
|
|
316
|
-
|
|
317
|
-
'
|
|
391
|
+
m('wf_preview.row_docker_done', {dockerDuration}),
|
|
392
|
+
m('wf_preview.row_deploy_progress'),
|
|
318
393
|
'',
|
|
319
|
-
|
|
394
|
+
m('wf_preview.link_logs', {runUrl}),
|
|
320
395
|
'',
|
|
321
396
|
'---',
|
|
322
|
-
'
|
|
397
|
+
m('wf_preview.footer_progress')
|
|
323
398
|
].join('\n');
|
|
324
399
|
|
|
325
400
|
await github.rest.issues.updateComment({
|
|
@@ -332,7 +407,7 @@ jobs:
|
|
|
332
407
|
core.setOutput('docker_duration', dockerDuration);
|
|
333
408
|
core.setOutput('deploy_start', now);
|
|
334
409
|
|
|
335
|
-
- name:
|
|
410
|
+
- name: Deploy to server
|
|
336
411
|
uses: appleboy/ssh-action@v1.0.3
|
|
337
412
|
env:
|
|
338
413
|
SSH_AUTH_METHOD: ${{ env.SSH_AUTH_METHOD }}
|
|
@@ -344,19 +419,21 @@ jobs:
|
|
|
344
419
|
port: ${{ env.SSH_PORT }}
|
|
345
420
|
envs: SSH_AUTH_METHOD
|
|
346
421
|
script: |
|
|
422
|
+
${{ env.PAW_REMOTE }}
|
|
423
|
+
|
|
347
424
|
set -e
|
|
348
425
|
|
|
349
426
|
export PATH=$PATH:/usr/local/bin
|
|
350
427
|
export PW="${{ secrets.SERVER_PASSWORD }}"
|
|
351
428
|
|
|
352
|
-
#
|
|
429
|
+
# sudo wrapper that depends on the SSH auth method
|
|
353
430
|
SSH_AUTH_METHOD="${SSH_AUTH_METHOD:-password}"
|
|
354
431
|
if [ "${SSH_AUTH_METHOD}" = "key" ]; then
|
|
355
432
|
SUDO() { sudo "$@"; }
|
|
356
433
|
else
|
|
357
434
|
SUDO() { echo "$PW" | sudo -S "$@"; }
|
|
358
435
|
fi
|
|
359
|
-
|
|
436
|
+
m wf_preview.r_ssh_auth ssh_auth_method="${SSH_AUTH_METHOD}"
|
|
360
437
|
|
|
361
438
|
PR_NUMBER=${{ github.event.issue.number }}
|
|
362
439
|
PROJECT_NAME="${{ env.PROJECT_PATH != '.' && format('{0}-python', env.PROJECT_NAME) || env.PROJECT_NAME }}"
|
|
@@ -367,18 +444,18 @@ jobs:
|
|
|
367
444
|
TRAEFIK_NETWORK="${{ env.TRAEFIK_NETWORK }}"
|
|
368
445
|
|
|
369
446
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
370
|
-
|
|
447
|
+
m wf_preview.r_deploy_head_pr
|
|
371
448
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
449
|
+
m wf_preview.r_project_name project_name="${PROJECT_NAME}"
|
|
450
|
+
m wf_preview.r_num_pr pr_number="${PR_NUMBER}"
|
|
451
|
+
m wf_preview.r_container_name container_name="${CONTAINER_NAME}"
|
|
452
|
+
m wf_preview.r_domain domain="${DOMAIN}"
|
|
376
453
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
377
454
|
|
|
378
|
-
|
|
455
|
+
m wf_preview.r_image_pulling
|
|
379
456
|
SUDO docker pull "${IMAGE}"
|
|
380
457
|
|
|
381
|
-
|
|
458
|
+
m wf_preview.r_existing_cleanup
|
|
382
459
|
SUDO docker rm -f "${CONTAINER_NAME}" 2>/dev/null || true
|
|
383
460
|
|
|
384
461
|
VOLUME_OPTS="-v /etc/localtime:/etc/localtime:ro"
|
|
@@ -386,7 +463,7 @@ jobs:
|
|
|
386
463
|
VOLUME_OPTS="$VOLUME_OPTS -v ${{ env.PROJECT_TARGET_DIR }}:${{ env.PROJECT_MNT_DIR }}"
|
|
387
464
|
fi
|
|
388
465
|
|
|
389
|
-
|
|
466
|
+
m wf_preview.r_container_starting
|
|
390
467
|
SUDO docker run -d \
|
|
391
468
|
--name "${CONTAINER_NAME}" \
|
|
392
469
|
--network "${TRAEFIK_NETWORK}" \
|
|
@@ -399,11 +476,11 @@ jobs:
|
|
|
399
476
|
$VOLUME_OPTS \
|
|
400
477
|
"${IMAGE}"
|
|
401
478
|
|
|
402
|
-
# Health
|
|
403
|
-
# 1.
|
|
404
|
-
# 2.
|
|
479
|
+
# Health check (waits up to 120s) - hybrid approach
|
|
480
|
+
# 1. Try the HTTP check when HEALTH_CHECK_PATH is set
|
|
481
|
+
# 2. Fallback: log pattern match (HEALTH_CHECK_LOG_PATTERN)
|
|
405
482
|
echo ""
|
|
406
|
-
|
|
483
|
+
m wf_preview.r_hc_start
|
|
407
484
|
HEALTH_PATH="${{ env.HEALTH_CHECK_PATH }}"
|
|
408
485
|
LOG_PATTERN="${{ env.HEALTH_CHECK_LOG_PATTERN }}"
|
|
409
486
|
MAX_RETRIES=24
|
|
@@ -415,13 +492,13 @@ jobs:
|
|
|
415
492
|
sleep 5
|
|
416
493
|
RETRY_COUNT=$((RETRY_COUNT + 1))
|
|
417
494
|
|
|
418
|
-
# 1.
|
|
495
|
+
# 1. Check the container state
|
|
419
496
|
STATUS=$(SUDO docker inspect --format='{{.State.Status}}' "${CONTAINER_NAME}" 2>/dev/null || echo "not_found")
|
|
420
497
|
|
|
421
498
|
if [ "$STATUS" = "exited" ]; then
|
|
422
|
-
|
|
499
|
+
m wf_preview.r_container_exited
|
|
423
500
|
echo ""
|
|
424
|
-
|
|
501
|
+
m wf_preview.r_container_logs
|
|
425
502
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
426
503
|
SUDO docker logs --tail 100 "${CONTAINER_NAME}"
|
|
427
504
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
@@ -429,24 +506,24 @@ jobs:
|
|
|
429
506
|
fi
|
|
430
507
|
|
|
431
508
|
if [ "$STATUS" = "running" ]; then
|
|
432
|
-
# 2. HTTP
|
|
509
|
+
# 2. Try the HTTP health check (only when HEALTH_CHECK_PATH is set)
|
|
433
510
|
if [ -n "$HEALTH_PATH" ]; then
|
|
434
511
|
HEALTH=$(SUDO docker exec "${CONTAINER_NAME}" curl -sf "http://localhost:${INTERNAL_PORT}${HEALTH_PATH}" 2>/dev/null || echo "")
|
|
435
512
|
|
|
436
513
|
if [ -n "$HEALTH" ]; then
|
|
437
|
-
|
|
514
|
+
m wf_preview.r_hc_ok_http health_path="${HEALTH_PATH}"
|
|
438
515
|
HEALTH_CHECK_PASSED=true
|
|
439
516
|
HEALTH_CHECK_METHOD="HTTP"
|
|
440
517
|
break
|
|
441
518
|
fi
|
|
442
519
|
fi
|
|
443
520
|
|
|
444
|
-
# 3. HTTP
|
|
521
|
+
# 3. Without an HTTP response, fall back to log pattern matching
|
|
445
522
|
if [ -n "$LOG_PATTERN" ]; then
|
|
446
523
|
STARTED=$(SUDO docker logs --tail 50 "${CONTAINER_NAME}" 2>&1 | grep -E "$LOG_PATTERN" || echo "")
|
|
447
524
|
|
|
448
525
|
if [ -n "$STARTED" ]; then
|
|
449
|
-
|
|
526
|
+
m wf_preview.r_hc_ok_log
|
|
450
527
|
echo " $STARTED"
|
|
451
528
|
HEALTH_CHECK_PASSED=true
|
|
452
529
|
HEALTH_CHECK_METHOD="Log"
|
|
@@ -455,14 +532,14 @@ jobs:
|
|
|
455
532
|
fi
|
|
456
533
|
fi
|
|
457
534
|
|
|
458
|
-
|
|
535
|
+
m wf_preview.r_hc_waiting retry_count="$RETRY_COUNT" max_retries="$MAX_RETRIES" status="$STATUS"
|
|
459
536
|
done
|
|
460
537
|
|
|
461
538
|
if [ "$HEALTH_CHECK_PASSED" = "false" ]; then
|
|
462
539
|
echo ""
|
|
463
|
-
|
|
540
|
+
m wf_preview.r_hc_timeout
|
|
464
541
|
echo ""
|
|
465
|
-
|
|
542
|
+
m wf_preview.r_container_logs
|
|
466
543
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
467
544
|
SUDO docker logs --tail 100 "${CONTAINER_NAME}"
|
|
468
545
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
@@ -470,13 +547,16 @@ jobs:
|
|
|
470
547
|
fi
|
|
471
548
|
|
|
472
549
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
473
|
-
|
|
550
|
+
m wf_preview.r_hc_done health_check_method="${HEALTH_CHECK_METHOD}"
|
|
474
551
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
475
552
|
|
|
476
|
-
- name:
|
|
553
|
+
- name: Deploy complete comment
|
|
477
554
|
uses: actions/github-script@v9
|
|
478
555
|
with:
|
|
479
556
|
script: |
|
|
557
|
+
const M = JSON.parse(process.env.PAW_MSG);
|
|
558
|
+
const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
|
|
559
|
+
|
|
480
560
|
const commentId = ${{ steps.progress.outputs.comment_id }};
|
|
481
561
|
const dockerDuration = '${{ steps.docker_progress.outputs.docker_duration }}';
|
|
482
562
|
const deployStart = ${{ steps.docker_progress.outputs.deploy_start }};
|
|
@@ -484,7 +564,7 @@ jobs:
|
|
|
484
564
|
const deployElapsed = now - deployStart;
|
|
485
565
|
const minutes = Math.floor(deployElapsed / 60000);
|
|
486
566
|
const seconds = Math.floor((deployElapsed % 60000) / 1000);
|
|
487
|
-
const deployDuration = minutes > 0 ?
|
|
567
|
+
const deployDuration = minutes > 0 ? m('wf_preview.dur_min_sec', {minutes, seconds}) : m('wf_preview.dur_sec', {seconds});
|
|
488
568
|
|
|
489
569
|
const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-python', env.PROJECT_NAME) || env.PROJECT_NAME }}';
|
|
490
570
|
const domainSuffix = '${{ env.PREVIEW_DOMAIN_SUFFIX }}';
|
|
@@ -495,40 +575,40 @@ jobs:
|
|
|
495
575
|
const previewUrl = `http://${domain}:${previewPort}`;
|
|
496
576
|
const sha = '${{ steps.pr.outputs.sha }}';
|
|
497
577
|
|
|
498
|
-
//
|
|
578
|
+
// Build the preview environment table (the API Docs row is added only when API_DOCS_PATH is set)
|
|
499
579
|
const envRows = [
|
|
500
580
|
`| **Preview URL** | ${previewUrl} |`,
|
|
501
581
|
];
|
|
502
582
|
if (apiDocsPath) {
|
|
503
583
|
envRows.push(`| **API Docs** | ${previewUrl}${apiDocsPath} |`);
|
|
504
584
|
}
|
|
505
|
-
envRows.push(
|
|
506
|
-
envRows.push(
|
|
585
|
+
envRows.push(m('wf_preview.env_container_pr', {projectName, prNumber}));
|
|
586
|
+
envRows.push(m('wf_preview.env_commit', {sha}));
|
|
507
587
|
|
|
508
588
|
const branchName = '${{ steps.pr.outputs.ref }}';
|
|
509
589
|
|
|
510
590
|
const body = [
|
|
511
|
-
'
|
|
591
|
+
m('wf_preview.title_pr_deployed'),
|
|
512
592
|
'',
|
|
513
|
-
'
|
|
593
|
+
m('wf_preview.tbl_steps'),
|
|
514
594
|
'|------|------|----------|',
|
|
515
|
-
|
|
516
|
-
|
|
595
|
+
m('wf_preview.row_docker_done', {dockerDuration}),
|
|
596
|
+
m('wf_preview.row_deploy_done', {deployDuration}),
|
|
517
597
|
'',
|
|
518
|
-
'
|
|
519
|
-
'
|
|
598
|
+
m('wf_preview.sec_env'),
|
|
599
|
+
m('wf_preview.tbl_items'),
|
|
520
600
|
'|------|-----|',
|
|
521
601
|
...envRows,
|
|
522
602
|
'',
|
|
523
|
-
'
|
|
524
|
-
'
|
|
603
|
+
m('wf_preview.sec_commands'),
|
|
604
|
+
m('wf_preview.tbl_commands'),
|
|
525
605
|
'|--------|------|',
|
|
526
|
-
'
|
|
527
|
-
'
|
|
528
|
-
'
|
|
606
|
+
m('wf_preview.cmd_build'),
|
|
607
|
+
m('wf_preview.cmd_destroy'),
|
|
608
|
+
m('wf_preview.cmd_status'),
|
|
529
609
|
'',
|
|
530
610
|
'<details>',
|
|
531
|
-
'
|
|
611
|
+
m('wf_preview.cmd_advanced'),
|
|
532
612
|
'',
|
|
533
613
|
'```',
|
|
534
614
|
`/wizard server build ${branchName}`,
|
|
@@ -538,7 +618,7 @@ jobs:
|
|
|
538
618
|
'</details>',
|
|
539
619
|
'',
|
|
540
620
|
'---',
|
|
541
|
-
'
|
|
621
|
+
m('wf_preview.footer_pr')
|
|
542
622
|
].join('\n');
|
|
543
623
|
|
|
544
624
|
await github.rest.issues.updateComment({
|
|
@@ -548,64 +628,67 @@ jobs:
|
|
|
548
628
|
body: body
|
|
549
629
|
});
|
|
550
630
|
|
|
551
|
-
- name:
|
|
631
|
+
- name: Build/deploy failure comment
|
|
552
632
|
if: failure()
|
|
553
633
|
uses: actions/github-script@v9
|
|
554
634
|
with:
|
|
555
635
|
script: |
|
|
636
|
+
const M = JSON.parse(process.env.PAW_MSG);
|
|
637
|
+
const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
|
|
638
|
+
|
|
556
639
|
const commentId = ${{ steps.progress.outputs.comment_id || 0 }};
|
|
557
640
|
const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-python', env.PROJECT_NAME) || env.PROJECT_NAME }}';
|
|
558
641
|
const prNumber = context.issue.number;
|
|
559
642
|
const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
|
560
643
|
|
|
561
|
-
//
|
|
644
|
+
// Show where it failed based on progress
|
|
562
645
|
const dockerDuration = '${{ steps.docker_progress.outputs.docker_duration }}';
|
|
563
646
|
|
|
564
|
-
//
|
|
647
|
+
// Work out where it failed - early-stage failures are distinguished too
|
|
565
648
|
let dockerStatus, deployStatus;
|
|
566
649
|
let dockerTime = '-';
|
|
567
650
|
|
|
568
651
|
if (!commentId) {
|
|
569
|
-
//
|
|
570
|
-
dockerStatus = '
|
|
652
|
+
// Failed before the progress comment was created (initialization stage)
|
|
653
|
+
dockerStatus = m('wf_preview.status_init_failed');
|
|
571
654
|
deployStatus = '-';
|
|
572
655
|
} else if (!dockerDuration || dockerDuration === '') {
|
|
573
|
-
// Docker
|
|
574
|
-
dockerStatus = '
|
|
575
|
-
deployStatus = '
|
|
656
|
+
// Failed at the Docker build stage
|
|
657
|
+
dockerStatus = m('wf_preview.status_failed');
|
|
658
|
+
deployStatus = m('wf_preview.status_waiting');
|
|
576
659
|
} else {
|
|
577
|
-
//
|
|
578
|
-
dockerStatus = '
|
|
579
|
-
deployStatus = '
|
|
660
|
+
// Failed at the deploy stage
|
|
661
|
+
dockerStatus = m('wf_preview.status_done');
|
|
662
|
+
deployStatus = m('wf_preview.status_failed');
|
|
580
663
|
dockerTime = dockerDuration;
|
|
581
664
|
}
|
|
582
665
|
|
|
583
666
|
const body = [
|
|
584
|
-
'
|
|
667
|
+
m('wf_preview.title_pr_failed'),
|
|
585
668
|
'',
|
|
586
|
-
'
|
|
669
|
+
m('wf_preview.tbl_steps'),
|
|
587
670
|
'|------|------|----------|',
|
|
588
|
-
|
|
589
|
-
|
|
671
|
+
m('wf_preview.row_docker_status', {dockerStatus, dockerTime}),
|
|
672
|
+
m('wf_preview.row_deploy_status', {deployStatus}),
|
|
590
673
|
'',
|
|
591
|
-
|
|
674
|
+
m('wf_preview.link_build_logs', {runUrl}),
|
|
592
675
|
'',
|
|
593
|
-
'
|
|
594
|
-
'
|
|
595
|
-
'
|
|
596
|
-
'
|
|
597
|
-
'
|
|
598
|
-
'
|
|
676
|
+
m('wf_preview.sec_causes'),
|
|
677
|
+
m('wf_preview.hint_secrets'),
|
|
678
|
+
m('wf_preview.hint_docker_python'),
|
|
679
|
+
m('wf_preview.hint_start_python'),
|
|
680
|
+
m('wf_preview.hint_health_timeout'),
|
|
681
|
+
m('wf_preview.hint_env_missing'),
|
|
599
682
|
'',
|
|
600
|
-
'
|
|
601
|
-
'
|
|
602
|
-
'
|
|
683
|
+
m('wf_preview.sec_next'),
|
|
684
|
+
m('wf_preview.next_logs'),
|
|
685
|
+
m('wf_preview.next_retry'),
|
|
603
686
|
'',
|
|
604
687
|
'---',
|
|
605
|
-
'
|
|
688
|
+
m('wf_preview.footer_pr')
|
|
606
689
|
].join('\n');
|
|
607
690
|
|
|
608
|
-
//
|
|
691
|
+
// Update the progress comment if it exists, otherwise create a new one
|
|
609
692
|
if (commentId) {
|
|
610
693
|
await github.rest.issues.updateComment({
|
|
611
694
|
owner: context.repo.owner,
|
|
@@ -622,9 +705,9 @@ jobs:
|
|
|
622
705
|
});
|
|
623
706
|
}
|
|
624
707
|
|
|
625
|
-
# Job 2-2:
|
|
708
|
+
# Job 2-2: extract the branch name from the issue
|
|
626
709
|
get-branch-from-issue:
|
|
627
|
-
name:
|
|
710
|
+
name: Extract branch from issue
|
|
628
711
|
needs: check-command
|
|
629
712
|
if: |
|
|
630
713
|
needs.check-command.outputs.is_valid == 'true' &&
|
|
@@ -636,30 +719,54 @@ jobs:
|
|
|
636
719
|
found: ${{ steps.extract.outputs.found }}
|
|
637
720
|
issue_number: ${{ steps.extract.outputs.issue_number }}
|
|
638
721
|
steps:
|
|
639
|
-
|
|
722
|
+
# Messages come from the same revision as this workflow (the PR merge commit on pull_request, otherwise the
|
|
723
|
+
# default branch) so they exist even before the default branch has them, and before the PR branch is checked out
|
|
724
|
+
- name: Checkout messages
|
|
725
|
+
uses: actions/checkout@v7
|
|
726
|
+
with:
|
|
727
|
+
ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
|
|
728
|
+
sparse-checkout: |
|
|
729
|
+
.github/scripts/messages.py
|
|
730
|
+
version.yml
|
|
731
|
+
sparse-checkout-cone-mode: false
|
|
732
|
+
|
|
733
|
+
- name: Load messages
|
|
734
|
+
run: |
|
|
735
|
+
# Keep a copy outside the workspace: checking out the PR branch later replaces its files
|
|
736
|
+
mkdir -p "$RUNNER_TEMP/paw"
|
|
737
|
+
cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
|
|
738
|
+
{
|
|
739
|
+
echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
|
|
740
|
+
echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
|
|
741
|
+
} >> "$GITHUB_ENV"
|
|
742
|
+
|
|
743
|
+
- name: Extract branch name from issue comment
|
|
640
744
|
id: extract
|
|
641
745
|
uses: actions/github-script@v9
|
|
642
746
|
with:
|
|
643
747
|
script: |
|
|
748
|
+
const M = JSON.parse(process.env.PAW_MSG);
|
|
749
|
+
const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
|
|
750
|
+
|
|
644
751
|
const issueNumber = context.issue.number;
|
|
645
752
|
const marker = '${{ env.ISSUE_HELPER_MARKER }}';
|
|
646
753
|
|
|
647
|
-
console.log(
|
|
648
|
-
console.log(
|
|
754
|
+
console.log(m('wf_preview.log_search_branch', {issueNumber}));
|
|
755
|
+
console.log(m('wf_preview.log_marker', {marker}));
|
|
649
756
|
|
|
650
|
-
//
|
|
757
|
+
// Fetch all comments of the issue
|
|
651
758
|
const comments = await github.rest.issues.listComments({
|
|
652
759
|
owner: context.repo.owner,
|
|
653
760
|
repo: context.repo.repo,
|
|
654
761
|
issue_number: issueNumber
|
|
655
762
|
});
|
|
656
763
|
|
|
657
|
-
//
|
|
658
|
-
// "###
|
|
659
|
-
const branchRegex = /###\s
|
|
764
|
+
// Branch extraction pattern (same format regardless of the marker)
|
|
765
|
+
// Match the issue helper heading in either language: "### Branch name" (en) or the Korean "### \uBE0C\uB79C\uCE58(\uBA85)" (ko)
|
|
766
|
+
const branchRegex = /###\s*(?:\uBE0C\uB79C\uCE58(?:\uBA85)?|Branch(?: name)?)\s*\n```\s*\n([^\n]+)\s*\n```/i;
|
|
660
767
|
|
|
661
768
|
for (const comment of comments.data) {
|
|
662
|
-
//
|
|
769
|
+
// Find comments that contain the marker
|
|
663
770
|
if (comment.body.includes(marker)) {
|
|
664
771
|
const match = comment.body.match(branchRegex);
|
|
665
772
|
if (match) {
|
|
@@ -667,40 +774,43 @@ jobs:
|
|
|
667
774
|
core.setOutput('branch', branchName);
|
|
668
775
|
core.setOutput('found', 'true');
|
|
669
776
|
core.setOutput('issue_number', issueNumber);
|
|
670
|
-
console.log(
|
|
777
|
+
console.log(m('wf_preview.log_branch_found', {branchName}));
|
|
671
778
|
return;
|
|
672
779
|
}
|
|
673
780
|
}
|
|
674
781
|
}
|
|
675
782
|
|
|
676
|
-
//
|
|
783
|
+
// Branch not found - not an error, handle it gracefully
|
|
677
784
|
core.setOutput('found', 'false');
|
|
678
785
|
core.setOutput('issue_number', issueNumber);
|
|
679
|
-
console.log('
|
|
786
|
+
console.log(m('wf_preview.log_helper_no_branch'));
|
|
680
787
|
|
|
681
|
-
- name:
|
|
788
|
+
- name: Branch not found notice
|
|
682
789
|
if: steps.extract.outputs.found == 'false'
|
|
683
790
|
uses: actions/github-script@v9
|
|
684
791
|
with:
|
|
685
792
|
script: |
|
|
793
|
+
const M = JSON.parse(process.env.PAW_MSG);
|
|
794
|
+
const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
|
|
795
|
+
|
|
686
796
|
const issueNumber = context.issue.number;
|
|
687
797
|
const marker = '${{ env.ISSUE_HELPER_MARKER }}';
|
|
688
798
|
|
|
689
799
|
const body = [
|
|
690
|
-
'
|
|
800
|
+
m('wf_preview.title_branch_missing_warn'),
|
|
691
801
|
'',
|
|
692
|
-
'
|
|
802
|
+
m('wf_preview.tbl_items'),
|
|
693
803
|
'|------|-----|',
|
|
694
804
|
`| **Issue** | #${issueNumber} |`,
|
|
695
|
-
|
|
805
|
+
m('wf_preview.env_marker', {marker}),
|
|
696
806
|
'',
|
|
697
|
-
'
|
|
698
|
-
'
|
|
699
|
-
'
|
|
700
|
-
'
|
|
807
|
+
m('wf_preview.sec_check'),
|
|
808
|
+
m('wf_preview.chk_helper_exists'),
|
|
809
|
+
m('wf_preview.chk_helper_section'),
|
|
810
|
+
m('wf_preview.chk_helper_marker'),
|
|
701
811
|
'',
|
|
702
812
|
'---',
|
|
703
|
-
'
|
|
813
|
+
m('wf_preview.footer_generic')
|
|
704
814
|
].join('\n');
|
|
705
815
|
|
|
706
816
|
await github.rest.issues.createComment({
|
|
@@ -710,9 +820,9 @@ jobs:
|
|
|
710
820
|
body: body
|
|
711
821
|
});
|
|
712
822
|
|
|
713
|
-
# Job 2-3:
|
|
823
|
+
# Job 2-3: build & deploy (triggered from an issue comment)
|
|
714
824
|
build-preview-issue:
|
|
715
|
-
name: Preview
|
|
825
|
+
name: Preview build & deploy (Issue)
|
|
716
826
|
needs: [check-command, get-branch-from-issue]
|
|
717
827
|
if: |
|
|
718
828
|
needs.check-command.outputs.is_valid == 'true' &&
|
|
@@ -722,15 +832,62 @@ jobs:
|
|
|
722
832
|
needs.get-branch-from-issue.outputs.found == 'true'
|
|
723
833
|
runs-on: ubuntu-latest
|
|
724
834
|
steps:
|
|
725
|
-
|
|
835
|
+
# Messages come from the same revision as this workflow (the PR merge commit on pull_request, otherwise the
|
|
836
|
+
# default branch) so they exist even before the default branch has them, and before the PR branch is checked out
|
|
837
|
+
- name: Checkout messages
|
|
838
|
+
uses: actions/checkout@v7
|
|
839
|
+
with:
|
|
840
|
+
ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
|
|
841
|
+
sparse-checkout: |
|
|
842
|
+
.github/scripts/messages.py
|
|
843
|
+
version.yml
|
|
844
|
+
sparse-checkout-cone-mode: false
|
|
845
|
+
|
|
846
|
+
- name: Load messages
|
|
847
|
+
run: |
|
|
848
|
+
# Keep a copy outside the workspace: checking out the PR branch later replaces its files
|
|
849
|
+
mkdir -p "$RUNNER_TEMP/paw"
|
|
850
|
+
cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
|
|
851
|
+
{
|
|
852
|
+
echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
|
|
853
|
+
echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
|
|
854
|
+
} >> "$GITHUB_ENV"
|
|
855
|
+
# The deploy script runs on the remote server, where messages.py does not exist:
|
|
856
|
+
# ship the messages it needs as a small shell function (m KEY name=value ...)
|
|
857
|
+
python3 - >> "$GITHUB_ENV" <<'PY'
|
|
858
|
+
import json, os, shlex, subprocess
|
|
859
|
+
script = os.path.join(os.environ["RUNNER_TEMP"], "paw", "messages.py")
|
|
860
|
+
msgs = json.loads(subprocess.check_output(["python3", script, "dump", "wf_preview.r_"], text=True))
|
|
861
|
+
head = ["m() {", ' case "$1" in']
|
|
862
|
+
for key, text in msgs.items():
|
|
863
|
+
head.append(" %s) _t=%s ;;" % (key, shlex.quote(text)))
|
|
864
|
+
tail = r''' *) _t="$1" ;;
|
|
865
|
+
esac
|
|
866
|
+
shift
|
|
867
|
+
while [ "$#" -gt 0 ]; do
|
|
868
|
+
_v=$(printf '%s' "${1#*=}" | sed 's/[\\&|]/\\&/g')
|
|
869
|
+
_t=$(printf '%s' "$_t" | sed "s|{${1%%=*}}|$_v|g")
|
|
870
|
+
shift
|
|
871
|
+
done
|
|
872
|
+
printf '%s\n' "$_t"
|
|
873
|
+
}'''
|
|
874
|
+
print("PAW_REMOTE<<PAW_EOF")
|
|
875
|
+
print("\n".join(head) + "\n" + tail)
|
|
876
|
+
print("PAW_EOF")
|
|
877
|
+
PY
|
|
878
|
+
|
|
879
|
+
- name: Check branch exists
|
|
726
880
|
id: check_branch
|
|
727
881
|
uses: actions/github-script@v9
|
|
728
882
|
with:
|
|
729
883
|
script: |
|
|
884
|
+
const M = JSON.parse(process.env.PAW_MSG);
|
|
885
|
+
const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
|
|
886
|
+
|
|
730
887
|
const branchName = '${{ needs.get-branch-from-issue.outputs.branch_name }}';
|
|
731
888
|
const issueNumber = ${{ needs.get-branch-from-issue.outputs.issue_number }};
|
|
732
889
|
|
|
733
|
-
console.log(
|
|
890
|
+
console.log(m('wf_preview.log_check_exists', {branchName}));
|
|
734
891
|
|
|
735
892
|
try {
|
|
736
893
|
await github.rest.repos.getBranch({
|
|
@@ -739,28 +896,28 @@ jobs:
|
|
|
739
896
|
branch: branchName
|
|
740
897
|
});
|
|
741
898
|
core.setOutput('exists', 'true');
|
|
742
|
-
console.log(
|
|
899
|
+
console.log(m('wf_preview.log_branch_exists', {branchName}));
|
|
743
900
|
} catch (error) {
|
|
744
901
|
if (error.status === 404) {
|
|
745
902
|
core.setOutput('exists', 'false');
|
|
746
|
-
console.log(
|
|
903
|
+
console.log(m('wf_preview.log_branch_missing', {branchName}));
|
|
747
904
|
|
|
748
|
-
//
|
|
905
|
+
// Comment for a missing branch
|
|
749
906
|
const body = [
|
|
750
|
-
'
|
|
907
|
+
m('wf_preview.title_branch_missing'),
|
|
751
908
|
'',
|
|
752
|
-
'
|
|
909
|
+
m('wf_preview.tbl_items'),
|
|
753
910
|
'|------|-----|',
|
|
754
911
|
`| **Issue** | #${issueNumber} |`,
|
|
755
|
-
|
|
912
|
+
m('wf_preview.env_branch', {branchName}),
|
|
756
913
|
'',
|
|
757
|
-
'
|
|
758
|
-
'
|
|
759
|
-
'
|
|
760
|
-
'
|
|
914
|
+
m('wf_preview.sec_check'),
|
|
915
|
+
m('wf_preview.chk_pushed_1'),
|
|
916
|
+
m('wf_preview.chk_name_2'),
|
|
917
|
+
m('wf_preview.chk_deleted_3'),
|
|
761
918
|
'',
|
|
762
919
|
'---',
|
|
763
|
-
'
|
|
920
|
+
m('wf_preview.footer_generic')
|
|
764
921
|
].join('\n');
|
|
765
922
|
|
|
766
923
|
await github.rest.issues.createComment({
|
|
@@ -774,13 +931,15 @@ jobs:
|
|
|
774
931
|
}
|
|
775
932
|
}
|
|
776
933
|
|
|
777
|
-
- name:
|
|
934
|
+
- name: Skip when branch is missing
|
|
778
935
|
if: steps.check_branch.outputs.exists == 'false'
|
|
779
936
|
run: |
|
|
780
|
-
|
|
937
|
+
m() { python3 "$RUNNER_TEMP/paw/messages.py" get "$@"; }
|
|
938
|
+
|
|
939
|
+
m wf_preview.skip_no_branch
|
|
781
940
|
exit 0
|
|
782
941
|
|
|
783
|
-
- name:
|
|
942
|
+
- name: Set issue info
|
|
784
943
|
if: steps.check_branch.outputs.exists == 'true'
|
|
785
944
|
id: issue
|
|
786
945
|
run: |
|
|
@@ -788,32 +947,35 @@ jobs:
|
|
|
788
947
|
echo "issue_number=${{ needs.get-branch-from-issue.outputs.issue_number }}" >> $GITHUB_OUTPUT
|
|
789
948
|
echo "sha=$(git ls-remote https://github.com/${{ github.repository }}.git refs/heads/${{ needs.get-branch-from-issue.outputs.branch_name }} | cut -c1-7)" >> $GITHUB_OUTPUT
|
|
790
949
|
|
|
791
|
-
#
|
|
792
|
-
- name:
|
|
950
|
+
# Progress comment system
|
|
951
|
+
- name: Create progress comment
|
|
793
952
|
if: steps.check_branch.outputs.exists == 'true'
|
|
794
953
|
id: progress
|
|
795
954
|
uses: actions/github-script@v9
|
|
796
955
|
with:
|
|
797
956
|
script: |
|
|
957
|
+
const M = JSON.parse(process.env.PAW_MSG);
|
|
958
|
+
const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
|
|
959
|
+
|
|
798
960
|
const issueNumber = ${{ needs.get-branch-from-issue.outputs.issue_number }};
|
|
799
961
|
const branchName = '${{ needs.get-branch-from-issue.outputs.branch_name }}';
|
|
800
962
|
const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
|
801
963
|
const startTime = Date.now();
|
|
802
964
|
|
|
803
965
|
const body = [
|
|
804
|
-
'
|
|
966
|
+
m('wf_preview.title_issue_building'),
|
|
805
967
|
'',
|
|
806
|
-
|
|
968
|
+
m('wf_preview.line_branch', {branchName}),
|
|
807
969
|
'',
|
|
808
|
-
'
|
|
970
|
+
m('wf_preview.tbl_steps'),
|
|
809
971
|
'|------|------|----------|',
|
|
810
|
-
'
|
|
811
|
-
'
|
|
972
|
+
m('wf_preview.row_docker_progress'),
|
|
973
|
+
m('wf_preview.row_deploy_wait'),
|
|
812
974
|
'',
|
|
813
|
-
|
|
975
|
+
m('wf_preview.link_logs', {runUrl}),
|
|
814
976
|
'',
|
|
815
977
|
'---',
|
|
816
|
-
'
|
|
978
|
+
m('wf_preview.footer_progress')
|
|
817
979
|
].join('\n');
|
|
818
980
|
|
|
819
981
|
const { data: comment } = await github.rest.issues.createComment({
|
|
@@ -827,14 +989,14 @@ jobs:
|
|
|
827
989
|
core.setOutput('start_time', startTime);
|
|
828
990
|
core.setOutput('docker_start', startTime);
|
|
829
991
|
|
|
830
|
-
- name:
|
|
992
|
+
- name: Checkout code
|
|
831
993
|
if: steps.check_branch.outputs.exists == 'true'
|
|
832
994
|
uses: actions/checkout@v7
|
|
833
995
|
with:
|
|
834
996
|
ref: ${{ needs.get-branch-from-issue.outputs.branch_name }}
|
|
835
997
|
|
|
836
|
-
#
|
|
837
|
-
- name:
|
|
998
|
+
# If a required secret or the Dockerfile is missing, report the missing items and stop instead of surfacing a third-party error such as a docker login failure
|
|
999
|
+
- name: Deploy pre-check (Secrets and Dockerfile)
|
|
838
1000
|
if: steps.check_branch.outputs.exists == 'true'
|
|
839
1001
|
working-directory: ${{ env.PROJECT_PATH }}
|
|
840
1002
|
env:
|
|
@@ -846,11 +1008,13 @@ jobs:
|
|
|
846
1008
|
SSH_KEY: ${{ secrets.SSH_KEY }}
|
|
847
1009
|
DOCKERFILE: ${{ env.DOCKERFILE_PATH }}
|
|
848
1010
|
run: |
|
|
1011
|
+
m() { python3 "$RUNNER_TEMP/paw/messages.py" get "$@"; }
|
|
1012
|
+
|
|
849
1013
|
MISSING=""
|
|
850
1014
|
for NAME in DOCKERHUB_USERNAME DOCKERHUB_TOKEN SERVER_HOST SERVER_USER; do
|
|
851
1015
|
[ -n "${!NAME}" ] || MISSING="$MISSING $NAME"
|
|
852
1016
|
done
|
|
853
|
-
# SSH_AUTH_METHOD=key
|
|
1017
|
+
# SSH_KEY is required when SSH_AUTH_METHOD=key, otherwise SERVER_PASSWORD
|
|
854
1018
|
if [ "${SSH_AUTH_METHOD:-password}" = "key" ]; then
|
|
855
1019
|
[ -n "$SSH_KEY" ] || MISSING="$MISSING SSH_KEY"
|
|
856
1020
|
else
|
|
@@ -858,20 +1022,20 @@ jobs:
|
|
|
858
1022
|
fi
|
|
859
1023
|
FAILED=false
|
|
860
1024
|
if [ -n "$MISSING" ]; then
|
|
861
|
-
echo "::error title
|
|
1025
|
+
echo "::error title=$(m wf_preview.precheck_secret_title)::$(m wf_preview.precheck_secret_body missing="$MISSING")"
|
|
862
1026
|
FAILED=true
|
|
863
1027
|
fi
|
|
864
1028
|
if [ ! -f "$DOCKERFILE" ]; then
|
|
865
|
-
echo "::error title=
|
|
1029
|
+
echo "::error title=$(m wf_preview.precheck_dockerfile_title)::$(m wf_preview.precheck_dockerfile_body path="${PROJECT_PATH}/${DOCKERFILE#./}")"
|
|
866
1030
|
FAILED=true
|
|
867
1031
|
fi
|
|
868
1032
|
if [ "$FAILED" = "true" ]; then
|
|
869
1033
|
exit 1
|
|
870
1034
|
fi
|
|
871
|
-
|
|
1035
|
+
m wf_preview.precheck_ok
|
|
872
1036
|
|
|
873
|
-
#
|
|
874
|
-
- name: "[
|
|
1037
|
+
# Create the environment file (project-specific section)
|
|
1038
|
+
- name: "[Required] Create .env file"
|
|
875
1039
|
working-directory: ${{ env.PROJECT_PATH }}
|
|
876
1040
|
if: steps.check_branch.outputs.exists == 'true'
|
|
877
1041
|
run: |
|
|
@@ -879,18 +1043,18 @@ jobs:
|
|
|
879
1043
|
${{ secrets.ENV_FILE }}
|
|
880
1044
|
EOF
|
|
881
1045
|
|
|
882
|
-
- name: Docker
|
|
1046
|
+
- name: Log in to Docker Hub
|
|
883
1047
|
if: steps.check_branch.outputs.exists == 'true'
|
|
884
1048
|
uses: docker/login-action@v3
|
|
885
1049
|
with:
|
|
886
1050
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
887
1051
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
888
1052
|
|
|
889
|
-
- name: Docker Buildx
|
|
1053
|
+
- name: Set up Docker Buildx
|
|
890
1054
|
if: steps.check_branch.outputs.exists == 'true'
|
|
891
1055
|
uses: docker/setup-buildx-action@v3
|
|
892
1056
|
|
|
893
|
-
- name:
|
|
1057
|
+
- name: Build & push Docker image
|
|
894
1058
|
if: steps.check_branch.outputs.exists == 'true'
|
|
895
1059
|
uses: docker/build-push-action@v5
|
|
896
1060
|
with:
|
|
@@ -901,12 +1065,15 @@ jobs:
|
|
|
901
1065
|
cache-from: type=gha
|
|
902
1066
|
cache-to: type=gha,mode=max
|
|
903
1067
|
|
|
904
|
-
- name:
|
|
1068
|
+
- name: Progress - Docker done
|
|
905
1069
|
if: steps.check_branch.outputs.exists == 'true'
|
|
906
1070
|
id: docker_progress
|
|
907
1071
|
uses: actions/github-script@v9
|
|
908
1072
|
with:
|
|
909
1073
|
script: |
|
|
1074
|
+
const M = JSON.parse(process.env.PAW_MSG);
|
|
1075
|
+
const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
|
|
1076
|
+
|
|
910
1077
|
const commentId = ${{ steps.progress.outputs.comment_id }};
|
|
911
1078
|
const branchName = '${{ needs.get-branch-from-issue.outputs.branch_name }}';
|
|
912
1079
|
const dockerStart = ${{ steps.progress.outputs.docker_start }};
|
|
@@ -914,23 +1081,23 @@ jobs:
|
|
|
914
1081
|
const dockerElapsed = now - dockerStart;
|
|
915
1082
|
const minutes = Math.floor(dockerElapsed / 60000);
|
|
916
1083
|
const seconds = Math.floor((dockerElapsed % 60000) / 1000);
|
|
917
|
-
const dockerDuration = minutes > 0 ?
|
|
1084
|
+
const dockerDuration = minutes > 0 ? m('wf_preview.dur_min_sec', {minutes, seconds}) : m('wf_preview.dur_sec', {seconds});
|
|
918
1085
|
const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
|
919
1086
|
|
|
920
1087
|
const body = [
|
|
921
|
-
'
|
|
1088
|
+
m('wf_preview.title_issue_building'),
|
|
922
1089
|
'',
|
|
923
|
-
|
|
1090
|
+
m('wf_preview.line_branch', {branchName}),
|
|
924
1091
|
'',
|
|
925
|
-
'
|
|
1092
|
+
m('wf_preview.tbl_steps'),
|
|
926
1093
|
'|------|------|----------|',
|
|
927
|
-
|
|
928
|
-
'
|
|
1094
|
+
m('wf_preview.row_docker_done', {dockerDuration}),
|
|
1095
|
+
m('wf_preview.row_deploy_progress'),
|
|
929
1096
|
'',
|
|
930
|
-
|
|
1097
|
+
m('wf_preview.link_logs', {runUrl}),
|
|
931
1098
|
'',
|
|
932
1099
|
'---',
|
|
933
|
-
'
|
|
1100
|
+
m('wf_preview.footer_progress')
|
|
934
1101
|
].join('\n');
|
|
935
1102
|
|
|
936
1103
|
await github.rest.issues.updateComment({
|
|
@@ -943,7 +1110,7 @@ jobs:
|
|
|
943
1110
|
core.setOutput('docker_duration', dockerDuration);
|
|
944
1111
|
core.setOutput('deploy_start', now);
|
|
945
1112
|
|
|
946
|
-
- name:
|
|
1113
|
+
- name: Deploy to server
|
|
947
1114
|
if: steps.check_branch.outputs.exists == 'true'
|
|
948
1115
|
uses: appleboy/ssh-action@v1.0.3
|
|
949
1116
|
env:
|
|
@@ -956,19 +1123,21 @@ jobs:
|
|
|
956
1123
|
port: ${{ env.SSH_PORT }}
|
|
957
1124
|
envs: SSH_AUTH_METHOD
|
|
958
1125
|
script: |
|
|
1126
|
+
${{ env.PAW_REMOTE }}
|
|
1127
|
+
|
|
959
1128
|
set -e
|
|
960
1129
|
|
|
961
1130
|
export PATH=$PATH:/usr/local/bin
|
|
962
1131
|
export PW="${{ secrets.SERVER_PASSWORD }}"
|
|
963
1132
|
|
|
964
|
-
#
|
|
1133
|
+
# sudo wrapper that depends on the SSH auth method
|
|
965
1134
|
SSH_AUTH_METHOD="${SSH_AUTH_METHOD:-password}"
|
|
966
1135
|
if [ "${SSH_AUTH_METHOD}" = "key" ]; then
|
|
967
1136
|
SUDO() { sudo "$@"; }
|
|
968
1137
|
else
|
|
969
1138
|
SUDO() { echo "$PW" | sudo -S "$@"; }
|
|
970
1139
|
fi
|
|
971
|
-
|
|
1140
|
+
m wf_preview.r_ssh_auth ssh_auth_method="${SSH_AUTH_METHOD}"
|
|
972
1141
|
|
|
973
1142
|
ISSUE_NUMBER=${{ needs.get-branch-from-issue.outputs.issue_number }}
|
|
974
1143
|
PROJECT_NAME="${{ env.PROJECT_PATH != '.' && format('{0}-python', env.PROJECT_NAME) || env.PROJECT_NAME }}"
|
|
@@ -979,18 +1148,18 @@ jobs:
|
|
|
979
1148
|
TRAEFIK_NETWORK="${{ env.TRAEFIK_NETWORK }}"
|
|
980
1149
|
|
|
981
1150
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
982
|
-
|
|
1151
|
+
m wf_preview.r_deploy_head_issue
|
|
983
1152
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1153
|
+
m wf_preview.r_project_name project_name="${PROJECT_NAME}"
|
|
1154
|
+
m wf_preview.r_num_issue issue_number="${ISSUE_NUMBER}"
|
|
1155
|
+
m wf_preview.r_container_name container_name="${CONTAINER_NAME}"
|
|
1156
|
+
m wf_preview.r_domain domain="${DOMAIN}"
|
|
988
1157
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
989
1158
|
|
|
990
|
-
|
|
1159
|
+
m wf_preview.r_image_pulling
|
|
991
1160
|
SUDO docker pull "${IMAGE}"
|
|
992
1161
|
|
|
993
|
-
|
|
1162
|
+
m wf_preview.r_existing_cleanup
|
|
994
1163
|
SUDO docker rm -f "${CONTAINER_NAME}" 2>/dev/null || true
|
|
995
1164
|
|
|
996
1165
|
VOLUME_OPTS="-v /etc/localtime:/etc/localtime:ro"
|
|
@@ -998,7 +1167,7 @@ jobs:
|
|
|
998
1167
|
VOLUME_OPTS="$VOLUME_OPTS -v ${{ env.PROJECT_TARGET_DIR }}:${{ env.PROJECT_MNT_DIR }}"
|
|
999
1168
|
fi
|
|
1000
1169
|
|
|
1001
|
-
|
|
1170
|
+
m wf_preview.r_container_starting
|
|
1002
1171
|
SUDO docker run -d \
|
|
1003
1172
|
--name "${CONTAINER_NAME}" \
|
|
1004
1173
|
--network "${TRAEFIK_NETWORK}" \
|
|
@@ -1011,11 +1180,11 @@ jobs:
|
|
|
1011
1180
|
$VOLUME_OPTS \
|
|
1012
1181
|
"${IMAGE}"
|
|
1013
1182
|
|
|
1014
|
-
# Health
|
|
1015
|
-
# 1.
|
|
1016
|
-
# 2.
|
|
1183
|
+
# Health check (waits up to 120s) - hybrid approach
|
|
1184
|
+
# 1. Try the HTTP check when HEALTH_CHECK_PATH is set
|
|
1185
|
+
# 2. Fallback: log pattern match (HEALTH_CHECK_LOG_PATTERN)
|
|
1017
1186
|
echo ""
|
|
1018
|
-
|
|
1187
|
+
m wf_preview.r_hc_start
|
|
1019
1188
|
HEALTH_PATH="${{ env.HEALTH_CHECK_PATH }}"
|
|
1020
1189
|
LOG_PATTERN="${{ env.HEALTH_CHECK_LOG_PATTERN }}"
|
|
1021
1190
|
MAX_RETRIES=24
|
|
@@ -1027,13 +1196,13 @@ jobs:
|
|
|
1027
1196
|
sleep 5
|
|
1028
1197
|
RETRY_COUNT=$((RETRY_COUNT + 1))
|
|
1029
1198
|
|
|
1030
|
-
# 1.
|
|
1199
|
+
# 1. Check the container state
|
|
1031
1200
|
STATUS=$(SUDO docker inspect --format='{{.State.Status}}' "${CONTAINER_NAME}" 2>/dev/null || echo "not_found")
|
|
1032
1201
|
|
|
1033
1202
|
if [ "$STATUS" = "exited" ]; then
|
|
1034
|
-
|
|
1203
|
+
m wf_preview.r_container_exited
|
|
1035
1204
|
echo ""
|
|
1036
|
-
|
|
1205
|
+
m wf_preview.r_container_logs
|
|
1037
1206
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1038
1207
|
SUDO docker logs --tail 100 "${CONTAINER_NAME}"
|
|
1039
1208
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
@@ -1041,24 +1210,24 @@ jobs:
|
|
|
1041
1210
|
fi
|
|
1042
1211
|
|
|
1043
1212
|
if [ "$STATUS" = "running" ]; then
|
|
1044
|
-
# 2. HTTP
|
|
1213
|
+
# 2. Try the HTTP health check (only when HEALTH_CHECK_PATH is set)
|
|
1045
1214
|
if [ -n "$HEALTH_PATH" ]; then
|
|
1046
1215
|
HEALTH=$(SUDO docker exec "${CONTAINER_NAME}" curl -sf "http://localhost:${INTERNAL_PORT}${HEALTH_PATH}" 2>/dev/null || echo "")
|
|
1047
1216
|
|
|
1048
1217
|
if [ -n "$HEALTH" ]; then
|
|
1049
|
-
|
|
1218
|
+
m wf_preview.r_hc_ok_http health_path="${HEALTH_PATH}"
|
|
1050
1219
|
HEALTH_CHECK_PASSED=true
|
|
1051
1220
|
HEALTH_CHECK_METHOD="HTTP"
|
|
1052
1221
|
break
|
|
1053
1222
|
fi
|
|
1054
1223
|
fi
|
|
1055
1224
|
|
|
1056
|
-
# 3. HTTP
|
|
1225
|
+
# 3. Without an HTTP response, fall back to log pattern matching
|
|
1057
1226
|
if [ -n "$LOG_PATTERN" ]; then
|
|
1058
1227
|
STARTED=$(SUDO docker logs --tail 50 "${CONTAINER_NAME}" 2>&1 | grep -E "$LOG_PATTERN" || echo "")
|
|
1059
1228
|
|
|
1060
1229
|
if [ -n "$STARTED" ]; then
|
|
1061
|
-
|
|
1230
|
+
m wf_preview.r_hc_ok_log
|
|
1062
1231
|
echo " $STARTED"
|
|
1063
1232
|
HEALTH_CHECK_PASSED=true
|
|
1064
1233
|
HEALTH_CHECK_METHOD="Log"
|
|
@@ -1067,14 +1236,14 @@ jobs:
|
|
|
1067
1236
|
fi
|
|
1068
1237
|
fi
|
|
1069
1238
|
|
|
1070
|
-
|
|
1239
|
+
m wf_preview.r_hc_waiting retry_count="$RETRY_COUNT" max_retries="$MAX_RETRIES" status="$STATUS"
|
|
1071
1240
|
done
|
|
1072
1241
|
|
|
1073
1242
|
if [ "$HEALTH_CHECK_PASSED" = "false" ]; then
|
|
1074
1243
|
echo ""
|
|
1075
|
-
|
|
1244
|
+
m wf_preview.r_hc_timeout
|
|
1076
1245
|
echo ""
|
|
1077
|
-
|
|
1246
|
+
m wf_preview.r_container_logs
|
|
1078
1247
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1079
1248
|
SUDO docker logs --tail 100 "${CONTAINER_NAME}"
|
|
1080
1249
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
@@ -1082,14 +1251,17 @@ jobs:
|
|
|
1082
1251
|
fi
|
|
1083
1252
|
|
|
1084
1253
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1085
|
-
|
|
1254
|
+
m wf_preview.r_hc_done health_check_method="${HEALTH_CHECK_METHOD}"
|
|
1086
1255
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1087
1256
|
|
|
1088
|
-
- name:
|
|
1257
|
+
- name: Deploy complete comment
|
|
1089
1258
|
if: steps.check_branch.outputs.exists == 'true'
|
|
1090
1259
|
uses: actions/github-script@v9
|
|
1091
1260
|
with:
|
|
1092
1261
|
script: |
|
|
1262
|
+
const M = JSON.parse(process.env.PAW_MSG);
|
|
1263
|
+
const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
|
|
1264
|
+
|
|
1093
1265
|
const commentId = ${{ steps.progress.outputs.comment_id }};
|
|
1094
1266
|
const branchName = '${{ needs.get-branch-from-issue.outputs.branch_name }}';
|
|
1095
1267
|
const dockerDuration = '${{ steps.docker_progress.outputs.docker_duration }}';
|
|
@@ -1098,7 +1270,7 @@ jobs:
|
|
|
1098
1270
|
const deployElapsed = now - deployStart;
|
|
1099
1271
|
const minutes = Math.floor(deployElapsed / 60000);
|
|
1100
1272
|
const seconds = Math.floor((deployElapsed % 60000) / 1000);
|
|
1101
|
-
const deployDuration = minutes > 0 ?
|
|
1273
|
+
const deployDuration = minutes > 0 ? m('wf_preview.dur_min_sec', {minutes, seconds}) : m('wf_preview.dur_sec', {seconds});
|
|
1102
1274
|
|
|
1103
1275
|
const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-python', env.PROJECT_NAME) || env.PROJECT_NAME }}';
|
|
1104
1276
|
const domainSuffix = '${{ env.PREVIEW_DOMAIN_SUFFIX }}';
|
|
@@ -1109,41 +1281,41 @@ jobs:
|
|
|
1109
1281
|
const previewUrl = `http://${domain}:${previewPort}`;
|
|
1110
1282
|
const sha = '${{ steps.issue.outputs.sha }}';
|
|
1111
1283
|
|
|
1112
|
-
//
|
|
1284
|
+
// Build the preview environment table (the API Docs row is added only when API_DOCS_PATH is set)
|
|
1113
1285
|
const envRows = [
|
|
1114
1286
|
`| **Preview URL** | ${previewUrl} |`,
|
|
1115
1287
|
];
|
|
1116
1288
|
if (apiDocsPath) {
|
|
1117
1289
|
envRows.push(`| **API Docs** | ${previewUrl}${apiDocsPath} |`);
|
|
1118
1290
|
}
|
|
1119
|
-
envRows.push(
|
|
1120
|
-
envRows.push(
|
|
1121
|
-
envRows.push(
|
|
1291
|
+
envRows.push(m('wf_preview.env_container_issue', {projectName, issueNumber}));
|
|
1292
|
+
envRows.push(m('wf_preview.env_branch', {branchName}));
|
|
1293
|
+
envRows.push(m('wf_preview.env_commit', {sha}));
|
|
1122
1294
|
|
|
1123
1295
|
const body = [
|
|
1124
|
-
'
|
|
1296
|
+
m('wf_preview.title_issue_deployed'),
|
|
1125
1297
|
'',
|
|
1126
|
-
|
|
1298
|
+
m('wf_preview.line_branch', {branchName}),
|
|
1127
1299
|
'',
|
|
1128
|
-
'
|
|
1300
|
+
m('wf_preview.tbl_steps'),
|
|
1129
1301
|
'|------|------|----------|',
|
|
1130
|
-
|
|
1131
|
-
|
|
1302
|
+
m('wf_preview.row_docker_done', {dockerDuration}),
|
|
1303
|
+
m('wf_preview.row_deploy_done', {deployDuration}),
|
|
1132
1304
|
'',
|
|
1133
|
-
'
|
|
1134
|
-
'
|
|
1305
|
+
m('wf_preview.sec_env'),
|
|
1306
|
+
m('wf_preview.tbl_items'),
|
|
1135
1307
|
'|------|-----|',
|
|
1136
1308
|
...envRows,
|
|
1137
1309
|
'',
|
|
1138
|
-
'
|
|
1139
|
-
'
|
|
1310
|
+
m('wf_preview.sec_commands'),
|
|
1311
|
+
m('wf_preview.tbl_commands'),
|
|
1140
1312
|
'|--------|------|',
|
|
1141
|
-
'
|
|
1142
|
-
'
|
|
1143
|
-
'
|
|
1313
|
+
m('wf_preview.cmd_build'),
|
|
1314
|
+
m('wf_preview.cmd_destroy'),
|
|
1315
|
+
m('wf_preview.cmd_status'),
|
|
1144
1316
|
'',
|
|
1145
1317
|
'<details>',
|
|
1146
|
-
'
|
|
1318
|
+
m('wf_preview.cmd_advanced'),
|
|
1147
1319
|
'',
|
|
1148
1320
|
'```',
|
|
1149
1321
|
`/wizard server build ${branchName}`,
|
|
@@ -1153,7 +1325,7 @@ jobs:
|
|
|
1153
1325
|
'</details>',
|
|
1154
1326
|
'',
|
|
1155
1327
|
'---',
|
|
1156
|
-
'
|
|
1328
|
+
m('wf_preview.footer_issue')
|
|
1157
1329
|
].join('\n');
|
|
1158
1330
|
|
|
1159
1331
|
await github.rest.issues.updateComment({
|
|
@@ -1163,11 +1335,14 @@ jobs:
|
|
|
1163
1335
|
body: body
|
|
1164
1336
|
});
|
|
1165
1337
|
|
|
1166
|
-
- name:
|
|
1338
|
+
- name: Build/deploy failure comment
|
|
1167
1339
|
if: failure() && steps.check_branch.outputs.exists == 'true'
|
|
1168
1340
|
uses: actions/github-script@v9
|
|
1169
1341
|
with:
|
|
1170
1342
|
script: |
|
|
1343
|
+
const M = JSON.parse(process.env.PAW_MSG);
|
|
1344
|
+
const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
|
|
1345
|
+
|
|
1171
1346
|
const commentId = ${{ steps.progress.outputs.comment_id || 0 }};
|
|
1172
1347
|
const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-python', env.PROJECT_NAME) || env.PROJECT_NAME }}';
|
|
1173
1348
|
const issueNumber = ${{ needs.get-branch-from-issue.outputs.issue_number }};
|
|
@@ -1180,42 +1355,42 @@ jobs:
|
|
|
1180
1355
|
let dockerTime = '-';
|
|
1181
1356
|
|
|
1182
1357
|
if (!commentId) {
|
|
1183
|
-
dockerStatus = '
|
|
1358
|
+
dockerStatus = m('wf_preview.status_init_failed');
|
|
1184
1359
|
deployStatus = '-';
|
|
1185
1360
|
} else if (!dockerDuration || dockerDuration === '') {
|
|
1186
|
-
dockerStatus = '
|
|
1187
|
-
deployStatus = '
|
|
1361
|
+
dockerStatus = m('wf_preview.status_failed');
|
|
1362
|
+
deployStatus = m('wf_preview.status_waiting');
|
|
1188
1363
|
} else {
|
|
1189
|
-
dockerStatus = '
|
|
1190
|
-
deployStatus = '
|
|
1364
|
+
dockerStatus = m('wf_preview.status_done');
|
|
1365
|
+
deployStatus = m('wf_preview.status_failed');
|
|
1191
1366
|
dockerTime = dockerDuration;
|
|
1192
1367
|
}
|
|
1193
1368
|
|
|
1194
1369
|
const body = [
|
|
1195
|
-
'
|
|
1370
|
+
m('wf_preview.title_issue_failed'),
|
|
1196
1371
|
'',
|
|
1197
|
-
|
|
1372
|
+
m('wf_preview.line_branch', {branchName}),
|
|
1198
1373
|
'',
|
|
1199
|
-
'
|
|
1374
|
+
m('wf_preview.tbl_steps'),
|
|
1200
1375
|
'|------|------|----------|',
|
|
1201
|
-
|
|
1202
|
-
|
|
1376
|
+
m('wf_preview.row_docker_status', {dockerStatus, dockerTime}),
|
|
1377
|
+
m('wf_preview.row_deploy_status', {deployStatus}),
|
|
1203
1378
|
'',
|
|
1204
|
-
|
|
1379
|
+
m('wf_preview.link_build_logs', {runUrl}),
|
|
1205
1380
|
'',
|
|
1206
|
-
'
|
|
1207
|
-
'
|
|
1208
|
-
'
|
|
1209
|
-
'
|
|
1210
|
-
'
|
|
1211
|
-
'
|
|
1381
|
+
m('wf_preview.sec_causes'),
|
|
1382
|
+
m('wf_preview.hint_secrets'),
|
|
1383
|
+
m('wf_preview.hint_docker_python'),
|
|
1384
|
+
m('wf_preview.hint_start_python'),
|
|
1385
|
+
m('wf_preview.hint_health_timeout'),
|
|
1386
|
+
m('wf_preview.hint_env_missing'),
|
|
1212
1387
|
'',
|
|
1213
|
-
'
|
|
1214
|
-
'
|
|
1215
|
-
'
|
|
1388
|
+
m('wf_preview.sec_next'),
|
|
1389
|
+
m('wf_preview.next_logs'),
|
|
1390
|
+
m('wf_preview.next_retry'),
|
|
1216
1391
|
'',
|
|
1217
1392
|
'---',
|
|
1218
|
-
'
|
|
1393
|
+
m('wf_preview.footer_issue')
|
|
1219
1394
|
].join('\n');
|
|
1220
1395
|
|
|
1221
1396
|
if (commentId) {
|
|
@@ -1234,17 +1409,61 @@ jobs:
|
|
|
1234
1409
|
});
|
|
1235
1410
|
}
|
|
1236
1411
|
|
|
1237
|
-
# Job 3:
|
|
1412
|
+
# Job 3: destroy the preview (PR/issue closed or destroy command)
|
|
1238
1413
|
destroy-preview:
|
|
1239
|
-
name:
|
|
1240
|
-
# needs
|
|
1414
|
+
name: Destroy preview
|
|
1415
|
+
# No needs dependency: lets this run on its own for PR/issue closed events
|
|
1241
1416
|
if: |
|
|
1242
1417
|
(github.event_name == 'pull_request' && github.event.action == 'closed') ||
|
|
1243
1418
|
(github.event_name == 'issues' && github.event.action == 'closed') ||
|
|
1244
1419
|
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '/wizard') && contains(github.event.comment.body, 'server destroy'))
|
|
1245
1420
|
runs-on: ubuntu-latest
|
|
1246
1421
|
steps:
|
|
1247
|
-
|
|
1422
|
+
# Messages come from the same revision as this workflow (the PR merge commit on pull_request, otherwise the
|
|
1423
|
+
# default branch) so they exist even before the default branch has them, and before the PR branch is checked out
|
|
1424
|
+
- name: Checkout messages
|
|
1425
|
+
uses: actions/checkout@v7
|
|
1426
|
+
with:
|
|
1427
|
+
ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
|
|
1428
|
+
sparse-checkout: |
|
|
1429
|
+
.github/scripts/messages.py
|
|
1430
|
+
version.yml
|
|
1431
|
+
sparse-checkout-cone-mode: false
|
|
1432
|
+
|
|
1433
|
+
- name: Load messages
|
|
1434
|
+
run: |
|
|
1435
|
+
# Keep a copy outside the workspace: checking out the PR branch later replaces its files
|
|
1436
|
+
mkdir -p "$RUNNER_TEMP/paw"
|
|
1437
|
+
cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
|
|
1438
|
+
{
|
|
1439
|
+
echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
|
|
1440
|
+
echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
|
|
1441
|
+
} >> "$GITHUB_ENV"
|
|
1442
|
+
# The deploy script runs on the remote server, where messages.py does not exist:
|
|
1443
|
+
# ship the messages it needs as a small shell function (m KEY name=value ...)
|
|
1444
|
+
python3 - >> "$GITHUB_ENV" <<'PY'
|
|
1445
|
+
import json, os, shlex, subprocess
|
|
1446
|
+
script = os.path.join(os.environ["RUNNER_TEMP"], "paw", "messages.py")
|
|
1447
|
+
msgs = json.loads(subprocess.check_output(["python3", script, "dump", "wf_preview.r_"], text=True))
|
|
1448
|
+
head = ["m() {", ' case "$1" in']
|
|
1449
|
+
for key, text in msgs.items():
|
|
1450
|
+
head.append(" %s) _t=%s ;;" % (key, shlex.quote(text)))
|
|
1451
|
+
tail = r''' *) _t="$1" ;;
|
|
1452
|
+
esac
|
|
1453
|
+
shift
|
|
1454
|
+
while [ "$#" -gt 0 ]; do
|
|
1455
|
+
_v=$(printf '%s' "${1#*=}" | sed 's/[\\&|]/\\&/g')
|
|
1456
|
+
_t=$(printf '%s' "$_t" | sed "s|{${1%%=*}}|$_v|g")
|
|
1457
|
+
shift
|
|
1458
|
+
done
|
|
1459
|
+
printf '%s\n' "$_t"
|
|
1460
|
+
}'''
|
|
1461
|
+
print("PAW_REMOTE<<PAW_EOF")
|
|
1462
|
+
print("\n".join(head) + "\n" + tail)
|
|
1463
|
+
print("PAW_EOF")
|
|
1464
|
+
PY
|
|
1465
|
+
|
|
1466
|
+
- name: Get PR/issue number
|
|
1248
1467
|
id: pr_number
|
|
1249
1468
|
run: |
|
|
1250
1469
|
if [[ "${{ github.event_name }}" == "issue_comment" ]]; then
|
|
@@ -1258,7 +1477,7 @@ jobs:
|
|
|
1258
1477
|
echo "type=issue_closed" >> $GITHUB_OUTPUT
|
|
1259
1478
|
fi
|
|
1260
1479
|
|
|
1261
|
-
- name:
|
|
1480
|
+
- name: Remove container & image
|
|
1262
1481
|
uses: appleboy/ssh-action@v1.0.3
|
|
1263
1482
|
env:
|
|
1264
1483
|
SSH_AUTH_METHOD: ${{ env.SSH_AUTH_METHOD }}
|
|
@@ -1270,17 +1489,19 @@ jobs:
|
|
|
1270
1489
|
port: ${{ env.SSH_PORT }}
|
|
1271
1490
|
envs: SSH_AUTH_METHOD
|
|
1272
1491
|
script: |
|
|
1492
|
+
${{ env.PAW_REMOTE }}
|
|
1493
|
+
|
|
1273
1494
|
export PATH=$PATH:/usr/local/bin
|
|
1274
1495
|
export PW="${{ secrets.SERVER_PASSWORD }}"
|
|
1275
1496
|
|
|
1276
|
-
#
|
|
1497
|
+
# sudo wrapper that depends on the SSH auth method
|
|
1277
1498
|
SSH_AUTH_METHOD="${SSH_AUTH_METHOD:-password}"
|
|
1278
1499
|
if [ "${SSH_AUTH_METHOD}" = "key" ]; then
|
|
1279
1500
|
SUDO() { sudo "$@"; }
|
|
1280
1501
|
else
|
|
1281
1502
|
SUDO() { echo "$PW" | sudo -S "$@"; }
|
|
1282
1503
|
fi
|
|
1283
|
-
|
|
1504
|
+
m wf_preview.r_ssh_auth ssh_auth_method="${SSH_AUTH_METHOD}"
|
|
1284
1505
|
|
|
1285
1506
|
PR_NUMBER=${{ steps.pr_number.outputs.number }}
|
|
1286
1507
|
PROJECT_NAME="${{ env.PROJECT_PATH != '.' && format('{0}-python', env.PROJECT_NAME) || env.PROJECT_NAME }}"
|
|
@@ -1288,21 +1509,21 @@ jobs:
|
|
|
1288
1509
|
IMAGE="${{ secrets.DOCKERHUB_USERNAME }}/${PROJECT_NAME}:pr-${PR_NUMBER}"
|
|
1289
1510
|
|
|
1290
1511
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1291
|
-
|
|
1512
|
+
m wf_preview.r_destroy_head_pr
|
|
1292
1513
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1293
|
-
|
|
1294
|
-
|
|
1514
|
+
m wf_preview.r_project_name project_name="${PROJECT_NAME}"
|
|
1515
|
+
m wf_preview.r_num_pr pr_number="${PR_NUMBER}"
|
|
1295
1516
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1296
1517
|
|
|
1297
|
-
|
|
1518
|
+
m wf_preview.r_container_removing
|
|
1298
1519
|
SUDO docker rm -f "${CONTAINER_NAME}" 2>/dev/null || true
|
|
1299
1520
|
|
|
1300
|
-
|
|
1521
|
+
m wf_preview.r_image_removing
|
|
1301
1522
|
SUDO docker rmi "${IMAGE}" 2>/dev/null || true
|
|
1302
1523
|
|
|
1303
|
-
|
|
1524
|
+
m wf_preview.r_removed
|
|
1304
1525
|
|
|
1305
|
-
- name:
|
|
1526
|
+
- name: Destroy complete comment
|
|
1306
1527
|
if: |
|
|
1307
1528
|
github.event_name == 'issue_comment' ||
|
|
1308
1529
|
(github.event_name == 'pull_request' && github.event.action == 'closed') ||
|
|
@@ -1310,21 +1531,24 @@ jobs:
|
|
|
1310
1531
|
uses: actions/github-script@v9
|
|
1311
1532
|
with:
|
|
1312
1533
|
script: |
|
|
1534
|
+
const M = JSON.parse(process.env.PAW_MSG);
|
|
1535
|
+
const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
|
|
1536
|
+
|
|
1313
1537
|
const number = ${{ steps.pr_number.outputs.number }};
|
|
1314
1538
|
const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-python', env.PROJECT_NAME) || env.PROJECT_NAME }}';
|
|
1315
1539
|
|
|
1316
1540
|
const body = [
|
|
1317
|
-
'
|
|
1541
|
+
m('wf_preview.title_destroyed'),
|
|
1318
1542
|
'',
|
|
1319
|
-
'
|
|
1543
|
+
m('wf_preview.tbl_items'),
|
|
1320
1544
|
'|------|-----|',
|
|
1321
|
-
|
|
1322
|
-
'
|
|
1545
|
+
m('wf_preview.env_container_num', {projectName, number}),
|
|
1546
|
+
m('wf_preview.env_status_deleted'),
|
|
1323
1547
|
'',
|
|
1324
|
-
'
|
|
1548
|
+
m('wf_preview.redeploy_hint'),
|
|
1325
1549
|
'',
|
|
1326
1550
|
'---',
|
|
1327
|
-
'
|
|
1551
|
+
m('wf_preview.footer_pr_issue')
|
|
1328
1552
|
].join('\n');
|
|
1329
1553
|
|
|
1330
1554
|
await github.rest.issues.createComment({
|
|
@@ -1334,14 +1558,58 @@ jobs:
|
|
|
1334
1558
|
body: body
|
|
1335
1559
|
});
|
|
1336
1560
|
|
|
1337
|
-
# Job 4:
|
|
1561
|
+
# Job 4: status check (PR and issue)
|
|
1338
1562
|
check-status:
|
|
1339
|
-
name: Preview
|
|
1563
|
+
name: Preview status
|
|
1340
1564
|
needs: check-command
|
|
1341
1565
|
if: needs.check-command.outputs.is_valid == 'true' && needs.check-command.outputs.command == 'status'
|
|
1342
1566
|
runs-on: ubuntu-latest
|
|
1343
1567
|
steps:
|
|
1344
|
-
|
|
1568
|
+
# Messages come from the same revision as this workflow (the PR merge commit on pull_request, otherwise the
|
|
1569
|
+
# default branch) so they exist even before the default branch has them, and before the PR branch is checked out
|
|
1570
|
+
- name: Checkout messages
|
|
1571
|
+
uses: actions/checkout@v7
|
|
1572
|
+
with:
|
|
1573
|
+
ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
|
|
1574
|
+
sparse-checkout: |
|
|
1575
|
+
.github/scripts/messages.py
|
|
1576
|
+
version.yml
|
|
1577
|
+
sparse-checkout-cone-mode: false
|
|
1578
|
+
|
|
1579
|
+
- name: Load messages
|
|
1580
|
+
run: |
|
|
1581
|
+
# Keep a copy outside the workspace: checking out the PR branch later replaces its files
|
|
1582
|
+
mkdir -p "$RUNNER_TEMP/paw"
|
|
1583
|
+
cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
|
|
1584
|
+
{
|
|
1585
|
+
echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
|
|
1586
|
+
echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
|
|
1587
|
+
} >> "$GITHUB_ENV"
|
|
1588
|
+
# The deploy script runs on the remote server, where messages.py does not exist:
|
|
1589
|
+
# ship the messages it needs as a small shell function (m KEY name=value ...)
|
|
1590
|
+
python3 - >> "$GITHUB_ENV" <<'PY'
|
|
1591
|
+
import json, os, shlex, subprocess
|
|
1592
|
+
script = os.path.join(os.environ["RUNNER_TEMP"], "paw", "messages.py")
|
|
1593
|
+
msgs = json.loads(subprocess.check_output(["python3", script, "dump", "wf_preview.r_"], text=True))
|
|
1594
|
+
head = ["m() {", ' case "$1" in']
|
|
1595
|
+
for key, text in msgs.items():
|
|
1596
|
+
head.append(" %s) _t=%s ;;" % (key, shlex.quote(text)))
|
|
1597
|
+
tail = r''' *) _t="$1" ;;
|
|
1598
|
+
esac
|
|
1599
|
+
shift
|
|
1600
|
+
while [ "$#" -gt 0 ]; do
|
|
1601
|
+
_v=$(printf '%s' "${1#*=}" | sed 's/[\\&|]/\\&/g')
|
|
1602
|
+
_t=$(printf '%s' "$_t" | sed "s|{${1%%=*}}|$_v|g")
|
|
1603
|
+
shift
|
|
1604
|
+
done
|
|
1605
|
+
printf '%s\n' "$_t"
|
|
1606
|
+
}'''
|
|
1607
|
+
print("PAW_REMOTE<<PAW_EOF")
|
|
1608
|
+
print("\n".join(head) + "\n" + tail)
|
|
1609
|
+
print("PAW_EOF")
|
|
1610
|
+
PY
|
|
1611
|
+
|
|
1612
|
+
- name: Check container status
|
|
1345
1613
|
id: status
|
|
1346
1614
|
uses: appleboy/ssh-action@v1.0.3
|
|
1347
1615
|
with:
|
|
@@ -1351,6 +1619,8 @@ jobs:
|
|
|
1351
1619
|
key: ${{ secrets.SSH_KEY }}
|
|
1352
1620
|
port: ${{ env.SSH_PORT }}
|
|
1353
1621
|
script: |
|
|
1622
|
+
${{ env.PAW_REMOTE }}
|
|
1623
|
+
|
|
1354
1624
|
export PATH=$PATH:/usr/local/bin
|
|
1355
1625
|
|
|
1356
1626
|
NUMBER=${{ github.event.issue.number }}
|
|
@@ -1358,19 +1628,19 @@ jobs:
|
|
|
1358
1628
|
CONTAINER_NAME="${PROJECT_NAME}-pr-${NUMBER}"
|
|
1359
1629
|
|
|
1360
1630
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1361
|
-
|
|
1631
|
+
m wf_preview.r_status_head
|
|
1362
1632
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1363
1633
|
|
|
1364
1634
|
if docker ps --filter "name=${CONTAINER_NAME}" --format "{{.Names}}" | grep -q "${CONTAINER_NAME}"; then
|
|
1365
1635
|
echo "STATUS=running"
|
|
1366
|
-
|
|
1636
|
+
m wf_preview.r_container_running
|
|
1367
1637
|
docker ps --filter "name=${CONTAINER_NAME}" --format "table {{.Names}}\t{{.Status}}\t{{.RunningFor}}"
|
|
1368
1638
|
else
|
|
1369
1639
|
echo "STATUS=not_found"
|
|
1370
|
-
|
|
1640
|
+
m wf_preview.r_container_absent
|
|
1371
1641
|
fi
|
|
1372
1642
|
|
|
1373
|
-
- name:
|
|
1643
|
+
- name: Status check (running)
|
|
1374
1644
|
uses: appleboy/ssh-action@v1.0.3
|
|
1375
1645
|
id: check_running
|
|
1376
1646
|
continue-on-error: true
|
|
@@ -1388,10 +1658,13 @@ jobs:
|
|
|
1388
1658
|
CONTAINER_NAME="${PROJECT_NAME}-pr-${NUMBER}"
|
|
1389
1659
|
docker ps --filter "name=${CONTAINER_NAME}" --format "{{.Names}}" | grep -q "${CONTAINER_NAME}"
|
|
1390
1660
|
|
|
1391
|
-
- name:
|
|
1661
|
+
- name: Post status comment
|
|
1392
1662
|
uses: actions/github-script@v9
|
|
1393
1663
|
with:
|
|
1394
1664
|
script: |
|
|
1665
|
+
const M = JSON.parse(process.env.PAW_MSG);
|
|
1666
|
+
const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
|
|
1667
|
+
|
|
1395
1668
|
const number = context.issue.number;
|
|
1396
1669
|
const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-python', env.PROJECT_NAME) || env.PROJECT_NAME }}';
|
|
1397
1670
|
const domainSuffix = '${{ env.PREVIEW_DOMAIN_SUFFIX }}';
|
|
@@ -1405,36 +1678,36 @@ jobs:
|
|
|
1405
1678
|
let body;
|
|
1406
1679
|
if (isRunning) {
|
|
1407
1680
|
body = [
|
|
1408
|
-
'
|
|
1681
|
+
m('wf_preview.title_running'),
|
|
1409
1682
|
'',
|
|
1410
|
-
'
|
|
1683
|
+
m('wf_preview.tbl_items'),
|
|
1411
1684
|
'|------|-----|',
|
|
1412
1685
|
`| **Preview URL** | ${previewUrl} |`,
|
|
1413
|
-
|
|
1414
|
-
'
|
|
1686
|
+
m('wf_preview.env_container_num', {projectName, number}),
|
|
1687
|
+
m('wf_preview.env_status_running'),
|
|
1415
1688
|
'',
|
|
1416
|
-
'
|
|
1417
|
-
'
|
|
1689
|
+
m('wf_preview.sec_commands'),
|
|
1690
|
+
m('wf_preview.tbl_commands'),
|
|
1418
1691
|
'|--------|------|',
|
|
1419
|
-
'
|
|
1420
|
-
'
|
|
1692
|
+
m('wf_preview.cmd_build'),
|
|
1693
|
+
m('wf_preview.cmd_destroy'),
|
|
1421
1694
|
'',
|
|
1422
1695
|
'---',
|
|
1423
|
-
|
|
1696
|
+
m('wf_preview.footer_context', {contextType})
|
|
1424
1697
|
].join('\n');
|
|
1425
1698
|
} else {
|
|
1426
1699
|
body = [
|
|
1427
|
-
'
|
|
1700
|
+
m('wf_preview.title_absent'),
|
|
1428
1701
|
'',
|
|
1429
|
-
'
|
|
1702
|
+
m('wf_preview.tbl_items'),
|
|
1430
1703
|
'|------|-----|',
|
|
1431
|
-
|
|
1432
|
-
'
|
|
1704
|
+
m('wf_preview.env_container_num', {projectName, number}),
|
|
1705
|
+
m('wf_preview.env_status_absent'),
|
|
1433
1706
|
'',
|
|
1434
|
-
'
|
|
1707
|
+
m('wf_preview.deploy_hint'),
|
|
1435
1708
|
'',
|
|
1436
1709
|
'---',
|
|
1437
|
-
|
|
1710
|
+
m('wf_preview.footer_context', {contextType})
|
|
1438
1711
|
].join('\n');
|
|
1439
1712
|
}
|
|
1440
1713
|
|
|
@@ -1445,9 +1718,9 @@ jobs:
|
|
|
1445
1718
|
body: body
|
|
1446
1719
|
});
|
|
1447
1720
|
|
|
1448
|
-
# Job 5:
|
|
1721
|
+
# Job 5: custom branch build & deploy
|
|
1449
1722
|
build-preview-custom-branch:
|
|
1450
|
-
name: Preview
|
|
1723
|
+
name: Preview build & deploy (Custom Branch)
|
|
1451
1724
|
needs: check-command
|
|
1452
1725
|
if: |
|
|
1453
1726
|
needs.check-command.outputs.is_valid == 'true' &&
|
|
@@ -1455,43 +1728,90 @@ jobs:
|
|
|
1455
1728
|
needs.check-command.outputs.is_custom_branch == 'true'
|
|
1456
1729
|
runs-on: ubuntu-latest
|
|
1457
1730
|
steps:
|
|
1458
|
-
|
|
1731
|
+
# Messages come from the same revision as this workflow (the PR merge commit on pull_request, otherwise the
|
|
1732
|
+
# default branch) so they exist even before the default branch has them, and before the PR branch is checked out
|
|
1733
|
+
- name: Checkout messages
|
|
1734
|
+
uses: actions/checkout@v7
|
|
1735
|
+
with:
|
|
1736
|
+
ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
|
|
1737
|
+
sparse-checkout: |
|
|
1738
|
+
.github/scripts/messages.py
|
|
1739
|
+
version.yml
|
|
1740
|
+
sparse-checkout-cone-mode: false
|
|
1741
|
+
|
|
1742
|
+
- name: Load messages
|
|
1743
|
+
run: |
|
|
1744
|
+
# Keep a copy outside the workspace: checking out the PR branch later replaces its files
|
|
1745
|
+
mkdir -p "$RUNNER_TEMP/paw"
|
|
1746
|
+
cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
|
|
1747
|
+
{
|
|
1748
|
+
echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
|
|
1749
|
+
echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
|
|
1750
|
+
} >> "$GITHUB_ENV"
|
|
1751
|
+
# The deploy script runs on the remote server, where messages.py does not exist:
|
|
1752
|
+
# ship the messages it needs as a small shell function (m KEY name=value ...)
|
|
1753
|
+
python3 - >> "$GITHUB_ENV" <<'PY'
|
|
1754
|
+
import json, os, shlex, subprocess
|
|
1755
|
+
script = os.path.join(os.environ["RUNNER_TEMP"], "paw", "messages.py")
|
|
1756
|
+
msgs = json.loads(subprocess.check_output(["python3", script, "dump", "wf_preview.r_"], text=True))
|
|
1757
|
+
head = ["m() {", ' case "$1" in']
|
|
1758
|
+
for key, text in msgs.items():
|
|
1759
|
+
head.append(" %s) _t=%s ;;" % (key, shlex.quote(text)))
|
|
1760
|
+
tail = r''' *) _t="$1" ;;
|
|
1761
|
+
esac
|
|
1762
|
+
shift
|
|
1763
|
+
while [ "$#" -gt 0 ]; do
|
|
1764
|
+
_v=$(printf '%s' "${1#*=}" | sed 's/[\\&|]/\\&/g')
|
|
1765
|
+
_t=$(printf '%s' "$_t" | sed "s|{${1%%=*}}|$_v|g")
|
|
1766
|
+
shift
|
|
1767
|
+
done
|
|
1768
|
+
printf '%s\n' "$_t"
|
|
1769
|
+
}'''
|
|
1770
|
+
print("PAW_REMOTE<<PAW_EOF")
|
|
1771
|
+
print("\n".join(head) + "\n" + tail)
|
|
1772
|
+
print("PAW_EOF")
|
|
1773
|
+
PY
|
|
1774
|
+
|
|
1775
|
+
- name: Check branch exists and extract info
|
|
1459
1776
|
id: branch_info
|
|
1460
1777
|
uses: actions/github-script@v9
|
|
1461
1778
|
with:
|
|
1462
1779
|
script: |
|
|
1780
|
+
const M = JSON.parse(process.env.PAW_MSG);
|
|
1781
|
+
const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
|
|
1782
|
+
|
|
1463
1783
|
const branchName = '${{ needs.check-command.outputs.custom_branch }}';
|
|
1464
1784
|
const issueNumber = context.issue.number;
|
|
1465
1785
|
|
|
1466
|
-
console.log(
|
|
1786
|
+
console.log(m('wf_preview.log_check_branch', {branchName}));
|
|
1467
1787
|
|
|
1468
|
-
//
|
|
1788
|
+
// Check whether the branch exists
|
|
1469
1789
|
try {
|
|
1470
1790
|
await github.rest.repos.getBranch({
|
|
1471
1791
|
owner: context.repo.owner,
|
|
1472
1792
|
repo: context.repo.repo,
|
|
1473
1793
|
branch: branchName
|
|
1474
1794
|
});
|
|
1475
|
-
console.log(
|
|
1795
|
+
console.log(m('wf_preview.log_branch_exists', {branchName}));
|
|
1476
1796
|
} catch (error) {
|
|
1477
1797
|
if (error.status === 404) {
|
|
1478
|
-
console.log(
|
|
1798
|
+
console.log(m('wf_preview.log_branch_missing', {branchName}));
|
|
1479
1799
|
|
|
1480
|
-
//
|
|
1800
|
+
// Post the error comment
|
|
1481
1801
|
const body = [
|
|
1482
|
-
'
|
|
1802
|
+
m('wf_preview.title_branch_missing'),
|
|
1483
1803
|
'',
|
|
1484
|
-
'
|
|
1804
|
+
m('wf_preview.tbl_items'),
|
|
1485
1805
|
'|------|-----|',
|
|
1486
|
-
|
|
1806
|
+
m('wf_preview.env_branch', {branchName}),
|
|
1487
1807
|
'',
|
|
1488
|
-
'
|
|
1489
|
-
'
|
|
1490
|
-
'
|
|
1491
|
-
'
|
|
1808
|
+
m('wf_preview.sec_check'),
|
|
1809
|
+
m('wf_preview.chk_name_1'),
|
|
1810
|
+
m('wf_preview.chk_pushed_2'),
|
|
1811
|
+
m('wf_preview.chk_deleted_3'),
|
|
1492
1812
|
'',
|
|
1493
1813
|
'---',
|
|
1494
|
-
'
|
|
1814
|
+
m('wf_preview.footer_generic')
|
|
1495
1815
|
].join('\n');
|
|
1496
1816
|
|
|
1497
1817
|
await github.rest.issues.createComment({
|
|
@@ -1501,22 +1821,22 @@ jobs:
|
|
|
1501
1821
|
body: body
|
|
1502
1822
|
});
|
|
1503
1823
|
|
|
1504
|
-
core.setFailed('
|
|
1824
|
+
core.setFailed(m('wf_preview.fail_branch_missing'));
|
|
1505
1825
|
return;
|
|
1506
1826
|
}
|
|
1507
1827
|
throw error;
|
|
1508
1828
|
}
|
|
1509
1829
|
|
|
1510
|
-
//
|
|
1830
|
+
// Extract the issue number from the branch name (#number pattern)
|
|
1511
1831
|
const issueMatch = branchName.match(/#(\d+)/);
|
|
1512
1832
|
|
|
1513
1833
|
if (issueMatch) {
|
|
1514
|
-
//
|
|
1834
|
+
// Number extracted
|
|
1515
1835
|
core.setOutput('container_number', issueMatch[1]);
|
|
1516
1836
|
core.setOutput('use_hash', 'false');
|
|
1517
|
-
console.log(
|
|
1837
|
+
console.log(m('wf_preview.log_issue_number', {n: issueMatch[1]}));
|
|
1518
1838
|
} else {
|
|
1519
|
-
//
|
|
1839
|
+
// Generate a hash
|
|
1520
1840
|
let hash = 0;
|
|
1521
1841
|
for (let i = 0; i < branchName.length; i++) {
|
|
1522
1842
|
const char = branchName.charCodeAt(i);
|
|
@@ -1526,44 +1846,47 @@ jobs:
|
|
|
1526
1846
|
const hashStr = Math.abs(hash).toString(16).slice(-6).padStart(6, '0');
|
|
1527
1847
|
core.setOutput('container_suffix', `custom-${hashStr}`);
|
|
1528
1848
|
core.setOutput('use_hash', 'true');
|
|
1529
|
-
console.log(
|
|
1849
|
+
console.log(m('wf_preview.log_hash', {hashStr}));
|
|
1530
1850
|
}
|
|
1531
1851
|
|
|
1532
1852
|
core.setOutput('branch_name', branchName);
|
|
1533
1853
|
core.setOutput('branch_exists', 'true');
|
|
1534
1854
|
|
|
1535
|
-
- name:
|
|
1855
|
+
- name: Get latest commit SHA of the branch
|
|
1536
1856
|
if: steps.branch_info.outputs.branch_exists == 'true'
|
|
1537
1857
|
id: get_sha
|
|
1538
1858
|
run: |
|
|
1539
1859
|
SHA=$(git ls-remote https://github.com/${{ github.repository }}.git refs/heads/${{ needs.check-command.outputs.custom_branch }} | cut -c1-7)
|
|
1540
1860
|
echo "sha=$SHA" >> $GITHUB_OUTPUT
|
|
1541
1861
|
|
|
1542
|
-
- name:
|
|
1862
|
+
- name: Create progress comment
|
|
1543
1863
|
if: steps.branch_info.outputs.branch_exists == 'true'
|
|
1544
1864
|
id: progress
|
|
1545
1865
|
uses: actions/github-script@v9
|
|
1546
1866
|
with:
|
|
1547
1867
|
script: |
|
|
1868
|
+
const M = JSON.parse(process.env.PAW_MSG);
|
|
1869
|
+
const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
|
|
1870
|
+
|
|
1548
1871
|
const issueNumber = context.issue.number;
|
|
1549
1872
|
const branchName = '${{ needs.check-command.outputs.custom_branch }}';
|
|
1550
1873
|
const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
|
1551
1874
|
const startTime = Date.now();
|
|
1552
1875
|
|
|
1553
1876
|
const body = [
|
|
1554
|
-
'
|
|
1877
|
+
m('wf_preview.title_custom_building'),
|
|
1555
1878
|
'',
|
|
1556
|
-
|
|
1879
|
+
m('wf_preview.line_branch', {branchName}),
|
|
1557
1880
|
'',
|
|
1558
|
-
'
|
|
1881
|
+
m('wf_preview.tbl_steps'),
|
|
1559
1882
|
'|------|------|----------|',
|
|
1560
|
-
'
|
|
1561
|
-
'
|
|
1883
|
+
m('wf_preview.row_docker_progress'),
|
|
1884
|
+
m('wf_preview.row_deploy_wait'),
|
|
1562
1885
|
'',
|
|
1563
|
-
|
|
1886
|
+
m('wf_preview.link_logs', {runUrl}),
|
|
1564
1887
|
'',
|
|
1565
1888
|
'---',
|
|
1566
|
-
'
|
|
1889
|
+
m('wf_preview.footer_progress')
|
|
1567
1890
|
].join('\n');
|
|
1568
1891
|
|
|
1569
1892
|
const { data: comment } = await github.rest.issues.createComment({
|
|
@@ -1577,14 +1900,14 @@ jobs:
|
|
|
1577
1900
|
core.setOutput('start_time', startTime);
|
|
1578
1901
|
core.setOutput('docker_start', startTime);
|
|
1579
1902
|
|
|
1580
|
-
- name:
|
|
1903
|
+
- name: Checkout code
|
|
1581
1904
|
if: steps.branch_info.outputs.branch_exists == 'true'
|
|
1582
1905
|
uses: actions/checkout@v7
|
|
1583
1906
|
with:
|
|
1584
1907
|
ref: ${{ needs.check-command.outputs.custom_branch }}
|
|
1585
1908
|
|
|
1586
|
-
#
|
|
1587
|
-
- name:
|
|
1909
|
+
# If a required secret or the Dockerfile is missing, report the missing items and stop instead of surfacing a third-party error such as a docker login failure
|
|
1910
|
+
- name: Deploy pre-check (Secrets and Dockerfile)
|
|
1588
1911
|
if: steps.branch_info.outputs.branch_exists == 'true'
|
|
1589
1912
|
working-directory: ${{ env.PROJECT_PATH }}
|
|
1590
1913
|
env:
|
|
@@ -1596,11 +1919,13 @@ jobs:
|
|
|
1596
1919
|
SSH_KEY: ${{ secrets.SSH_KEY }}
|
|
1597
1920
|
DOCKERFILE: ${{ env.DOCKERFILE_PATH }}
|
|
1598
1921
|
run: |
|
|
1922
|
+
m() { python3 "$RUNNER_TEMP/paw/messages.py" get "$@"; }
|
|
1923
|
+
|
|
1599
1924
|
MISSING=""
|
|
1600
1925
|
for NAME in DOCKERHUB_USERNAME DOCKERHUB_TOKEN SERVER_HOST SERVER_USER; do
|
|
1601
1926
|
[ -n "${!NAME}" ] || MISSING="$MISSING $NAME"
|
|
1602
1927
|
done
|
|
1603
|
-
# SSH_AUTH_METHOD=key
|
|
1928
|
+
# SSH_KEY is required when SSH_AUTH_METHOD=key, otherwise SERVER_PASSWORD
|
|
1604
1929
|
if [ "${SSH_AUTH_METHOD:-password}" = "key" ]; then
|
|
1605
1930
|
[ -n "$SSH_KEY" ] || MISSING="$MISSING SSH_KEY"
|
|
1606
1931
|
else
|
|
@@ -1608,19 +1933,19 @@ jobs:
|
|
|
1608
1933
|
fi
|
|
1609
1934
|
FAILED=false
|
|
1610
1935
|
if [ -n "$MISSING" ]; then
|
|
1611
|
-
echo "::error title
|
|
1936
|
+
echo "::error title=$(m wf_preview.precheck_secret_title)::$(m wf_preview.precheck_secret_body missing="$MISSING")"
|
|
1612
1937
|
FAILED=true
|
|
1613
1938
|
fi
|
|
1614
1939
|
if [ ! -f "$DOCKERFILE" ]; then
|
|
1615
|
-
echo "::error title=
|
|
1940
|
+
echo "::error title=$(m wf_preview.precheck_dockerfile_title)::$(m wf_preview.precheck_dockerfile_body path="${PROJECT_PATH}/${DOCKERFILE#./}")"
|
|
1616
1941
|
FAILED=true
|
|
1617
1942
|
fi
|
|
1618
1943
|
if [ "$FAILED" = "true" ]; then
|
|
1619
1944
|
exit 1
|
|
1620
1945
|
fi
|
|
1621
|
-
|
|
1946
|
+
m wf_preview.precheck_ok
|
|
1622
1947
|
|
|
1623
|
-
- name: "[
|
|
1948
|
+
- name: "[Required] Create .env file"
|
|
1624
1949
|
working-directory: ${{ env.PROJECT_PATH }}
|
|
1625
1950
|
if: steps.branch_info.outputs.branch_exists == 'true'
|
|
1626
1951
|
run: |
|
|
@@ -1628,18 +1953,18 @@ jobs:
|
|
|
1628
1953
|
${{ secrets.ENV_FILE }}
|
|
1629
1954
|
EOF
|
|
1630
1955
|
|
|
1631
|
-
- name: Docker
|
|
1956
|
+
- name: Log in to Docker Hub
|
|
1632
1957
|
if: steps.branch_info.outputs.branch_exists == 'true'
|
|
1633
1958
|
uses: docker/login-action@v3
|
|
1634
1959
|
with:
|
|
1635
1960
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
1636
1961
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
1637
1962
|
|
|
1638
|
-
- name: Docker Buildx
|
|
1963
|
+
- name: Set up Docker Buildx
|
|
1639
1964
|
if: steps.branch_info.outputs.branch_exists == 'true'
|
|
1640
1965
|
uses: docker/setup-buildx-action@v3
|
|
1641
1966
|
|
|
1642
|
-
- name:
|
|
1967
|
+
- name: Resolve container name
|
|
1643
1968
|
if: steps.branch_info.outputs.branch_exists == 'true'
|
|
1644
1969
|
id: container
|
|
1645
1970
|
run: |
|
|
@@ -1651,7 +1976,7 @@ jobs:
|
|
|
1651
1976
|
echo "suffix=$SUFFIX" >> $GITHUB_OUTPUT
|
|
1652
1977
|
echo "name=${{ env.PROJECT_PATH != '.' && format('{0}-python', env.PROJECT_NAME) || env.PROJECT_NAME }}-${SUFFIX}" >> $GITHUB_OUTPUT
|
|
1653
1978
|
|
|
1654
|
-
- name:
|
|
1979
|
+
- name: Build & push Docker image
|
|
1655
1980
|
if: steps.branch_info.outputs.branch_exists == 'true'
|
|
1656
1981
|
uses: docker/build-push-action@v5
|
|
1657
1982
|
with:
|
|
@@ -1662,12 +1987,15 @@ jobs:
|
|
|
1662
1987
|
cache-from: type=gha
|
|
1663
1988
|
cache-to: type=gha,mode=max
|
|
1664
1989
|
|
|
1665
|
-
- name:
|
|
1990
|
+
- name: Progress - Docker done
|
|
1666
1991
|
if: steps.branch_info.outputs.branch_exists == 'true'
|
|
1667
1992
|
id: docker_progress
|
|
1668
1993
|
uses: actions/github-script@v9
|
|
1669
1994
|
with:
|
|
1670
1995
|
script: |
|
|
1996
|
+
const M = JSON.parse(process.env.PAW_MSG);
|
|
1997
|
+
const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
|
|
1998
|
+
|
|
1671
1999
|
const commentId = ${{ steps.progress.outputs.comment_id }};
|
|
1672
2000
|
const branchName = '${{ needs.check-command.outputs.custom_branch }}';
|
|
1673
2001
|
const dockerStart = ${{ steps.progress.outputs.docker_start }};
|
|
@@ -1675,23 +2003,23 @@ jobs:
|
|
|
1675
2003
|
const dockerElapsed = now - dockerStart;
|
|
1676
2004
|
const minutes = Math.floor(dockerElapsed / 60000);
|
|
1677
2005
|
const seconds = Math.floor((dockerElapsed % 60000) / 1000);
|
|
1678
|
-
const dockerDuration = minutes > 0 ?
|
|
2006
|
+
const dockerDuration = minutes > 0 ? m('wf_preview.dur_min_sec', {minutes, seconds}) : m('wf_preview.dur_sec', {seconds});
|
|
1679
2007
|
const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
|
1680
2008
|
|
|
1681
2009
|
const body = [
|
|
1682
|
-
'
|
|
2010
|
+
m('wf_preview.title_custom_building'),
|
|
1683
2011
|
'',
|
|
1684
|
-
|
|
2012
|
+
m('wf_preview.line_branch', {branchName}),
|
|
1685
2013
|
'',
|
|
1686
|
-
'
|
|
2014
|
+
m('wf_preview.tbl_steps'),
|
|
1687
2015
|
'|------|------|----------|',
|
|
1688
|
-
|
|
1689
|
-
'
|
|
2016
|
+
m('wf_preview.row_docker_done', {dockerDuration}),
|
|
2017
|
+
m('wf_preview.row_deploy_progress'),
|
|
1690
2018
|
'',
|
|
1691
|
-
|
|
2019
|
+
m('wf_preview.link_logs', {runUrl}),
|
|
1692
2020
|
'',
|
|
1693
2021
|
'---',
|
|
1694
|
-
'
|
|
2022
|
+
m('wf_preview.footer_progress')
|
|
1695
2023
|
].join('\n');
|
|
1696
2024
|
|
|
1697
2025
|
await github.rest.issues.updateComment({
|
|
@@ -1704,7 +2032,7 @@ jobs:
|
|
|
1704
2032
|
core.setOutput('docker_duration', dockerDuration);
|
|
1705
2033
|
core.setOutput('deploy_start', now);
|
|
1706
2034
|
|
|
1707
|
-
- name:
|
|
2035
|
+
- name: Deploy to server
|
|
1708
2036
|
if: steps.branch_info.outputs.branch_exists == 'true'
|
|
1709
2037
|
uses: appleboy/ssh-action@v1.0.3
|
|
1710
2038
|
env:
|
|
@@ -1717,19 +2045,21 @@ jobs:
|
|
|
1717
2045
|
port: ${{ env.SSH_PORT }}
|
|
1718
2046
|
envs: SSH_AUTH_METHOD
|
|
1719
2047
|
script: |
|
|
2048
|
+
${{ env.PAW_REMOTE }}
|
|
2049
|
+
|
|
1720
2050
|
set -e
|
|
1721
2051
|
|
|
1722
2052
|
export PATH=$PATH:/usr/local/bin
|
|
1723
2053
|
export PW="${{ secrets.SERVER_PASSWORD }}"
|
|
1724
2054
|
|
|
1725
|
-
#
|
|
2055
|
+
# sudo wrapper that depends on the SSH auth method
|
|
1726
2056
|
SSH_AUTH_METHOD="${SSH_AUTH_METHOD:-password}"
|
|
1727
2057
|
if [ "${SSH_AUTH_METHOD}" = "key" ]; then
|
|
1728
2058
|
SUDO() { sudo "$@"; }
|
|
1729
2059
|
else
|
|
1730
2060
|
SUDO() { echo "$PW" | sudo -S "$@"; }
|
|
1731
2061
|
fi
|
|
1732
|
-
|
|
2062
|
+
m wf_preview.r_ssh_auth ssh_auth_method="${SSH_AUTH_METHOD}"
|
|
1733
2063
|
|
|
1734
2064
|
PROJECT_NAME="${{ env.PROJECT_PATH != '.' && format('{0}-python', env.PROJECT_NAME) || env.PROJECT_NAME }}"
|
|
1735
2065
|
CONTAINER_NAME="${{ steps.container.outputs.name }}"
|
|
@@ -1740,17 +2070,17 @@ jobs:
|
|
|
1740
2070
|
TRAEFIK_NETWORK="${{ env.TRAEFIK_NETWORK }}"
|
|
1741
2071
|
|
|
1742
2072
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1743
|
-
|
|
2073
|
+
m wf_preview.r_deploy_head_custom
|
|
1744
2074
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
2075
|
+
m wf_preview.r_project_name project_name="${PROJECT_NAME}"
|
|
2076
|
+
m wf_preview.r_container_name container_name="${CONTAINER_NAME}"
|
|
2077
|
+
m wf_preview.r_domain domain="${DOMAIN}"
|
|
1748
2078
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1749
2079
|
|
|
1750
|
-
|
|
2080
|
+
m wf_preview.r_image_pulling
|
|
1751
2081
|
SUDO docker pull "${IMAGE}"
|
|
1752
2082
|
|
|
1753
|
-
|
|
2083
|
+
m wf_preview.r_existing_cleanup
|
|
1754
2084
|
SUDO docker rm -f "${CONTAINER_NAME}" 2>/dev/null || true
|
|
1755
2085
|
|
|
1756
2086
|
VOLUME_OPTS="-v /etc/localtime:/etc/localtime:ro"
|
|
@@ -1758,7 +2088,7 @@ jobs:
|
|
|
1758
2088
|
VOLUME_OPTS="$VOLUME_OPTS -v ${{ env.PROJECT_TARGET_DIR }}:${{ env.PROJECT_MNT_DIR }}"
|
|
1759
2089
|
fi
|
|
1760
2090
|
|
|
1761
|
-
|
|
2091
|
+
m wf_preview.r_container_starting
|
|
1762
2092
|
SUDO docker run -d \
|
|
1763
2093
|
--name "${CONTAINER_NAME}" \
|
|
1764
2094
|
--network "${TRAEFIK_NETWORK}" \
|
|
@@ -1772,7 +2102,7 @@ jobs:
|
|
|
1772
2102
|
"${IMAGE}"
|
|
1773
2103
|
|
|
1774
2104
|
echo ""
|
|
1775
|
-
|
|
2105
|
+
m wf_preview.r_hc_start
|
|
1776
2106
|
HEALTH_PATH="${{ env.HEALTH_CHECK_PATH }}"
|
|
1777
2107
|
LOG_PATTERN="${{ env.HEALTH_CHECK_LOG_PATTERN }}"
|
|
1778
2108
|
MAX_RETRIES=24
|
|
@@ -1787,9 +2117,9 @@ jobs:
|
|
|
1787
2117
|
STATUS=$(SUDO docker inspect --format='{{.State.Status}}' "${CONTAINER_NAME}" 2>/dev/null || echo "not_found")
|
|
1788
2118
|
|
|
1789
2119
|
if [ "$STATUS" = "exited" ]; then
|
|
1790
|
-
|
|
2120
|
+
m wf_preview.r_container_exited
|
|
1791
2121
|
echo ""
|
|
1792
|
-
|
|
2122
|
+
m wf_preview.r_container_logs
|
|
1793
2123
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1794
2124
|
SUDO docker logs --tail 100 "${CONTAINER_NAME}"
|
|
1795
2125
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
@@ -1801,7 +2131,7 @@ jobs:
|
|
|
1801
2131
|
HEALTH=$(SUDO docker exec "${CONTAINER_NAME}" curl -sf "http://localhost:${INTERNAL_PORT}${HEALTH_PATH}" 2>/dev/null || echo "")
|
|
1802
2132
|
|
|
1803
2133
|
if [ -n "$HEALTH" ]; then
|
|
1804
|
-
|
|
2134
|
+
m wf_preview.r_hc_ok_http health_path="${HEALTH_PATH}"
|
|
1805
2135
|
HEALTH_CHECK_PASSED=true
|
|
1806
2136
|
HEALTH_CHECK_METHOD="HTTP"
|
|
1807
2137
|
break
|
|
@@ -1812,7 +2142,7 @@ jobs:
|
|
|
1812
2142
|
STARTED=$(SUDO docker logs --tail 50 "${CONTAINER_NAME}" 2>&1 | grep -E "$LOG_PATTERN" || echo "")
|
|
1813
2143
|
|
|
1814
2144
|
if [ -n "$STARTED" ]; then
|
|
1815
|
-
|
|
2145
|
+
m wf_preview.r_hc_ok_log
|
|
1816
2146
|
echo " $STARTED"
|
|
1817
2147
|
HEALTH_CHECK_PASSED=true
|
|
1818
2148
|
HEALTH_CHECK_METHOD="Log"
|
|
@@ -1821,14 +2151,14 @@ jobs:
|
|
|
1821
2151
|
fi
|
|
1822
2152
|
fi
|
|
1823
2153
|
|
|
1824
|
-
|
|
2154
|
+
m wf_preview.r_hc_waiting retry_count="$RETRY_COUNT" max_retries="$MAX_RETRIES" status="$STATUS"
|
|
1825
2155
|
done
|
|
1826
2156
|
|
|
1827
2157
|
if [ "$HEALTH_CHECK_PASSED" = "false" ]; then
|
|
1828
2158
|
echo ""
|
|
1829
|
-
|
|
2159
|
+
m wf_preview.r_hc_timeout
|
|
1830
2160
|
echo ""
|
|
1831
|
-
|
|
2161
|
+
m wf_preview.r_container_logs
|
|
1832
2162
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1833
2163
|
SUDO docker logs --tail 100 "${CONTAINER_NAME}"
|
|
1834
2164
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
@@ -1836,14 +2166,17 @@ jobs:
|
|
|
1836
2166
|
fi
|
|
1837
2167
|
|
|
1838
2168
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1839
|
-
|
|
2169
|
+
m wf_preview.r_hc_done health_check_method="${HEALTH_CHECK_METHOD}"
|
|
1840
2170
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1841
2171
|
|
|
1842
|
-
- name:
|
|
2172
|
+
- name: Deploy complete comment
|
|
1843
2173
|
if: steps.branch_info.outputs.branch_exists == 'true'
|
|
1844
2174
|
uses: actions/github-script@v9
|
|
1845
2175
|
with:
|
|
1846
2176
|
script: |
|
|
2177
|
+
const M = JSON.parse(process.env.PAW_MSG);
|
|
2178
|
+
const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
|
|
2179
|
+
|
|
1847
2180
|
const commentId = ${{ steps.progress.outputs.comment_id }};
|
|
1848
2181
|
const branchName = '${{ needs.check-command.outputs.custom_branch }}';
|
|
1849
2182
|
const dockerDuration = '${{ steps.docker_progress.outputs.docker_duration }}';
|
|
@@ -1852,7 +2185,7 @@ jobs:
|
|
|
1852
2185
|
const deployElapsed = now - deployStart;
|
|
1853
2186
|
const minutes = Math.floor(deployElapsed / 60000);
|
|
1854
2187
|
const seconds = Math.floor((deployElapsed % 60000) / 1000);
|
|
1855
|
-
const deployDuration = minutes > 0 ?
|
|
2188
|
+
const deployDuration = minutes > 0 ? m('wf_preview.dur_min_sec', {minutes, seconds}) : m('wf_preview.dur_sec', {seconds});
|
|
1856
2189
|
|
|
1857
2190
|
const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-python', env.PROJECT_NAME) || env.PROJECT_NAME }}';
|
|
1858
2191
|
const domainSuffix = '${{ env.PREVIEW_DOMAIN_SUFFIX }}';
|
|
@@ -1863,41 +2196,41 @@ jobs:
|
|
|
1863
2196
|
const previewUrl = `http://${domain}:${previewPort}`;
|
|
1864
2197
|
const sha = '${{ steps.get_sha.outputs.sha }}';
|
|
1865
2198
|
|
|
1866
|
-
//
|
|
2199
|
+
// Build the preview environment table
|
|
1867
2200
|
const envRows = [
|
|
1868
2201
|
`| **Preview URL** | ${previewUrl} |`,
|
|
1869
2202
|
];
|
|
1870
2203
|
if (apiDocsPath) {
|
|
1871
2204
|
envRows.push(`| **API Docs** | ${previewUrl}${apiDocsPath} |`);
|
|
1872
2205
|
}
|
|
1873
|
-
envRows.push(
|
|
1874
|
-
envRows.push(
|
|
1875
|
-
envRows.push(
|
|
2206
|
+
envRows.push(m('wf_preview.env_container_name', {containerName}));
|
|
2207
|
+
envRows.push(m('wf_preview.env_branch', {branchName}));
|
|
2208
|
+
envRows.push(m('wf_preview.env_commit', {sha}));
|
|
1876
2209
|
|
|
1877
2210
|
const body = [
|
|
1878
|
-
'
|
|
2211
|
+
m('wf_preview.title_custom_deployed'),
|
|
1879
2212
|
'',
|
|
1880
|
-
|
|
2213
|
+
m('wf_preview.line_branch', {branchName}),
|
|
1881
2214
|
'',
|
|
1882
|
-
'
|
|
2215
|
+
m('wf_preview.tbl_steps'),
|
|
1883
2216
|
'|------|------|----------|',
|
|
1884
|
-
|
|
1885
|
-
|
|
2217
|
+
m('wf_preview.row_docker_done', {dockerDuration}),
|
|
2218
|
+
m('wf_preview.row_deploy_done', {deployDuration}),
|
|
1886
2219
|
'',
|
|
1887
|
-
'
|
|
1888
|
-
'
|
|
2220
|
+
m('wf_preview.sec_env'),
|
|
2221
|
+
m('wf_preview.tbl_items'),
|
|
1889
2222
|
'|------|-----|',
|
|
1890
2223
|
...envRows,
|
|
1891
2224
|
'',
|
|
1892
|
-
'
|
|
1893
|
-
'
|
|
2225
|
+
m('wf_preview.sec_commands'),
|
|
2226
|
+
m('wf_preview.tbl_commands'),
|
|
1894
2227
|
'|--------|------|',
|
|
1895
|
-
'
|
|
1896
|
-
'
|
|
1897
|
-
'
|
|
2228
|
+
m('wf_preview.cmd_build'),
|
|
2229
|
+
m('wf_preview.cmd_destroy'),
|
|
2230
|
+
m('wf_preview.cmd_status'),
|
|
1898
2231
|
'',
|
|
1899
2232
|
'<details>',
|
|
1900
|
-
'
|
|
2233
|
+
m('wf_preview.cmd_advanced'),
|
|
1901
2234
|
'',
|
|
1902
2235
|
'```',
|
|
1903
2236
|
`/wizard server build ${branchName}`,
|
|
@@ -1907,7 +2240,7 @@ jobs:
|
|
|
1907
2240
|
'</details>',
|
|
1908
2241
|
'',
|
|
1909
2242
|
'---',
|
|
1910
|
-
'
|
|
2243
|
+
m('wf_preview.footer_custom')
|
|
1911
2244
|
].join('\n');
|
|
1912
2245
|
|
|
1913
2246
|
await github.rest.issues.updateComment({
|
|
@@ -1917,11 +2250,14 @@ jobs:
|
|
|
1917
2250
|
body: body
|
|
1918
2251
|
});
|
|
1919
2252
|
|
|
1920
|
-
- name:
|
|
2253
|
+
- name: Build/deploy failure comment
|
|
1921
2254
|
if: failure() && steps.branch_info.outputs.branch_exists == 'true'
|
|
1922
2255
|
uses: actions/github-script@v9
|
|
1923
2256
|
with:
|
|
1924
2257
|
script: |
|
|
2258
|
+
const M = JSON.parse(process.env.PAW_MSG);
|
|
2259
|
+
const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
|
|
2260
|
+
|
|
1925
2261
|
const commentId = ${{ steps.progress.outputs.comment_id || 0 }};
|
|
1926
2262
|
const branchName = '${{ needs.check-command.outputs.custom_branch }}';
|
|
1927
2263
|
const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
|
@@ -1932,42 +2268,42 @@ jobs:
|
|
|
1932
2268
|
let dockerTime = '-';
|
|
1933
2269
|
|
|
1934
2270
|
if (!commentId) {
|
|
1935
|
-
dockerStatus = '
|
|
2271
|
+
dockerStatus = m('wf_preview.status_init_failed');
|
|
1936
2272
|
deployStatus = '-';
|
|
1937
2273
|
} else if (!dockerDuration || dockerDuration === '') {
|
|
1938
|
-
dockerStatus = '
|
|
1939
|
-
deployStatus = '
|
|
2274
|
+
dockerStatus = m('wf_preview.status_failed');
|
|
2275
|
+
deployStatus = m('wf_preview.status_waiting');
|
|
1940
2276
|
} else {
|
|
1941
|
-
dockerStatus = '
|
|
1942
|
-
deployStatus = '
|
|
2277
|
+
dockerStatus = m('wf_preview.status_done');
|
|
2278
|
+
deployStatus = m('wf_preview.status_failed');
|
|
1943
2279
|
dockerTime = dockerDuration;
|
|
1944
2280
|
}
|
|
1945
2281
|
|
|
1946
2282
|
const body = [
|
|
1947
|
-
'
|
|
2283
|
+
m('wf_preview.title_custom_failed'),
|
|
1948
2284
|
'',
|
|
1949
|
-
|
|
2285
|
+
m('wf_preview.line_branch', {branchName}),
|
|
1950
2286
|
'',
|
|
1951
|
-
'
|
|
2287
|
+
m('wf_preview.tbl_steps'),
|
|
1952
2288
|
'|------|------|----------|',
|
|
1953
|
-
|
|
1954
|
-
|
|
2289
|
+
m('wf_preview.row_docker_status', {dockerStatus, dockerTime}),
|
|
2290
|
+
m('wf_preview.row_deploy_status', {deployStatus}),
|
|
1955
2291
|
'',
|
|
1956
|
-
|
|
2292
|
+
m('wf_preview.link_build_logs', {runUrl}),
|
|
1957
2293
|
'',
|
|
1958
|
-
'
|
|
1959
|
-
'
|
|
1960
|
-
'
|
|
1961
|
-
'
|
|
1962
|
-
'
|
|
1963
|
-
'
|
|
2294
|
+
m('wf_preview.sec_causes'),
|
|
2295
|
+
m('wf_preview.hint_secrets'),
|
|
2296
|
+
m('wf_preview.hint_docker_python'),
|
|
2297
|
+
m('wf_preview.hint_start_python'),
|
|
2298
|
+
m('wf_preview.hint_health_timeout'),
|
|
2299
|
+
m('wf_preview.hint_env_missing'),
|
|
1964
2300
|
'',
|
|
1965
|
-
'
|
|
1966
|
-
'
|
|
1967
|
-
|
|
2301
|
+
m('wf_preview.sec_next'),
|
|
2302
|
+
m('wf_preview.next_logs'),
|
|
2303
|
+
m('wf_preview.next_retry_branch', {branchName}),
|
|
1968
2304
|
'',
|
|
1969
2305
|
'---',
|
|
1970
|
-
'
|
|
2306
|
+
m('wf_preview.footer_custom')
|
|
1971
2307
|
].join('\n');
|
|
1972
2308
|
|
|
1973
2309
|
if (commentId) {
|
|
@@ -1986,9 +2322,9 @@ jobs:
|
|
|
1986
2322
|
});
|
|
1987
2323
|
}
|
|
1988
2324
|
|
|
1989
|
-
# Job 6:
|
|
2325
|
+
# Job 6: destroy custom branch preview
|
|
1990
2326
|
destroy-preview-custom-branch:
|
|
1991
|
-
name:
|
|
2327
|
+
name: Destroy preview (Custom Branch)
|
|
1992
2328
|
needs: check-command
|
|
1993
2329
|
if: |
|
|
1994
2330
|
needs.check-command.outputs.is_valid == 'true' &&
|
|
@@ -1996,23 +2332,70 @@ jobs:
|
|
|
1996
2332
|
needs.check-command.outputs.is_custom_branch == 'true'
|
|
1997
2333
|
runs-on: ubuntu-latest
|
|
1998
2334
|
steps:
|
|
1999
|
-
|
|
2335
|
+
# Messages come from the same revision as this workflow (the PR merge commit on pull_request, otherwise the
|
|
2336
|
+
# default branch) so they exist even before the default branch has them, and before the PR branch is checked out
|
|
2337
|
+
- name: Checkout messages
|
|
2338
|
+
uses: actions/checkout@v7
|
|
2339
|
+
with:
|
|
2340
|
+
ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
|
|
2341
|
+
sparse-checkout: |
|
|
2342
|
+
.github/scripts/messages.py
|
|
2343
|
+
version.yml
|
|
2344
|
+
sparse-checkout-cone-mode: false
|
|
2345
|
+
|
|
2346
|
+
- name: Load messages
|
|
2347
|
+
run: |
|
|
2348
|
+
# Keep a copy outside the workspace: checking out the PR branch later replaces its files
|
|
2349
|
+
mkdir -p "$RUNNER_TEMP/paw"
|
|
2350
|
+
cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
|
|
2351
|
+
{
|
|
2352
|
+
echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
|
|
2353
|
+
echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
|
|
2354
|
+
} >> "$GITHUB_ENV"
|
|
2355
|
+
# The deploy script runs on the remote server, where messages.py does not exist:
|
|
2356
|
+
# ship the messages it needs as a small shell function (m KEY name=value ...)
|
|
2357
|
+
python3 - >> "$GITHUB_ENV" <<'PY'
|
|
2358
|
+
import json, os, shlex, subprocess
|
|
2359
|
+
script = os.path.join(os.environ["RUNNER_TEMP"], "paw", "messages.py")
|
|
2360
|
+
msgs = json.loads(subprocess.check_output(["python3", script, "dump", "wf_preview.r_"], text=True))
|
|
2361
|
+
head = ["m() {", ' case "$1" in']
|
|
2362
|
+
for key, text in msgs.items():
|
|
2363
|
+
head.append(" %s) _t=%s ;;" % (key, shlex.quote(text)))
|
|
2364
|
+
tail = r''' *) _t="$1" ;;
|
|
2365
|
+
esac
|
|
2366
|
+
shift
|
|
2367
|
+
while [ "$#" -gt 0 ]; do
|
|
2368
|
+
_v=$(printf '%s' "${1#*=}" | sed 's/[\\&|]/\\&/g')
|
|
2369
|
+
_t=$(printf '%s' "$_t" | sed "s|{${1%%=*}}|$_v|g")
|
|
2370
|
+
shift
|
|
2371
|
+
done
|
|
2372
|
+
printf '%s\n' "$_t"
|
|
2373
|
+
}'''
|
|
2374
|
+
print("PAW_REMOTE<<PAW_EOF")
|
|
2375
|
+
print("\n".join(head) + "\n" + tail)
|
|
2376
|
+
print("PAW_EOF")
|
|
2377
|
+
PY
|
|
2378
|
+
|
|
2379
|
+
- name: Extract container info from branch name
|
|
2000
2380
|
id: container_info
|
|
2001
2381
|
uses: actions/github-script@v9
|
|
2002
2382
|
with:
|
|
2003
2383
|
script: |
|
|
2384
|
+
const M = JSON.parse(process.env.PAW_MSG);
|
|
2385
|
+
const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
|
|
2386
|
+
|
|
2004
2387
|
const branchName = '${{ needs.check-command.outputs.custom_branch }}';
|
|
2005
2388
|
const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-python', env.PROJECT_NAME) || env.PROJECT_NAME }}';
|
|
2006
2389
|
|
|
2007
|
-
//
|
|
2390
|
+
// Extract the issue number from the branch name (#number pattern)
|
|
2008
2391
|
const issueMatch = branchName.match(/#(\d+)/);
|
|
2009
2392
|
|
|
2010
2393
|
let containerName;
|
|
2011
2394
|
if (issueMatch) {
|
|
2012
2395
|
containerName = `${projectName}-pr-${issueMatch[1]}`;
|
|
2013
|
-
console.log(
|
|
2396
|
+
console.log(m('wf_preview.log_issue_number', {n: issueMatch[1]}));
|
|
2014
2397
|
} else {
|
|
2015
|
-
//
|
|
2398
|
+
// Generate a hash
|
|
2016
2399
|
let hash = 0;
|
|
2017
2400
|
for (let i = 0; i < branchName.length; i++) {
|
|
2018
2401
|
const char = branchName.charCodeAt(i);
|
|
@@ -2021,13 +2404,13 @@ jobs:
|
|
|
2021
2404
|
}
|
|
2022
2405
|
const hashStr = Math.abs(hash).toString(16).slice(-6).padStart(6, '0');
|
|
2023
2406
|
containerName = `${projectName}-custom-${hashStr}`;
|
|
2024
|
-
console.log(
|
|
2407
|
+
console.log(m('wf_preview.log_hash', {hashStr}));
|
|
2025
2408
|
}
|
|
2026
2409
|
|
|
2027
2410
|
core.setOutput('container_name', containerName);
|
|
2028
2411
|
core.setOutput('branch_name', branchName);
|
|
2029
2412
|
|
|
2030
|
-
- name:
|
|
2413
|
+
- name: Remove container & image
|
|
2031
2414
|
uses: appleboy/ssh-action@v1.0.3
|
|
2032
2415
|
env:
|
|
2033
2416
|
SSH_AUTH_METHOD: ${{ env.SSH_AUTH_METHOD }}
|
|
@@ -2039,60 +2422,65 @@ jobs:
|
|
|
2039
2422
|
port: ${{ env.SSH_PORT }}
|
|
2040
2423
|
envs: SSH_AUTH_METHOD
|
|
2041
2424
|
script: |
|
|
2425
|
+
${{ env.PAW_REMOTE }}
|
|
2426
|
+
|
|
2042
2427
|
export PATH=$PATH:/usr/local/bin
|
|
2043
2428
|
export PW="${{ secrets.SERVER_PASSWORD }}"
|
|
2044
2429
|
|
|
2045
|
-
#
|
|
2430
|
+
# sudo wrapper that depends on the SSH auth method
|
|
2046
2431
|
SSH_AUTH_METHOD="${SSH_AUTH_METHOD:-password}"
|
|
2047
2432
|
if [ "${SSH_AUTH_METHOD}" = "key" ]; then
|
|
2048
2433
|
SUDO() { sudo "$@"; }
|
|
2049
2434
|
else
|
|
2050
2435
|
SUDO() { echo "$PW" | sudo -S "$@"; }
|
|
2051
2436
|
fi
|
|
2052
|
-
|
|
2437
|
+
m wf_preview.r_ssh_auth ssh_auth_method="${SSH_AUTH_METHOD}"
|
|
2053
2438
|
|
|
2054
2439
|
CONTAINER_NAME="${{ steps.container_info.outputs.container_name }}"
|
|
2055
2440
|
PROJECT_NAME="${{ env.PROJECT_PATH != '.' && format('{0}-python', env.PROJECT_NAME) || env.PROJECT_NAME }}"
|
|
2056
2441
|
|
|
2057
|
-
#
|
|
2442
|
+
# Extract the suffix from the container name
|
|
2058
2443
|
SUFFIX="${CONTAINER_NAME#${PROJECT_NAME}-}"
|
|
2059
2444
|
IMAGE="${{ secrets.DOCKERHUB_USERNAME }}/${PROJECT_NAME}:${SUFFIX}"
|
|
2060
2445
|
|
|
2061
2446
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
2062
|
-
|
|
2447
|
+
m wf_preview.r_destroy_head_custom
|
|
2063
2448
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
2064
|
-
|
|
2065
|
-
|
|
2449
|
+
m wf_preview.r_project_name project_name="${PROJECT_NAME}"
|
|
2450
|
+
m wf_preview.r_container_name container_name="${CONTAINER_NAME}"
|
|
2066
2451
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
2067
2452
|
|
|
2068
|
-
|
|
2453
|
+
m wf_preview.r_container_removing
|
|
2069
2454
|
SUDO docker rm -f "${CONTAINER_NAME}" 2>/dev/null || true
|
|
2070
2455
|
|
|
2071
|
-
|
|
2456
|
+
m wf_preview.r_image_removing
|
|
2072
2457
|
SUDO docker rmi "${IMAGE}" 2>/dev/null || true
|
|
2073
2458
|
|
|
2074
|
-
|
|
2459
|
+
m wf_preview.r_removed
|
|
2075
2460
|
|
|
2076
|
-
- name:
|
|
2461
|
+
- name: Destroy complete comment
|
|
2077
2462
|
uses: actions/github-script@v9
|
|
2078
2463
|
with:
|
|
2079
2464
|
script: |
|
|
2465
|
+
const M = JSON.parse(process.env.PAW_MSG);
|
|
2466
|
+
const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
|
|
2467
|
+
|
|
2080
2468
|
const branchName = '${{ needs.check-command.outputs.custom_branch }}';
|
|
2081
2469
|
const containerName = '${{ steps.container_info.outputs.container_name }}';
|
|
2082
2470
|
|
|
2083
2471
|
const body = [
|
|
2084
|
-
'
|
|
2472
|
+
m('wf_preview.title_custom_destroyed'),
|
|
2085
2473
|
'',
|
|
2086
|
-
'
|
|
2474
|
+
m('wf_preview.tbl_items'),
|
|
2087
2475
|
'|------|-----|',
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
'
|
|
2476
|
+
m('wf_preview.env_branch', {branchName}),
|
|
2477
|
+
m('wf_preview.env_container_name', {containerName}),
|
|
2478
|
+
m('wf_preview.env_status_deleted'),
|
|
2091
2479
|
'',
|
|
2092
|
-
|
|
2480
|
+
m('wf_preview.redeploy_hint_branch', {branchName}),
|
|
2093
2481
|
'',
|
|
2094
2482
|
'---',
|
|
2095
|
-
'
|
|
2483
|
+
m('wf_preview.footer_custom')
|
|
2096
2484
|
].join('\n');
|
|
2097
2485
|
|
|
2098
2486
|
await github.rest.issues.createComment({
|
|
@@ -2102,9 +2490,9 @@ jobs:
|
|
|
2102
2490
|
body: body
|
|
2103
2491
|
});
|
|
2104
2492
|
|
|
2105
|
-
# Job 7:
|
|
2493
|
+
# Job 7: custom branch preview status
|
|
2106
2494
|
check-status-custom-branch:
|
|
2107
|
-
name: Preview
|
|
2495
|
+
name: Preview status (Custom Branch)
|
|
2108
2496
|
needs: check-command
|
|
2109
2497
|
if: |
|
|
2110
2498
|
needs.check-command.outputs.is_valid == 'true' &&
|
|
@@ -2112,7 +2500,51 @@ jobs:
|
|
|
2112
2500
|
needs.check-command.outputs.is_custom_branch == 'true'
|
|
2113
2501
|
runs-on: ubuntu-latest
|
|
2114
2502
|
steps:
|
|
2115
|
-
|
|
2503
|
+
# Messages come from the same revision as this workflow (the PR merge commit on pull_request, otherwise the
|
|
2504
|
+
# default branch) so they exist even before the default branch has them, and before the PR branch is checked out
|
|
2505
|
+
- name: Checkout messages
|
|
2506
|
+
uses: actions/checkout@v7
|
|
2507
|
+
with:
|
|
2508
|
+
ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
|
|
2509
|
+
sparse-checkout: |
|
|
2510
|
+
.github/scripts/messages.py
|
|
2511
|
+
version.yml
|
|
2512
|
+
sparse-checkout-cone-mode: false
|
|
2513
|
+
|
|
2514
|
+
- name: Load messages
|
|
2515
|
+
run: |
|
|
2516
|
+
# Keep a copy outside the workspace: checking out the PR branch later replaces its files
|
|
2517
|
+
mkdir -p "$RUNNER_TEMP/paw"
|
|
2518
|
+
cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
|
|
2519
|
+
{
|
|
2520
|
+
echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
|
|
2521
|
+
echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
|
|
2522
|
+
} >> "$GITHUB_ENV"
|
|
2523
|
+
# The deploy script runs on the remote server, where messages.py does not exist:
|
|
2524
|
+
# ship the messages it needs as a small shell function (m KEY name=value ...)
|
|
2525
|
+
python3 - >> "$GITHUB_ENV" <<'PY'
|
|
2526
|
+
import json, os, shlex, subprocess
|
|
2527
|
+
script = os.path.join(os.environ["RUNNER_TEMP"], "paw", "messages.py")
|
|
2528
|
+
msgs = json.loads(subprocess.check_output(["python3", script, "dump", "wf_preview.r_"], text=True))
|
|
2529
|
+
head = ["m() {", ' case "$1" in']
|
|
2530
|
+
for key, text in msgs.items():
|
|
2531
|
+
head.append(" %s) _t=%s ;;" % (key, shlex.quote(text)))
|
|
2532
|
+
tail = r''' *) _t="$1" ;;
|
|
2533
|
+
esac
|
|
2534
|
+
shift
|
|
2535
|
+
while [ "$#" -gt 0 ]; do
|
|
2536
|
+
_v=$(printf '%s' "${1#*=}" | sed 's/[\\&|]/\\&/g')
|
|
2537
|
+
_t=$(printf '%s' "$_t" | sed "s|{${1%%=*}}|$_v|g")
|
|
2538
|
+
shift
|
|
2539
|
+
done
|
|
2540
|
+
printf '%s\n' "$_t"
|
|
2541
|
+
}'''
|
|
2542
|
+
print("PAW_REMOTE<<PAW_EOF")
|
|
2543
|
+
print("\n".join(head) + "\n" + tail)
|
|
2544
|
+
print("PAW_EOF")
|
|
2545
|
+
PY
|
|
2546
|
+
|
|
2547
|
+
- name: Extract container info from branch name
|
|
2116
2548
|
id: container_info
|
|
2117
2549
|
uses: actions/github-script@v9
|
|
2118
2550
|
with:
|
|
@@ -2120,14 +2552,14 @@ jobs:
|
|
|
2120
2552
|
const branchName = '${{ needs.check-command.outputs.custom_branch }}';
|
|
2121
2553
|
const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-python', env.PROJECT_NAME) || env.PROJECT_NAME }}';
|
|
2122
2554
|
|
|
2123
|
-
//
|
|
2555
|
+
// Extract the issue number from the branch name (#number pattern)
|
|
2124
2556
|
const issueMatch = branchName.match(/#(\d+)/);
|
|
2125
2557
|
|
|
2126
2558
|
let containerName;
|
|
2127
2559
|
if (issueMatch) {
|
|
2128
2560
|
containerName = `${projectName}-pr-${issueMatch[1]}`;
|
|
2129
2561
|
} else {
|
|
2130
|
-
//
|
|
2562
|
+
// Generate a hash
|
|
2131
2563
|
let hash = 0;
|
|
2132
2564
|
for (let i = 0; i < branchName.length; i++) {
|
|
2133
2565
|
const char = branchName.charCodeAt(i);
|
|
@@ -2141,7 +2573,7 @@ jobs:
|
|
|
2141
2573
|
core.setOutput('container_name', containerName);
|
|
2142
2574
|
core.setOutput('branch_name', branchName);
|
|
2143
2575
|
|
|
2144
|
-
- name:
|
|
2576
|
+
- name: Check container status
|
|
2145
2577
|
uses: appleboy/ssh-action@v1.0.3
|
|
2146
2578
|
id: check_running
|
|
2147
2579
|
continue-on-error: true
|
|
@@ -2152,28 +2584,33 @@ jobs:
|
|
|
2152
2584
|
key: ${{ secrets.SSH_KEY }}
|
|
2153
2585
|
port: ${{ env.SSH_PORT }}
|
|
2154
2586
|
script: |
|
|
2587
|
+
${{ env.PAW_REMOTE }}
|
|
2588
|
+
|
|
2155
2589
|
export PATH=$PATH:/usr/local/bin
|
|
2156
2590
|
|
|
2157
2591
|
CONTAINER_NAME="${{ steps.container_info.outputs.container_name }}"
|
|
2158
2592
|
|
|
2159
2593
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
2160
|
-
|
|
2594
|
+
m wf_preview.r_status_head_custom
|
|
2161
2595
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
2162
2596
|
|
|
2163
2597
|
if docker ps --filter "name=${CONTAINER_NAME}" --format "{{.Names}}" | grep -q "${CONTAINER_NAME}"; then
|
|
2164
2598
|
echo "STATUS=running"
|
|
2165
|
-
|
|
2599
|
+
m wf_preview.r_container_running
|
|
2166
2600
|
docker ps --filter "name=${CONTAINER_NAME}" --format "table {{.Names}}\t{{.Status}}\t{{.RunningFor}}"
|
|
2167
2601
|
else
|
|
2168
2602
|
echo "STATUS=not_found"
|
|
2169
|
-
|
|
2603
|
+
m wf_preview.r_container_absent
|
|
2170
2604
|
exit 1
|
|
2171
2605
|
fi
|
|
2172
2606
|
|
|
2173
|
-
- name:
|
|
2607
|
+
- name: Post status comment
|
|
2174
2608
|
uses: actions/github-script@v9
|
|
2175
2609
|
with:
|
|
2176
2610
|
script: |
|
|
2611
|
+
const M = JSON.parse(process.env.PAW_MSG);
|
|
2612
|
+
const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
|
|
2613
|
+
|
|
2177
2614
|
const branchName = '${{ needs.check-command.outputs.custom_branch }}';
|
|
2178
2615
|
const containerName = '${{ steps.container_info.outputs.container_name }}';
|
|
2179
2616
|
const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-python', env.PROJECT_NAME) || env.PROJECT_NAME }}';
|
|
@@ -2186,38 +2623,38 @@ jobs:
|
|
|
2186
2623
|
let body;
|
|
2187
2624
|
if (isRunning) {
|
|
2188
2625
|
body = [
|
|
2189
|
-
'
|
|
2626
|
+
m('wf_preview.title_custom_running'),
|
|
2190
2627
|
'',
|
|
2191
|
-
'
|
|
2628
|
+
m('wf_preview.tbl_items'),
|
|
2192
2629
|
'|------|-----|',
|
|
2193
|
-
|
|
2630
|
+
m('wf_preview.env_branch', {branchName}),
|
|
2194
2631
|
`| **Preview URL** | ${previewUrl} |`,
|
|
2195
|
-
|
|
2196
|
-
'
|
|
2632
|
+
m('wf_preview.env_container_name', {containerName}),
|
|
2633
|
+
m('wf_preview.env_status_running'),
|
|
2197
2634
|
'',
|
|
2198
|
-
'
|
|
2199
|
-
'
|
|
2635
|
+
m('wf_preview.sec_commands'),
|
|
2636
|
+
m('wf_preview.tbl_commands'),
|
|
2200
2637
|
'|--------|------|',
|
|
2201
|
-
|
|
2202
|
-
|
|
2638
|
+
m('wf_preview.cmd_build_branch', {branchName}),
|
|
2639
|
+
m('wf_preview.cmd_destroy_branch', {branchName}),
|
|
2203
2640
|
'',
|
|
2204
2641
|
'---',
|
|
2205
|
-
'
|
|
2642
|
+
m('wf_preview.footer_custom')
|
|
2206
2643
|
].join('\n');
|
|
2207
2644
|
} else {
|
|
2208
2645
|
body = [
|
|
2209
|
-
'
|
|
2646
|
+
m('wf_preview.title_custom_absent'),
|
|
2210
2647
|
'',
|
|
2211
|
-
'
|
|
2648
|
+
m('wf_preview.tbl_items'),
|
|
2212
2649
|
'|------|-----|',
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
'
|
|
2650
|
+
m('wf_preview.env_branch', {branchName}),
|
|
2651
|
+
m('wf_preview.env_container_name', {containerName}),
|
|
2652
|
+
m('wf_preview.env_status_absent'),
|
|
2216
2653
|
'',
|
|
2217
|
-
|
|
2654
|
+
m('wf_preview.deploy_hint_branch', {branchName}),
|
|
2218
2655
|
'',
|
|
2219
2656
|
'---',
|
|
2220
|
-
'
|
|
2657
|
+
m('wf_preview.footer_custom')
|
|
2221
2658
|
].join('\n');
|
|
2222
2659
|
}
|
|
2223
2660
|
|