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