From 586fc977bcb53d6e8e23caedb01a63eeb775697e Mon Sep 17 00:00:00 2001 From: Facundo Farias Date: Sat, 23 May 2026 10:04:30 +0200 Subject: [PATCH] Rewrite action as composite wrapper around the dhq CLI (v2) Replaces the v1 Docker/Alpine action that POSTed to a DeployHQ webhook URL with a composite action that installs and invokes the official dhq CLI (deployhq/deployhq-cli). The same tool now backs customer dashboards, agents, and CI. Architecture - runs.using: composite (Linux/macOS/Windows, amd64/arm64). - scripts/install-cli.sh downloads the pinned dhq release from GitHub Releases, verifies SHA-256 against checksums.txt, and caches under RUNNER_TOOL_CACHE. - scripts/deploy.sh builds the dhq deploy --json --non-interactive argv from INPUT_* env vars, parses the envelope with jq, and emits action outputs + a step summary table. - --wait is implemented in deploy.sh (polling dhq deployments show) because the CLI's --wait is a no-op in JSON mode. Inputs / outputs - New required inputs: api-key, account, email (replaces the webhook URL). - New optional inputs: project, server, wait, timeout, dry-run, full, start-revision, extra-args, cli-version. - Outputs: deployment_id, deployment_url, status, server, project. Removed - Dockerfile, entrypoint.sh. - DEPLOYHQ_WEBHOOK_URL, REPO_CLONE_URL inputs. Docs / CI - README rewritten with input/output tables and a v1 -> v2 migration guide. - CHANGELOG.md documents the break. - CLAUDE.md updated for the new shape. - .github/workflows/test.yml: cross-platform install matrix + gated dry-run e2e job. Pinned CLI: v0.17.1. Legacy behaviour stays available at @v1. --- .github/workflows/test.yml | 72 ++++++++++++++++ CHANGELOG.md | 29 +++++++ CLAUDE.md | 56 ++++++++++++ Dockerfile | 16 ---- README.md | 168 +++++++++++++++++++++++++++++------- action.yml | 118 ++++++++++++++++++++++++-- entrypoint.sh | 73 ---------------- scripts/deploy.sh | 169 +++++++++++++++++++++++++++++++++++++ scripts/install-cli.sh | 80 ++++++++++++++++++ 9 files changed, 654 insertions(+), 127 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 CHANGELOG.md create mode 100644 CLAUDE.md delete mode 100644 Dockerfile delete mode 100755 entrypoint.sh create mode 100755 scripts/deploy.sh create mode 100755 scripts/install-cli.sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..f597c51 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,72 @@ +name: Test + +on: + push: + branches: [main, v2] + pull_request: + workflow_dispatch: + +jobs: + # Cross-platform smoke test: install the pinned CLI on every supported runner + # and confirm it executes. Doesn't touch any DeployHQ resources. + install: + name: Install CLI (${{ matrix.os }}) + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - name: Run install-cli.sh + shell: bash + env: + DHQ_VERSION: v0.17.1 + run: ./scripts/install-cli.sh + + - name: dhq --version + shell: bash + run: dhq --version + + # End-to-end dry-run against a real DeployHQ account. Gated on secrets so + # external PRs don't fail; runs on push to v2/main and on workflow_dispatch. + dry-run: + name: dhq deploy --dry-run + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + steps: + - uses: actions/checkout@v4 + + - name: Skip if secrets are missing + id: gate + env: + API_KEY: ${{ secrets.TEST_DEPLOYHQ_API_KEY }} + run: | + if [[ -z "$API_KEY" ]]; then + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "::notice::TEST_DEPLOYHQ_API_KEY not set; skipping dry-run job." + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - name: Run the action (dry-run) + if: steps.gate.outputs.skip == 'false' + id: deploy + uses: ./ + with: + api-key: ${{ secrets.TEST_DEPLOYHQ_API_KEY }} + account: ${{ secrets.TEST_DEPLOYHQ_ACCOUNT }} + email: ${{ secrets.TEST_DEPLOYHQ_EMAIL }} + project: ${{ secrets.TEST_DEPLOYHQ_PROJECT }} + server: ${{ secrets.TEST_DEPLOYHQ_SERVER }} + dry-run: "true" + wait: "false" + + - name: Assert outputs are populated + if: steps.gate.outputs.skip == 'false' + run: | + test -n "${{ steps.deploy.outputs.deployment_id }}" \ + || { echo "deployment_id was empty"; exit 1; } + echo "deployment_id=${{ steps.deploy.outputs.deployment_id }}" + echo "status=${{ steps.deploy.outputs.status }}" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..752478f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,29 @@ +# Changelog + +## v2.0.0 — Switch to the official DeployHQ CLI + +This is a breaking rewrite. The action now wraps the [`dhq` CLI](https://github.com/deployhq/deployhq-cli) instead of POSTing to a webhook URL. To stay on the old behaviour, pin `@v1`. + +### Breaking changes + +- **Auth**: webhook URL replaced by API key. New required inputs: `api-key`, `account`, `email`. +- **Input style**: inputs are now declared properly (`with:`) instead of passed via `env:`. The legacy `DEPLOYHQ_*` env vars are no longer recognised. +- **Removed inputs**: `DEPLOYHQ_WEBHOOK_URL`, `REPO_CLONE_URL`. The CLI resolves the repository from the project configuration. +- **Defaults changed**: `revision` defaults to `${{ github.sha }}` (was `"latest"`). `branch` no longer defaults to `main` — the CLI auto-resolves it from server config. +- **Runtime**: Docker (Alpine) action replaced by a composite action. Runs on Linux, macOS, and Windows runners, including self-hosted. + +### Added + +- New inputs: `project`, `server`, `wait`, `timeout`, `dry-run`, `full`, `start-revision`, `extra-args`, `cli-version`. +- New outputs: `deployment_id`, `deployment_url`, `status`, `server`, `project`. +- Built-in wait-for-completion with timeout (polls `dhq deployments show` until terminal). +- Job step summary table with deployment ID, status, project, server, and link. +- Pinned CLI version with SHA-256 checksum verification on download. + +### Migration + +See the **Migration from v1** section in `README.md`. + +## v1.x — Legacy Docker webhook action + +Final webhook-based release. Maintained on the `v1` tag only. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9918e0a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,56 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +A GitHub composite action that triggers a deployment on DeployHQ by invoking the official `dhq` CLI (). v1 (Docker + `curl` to a webhook URL) lives on the `v1` git tag and is no longer maintained on `main`. + +## Architecture + +`runs.using: composite` in `action.yml`. Two steps: + +1. **Install dhq CLI** — `scripts/install-cli.sh` detects `RUNNER_OS`/`RUNNER_ARCH`, downloads the pinned `dhq` archive from `github.com/deployhq/deployhq-cli/releases`, verifies its SHA-256 against the release's `checksums.txt`, extracts to `$RUNNER_TOOL_CACHE/dhq//_`, and appends that dir to `$GITHUB_PATH`. Cross-platform: Linux, macOS, Windows (Git Bash); amd64 + arm64. +2. **Deploy** — `scripts/deploy.sh` builds the `dhq deploy --json --non-interactive` argv from `INPUT_*` env vars, runs the CLI, parses the JSON envelope with `jq`, and emits action outputs + a `$GITHUB_STEP_SUMMARY` table. + +### The `--wait` trick + +`dhq deploy --json` returns immediately after queueing — its built-in `--wait` is a no-op in JSON mode (see `internal/commands/deploy.go` in the CLI). So `scripts/deploy.sh` implements waiting itself by polling `dhq deployments show -p --json` every 5s until the status is terminal (`completed`/`failed`/`cancelled`), respecting `INPUT_TIMEOUT`. If this changes upstream, the wait loop can be removed. + +### Auth + +Three required env vars passed straight through to the CLI: `DEPLOYHQ_API_KEY`, `DEPLOYHQ_ACCOUNT`, `DEPLOYHQ_EMAIL`. No `dhq auth login` call — the CLI reads these directly. + +### Outputs + +`scripts/deploy.sh` writes to `$GITHUB_OUTPUT`: `deployment_id`, `deployment_url`, `status`, `server`, `project`. The URL is constructed locally as `https://${DEPLOYHQ_ACCOUNT}.deployhq.com/projects//deployments/` — the API doesn't return one. If the URL pattern changes server-side, fix it here. + +### CLI version pin + +The pinned default lives in **one place only**: `inputs.cli-version.default` in `action.yml`. Bump it there when validating against a new CLI release. Users can override per-workflow via the `cli-version` input. + +## Things to know before editing + +- **Two shells, one language.** Scripts use bash (`#!/usr/bin/env bash`, `set -euo pipefail`). On Windows runners GitHub's `shell: bash` picks Git Bash; `jq`, `tar`, `unzip`, `curl`, and `shasum`/`sha256sum` are all available there. Don't assume GNU-only flags. +- **`jq` is a hard dependency.** Preinstalled on all GitHub-hosted runners; self-hosted users must install it. The script fails fast with a clear message. +- **Exit codes are meaningful.** `0` success/queued, `1` deploy failed, `2` cancelled, `124` wait timeout, anything else = CLI error propagated. +- **`extra-args` is word-split unquoted by design** (escape hatch for new CLI flags). Don't quote it. +- **No `Dockerfile`/`entrypoint.sh`** — those live on `@v1`. Don't reintroduce them. +- Consumers can pin `@v2`, `@v2.x.y`, `@v1` (legacy), or `@main` (rolling). + +## Local testing + +```sh +DHQ_VERSION=v0.17.1 \ +RUNNER_OS=$(uname -s) RUNNER_ARCH=$(uname -m) \ +GITHUB_ACTION_PATH="$PWD" GITHUB_PATH=/tmp/gh-path \ +./scripts/install-cli.sh + +DEPLOYHQ_API_KEY=... DEPLOYHQ_ACCOUNT=... DEPLOYHQ_EMAIL=... \ +INPUT_PROJECT=... INPUT_SERVER=... INPUT_REVISION=... \ +INPUT_WAIT=false INPUT_DRY_RUN=true \ +GITHUB_OUTPUT=/tmp/gh-output GITHUB_STEP_SUMMARY=/tmp/gh-summary \ +./scripts/deploy.sh +``` + +(Set `GITHUB_OUTPUT`/`GITHUB_STEP_SUMMARY` to writable file paths when running outside Actions; they're appended-to, not created.) diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 819daf6..0000000 --- a/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -FROM alpine:latest - -LABEL "com.github.actions.name"="DeployHQ Action" -LABEL "com.github.actions.description"="Trigger a DeployHQ deployment using Github Actions" -LABEL "com.github.actions.icon"="upload-cloud" -LABEL "com.github.actions.color"="purple" - -LABEL version="0.0.1" -LABEL repository="https://github.com/deployhq/deployhq-action" -LABEL homepage="https://www.deployhq.com/" -LABEL maintainer="Support DeployHQ " - -RUN apk update && apk add openssl curl - -ADD entrypoint.sh /entrypoint.sh -ENTRYPOINT ["/entrypoint.sh"] diff --git a/README.md b/README.md index c43e036..897d8df 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,152 @@ -# GitHub Action to Trigger a Deployment on DeployHQ with a Webhook URL 🚀 +# DeployHQ — GitHub Action -This action calls the DeployHQ's [Webhook URL](https://www.deployhq.com/support/deployments/automatic-deployments/custom) created for your DeployHQ account to trigger a deployment on DeployHQ. +Trigger a deployment on [DeployHQ](https://www.deployhq.com/) from a GitHub workflow. Wraps the official [`dhq` CLI](https://github.com/deployhq/deployhq-cli) so customers, agents, and CI all share one tool. -## Usage +> **Looking for the legacy webhook action?** That's v1. Pin `deployhq/deployhq-action@v1` to keep the old behaviour. See [Migration from v1](#migration-from-v1) below. -All sensitive variables should be [set as encrypted secrets](https://help.github.com/en/articles/virtual-environments-for-github-actions#creating-and-using-secrets-encrypted-variables) in the action's configuration. - -### Configuration Variables - -| Key | Value | Suggested Type | Required | -|----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------- | ------------- | -| `DEPLOYHQ_WEBHOOK_URL` | **Required.** Your DeployHQ webhook URL. Can be found in your DeployHQ Dashboard, under "Automatic Deployments" | `secret` | **Yes** | -| `REPO_REVISION` | The revision you wish to deploy. Can also be set to "latest" if you wish to deploy the latest revision in your set branch. If not set, the default value is "latest". | `secret` | **No** | -| `REPO_BRANCH` | The branch your revision is on. If not set, the default value is set to "main". | `secret` | **No** | -| `DEPLOYHQ_EMAIL` | **Required.** Your DeployHQ user. For example, matias@barilla.com. | `secret` | **Yes** | -| `REPO_CLONE_URL` | The path to your repository (as entered in the Deploy UI). If not set, it will be generated with [GitHub Action's default environment variables](https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables). Nonetheless, we highly recommend setting this variable to avoid unexpected results. | `secret` | **No** | - -### `workflow.yml` Example - -Place in a `.yml` file such as this one in your `.github/workflows` folder. [Refer to the documentation on workflow YAML syntax here.](https://help.github.com/en/articles/workflow-syntax-for-github-actions) +## Quick start ```yaml -name: Deploy my website in DeployHQ w/ my webhook URL -on: push +name: Deploy +on: + push: + branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: + - name: Trigger DeployHQ deployment + uses: deployhq/deployhq-action@v2 + with: + api-key: ${{ secrets.DEPLOYHQ_API_KEY }} + account: ${{ secrets.DEPLOYHQ_ACCOUNT }} + email: ${{ secrets.DEPLOYHQ_EMAIL }} + project: my-project + server: production +``` + +The action installs the pinned `dhq` CLI on the runner, calls `dhq deploy`, waits for the deployment to reach a terminal status, and fails the job if it didn't succeed. + +## Inputs + +| Name | Required | Default | Description | +|---|---|---|---| +| `api-key` | **yes** | — | DeployHQ API key. Generate one in **Account Settings → API access**. | +| `account` | **yes** | — | Your DeployHQ account subdomain (e.g. `acme` for `acme.deployhq.com`). | +| `email` | **yes** | — | DeployHQ user email associated with the API key. | +| `project` | no | `""` | Project identifier or permalink. Falls back to `DEPLOYHQ_PROJECT` if unset. | +| `server` | no | `""` | Server identifier or name. Fuzzy-matched. Auto-selected if the project has only one server. | +| `revision` | no | `${{ github.sha }}` | Commit SHA to deploy. | +| `branch` | no | `""` | Branch the revision lives on. Auto-resolved from server config if omitted. | +| `wait` | no | `"true"` | Block until the deployment reaches a terminal status. Job exit code reflects the result. | +| `timeout` | no | `"0"` | Max seconds to wait when `wait=true`. `0` waits indefinitely. | +| `dry-run` | no | `"false"` | Preview the deploy without executing it. | +| `full` | no | `"false"` | Deploy the entire branch from the first commit (`--full`). | +| `start-revision` | no | `""` | Start an incremental deploy from this commit. | +| `extra-args` | no | `""` | Additional raw flags appended to `dhq deploy`. Escape hatch for newer CLI flags. | +| `cli-version` | no | pinned | Pin a specific `dhq` CLI release (e.g. `v0.17.1`). Defaults to the version this action was tested against. | + +## Outputs + +| Name | Description | +|---|---| +| `deployment_id` | DeployHQ deployment identifier (e.g. `dep-abc123`). | +| `deployment_url` | Web URL of the deployment in DeployHQ. | +| `status` | Final status (`completed`/`failed`/`cancelled`/`timeout`) when `wait=true`, else the queued status. | +| `server` | Resolved server identifier. | +| `project` | Resolved project permalink. | + +```yaml +- id: deploy + uses: deployhq/deployhq-action@v2 + with: + api-key: ${{ secrets.DEPLOYHQ_API_KEY }} + account: ${{ secrets.DEPLOYHQ_ACCOUNT }} + email: ${{ secrets.DEPLOYHQ_EMAIL }} + project: my-project + server: production + +- name: Open deployment + if: success() + run: echo "Deployed → ${{ steps.deploy.outputs.deployment_url }}" +``` + +## Common patterns + +### Don't block the workflow on the deploy + +```yaml +- uses: deployhq/deployhq-action@v2 + with: + api-key: ${{ secrets.DEPLOYHQ_API_KEY }} + account: ${{ secrets.DEPLOYHQ_ACCOUNT }} + email: ${{ secrets.DEPLOYHQ_EMAIL }} + project: my-project + server: production + wait: "false" +``` + +### Dry-run on pull requests + +```yaml +- uses: deployhq/deployhq-action@v2 + with: + api-key: ${{ secrets.DEPLOYHQ_API_KEY }} + account: ${{ secrets.DEPLOYHQ_ACCOUNT }} + email: ${{ secrets.DEPLOYHQ_EMAIL }} + project: my-project + server: staging + dry-run: "true" +``` + +### Pass through a flag the action doesn't expose yet + +```yaml +- uses: deployhq/deployhq-action@v2 + with: + api-key: ${{ secrets.DEPLOYHQ_API_KEY }} + account: ${{ secrets.DEPLOYHQ_ACCOUNT }} + email: ${{ secrets.DEPLOYHQ_EMAIL }} + project: my-project + server: production + extra-args: "--copy-config --run-build" +``` - # Put steps here to build your site, deploy it to a service, etc. - - name: Trigger deployment in DeployHQ w/ webhook URL - uses: deployhq/deployhq-action@main - env: - # All these values should be set as encrypted secrets in your repository settings - DEPLOYHQ_WEBHOOK_URL: ${{ secrets.DEPLOYHQ_WEBHOOK_URL }} - REPO_REVISION: ${{ secrets.REPO_REVISION }} - REPO_BRANCH: ${{ secrets.REPO_BRANCH }} - DEPLOYHQ_EMAIL: ${{ secrets.DEPLOYHQ_EMAIL }} - REPO_CLONE_URL: ${{ secrets.REPO_CLONE_URL }} +## Requirements + +- A DeployHQ API key (**Account Settings → API access**). +- Runner with `bash`, `curl`, and `jq` available. GitHub-hosted runners (Ubuntu, macOS, Windows) ship with all three. Self-hosted runners must install `jq`. + +## Migration from v1 + +v1 was a Docker action that POSTed to a webhook URL. v2 is a composite action that calls the `dhq` CLI directly. + +| v1 input (env var) | v2 input | Notes | +|---|---|---| +| `DEPLOYHQ_WEBHOOK_URL` | _(removed)_ | Replaced by API key auth. | +| `DEPLOYHQ_EMAIL` | `email` | Now declared as a proper action input. | +| `REPO_REVISION` | `revision` | Defaults to `github.sha` instead of `"latest"`. | +| `REPO_BRANCH` | `branch` | Default `main` removed — CLI auto-resolves from server config. | +| `REPO_CLONE_URL` | _(removed)_ | CLI looks up the repo from the project config. | +| _(n/a)_ | `api-key`, `account` | **New required inputs.** | +| _(n/a)_ | `server`, `project` | Target a specific server/project. | +| _(n/a)_ | `wait`, `timeout`, `dry-run`, `full`, `start-revision`, `extra-args`, `cli-version` | New behaviour controls. | + +**To stay on v1**, pin to it: + +```yaml +uses: deployhq/deployhq-action@v1 ``` +**To migrate to v2:** + +1. Generate an API key in DeployHQ (**Account Settings → API access**). +2. Add `DEPLOYHQ_API_KEY` and `DEPLOYHQ_ACCOUNT` as repository secrets. (`DEPLOYHQ_EMAIL` you already have.) +3. Switch your workflow to `with:` syntax (see [Quick start](#quick-start)). +4. Set `project:` and `server:` explicitly — the webhook implied these; the API requires them. +5. Remove `DEPLOYHQ_WEBHOOK_URL` from your secrets when no other workflow uses it. + ## License -This project is distributed under the [MIT license](LICENSE.md). +MIT. See [LICENSE](LICENSE). diff --git a/action.yml b/action.yml index 54f33fb..1d8d8b5 100644 --- a/action.yml +++ b/action.yml @@ -1,9 +1,113 @@ -name: "DeployHQ - Trigger a Deployment with a webhook" -description: "Trigger a given deployment using DeployHQ's Webhook." -author: facundofarias -runs: - using: docker - image: Dockerfile +name: "DeployHQ — Deploy via CLI" +description: "Trigger a DeployHQ deployment using the official dhq CLI." +author: deployhq branding: color: purple - icon: upload-cloud \ No newline at end of file + icon: upload-cloud + +inputs: + # --- Auth (required) --- + api-key: + description: "DeployHQ API key. Pass as a secret." + required: true + account: + description: "DeployHQ account subdomain (e.g. 'acme' for acme.deployhq.com)." + required: true + email: + description: "DeployHQ user email." + required: true + + # --- Deploy target --- + project: + description: "Project identifier or permalink. Falls back to DEPLOYHQ_PROJECT if unset." + required: false + default: "" + server: + description: "Server identifier or name (fuzzy-matched). Auto-selected if the project has only one server." + required: false + default: "" + revision: + description: "Commit SHA to deploy. Defaults to github.sha." + required: false + default: ${{ github.sha }} + branch: + description: "Branch the revision lives on. CLI auto-resolves from server config if omitted." + required: false + default: "" + + # --- Behaviour --- + wait: + description: "Block until the deployment reaches a terminal status (completed/failed/cancelled). Action exit code reflects the result." + required: false + default: "true" + timeout: + description: "Max seconds to wait when wait=true. 0 (default) waits indefinitely." + required: false + default: "0" + dry-run: + description: "Preview the deploy without executing it." + required: false + default: "false" + full: + description: "Deploy the entire branch from the first commit (--full)." + required: false + default: "false" + start-revision: + description: "Start an incremental deploy from this commit." + required: false + default: "" + extra-args: + description: "Additional raw flags appended to `dhq deploy`. Escape hatch for newer CLI flags." + required: false + default: "" + + # --- CLI install --- + cli-version: + description: "Pinned dhq CLI version (with or without leading 'v'). Override to track a newer or older release." + required: false + default: "v0.17.1" + +outputs: + deployment_id: + description: "DeployHQ deployment identifier." + value: ${{ steps.deploy.outputs.deployment_id }} + deployment_url: + description: "Web URL of the deployment in DeployHQ." + value: ${{ steps.deploy.outputs.deployment_url }} + status: + description: "Final deployment status when wait=true, or the queued status otherwise." + value: ${{ steps.deploy.outputs.status }} + server: + description: "Resolved server identifier." + value: ${{ steps.deploy.outputs.server }} + project: + description: "Resolved project permalink." + value: ${{ steps.deploy.outputs.project }} + +runs: + using: composite + steps: + - name: Install dhq CLI + shell: bash + env: + DHQ_VERSION: ${{ inputs.cli-version }} + run: ${{ github.action_path }}/scripts/install-cli.sh + + - name: Deploy + id: deploy + shell: bash + env: + DEPLOYHQ_API_KEY: ${{ inputs.api-key }} + DEPLOYHQ_ACCOUNT: ${{ inputs.account }} + DEPLOYHQ_EMAIL: ${{ inputs.email }} + INPUT_PROJECT: ${{ inputs.project }} + INPUT_SERVER: ${{ inputs.server }} + INPUT_REVISION: ${{ inputs.revision }} + INPUT_BRANCH: ${{ inputs.branch }} + INPUT_WAIT: ${{ inputs.wait }} + INPUT_TIMEOUT: ${{ inputs.timeout }} + INPUT_DRY_RUN: ${{ inputs.dry-run }} + INPUT_FULL: ${{ inputs.full }} + INPUT_START_REVISION: ${{ inputs.start-revision }} + INPUT_EXTRA_ARGS: ${{ inputs.extra-args }} + run: ${{ github.action_path }}/scripts/deploy.sh diff --git a/entrypoint.sh b/entrypoint.sh deleted file mode 100755 index 16ef66b..0000000 --- a/entrypoint.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/bin/sh - -set -e - -######## Check for required/optional inputs. ######## - -# Check if DeployHQ webhook URL is set (Required). -# Found in DeployHQ's dashboard, under "Automatic Deployments". -if [ -z "$DEPLOYHQ_WEBHOOK_URL" ]; then - echo "DEPLOYHQ_WEBHOOK_URL is not set. Please set your DEPLOYHQ_WEBHOOK_URL variable before retrying. Quitting." - exit 1 -fi - -# Check if revision is set (Required). -# Can be set to "latest". -if [ -z "$REPO_REVISION" ]; then - REPO_REVISION="latest" - echo "REPO_REVISION is not set. Setting to "latest" by default. If this is incorrect, please set "REPO_REVISION" to your preferred branch and retry." -fi - -# Check if repo branch is set (Required). -if [ -z "$REPO_BRANCH" ]; then - REPO_BRANCH="main" - echo "REPO_BRANCH is not set. Setting to "main" by default. If this is incorrect, please set "REPO_BRANCH" to your preferred branch and retry." -fi - -# Check if the email is set (Required). -# If request is sent without email, 500 "INTERNAL_ERROR" response -if [ -z "$DEPLOYHQ_EMAIL" ]; then - echo "DEPLOYHQ_EMAIL is not set. Please set your DEPLOYHQ_EMAIL variable before retrying. Quitting." - exit 1 -fi - -# Check if repo clone URL is set (Required). -# URL must follow the SSH pattern: "git@yourhost:path". -### ${TESTING_SUBSTRING#*//} -> removes all prefix until "//", used for domain substring, as GITHUB_SERVER_URL is given with "https://"." -if [ -z "$REPO_CLONE_URL" ]; then - REPO_CLONE_URL="git@${GITHUB_SERVER_URL#*//}:${GITHUB_REPOSITORY}.git" - echo "REPO_CLONE_URL is not set. Setting REPO_CLONE_URL as ${REPO_CLONE_URL}." -fi - - - -set -- --data '{"payload":{ "new_ref":"'"${REPO_REVISION}"'","branch":"'"${REPO_BRANCH}"'","email":"'"${DEPLOYHQ_EMAIL}"'","clone_url":"'"${REPO_CLONE_URL}"'"}}' - -######## Call the webhook API (POST) and store the response for later. ######## -# Payload reference: https://www.deployhq.com/support/deployments/automatic-deployments/custom -# For full API call reference, send a GET API call to "DEPLOYHQ_WEBHOOK_URL". - -HTTP_RESPONSE=$(curl -sS "${DEPLOYHQ_WEBHOOK_URL}" \ - -H "Content-type: application/json" \ - -H "Accept: application/json" \ - -w "HTTP_STATUS:%{http_code}" \ - "$@") - -######## Format response for a pretty command line output. ######## - -# Store result and HTTP status code separately to appropriately throw CI errors. -# https://gist.github.com/maxcnunes/9f77afdc32df354883df - -HTTP_BODY=$(echo "${HTTP_RESPONSE}" | sed -E 's/HTTP_STATUS\:[0-9]{3}$//') -HTTP_STATUS=$(echo "${HTTP_RESPONSE}" | tr -d '\n' | sed -E 's/.*HTTP_STATUS:([0-9]{3})$/\1/') - -# Fail pipeline and print errors if API doesn't return an OK status. -if [ "${HTTP_STATUS}" -eq "200" ]; then - echo "Successfully triggered deployment on DeployBot!" - echo "${HTTP_BODY}" - exit 0 -else - echo "Trigger deployment failed. API response was: " - echo "${HTTP_BODY}" - exit 1 -fi \ No newline at end of file diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..53abd6a --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,169 @@ +#!/usr/bin/env bash +# +# Invoke `dhq deploy` with arguments derived from INPUT_* env vars, parse the +# JSON envelope, and emit action outputs + a step summary. +# +# In JSON mode the CLI returns immediately after queueing (see +# internal/commands/deploy.go ~L434), so this script handles --wait itself by +# polling `dhq deployments show` until the deployment reaches a terminal status. +set -euo pipefail + +require_jq() { + if ! command -v jq >/dev/null 2>&1; then + echo "jq is required but was not found on PATH." >&2 + echo "GitHub-hosted runners ship with jq; install it on self-hosted runners." >&2 + exit 1 + fi +} + +truthy() { + case "${1:-}" in + true|TRUE|True|1|yes|YES) return 0 ;; + *) return 1 ;; + esac +} + +emit_output() { + local name="$1" value="$2" + if [[ "$value" == *$'\n'* ]]; then + local delim="ghadelim_$RANDOM" + printf '%s<<%s\n%s\n%s\n' "$name" "$delim" "$value" "$delim" >> "$GITHUB_OUTPUT" + else + printf '%s=%s\n' "$name" "$value" >> "$GITHUB_OUTPUT" + fi +} + +require_jq + +# --- Build dhq deploy argv ----------------------------------------------------- + +args=(deploy --json --non-interactive) + +[[ -n "${INPUT_PROJECT:-}" ]] && args+=(--project "$INPUT_PROJECT") +[[ -n "${INPUT_SERVER:-}" ]] && args+=(--server "$INPUT_SERVER") +[[ -n "${INPUT_REVISION:-}" ]] && args+=(--revision "$INPUT_REVISION") +[[ -n "${INPUT_BRANCH:-}" ]] && args+=(--branch "$INPUT_BRANCH") +[[ -n "${INPUT_START_REVISION:-}" ]] && args+=(--start-revision "$INPUT_START_REVISION") +[[ -n "${INPUT_TIMEOUT:-}" ]] && args+=(--timeout "$INPUT_TIMEOUT") +truthy "${INPUT_FULL:-}" && args+=(--full) +truthy "${INPUT_DRY_RUN:-}" && args+=(--dry-run) + +# --wait is handled by this script, not the CLI (see header). + +if [[ -n "${INPUT_EXTRA_ARGS:-}" ]]; then + # shellcheck disable=SC2206 + extra=( $INPUT_EXTRA_ARGS ) + args+=("${extra[@]}") +fi + +echo "::group::dhq ${args[*]}" +set +e +deploy_output=$(dhq "${args[@]}") +deploy_exit=$? +set -e +printf '%s\n' "$deploy_output" +echo "::endgroup::" + +if [[ $deploy_exit -ne 0 ]]; then + echo "dhq deploy exited $deploy_exit" >&2 + # Still try to surface envelope error data if we got any. + if [[ -n "$deploy_output" ]] && echo "$deploy_output" | jq -e . >/dev/null 2>&1; then + echo "$deploy_output" | jq -r '.data.error // empty' >&2 || true + fi + exit "$deploy_exit" +fi + +# --- Parse envelope ----------------------------------------------------------- + +if ! echo "$deploy_output" | jq -e '.ok == true' >/dev/null 2>&1; then + echo "dhq deploy returned a non-ok envelope:" >&2 + echo "$deploy_output" >&2 + exit 1 +fi + +deployment_id=$(echo "$deploy_output" | jq -r '.data.identifier // empty') +status=$(echo "$deploy_output" | jq -r '.data.status // empty') +project_permalink=$(echo "$deploy_output" | jq -r '.data.project.permalink // empty') +server_id=$(echo "$deploy_output" | jq -r '.data.servers[0].identifier // empty') + +if [[ -z "$deployment_id" ]]; then + echo "Could not extract deployment identifier from response." >&2 + exit 1 +fi + +# Project identifier used for follow-up calls — prefer the value the user passed +# in (it's what the CLI was configured with), fall back to the permalink. +project_for_polling="${INPUT_PROJECT:-$project_permalink}" + +deployment_url="" +if [[ -n "${DEPLOYHQ_ACCOUNT:-}" && -n "$project_permalink" ]]; then + deployment_url="https://${DEPLOYHQ_ACCOUNT}.deployhq.com/projects/${project_permalink}/deployments/${deployment_id}" +fi + +# --- Wait loop ---------------------------------------------------------------- + +if truthy "${INPUT_WAIT:-}" && ! truthy "${INPUT_DRY_RUN:-}"; then + timeout_seconds="${INPUT_TIMEOUT:-0}" + started=$(date +%s) + poll_interval=5 + + echo "Waiting for deployment ${deployment_id} to complete..." + while true; do + if [[ "$timeout_seconds" -gt 0 ]]; then + elapsed=$(( $(date +%s) - started )) + if [[ $elapsed -ge $timeout_seconds ]]; then + echo "Timed out after ${timeout_seconds}s waiting for deployment ${deployment_id}" >&2 + status="timeout" + break + fi + fi + + sleep "$poll_interval" + + show_output=$(dhq deployments show "$deployment_id" -p "$project_for_polling" --json --non-interactive 2>/dev/null || true) + if [[ -z "$show_output" ]] || ! echo "$show_output" | jq -e '.ok == true' >/dev/null 2>&1; then + echo "Failed to fetch deployment status; will retry..." + continue + fi + + status=$(echo "$show_output" | jq -r '.data.status // empty') + echo " status: ${status}" + + case "$status" in + completed|failed|cancelled) break ;; + esac + done +fi + +# --- Outputs ------------------------------------------------------------------ + +emit_output "deployment_id" "$deployment_id" +emit_output "deployment_url" "$deployment_url" +emit_output "status" "$status" +emit_output "server" "$server_id" +emit_output "project" "$project_permalink" + +# --- Step summary ------------------------------------------------------------- + +if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then + { + echo "### DeployHQ deployment" + echo "" + echo "| Field | Value |" + echo "| --- | --- |" + echo "| ID | \`${deployment_id}\` |" + [[ -n "$status" ]] && echo "| Status | \`${status}\` |" + [[ -n "$project_permalink" ]] && echo "| Project | \`${project_permalink}\` |" + [[ -n "$server_id" ]] && echo "| Server | \`${server_id}\` |" + [[ -n "$deployment_url" ]] && echo "| URL | <${deployment_url}> |" + } >> "$GITHUB_STEP_SUMMARY" +fi + +# --- Exit code ---------------------------------------------------------------- + +case "$status" in + failed) exit 1 ;; + timeout) exit 124 ;; + cancelled) exit 2 ;; + *) exit 0 ;; +esac diff --git a/scripts/install-cli.sh b/scripts/install-cli.sh new file mode 100755 index 0000000..05aa7b2 --- /dev/null +++ b/scripts/install-cli.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# +# Install the dhq CLI at the version pinned by DHQ_VERSION. +# Detects RUNNER_OS/RUNNER_ARCH, downloads the matching release archive from +# github.com/deployhq/deployhq-cli, verifies its SHA-256 against the release's +# checksums.txt, extracts to a per-version cache directory, and appends the +# directory to $GITHUB_PATH so the next step can call `dhq` directly. +set -euo pipefail + +: "${DHQ_VERSION:?DHQ_VERSION must be set}" +VERSION="${DHQ_VERSION#v}" + +REPO="deployhq/deployhq-cli" + +case "${RUNNER_OS:-$(uname -s)}" in + Linux|linux) OS="linux"; EXT="tar.gz"; BIN_NAME="dhq" ;; + macOS|Darwin|darwin) OS="darwin"; EXT="tar.gz"; BIN_NAME="dhq" ;; + Windows|MINGW*|MSYS*|CYGWIN*) OS="windows"; EXT="zip"; BIN_NAME="dhq.exe" ;; + *) echo "Unsupported OS: ${RUNNER_OS:-$(uname -s)}" >&2; exit 1 ;; +esac + +case "${RUNNER_ARCH:-$(uname -m)}" in + X64|x86_64|amd64) ARCH="amd64" ;; + ARM64|arm64|aarch64) ARCH="arm64" ;; + *) echo "Unsupported arch: ${RUNNER_ARCH:-$(uname -m)}" >&2; exit 1 ;; +esac + +CACHE_ROOT="${RUNNER_TOOL_CACHE:-${GITHUB_ACTION_PATH}/.cache}" +INSTALL_DIR="${CACHE_ROOT}/dhq/${VERSION}/${OS}_${ARCH}" +mkdir -p "$INSTALL_DIR" + +if [[ -x "${INSTALL_DIR}/${BIN_NAME}" ]]; then + echo "dhq v${VERSION} already cached at ${INSTALL_DIR}" +else + ARCHIVE="dhq_${VERSION}_${OS}_${ARCH}.${EXT}" + BASE_URL="https://github.com/${REPO}/releases/download/v${VERSION}" + TMP="$(mktemp -d)" + trap 'rm -rf "$TMP"' EXIT + + echo "Downloading ${BASE_URL}/${ARCHIVE}" + curl -fsSL "${BASE_URL}/${ARCHIVE}" -o "${TMP}/${ARCHIVE}" + + echo "Downloading checksums.txt" + curl -fsSL "${BASE_URL}/checksums.txt" -o "${TMP}/checksums.txt" + + EXPECTED="$(awk -v f="${ARCHIVE}" '$2 == f {print $1}' "${TMP}/checksums.txt")" + if [[ -z "$EXPECTED" ]]; then + echo "No checksum entry for ${ARCHIVE} in checksums.txt" >&2 + exit 1 + fi + + if command -v sha256sum >/dev/null 2>&1; then + ACTUAL="$(sha256sum "${TMP}/${ARCHIVE}" | awk '{print $1}')" + elif command -v shasum >/dev/null 2>&1; then + ACTUAL="$(shasum -a 256 "${TMP}/${ARCHIVE}" | awk '{print $1}')" + else + echo "Neither sha256sum nor shasum available; cannot verify checksum" >&2 + exit 1 + fi + + if [[ "$EXPECTED" != "$ACTUAL" ]]; then + echo "Checksum mismatch for ${ARCHIVE}" >&2 + echo " expected: $EXPECTED" >&2 + echo " actual: $ACTUAL" >&2 + exit 1 + fi + + echo "Extracting ${ARCHIVE}" + if [[ "$EXT" == "zip" ]]; then + unzip -q "${TMP}/${ARCHIVE}" -d "$TMP" + else + tar -xzf "${TMP}/${ARCHIVE}" -C "$TMP" + fi + + mv "${TMP}/${BIN_NAME}" "${INSTALL_DIR}/${BIN_NAME}" + chmod +x "${INSTALL_DIR}/${BIN_NAME}" +fi + +echo "${INSTALL_DIR}" >> "$GITHUB_PATH" +"${INSTALL_DIR}/${BIN_NAME}" --version