Skip to content
Closed
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
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,25 @@ jobs:
grep -q '^preview-id=pr-1$' "$tmp/github-output"
grep -q '^console-url=https://console.example.com$' "$tmp/github-output"

- name: Validate dry-run self-hosted server config deploy
env:
APPALOFT_DEPLOY_ACTION_DRY_RUN: "true"
INPUT_CONFIG: appaloft.yml
INPUT_CONTROL_PLANE_MODE: self-hosted
INPUT_CONTROL_PLANE_URL: https://console.example.com/
INPUT_SERVER_CONFIG_DEPLOY: "true"
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-config-package$' "$tmp/argv"
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
39 changes: 30 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ jobs:
steps:
- uses: actions/checkout@v4

- uses: appaloft/deploy-action@main
- uses: appaloft/deploy-action@v1
with:
version: v0.9.2
version: v0.9.0
config: appaloft.yml
ssh-host: ${{ secrets.APPALOFT_SSH_HOST }}
ssh-user: ${{ secrets.APPALOFT_SSH_USER }}
Expand Down Expand Up @@ -84,10 +84,10 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha }}

- uses: appaloft/deploy-action@main
- uses: appaloft/deploy-action@v1
id: deploy
with:
version: v0.9.2
version: v0.9.0
config: appaloft.preview.yml
preview: pull-request
preview-id: pr-${{ github.event.pull_request.number }}
Expand Down Expand Up @@ -135,10 +135,10 @@ jobs:
steps:
- uses: actions/checkout@v4

- uses: appaloft/deploy-action@main
- uses: appaloft/deploy-action@v1
with:
command: preview-cleanup
version: v0.9.2
version: v0.9.0
config: appaloft.preview.yml
preview: pull-request
preview-id: pr-${{ github.event.pull_request.number }}
Expand Down Expand Up @@ -176,7 +176,7 @@ jobs:
name: production
url: ${{ steps.deploy.outputs.console-url }}
steps:
- uses: appaloft/deploy-action@main
- uses: appaloft/deploy-action@v1
id: deploy
with:
control-plane-mode: self-hosted
Expand All @@ -195,14 +195,32 @@ 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.

