@@ -0,0 +1,45 @@
|
||||
name: Laravel 1Panel Release
|
||||
description: Atomically deploy a Laravel release to a 1Panel PHP container.
|
||||
|
||||
inputs:
|
||||
source-dir:
|
||||
description: Repository-relative Laravel source directory.
|
||||
required: true
|
||||
deploy-root:
|
||||
description: Host deployment root.
|
||||
required: true
|
||||
container-deploy-root:
|
||||
description: Deployment root as mounted inside the PHP container.
|
||||
required: true
|
||||
php-container:
|
||||
description: 1Panel PHP container name.
|
||||
required: true
|
||||
healthcheck-url:
|
||||
description: HTTPS Laravel health-check URL.
|
||||
required: true
|
||||
backup-command:
|
||||
description: Server-side database backup command.
|
||||
required: true
|
||||
release-id:
|
||||
description: Unique release identifier.
|
||||
required: true
|
||||
keep-releases:
|
||||
description: Number of releases to retain.
|
||||
required: false
|
||||
default: "5"
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Deploy release
|
||||
shell: bash
|
||||
env:
|
||||
SOURCE_DIR: ${{ gitea.workspace }}/${{ inputs.source-dir }}
|
||||
DEPLOY_ROOT: ${{ inputs.deploy-root }}
|
||||
CONTAINER_DEPLOY_ROOT: ${{ inputs.container-deploy-root }}
|
||||
PHP_CONTAINER: ${{ inputs.php-container }}
|
||||
HEALTHCHECK_URL: ${{ inputs.healthcheck-url }}
|
||||
BACKUP_COMMAND: ${{ inputs.backup-command }}
|
||||
RELEASE_ID: ${{ inputs.release-id }}
|
||||
KEEP_RELEASES: ${{ inputs.keep-releases }}
|
||||
run: '"$GITHUB_ACTION_PATH/deploy-release.sh"'
|
||||
Executable
+151
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
required_variables=(
|
||||
SOURCE_DIR
|
||||
DEPLOY_ROOT
|
||||
CONTAINER_DEPLOY_ROOT
|
||||
PHP_CONTAINER
|
||||
HEALTHCHECK_URL
|
||||
BACKUP_COMMAND
|
||||
)
|
||||
|
||||
for variable_name in "${required_variables[@]}"; do
|
||||
test -n "${!variable_name:-}" || {
|
||||
printf 'ERROR: %s is required.\n' "$variable_name" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
release_id="${RELEASE_ID:-$(date -u +%Y%m%d%H%M%S)}"
|
||||
keep_releases="${KEEP_RELEASES:-5}"
|
||||
php_user="${PHP_RUNTIME_USER:-1000:1000}"
|
||||
dry_run="${DRY_RUN:-false}"
|
||||
host_release="$DEPLOY_ROOT/releases/$release_id"
|
||||
container_release="$CONTAINER_DEPLOY_ROOT/releases/$release_id"
|
||||
previous_release=""
|
||||
maintenance_enabled=false
|
||||
|
||||
run() {
|
||||
if test "$dry_run" = "true"; then
|
||||
printf 'DRY-RUN:'
|
||||
printf ' %q' "$@"
|
||||
printf '\n'
|
||||
return 0
|
||||
fi
|
||||
"$@"
|
||||
}
|
||||
|
||||
artisan() {
|
||||
run docker exec --user "$php_user" --workdir "$container_release" \
|
||||
"$PHP_CONTAINER" php artisan "$@"
|
||||
}
|
||||
|
||||
restore_previous_release() {
|
||||
if test "$dry_run" != "true" && test -n "$previous_release"; then
|
||||
ln -s "$previous_release" "$DEPLOY_ROOT/.current.rollback"
|
||||
mv -Tf "$DEPLOY_ROOT/.current.rollback" "$DEPLOY_ROOT/current"
|
||||
fi
|
||||
}
|
||||
|
||||
recover() {
|
||||
exit_code=$?
|
||||
if test "$exit_code" -ne 0; then
|
||||
printf 'Deployment failed; restoring the previous code release.\n' >&2
|
||||
restore_previous_release
|
||||
fi
|
||||
if test "$maintenance_enabled" = "true" && test -L "$DEPLOY_ROOT/current"; then
|
||||
docker exec --user "$php_user" \
|
||||
--workdir "$CONTAINER_DEPLOY_ROOT/current" \
|
||||
"$PHP_CONTAINER" php artisan up >/dev/null 2>&1 || true
|
||||
fi
|
||||
exit "$exit_code"
|
||||
}
|
||||
trap recover EXIT
|
||||
trap 'exit 143' TERM
|
||||
trap 'exit 130' INT
|
||||
|
||||
test -d "$SOURCE_DIR" || {
|
||||
printf 'ERROR: Source directory does not exist: %s\n' "$SOURCE_DIR" >&2
|
||||
exit 1
|
||||
}
|
||||
test -f "$DEPLOY_ROOT/shared/.env" || {
|
||||
printf 'ERROR: Shared production .env does not exist.\n' >&2
|
||||
exit 1
|
||||
}
|
||||
test -d "$DEPLOY_ROOT/shared/storage" || {
|
||||
printf 'ERROR: Shared storage directory does not exist.\n' >&2
|
||||
exit 1
|
||||
}
|
||||
command -v flock >/dev/null 2>&1 || {
|
||||
printf 'ERROR: flock is required on the production host.\n' >&2
|
||||
exit 1
|
||||
}
|
||||
exec 9>"$DEPLOY_ROOT/.deploy.lock"
|
||||
flock -w 600 9 || {
|
||||
printf 'ERROR: Another deployment still holds the production lock.\n' >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if test -L "$DEPLOY_ROOT/current"; then
|
||||
previous_release="$(readlink "$DEPLOY_ROOT/current")"
|
||||
if test "$dry_run" != "true"; then
|
||||
docker exec --user "$php_user" \
|
||||
--workdir "$CONTAINER_DEPLOY_ROOT/current" \
|
||||
"$PHP_CONTAINER" php artisan down --retry=30
|
||||
maintenance_enabled=true
|
||||
fi
|
||||
fi
|
||||
|
||||
run mkdir -p "$host_release"
|
||||
run rsync -a --delete \
|
||||
--exclude='.env' \
|
||||
--exclude='node_modules' \
|
||||
--exclude='storage' \
|
||||
--exclude='vendor' \
|
||||
"$SOURCE_DIR/" "$host_release/"
|
||||
run ln -s ../../shared/.env "$host_release/.env"
|
||||
run ln -s ../../shared/storage "$host_release/storage"
|
||||
|
||||
run docker exec --user "$php_user" --workdir "$container_release" \
|
||||
"$PHP_CONTAINER" composer install \
|
||||
--no-dev --no-interaction --prefer-dist --optimize-autoloader
|
||||
|
||||
if test "$dry_run" != "true"; then
|
||||
bash -c "$BACKUP_COMMAND"
|
||||
fi
|
||||
|
||||
artisan migrate --force
|
||||
artisan storage:link
|
||||
artisan optimize:clear
|
||||
artisan config:cache
|
||||
artisan route:cache
|
||||
artisan view:cache
|
||||
|
||||
run ln -s "releases/$release_id" "$DEPLOY_ROOT/.current.next"
|
||||
run mv -Tf "$DEPLOY_ROOT/.current.next" "$DEPLOY_ROOT/current"
|
||||
|
||||
if test "$dry_run" != "true"; then
|
||||
docker exec "$PHP_CONTAINER" kill -USR2 1
|
||||
docker exec --user "$php_user" \
|
||||
--workdir "$CONTAINER_DEPLOY_ROOT/current" \
|
||||
"$PHP_CONTAINER" php artisan up
|
||||
maintenance_enabled=false
|
||||
curl --fail --silent --show-error \
|
||||
--retry 6 --retry-delay 5 --retry-connrefused \
|
||||
"$HEALTHCHECK_URL" >/dev/null
|
||||
|
||||
active_target="$(readlink "$DEPLOY_ROOT/current")"
|
||||
mapfile -t old_releases < <(
|
||||
ls -1dt "$DEPLOY_ROOT"/releases/* 2>/dev/null |
|
||||
awk -v keep="$keep_releases" -v active="$DEPLOY_ROOT/$active_target" '
|
||||
$0 != active { seen++; if (seen >= keep) print }
|
||||
'
|
||||
)
|
||||
if test "${#old_releases[@]}" -gt 0; then
|
||||
rm -rf -- "${old_releases[@]}"
|
||||
fi
|
||||
fi
|
||||
|
||||
trap - EXIT
|
||||
printf 'Deployment succeeded: %s\n' "$release_id"
|
||||
@@ -0,0 +1,41 @@
|
||||
name: WeChat Mini Program Preview
|
||||
description: Publish a WeChat Mini Program preview and generate a QR code.
|
||||
|
||||
inputs:
|
||||
project-path:
|
||||
description: Repository-relative directory containing project.config.json.
|
||||
required: true
|
||||
appid:
|
||||
description: WeChat Mini Program AppID.
|
||||
required: true
|
||||
robot:
|
||||
description: WeChat CI robot number from 1 to 30.
|
||||
required: false
|
||||
default: "1"
|
||||
private-key-path:
|
||||
description: Temporary code-upload key path.
|
||||
required: true
|
||||
qrcode-output:
|
||||
description: Destination PNG path.
|
||||
required: true
|
||||
version-label:
|
||||
description: Traceable commit or release label.
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Publish preview
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.project-path }}
|
||||
env:
|
||||
MINIAPP_APPID: ${{ inputs.appid }}
|
||||
MINIAPP_ROBOT: ${{ inputs.robot }}
|
||||
WECHAT_CI_PRIVATE_KEY_PATH: ${{ inputs.private-key-path }}
|
||||
MINIAPP_QR_OUTPUT: ${{ inputs.qrcode-output }}
|
||||
MINIAPP_VERSION_LABEL: ${{ inputs.version-label }}
|
||||
run: |
|
||||
temporary_script=".wechat-preview.cjs"
|
||||
cp "$GITHUB_ACTION_PATH/preview.cjs" "$temporary_script"
|
||||
trap 'rm -f "$temporary_script"' EXIT
|
||||
node "$temporary_script"
|
||||
Executable
+70
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const process = require("node:process");
|
||||
const ci = require("miniprogram-ci");
|
||||
|
||||
function requireEnvironment(name) {
|
||||
const value = process.env[name];
|
||||
if (!value) {
|
||||
throw new Error(`${name} is required.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const projectPath = process.cwd();
|
||||
const privateKeyPath = path.resolve(
|
||||
requireEnvironment("WECHAT_CI_PRIVATE_KEY_PATH"),
|
||||
);
|
||||
const qrcodeOutputDest = path.resolve(
|
||||
requireEnvironment("MINIAPP_QR_OUTPUT"),
|
||||
);
|
||||
const appid = requireEnvironment("MINIAPP_APPID");
|
||||
const robot = Number.parseInt(process.env.MINIAPP_ROBOT ?? "1", 10);
|
||||
const versionLabel = requireEnvironment("MINIAPP_VERSION_LABEL");
|
||||
|
||||
if (!fs.existsSync(path.join(projectPath, "project.config.json"))) {
|
||||
throw new Error("The working directory must contain project.config.json.");
|
||||
}
|
||||
if (!fs.existsSync(privateKeyPath)) {
|
||||
throw new Error("The WeChat code-upload key file does not exist.");
|
||||
}
|
||||
if (!Number.isInteger(robot) || robot < 1 || robot > 30) {
|
||||
throw new Error("MINIAPP_ROBOT must be an integer from 1 to 30.");
|
||||
}
|
||||
|
||||
const project = new ci.Project({
|
||||
appid,
|
||||
type: "miniProgram",
|
||||
projectPath,
|
||||
privateKeyPath,
|
||||
ignores: ["node_modules/**/*", ".git/**/*"],
|
||||
});
|
||||
|
||||
await ci.preview({
|
||||
project,
|
||||
desc: `Automated preview ${versionLabel}`.slice(0, 50),
|
||||
robot,
|
||||
setting: {
|
||||
es6: true,
|
||||
minify: true,
|
||||
autoPrefixWXSS: true,
|
||||
},
|
||||
qrcodeFormat: "image",
|
||||
qrcodeOutputDest,
|
||||
onProgressUpdate: (progress) => {
|
||||
if (typeof progress === "string") {
|
||||
console.log(progress);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Preview QR code created: ${qrcodeOutputDest}`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`Mini Program preview failed: ${error.message}`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Reference in New Issue
Block a user