Run Appaloft deployments from GitHub Actions.
The default mode is a thin wrapper around the released appaloft binary for pure SSH deployments.
Self-hosted server API mode is available for repositories that already have their project,
environment, resource, and deployment target registered in an Appaloft server. In self-hosted mode,
the action may read non-secret project/environment/resource/server ids from
controlPlane.deploymentContext in committed appaloft.yml; credentials, tokens, and secret
values still come from trusted workflow inputs or secrets.
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: appaloft/deploy-action@v1
with:
version: v0.9.0
config: appaloft.yml
ssh-host: ${{ secrets.APPALOFT_SSH_HOST }}
ssh-user: ${{ secrets.APPALOFT_SSH_USER }}
ssh-private-key: ${{ secrets.APPALOFT_SSH_PRIVATE_KEY }}Pin version to an Appaloft CLI release for production workflows. version: latest is useful for
quick experiments, but it trades repeatability for convenience.
Minimal appaloft.yml:
runtime:
strategy: workspace-commands
buildCommand: bun install && bun run build
startCommand: bun run start
network:
internalPort: 3000Application secrets should be mapped by the workflow and referenced from config, not committed as values:
secrets:
DATABASE_URL:
from: ci-env:DATABASE_URLUse command: install-console when a workflow should install or upgrade an Appaloft console on an
SSH host before other repositories deploy through control-plane-mode: self-hosted.
name: Install Appaloft Console
on:
workflow_dispatch:
jobs:
install:
runs-on: ubuntu-latest
permissions:
contents: read
environment:
name: appaloft-console
url: ${{ steps.console.outputs.console-url }}
steps:
- uses: appaloft/deploy-action@v1
id: console
with:
command: install-console
version: latest
ssh-host: ${{ secrets.APPALOFT_CONSOLE_SSH_HOST }}
ssh-user: ${{ secrets.APPALOFT_CONSOLE_SSH_USER }}
ssh-private-key: ${{ secrets.APPALOFT_CONSOLE_SSH_PRIVATE_KEY }}
console-domain: console.example.com
console-database: postgres
console-proxy: traefik
console-orchestrator: compose
console-skip-docker-install: trueThe action connects to the SSH host, downloads the matching Appaloft release install.sh, runs the
self-hosted Docker installer with the selected public console origin and Docker orchestrator, and
verifies /api/health. The installer defaults to PostgreSQL, direct host access on port 3721,
and an Appaloft-managed Traefik edge proxy. console-domain is passed to the installer as the
Appaloft instance console route. console-url may be supplied directly when the public origin is
not https://<console-domain>. Set console-orchestrator: swarm to deploy the console as a Docker
Swarm stack; console-swarm-init: true may initialize a single-node Swarm manager when the host is
not already a manager. Use console-proxy: none only when another reverse proxy owns public
routing. This command is separate from deploy, so the original pure SSH CLI deployment path
remains available.
Non-secret console install settings can live in the selected config file. SSH host, SSH key, API tokens, and raw database credentials still come from trusted workflow inputs or secrets.
controlPlane:
mode: self-hosted
url: https://console.example.com
deploymentContext:
projectId: prj_www
environmentId: env_prod
resourceId: res_www
serverId: srv_prod
# destinationId is optional.
install:
database: postgres
domain: console.example.com
proxy: traefik
orchestrator: swarm
httpPort: 3721
swarmStackName: appaloft-console
swarmInit: trueWhen command: install-console is used, explicit action inputs override the matching
controlPlane.install.* values. If console-url is omitted, the action uses
controlPlane.install.url, then controlPlane.url, then https://<console-domain>, and finally
http://<ssh-host>:<console-http-port>.
Action-only pull request previews require a workflow file. The action does not install a webhook or make GitHub run previews on its own.
name: Appaloft Preview
on:
pull_request:
types: [opened, reopened, synchronize]
jobs:
preview:
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
environment:
name: preview-pr-${{ github.event.pull_request.number }}
url: ${{ steps.deploy.outputs.preview-url }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- uses: appaloft/deploy-action@v1
id: deploy
with:
version: v0.9.0
config: appaloft.preview.yml
preview: pull-request
preview-id: pr-${{ github.event.pull_request.number }}
preview-domain-template: pr-${{ github.event.pull_request.number }}.preview.example.com
preview-tls-mode: disabled
require-preview-url: true
pr-comment: true
github-token: ${{ github.token }}
ssh-host: ${{ secrets.APPALOFT_SSH_HOST }}
ssh-user: ${{ secrets.APPALOFT_SSH_USER }}
ssh-private-key: ${{ secrets.APPALOFT_SSH_PRIVATE_KEY }}
environment-variables: |
HOST=0.0.0.0
PORT=3000
secret-variables: |
DATABASE_URL=ci-env:DATABASE_URLThe default example skips fork pull requests before deployment credentials are exposed. Fork previews need an explicit reduced-credential policy.
Use appaloft.preview.yml when the root config is production-oriented. Preview route intent should
come from generated/default access, this trusted preview-domain-template, or an explicitly
selected preview config file. Production access.domains[] should not be reinterpreted as pull
request preview hostnames.
Add a separate close-event workflow so preview runtime and route state are cleaned when the pull request closes:
name: Appaloft Preview Cleanup
on:
pull_request:
types: [closed]
jobs:
cleanup:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: appaloft/deploy-action@v1
with:
command: preview-cleanup
version: v0.9.0
config: appaloft.preview.yml
preview: pull-request
preview-id: pr-${{ github.event.pull_request.number }}
pr-comment: true
github-token: ${{ github.token }}
ssh-host: ${{ secrets.APPALOFT_SSH_HOST }}
ssh-user: ${{ secrets.APPALOFT_SSH_USER }}
ssh-private-key: ${{ secrets.APPALOFT_SSH_PRIVATE_KEY }}Cleanup is idempotent. It stops preview-owned runtime state when present, removes preview route desired state, unlinks preview source identity, and preserves production deployments and ordinary 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 apply preview route/profile inputs from the runner.
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
environment:
name: production
url: ${{ steps.deploy.outputs.console-url }}
steps:
- 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 }}
project-id: ${{ secrets.APPALOFT_PROJECT_ID }}
environment-id: ${{ secrets.APPALOFT_ENVIRONMENT_ID }}
resource-id: ${{ secrets.APPALOFT_RESOURCE_ID }}
server-id: ${{ secrets.APPALOFT_SERVER_ID }}Server API mode performs a lightweight compatibility check against /api/version, derives a safe
source fingerprint from GitHub repository context and config path, and calls
POST /api/action/deployments/from-source-link. When trusted ids are supplied, the server can use
them to bootstrap a missing source link before later runs omit ids. When ids are omitted, the server
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.
- uses: appaloft/deploy-action@v1
id: deploy
with:
server-config-deploy: true
config: appaloft.ymlWith this shape, controlPlane.mode, controlPlane.url, and
controlPlane.deploymentContext come from appaloft.yml. Explicit action inputs still override
the matching config values when a workflow needs to do so. appaloft-token is optional in the
current 0.9.x slice, but production hardening will require a deploy token or OIDC exchange for
Action mutation endpoints.
If the committed config declares secrets.KEY.from: ci-env:NAME, expose the GitHub secret to the
runner environment and list the matching secret-variables entry. The action resolves the value
locally, sends it only as transient API payload to the self-hosted server, and the server applies it
as a runtime secret through its environment-variable operation.
- uses: appaloft/deploy-action@v1
id: deploy
env:
APPALOFT_BETTER_AUTH_SECRET: ${{ secrets.APPALOFT_BETTER_AUTH_SECRET }}
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
secret-variables: |
APPALOFT_BETTER_AUTH_SECRET=ci-env:APPALOFT_BETTER_AUTH_SECRETFor 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. Existing source-link server mode does not apply runner-side route/profile
inputs. In server-config-deploy mode, pull request previews may keep using
environment-variables, preview-domain-template, preview-tls-mode, and require-preview-url
for per-run values such as PR ports, callback URLs, and preview hostnames. Those values are sent as
transient API payload; they do not need to be committed to appaloft.yml. Production config
domains from appaloft.yml are not reused for pull request preview routing unless a later
preview-safe policy explicitly selects them. runtime-name remains a pure SSH CLI input and is not
accepted by server config deploy. In server-config-deploy mode, secret-variables is reserved
for resolving committed ci-env: secret references before the API request.
- 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.preview.yml
preview: pull-request
preview-id: pr-${{ github.event.pull_request.number }}
preview-domain-template: pr-${{ github.event.pull_request.number }}.preview.example.com
preview-tls-mode: disabled
require-preview-url: true
environment-variables: |
HOST=0.0.0.0
PORT=4321
APPALOFT_BETTER_AUTH_URL=http://pr-${{ github.event.pull_request.number }}.preview.example.comFor 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 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 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
reported as warnings and do not fail an otherwise successful deployment or cleanup.
The control-plane connection policy can live in appaloft.yml:
controlPlane:
mode: self-hosted
url: https://console.example.comExplicit action inputs override config values. Project, environment, resource, server, token, SSH, and database identity still come from trusted workflow inputs, variables, secrets, existing source-link state, or the Appaloft server, not from committed config.
| Input | Default | Purpose |
|---|---|---|
command |
deploy |
deploy, preview-cleanup, or install-console. |
version |
latest |
Appaloft release tag such as v0.9.0. Used for CLI install and self-hosted console install. |
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. |
ssh-host |
empty | SSH target host for pure SSH deployments. |
ssh-user |
empty | SSH username. |
ssh-port |
empty | SSH port. |
ssh-private-key |
empty | SSH private key value, written to a temp file before invoking Appaloft. |
ssh-private-key-file |
empty | Existing runner-local private key path. Mutually exclusive with ssh-private-key. |
console-url |
empty | Public console origin for command: install-console. Defaults to https://<console-domain> or http://<ssh-host>:<console-http-port>. |
console-domain |
empty | Public console domain used to derive console-url when console-url is empty. |
console-database |
config or postgres |
Self-hosted console database backend for command: install-console; pglite or postgres. |
console-orchestrator |
config or compose |
Self-hosted Docker orchestrator for command: install-console; compose or swarm. |
console-proxy |
config or traefik |
Self-hosted console proxy mode for command: install-console; traefik or none. |
console-http-host |
config or 0.0.0.0 |
Host bind address passed to the self-hosted console installer. |
console-http-port |
config or 3721 |
Host HTTP port passed to the self-hosted console installer. |
console-install-dir |
empty | Remote install directory passed to the self-hosted console installer. Empty uses the installer default. |
console-compose-project-name |
config or appaloft |
Docker Compose project name passed to the self-hosted console installer. |
console-swarm-stack-name |
config or appaloft |
Docker Swarm stack name passed to the self-hosted console installer. |
console-swarm-init |
config or false |
Initialize a single-node Swarm manager when console-orchestrator is swarm. |
console-swarm-advertise-addr |
empty | Optional advertise address passed to docker swarm init. |
console-image |
config or ghcr.io/appaloft/appaloft |
Appaloft console image repository or full image reference passed to the self-hosted console installer. |
console-installer-url |
empty | Override URL for the self-hosted install.sh used by command: install-console. |
console-skip-docker-install |
false |
Require Docker Engine to already exist on the SSH host during command: install-console. |
server-provider |
generic-ssh |
Server provider key. |
server-proxy-kind |
empty | Server proxy kind such as traefik or caddy. |
state-backend |
empty | Explicit state backend. SSH targets default to ssh-pglite. |
environment-variables |
empty | Newline-separated values passed as repeated CLI --env flags in pure SSH CLI mode. In server-config-deploy pull request previews, sent as transient server API payload and applied through the server environment-variable operation after committed env values, so Action values can override per-preview fields. |
secret-variables |
empty | Newline-separated values passed as repeated CLI --secret flags in pure SSH CLI mode. In server-config-deploy mode, resolves ci-env: references and sends the matched values as transient server API payload for committed config secrets entries. Prefer ci-env: references over raw secret values. |
preview |
empty | Use pull-request for PR preview deploy or cleanup. |
preview-id |
empty | Trusted preview scope, for example pr-123. Required for pull request previews. |
preview-domain-template |
empty | Trusted preview hostname for deploy, for example pr-123.preview.example.com. |
preview-tls-mode |
empty | Preview TLS mode for preview-domain-template. |
require-preview-url |
false |
Fail deploy if no public preview URL can be resolved. |
pr-comment |
false |
Post or update one pull request comment with preview, deployment, cleanup, and console feedback. |
github-token |
empty | GitHub token used only when pr-comment is true. |
control-plane-mode |
empty | Use none for pure SSH CLI mode or self-hosted for server API mode. When empty, controlPlane.mode from config may select the mode; otherwise the effective default is none. |
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 |
config or empty | Optional trusted project id for server API mode. Defaults to controlPlane.deploymentContext.projectId when present. |
environment-id |
config or empty | Optional trusted environment id for server API mode. Defaults to controlPlane.deploymentContext.environmentId when present. Required only when any explicit deployment id is supplied. |
resource-id |
config or empty | Optional trusted resource id for server API mode. Defaults to controlPlane.deploymentContext.resourceId when present. Required only when any explicit deployment id is supplied. |
server-id |
config or empty | Optional trusted deployment target id for server API mode. Defaults to controlPlane.deploymentContext.serverId when present. Required only when any explicit deployment id is supplied. |
destination-id |
config or empty | Optional trusted destination id for server API mode. Defaults to controlPlane.deploymentContext.destinationId when present. |
| Output | Purpose |
|---|---|
appaloft-version |
Installed CLI version. |
appaloft-target |
Selected release target. |
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 installed by install-console or used by server API mode. |
preview-cleanup-status |
Cleanup status returned by server API mode for command: preview-cleanup. |
ssh-private-keyis written to a runner temp file with mode0600; raw key material is not passed as a command-line argument.- Do not commit SSH keys, tokens, database URLs, production secret values, or Appaloft identity
selectors into
appaloft.yml. - The action defaults SSH deployments to server-owned
ssh-pglitestate whenssh-hostis set and no control plane is selected. control-plane-mode: self-hosteddoes not accept SSH keys orstate-backend; the action calls the Appaloft server API and leaves state ownership with the server.- In self-hosted server API mode, preview deploy accepts trusted
previewandpreview-idinputs for source fingerprinting and feedback outputs. Preview route/profile inputs remain rejected by source-link trigger mode.server-config-deploymay accept preview route and environment inputs only withpreview=pull-request;command: preview-cleanupaccepts 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-deployrequires explicit self-hosted server support. The action fails before source package handoff when the server handshake does not advertise the required capability.- In
server-config-deploymode,secret-variablessupports onlyKEY=ci-env:NAMEreferences. Missing runner environment values fail before the server API request, and raw secret values are not written to step summaries or PR comments. pr-commentrequires 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.
This action supports workflow-file previews. Product-grade previews with GitHub App webhooks, preview policy, comments/checks, cleanup retries, quotas, audit, and managed domain lifecycle are future Appaloft Cloud or self-hosted control-plane features.