`server-config-deploy: true` selects the next self-hosted server config workflow. In that mode the
action feature-gates server support through `/api/version` before source package handoff and then
calls `POST /api/action/deployments/from-config-package`. A server that does not advertise source
package and server-side config bootstrap support fails before package upload or state mutation. This
mode is for compatible `0.9.x` self-hosted servers; leave it unset for the existing source-link
trigger 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 }}
server-config-deploy: true
config: appaloft.yml
```

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@main
- uses: appaloft/deploy-action@v1
id: deploy
with:
control-plane-mode: self-hosted
Expand Down Expand Up @@ -253,7 +271,7 @@ source-link state, or the Appaloft server, not from committed config.
| Input | Default | Purpose |
| --- | --- | --- |
| `command` | `deploy` | `deploy` or `preview-cleanup`. |
| `version` | `latest` | Appaloft CLI release tag such as `v0.9.2`. |
| `version` | `latest` | Appaloft CLI release tag such as `v0.9.0`. |
| `config` | empty | Optional Appaloft config path. If omitted, `appaloft.yml` is used only when present. |
| `source` | `.` | Source path or locator passed to the CLI. |
| `runtime-name` | empty | Trusted runtime name override for deploy. |
Expand All @@ -278,6 +296,7 @@ source-link state, or the Appaloft server, not from committed config.
| `control-plane-url` | empty | Self-hosted Appaloft server endpoint for server API mode. When empty, `controlPlane.url` from config may supply the endpoint. |
| `appaloft-token` | empty | Optional bearer token for server API mode. |
| `use-oidc` | `false` | Reserved for future GitHub OIDC exchange. |
| `server-config-deploy` | `false` | Experimental self-hosted mode that calls `POST /api/action/deployments/from-config-package` after the server advertises source package and server-side config bootstrap support. |
| `project-id` | empty | Optional trusted project id for server API mode. When supplied with environment/resource/server ids, the server may bootstrap a missing source link. When omitted with the other ids, the server resolves context from source-link state. |
| `environment-id` | empty | Optional trusted environment id for server API mode. Required only when any explicit deployment id is supplied. |
| `resource-id` | empty | Optional trusted resource id for server API mode. Required only when any explicit deployment id is supplied. |
Expand Down Expand Up @@ -312,6 +331,8 @@ source-link state, or the Appaloft server, not from committed config.
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.
- `server-config-deploy` requires explicit self-hosted server support. The action fails before
source package handoff when the server handshake does not advertise the required capability.
- `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
7 changes: 6 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ inputs:
required: false
default: deploy
version:
description: Appaloft CLI release tag, for example v0.9.2. Use latest for the latest stable release.
description: Appaloft CLI release tag, for example v0.9.0. Use latest for the latest stable release.
required: false
default: latest
config:
Expand Down Expand Up @@ -106,6 +106,10 @@ inputs:
description: Future GitHub OIDC exchange toggle. Not yet supported.
required: false
default: "false"
server-config-deploy:
description: Experimental self-hosted server config deploy mode. Requires server-side source package and config bootstrap support.
required: false
default: "false"
project-id:
description: Trusted Appaloft project id for self-hosted server API mode.
required: false
Expand Down Expand Up @@ -201,6 +205,7 @@ runs:
INPUT_CONTROL_PLANE_URL: ${{ steps.resolve.outputs.control-plane-url }}
INPUT_APPALOFT_TOKEN: ${{ inputs.appaloft-token }}
INPUT_USE_OIDC: ${{ inputs.use-oidc }}
INPUT_SERVER_CONFIG_DEPLOY: ${{ inputs.server-config-deploy }}
INPUT_PROJECT_ID: ${{ inputs.project-id }}
INPUT_ENVIRONMENT_ID: ${{ inputs.environment-id }}
INPUT_RESOURCE_ID: ${{ inputs.resource-id }}
Expand Down
101 changes: 99 additions & 2 deletions scripts/run-deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,78 @@ console_href_url() {
esac
}

version_supports_action_server_config_deploy() {
node -e '
const fs = require("fs");
const input = fs.readFileSync(0, "utf8");
const parsed = JSON.parse(input);
const features = parsed && typeof parsed.features === "object" && parsed.features
? parsed.features
: {};
const supported =
parsed.actionServerConfigDeploy === true ||
features.actionServerConfigDeploy === true ||
(
(features.sourcePackage === true || features.sourcePackages === true) &&
features.serverSideConfigBootstrap === true
);
process.exit(supported ? 0 : 1);
'
}

