📚 Documentation Index - Complete guide to all documentation
Secure, bidirectional file transfer via SSH with automatic backups, compression, and comprehensive security validations.
Key capabilities:
- ✅ Upload with tar.gz compression, download with rsync
- ✅ Auto-backup before upload (keeps last 10)
- ✅ 2GB upload / 10GB download limits, disk space validation
- ✅ Dangerous command blocking in scripts
- ✅ Resource limits and timeout protection
- ✅ Six-phase modular workflow
| Name | Description | Required | Default |
|---|---|---|---|
host |
SSH host (source for download, destination for upload) | Yes | - |
port |
SSH port | No | 22 |
username |
SSH username | Yes | - |
key |
SSH private key | Yes | - |
passphrase |
Passphrase for SSH private key | No | - |
source |
Source file/directory to transfer | Yes | - |
destination |
Destination path | Yes | - |
direction |
Transfer direction (upload or download) |
No | upload |
destination_host |
Destination host for remote-to-remote downloads | No | - |
destination_port |
Destination SSH port | No | 22 |
destination_username |
Destination SSH username | No | - |
destination_key |
Destination SSH private key | No | - |
destination_passphrase |
Destination key passphrase | No | - |
recursive |
Transfer files recursively | No | true |
strict_host_key_checking |
Enable strict host key checking | No | true |
post_script |
Inline script to run on remote after transfer | No | - |
post_script_path |
Path to script on remote to run after transfer | No | - |
backup_before_transfer |
Create backup before upload | No | true |
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Transfer files to server
uses: kellydc/sshft@v1
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: "dist/"
destination: "/var/www/html/"
# backup_before_transfer defaults to true
- name: Transfer single file
uses: kellydc/sshft@v1
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: "config.json"
destination: "/etc/myapp/config.json"The action automatically creates a backup of the destination before uploading files. Backups are stored in ~/backups on the remote server with timestamped filenames and compressed using tar.gz.
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy with automatic backup
id: deploy
uses: kellydc/sshft@v1
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: "dist/"
destination: "/var/www/html/"
# backup_before_transfer: true (default)
- name: Display backup information
if: steps.deploy.outputs.backup_created == 'true'
run: |
echo "✓ Backup created successfully"
echo "Location: ${{ steps.deploy.outputs.backup_path }}"
echo "Size: ${{ steps.deploy.outputs.backup_size }}"
- name: Deploy without backup
uses: kellydc/sshft@v1
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: "temp-files/"
destination: "/tmp/uploads/"
backup_before_transfer: falseBackup Features:
- ✅ Automatic backup creation before file transfer (upload only)
- ✅ Backups stored in
~/backups/on remote server (user's home directory) - ✅ Descriptive filenames using full destination path:
var_www_mysite_{YYYYMMDD_HHMMSS}_{random_id}.tar.gz - ✅ Efficient tar.gz compression
- ✅ Automatic retention policy (keeps last 10 backups per destination)
- ✅ Comprehensive error handling
- ✅ Skips gracefully if destination doesn't exist yet (first-time deployment)
- ✅ Informative output with backup location and size
Two download modes available (using rsync for efficient transfer):
Files download to GitHub runner using rsync (ephemeral - use actions/upload-artifact to persist).
jobs:
backup:
runs-on: ubuntu-latest
steps:
- name: Download logs to runner
uses: kellydc/sshft@v1
with:
host: ${{ secrets.SOURCE_HOST }}
username: ${{ secrets.SOURCE_USERNAME }}
key: ${{ secrets.SOURCE_KEY }}
source: "/var/log/app.log"
destination: "./logs/"
direction: "download"
# REQUIRED: Persist downloaded files
- name: Save as artifact
uses: actions/upload-artifact@v4
with:
name: server-logs
path: ./logs/Transfer files directly between servers using rsync (persistent storage, preserves permissions and timestamps).
jobs:
server-migration:
runs-on: ubuntu-latest
steps:
- name: Transfer between servers
uses: kellydc/sshft@v1
with:
# Source server
host: ${{ secrets.SOURCE_HOST }}
username: ${{ secrets.SOURCE_USERNAME }}
key: ${{ secrets.SOURCE_KEY }}
source: "/var/www/html/"
# Destination server
destination_host: ${{ secrets.DEST_HOST }}
destination_username: ${{ secrets.DEST_USERNAME }}
destination_key: ${{ secrets.DEST_KEY }}
destination: "/backup/www/"
direction: "download"| Feature | Upload | Download (Remote→Runner) | Download (Remote→Remote) |
|---|---|---|---|
| Direction | Runner → Remote | Remote → Runner | Remote → Remote |
| Transfer Method | tar.gz + scp | rsync | rsync (direct) |
| Backup Created | ✅ Yes (if enabled) | ❌ No | ❌ No |
| Persistence | ✅ Persistent | ✅ Persistent | |
| Size Limit | 2GB | 10GB | 10GB |
| Preserves Attributes | ✅ Yes | ✅ Yes (permissions, timestamps) | ✅ Yes (permissions, timestamps) |
| Use Case | Deploy code | Retrieve logs/data | Server migration/backup |
| Requires destination_host | ❌ No | ❌ No | ✅ Yes |
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Transfer with custom port
uses: kellydc/sshft@v1
with:
host: ${{ secrets.SSH_HOST }}
port: 2022
username: ${{ secrets.SSH_USERNAME }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: "dist/"
destination: "/var/www/html/"
- name: Transfer to new server without key verification
uses: kellydc/sshft@v1
with:
host: ${{ secrets.NEW_SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: "config/"
destination: "/etc/myapp/"
strict_host_key_checking: false # Disable for first connection
- name: Transfer with SSH key passphrase
uses: kellydc/sshft@v1
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
passphrase: ${{ secrets.SSH_PASSPHRASE }}
source: "sensitive_data/"
destination: "/secure/location/"Execute scripts on the remote server after successful file transfer. Scripts can be provided inline or reference an existing script on the remote server. Script execution is optional and includes robust error handling.
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy and restart service
uses: kellydc/sshft@v1
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: "dist/"
destination: "/var/www/html/"
post_script: |
echo "Deployment completed at $(date)"
sudo systemctl restart nginx
echo "Service restarted successfully"
- name: Deploy with permissions fix
uses: kellydc/sshft@v1
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: "app/"
destination: "/opt/myapp/"
post_script: |
cd /opt/myapp
chmod +x *.sh
chown -R www-data:www-data /opt/myappjobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy and run deployment script
uses: kellydc/sshft@v1
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: "dist/"
destination: "/var/www/html/"
post_script_path: "/opt/scripts/post-deploy.sh"
- name: Deploy with custom maintenance script
uses: kellydc/sshft@v1
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: "updates/"
destination: "/var/app/"
post_script_path: "~/scripts/maintenance.sh"jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy with post-script
id: deploy
uses: kellydc/sshft@v1
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: "dist/"
destination: "/var/www/html/"
post_script: |
echo "Server: $(hostname)"
echo "Disk usage: $(df -h / | tail -n1 | awk '{print $5}')"
- name: Display script output
if: steps.deploy.outputs.script_executed == 'true'
run: |
echo "Script was executed successfully!"
echo "Output: ${{ steps.deploy.outputs.script_output }}"
- name: Handle script errors
if: steps.deploy.outputs.script_error != ''
run: |
echo "Script error: ${{ steps.deploy.outputs.script_error }}"- Three Transfer Modes: Runner→Remote upload, Remote→Runner download, Remote→Remote download
- Automatic Compression: tar.gz for efficient transfer
- Smart Compression: Skips re-compression of already compressed files
- Auto-Create Destinations: Creates destination directories automatically
- Backup & Retention: Auto-backup before upload, keeps last 10
- Modular Design: Six independent phases with clear error handling
- File Size Limits: 2GB for uploads, 10GB for downloads (prevents resource exhaustion)
- Disk Space Validation: Pre-transfer check with 20% buffer
- Path Normalization: Prevents path traversal attacks
- Symlink Safety: Validates symlink targets
- Dangerous Command Blocking: Prevents rm -rf /, dd, shutdown, fork bombs, etc.
- Script Security: No sudo/su, no remote code execution patterns
- Resource Limits: CPU, memory, process, and timeout restrictions
- Secure Cleanup: SSH keys overwritten with zeros
- Pre-flight Connection Test: Validates SSH before file operations
- Comprehensive Validation: Source, destination, permissions, space
- Error Handling: Detailed error messages at each phase
- Automatic Cleanup: Temporary files removed even on failure
- Script Validation: Syntax and structure checks before execution
- Dual Modes: Inline or remote script execution
- Validation: Syntax, structure, and security checks
- Resource Control: ulimit restrictions and timeouts
- Non-blocking: Script errors don't fail the action
The action supports various test scenarios to validate functionality:
Tests standard file upload with default settings (backup enabled).
jobs:
test-basic:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Create test files
run: |
mkdir -p test-data
echo "Test file" > test-data/test.txt
- name: Upload test files
uses: your-org/sshft@main
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
source: "test-data/"
destination: "/tmp/test-upload/"What it validates:
- SSH connection establishment
- Source file compression
- Destination directory creation (if doesn't exist)
- File transfer and extraction
- Default backup creation
Tests backup functionality with explicit backup configuration.
- name: Upload with backup
uses: your-org/sshft@main
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
source: "test-data/"
destination: "/tmp/test-with-backup/"
backup_before_transfer: trueWhat it validates:
- Backup creation before transfer
- Backup file naming with timestamp
- Backup compression (tar.gz)
- Backup retention policy (keeps last 10)
- First-time deployment (no backup needed)
Tests upload with backup disabled.
- name: Upload without backup
uses: your-org/sshft@main
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
source: "test-data/"
destination: "/tmp/test-no-backup/"
backup_before_transfer: falseWhat it validates:
- Transfer without backup step
- Faster deployment for non-critical files
- Backup step properly skipped
Tests post-transfer script execution.
- name: Upload with post-script
uses: your-org/sshft@main
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
source: "test-data/"
destination: "/tmp/test-with-script/"
post_script: |
echo "Post-transfer script executed"
ls -la /tmp/test-with-script/
echo "File count: $(find /tmp/test-with-script/ -type f | wc -l)"What it validates:
- Inline script upload and execution
- Script syntax validation
- Script output capture
- Security validations (dangerous commands blocked)
- Resource limits enforcement
Tests file download from remote server.
jobs:
test-download:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Upload test files first
- name: Upload test files
uses: your-org/sshft@main
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
source: "test-data/"
destination: "/tmp/test-download/"
backup_before_transfer: false
# Download them back
- name: Download files
uses: your-org/sshft@main
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
source: "/tmp/test-download/"
destination: "./downloaded-files/"
direction: "download"
# Save downloaded files as artifacts to persist
- name: Save downloaded files
uses: actions/upload-artifact@v4
with:
name: test-downloaded-files
path: ./downloaded-files/
- name: Verify downloaded files
run: |
ls -la ./downloaded-files/
echo "File count: $(find ./downloaded-files/ -type f | wc -l)"What it validates:
- Bidirectional transfer capability
- Remote file compression
- Local file extraction
- File integrity after round-trip
- Artifact persistence for downloads
Non-existent source (should fail):
- name: Test non-existent source
continue-on-error: true
uses: your-org/sshft@main
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
source: "/tmp/does-not-exist/"
destination: "./should-fail/"
direction: "download"Large file handling:
- name: Create large file
run: dd if=/dev/zero of=large-file.bin bs=1M count=100
- name: Upload large file
uses: your-org/sshft@main
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
source: "large-file.bin"
destination: "/tmp/large-file-test/"-
Clean Up Remote Files: Remove test files after each run
- name: Cleanup if: always() run: | ssh -i key user@host "rm -rf /tmp/test-*"
-
Use Unique Paths: Include run ID or timestamp in paths
destination: "/tmp/test-${{ github.run_id }}/"
-
Verify Outputs: Check action outputs after each test
- name: Verify success run: | if [ "${{ steps.upload.outputs.success }}" != "true" ]; then echo "Upload failed!" exit 1 fi
-
Test Error Conditions: Use
continue-on-error: truefor negative tests- name: Test failure scenario continue-on-error: true id: should_fail uses: your-org/sshft@main # ... invalid configuration ... - name: Verify it failed run: | if [ "${{ steps.should_fail.outcome }}" != "failure" ]; then echo "Should have failed but didn't!" exit 1 fi
| Scenario | Backup Created | Script Executed | Expected Outcome |
|---|---|---|---|
| Basic Upload (first time) | No (destination doesn't exist) | No | ✅ Success |
| Basic Upload (subsequent) | Yes | No | ✅ Success |
| With Backup Enabled | Yes (if destination exists) | No | ✅ Success |
| Without Backup | No | No | ✅ Success |
| With Post-Script | Varies | Yes | ✅ Success |
| Download | N/A (download only) | Varies | ✅ Success |
| Non-existent Source | N/A | No | ❌ Failure (expected) |
📚 Complete Documentation Set:
- FEATURES.md - ⚡ Feature summary and quick reference
- README.md - 📖 Main documentation (you are here)
- SECURITY.md - 🔒 Security protections and best practices
- EXAMPLES.md - 💡 Usage examples and patterns
- QUICK_REFERENCE.md - 📋 Quick lookup and troubleshooting
- VISUAL_GUIDE.md - 📊 Visual diagrams and workflows
- ARCHITECTURE.md - 🏗️ Technical architecture
- IMPROVEMENTS.md - 📝 Feature history and changes
Recommended Reading:
- New users: FEATURES.md → README.md → EXAMPLES.md
- Security review: SECURITY.md → FEATURES.md
- Troubleshooting: QUICK_REFERENCE.md
- Complete guide: See DOCS_INDEX.md
The action follows a modular workflow with clear separation of concerns. Each phase can succeed or fail independently with proper error handling.
graph LR
A[Phase 1<br/>SSH Setup] --> B[Phase 2<br/>Connection Test]
B --> C[Phase 3<br/>Backup]
C --> D[Phase 4<br/>File Transfer]
D --> E[Phase 5<br/>Post-Script]
E --> F[Phase 6<br/>Cleanup]
style A fill:#e1f5fe
style B fill:#f3e5f5
style C fill:#e0f7fa
style D fill:#fff3e0
style E fill:#e8eaf6
style F fill:#f1f8e9
flowchart LR
A[Start] --> B[Create SSH Directory]
B --> C[Save SSH Key]
C --> D[Validate Key Format]
D --> E{Valid?}
E -->|Yes| F[Create SSH Config]
E -->|No| G[❌ Exit]
F --> H[Setup Host Keys]
H --> I[✓ Ready]
style A fill:#e1f5fe
style I fill:#e8f5e8
style G fill:#ffebee
What happens: Sets up SSH credentials, validates the key, and configures connection parameters.
flowchart LR
A[SSH Config Ready] --> B[Test Connection]
B --> C{Connected?}
C -->|Yes| D[✓ Verified]
C -->|No| E[❌ Exit]
style A fill:#f3e5f5
style D fill:#e8f5e8
style E fill:#ffebee
What happens: Verifies SSH connectivity before attempting any file operations.
flowchart TD
A[Backup Enabled?] --> B{Yes}
B -->|Upload| C[Check Destination]
B -->|Download| Z[⊘ Skip]
C --> D{Exists?}
D -->|No| E[ℹ️ Skip - First Deploy]
D -->|Yes| F[Create ~/backups Dir]
F --> G[Compress Destination]
G --> H[Save as tar.gz]
H --> I[Report Size & Path]
I --> J[Cleanup Old Backups]
J --> K[✓ Backup Complete]
style A fill:#e0f7fa
style K fill:#e8f5e8
style E fill:#fff9c4
style Z fill:#f5f5f5
What happens:
- Creates backup of existing destination (upload only)
- Stores in
~/backups/backup_name_timestamp_id.tar.gz - Keeps last 10 backups per destination
- Skips if destination doesn't exist or if downloading
Outputs: backup_created, backup_path, backup_size
flowchart TD
A[Source Files] --> B[Check Source Exists]
B --> C[Compress Locally]
C --> D[Transfer tar.gz]
D --> E[Verify Destination Writable]
E --> F[Extract on Remote]
F --> G[Cleanup Temp Files]
G --> H[✓ Upload Complete]
style A fill:#fff3e0
style H fill:#e8f5e8
flowchart TD
A[Remote Files] --> B[Check Remote Source]
B --> C[Compress on Remote]
C --> D[Download tar.gz]
D --> E[Create Local Destination]
E --> F[Extract Locally]
F --> G[Cleanup Temp Files]
G --> H[✓ Download Complete]
style A fill:#fff3e0
style H fill:#e8f5e8
What happens:
- Validates source file/directory exists
- Creates destination directory if it doesn't exist (upload only)
- Compresses files using tar.gz for efficiency
- Validates disk space on remote server
- Transfers compressed archive
- Extracts at destination
- Cleans up temporary files on both ends
Output: success or error
flowchart TD
A[Script Provided?] --> B{Type}
B -->|None| Z[⊘ Skip]
B -->|Inline| C[Validate Syntax Locally]
B -->|Remote Path| D[Check File Exists]
C --> E[Check Structure]
D --> F[Validate Syntax Remotely]
E --> G{Valid?}
F --> G
G -->|No| H[⚠️ Log Error]
G -->|Yes| I[Execute Script]
I --> J{Success?}
J -->|Yes| K[📄 Capture Output]
J -->|No| L[⚠️ Log Error]
H --> M[✓ Continue]
K --> M
L --> M
style A fill:#e8eaf6
style M fill:#e8f5e8
style H fill:#fff9c4
style L fill:#fff9c4
style Z fill:#f5f5f5
Validation checks:
- ✅ Syntax errors (
bash -n) - ✅ Unmatched braces, brackets, parentheses
- ✅ Empty scripts
- ✅ File existence/readability (remote scripts)
What happens:
- Scripts are validated before execution
- Errors don't fail the action (graceful handling)
- Output and errors are captured separately
Outputs: script_executed, script_output, script_error
flowchart LR
A[Always Runs] --> B[Stop ssh-agent]
B --> C[Securely Delete SSH Key]
C --> D[Remove Config Files]
D --> E[Remove Temp Files]
E --> F[✓ Complete]
style A fill:#f1f8e9
style F fill:#e8f5e8
What happens: Securely removes all temporary files and SSH credentials, regardless of success or failure.
| Phase | Key Features |
|---|---|
| SSH Setup | • Key validation • Unique file names • Passphrase support |
| Connection | • Pre-flight check • Early failure detection |
| Backup | • Automatic creation • Timestamped archives • Retention policy (10 backups) • Upload only |
| Transfer | • Auto-create destination • tar.gz compression • Bidirectional support • Disk space validation • Integrity checks |
| Post-Script | • Syntax validation • Structural checks • Graceful error handling |
| Cleanup | • Secure key deletion • Temp file removal • Always executes |
Each phase has independent error handling:
- SSH Setup: Validates key format before proceeding
- Connection: Fails fast if SSH connection isn't possible
- Backup: Fails only if backup is enabled and fails (transfer won't proceed)
- Transfer: Validates source/destination before transfer
- Post-Script: Logs errors but doesn't fail the action
- Cleanup: Always runs, even on failure
graph TD
Start([Start Action]) --> SSH[SSH Setup]
SSH --> |✓| Conn[Connection Test]
SSH --> |✗| End
Conn --> |✓| Backup[Backup Phase]
Conn --> |✗| End
Backup --> |✓ or Skipped| Transfer[File Transfer]
Backup --> |✗| End
Transfer --> |✓| Script[Post-Script]
Transfer --> |✗| End
Script --> |Any Result| Cleanup[Cleanup]
Cleanup --> End([End Action])
End --> |Always| Report[Report Outputs]
style Start fill:#e1f5fe
style End fill:#e1f5fe
style Report fill:#e8f5e8
style SSH fill:#f3e5f5
style Conn fill:#fff3e0
style Backup fill:#e0f7fa
style Transfer fill:#fff9c4
style Script fill:#e8eaf6
style Cleanup fill:#f1f8e9
The post-transfer script feature includes comprehensive error handling and validation to ensure reliability:
-
Script Validation:
- Syntax checking: Scripts are validated using
bash -nbefore execution - Structural validation: Checks for unmatched braces, parentheses, and brackets
- Pipeline validation: Detects incomplete or malformed pipelines
- Empty script detection: Prevents execution of empty or whitespace-only scripts
- Quote matching: Identifies potential unmatched quotes
- Inline scripts: Validated locally before upload to remote server
- Remote scripts: Validated for existence, readability, and syntax before execution
- Double validation: Remote scripts are checked both locally and on the remote server
- Syntax checking: Scripts are validated using
-
Graceful Failure:
- Script validation failures do NOT fail the entire action
- Script execution failures do NOT fail the entire action
- Clear, detailed error messages are provided for all validation failures
- Script errors are captured in the
script_erroroutput - Validation errors specify the exact issue (e.g., "unmatched braces: open 3, close 2")
-
Security:
- Inline scripts are uploaded to temporary files with unique names
- Temporary script files are automatically cleaned up after execution
- Scripts are executed with proper permissions
- Non-executable scripts are run with bash interpreter
- All paths are properly quoted to handle special characters
-
Output Handling:
- Script output is captured and sanitized
- Output is truncated to 10KB to prevent workflow log overflow
- All script output is available via the
script_outputoutput variable - Exit codes are captured and reported
| Error Message | Cause | Solution |
|---|---|---|
Script is empty or malformed |
Inline script contains only whitespace | Provide a valid script with commands |
Script syntax error: unexpected token or malformed command |
Bash syntax validation failed | Fix syntax errors (missing semicolons, quotes, etc.) |
Script validation failed: malformed syntax |
Structural issues detected | Fix unmatched braces, brackets, or parentheses |
Unmatched braces (open: X, close: Y) |
Mismatched { and } in script |
Balance braces in conditional or function blocks |
Unmatched parentheses (open: X, close: Y) |
Mismatched ( and ) in script |
Balance parentheses in subshells or arithmetic |
Unmatched brackets (open: X, close: Y) |
Mismatched [ and ] in script |
Balance brackets in test conditions |
Pipeline without command detected |
Pipe symbol with missing command | Add command after pipe or remove extra pipe |
Script file does not exist |
Remote script path is incorrect | Verify the path on the remote server |
Script file is not readable |
Permission issues with remote script | Check file permissions on remote server |
Script file is empty |
Remote script file has zero size | Add content to the script file |
Script execution failed with exit code X |
Script ran but returned error | Debug script logic and check error logs |
Failed to upload script file |
Network or permission issues | Check SSH connection and permissions |
Remote script syntax validation failed |
Script has different behavior on remote | Test script on remote server manually |
- SSH private keys stored as GitHub secrets
- Key format validation before use
- Host key verification enabled by default
- Unique temporary filenames prevent conflicts
- Secure cleanup: keys overwritten with zeros before deletion
- Connection timeouts and keep-alive settings
- Passphrase support for encrypted keys
- Size limits: 2GB for uploads, 10GB for downloads (prevents resource exhaustion)
- Disk space validation: Checks available space with 20% buffer
- Path security: Normalizes paths, validates destinations
- Auto-create destinations: Creates missing directories with secure permissions (755)
- Symlink safety: Detects and validates symlink targets
- Permission checks: Verifies read/write access before operations
- Dangerous command blocking: Prevents destructive operations (rm -rf /, dd, shutdown, etc.)
- Privilege escalation prevention: Blocks sudo/su commands
- Remote code execution prevention: Blocks curl/wget piped to bash
- Syntax validation: Pre-execution bash syntax checking
- Resource limits: CPU (5min), memory (2GB), processes (100), file size (1GB)
- Execution timeout: 10-minute maximum per script
- Command injection prevention: Detects nested substitutions
- Secure upload: Scripts transferred via SCP, cleaned up after execution
- Use
strict_host_key_checking: truein production - Keep SSH keys in GitHub secrets, never commit them
- Use
post_script_pathfor complex scripts (easier to test) - Test scripts on target server before using in workflows
- Monitor backup storage (
~/backups) periodically - Review script validation errors carefully
| Name | Description |
|---|---|
success |
File transfer was successful |
backup_created |
Whether a backup was created before transfer |
backup_path |
Full path to the backup file on remote server |
backup_size |
Human-readable size of the backup file |
error |
Error message on failure |
script_executed |
Whether a post-transfer script was executed |
script_output |
Output from the post-transfer script execution |
script_error |
Error message if script execution failed |