{"id":26365408,"url":"https://github.com/2moe/local-ssh-action","last_synced_at":"2025-06-27T15:07:11.303Z","repository":{"id":230150482,"uuid":"778550644","full_name":"2moe/local-ssh-action","owner":"2moe","description":"Github Action: Connect using the local ssh bin and run commands.","archived":false,"fork":false,"pushed_at":"2025-03-14T03:08:41.000Z","size":484,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"dev","last_synced_at":"2025-06-27T15:06:13.789Z","etag":null,"topics":["github-actions","ssh","ssh-action"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/2moe.png","metadata":{"files":{"readme":"Readme.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE-APACHE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-03-27T23:39:13.000Z","updated_at":"2025-03-15T05:03:15.000Z","dependencies_parsed_at":"2024-03-28T05:31:20.178Z","dependency_job_id":"4e4990ab-6c1c-425a-8a2a-a9d5a69a5ff4","html_url":"https://github.com/2moe/local-ssh-action","commit_stats":null,"previous_names":["2moe/local-ssh-action"],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/2moe/local-ssh-action","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/2moe%2Flocal-ssh-action","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/2moe%2Flocal-ssh-action/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/2moe%2Flocal-ssh-action/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/2moe%2Flocal-ssh-action/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/2moe","download_url":"https://codeload.github.com/2moe/local-ssh-action/tar.gz/refs/heads/dev","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/2moe%2Flocal-ssh-action/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":262279119,"owners_count":23286549,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["github-actions","ssh","ssh-action"],"created_at":"2025-03-16T19:36:24.420Z","updated_at":"2025-06-27T15:07:11.266Z","avatar_url":"https://github.com/2moe.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Local SSH Action\n\n| Language/語言                        | ID         |\n| ------------------------------------ | ---------- |\n| English                              | en-Latn-US |\n| [简体中文](./docs/Readme-zh.md)      | zh-Hans-CN |\n| [繁體中文](./docs/Readme-zh-Hant.md) | zh-Hant-TW |\n\nUnlike other ssh actions, this one depends on local ssh.\n\nIt is more suitable for **self-hosted** runners.\n\nOn Ubuntu, if you don't have an ssh client, you will need to install `openssh-client` manually.\n\n## What was the original intention of creating this action?\n\nThe original intention was to facilitate the connection of virtual machine.\n\nBecause some platform's cross-compilation toolchains are a bit tricky to handle, even if you get it done, setting up a test environment might not be as simple as you think.\n\nFor example, if I want to build a rust bin crate (binary package) for OpenBSD riscv64 on Linux arm64, then using the conventional method would be very tough.\nBut opening a virtual machine directly is much easier, you don't need to understand many details, and you can get started right away.\n\nInitially, it was just a few lines of simple javascript scripts.\nLater, based on my experience using ssh on github actions, I rewrote the core logic in rust and added many useful options.\n\nI believe it will bring you a good ssh action experience.\n\n## Inputs\n\n| Inputs                      | Description                                                                                                       | Default   |\n| --------------------------- | ----------------------------------------------------------------------------------------------------------------- | --------- |\n| log-level                   | optional values: \"trace\", \"debug\", \"info\", \"warn\", \"error\", \"off\"                                                 | info      |\n| pre-local-workdir           | Localhost working directory                                                                                       |           |\n| pre-local-cmd               | Execute the command through NodeJS's `spawn()` or `spawnSync()` before connecting to ssh.                         |           |\n| pre-local-cmd-async         | Type: boolean. When true, the command is run asynchronously.                                                      | true      |\n| allow-pre-local-cmd-failure | When true, ignore the errors of pre-local-cmd.                                                                    | false     |\n| pre-sleep                   | blocking for a specific time before connecting to ssh, in seconds.                                                | 0         |\n| pre-timeout                 | Since the ssh connection may fail, specifying pre-timeout allows it to keep trying to connect until it times out. | 0         |\n| pre-exit-cmd                | The command needed to test ssh connection.                                                                        | exit      |\n| host                        | Remote Server Host                                                                                                | 127.0.0.1 |\n| ssh-bin                     | In an unstable network environment, you may need to use a specific ssh, not the openssh client.                   | ssh       |\n| run                         | Commands to be executed on remote server                                                                          |           |\n| allow-run-failure           | Type: boolean                                                                                                     | false     |\n| post-run                    | After the main `run` is completed, you can continue to run `post-run`                                             |           |\n| allow-post-run-failure      | Type: boolean                                                                                                     | true      |\n| args                        | SSH Client args. You can enter shell arguments for ssh, such as `-q`                                              |           |\n\n## Get Started\n\nLet's start with a simple example.\n\n```yaml\nname: test\non: push\njobs:\n  test:\n    runs-on: self-hosted\n    steps:\n      - id: ssh-action\n        uses: 2moe/local-ssh-action@v0\n        with:\n          log-level: debug\n          pre-local-cmd: printf \"pre-local-cmd is running on the local host\\n\"\n          pre-timeout: 20\n          args: -q\n          host: android-mobile\n          run: |\n            printf \"This is on the remote host\\n\"\n            /system/bin/toybox uname -m\n          allow-run-failure: true\n          post-run: printf \"Bye\\n\"\n```\n\nYou may not understand the content above, and this workflow may fail.\n\nDon't worry, let me explain it to you slowly.\n\nThe example above is essentially executing `ssh android-mobile uname -m`. If there is no ssh configuration for `android-mobile` on the machine running actions, then it cannot connect.\n\nThe solution is simple, just create a configuration.\n\n```yaml\nname: test\non: push\njobs:\n  test:\n    runs-on: self-hosted\n    steps:\n      - name: create ssh config\n        shell: sh -ex {0}\n        run: |\n          printf '\n          Host android-mobile\n            Hostname 192.168.123.234\n            User shell\n            Port 8222\n            IdentityFile ~/.ssh/key/mobile_ed25519\n            StrictHostKeyChecking no\n          ' \u003e cfg\n          install -Dm600 cfg ~/.ssh/config.d/mobile.sshconf\n          printf \"%s\\n\" 'Include config.d/*.sshconf' \u003e\u003e ~/.ssh/config\n\n      - id: ssh-action\n        uses: 2moe/local-ssh-action@v0\n        with:\n          log-level: warn\n          host: android-mobile\n          run: uname -m\n```\n\nThe configuration only needs to be created once. If the self-hosted machine has already created the android-mobile configuration, then there is no need to create it again before the next connection.\n\nIf you don't want to create a configuration, don't want to use the ed25519 key, and want to connect with a password in a single step, that can also be done.\n\nBut before that, we need to understand its basic usage first.\n\n## Detailed Explanation\n\nThe `with` can accept many options, such as:\n\n```yaml\nwith:\n  log-level: debug\n```\n\nThis section will provide a detailed explanation of these options.\n\n### Core\n\nBefore expanding the explanation, you need to understand a core element: the process is carried out in stages.\n\nThey are:\n\n- Pre\n- Main\n- Post\n\nThe formal stage of the ssh connection is Main, before the connection is Pre, and after the connection is Post.\n\nQ: Why divide it into stages?\n\nA:\n\n- Because the ssh connection may fail before it is established. Therefore, in the Pre stage, you can keep trying to connect within a specified time until the connection is successful.\n- After the ssh connection is completed, you may need to perform some cleanup tasks (e.g., shut down the virtual machine)\n  - The cleanup task may fail, separating main and post can handle the task status separately.\n    - i.e., main failure is not allowed, but post failure is allowed.\n\n### Pre Stage\n\n#### pre-local-workdir\n\n```yaml\nwith:\n  pre-local-workdir: /path/to/local-dir\n```\n\n- Type: String (file path)\n\nThis option modifies the local working directory, not the remote ssh directory.\n\nQ: Why is there this option?\n\nA: Suppose the ssh configuration file is not in **~/.ssh**, but is located in a specific directory. By specifying the directory before connecting to ssh, some operations can be simplified.\n\n#### pre-local-cmd\n\n```yaml\nwith:\n  pre-local-cmd: ls -lah\n```\n\n- Type: String\n\nBefore connecting to ssh, execute commands through NodeJS's `spawn()` or `spawnSync()`.\n\n\u003e This is running on the local host, not the remote host.\n\nSuppose `pre-local-cmd: ls -la -h` and `pre-local-cmd-async` is not configured, then it will automatically be parsed as `spawn(\"ls\", [\"-la\", \"-h\"])`\n\n#### pre-local-cmd-async\n\n```yaml\nwith:\n  pre-local-cmd-async: true\n```\n\n- Type: `bool`\n- Default: `true`\n\n- When it's true, the command runs asynchronously.\n  - That is, before connecting to the remote ssh, let the local task run in the background.\n- When it's false, the command runs synchronously (blocking).\n  - Before connecting to the remote ssh, you must wait for the pre-local-cmd task to complete before you can continue to connect to ssh.\n\nThe default is true, i.e., it is asynchronous by default.\n\n#### allow-pre-local-cmd-failure\n\n```yaml\nwith:\n  allow-pre-local-cmd-failure: true\n```\n\n- Type: `bool`\n- Default: `true`\n\n- When it's true, ignore the errors of pre-local-cmd.\n  - More accurately, when pre-local-cmd fails, it will not cause the current step to crash.\n- When it's false, if pre-local-cmd errors out, then this step will exit abnormally.\n\n#### pre-sleep\n\n```yaml\nwith:\n  pre-sleep: 0\n```\n\n- Typescript type: number\n- Rust type: u32\n- Default: 0\n\nBefore connecting to ssh, synchronously (blocking) for a specific time, in seconds.\n\nSuppose you want to connect to a machine that is currently restarting, if you connect now, it may disconnect after a few seconds.\n\nAt this time, you need to forcibly block, wait a few seconds, let it completely shut down, and then try to connect.\n\n- Examples:\n  - `pre-sleep: 1`, block for 1 second.\n  - `pre-sleep: 30`, block for 30 seconds.\n\nThe reason for emphasizing the rust type is because it is parsed through the following function in the internal implementation.\n\n```rust\nfn parse_gh_num(input: \u0026str) -\u003e u32 {\n    match get_action_input(input).trim() {\n        \"\" =\u003e 0,\n        x =\u003e x.parse().unwrap_or(0),\n    }\n}\n```\n\nu32 must be `\u003e=0`, i.e., you cannot use `pre_sleep: -1` to represent infinite blocking.\n\nP.S. If you need to test whether you can connect normally within a specified time, please use `pre-timeout`, not `pre-sleep`.\n\n#### pre-timeout\n\n```yaml\nwith:\n  pre-timeout: 0\n```\n\n- Type: u32\n- Default: 0\n\nBecause the ssh connection may fail, specifying pre-timeout allows you to wait for a specific time.\n\nUnlike the blocking pre-sleep, for pre-timeout, once the test connection is successful, it will exit waiting.\n\n- Examples:\n  - `pre-timeout: 120`, the waiting timeout is 120 seconds.\n\nSuppose you want to connect to a virtual machine that is currently booting up, then you have to wait for it to connect to the network and start the `sshd` process before you can connect to it.\n\nIf `pre-timeout: 30`, and the boot time of the virtual machine + the time to start sshd is 10 seconds, then it will not wait for 30 seconds, at 10+ seconds, once the test connection is successful, it will exit waiting.\n\n#### pre-exit-cmd\n\n```yaml\nwith:\n  pre-exit-cmd: exit\n```\n\n- Type: String\n- Default: \"exit\"\n\nThe command needed to test the ssh connection.\n\nThis option will only take effect when `pre-timeout \u003e 0`.\n\nSuppose `pre-timeout: 20`, `pre-exit-cmd: exit`, `host: netbsd-vm`\n\nThen it will keep performing the `ssh netbsd-vm exit` connection test within 20 seconds.\n\nOnly after the test connection is successful, will it proceed to the next step.\n\nIf it fails after 20 seconds, then the entire step will fail.\n\n### Shared in Multiple Stages\n\n#### log-level\n\n```yaml\nwith:\n  log-level: debug\n```\n\n- Type: `enum LogLevel`\n- Default: `info`\n- Optional values: \"trace\", \"debug\", \"info\", \"warn\", \"error\", \"off\"\n\nAmong them, trace is the most detailed, debug is the second most detailed, and off has no logs.\n\n#### host\n\n```yaml\nwith:\n  host: \"127.0.0.1\"\n```\n\n- Type: String\n- Default: \"127.0.0.1\"\n\nRemote host name or IP address.\n\n#### ssh-bin\n\n```yaml\nwith:\n  ssh-bin: ssh\n```\n\n- Type: String\n- Default: \"ssh\"\n\nIn an unstable network environment, you may need to use a specific ssh, not the openssh client.\n\nAs long as the command syntax conforms to the `{ssh-bin} {args} {host} {run}` rule, anything can be used.\n\nSuppose there is `adb -s android-16 shell [run]`, then you can use\n\n```yaml\nssh-bin: adb\nargs: -s android-16\nhost: shell\nrun: |\n  ls -lh\n  toybox printf \"I am Android\\n\"\n```\n\nSuppose you need to automatically enter the password, then `sshpass -p $passwd ssh 192.168.50.10 [run]` can be converted to:\n\n```yaml\nssh-bin: sshpass\n# Please change 123456 to ${{secrets.SSH_PASSWD}}\nargs: |\n  -p 123456\n  ssh\nhost: \"192.168.50.10\"\nrun: |\n  printf \"Hello\\n\"\n```\n\n#### args\n\nThe parameters passed to `ssh_bin`, for example `-q -o ServerAliveInterval=60`\n\n### Main Stage\n\n#### run\n\n- required: true\n- Type: String\n\nThe command executed on the remote host, the shell called during execution depends on the default login shell on the remote host.\n\n#### allow-run-failure\n\n```yaml\nwith:\n  allow-run-failure: true\n```\n\n- Type: `bool`\n- Default: `true`\n\n- When it's true, if run errors out, it will not cause the current step to crash.\n- When it's false, if run errors out, then this step will exit abnormally.\n\n### Post Stage\n\n#### post-run\n\nSimilar to run, but it runs in the Post stage.\n\n#### allow-post-run-failure\n\nSimilar to allow-run-failure, but it captures the exit status of post-run, not run.\n\n## Outputs\n\n| Outputs               |\n| --------------------- |\n| pre-local-cmd-success |\n| main-run-success      |\n| post-run-success      |\n\nYou can use `${{steps.\"ssh-step-id\".outputs.main-run-success}}`, and change \"ssh-step-id\" to a specific id, to judge whether run is successful.\n\n```rs\nSuccess =\u003e true,\nFailure | Not-Run =\u003e false\n```\n\nPlease see the example below:\n\n```yaml\n      - name: ssh-action\n        id: act\n        uses: 2moe/local-ssh-action@v0\n        with:\n          log-level: debug\n          args: -q\n          host: rv64\n          pre-local-workdir: /tmp\n          pre-local-cmd: pwd\n          pre-local-cmd-async: false\n          allow-pre-local-cmd-failure: false\n          pre-sleep: 1\n          pre-timeout: 20\n          run: printf \"It's on the remote-host\\n\"\n          allow-run-failure: false\n          post-run: exit 127\n          allow-post-run-failure: true\n\n      - name: get ssh-action outputs\n        run: |\n          printf \"\n            pre-local: ${{steps.act.outputs.pre-local-cmd-success}}\n            main: ${{steps.act.outputs.main-run-success}}\n            post: ${{steps.act.outputs.post-run-success}}\n            \"\n```\n\nThe output result is:\n\n```log\n21:59:05.171 [DEBUG] ssh_action_wasm:140 set_pre_local_workdir()\n21:59:05.193 [INFO] ssh_action_wasm:153 old local working directory: /var/runners/2moe-private/_work/private/private\n21:59:05.196 [INFO] ssh_action_wasm:155 new local working directory: /tmp\n21:59:05.199 [DEBUG] ssh_action_wasm:102 run_pre_local_cmd()\n21:59:05.200 [DEBUG] ssh_action_wasm:114 raw: pwd\n21:59:05.201 [DEBUG] ssh_action_wasm:87 split_shell_cmd()\n21:59:05.204 [INFO] ssh_action_wasm:96 cmd: pwd, args: []\n21:59:05.206 [INFO] ssh_action_wasm:127 running pwd...\n/tmp\n21:59:05.241 [DEBUG] ssh_action_wasm:230 pre_timeout_connection()\n21:59:05.244 [DEBUG] ssh_action_wasm:244 InputConfig { ssh_bin: \"ssh\", args: Some([\"-q\"]), host: \"rv64\", pre_exit_cmd: [\"exit\"], pre_sleep: 1, pre_timeout: 20, run: \"printf \\\"It's on the remote-host\\\\n\\\"\", allow_run_failure: false, post_run: Some(\"exit 127\"), allow_post_run_failure: true }\n21:59:05.248 [INFO] ssh_action_wasm:247 sleep: 1s\n21:59:06.249 [DEBUG] ssh_action_wasm:263 pre_args: [\"-q\", \"rv64\", \"exit\"]\n21:59:06.251 [INFO] ssh_action_wasm:264 pre_timeout: 20s\n21:59:09.268 [DEBUG] ssh_action_wasm:279 main_args: [\"-q\", \"rv64\", \"printf \\\"It's on the remote-host\\\\n\\\"\"]\nIt's on the remote-host\n21:59:09.952 [DEBUG] ssh_action_wasm:294 post_args: [\"-q\", \"rv64\", \"exit 127\"]\n21:59:10.624 [ERROR] ssh_action_wasm:298 Post-Run: JsValue(Error: Failed to run the task:\n    cmd: ssh\n    args: -q,rv64,exit 127\n    exit-code: 127\n    sync: true\n...\n```\n\n```yaml\n  pre-local: true\n  main: true\n  post: false\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F2moe%2Flocal-ssh-action","html_url":"https://awesome.ecosyste.ms/projects/github.com%2F2moe%2Flocal-ssh-action","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F2moe%2Flocal-ssh-action/lists"}