Repository: owenthereal/action-upterm
Pull request #5: Support linux amd64
Author: login: owenthereal
Created: 2025-05-19T17:40:30Z | Merged: 2025-05-19T18:05:22Z

Support linux amd64 for upterm.

This closes https://github.com/owenthereal/action-upterm/pull/4


Diff:
diff --git a/src/index.test.ts b/src/index.test.ts
index 57e4660..07f6d4d 100644
--- a/src/index.test.ts
+++ b/src/index.test.ts
@@ -37,10 +37,13 @@ describe('upterm GitHub integration', () => {
     expect(core.info).toHaveBeenCalledWith('Windows is not supported by upterm, skipping...');
   });
 
-  it('should handle the main loop', async () => {
+  it('should handle the main loop for linux x64', async () => {
     Object.defineProperty(process, 'platform', {
       value: 'linux'
     });
+    Object.defineProperty(process, 'arch', {
+      value: 'x64'
+    });
     when(core.getInput).calledWith('limit-access-to-users').mockReturnValue('');
     when(core.getInput).calledWith('limit-access-to-actor').mockReturnValue('false');
     when(core.getInput).calledWith('wait-timeout-minutes').mockReturnValue('');
@@ -58,10 +61,55 @@ describe('upterm GitHub integration', () => {
     expect(core.info).toHaveBeenNthCalledWith(4, "Exiting debugging session because '/continue' file was created");
   });
 
+  it('should handle the main loop for linux arm64', async () => {
+    Object.defineProperty(process, 'platform', {
+      value: 'linux'
+    });
+    Object.defineProperty(process, 'arch', {
+      value: 'arm64'
+    });
+    when(core.getInput).calledWith('limit-access-to-users').mockReturnValue('');
+    when(core.getInput).calledWith('limit-access-to-actor').mockReturnValue('false');
+    when(core.getInput).calledWith('wait-timeout-minutes').mockReturnValue('');
+    when(core.getInput).calledWith('upterm-server').mockReturnValue('ssh://myserver:22');
+
+    mockedExecShellCommand.mockReturnValue(Promise.resolve('foobar'));
+    await run();
+
+    expect(mockedExecShellCommand).toHaveBeenNthCalledWith(1, 'curl -sL https://github.com/owenthereal/upterm/releases/latest/download/upterm_linux_arm64.tar.gz | tar zxvf - -C /tmp upterm && sudo install /tmp/upterm /usr/local/bin/');
+    expect(mockedExecShellCommand).toHaveBeenNthCalledWith(2, 'if ! command -v tmux &>/dev/null; then sudo apt-get update && sudo apt-get -y install tmux; fi');
+
+    expect(core.info).toHaveBeenNthCalledWith(1, 'Auto-generating ~/.ssh/known_hosts by attempting connection to uptermd.upterm.dev');
+    expect(core.info).toHaveBeenNthCalledWith(2, 'Creating a new session. Connecting to upterm server ssh://myserver:22');
+    expect(core.info).toHaveBeenNthCalledWith(3, 'Waiting for upterm to be ready...');
+    expect(core.info).toHaveBeenNthCalledWith(4, "Exiting debugging session because '/continue' file was created");
+  });
+
+  it('error handling for unsupported linux arch', async () => {
+    Object.defineProperty(process, 'platform', {
+      value: 'linux'
+    });
+    Object.defineProperty(process, 'arch', {
+      value: 'unknown'
+    });
+    when(core.getInput).calledWith('limit-access-to-users').mockReturnValue('');
+    when(core.getInput).calledWith('limit-access-to-actor').mockReturnValue('false');
+    when(core.getInput).calledWith('wait-timeout-minutes').mockReturnValue('');
+    when(core.getInput).calledWith('upterm-server').mockReturnValue('ssh://myserver:22');
+
+    mockedExecShellCommand.mockReturnValue(Promise.resolve('foobar'));
+    await run();
+
+    expect(core.error).toHaveBeenNthCalledWith(1, 'Unsupported architecture for upterm: unknown. Only x64 and arm64 are supported.');
+  });
+
   it('should support custom known_hosts content', async () => {
     Object.defineProperty(process, 'platform', {
       value: 'linux'
     });
+    Object.defineProperty(process, 'arch', {
+      value: 'x64'
+    });
     when(core.getInput).calledWith('limit-access-to-users').mockReturnValue('');
     when(core.getInput).calledWith('limit-access-to-actor').mockReturnValue('false');
     when(core.getInput).calledWith('wait-timeout-minutes').mockReturnValue('');
diff --git a/src/index.ts b/src/index.ts
index c8a7a55..0003ad3 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -3,7 +3,6 @@ import fs from 'fs';
 import path from 'path';
 import * as core from '@actions/core';
 import * as github from '@actions/github';
-
 import {execShellCommand} from './helpers';
 
 const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
@@ -16,22 +15,37 @@ export async function run() {
     }
 
     core.debug('Installing dependencies');
-    if (process.platform == 'linux') {
-      await execShellCommand(`curl -sL https://github.com/owenthereal/upterm/releases/latest/download/upterm_linux_amd64.tar.gz | tar zxvf - -C /tmp upterm && sudo install /tmp/upterm /usr/local/bin/`);
+    if (process.platform === 'linux') {
+      let uptermArch: string;
+      switch (process.arch) {
+        case 'x64':
+          uptermArch = 'amd64';
+          break;
+        case 'arm64':
+          uptermArch = 'arm64';
+          break;
+        default:
+          core.error(`Unsupported architecture for upterm: ${process.arch}. Only x64 and arm64 are supported.`);
+          return;
+      }
+
+      await execShellCommand(`curl -sL https://github.com/owenthereal/upterm/releases/latest/download/upterm_linux_${uptermArch}.tar.gz | tar zxvf - -C /tmp upterm && sudo install /tmp/upterm /usr/local/bin/`);
       await execShellCommand('if ! command -v tmux &>/dev/null; then sudo apt-get update && sudo apt-get -y install tmux; fi');
     } else {
       await execShellCommand('brew install owenthereal/upterm/upterm tmux');
     }
     core.debug('Installed dependencies successfully');
 
+    // SSH key setup
     const sshPath = path.join(os.homedir(), '.ssh');
-    if (!fs.existsSync(path.join(sshPath, 'id_rsa'))) {
+    const idRsaPath = path.join(sshPath, 'id_rsa');
+    if (!fs.existsSync(idRsaPath)) {
       core.debug('Generating SSH keys');
       fs.mkdirSync(sshPath, {recursive: true});
       try {
         await execShellCommand(`ssh-keygen -q -t rsa -N "" -f ~/.ssh/id_rsa; ssh-keygen -q -t ed25519 -N "" -f ~/.ssh/id_ed25519`);
       } catch (error) {
-        core.error(`error running ssh-keygen. Error: ${error}`);
+        core.error(`Error running ssh-keygen: ${error}`);
         throw error;
       }
       core.debug('Generated SSH keys successfully');
@@ -39,35 +53,48 @@ export async function run() {
       core.debug('SSH key already exists');
     }
 
+    // SSH config
     core.debug('Configuring ssh client');
-    fs.appendFileSync(path.join(sshPath, 'config'), 'Host *\nStrictHostKeyChecking no\nCheckHostIP no\n' + 'TCPKeepAlive yes\nServerAliveInterval 30\nServerAliveCountMax 180\nVerifyHostKeyDNS yes\nUpdateHostKeys yes\n');
-    // entry in known hosts file in mandatory in upterm. attempt ssh connection to upterm server
-    // to get the host key added to ~/.ssh/known_hosts
-    if (core.getInput('ssh-known-hosts') && core.getInput('ssh-known-hosts') !== '') {
+    const sshConfig = `Host *
+  StrictHostKeyChecking no
+  CheckHostIP no
+  TCPKeepAlive yes
+  ServerAliveInterval 30
+  ServerAliveCountMax 180
+  VerifyHostKeyDNS yes
+  UpdateHostKeys yes
+`;
+    fs.appendFileSync(path.join(sshPath, 'config'), sshConfig);
+
+    // known_hosts setup
+    const knownHostsPath = path.join(sshPath, 'known_hosts');
+    const sshKnownHosts = core.getInput('ssh-known-hosts');
+    if (sshKnownHosts && sshKnownHosts !== '') {
       core.info('Appending ssh-known-hosts to ~/.ssh/known_hosts. Contents of ~/.ssh/known_hosts:');
-      fs.appendFileSync(path.join(sshPath, 'known_hosts'), core.getInput('ssh-known-hosts'));
+      fs.appendFileSync(knownHostsPath, sshKnownHosts);
       core.info(await execShellCommand('cat ~/.ssh/known_hosts'));
     } else {
       core.info('Auto-generating ~/.ssh/known_hosts by attempting connection to uptermd.upterm.dev');
       try {
         await execShellCommand('ssh-keyscan uptermd.upterm.dev 2> /dev/null >> ~/.ssh/known_hosts');
       } catch (error) {
-        core.error(`error running ssh-keyscan. Error: ${error}`);
+        core.error(`Error running ssh-keyscan: ${error}`);
         throw error;
       }
-      // @cert-authority entry is the mandatory entry. generate the entry based on the known_hosts entry key
+      // Add @cert-authority entry
       try {
-        await execShellCommand('cat <(cat ~/.ssh/known_hosts | awk \'{ print "@cert-authority * " $2 " " $3 }\') >> ~/.ssh/known_hosts');
+        await execShellCommand(`cat <(cat ~/.ssh/known_hosts | awk '{ print "@cert-authority * " $2 " " $3 }') >> ~/.ssh/known_hosts`);
       } catch (error) {
-        core.error(`error generating cert-authority entry. Error: ${error}`);
+        core.error(`Error generating cert-authority entry: ${error}`);
         throw error;
       }
     }
 
+    // Allowed users
     const allowedUsers = core
       .getInput('limit-access-to-users')
       .split(/[\s\n,]+/)
-      .filter(x => x !== '');
+      .filter(Boolean);
     if (core.getInput('limit-access-to-actor') === 'true') {
       core.info(`Adding actor "${github.context.actor}" to allowed users.`);
       allowedUsers.push(github.context.actor);
@@ -76,74 +103,73 @@ export async function run() {
 
     let authorizedKeysParameter = '';
     for (const allowedUser of uniqueAllowedUsers) {
-      if (allowedUser) {
-        authorizedKeysParameter += `--github-user "${allowedUser}"`;
-      }
+      authorizedKeysParameter += `--github-user "${allowedUser}" `;
     }
 
+    // Upterm session
     const uptermServer = core.getInput('upterm-server');
     const waitTimeoutMinutes = core.getInput('wait-timeout-minutes');
     core.info(`Creating a new session. Connecting to upterm server ${uptermServer}`);
     await execShellCommand(`tmux new -d -s upterm-wrapper -x 132 -y 43 "upterm host --accept --server '${uptermServer}' ${authorizedKeysParameter} --force-command 'tmux attach -t upterm' -- tmux new -s upterm -x 132 -y 43"`);
-    // resize terminal for largest client by default
+    // Resize terminal for largest client by default
     await execShellCommand('tmux set -t upterm-wrapper window-size largest; tmux set -t upterm window-size largest');
-    console.debug('Created new session successfully');
-    if (waitTimeoutMinutes !== '') {
-      let timeout;
-      try {
-        timeout = parseInt(waitTimeoutMinutes);
-      } catch (error) {
-        core.error(`wait-timeout-minutes must be set to an integer. Error: ${error}`);
-        throw error;
+    core.debug('Created new session successfully');
+
+    // Wait timeout logic
+    if (waitTimeoutMinutes) {
+      const timeout = parseInt(waitTimeoutMinutes, 10);
+      if (isNaN(timeout)) {
+        core.error(`wait-timeout-minutes must be set to an integer.`);
+        throw new Error('Invalid wait-timeout-minutes value');
       }
       await execShellCommand(`( sleep $(( ${timeout} * 60 )); if ! pgrep -f '^tmux attach ' &>/dev/null; then tmux kill-server; fi ) & disown`);
       core.info(`wait-timeout-minutes set - will wait for ${waitTimeoutMinutes} minutes for someone to connect, otherwise shut down`);
     }
 
+    // Wait for upterm socket to be ready
     let tries = 10;
-    while (true) {
-      if (tries <= 0) {
-        throw new Error('Failed to start upterm');
-      }
-
+    while (tries-- > 0) {
       core.info('Waiting for upterm to be ready...');
-      if (uptermSocketExists()) {
-        break;
-      }
-
+      if (uptermSocketExists()) break;
       await sleep(1000);
-      tries--;
+    }
+    if (!uptermSocketExists()) {
+      throw new Error('Failed to start upterm');
     }
 
-    console.debug('Entering main loop');
+    core.debug('Entering main loop');
+    // Main loop: wait for /continue file or upterm exit
     /*eslint no-constant-condition: ["error", { "checkLoops": false }]*/
     while (true) {
       if (continueFileExists()) {
         core.info("Exiting debugging session because '/continue' file was created");
         break;
       }
-
       if (!uptermSocketExists()) {
-        core.info("Exiting debugging session 'upterm' quit");
+        core.info("Exiting debugging session: 'upterm' quit");
         break;
       }
-
       try {
         core.info(await execShellCommand('upterm session current --admin-socket ~/.upterm/*.sock'));
       } catch (error) {
-        core.error(`error getting upterm session. Error: ${error}`);
+        core.error(`Error getting upterm session: ${error}`);
         throw error;
       }
-
       await sleep(5000);
     }
-  } catch (error) {
-    core.setFailed(error.message);
+  } catch (error: unknown) {
+    if (error instanceof Error) {
+      core.setFailed(error.message);
+    } else {
+      core.setFailed(String(error));
+    }
   }
 }
 
 function uptermSocketExists(): boolean {
-  return fs.readdirSync(path.join(os.homedir(), '.upterm')).filter(file => file.endsWith('.sock')).length > 0;
+  const uptermDir = path.join(os.homedir(), '.upterm');
+  if (!fs.existsSync(uptermDir)) return false;
+  return fs.readdirSync(uptermDir).some(file => file.endsWith('.sock'));
 }
 
 function continueFileExists(): boolean {


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR enhances Linux support by selecting the correct upterm binary for x64 and arm64, refactors SSH setup and session logic, and updates tests to cover the new architectures.

- Added `process.arch` switch to download `upterm_linux_amd64` or `upterm_linux_arm64`
- Refactored SSH key/config/known_hosts setup and improved loop & timeout logic
- Extended tests for linux x64, linux arm64, and unsupported architectures

### Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

| File              | Description                                                   |
| ----------------- | ------------------------------------------------------------- |
| src/index.ts      | Dynamic architecture detection, SSH config refactor, improved loops & error handling |
| src/index.test.ts | Added tests for linux x64, linux arm64, and unsupported arch |


<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**src/index.ts:51**
* [nitpick] Using `~/.ssh` in shell commands may not resolve in all environments. Consider using the absolute `idRsaPath` and a variable for the ed25519 path to ensure consistency.
```
`ssh-keygen -q -t rsa -N "" -f ~/.ssh/id_rsa; ssh-keygen -q -t ed25519 -N "" -f ~/.ssh/id_ed25519`
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-05-19T17:41:57Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR adds support for the Linux AMD64 and ARM64 architectures for upterm. Key changes include:
- Introducing architecture detection via a switch-case to dynamically build the download URL.
- Refactoring SSH key, config, and known_hosts setup with improved error messages.
- Updating tests to verify functionality for both x64 and arm64 environments.

### Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

| File                | Description                                                        |
| ------------------- | ------------------------------------------------------------------ |
| src/index.ts        | Added arch detection, conditional command building, and refactored error messaging for SSH setup and session handling. |
| src/index.test.ts   | Updated tests to cover Linux x64 and arm64 scenarios and error handling for unsupported architectures. |


<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**src/index.ts:131**
* [nitpick] Consider storing the result of uptermSocketExists() in a local variable inside the loop to avoid redundant file system calls, which could improve both performance and code readability.
```
while (tries-- > 0) { ... if (uptermSocketExists()) break; ... }
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-05-19T17:45:12Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR adds support for Linux on the AMD64 platform (and arm64) for upterm by refining the dependency installation, SSH setup, and session handling logic.  
- Added handling of arm64 alongside AMD64 using a switch on process.arch  
- Improved SSH key, config, and known_hosts management with updated error messages and command syntax  
- Extended test coverage for both linux x64 and arm64 as well as handling unsupported architectures

### Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

| File               | Description                                                               |
| ------------------ | ------------------------------------------------------------------------- |
| src/index.ts       | Added arm64 support, improved SSH setup and session management logic      |
| src/index.test.ts  | Enhanced tests to cover linux x64/arm64 scenarios and unsupported arch case |


<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**src/index.ts:105**
* [nitpick] Consider using Array.map and join to construct authorizedKeysParameter for improved readability.
```
for (const allowedUser of uniqueAllowedUsers) { authorizedKeysParameter += `--github-user "${allowedUser}" `; }
```
**src/index.ts:86**
* [nitpick] Ensure the target environment uses bash, as process substitution syntax may not be supported in all shells.
```
await execShellCommand(`cat <(cat ~/.ssh/known_hosts | awk '{ print "@cert-authority * " $2 " " $3 }') >> ~/.ssh/known_hosts`);
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-05-19T17:47:13Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR adds support for linux amd64 (and arm64) by updating the dependency installation and SSH configuration logic.  
- Implements support for both x64 and arm64 architectures on Linux.  
- Refactors SSH configuration and known_hosts setup, with updated error handling and logging.  
- Updates tests to validate behavior for different architectures and error conditions.

### Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

| File              | Description                                                                |
| ----------------- | -------------------------------------------------------------------------- |
| src/index.ts      | Refactored Linux dependency installation, SSH setup, and session launching. |
| src/index.test.ts | Added tests for linux x64, linux arm64, and unsupported architectures.     |




state: COMMENTED
author_association: NONE
submitted_at: 2025-05-19T18:00:56Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

Adds support for selecting the correct Linux upterm binary based on CPU architecture and refactors several setup steps.

- Dynamic `uptermArch` switch for `x64` → `amd64` and `arm64` binaries, with error handling for unsupported arches
- Refactored SSH setup, wait-timeout parsing, socket-check loop, and test coverage for new cases

### Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

| File               | Description                                            |
| ------------------ | ------------------------------------------------------ |
| src/index.ts       | Architecture-based download, SSH config refactoring, enhanced timeout & socket logic |
| src/index.test.ts  | Renamed tests and added linux/arm64 and unsupported-arch cases |


<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**src/index.ts:3**
* The code calls `os.homedir()` but `os` is not imported. Add `import os from 'os';` at the top of the file.
```
import path from 'path';
```
**src/index.test.ts:40**
* [nitpick] Add an assertion in the linux x64 test to verify that the `execShellCommand` download URL is called with `upterm_linux_amd64.tar.gz`.
```
it('should handle the main loop for linux x64', async () => {
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-05-19T18:04:03Z

Review comments:
diff_hunk: @@ -76,77 +112,81 @@ export async function run() {
 
     let authorizedKeysParameter = '';
     for (const allowedUser of uniqueAllowedUsers) {
-      if (allowedUser) {
-        authorizedKeysParameter += `--github-user "${allowedUser}"`;
-      }
+      authorizedKeysParameter += `--github-user "${allowedUser}" `;
     }
 
+    // Upterm session
     const uptermServer = core.getInput('upterm-server');
     const waitTimeoutMinutes = core.getInput('wait-timeout-minutes');
     core.info(`Creating a new session. Connecting to upterm server ${uptermServer}`);
-    await execShellCommand(`tmux new -d -s upterm-wrapper -x 132 -y 43 "upterm host --accept --server '${uptermServer}' ${authorizedKeysParameter} --force-command 'tmux attach -t upterm' -- tmux new -s upterm -x 132 -y 43"`);
-    // resize terminal for largest client by default
+    await execShellCommand(
+      `tmux new -d -s upterm-wrapper -x 132 -y 43 "upterm host --accept --server '${uptermServer}' ${authorizedKeysParameter}--force-command 'tmux attach -t upterm' -- tmux new -s upterm -x 132 -y 43"`
path: src/index.ts
user: login: Copilot
body: There's no space between `${authorizedKeysParameter}` and `--force-command`, causing those flags to run together. Add a space before `--force-command`.
```suggestion
      `tmux new -d -s upterm-wrapper -x 132 -y 43 "upterm host --accept --server '${uptermServer}' ${authorizedKeysParameter} --force-command 'tmux attach -t upterm' -- tmux new -s upterm -x 132 -y 43"`
```
created_at: 2025-05-19T17:41:57Z
author_association: NONE
diff_hunk: @@ -16,58 +15,95 @@ export async function run() {
     }
 
     core.debug('Installing dependencies');
-    if (process.platform == 'linux') {
-      await execShellCommand(`curl -sL https://github.com/owenthereal/upterm/releases/latest/download/upterm_linux_amd64.tar.gz | tar zxvf - -C /tmp upterm && sudo install /tmp/upterm /usr/local/bin/`);
-      await execShellCommand('if ! command -v tmux &>/dev/null; then sudo apt-get update && sudo apt-get -y install tmux; fi');
+    if (process.platform === 'linux') {
+      let uptermArch: string;
+      switch (process.arch) {
+        case 'x64':
+          uptermArch = 'amd64';
+          break;
+        case 'arm64':
+          uptermArch = 'arm64';
+          break;
+        default:
+          core.error(`Unsupported architecture for upterm: ${process.arch}. Only x64 and arm64 are supported.`);
+          return;
+      }
+
+      await execShellCommand(
+        `curl -sL https://github.com/owenthereal/upterm/releases/latest/download/upterm_linux_${uptermArch}.tar.gz | tar zxvf - -C /tmp upterm && sudo install /tmp/upterm /usr/local/bin/`
+      );
+      await execShellCommand(
+        'if ! command -v tmux &>/dev/null; then sudo apt-get update && sudo apt-get -y install tmux; fi'
+      );
     } else {
       await execShellCommand('brew install owenthereal/upterm/upterm tmux');
     }
     core.debug('Installed dependencies successfully');
 
+    // SSH key setup
     const sshPath = path.join(os.homedir(), '.ssh');
-    if (!fs.existsSync(path.join(sshPath, 'id_rsa'))) {
+    const idRsaPath = path.join(sshPath, 'id_rsa');
+    if (!fs.existsSync(idRsaPath)) {
       core.debug('Generating SSH keys');
-      fs.mkdirSync(sshPath, {recursive: true});
+      fs.mkdirSync(sshPath, { recursive: true });
       try {
-        await execShellCommand(`ssh-keygen -q -t rsa -N "" -f ~/.ssh/id_rsa; ssh-keygen -q -t ed25519 -N "" -f ~/.ssh/id_ed25519`);
+        await execShellCommand(
+          `ssh-keygen -q -t rsa -N "" -f ~/.ssh/id_rsa; ssh-keygen -q -t ed25519 -N "" -f ~/.ssh/id_ed25519`
+        );
       } catch (error) {
-        core.error(`error running ssh-keygen. Error: ${error}`);
+        core.error(`Error running ssh-keygen: ${error}`);
         throw error;
       }
       core.debug('Generated SSH keys successfully');
     } else {
       core.debug('SSH key already exists');
     }
 
+    // SSH config
     core.debug('Configuring ssh client');
-    fs.appendFileSync(path.join(sshPath, 'config'), 'Host *\nStrictHostKeyChecking no\nCheckHostIP no\n' + 'TCPKeepAlive yes\nServerAliveInterval 30\nServerAliveCountMax 180\nVerifyHostKeyDNS yes\nUpdateHostKeys yes\n');
-    // entry in known hosts file in mandatory in upterm. attempt ssh connection to upterm server
-    // to get the host key added to ~/.ssh/known_hosts
-    if (core.getInput('ssh-known-hosts') && core.getInput('ssh-known-hosts') !== '') {
+    const sshConfig = `
path: src/index.ts
user: login: Copilot
body: [nitpick] The `sshConfig` template literal starts with a leading newline, which will insert a blank line in the SSH config file. You may want to trim the leading `\n` or build the string without the initial newline.
created_at: 2025-05-19T17:41:57Z
author_association: NONE
diff_hunk: @@ -76,74 +103,69 @@ export async function run() {
 
     let authorizedKeysParameter = '';
     for (const allowedUser of uniqueAllowedUsers) {
-      if (allowedUser) {
-        authorizedKeysParameter += `--github-user "${allowedUser}"`;
-      }
+      authorizedKeysParameter += `--github-user "${allowedUser}" `;
     }
 
+    // Upterm session
     const uptermServer = core.getInput('upterm-server');
     const waitTimeoutMinutes = core.getInput('wait-timeout-minutes');
     core.info(`Creating a new session. Connecting to upterm server ${uptermServer}`);
     await execShellCommand(`tmux new -d -s upterm-wrapper -x 132 -y 43 "upterm host --accept --server '${uptermServer}' ${authorizedKeysParameter} --force-command 'tmux attach -t upterm' -- tmux new -s upterm -x 132 -y 43"`);
-    // resize terminal for largest client by default
+    // Resize terminal for largest client by default
     await execShellCommand('tmux set -t upterm-wrapper window-size largest; tmux set -t upterm window-size largest');
-    console.debug('Created new session successfully');
-    if (waitTimeoutMinutes !== '') {
-      let timeout;
-      try {
-        timeout = parseInt(waitTimeoutMinutes);
-      } catch (error) {
-        core.error(`wait-timeout-minutes must be set to an integer. Error: ${error}`);
-        throw error;
+    core.debug('Created new session successfully');
+
+    // Wait timeout logic
+    if (waitTimeoutMinutes) {
path: src/index.ts
user: login: Copilot
body: [nitpick] Consider explicitly checking for a non-empty string (e.g., waitTimeoutMinutes !== '') rather than a truthy value to properly handle inputs like "0".
```suggestion
    if (waitTimeoutMinutes !== '') {
```
created_at: 2025-05-19T17:47:13Z
author_association: NONE
diff_hunk: @@ -16,58 +15,86 @@ export async function run() {
     }
 
     core.debug('Installing dependencies');
-    if (process.platform == 'linux') {
-      await execShellCommand(`curl -sL https://github.com/owenthereal/upterm/releases/latest/download/upterm_linux_amd64.tar.gz | tar zxvf - -C /tmp upterm && sudo install /tmp/upterm /usr/local/bin/`);
+    if (process.platform === 'linux') {
+      let uptermArch: string;
+      switch (process.arch) {
+        case 'x64':
+          uptermArch = 'amd64';
+          break;
+        case 'arm64':
+          uptermArch = 'arm64';
+          break;
+        default:
+          core.error(`Unsupported architecture for upterm: ${process.arch}. Only x64 and arm64 are supported.`);
+          return;
+      }
+
+      await execShellCommand(`curl -sL https://github.com/owenthereal/upterm/releases/latest/download/upterm_linux_${uptermArch}.tar.gz | tar zxvf - -C /tmp upterm && sudo install /tmp/upterm /usr/local/bin/`);
       await execShellCommand('if ! command -v tmux &>/dev/null; then sudo apt-get update && sudo apt-get -y install tmux; fi');
     } else {
       await execShellCommand('brew install owenthereal/upterm/upterm tmux');
     }
     core.debug('Installed dependencies successfully');
 
+    // SSH key setup
     const sshPath = path.join(os.homedir(), '.ssh');
-    if (!fs.existsSync(path.join(sshPath, 'id_rsa'))) {
+    const idRsaPath = path.join(sshPath, 'id_rsa');
+    if (!fs.existsSync(idRsaPath)) {
       core.debug('Generating SSH keys');
       fs.mkdirSync(sshPath, {recursive: true});
       try {
         await execShellCommand(`ssh-keygen -q -t rsa -N "" -f ~/.ssh/id_rsa; ssh-keygen -q -t ed25519 -N "" -f ~/.ssh/id_ed25519`);
       } catch (error) {
-        core.error(`error running ssh-keygen. Error: ${error}`);
+        core.error(`Error running ssh-keygen: ${error}`);
         throw error;
       }
       core.debug('Generated SSH keys successfully');
     } else {
       core.debug('SSH key already exists');
     }
 
+    // SSH config
     core.debug('Configuring ssh client');
-    fs.appendFileSync(path.join(sshPath, 'config'), 'Host *\nStrictHostKeyChecking no\nCheckHostIP no\n' + 'TCPKeepAlive yes\nServerAliveInterval 30\nServerAliveCountMax 180\nVerifyHostKeyDNS yes\nUpdateHostKeys yes\n');
-    // entry in known hosts file in mandatory in upterm. attempt ssh connection to upterm server
-    // to get the host key added to ~/.ssh/known_hosts
-    if (core.getInput('ssh-known-hosts') && core.getInput('ssh-known-hosts') !== '') {
+    const sshConfig = `Host *
+  StrictHostKeyChecking no
+  CheckHostIP no
+  TCPKeepAlive yes
+  ServerAliveInterval 30
+  ServerAliveCountMax 180
+  VerifyHostKeyDNS yes
+  UpdateHostKeys yes
+`;
+    fs.appendFileSync(path.join(sshPath, 'config'), sshConfig);
+
+    // known_hosts setup
+    const knownHostsPath = path.join(sshPath, 'known_hosts');
+    const sshKnownHosts = core.getInput('ssh-known-hosts');
+    if (sshKnownHosts && sshKnownHosts !== '') {
       core.info('Appending ssh-known-hosts to ~/.ssh/known_hosts. Contents of ~/.ssh/known_hosts:');
-      fs.appendFileSync(path.join(sshPath, 'known_hosts'), core.getInput('ssh-known-hosts'));
+      fs.appendFileSync(knownHostsPath, sshKnownHosts);
       core.info(await execShellCommand('cat ~/.ssh/known_hosts'));
     } else {
       core.info('Auto-generating ~/.ssh/known_hosts by attempting connection to uptermd.upterm.dev');
       try {
         await execShellCommand('ssh-keyscan uptermd.upterm.dev 2> /dev/null >> ~/.ssh/known_hosts');
       } catch (error) {
-        core.error(`error running ssh-keyscan. Error: ${error}`);
+        core.error(`Error running ssh-keyscan: ${error}`);
         throw error;
       }
-      // @cert-authority entry is the mandatory entry. generate the entry based on the known_hosts entry key
+      // Add @cert-authority entry
       try {
-        await execShellCommand('cat <(cat ~/.ssh/known_hosts | awk \'{ print "@cert-authority * " $2 " " $3 }\') >> ~/.ssh/known_hosts');
+        await execShellCommand(`cat <(cat ~/.ssh/known_hosts | awk '{ print "@cert-authority * " $2 " " $3 }') >> ~/.ssh/known_hosts`);
path: src/index.ts
user: login: Copilot
body: Consider refactoring the process substitution command to a more portable alternative, as this syntax may not be supported in shells other than Bash.
```suggestion
        await execShellCommand(`awk '{ print "@cert-authority * " $2 " " $3 }' ~/.ssh/known_hosts > /tmp/known_hosts_cert_authority`);
        await execShellCommand(`cat /tmp/known_hosts_cert_authority >> ~/.ssh/known_hosts`);
        await execShellCommand(`rm -f /tmp/known_hosts_cert_authority`);
```
created_at: 2025-05-19T18:00:56Z
author_association: NONE
diff_hunk: @@ -76,74 +103,69 @@ export async function run() {
 
     let authorizedKeysParameter = '';
     for (const allowedUser of uniqueAllowedUsers) {
-      if (allowedUser) {
-        authorizedKeysParameter += `--github-user "${allowedUser}"`;
-      }
+      authorizedKeysParameter += `--github-user "${allowedUser}" `;
     }
 
+    // Upterm session
     const uptermServer = core.getInput('upterm-server');
     const waitTimeoutMinutes = core.getInput('wait-timeout-minutes');
     core.info(`Creating a new session. Connecting to upterm server ${uptermServer}`);
     await execShellCommand(`tmux new -d -s upterm-wrapper -x 132 -y 43 "upterm host --accept --server '${uptermServer}' ${authorizedKeysParameter} --force-command 'tmux attach -t upterm' -- tmux new -s upterm -x 132 -y 43"`);
-    // resize terminal for largest client by default
+    // Resize terminal for largest client by default
     await execShellCommand('tmux set -t upterm-wrapper window-size largest; tmux set -t upterm window-size largest');
-    console.debug('Created new session successfully');
+    core.debug('Created new session successfully');
+
+    // Wait timeout logic
     if (waitTimeoutMinutes !== '') {
-      let timeout;
-      try {
-        timeout = parseInt(waitTimeoutMinutes);
-      } catch (error) {
-        core.error(`wait-timeout-minutes must be set to an integer. Error: ${error}`);
-        throw error;
+      const timeout = parseInt(waitTimeoutMinutes, 10);
+      if (isNaN(timeout)) {
+        core.error(`wait-timeout-minutes must be set to an integer.`);
+        throw new Error('Invalid wait-timeout-minutes value');
       }
       await execShellCommand(`( sleep $(( ${timeout} * 60 )); if ! pgrep -f '^tmux attach ' &>/dev/null; then tmux kill-server; fi ) & disown`);
       core.info(`wait-timeout-minutes set - will wait for ${waitTimeoutMinutes} minutes for someone to connect, otherwise shut down`);
     }
 
+    // Wait for upterm socket to be ready
     let tries = 10;
-    while (true) {
-      if (tries <= 0) {
-        throw new Error('Failed to start upterm');
-      }
-
+    while (tries-- > 0) {
       core.info('Waiting for upterm to be ready...');
-      if (uptermSocketExists()) {
-        break;
-      }
-
+      if (uptermSocketExists()) break;
       await sleep(1000);
-      tries--;
+    }
+    if (!uptermSocketExists()) {
+      throw new Error('Failed to start upterm');
     }
 
-    console.debug('Entering main loop');
+    core.debug('Entering main loop');
+    // Main loop: wait for /continue file or upterm exit
     /*eslint no-constant-condition: ["error", { "checkLoops": false }]*/
     while (true) {
       if (continueFileExists()) {
         core.info("Exiting debugging session because '/continue' file was created");
         break;
       }
-
       if (!uptermSocketExists()) {
-        core.info("Exiting debugging session 'upterm' quit");
+        core.info("Exiting debugging session: 'upterm' quit");
         break;
       }
-
       try {
         core.info(await execShellCommand('upterm session current --admin-socket ~/.upterm/*.sock'));
       } catch (error) {
-        core.error(`error getting upterm session. Error: ${error}`);
+        core.error(`Error getting upterm session: ${error}`);
         throw error;
       }
-
       await sleep(5000);
     }
-  } catch (error) {
-    core.setFailed(error.message);
+  } catch (error: any) {
+    core.setFailed(error.message || String(error));
   }
 }
 
 function uptermSocketExists(): boolean {
-  return fs.readdirSync(path.join(os.homedir(), '.upterm')).filter(file => file.endsWith('.sock')).length > 0;
+  const uptermDir = path.join(os.homedir(), '.upterm');
path: src/index.ts
user: login: Copilot
body: [nitpick] If os.homedir() is used multiple times within this function, consider caching its value to improve readability and reduce redundant calls.
```suggestion
  const homeDir = os.homedir();
  const uptermDir = path.join(homeDir, '.upterm');
```
created_at: 2025-05-19T18:00:56Z
author_association: NONE

PR comments:
user: login: Defelo
created_at: 2025-05-19T19:26:31Z
body: Hey @owenthereal, thanks for adding support for `aarch64-linux`! Unfortunately I couldn't get this to work on the `ubuntu-24.04-arm` runner ([logs](https://github.com/Defelo/nixpkgs-review-gha/actions/runs/15121401236/job/42504383279#step:5:15)). Also for some reason I had to manually change the version from `v1` to `v1.2.0`, otherwise it would just use the old version.
author_association: NONE
user: login: owenthereal
created_at: 2025-05-19T21:07:52Z
body: @Defelo Would you mind turnning on github actions debug by adding the followings to the upterm steps:

```yaml
 env:
          ACTIONS_RUNNER_DEBUG: true
          ACTIONS_STEP_DEBUG: true
```

Are you using a custom image? It's possible that `tmux` is not installed.

>  Also for some reason I had to manually change the version from v1 to v1.2.0, otherwise it would just use the old version.

I fixed this by updating `v1` tag. Thanks for reporting.

author_association: OWNER
user: login: owenthereal
created_at: 2025-05-19T22:21:17Z
body: >thanks for adding support for aarch64-linux! Unfortunately I couldn't get this to work on the ubuntu-24.04-arm runner ([logs](https://github.com/Defelo/nixpkgs-review-gha/actions/runs/15121401236/job/42504383279#step:5:15)).

I figured out the issue. I forgot to run the build with `npm run build` to compile typescript...https://github.com/owenthereal/action-upterm/commit/57dc63cab27f7891d9ce116ee4bce34a2fe041a3. Now it should be fixed. Please try and let me know if you there are more issues
author_association: OWNER
user: login: Defelo
created_at: 2025-05-20T12:00:57Z
body: Great, now it's working on `v1`/`v1.2.0`, thank you! Regarding the releases, I think if you delete the `v1` tag and instead create a `v1.0.0` tag (for the commit `v1` was pointing to initially) then GitHub should always choose the latest matching version automatically if someone only specifies `v1` or `v1.x` instead of the full version.
author_association: NONE