Skip to content
Open
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
62 changes: 61 additions & 1 deletion .github/workflows/pullpreview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,64 @@ jobs:
ttl: 1h
env:
HCLOUD_TOKEN: "${{ secrets.HCLOUD_TOKEN }}"
HETZNER_CA_KEY: "${{ secrets.HETZNER_CA_KEY }}"
PULLPREVIEW_CA_KEY: "${{ secrets.PULLPREVIEW_CA_KEY || secrets.HETZNER_CA_KEY }}"

deploy_smoke_ec2:
runs-on: ubuntu-slim
if: github.event_name == 'schedule' || github.event.label.name == 'pullpreview' || contains(github.event.pull_request.labels.*.name, 'pullpreview')
timeout-minutes: 35
steps:
- uses: actions/checkout@v6

- name: Deploy smoke app on EC2
id: pullpreview
uses: "./"
with:
admins: "@collaborators/push"
app_path: ./examples/workflow-smoke
provider: ec2
region: us-east-1
image: al2023-ami-2023
instance_type: t3.small
dns: rev3.click
max_domain_length: 30
# required here because the mysql image is private in GHCR
registries: docker://${{ secrets.GHCR_PAT }}@ghcr.io
proxy_tls: web:8080
ttl: 1h
env:
AWS_ACCESS_KEY_ID: "${{ secrets.AWS_ACCESS_KEY_ID }}"
AWS_SECRET_ACCESS_KEY: "${{ secrets.AWS_SECRET_ACCESS_KEY }}"
AWS_REGION: "us-east-1"
PULLPREVIEW_CA_KEY: "${{ secrets.PULLPREVIEW_CA_KEY || secrets.HETZNER_CA_KEY }}"

- name: Assert deploy and seed state on EC2
if: steps.pullpreview.outputs.live == 'true'
shell: bash
env:
PREVIEW_URL: ${{ steps.pullpreview.outputs.url }}
run: |
set -euo pipefail

