Skip to content

ci: add ability to debug SSH sessions in CI #47876

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
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
20 changes: 20 additions & 0 deletions .github/actions/ssh-debug/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Debug via SSH
description: Setup a SSH server with a tunnel to access it to debug via SSH.
inputs:
tunnel:
description: 'Enable SSH tunneling via cloudflared'
required: true
default: 'false'
timeout:
description: 'SSH session timeout in minutes'
required: false
type: number
default: 60
runs:
using: composite
steps:
- run: $GITHUB_ACTION_PATH/setup-ssh.sh
shell: bash
env:
TUNNEL: ${{ inputs.tunnel }}
TIMEOUT: ${{ inputs.timeout }}
4 changes: 4 additions & 0 deletions .github/actions/ssh-debug/bashrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# If we're in an interactive SSH session and we're not already in tmux and there's no explicit SSH command, auto attach tmux
if [ -n "$SSH_TTY" ] && [ -z "$TMUX" ] && [ -z "$SSH_ORIGINAL_COMMAND" ]; then
exec tmux attach || exec tmux
fi
140 changes: 140 additions & 0 deletions .github/actions/ssh-debug/setup-ssh.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
#!/bin/bash -e

get_authorized_keys() {
if [ -z "$AUTHORIZED_USERS" ] || ! echo "$AUTHORIZED_USERS" | grep -q "\b$GITHUB_ACTOR\b"; then
return 1
fi

api_response=$(curl -s "https://api.github.com/users/$GITHUB_ACTOR/keys")

if echo "$api_response" | jq -e 'type == "object" and has("message")' >/dev/null; then
error_msg=$(echo "$api_response" | jq -r '.message')
echo "Error: $error_msg"
return 1
else
echo "$api_response" | jq -r '.[].key'
fi
}

authorized_keys=$(get_authorized_keys "$GITHUB_ACTOR")

if [ -n "$authorized_keys" ]; then
echo "Configured SSH key(s) for user: $GITHUB_ACTOR"
else
echo "Error: User '$GITHUB_ACTOR' is not authorized to access this debug session."
echo "Authorized users: $AUTHORIZED_USERS"
exit 1
fi

if [ "$TUNNEL" != "true" ]; then
echo "SSH tunneling is disabled. Set enable-tunnel: true to enable remote access."
echo "Local SSH server would be available on localhost:2222 if this were a local environment."
exit 0
fi

echo "SSH tunneling enabled. Setting up remote access..."

EXTERNAL_DEPS="curl jq ssh-keygen"

for dep in $EXTERNAL_DEPS; do
if ! command -v "$dep" > /dev/null 2>&1; then
echo "Command $dep not installed on the system!" >&2
exit 1
fi
done

cd "$GITHUB_ACTION_PATH"

bashrc_path=$(pwd)/bashrc

# Source `bashrc` to auto start tmux on SSH login.
if ! grep -q "$bashrc_path" ~/.bash_profile; then
echo >> ~/.bash_profile # On macOS runner there's no newline at the end of the file
echo "source \"$bashrc_path\"" >> ~/.bash_profile
fi

OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)

if [ "$ARCH" = "x86_64" ]; then
ARCH="amd64"
elif [ "$ARCH" = "aarch64" ]; then
ARCH="arm64"
fi

# Install tmux on macOS runners if not present.
if [ "$OS" = "darwin" ] && ! command -v tmux > /dev/null 2>&1; then
echo "Installing tmux..."
brew install tmux
fi

if [ "$OS" = "darwin" ]; then
cloudflared_url="https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-${OS}-${ARCH}.tgz"
echo "Downloading \`cloudflared\` from <$cloudflared_url>..."
curl --location --silent --output cloudflared.tgz "$cloudflared_url"
tar xf cloudflared.tgz
rm cloudflared.tgz
else
cloudflared_url="https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-${OS}-${ARCH}"
echo "Downloading \`cloudflared\` from <$cloudflared_url>..."
curl --location --silent --output cloudflared "$cloudflared_url"
fi

chmod +x cloudflared

echo "Setting up SSH key for authorized user: $GITHUB_ACTOR"
echo "$authorized_keys" > authorized_keys

echo 'Creating SSH server key...'
ssh-keygen -q -f ssh_host_rsa_key -N ''

echo 'Creating SSH server config...'
sed "s,\$PWD,$PWD,;s,\$USER,$USER," sshd_config.template > sshd_config

echo 'Starting SSH server...'
/usr/sbin/sshd -f sshd_config -D &
sshd_pid=$!

echo 'Starting tmux session...'
(cd "$GITHUB_WORKSPACE" && tmux new-session -d -s debug)

#if no cloudflare tunnel token is provided, exit
if [ -z "$CLOUDFLARE_TUNNEL_TOKEN" ]; then
echo "Error: required CLOUDFLARE_TUNNEL_TOKEN not found"
exit 1
fi

echo 'Starting Cloudflare tunnel...'

./cloudflared tunnel --no-autoupdate run --token "$CLOUDFLARE_TUNNEL_TOKEN" 2>&1 | tee cloudflared.log | sed -u 's/^/cloudflared: /' &
cloudflared_pid=$!

url="$TUNNEL_HOSTNAME"

public_key=$(cut -d' ' -f1,2 < ssh_host_rsa_key.pub)

