--- description: "Deploy from GitHub Actions to any SSH server, SFTP or SCP only, with the node-scp action: every input, where files end up and how to pin the host key." --- # Deploying from GitHub Actions The repository is also a GitHub Action. It runs the node-scp CLI, so it works with SFTP servers and SCP only devices alike. ```mermaid flowchart LR A["git push"] --> B["checkout
and build"] B --> C["maitrungduc1410/node-scp-async@v1"] S[("secrets
key, fingerprint")] -.-> C C -- "SFTP or SCP" --> D["your server"] ``` ```yaml name: Deploy on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - run: npm ci && npm run build - uses: maitrungduc1410/node-scp-async@v1 with: host: ${{ secrets.DEPLOY_HOST }} username: deploy private-key: ${{ secrets.DEPLOY_KEY }} fingerprint: ${{ secrets.DEPLOY_HOST_FINGERPRINT }} source: dist/ target: /var/www/app exclude: | *.map .DS_Store ``` ## Inputs | Input | Default | Description | | --- | --- | --- | | `host` | required | Host name or IP address. IPv6 works too. | | `port` | `22` | SSH port. | | `username` | required | User name. | | `private-key` | | Private key contents. Use a secret. | | `passphrase` | | Passphrase for the key. | | `password` | | Password, if you cannot use a key. | | `fingerprint` | | Expected host key, `SHA256:...`. Comma separated for several. | | `source` | required | Paths to copy, one per line. | | `target` | required | Destination. | | `direction` | `upload` | `upload` or `download`. For downloads `source` is remote and `target` local. | | `protocol` | `auto` | `auto`, `sftp` or `scp`. | | `recursive` | `true` | Copy directories. | | `preserve` | `false` | Keep times and full modes. New files get the source permissions either way. | | `exclude` | | Patterns to skip, one per line. A pattern without `/` matches names at any depth. | | `concurrency` | `4` | Parallel files over SFTP. | | `version` | `1` | node-scp version the action runs. | ## Where files end up The action follows `scp` rules: - `source: dist` and `target: /var/www/app` makes `/var/www/app` a copy of `dist` when `/var/www/app` does not exist yet, and creates `/var/www/app/dist` when it does. - A trailing slash on the target (`/var/www/app/`) always means "into this directory". - Several sources always go into the target directory. To replace a directory atomically, upload to a new release directory and switch a symlink afterwards with an `ssh` step. ## Pin the host key Without `fingerprint` the action accepts any host key and prints a warning. Get the value once from a trusted network: ```sh ssh-keyscan -t ed25519 example.com 2>/dev/null | ssh-keygen -lf - # 256 SHA256:q7Jx0fT3cM2yKpV9LrWn4sEaB8uHdZ1oGiXe6NcRtYk example.com (ED25519) ``` Store `SHA256:q7Jx...` as a secret or variable and pass it as `fingerprint`. ## Using the CLI directly The action is a thin wrapper, so any step with Node can do the same: ```yaml - run: npx node-scp@1 -r --fingerprint "$FP" dist/ deploy@example.com:/var/www/app env: NODE_SCP_PRIVATE_KEY: ${{ secrets.DEPLOY_KEY }} FP: ${{ vars.DEPLOY_HOST_FINGERPRINT }} ```