if [[ "${PREVIEW_URL}" != https://* ]]; then
echo "::error::Expected https preview URL when proxy_tls is enabled, got ${PREVIEW_URL}"
exit 1
fi

response=""
for attempt in $(seq 1 60); do
response="$(curl -fsSL --max-time 15 "${PREVIEW_URL}" || true)"
if printf '%s' "${response}" | grep -q 'Hello World Deploy 1' && \
printf '%s' "${response}" | grep -q 'seed_count=1' && \
printf '%s' "${response}" | grep -q 'seed_label=persisted'; then
echo "EC2 smoke checks passed for ${PREVIEW_URL}"
exit 0
fi

echo "Attempt ${attempt}/60: waiting for EC2 smoke response from ${PREVIEW_URL}"
sleep 5
done

echo "::error::Unexpected response from ${PREVIEW_URL}"
printf '%s\n' "${response}"
exit 1
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
GO ?= mise exec -- go
GO_TEST ?= $(GO) test ./internal/providers ./internal/pullpreview ./internal/providers/hetzner
GO_TEST ?= $(GO) test ./internal/providers ./internal/pullpreview ./internal/providers/hetzner ./internal/providers/ec2
DIST_DIR := dist
BIN_NAME := pullpreview
GO_LDFLAGS ?= -s -w
Expand Down
48 changes: 40 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ is made to Pull Requests labelled with the `pullpreview` label.
When triggered, it will:

1. Check out the repository code
2. Provision a preview instance (Lightsail by default, or Hetzner with `provider: hetzner`), with docker and docker-compose set up
2. Provision a preview instance (Lightsail by default, or `provider: hetzner` / `provider: ec2`), with docker and docker-compose set up
3. Continuously deploy the specified pull requests using your docker-compose file(s)
4. Report the preview instance URL in the GitHub UI

Expand Down Expand Up @@ -118,11 +118,11 @@ All supported `with:` inputs from `action.yml`:
| `compose_files` | `docker-compose.yml` | Comma-separated Compose files passed to deploy. |
| `compose_options` | `--build` | Additional options appended to `docker compose up`. |
| `license` | `""` | PullPreview license key. |
| `instance_type` | `small` | Provider-specific instance size (`small` for Lightsail, `cpx21` for Hetzner). |
| `instance_type` | `""` | Provider-specific instance size (defaults: Lightsail `small`, Hetzner `cpx21`, EC2 `t3.small`). |
| `region` | `` | Optional provider region/datacenter override (`AWS_REGION`/Hetzner location). If empty, provider defaults apply. |
| `image` | `ubuntu-24.04` | Instance image for Hetzner (provider-specific) and ignored for AWS. |
| `image` | `""` | Provider image selector: Hetzner image name, or EC2 AMI ID / AMI name prefix. |
| `deployment_variant` | `""` | Optional short suffix to run multiple preview environments per PR (max 4 chars). |
| `provider` | `lightsail` | Cloud provider (`lightsail`, `hetzner`). |
| `provider` | `lightsail` | Cloud provider (`lightsail`, `hetzner`, `ec2`). |
| `registries` | `""` | Private registry credentials, e.g. `docker://user:password@ghcr.io`. |
| `proxy_tls` | `""` | Automatic HTTPS forwarding with Caddy + Let's Encrypt (`service:port`, e.g. `web:80`). |
| `pre_script` | `""` | Path to a local shell script (relative to `app_path`) executed inline over SSH before compose deploy (should be self-contained). |
Expand All @@ -133,16 +133,19 @@ Notes:
- `proxy_tls` forces URL/output/comment links to HTTPS on port `443`, injects a Caddy proxy service, and suppresses firewall exposure for port `80`. **When using `proxy_tls`, it is strongly recommended to set `dns` to a [custom domain](https://github.com/pullpreview/action/wiki/Using-a-custom-domain) or one of the built-in `revN.click` alternatives** to avoid hitting shared Let's Encrypt rate limits on `my.preview.run`.
- `admins: "@collaborators/push"` uses GitHub API collaborators with push permission (first page, up to 100 users; warning is logged if more exist).
- SSH key fetches are cached between runs in the action cache.
- For Hetzner, configure credentials and defaults via action inputs and environment: `HCLOUD_TOKEN` (required), `HETZNER_CA_KEY` (required), optional `region` and `image` (`region` defaults to `nbg1`, `image` defaults to `ubuntu-24.04`). `instance_type` defaults to `cpx21` when provider is Hetzner.
- `HETZNER_CA_KEY` must be an SSH private key (RSA or Ed25519) for the instance-access CA. PullPreview signs a per-run ephemeral login key with this CA key and uses SSH certificates (`...-cert.pub`) instead of reusing a persistent private key across runs.
- CA key env is `PULLPREVIEW_CA_KEY` (canonical). For Hetzner, legacy `HETZNER_CA_KEY` is still accepted if canonical is unset.
- For Hetzner, configure credentials and defaults via action inputs and environment: `HCLOUD_TOKEN` (required), `PULLPREVIEW_CA_KEY` (required; legacy `HETZNER_CA_KEY` fallback), optional `region` and `image` (`region` defaults to `nbg1`, `image` defaults to `ubuntu-24.04`). `instance_type` defaults to `cpx21` when provider is Hetzner.
- For EC2, configure AWS credentials plus `PULLPREVIEW_CA_KEY` and set `provider: ec2`. PullPreview requires a pre-existing public subnet tagged `pullpreview-enabled=true` in the selected region.
- For EC2 `image`: when `image` starts with `ami-`, it is used directly. Otherwise `image` is treated as an AMI name prefix and PullPreview selects the newest available match from owners `self` + `amazon`. If `image` is empty, PullPreview uses the default Amazon Linux 2023 prefix.
- `PULLPREVIEW_CA_KEY` must be an SSH private key (RSA or Ed25519) for the instance-access CA. PullPreview signs a per-run ephemeral login key with this CA key and uses SSH certificates (`...-cert.pub`) instead of reusing a persistent private key across runs.
- Generate a CA key once for your repository secret:

```bash
ssh-keygen -t rsa -b 3072 -m PEM -N "" -f hetzner_ca_key
```

- **Let's Encrypt rate limits**: Let's Encrypt allows a maximum of [50 certificates per registered domain per week](https://letsencrypt.org/docs/rate-limits/#new-certificates-per-registered-domain). If you use `proxy_tls` and hit this limit on the default `my.preview.run` domain, switch to one of the built-in alternatives: `rev1.click`, `rev2.click`, ... `rev9.click`. Set `dns: rev1.click` in your workflow inputs. You can also use a [custom domain](https://github.com/pullpreview/action/wiki/Using-a-custom-domain).
- For local CLI runs, set `HCLOUD_TOKEN` and `HETZNER_CA_KEY` (for example via `.env`) when using `provider: hetzner` to avoid relying on action inputs.
- For local CLI runs, set provider-specific credentials plus `PULLPREVIEW_CA_KEY` (for example via `.env`).

## Example

Expand Down Expand Up @@ -221,10 +224,39 @@ jobs:
ttl: 1h
env:
HCLOUD_TOKEN: "${{ secrets.HCLOUD_TOKEN }}"
HETZNER_CA_KEY: "${{ secrets.HETZNER_CA_KEY }}"
PULLPREVIEW_CA_KEY: "${{ secrets.PULLPREVIEW_CA_KEY }}"

```

## EC2 example

```yaml
# .github/workflows/pullpreview-ec2.yml
name: PullPreview
on:
pull_request:
types: [labeled, unlabeled, synchronize, closed, reopened, opened]

jobs:
deploy_ec2:
runs-on: ubuntu-slim
if: github.event.label.name == 'pullpreview' || contains(github.event.pull_request.labels.*.name, 'pullpreview')
steps:
- uses: actions/checkout@v5
- uses: pullpreview/action@v6
with:
provider: ec2
# optional: AMI ID or AMI name prefix
image: al2023-ami-2023
# optional: raw EC2 instance type
instance_type: t3.small
env:
AWS_ACCESS_KEY_ID: "${{ secrets.AWS_ACCESS_KEY_ID }}"
AWS_SECRET_ACCESS_KEY: "${{ secrets.AWS_SECRET_ACCESS_KEY }}"
AWS_REGION: "us-east-1"
PULLPREVIEW_CA_KEY: "${{ secrets.PULLPREVIEW_CA_KEY }}"
```

## CLI usage (installed binary)

Pull the released CLI binary from GitHub Releases, install it in your PATH, then use:
Expand Down
10 changes: 5 additions & 5 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ inputs:
required: false
default: ""
instance_type:
description: "Instance type to use"
description: "Instance type to use (provider-specific)"
required: false
default: "small"
default: ""
region:
description: "Provider region (AWS region or Hetzner location), overrides provider defaults"
required: false
Expand All @@ -62,11 +62,11 @@ inputs:
required: false
default: ""
image:
description: "Instance image (Hetzner only; ignored by AWS)"
description: "Provider image selector (Hetzner image name, or EC2 AMI ID/name prefix)"
required: false
default: "ubuntu-24.04"
default: ""
provider:
description: "Cloud provider to use: lightsail, hetzner"
description: "Cloud provider to use: lightsail, hetzner, ec2"
required: false
default: "lightsail"
registries:
Expand Down
3 changes: 2 additions & 1 deletion cmd/pullpreview/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"syscall"

"github.com/pullpreview/action/internal/providers"
_ "github.com/pullpreview/action/internal/providers/ec2"
_ "github.com/pullpreview/action/internal/providers/hetzner"
_ "github.com/pullpreview/action/internal/providers/lightsail"
"github.com/pullpreview/action/internal/pullpreview"
Expand Down Expand Up @@ -199,7 +200,7 @@ func registerCommonFlags(fs *flag.FlagSet) *commonFlagValues {
fs.StringVar(&values.options.ProxyTLS, "proxy-tls", "", "Enable automatic HTTPS proxying with Let's Encrypt (format: service:port, e.g. web:80)")
fs.StringVar(&values.options.DNS, "dns", "my.preview.run", "DNS suffix to use")
fs.StringVar(&values.ports, "ports", "80/tcp,443/tcp", "Ports to open for external access")
fs.StringVar(&values.options.InstanceType, "instance-type", "small", "Instance type to use")
fs.StringVar(&values.options.InstanceType, "instance-type", "", "Instance type to use")
fs.StringVar(&values.options.DefaultPort, "default-port", "80", "Default port for URL")
fs.Var(&values.tags, "tags", "Tags to add to the instance (key:value), comma-separated")
fs.StringVar(&values.composeFiles, "compose-files", "docker-compose.yml", "Compose files to use")
Expand Down
Binary file modified dist/pullpreview-linux-amd64
Binary file not shown.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.25.1
require (
github.com/aws/aws-sdk-go-v2 v1.41.1
github.com/aws/aws-sdk-go-v2/config v1.32.7
github.com/aws/aws-sdk-go-v2/service/ec2 v1.289.0
github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.11
github.com/google/go-github/v60 v60.0.0
github.com/hetznercloud/hcloud-go/v2 v2.36.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/service/ec2 v1.289.0 h1:Ftj1M28RtAjgHpycBeQaFhfGx+aQ/swYEz+tBtIh9nE=
github.com/aws/aws-sdk-go-v2/service/ec2 v1.289.0/go.mod h1:Uy+C+Sc58jozdoL1McQr8bDsEvNFx+/nBY+vpO1HVUY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY=
Expand Down
Loading
Loading