From d859e546c1bcfe46084110689896a50a3675dc49 Mon Sep 17 00:00:00 2001 From: nichenqin Date: Thu, 7 May 2026 21:26:36 +0800 Subject: [PATCH 1/3] feat: allow server-mode preview deploy trigger --- .github/workflows/ci.yml | 25 +++++++++++++++++++++++++ README.md | 30 ++++++++++++++++++++++++++---- scripts/run-deploy.sh | 6 +++--- 3 files changed, 54 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 045ac0a..b8c5410 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,6 +43,31 @@ jobs: grep -q '^preview-id=pr-1$' "$tmp/github-output" grep -q '^preview-url=http://pr-1.preview.example.com$' "$tmp/github-output" + - name: Validate dry-run self-hosted preview deploy + env: + APPALOFT_DEPLOY_ACTION_DRY_RUN: "true" + INPUT_CONFIG: appaloft.preview.yml + INPUT_CONTROL_PLANE_MODE: self-hosted + INPUT_CONTROL_PLANE_URL: https://console.example.com/ + INPUT_PREVIEW: pull-request + INPUT_PREVIEW_ID: pr-1 + INPUT_PROJECT_ID: prj_console + INPUT_ENVIRONMENT_ID: env_preview + INPUT_RESOURCE_ID: res_preview + INPUT_SERVER_ID: srv_prod + run: | + tmp="$(mktemp -d)" + trap 'rm -rf "$tmp"' EXIT + export GITHUB_OUTPUT="$tmp/github-output" + export APPALOFT_DEPLOY_ACTION_ARGV_PATH="$tmp/argv" + + bash scripts/run-deploy.sh + + grep -q '^GET https://console.example.com/api/version$' "$tmp/argv" + grep -q '^POST https://console.example.com/api/action/deployments/from-source-link$' "$tmp/argv" + grep -q '^preview-id=pr-1$' "$tmp/github-output" + grep -q '^console-url=https://console.example.com$' "$tmp/github-output" + - name: Opt-in exact-version install smoke if: ${{ vars.APPALOFT_INSTALL_SMOKE_VERSION != '' }} env: diff --git a/README.md b/README.md index a50a876..a087b0f 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ deployment history. Use this mode when a self-hosted Appaloft server owns deployment state and the repository should only trigger a deployment through the server API. The resource profile must already exist in the server; this first slice does not apply `appaloft.yml`, upload a source archive, create resources, -or run preview cleanup. +or apply preview route/profile inputs from the runner. ```yaml name: Deploy @@ -195,6 +195,26 @@ them to bootstrap a missing source link before later runs omit ids. When ids are resolves project, environment, resource, and target from existing source-link state. It does not install or invoke the Appaloft CLI, open SSH, or read or write SSH-server PGlite state. +For `preview: pull-request`, server API mode derives a preview-scoped source fingerprint and calls +the same deployment endpoint. It writes `preview-id`, `deployment-id`, and `console-url` outputs, +but it does not apply `preview-domain-template`, `preview-tls-mode`, `require-preview-url`, +`runtime-name`, `environment-variables`, or `secret-variables` in server mode. + +```yaml +- uses: appaloft/deploy-action@v1 + id: deploy + with: + control-plane-mode: self-hosted + control-plane-url: https://console.example.com + appaloft-token: ${{ secrets.APPALOFT_TOKEN }} + preview: pull-request + preview-id: pr-${{ github.event.pull_request.number }} + project-id: ${{ secrets.APPALOFT_PROJECT_ID }} + environment-id: ${{ secrets.APPALOFT_PREVIEW_ENVIRONMENT_ID }} + resource-id: ${{ secrets.APPALOFT_PREVIEW_RESOURCE_ID }} + server-id: ${{ secrets.APPALOFT_SERVER_ID }} +``` + For `command: preview-cleanup`, server API mode derives the preview-scoped source fingerprint from the trusted `preview` and `preview-id` inputs and calls `POST /api/deployments/cleanup-preview`. Cleanup context is resolved from source-link state; project/resource/server ids are not accepted for @@ -281,9 +301,11 @@ source-link state, or the Appaloft server, not from committed config. no control plane is selected. - `control-plane-mode: self-hosted` does not accept SSH keys or `state-backend`; the action calls the Appaloft server API and leaves state ownership with the server. -- In self-hosted server API mode, `command: preview-cleanup` accepts only source/config and trusted - preview scope inputs. Deployment target ids are intentionally ignored/rejected because cleanup - resolves from server-owned source-link state. +- In self-hosted server API mode, preview deploy accepts trusted `preview` and `preview-id` inputs + only for source fingerprinting and feedback outputs. Preview route/profile inputs remain + rejected until the server owns that policy. `command: preview-cleanup` accepts only source/config + and trusted preview scope inputs. Deployment target ids are intentionally ignored/rejected for + cleanup because cleanup resolves from server-owned source-link state. - `pr-comment` requires explicit workflow permission and token wiring. The action updates the same marker comment for the PR instead of creating a new comment on each run. Comment API failures are warnings so they do not mask a successful deployment. diff --git a/scripts/run-deploy.sh b/scripts/run-deploy.sh index 4a1dba4..8de11f8 100755 --- a/scripts/run-deploy.sh +++ b/scripts/run-deploy.sh @@ -526,8 +526,8 @@ if [ "$control_plane_mode" = "self-hosted" ]; then exit 1 fi - if [ "$wrapper_command" = "deploy" ] && { [ "$source_locator" != "." ] || [ -n "${INPUT_RUNTIME_NAME:-}" ] || [ -n "$preview" ] || [ -n "$preview_domain_template" ] || [ -n "$preview_tls_mode" ] || truthy "$require_preview_url"; }; then - error "self-hosted control-plane mode deploys an existing Appaloft resource profile; config, source, runtime-name, and preview inputs are not applied in this slice" + if [ "$wrapper_command" = "deploy" ] && { [ "$source_locator" != "." ] || [ -n "${INPUT_RUNTIME_NAME:-}" ] || [ -n "$preview_domain_template" ] || [ -n "$preview_tls_mode" ] || truthy "$require_preview_url" || [ -n "$environment_variables" ] || [ -n "$secret_variables" ]; }; then + error "self-hosted control-plane mode deploys an existing Appaloft resource profile; source, runtime/profile, environment, secret, and preview route inputs are not applied in this slice" exit 1 fi @@ -596,7 +596,7 @@ if [ "$control_plane_mode" = "self-hosted" ]; then fi fi - if [ "$wrapper_command" = "preview-cleanup" ] && [ -n "$preview_id" ]; then + if [ -n "$preview_id" ]; then echo "preview-id=$preview_id" >> "${GITHUB_OUTPUT:-/dev/null}" fi echo "console-url=$control_plane_url" >> "${GITHUB_OUTPUT:-/dev/null}" From 0b7a97d19a2d7f19aaf2fd885b388c1924e3a984 Mon Sep 17 00:00:00 2001 From: nichenqin Date: Thu, 7 May 2026 21:51:09 +0800 Subject: [PATCH 2/3] feat: expose server deployment console urls --- README.md | 18 +++++++++++------- action.yml | 3 +++ scripts/run-deploy.sh | 30 ++++++++++++++++++++++++++---- 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index a087b0f..2244005 100644 --- a/README.md +++ b/README.md @@ -196,9 +196,10 @@ resolves project, environment, resource, and target from existing source-link st install or invoke the Appaloft CLI, open SSH, or read or write SSH-server PGlite state. For `preview: pull-request`, server API mode derives a preview-scoped source fingerprint and calls -the same deployment endpoint. It writes `preview-id`, `deployment-id`, and `console-url` outputs, -but it does not apply `preview-domain-template`, `preview-tls-mode`, `require-preview-url`, -`runtime-name`, `environment-variables`, or `secret-variables` in server mode. +the same deployment endpoint. It writes `preview-id`, `deployment-id`, `deployment-url`, and +`console-url` outputs, but it does not apply `preview-domain-template`, `preview-tls-mode`, +`require-preview-url`, `runtime-name`, `environment-variables`, or `secret-variables` in server +mode. ```yaml - uses: appaloft/deploy-action@v1 @@ -220,12 +221,14 @@ the trusted `preview` and `preview-id` inputs and calls `POST /api/deployments/c Cleanup context is resolved from source-link state; project/resource/server ids are not accepted for server-mode preview cleanup. -Server API mode writes the console URL and deployment id to the GitHub step summary when GitHub -provides `GITHUB_STEP_SUMMARY`. For cleanup it writes the console URL and cleanup status. Workflows -can also use the `console-url` output for environment URLs or PR comments. +Server API mode writes the console URL and deployment detail URL to the GitHub step summary when +GitHub provides `GITHUB_STEP_SUMMARY`. For cleanup it writes the console URL and cleanup status. +Workflows can also use the `console-url` or `deployment-url` output for environment URLs or PR +comments. When `pr-comment: true`, the action posts or updates one stable pull request comment with the -preview URL, console URL, deployment id, or cleanup status that is available for the selected mode. +preview URL, console URL, deployment detail URL, or cleanup status that is available for the +selected mode. The workflow must pass `github-token: ${{ github.token }}` and grant `pull-requests: write` or `issues: write`. This is entrypoint feedback only; product-grade GitHub App comments/checks remain control-plane features. Comment publishing is best-effort: GitHub API permission failures are @@ -288,6 +291,7 @@ source-link state, or the Appaloft server, not from committed config. | `preview-id` | Preview id when preview mode is selected. | | `preview-url` | Public preview URL when Appaloft resolves one during deploy. | | `deployment-id` | Deployment id accepted by Appaloft. | +| `deployment-url` | Self-hosted Appaloft console deployment detail URL when available. | | `console-url` | Self-hosted Appaloft console URL used by server API mode. | | `preview-cleanup-status` | Cleanup status returned by server API mode for `command: preview-cleanup`. | diff --git a/action.yml b/action.yml index b50c31d..1203743 100644 --- a/action.yml +++ b/action.yml @@ -143,6 +143,9 @@ outputs: deployment-id: description: Deployment id accepted by Appaloft. value: ${{ steps.deploy.outputs.deployment-id }} + deployment-url: + description: Self-hosted Appaloft console deployment detail URL when available. + value: ${{ steps.deploy.outputs.deployment-url }} console-url: description: Self-hosted Appaloft console URL used by server API mode. value: ${{ steps.deploy.outputs.console-url }} diff --git a/scripts/run-deploy.sh b/scripts/run-deploy.sh index 8de11f8..0596872 100755 --- a/scripts/run-deploy.sh +++ b/scripts/run-deploy.sh @@ -241,6 +241,17 @@ append_auth_header() { fi } +deployment_console_url() { + local base_url="$1" + local deployment="$2" + if [ -z "$base_url" ] || [ -z "$deployment" ]; then + printf '' + return 0 + fi + + printf '%s/deployments/%s' "${base_url%/}" "$deployment" +} + append_step_summary() { if [ -z "${GITHUB_STEP_SUMMARY:-}" ]; then return 0 @@ -254,7 +265,11 @@ append_step_summary() { fi printf -- '- Console: %s\n' "$control_plane_url" if [ -n "${deployment_id:-}" ]; then - printf -- '- Deployment: `%s`\n' "$deployment_id" + if [ -n "${deployment_url:-}" ]; then + printf -- '- Deployment: [%s](%s)\n' "$deployment_id" "$deployment_url" + else + printf -- '- Deployment: `%s`\n' "$deployment_id" + fi fi if [ -n "${cleanup_status:-}" ]; then printf -- '- Cleanup status: `%s`\n' "$cleanup_status" @@ -300,15 +315,20 @@ build_pr_comment_body() { const consoleUrl = process.argv[4]; const previewUrl = process.argv[5]; const deploymentId = process.argv[6]; - const cleanupStatus = process.argv[7]; + const deploymentUrl = process.argv[7]; + const cleanupStatus = process.argv[8]; const lines = [marker, "", command === "preview-cleanup" ? "### Appaloft preview cleanup" : "### Appaloft deployment", ""]; if (previewId) lines.push(`- Preview: \`${previewId}\``); if (previewUrl) lines.push(`- Preview URL: ${previewUrl}`); if (consoleUrl) lines.push(`- Console: ${consoleUrl}`); - if (deploymentId) lines.push(`- Deployment: \`${deploymentId}\``); + if (deploymentUrl) { + lines.push(`- Deployment: [${deploymentId || "Open deployment"}](${deploymentUrl})`); + } else if (deploymentId) { + lines.push(`- Deployment: \`${deploymentId}\``); + } if (cleanupStatus) lines.push(`- Cleanup status: \`${cleanupStatus}\``); process.stdout.write(JSON.stringify({ body: `${lines.join("\n")}\n` })); - ' "$1" "$wrapper_command" "$preview_id" "${control_plane_url:-}" "${preview_url:-}" "${deployment_id:-}" "${cleanup_status:-}" + ' "$1" "$wrapper_command" "$preview_id" "${control_plane_url:-}" "${preview_url:-}" "${deployment_id:-}" "${deployment_url:-}" "${cleanup_status:-}" } warn_pr_comment_skipped() { @@ -592,7 +612,9 @@ if [ "$control_plane_mode" = "self-hosted" ]; then error "self-hosted control-plane deploy response did not include deployment id" exit 1 fi + deployment_url="$(deployment_console_url "$control_plane_url" "$deployment_id")" echo "deployment-id=$deployment_id" >> "${GITHUB_OUTPUT:-/dev/null}" + echo "deployment-url=$deployment_url" >> "${GITHUB_OUTPUT:-/dev/null}" fi fi From b86235e1c66c7a85983a05da5a095c5bc6ef0485 Mon Sep 17 00:00:00 2001 From: nichenqin Date: Thu, 7 May 2026 22:10:44 +0800 Subject: [PATCH 3/3] feat: use server deployment hrefs --- README.md | 8 +++++--- scripts/run-deploy.sh | 30 +++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2244005..4b7dac6 100644 --- a/README.md +++ b/README.md @@ -222,9 +222,11 @@ Cleanup context is resolved from source-link state; project/resource/server ids server-mode preview cleanup. Server API mode writes the console URL and deployment detail URL to the GitHub step summary when -GitHub provides `GITHUB_STEP_SUMMARY`. For cleanup it writes the console URL and cleanup status. -Workflows can also use the `console-url` or `deployment-url` output for environment URLs or PR -comments. +GitHub provides `GITHUB_STEP_SUMMARY`. When the server response includes a deployment href or URL, +the action uses that server-provided console target; otherwise it falls back to the standard +`/deployments/{deploymentId}` console route. For cleanup it writes the console URL and cleanup +status. Workflows can also use the `console-url` or `deployment-url` output for environment URLs or +PR comments. When `pr-comment: true`, the action posts or updates one stable pull request comment with the preview URL, console URL, deployment detail URL, or cleanup status that is available for the diff --git a/scripts/run-deploy.sh b/scripts/run-deploy.sh index 0596872..21addb0 100755 --- a/scripts/run-deploy.sh +++ b/scripts/run-deploy.sh @@ -252,6 +252,27 @@ deployment_console_url() { printf '%s/deployments/%s' "${base_url%/}" "$deployment" } +console_href_url() { + local base_url="$1" + local href="$2" + if [ -z "$href" ]; then + printf '' + return 0 + fi + + case "$href" in + http://*|https://*) + printf '%s' "$href" + ;; + /*) + printf '%s%s' "${base_url%/}" "$href" + ;; + *) + printf '%s/%s' "${base_url%/}" "$href" + ;; + esac +} + append_step_summary() { if [ -z "${GITHUB_STEP_SUMMARY:-}" ]; then return 0 @@ -612,7 +633,14 @@ if [ "$control_plane_mode" = "self-hosted" ]; then error "self-hosted control-plane deploy response did not include deployment id" exit 1 fi - deployment_url="$(deployment_console_url "$control_plane_url" "$deployment_id")" + deployment_url="$(printf '%s\n' "$deploy_response" | sed -n 's/.*"deploymentUrl"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')" + deployment_href="$(printf '%s\n' "$deploy_response" | sed -n 's/.*"deploymentHref"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')" + if [ -z "$deployment_url" ] && [ -n "$deployment_href" ]; then + deployment_url="$(console_href_url "$control_plane_url" "$deployment_href")" + fi + if [ -z "$deployment_url" ]; then + deployment_url="$(deployment_console_url "$control_plane_url" "$deployment_id")" + fi echo "deployment-id=$deployment_id" >> "${GITHUB_OUTPUT:-/dev/null}" echo "deployment-url=$deployment_url" >> "${GITHUB_OUTPUT:-/dev/null}" fi