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."