source_package_payload_for_action() {
local source_fingerprint="$1"
local selected_config="$2"
local source_root="$3"
local payload

payload="{\"sourceFingerprint\":\"$(json_escape "$source_fingerprint")\",\"configPath\":\"$(json_escape "$selected_config")\",\"sourceRoot\":\"$(json_escape "$source_root")\",\"sourcePackage\":{\"transport\":\"server-github-fetch\",\"sourceFingerprint\":\"$(json_escape "$source_fingerprint")\",\"configPath\":\"$(json_escape "$selected_config")\",\"sourceRoot\":\"$(json_escape "$source_root")\""
if [ -n "${GITHUB_SHA:-}" ]; then
payload="${payload},\"revision\":\"$(json_escape "$GITHUB_SHA")\""
fi
if [ -n "${GITHUB_REPOSITORY:-}" ]; then
payload="${payload},\"repositoryFullName\":\"$(json_escape "$GITHUB_REPOSITORY")\""
fi
if [ -n "${GITHUB_REPOSITORY_ID:-}" ]; then
payload="${payload},\"repositoryId\":\"$(json_escape "$GITHUB_REPOSITORY_ID")\""
fi
payload="${payload}}"
if [ -n "$project_id" ] || [ -n "$environment_id" ] || [ -n "$resource_id" ] || [ -n "$server_id" ] || [ -n "$destination_id" ] || [ -n "${GITHUB_REPOSITORY:-}" ] || [ -n "${GITHUB_REPOSITORY_ID:-}" ] || [ -n "${GITHUB_REF:-}" ] || [ -n "${GITHUB_SHA:-}" ]; then
payload="${payload},\"trustedContext\":{"
local separator=""
if [ -n "$project_id" ]; then payload="${payload}${separator}\"projectId\":\"$(json_escape "$project_id")\""; separator=","; fi
if [ -n "$environment_id" ]; then payload="${payload}${separator}\"environmentId\":\"$(json_escape "$environment_id")\""; separator=","; fi
if [ -n "$resource_id" ]; then payload="${payload}${separator}\"resourceId\":\"$(json_escape "$resource_id")\""; separator=","; fi
if [ -n "$server_id" ]; then payload="${payload}${separator}\"serverId\":\"$(json_escape "$server_id")\""; separator=","; fi
if [ -n "$destination_id" ]; then payload="${payload}${separator}\"destinationId\":\"$(json_escape "$destination_id")\""; separator=","; fi
if [ -n "${GITHUB_REPOSITORY:-}" ]; then payload="${payload}${separator}\"repositoryFullName\":\"$(json_escape "$GITHUB_REPOSITORY")\""; separator=","; fi
if [ -n "${GITHUB_REPOSITORY_ID:-}" ]; then payload="${payload}${separator}\"repositoryId\":\"$(json_escape "$GITHUB_REPOSITORY_ID")\""; separator=","; fi
if [ -n "${GITHUB_REF:-}" ]; then payload="${payload}${separator}\"ref\":\"$(json_escape "$GITHUB_REF")\""; separator=","; fi
if [ -n "${GITHUB_SHA:-}" ]; then payload="${payload}${separator}\"revision\":\"$(json_escape "$GITHUB_SHA")\""; fi
payload="${payload}}"
fi
if [ "$preview" = "pull-request" ]; then
payload="${payload},\"preview\":{\"kind\":\"pull-request\",\"previewId\":\"$(json_escape "$preview_id")\""
local pr_number
pr_number="$(pull_request_number_from_context)"
if [ -n "$pr_number" ]; then
payload="${payload},\"pullRequestNumber\":${pr_number}"
fi
if [ -n "${GITHUB_SHA:-}" ]; then
payload="${payload},\"headSha\":\"$(json_escape "$GITHUB_SHA")\""
fi
if [ -n "${GITHUB_BASE_REF:-}" ]; then
payload="${payload},\"baseRef\":\"$(json_escape "$GITHUB_BASE_REF")\""
fi
if [ -n "${GITHUB_HEAD_REF:-}" ]; then
payload="${payload},\"headRef\":\"$(json_escape "$GITHUB_HEAD_REF")\""
fi
payload="${payload}}"
fi
payload="${payload}}"
printf '%s' "$payload"
}

