265 lines
10 KiB
YAML
265 lines
10 KiB
YAML
name: 'Setup SSH Client'
|
|
description: 'Configure SSH client for running remote commands with custom shell wrapper'
|
|
author: 'Karolis Kundrotas'
|
|
branding:
|
|
icon: 'terminal'
|
|
color: 'blue'
|
|
|
|
inputs:
|
|
ssh-private-key:
|
|
description: 'SSH private key for authentication'
|
|
required: true
|
|
ssh-host:
|
|
description: 'SSH host to connect to'
|
|
required: true
|
|
ssh-user:
|
|
description: 'SSH username'
|
|
required: true
|
|
ssh-port:
|
|
description: 'SSH port'
|
|
required: false
|
|
default: '22'
|
|
ssh-known-hosts:
|
|
description: 'Known hosts content (optional, will use ssh-keyscan if not provided)'
|
|
required: false
|
|
default: ''
|
|
strict-host-key-checking:
|
|
description: 'Enable strict host key checking (yes/no/accept-new)'
|
|
required: false
|
|
default: 'accept-new'
|
|
use-shell-wrapper:
|
|
description: 'Create shell wrapper for remote execution (enables shell: ssh-remote)'
|
|
required: false
|
|
default: 'true'
|
|
remote-shell:
|
|
description: 'Shell to use on remote server (bash, sh, zsh, etc.)'
|
|
required: false
|
|
default: 'bash'
|
|
debug-shell-wrapper:
|
|
description: 'Enable debug logging in the SSH remote wrapper (prints script path and contents)'
|
|
required: false
|
|
default: 'false'
|
|
|
|
outputs:
|
|
ssh-config-path:
|
|
description: 'Path to the SSH config file'
|
|
value: ${{ steps.setup-ssh.outputs.ssh-config-path }}
|
|
ssh-key-path:
|
|
description: 'Path to the SSH private key'
|
|
value: ${{ steps.setup-ssh.outputs.ssh-key-path }}
|
|
shell-wrapper-path:
|
|
description: 'Path to the SSH remote shell wrapper script'
|
|
value: ${{ steps.setup-shell-wrapper.outputs.shell-wrapper-path }}
|
|
|
|
runs:
|
|
using: 'composite'
|
|
steps:
|
|
- name: Setup SSH
|
|
id: setup-ssh
|
|
shell: bash
|
|
run: |
|
|
# Create SSH directory
|
|
mkdir -p ~/.ssh
|
|
chmod 700 ~/.ssh
|
|
|
|
# Ensure ssh client is present. If not, try to install using a known
|
|
# package manager (apt, yum/dnf, apk, brew, pkg). If installation
|
|
# fails, stop early with helpful message.
|
|
if ! command -v ssh >/dev/null 2>&1; then
|
|
echo "SSH client 'ssh' not found. Attempting to install..."
|
|
|
|
if command -v apt-get >/dev/null 2>&1; then
|
|
echo "Using apt-get to install openssh-client"
|
|
sudo apt-get update -qq
|
|
DEBIAN_FRONTEND=noninteractive sudo apt-get install -y openssh-client
|
|
|
|
elif command -v yum >/dev/null 2>&1 || command -v dnf >/dev/null 2>&1; then
|
|
echo "Using yum/dnf to install openssh-clients"
|
|
sudo yum install -y openssh-clients || sudo dnf install -y openssh-clients
|
|
|
|
elif command -v apk >/dev/null 2>&1; then
|
|
echo "Using apk to install openssh-client"
|
|
sudo apk add --no-cache openssh-client
|
|
|
|
elif command -v brew >/dev/null 2>&1; then
|
|
echo "Using Homebrew to install openssh"
|
|
brew update >/dev/null || true
|
|
brew install openssh
|
|
|
|
elif command -v pkg >/dev/null 2>&1; then
|
|
echo "Using pkg to install openssh"
|
|
sudo pkg install -y openssh
|
|
|
|
else
|
|
echo "No known package manager found to install SSH client. Please install 'ssh' and rerun." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Verify the install worked
|
|
if ! command -v ssh >/dev/null 2>&1; then
|
|
echo "Failed to install SSH client automatically — please install 'ssh' and rerun." >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# Setup SSH private key
|
|
SSH_KEY_PATH="$HOME/.ssh/github_action_key"
|
|
echo "${{ inputs.ssh-private-key }}" > "$SSH_KEY_PATH"
|
|
chmod 600 "$SSH_KEY_PATH"
|
|
echo "ssh-key-path=$SSH_KEY_PATH" >> $GITHUB_OUTPUT
|
|
|
|
# Setup known hosts
|
|
if [ -n "${{ inputs.ssh-known-hosts }}" ]; then
|
|
echo "${{ inputs.ssh-known-hosts }}" > ~/.ssh/known_hosts
|
|
chmod 644 ~/.ssh/known_hosts
|
|
else
|
|
if [ "${{ inputs.strict-host-key-checking }}" = "yes" ]; then
|
|
echo "❌ strict-host-key-checking is 'yes' but no ssh-known-hosts provided. Refusing to auto-add host key via ssh-keyscan." >&2
|
|
echo "Provide the host's key (e.g. from a trusted source) via 'ssh-known-hosts' input." >&2
|
|
exit 1
|
|
fi
|
|
echo "Scanning host keys for ${{ inputs.ssh-host }}..."
|
|
ssh-keyscan -p ${{ inputs.ssh-port }} -H "${{ inputs.ssh-host }}" >> ~/.ssh/known_hosts 2>/dev/null || true
|
|
chmod 644 ~/.ssh/known_hosts
|
|
fi
|
|
|
|
# Create SSH config
|
|
SSH_CONFIG_PATH="$HOME/.ssh/config"
|
|
cat >> "$SSH_CONFIG_PATH" << EOF
|
|
Host github-action-host
|
|
HostName ${{ inputs.ssh-host }}
|
|
User ${{ inputs.ssh-user }}
|
|
Port ${{ inputs.ssh-port }}
|
|
IdentityFile $SSH_KEY_PATH
|
|
StrictHostKeyChecking ${{ inputs.strict-host-key-checking }}
|
|
UserKnownHostsFile ~/.ssh/known_hosts
|
|
EOF
|
|
chmod 600 "$SSH_CONFIG_PATH"
|
|
echo "ssh-config-path=$SSH_CONFIG_PATH" >> $GITHUB_OUTPUT
|
|
|
|
echo "✅ SSH client configured successfully"
|
|
echo "Connect using: ssh github-action-host"
|
|
|
|
- name: Test SSH Connection
|
|
shell: bash
|
|
run: |
|
|
echo "Testing SSH connection..."
|
|
if ssh -o ConnectTimeout=10 -o BatchMode=yes github-action-host "echo 'SSH connection successful'" 2>&1; then
|
|
echo "✅ SSH connection test passed"
|
|
else
|
|
echo "⚠️ SSH connection test failed - please verify your credentials and host"
|
|
exit 1
|
|
fi
|
|
|
|
- name: Create SSH Remote Shell Wrapper
|
|
id: setup-shell-wrapper
|
|
shell: bash
|
|
run: |
|
|
# Create a unique temporary directory for the wrapper. Do NOT place
|
|
# the wrapper in the `GITHUB_WORKSPACE` to avoid polluting repo files.
|
|
# Prefer runner-specific temp folders (RUNNER_TEMP), then /tmp.
|
|
TEMP_DIR="${RUNNER_TEMP:-/tmp}"
|
|
# Create temporary directory. mktemp implementation differs between
|
|
# Linux (GNU coreutils) and macOS (BSD). Use common-fallback patterns
|
|
# to be portable across runners.
|
|
if WRAPPER_DIR=$(mktemp -d "$TEMP_DIR/setup-ssh-client-XXXXXX" 2>/dev/null); then
|
|
:
|
|
elif WRAPPER_DIR=$(mktemp -d -p "$TEMP_DIR" setup-ssh-client-XXXXXX 2>/dev/null); then
|
|
:
|
|
else
|
|
WRAPPER_DIR="$TEMP_DIR/setup-ssh-client-$(date +%s)-$$"
|
|
mkdir -p "$WRAPPER_DIR"
|
|
fi
|
|
mkdir -p "$WRAPPER_DIR"
|
|
# Use command substitution $(...) — earlier use of ${...} is invalid and
|
|
# caused a "bad substitution" error in POSIX shells. `mktemp` accepts a
|
|
# template including a directory, but options differ between GNU and BSD
|
|
# implementations. Use template first, and fall back to -p for macOS.
|
|
if WRAPPER_PATH=$(mktemp "$WRAPPER_DIR/ssh-remote-shell-XXXXXX.sh" 2>/dev/null); then
|
|
: # created by GNU extension or compatible mktemp
|
|
elif WRAPPER_PATH=$(mktemp -p "$WRAPPER_DIR" ssh-remote-shell-XXXXXX.sh 2>/dev/null); then
|
|
: # fallback for BSD mktemp on macOS
|
|
else
|
|
# Last resort: generate a safe filename and create it
|
|
WRAPPER_PATH="$WRAPPER_DIR/ssh-remote-shell-$(date +%s)-$$.sh"
|
|
: > "$WRAPPER_PATH"
|
|
fi
|
|
|
|
cat << 'WRAPPER_EOF' > "$WRAPPER_PATH"
|
|
#!/bin/bash
|
|
set -euo pipefail
|
|
|
|
# Runner normally passes a temp script path as the first argument.
|
|
# If that isn't present, allow script to be piped to stdin.
|
|
TMPDIR="${TMPDIR:-/tmp}"
|
|
SCRIPT_FILE=""
|
|
if [ -n "${1-}" ]; then
|
|
SCRIPT_FILE="$1"
|
|
elif ! [ -t 0 ]; then
|
|
# Write stdin to a temp file
|
|
TMP_SCRIPT=$(mktemp "$TMPDIR/ssh-remote-stdin-XXXXXX.sh" 2>/dev/null || mktemp -t ssh-remote-stdin-XXXXXX)
|
|
cat - > "$TMP_SCRIPT"
|
|
chmod +x "$TMP_SCRIPT"
|
|
SCRIPT_FILE="$TMP_SCRIPT"
|
|
else
|
|
echo "Error: No script file provided" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Check if script file exists
|
|
if [ ! -f "$SCRIPT_FILE" ]; then
|
|
echo "Error: Script file '$SCRIPT_FILE' not found" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Execute script on remote server
|
|
# Use BatchMode to prevent interactive prompts
|
|
# ConnectTimeout to fail fast if connection issues
|
|
echo "Executing script on remote server '${{ inputs.ssh-host }}' via SSH..."
|
|
# Print some debug information if requested. See 'debug-shell-wrapper' input.
|
|
if [ "${SSH_REMOTE_DEBUG-}" = "true" ]; then
|
|
echo "[ssh-remote wrapper] Script file: $SCRIPT_FILE"
|
|
echo "[ssh-remote wrapper] Script contents:" >&2
|
|
sed -n '1,200p' "$SCRIPT_FILE"
|
|
echo "[ssh-remote wrapper] ---------- end script ----------"
|
|
fi
|
|
ssh -o BatchMode=yes \
|
|
-o ConnectTimeout=10 \
|
|
github-action-host \
|
|
"${{ inputs.remote-shell }} -s" < "$SCRIPT_FILE"
|
|
SSH_EXIT_CODE=$?
|
|
if [ "${SSH_REMOTE_DEBUG-}" = "true" ]; then
|
|
echo "[ssh-remote wrapper] SSH exit code: $SSH_EXIT_CODE"
|
|
fi
|
|
WRAPPER_EOF
|
|
|
|
chmod +x "$WRAPPER_PATH"
|
|
echo "✅ SSH remote shell wrapper created at $WRAPPER_PATH"
|
|
# We also create symlink without the .sh extension to provide a
|
|
# short executable name inside the wrapper dir (e.g. $WRAPPER_DIR/ssh-remote)
|
|
ln -s "$WRAPPER_PATH" "$WRAPPER_DIR/ssh-remote"
|
|
if [ "${{ inputs.debug-shell-wrapper }}" = "true" ]; then
|
|
# Persist debug flag to the job environment; the wrapper can pick
|
|
# this up later when the job runs. This allows debugging without
|
|
# changing the wrapper script at runtime.
|
|
echo "SSH_REMOTE_DEBUG=true" >> $GITHUB_ENV
|
|
echo "⚠️ SSH remote shell wrapper debug is enabled (SSH_REMOTE_DEBUG=true)"
|
|
fi
|
|
echo "shell-wrapper-dir=$WRAPPER_DIR" >> $GITHUB_OUTPUT
|
|
echo "shell-wrapper-path=$WRAPPER_PATH" >> $GITHUB_OUTPUT
|
|
|
|
|
|
- name: Register Custom Shell
|
|
if: inputs.use-shell-wrapper == 'true'
|
|
shell: bash
|
|
run: |
|
|
SHELL_WRAPPER_DIR="${{ steps.setup-shell-wrapper.outputs.shell-wrapper-dir }}"
|
|
# Register custom shell for this job
|
|
echo "Adding custom shell 'ssh-remote' to PATH"
|
|
echo "PATH=$SHELL_WRAPPER_DIR:$PATH" >> $GITHUB_ENV
|
|
echo "✅ Custom shell 'ssh-remote' registered for this job"
|
|
echo ""
|
|
echo "To use the remote shell in subsequent steps, add:"
|
|
echo " shell: ssh-remote {0}"
|
|
echo ""
|
|
echo "The 'ssh-remote' shell is now available for use."
|