(
echo ' '
echo ' '
echo '🔗 SSH Debug Session Ready!'
echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'
echo ' '
echo '📋 Copy and run this command to connect:'
echo ' '
if [ -n "$TUNNEL_HOSTNAME" ]; then
echo "ssh-keygen -R action-ssh-debug && echo 'action-ssh-debug $public_key' >> ~/.ssh/known_hosts && ssh -o ProxyCommand='cloudflared access tcp --hostname $url' runner@action-ssh-debug"
else
echo "ssh-keygen -R action-ssh-debug && echo 'action-ssh-debug $public_key' >> ~/.ssh/known_hosts && ssh -o ProxyCommand='cloudflared access tcp --hostname $url' runner@action-ssh-debug"
fi
echo ' '
echo "⏰ Session expires automatically in $TIMEOUT minutes"
echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'
echo ' '
echo ' '
) | cat

echo 'Starting SSH session in background...'
./ssh-session.sh "$sshd_pid" "$cloudflared_pid" $TIMEOUT &

echo 'SSH session is running in background. GitHub Action will continue.'
echo 'Session will auto-cleanup after timeout or when processes end.'
21 changes: 21 additions & 0 deletions .github/actions/ssh-debug/ssh-session.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/bin/bash

SSHD_PID=$1
CLOUDFLARED_PID=$2
SESSION_TIMEOUT=${3:-3600}

# Wait for timeout or until processes die.
sleep "$SESSION_TIMEOUT" &
SLEEP_PID=$!

# Monitor if SSH or cloudflared dies early.
while kill -0 "$SSHD_PID" 2>/dev/null && kill -0 "$CLOUDFLARED_PID" 2>/dev/null && kill -0 "$SLEEP_PID" 2>/dev/null; do
sleep 10
done

# Cleanup.
kill "$SLEEP_PID" 2>/dev/null || true
kill "$SSHD_PID" 2>/dev/null || true
kill "$CLOUDFLARED_PID" 2>/dev/null || true

echo "SSH session ended"
9 changes: 9 additions & 0 deletions .github/actions/ssh-debug/sshd_config.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Port 2222
HostKey $PWD/ssh_host_rsa_key
PidFile $PWD/sshd.pid

# Only allow single user
AllowUsers $USER

# Only allow those keys
AuthorizedKeysFile $PWD/authorized_keys
10 changes: 10 additions & 0 deletions .github/workflows/pipeline-segment-electron-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ env:
SUDOWOODO_EXCHANGE_TOKEN: ${{ secrets.SUDOWOODO_EXCHANGE_TOKEN }}
GCLIENT_EXTRA_ARGS: ${{ inputs.target-platform == 'macos' && '--custom-var=checkout_mac=True --custom-var=host_os=mac' || inputs.target-platform == 'win' && '--custom-var=checkout_win=True' || '--custom-var=checkout_arm=True --custom-var=checkout_arm64=True' }}
ELECTRON_OUT_DIR: Default
ACTIONS_STEP_DEBUG: ${{ secrets.ACTIONS_STEP_DEBUG }}

jobs:
build:
Expand All @@ -94,6 +95,15 @@ jobs:
path: src/electron
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: Setup SSH Debugging
if: ${{ inputs.target-platform == 'macos' && env.ACTIONS_STEP_DEBUG == 'true' }}
uses: ./src/electron/.github/actions/ssh-debug
with:
tunnel: 'true'
env:
CLOUDFLARE_TUNNEL_TOKEN: ${{ secrets.CLOUDFLARE_TUNNEL_TOKEN }}
TUNNEL_HOSTNAME: ${{ secrets.CLOUDFLARED_SSH_HOSTNAME }}
AUTHORIZED_USERS: ${{ secrets.SSH_DEBUG_AUTHORIZED_USERS }}
- name: Free up space (macOS)
if: ${{ inputs.target-platform == 'macos' }}
uses: ./src/electron/.github/actions/free-space-macos
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/pipeline-segment-electron-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ env:
CHROMIUM_GIT_COOKIE_WINDOWS_STRING: ${{ secrets.CHROMIUM_GIT_COOKIE_WINDOWS_STRING }}
ELECTRON_OUT_DIR: Default
ELECTRON_RBE_JWT: ${{ secrets.ELECTRON_RBE_JWT }}
ACTIONS_STEP_DEBUG: ${{ secrets.ACTIONS_STEP_DEBUG }}

jobs:
test:
Expand Down Expand Up @@ -124,6 +125,15 @@ jobs:
path: src/electron
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: Setup SSH Debugging
if: ${{ inputs.target-platform == 'macos' && env.ACTIONS_STEP_DEBUG == 'true' }}
uses: ./src/electron/.github/actions/ssh-debug
with:
tunnel: 'true'
env:
CLOUDFLARE_TUNNEL_TOKEN: ${{ secrets.CLOUDFLARE_TUNNEL_TOKEN }}
TUNNEL_HOSTNAME: ${{ secrets.CLOUDFLARED_SSH_HOSTNAME }}
AUTHORIZED_USERS: ${{ secrets.SSH_DEBUG_AUTHORIZED_USERS }}
- name: Install Dependencies
uses: ./src/electron/.github/actions/install-dependencies
- name: Set Chromium Git Cookie
Expand Down
Loading