Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
44 changes: 36 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -195,17 +195,42 @@ 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`, `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
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
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`. 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 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
Expand Down Expand Up @@ -268,6 +293,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`. |

Expand All @@ -281,9 +307,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.
Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
64 changes: 57 additions & 7 deletions scripts/run-deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,38 @@ 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"
}

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
Expand All @@ -254,7 +286,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"
Expand Down Expand Up @@ -300,15 +336,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() {
Expand Down Expand Up @@ -526,8 +567,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

Expand Down Expand Up @@ -592,11 +633,20 @@ if [ "$control_plane_mode" = "self-hosted" ]; then
error "self-hosted control-plane deploy response did not include deployment id"
exit 1
fi
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
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}"
Expand Down
Loading