From 2d855cd680fc10289f39b39dfeafbb9c33d93b61 Mon Sep 17 00:00:00 2001 From: "trop[bot]" <37223003+trop[bot]@users.noreply.github.com> Date: Wed, 30 Jul 2025 11:43:51 -0700 Subject: [PATCH] ci: add ability to debug SSH sessions in CI (#47875) Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com> Co-authored-by: Shelley Vohr --- .github/actions/ssh-debug/action.yml | 20 +++ .github/actions/ssh-debug/bashrc | 4 + .github/actions/ssh-debug/setup-ssh.sh | 140 ++++++++++++++++++ .github/actions/ssh-debug/ssh-session.sh | 21 +++ .../actions/ssh-debug/sshd_config.template | 9 ++ .../pipeline-segment-electron-build.yml | 10 ++ .../pipeline-segment-electron-test.yml | 10 ++ 7 files changed, 214 insertions(+) create mode 100644 .github/actions/ssh-debug/action.yml create mode 100644 .github/actions/ssh-debug/bashrc create mode 100755 .github/actions/ssh-debug/setup-ssh.sh create mode 100755 .github/actions/ssh-debug/ssh-session.sh create mode 100644 .github/actions/ssh-debug/sshd_config.template diff --git a/.github/actions/ssh-debug/action.yml b/.github/actions/ssh-debug/action.yml new file mode 100644 index 0000000000..96b99671fd --- /dev/null +++ b/.github/actions/ssh-debug/action.yml @@ -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 }} diff --git a/.github/actions/ssh-debug/bashrc b/.github/actions/ssh-debug/bashrc new file mode 100644 index 0000000000..52ecb95920 --- /dev/null +++ b/.github/actions/ssh-debug/bashrc @@ -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 diff --git a/.github/actions/ssh-debug/setup-ssh.sh b/.github/actions/ssh-debug/setup-ssh.sh new file mode 100755 index 0000000000..7407dc2095 --- /dev/null +++ b/.github/actions/ssh-debug/setup-ssh.sh @@ -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.' diff --git a/.github/actions/ssh-debug/ssh-session.sh b/.github/actions/ssh-debug/ssh-session.sh new file mode 100755 index 0000000000..875acf1c66 --- /dev/null +++ b/.github/actions/ssh-debug/ssh-session.sh @@ -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" diff --git a/.github/actions/ssh-debug/sshd_config.template b/.github/actions/ssh-debug/sshd_config.template new file mode 100644 index 0000000000..9c7949d8b8 --- /dev/null +++ b/.github/actions/ssh-debug/sshd_config.template @@ -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 diff --git a/.github/workflows/pipeline-segment-electron-build.yml b/.github/workflows/pipeline-segment-electron-build.yml index ac7d2e78c9..96768f2092 100644 --- a/.github/workflows/pipeline-segment-electron-build.yml +++ b/.github/workflows/pipeline-segment-electron-build.yml @@ -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: @@ -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 diff --git a/.github/workflows/pipeline-segment-electron-test.yml b/.github/workflows/pipeline-segment-electron-test.yml index 108c971e72..d9adbb0c7e 100644 --- a/.github/workflows/pipeline-segment-electron-test.yml +++ b/.github/workflows/pipeline-segment-electron-test.yml @@ -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: @@ -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