Fallout-build / Fallout-build/Fallout
Fallout.Ssh — managed SSH/SFTP wrapper with ssh-config integration and typed PosixMode
- Dominant language
- C#
- Stars
- 154
- Forks
- 19
- Avg merge
- 1d 22h
- Merged PRs (30d)
- 15
Description
## Motivation
Same dogfood context as #249. SSH-over-Renci.SshNet is the most common deploy transport: connect, upload files, run a command, surface exit status. We wrote ~300 LOC of `SshConnectionResolver` + `SshDeployer` + `RemoteFileUpload` to wrap it. Three things stood out as Fallout-shaped:
### 1. SSH-config alias as a first-class input
`SshConnectionResolver` shells out to `ssh -G ` and reads `~/.ssh/config` for hostname/port/user/identity. Expressing the deploy target as an alias rather than a tuple means the same `Build.cs` works across dev box, CI, and a fresh laptop without env-var sprawl.
It also inherits OpenSSH's config story for free — `ProxyCommand`, bastion hops, key management, `ServerAliveInterval` — none of which needs Fallout APIs.
### 2. Typed `PosixMode` instead of `short`
`Renci.SshNet.SftpClient.ChangePermissions(string path, short mode)` reads the decimal digits as an octal triple (`644` → `0o644`). We hit it twice: once when a draft double-converted the value to literal `420` and produced `-r---w----` on remote files, again in the post-mortem.
It only manifests on **redeploy**: the first run silently ships wrong modes; the second hits `SftpPermissionDeniedException` because the file lacks owner-write. Classic CD trap (see #251 for the broader idempotency pattern).
A typed wrapper makes it unconstructable:
```csharp
public readonly struct PosixMode {
public static PosixMode FromOctal(int octalLiteral); // PosixMode.FromOctal(0o644)
public short ToRenciDigitEncoded(); // internal
public override string ToString() => $"0{Octal:o}"; // "0644"
}
```
Caller writes `PosixMode.FromOctal(0o644)`. Can't be misused.
### 3. Self-healing remote write
`UploadFile(canOverride: true)` still raises `SftpPermissionDeniedException` when the existing file lacks owner-write — `canOverride` means "OK to truncate", not "ignore perms". Idiomatic recovery: `chmod 0644 ` before each upload, swallow `SftpPathNotFoundException` for first-run. Added in [ErpForFactoryGames@b1d117d](https://github.com/ChrisonSimtian/ErpForFactoryGames/commit/b1d117d). If Fallout owns the SFTP wrapper, this lives in one place.
## Proposed API sketch
```csharp
namespace Fallout.Ssh;
public sealed record SshTarget(string Alias);
public sealed class SshConnection : IDisposable {
public static SshConnection Open(SshTarget target, IAnsiConsole? console = null);
public void UploadFile(string remotePath, byte[] content, PosixMode mode);
public RemoteCommandResult Run(string command, TimeSpan? timeout = null);
}
public readonly struct PosixMode { /* see above */ }
public sealed record RemoteCommandResult(string Command, int ExitStatus, string StdOut, string StdErr);
```
`UploadFile` is idempotent by construction (pre-chmod → SFTP write → final chmod). Callers can't reintroduce the wedge-on-redeploy footgun.
## Open questions
- **Host-key verification.** Today we TOFU — accept any key on first connect, log the SHA256. CI/CD wants strict known_hosts with an explicit "trust this fingerprint" affordance: e.g. `--trust-host-key=`, mirroring OpenSSH `StrictHostKeyChecking accept-new`.
- **Windows interop.** Memory note `project_windows_pwsh_ssh_hang` records an unresolved Windows-only hang in the old PS-driven path. Renci managed code should be immune, but validate on Windows runners in this package's CI matrix.
- **Path quoting.** `EnsureDir` issues `mkdir -p ''` over SSH exec (single-quote-wrapped, `'\''` escape). Should the API ever pass user-controlled paths to remote exec? If yes, needs a typed `RemotePath` with an opinionated quoter; if no, document that paths come from config only.
## Related
- #113 (CD deployment agent/runner RFC) — proposes a coordinator-driven model. `Fallout.Ssh` is the alternative for users who don't want a long-lived agent: direct push from CI/local with managed SSH. Both coexist — agent for firewalled targets, direct SSH for everyone else.
- #249 (`Fallout.Reconcile`) — composes naturally: reconcile a remote-file resource via `SshConnection.UploadFile`.
## Reference implementation (to be replaced)
[`ErpForFactoryGames/src/Deploy/Erp.Deploy/Ssh/`](https://github.com/ChrisonSimtian/ErpForFactoryGames/tree/main/src/Deploy/Erp.Deploy/Ssh)
Contributor guide
Assessment
This issue has not been assessed yet.