diff --git a/.github/actions/arcane-deploy/README.md b/.github/actions/arcane-deploy/README.md index 1659f13..7d2fabb 100644 --- a/.github/actions/arcane-deploy/README.md +++ b/.github/actions/arcane-deploy/README.md @@ -42,6 +42,22 @@ Deploy Docker Compose stacks to [Arcane](https://github.com/getarcaneapp/arcane) git-token: ${{ secrets.REPO_TOKEN }} ``` +**SSH deploy key** — use a GitHub deploy key instead of a PAT: + +```yaml +- name: Deploy stacks to Arcane + uses: nsheaps/github-actions/.github/actions/arcane-deploy@main + with: + arcane-url: ${{ secrets.ARCANE_URL }} + arcane-api-key: ${{ secrets.ARCANE_API_KEY }} + environment-id: '1' + compose-dir: stacks + auth-type: ssh + ssh-private-key: ${{ secrets.DEPLOY_KEY }} +``` + +> **Note:** When `auth-type: ssh`, the `repository-url` defaults to the SSH format (`git@github.com:owner/repo.git`) automatically. You can override it with `repository-url` if needed. + **With workflow environment variables** (available to subsequent steps, not inside containers): ```yaml @@ -61,23 +77,25 @@ Deploy Docker Compose stacks to [Arcane](https://github.com/getarcaneapp/arcane) ## Inputs -| Input | Required | Default | Description | -| ------------------ | -------- | --------------------- | ------------------------------------------------------------------------------- | -| `arcane-url` | Yes | | Base URL of the Arcane instance (must use HTTPS) | -| `arcane-api-key` | Yes | | API key (from Arcane Settings > API Keys) | -| `environment-id` | Yes | | Arcane environment ID | -| `compose-dir` | No | | Directory to scan for compose files (up to 2 levels deep) | -| `compose-files` | No | | Newline-separated list of compose file paths | -| `repository-url` | No | GitHub repo HTTPS URL | Git URL for Arcane to clone | -| `repository-name` | No | GitHub repo name | Display name in Arcane | -| `branch` | No | Triggering branch | Branch to sync from | -| `auth-type` | No | `http` | Git auth type: `none` or `http` | -| `git-token` | No | | Token for HTTP git auth. Required when auth-type=http. | -| `auto-sync` | No | `true` | Enable Arcane auto-sync polling | -| `sync-interval` | No | `5` | Minutes between auto-sync polls | -| `trigger-sync` | No | `true` | Trigger immediate sync after create/update | -| `sync-name-prefix` | No | GitHub repo name | Prefix for sync names in Arcane | -| `env-vars` | No | | Runner env vars (`KEY=VALUE` per line) for subsequent steps. Values are masked. | +| Input | Required | Default | Description | +| --------------------------- | -------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `arcane-url` | Yes | | Base URL of the Arcane instance (must use HTTPS) | +| `arcane-api-key` | Yes | | API key (from Arcane Settings > API Keys) | +| `environment-id` | Yes | | Arcane environment ID | +| `compose-dir` | No | | Directory to scan for compose files (up to 2 levels deep) | +| `compose-files` | No | | Newline-separated list of compose file paths | +| `repository-url` | No | GitHub repo HTTPS URL | Git URL for Arcane to clone. Defaults to SSH format when auth-type=ssh. | +| `repository-name` | No | GitHub repo name | Display name in Arcane | +| `branch` | No | Triggering branch | Branch to sync from | +| `auth-type` | No | `http` | Git auth type: `none`, `http`, or `ssh` | +| `git-token` | No | | Token for HTTP git auth. Required when auth-type=http. | +| `ssh-private-key` | No | | SSH private key for git auth (e.g. deploy key). Required when auth-type=ssh. | +| `ssh-host-key-verification` | No | `accept_new` | SSH host key verification mode: `accept_new`, `accept_all`, or `reject`. **Warning:** `accept_all` disables host key checking and should only be used for testing. | +| `auto-sync` | No | `true` | Enable Arcane auto-sync polling | +| `sync-interval` | No | `5` | Minutes between auto-sync polls | +| `trigger-sync` | No | `true` | Trigger immediate sync after create/update | +| `sync-name-prefix` | No | GitHub repo name | Prefix for sync names in Arcane | +| `env-vars` | No | | Runner env vars (`KEY=VALUE` per line) for subsequent steps. Values are masked. | ## Outputs diff --git a/.github/actions/arcane-deploy/action.sh b/.github/actions/arcane-deploy/action.sh index 82d16b4..1a4a99e 100755 --- a/.github/actions/arcane-deploy/action.sh +++ b/.github/actions/arcane-deploy/action.sh @@ -7,11 +7,18 @@ API_KEY="${INPUT_ARCANE_API_KEY}" ENV_ID="${INPUT_ENVIRONMENT_ID}" COMPOSE_DIR="${INPUT_COMPOSE_DIR:-}" COMPOSE_FILES_INPUT="${INPUT_COMPOSE_FILES:-}" -REPO_URL="${INPUT_REPOSITORY_URL:-https://github.com/${GITHUB_REPOSITORY}.git}" +# Default to SSH URL format when auth-type is ssh, HTTPS otherwise +if [[ -z "${INPUT_REPOSITORY_URL:-}" && "${INPUT_AUTH_TYPE:-http}" == "ssh" ]]; then + REPO_URL="git@github.com:${GITHUB_REPOSITORY}.git" +else + REPO_URL="${INPUT_REPOSITORY_URL:-https://github.com/${GITHUB_REPOSITORY}.git}" +fi REPO_NAME="${INPUT_REPOSITORY_NAME:-${GITHUB_REPOSITORY##*/}}" BRANCH="${INPUT_BRANCH:-${GITHUB_REF_NAME:-main}}" AUTH_TYPE="${INPUT_AUTH_TYPE:-http}" GIT_TOKEN="${INPUT_GIT_TOKEN:-}" +SSH_PRIVATE_KEY="${INPUT_SSH_PRIVATE_KEY:-}" +SSH_HOST_KEY_VERIFICATION="${INPUT_SSH_HOST_KEY_VERIFICATION:-accept_new}" AUTO_SYNC="${INPUT_AUTO_SYNC:-true}" SYNC_INTERVAL="${INPUT_SYNC_INTERVAL:-5}" TRIGGER_SYNC="${INPUT_TRIGGER_SYNC:-true}" @@ -20,6 +27,14 @@ ENV_VARS="${INPUT_ENV_VARS:-}" # [C1/C2] Mask secrets immediately, before any logging or API calls [[ -n "${API_KEY}" ]] && echo "::add-mask::${API_KEY}" [[ -n "${GIT_TOKEN}" ]] && echo "::add-mask::${GIT_TOKEN}" +# SSH private keys are multiline; ::add-mask:: works per-line, so mask each line +# to prevent any part of the key from appearing in logs. +if [[ -n "${SSH_PRIVATE_KEY}" ]]; then + echo "::add-mask::${SSH_PRIVATE_KEY}" + while IFS= read -r _line; do + [[ -n "${_line}" ]] && echo "::add-mask::${_line}" + done <<< "${SSH_PRIVATE_KEY}" +fi SYNCS_CREATED=0 SYNCS_UPDATED=0 @@ -196,7 +211,7 @@ ensure_repository() { if [[ -n "${REPOSITORY_ID}" && "${REPOSITORY_ID}" != "null" ]]; then log_info "Found existing repository: ${REPOSITORY_ID}" - # Update credentials so the token stays current + # Update credentials so they stay current if [[ "${AUTH_TYPE}" == "http" && -n "${GIT_TOKEN}" ]]; then local update_payload update_payload=$(jq -n \ @@ -206,6 +221,19 @@ ensure_repository() { arcane_api PUT "/customize/git-repositories/${REPOSITORY_ID}" \ -d "${update_payload}" > /dev/null log_info "Updated repository credentials" + elif [[ "${AUTH_TYPE}" == "ssh" && -n "${SSH_PRIVATE_KEY}" ]]; then + # Arcane API field names use camelCase (matching existing authType/token convention). + # Backend model: ssh_key -> sshKey, ssh_host_key_verification -> sshHostKeyVerification + local update_payload + update_payload=$(jq -n \ + --arg sshKey "${SSH_PRIVATE_KEY}" \ + --arg username "git" \ + --arg sshHostKeyVerification "${SSH_HOST_KEY_VERIFICATION}" \ + '{sshKey: $sshKey, username: $username, sshHostKeyVerification: $sshHostKeyVerification}') + + arcane_api PUT "/customize/git-repositories/${REPOSITORY_ID}" \ + -d "${update_payload}" > /dev/null + log_info "Updated repository SSH credentials" fi else log_info "Creating new repository: ${REPO_NAME}" @@ -218,6 +246,15 @@ ensure_repository() { --arg authType "${AUTH_TYPE}" \ --arg token "${GIT_TOKEN}" \ '{name: $name, url: $url, authType: $authType, token: $token}') + elif [[ "${AUTH_TYPE}" == "ssh" ]]; then + create_payload=$(jq -n \ + --arg name "${REPO_NAME}" \ + --arg url "${REPO_URL}" \ + --arg authType "${AUTH_TYPE}" \ + --arg sshKey "${SSH_PRIVATE_KEY}" \ + --arg username "git" \ + --arg sshHostKeyVerification "${SSH_HOST_KEY_VERIFICATION}" \ + '{name: $name, url: $url, authType: $authType, sshKey: $sshKey, username: $username, sshHostKeyVerification: $sshHostKeyVerification}') else create_payload=$(jq -n \ --arg name "${REPO_NAME}" \ @@ -379,9 +416,15 @@ if [[ "${AUTH_TYPE}" == "http" && -z "${GIT_TOKEN}" ]]; then exit 1 fi +# Validate ssh-private-key is set when auth-type is ssh +if [[ "${AUTH_TYPE}" == "ssh" && -z "${SSH_PRIVATE_KEY}" ]]; then + log_error "ssh-private-key is required when auth-type is ssh." + exit 1 +fi + # [H4] Reject unsupported auth-type values -if [[ "${AUTH_TYPE}" != "none" && "${AUTH_TYPE}" != "http" ]]; then - log_error "auth-type '${AUTH_TYPE}' is not supported. Valid values: none, http." +if [[ "${AUTH_TYPE}" != "none" && "${AUTH_TYPE}" != "http" && "${AUTH_TYPE}" != "ssh" ]]; then + log_error "auth-type '${AUTH_TYPE}' is not supported. Valid values: none, http, ssh." exit 1 fi diff --git a/.github/actions/arcane-deploy/action.yml b/.github/actions/arcane-deploy/action.yml index 9f28358..269a3b8 100644 --- a/.github/actions/arcane-deploy/action.yml +++ b/.github/actions/arcane-deploy/action.yml @@ -47,7 +47,7 @@ inputs: default: '' auth-type: - description: 'Git authentication type: none or http' + description: 'Git authentication type: none, http, or ssh' required: false default: 'http' @@ -56,6 +56,16 @@ inputs: required: false default: '' + ssh-private-key: + description: 'SSH private key for git authentication (e.g. a GitHub deploy key). Required when auth-type is ssh.' + required: false + default: '' + + ssh-host-key-verification: + description: 'SSH host key verification mode for Arcane. Valid values: accept_new, accept_all, reject.' + required: false + default: 'accept_new' + # Sync behavior auto-sync: description: 'Enable auto-sync polling in Arcane so it periodically pulls changes' @@ -109,6 +119,8 @@ runs: INPUT_BRANCH: ${{ inputs.branch }} INPUT_AUTH_TYPE: ${{ inputs.auth-type }} INPUT_GIT_TOKEN: ${{ inputs.git-token }} + INPUT_SSH_PRIVATE_KEY: ${{ inputs.ssh-private-key }} + INPUT_SSH_HOST_KEY_VERIFICATION: ${{ inputs.ssh-host-key-verification }} INPUT_AUTO_SYNC: ${{ inputs.auto-sync }} INPUT_SYNC_INTERVAL: ${{ inputs.sync-interval }} INPUT_TRIGGER_SYNC: ${{ inputs.trigger-sync }}