append_step_summary() {
if [ -z "${GITHUB_STEP_SUMMARY:-}" ]; then
return 0
Expand Down Expand Up @@ -444,6 +516,7 @@ control_plane_mode="$input_control_plane_mode"
control_plane_url="${INPUT_CONTROL_PLANE_URL:-}"
appaloft_token="${INPUT_APPALOFT_TOKEN:-}"
use_oidc="${INPUT_USE_OIDC:-false}"
server_config_deploy="${INPUT_SERVER_CONFIG_DEPLOY:-false}"
ssh_private_key="${INPUT_SSH_PRIVATE_KEY:-}"
ssh_private_key_file="${INPUT_SSH_PRIVATE_KEY_FILE:-}"
state_backend="${INPUT_STATE_BACKEND:-}"
Expand Down Expand Up @@ -522,6 +595,11 @@ if truthy "$use_oidc"; then
exit 1
fi

if truthy "$server_config_deploy" && { [ "$control_plane_mode" != "self-hosted" ] || [ "$wrapper_command" != "deploy" ]; }; then
error "server-config-deploy requires control-plane-mode=self-hosted and command=deploy"
exit 1
fi

if [ -n "$ssh_private_key" ] && [ -n "$ssh_private_key_file" ]; then
error "ssh-private-key and ssh-private-key-file are mutually exclusive"
exit 1
Expand Down Expand Up @@ -567,11 +645,16 @@ if [ "$control_plane_mode" = "self-hosted" ]; then
exit 1
fi

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
if [ "$wrapper_command" = "deploy" ] && ! truthy "$server_config_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

if truthy "$server_config_deploy" && { [ -n "${INPUT_RUNTIME_NAME:-}" ] || [ -n "$preview_domain_template" ] || [ -n "$preview_tls_mode" ] || truthy "$require_preview_url" || [ -n "$environment_variables" ] || [ -n "$secret_variables" ]; }; then
error "server-config-deploy hands source/config to the self-hosted server and does not accept runner-side profile, env, secret, or preview route inputs"
exit 1
fi

if [ "$wrapper_command" = "preview-cleanup" ] && { [ -n "${INPUT_RUNTIME_NAME:-}" ] || [ -n "$preview_domain_template" ] || [ -n "$preview_tls_mode" ] || truthy "$require_preview_url"; }; then
error "self-hosted preview-cleanup accepts source, config, preview, and preview-id only"
exit 1
Expand All @@ -588,6 +671,8 @@ if [ "$control_plane_mode" = "self-hosted" ]; then
printf 'GET %s/api/version\n' "$control_plane_url"
if [ "$wrapper_command" = "preview-cleanup" ]; then
printf 'POST %s/api/deployments/cleanup-preview\n' "$control_plane_url"
elif truthy "$server_config_deploy"; then
printf 'POST %s/api/action/deployments/from-config-package\n' "$control_plane_url"
else
printf 'POST %s/api/action/deployments/from-source-link\n' "$control_plane_url"
fi
Expand All @@ -596,6 +681,8 @@ if [ "$control_plane_mode" = "self-hosted" ]; then
printf 'GET %s/api/version\n' "$control_plane_url"
if [ "$wrapper_command" = "preview-cleanup" ]; then
printf 'POST %s/api/deployments/cleanup-preview\n' "$control_plane_url"
elif truthy "$server_config_deploy"; then
printf 'POST %s/api/action/deployments/from-config-package\n' "$control_plane_url"
else
printf 'POST %s/api/action/deployments/from-source-link\n' "$control_plane_url"
fi
Expand All @@ -607,6 +694,11 @@ if [ "$control_plane_mode" = "self-hosted" ]; then
exit 1
fi

if truthy "$server_config_deploy" && ! printf '%s' "$version_response" | version_supports_action_server_config_deploy; then
error "self-hosted control-plane does not support Action Server Config Deploy; missing sourcePackage/serverSideConfigBootstrap feature"
exit 1
fi

payload="{\"sourceFingerprint\":\"$(json_escape "$source_fingerprint")\""
if [ "$wrapper_command" = "deploy" ] && $has_explicit_deployment_context; then
payload="${payload},\"projectId\":\"$(json_escape "$project_id")\",\"environmentId\":\"$(json_escape "$environment_id")\",\"resourceId\":\"$(json_escape "$resource_id")\",\"serverId\":\"$(json_escape "$server_id")\""
Expand All @@ -626,7 +718,12 @@ if [ "$control_plane_mode" = "self-hosted" ]; then
fi
echo "preview-cleanup-status=$cleanup_status" >> "${GITHUB_OUTPUT:-/dev/null}"
else
deploy_endpoint="$control_plane_url/api/action/deployments/from-source-link"
if truthy "$server_config_deploy"; then
deploy_endpoint="$control_plane_url/api/action/deployments/from-config-package"
payload="$(source_package_payload_for_action "$source_fingerprint" "${selected_config_path:-appaloft.yml}" "${config_source_base_directory:-.}")"
else
deploy_endpoint="$control_plane_url/api/action/deployments/from-source-link"
fi
deploy_response="$(curl "${curl_args[@]}" -X POST "$deploy_endpoint" -H "Content-Type: application/json" --data "$payload")"
deployment_id="$(printf '%s\n' "$deploy_response" | sed -n 's/.*"id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
if [ -z "$deployment_id" ]; then
Expand Down
Loading