@smbcloud / cli / commits / d5c477a

Add .NET tool packaging and NuGet release workflow for smb CLI

- Add nuget/smbcloud-cli .NET tool launcher and packaging - Add release-nuget.yml workflow for cross-platform native builds and NuGet publish - Update badges in README files to include NuGet - Document .NET tool usage and installation

keypair34 committed May 2, 2026 at 19:02 UTC d5c477ad4cf9c44e6cc98cf28fb4972b499e8d7d
9 files changed +518
.github/workflows/ci.yml
+17
@@ -7,6 +7,7 @@ name: Tests
7 # - sdk/python → maturin wheel build (smbcloud-sdk-auth)
8 # - sdk/npm → wasm-pack + prepare-package.mjs (@smbcloud/sdk-auth)
9 # - sdk/gems → rb_sys native-extension compile (smbcloud-auth gem)
10 +# - nuget → .NET tool launcher build (SmbCloud.Cli)
11
12 on: push
13
@@ -170,6 +171,22 @@ jobs:
171 working-directory: sdk/npm/smbcloud-auth
172 run: node ./prepare-package.mjs
173
174 + # ── .NET tool ───────────────────────────────────────────────────────────────
175 + check-nuget-tool:
176 + name: NuGet — .NET tool launcher (SmbCloud.Cli)
177 + runs-on: ubuntu-latest
178 + steps:
179 + - name: Checkout
180 + uses: actions/checkout@v6
181 +
182 + - name: Setup .NET SDK
183 + uses: actions/setup-dotnet@v4
184 + with:
185 + dotnet-version: 8.0.x
186 +
187 + - name: Build .NET tool launcher
188 + run: dotnet build nuget/smbcloud-cli/SmbCloud.Cli.csproj --configuration Release
189 +
190 # ── Ruby Gem ────────────────────────────────────────────────────────────────
191 check-ruby-gem:
192 name: SDK — Ruby gem (smbcloud-auth)
.github/workflows/release-nuget.yml new
+293
@@ -0,0 +1,293 @@
1 +name: NuGet CLI Release
2 +
3 +on:
4 + push:
5 + tags:
6 + - "v*.*.*"
7 + workflow_dispatch:
8 + inputs:
9 + tag:
10 + description: "Release tag (e.g. v0.3.38)"
11 + required: true
12 +
13 +permissions:
14 + contents: read
15 +
16 +env:
17 + CARGO_TERM_COLOR: always
18 + CLI_CLIENT_SECRET: ${{ secrets.CLI_CLIENT_SECRET }}
19 + DOTNET_VERSION: 8.0.x
20 + NUGET_PACKAGE_ID: SmbCloud.Cli
21 +
22 +jobs:
23 + build-native-binaries:
24 + name: Build native binary (${{ matrix.build.NAME }})
25 + runs-on: ${{ matrix.build.OS }}
26 + strategy:
27 + fail-fast: false
28 + matrix:
29 + build:
30 + - {
31 + NAME: linux-x64,
32 + OS: ubuntu-latest,
33 + TARGET: x86_64-unknown-linux-gnu,
34 + }
35 + - {
36 + NAME: linux-arm64,
37 + OS: ubuntu-24.04-arm,
38 + TARGET: aarch64-unknown-linux-gnu,
39 + }
40 + - {
41 + NAME: windows-x64,
42 + OS: windows-2022,
43 + TARGET: x86_64-pc-windows-msvc,
44 + }
45 + - {
46 + NAME: windows-arm64,
47 + OS: windows-2022,
48 + TARGET: aarch64-pc-windows-msvc,
49 + }
50 + - {
51 + NAME: darwin-x64,
52 + OS: macos-15-intel,
53 + TARGET: x86_64-apple-darwin,
54 + }
55 + - {
56 + NAME: darwin-arm64,
57 + OS: macos-latest,
58 + TARGET: aarch64-apple-darwin,
59 + }
60 + steps:
61 + - name: Checkout
62 + uses: actions/checkout@v6
63 + with:
64 + ref: ${{ github.event.inputs.tag || github.ref }}
65 +
66 + - name: Set the release version
67 + shell: bash
68 + run: |
69 + if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
70 + release_version="${GITHUB_REF_NAME#v}"
71 + else
72 + release_version="${{ github.event.inputs.tag }}"
73 + release_version="${release_version#v}"
74 + fi
75 +
76 + echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
77 +
78 + - name: Read Rust toolchain
79 + shell: bash
80 + run: |
81 + rust_toolchain="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml | head -n 1)"
82 + if [ -z "$rust_toolchain" ]; then
83 + echo "Failed to read Rust toolchain from rust-toolchain.toml" >&2
84 + exit 1
85 + fi
86 +
87 + echo "RUST_TOOLCHAIN=${rust_toolchain}" >> "$GITHUB_ENV"
88 +
89 + - name: Install toolkit
90 + uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
91 + with:
92 + toolchain: ${{ env.RUST_TOOLCHAIN }}
93 + targets: ${{ matrix.build.TARGET }}
94 +
95 + - name: Setup Rust cache
96 + uses: Swatinem/rust-cache@v2
97 + with:
98 + key: nuget-${{ matrix.build.TARGET }}
99 +
100 + - name: Build
101 + env:
102 + GH_OAUTH_CLIENT_ID: ${{ secrets.GH_OAUTH_CLIENT_ID }}
103 + GH_OAUTH_CLIENT_SECRET: ${{ secrets.GH_OAUTH_CLIENT_SECRET }}
104 + GH_OAUTH_REDIRECT_URI: ${{ secrets.GH_OAUTH_REDIRECT_URI }}
105 + shell: bash
106 + run: cargo build --release --locked --target ${{ matrix.build.TARGET }} --package smbcloud-cli
107 +
108 + - name: Prepare artifact
109 + shell: bash
110 + run: |
111 + executable_name="smb"
112 + if [[ "${{ matrix.build.OS }}" == "windows-2022" ]]; then
113 + executable_name="smb.exe"
114 + fi
115 +
116 + mkdir -p "nuget-artifacts/native/${{ matrix.build.NAME }}"
117 + cp "target/${{ matrix.build.TARGET }}/release/${executable_name}" "nuget-artifacts/native/${{ matrix.build.NAME }}/${executable_name}"
118 +
119 + - name: Upload native artifact
120 + uses: actions/upload-artifact@v7
121 + with:
122 + name: nuget-native-${{ matrix.build.NAME }}
123 + path: nuget-artifacts/native
124 +
125 + pack-nuget:
126 + name: Pack NuGet .NET tool
127 + runs-on: ubuntu-latest
128 + needs: build-native-binaries
129 + steps:
130 + - name: Checkout
131 + uses: actions/checkout@v6
132 + with:
133 + ref: ${{ github.event.inputs.tag || github.ref }}
134 +
135 + - name: Set the release version
136 + shell: bash
137 + run: |
138 + if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
139 + release_version="${GITHUB_REF_NAME#v}"
140 + else
141 + release_version="${{ github.event.inputs.tag }}"
142 + release_version="${release_version#v}"
143 + fi
144 +
145 + echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
146 +
147 + - name: Download native artifacts
148 + uses: actions/download-artifact@v7
149 + with:
150 + pattern: nuget-native-*
151 + path: nuget/smbcloud-cli
152 + merge-multiple: true
153 +
154 + - name: Setup .NET SDK
155 + uses: actions/setup-dotnet@v4
156 + with:
157 + dotnet-version: ${{ env.DOTNET_VERSION }}
158 +
159 + - name: Pack .NET tool
160 + shell: bash
161 + run: |
162 + dotnet pack nuget/smbcloud-cli/SmbCloud.Cli.csproj \
163 + --configuration Release \
164 + --output nuget/dist \
165 + -p:PackageVersion=${RELEASE_VERSION}
166 +
167 + - name: Upload package artifact
168 + uses: actions/upload-artifact@v7
169 + with:
170 + name: nuget-package
171 + path: nuget/dist/*.nupkg
172 +
173 + smoke-test:
174 + name: Smoke test (${{ matrix.os }})
175 + runs-on: ${{ matrix.os }}
176 + needs: pack-nuget
177 + strategy:
178 + fail-fast: false
179 + matrix:
180 + os:
181 + - ubuntu-latest
182 + - windows-2022
183 + - macos-latest
184 + steps:
185 + - name: Checkout
186 + uses: actions/checkout@v6
187 + with:
188 + ref: ${{ github.event.inputs.tag || github.ref }}
189 +
190 + - name: Set the release version
191 + shell: bash
192 + run: |
193 + if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
194 + release_version="${GITHUB_REF_NAME#v}"
195 + else
196 + release_version="${{ github.event.inputs.tag }}"
197 + release_version="${release_version#v}"
198 + fi
199 +
200 + echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
201 +
202 + - name: Download package artifact
203 + uses: actions/download-artifact@v7
204 + with:
205 + name: nuget-package
206 + path: nuget/dist
207 +
208 + - name: Setup .NET SDK
209 + uses: actions/setup-dotnet@v4
210 + with:
211 + dotnet-version: ${{ env.DOTNET_VERSION }}
212 +
213 + - name: Install tool from local package
214 + shell: pwsh
215 + run: |
216 + $packageSource = (Resolve-Path "nuget/dist").Path
217 + @"
218 + <?xml version="1.0" encoding="utf-8"?>
219 + <configuration>
220 + <packageSources>
221 + <clear />
222 + <add key="local" value="$packageSource" />
223 + </packageSources>
224 + </configuration>
225 + "@ | Set-Content -Path "nuget/NuGet.Config"
226 +
227 + dotnet tool install `
228 + --tool-path ".tool" `
229 + --configfile "nuget/NuGet.Config" `
230 + --version "$env:RELEASE_VERSION" `
231 + "$env:NUGET_PACKAGE_ID"
232 +
233 + - name: Run smb --version
234 + shell: pwsh
235 + run: |
236 + $toolPath = if ($IsWindows) { ".\.tool\smb.exe" } else { "./.tool/smb" }
237 + & $toolPath --version
238 +
239 + publish-nuget:
240 + name: Publish to NuGet
241 + runs-on: ubuntu-latest
242 + needs: smoke-test
243 + steps:
244 + - name: Checkout
245 + uses: actions/checkout@v6
246 + with:
247 + ref: ${{ github.event.inputs.tag || github.ref }}
248 +
249 + - name: Set the release version
250 + shell: bash
251 + run: |
252 + if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
253 + release_version="${GITHUB_REF_NAME#v}"
254 + else
255 + release_version="${{ github.event.inputs.tag }}"
256 + release_version="${release_version#v}"
257 + fi
258 +
259 + echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
260 +
261 + - name: Download package artifact
262 + uses: actions/download-artifact@v7
263 + with:
264 + name: nuget-package
265 + path: nuget/dist
266 +
267 + - name: Setup .NET SDK
268 + uses: actions/setup-dotnet@v4
269 + with:
270 + dotnet-version: ${{ env.DOTNET_VERSION }}
271 +
272 + - name: Check whether release already exists on NuGet
273 + id: nuget-check
274 + shell: bash
275 + run: |
276 + package_index_url="https://api.nuget.org/v3-flatcontainer/smbcloud.cli/index.json"
277 + if curl -fsS "${package_index_url}" | grep -F "\"${RELEASE_VERSION}\"" >/dev/null; then
278 + echo "${NUGET_PACKAGE_ID} ${RELEASE_VERSION} already exists on NuGet, skipping publish"
279 + echo "exists=true" >> "$GITHUB_OUTPUT"
280 + else
281 + echo "exists=false" >> "$GITHUB_OUTPUT"
282 + fi
283 +
284 + - name: Publish package
285 + if: steps.nuget-check.outputs.exists != 'true'
286 + env:
287 + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
288 + shell: bash
289 + run: |
290 + dotnet nuget push nuget/dist/*.nupkg \
291 + --api-key "${NUGET_API_KEY}" \
292 + --source https://api.nuget.org/v3/index.json \
293 + --skip-duplicate
README.MD
+7
@@ -19,6 +19,7 @@
19 <p align="center">
20 <a href="https://crates.io/crates/smbcloud-cli"><img alt="Crates.io" src="https://img.shields.io/crates/v/smbcloud-cli"></a>
21 <a href="https://www.npmjs.com/package/@smbcloud/cli"><img alt="npm" src="https://img.shields.io/npm/v/@smbcloud/cli"></a>
22 + <a href="https://www.nuget.org/packages/SmbCloud.Cli"><img alt="NuGet" src="https://img.shields.io/nuget/v/SmbCloud.Cli"></a>
23 <a href="https://github.com/smbcloudXYZ/homebrew-tap"><img alt="Homebrew" src="https://img.shields.io/badge/homebrew-tap-orange?logo=homebrew"></a>
24 <a href="https://pypi.org/project/smbcloud-cli/"><img alt="PyPI" src="https://img.shields.io/pypi/v/smbcloud-cli"></a>
25 <a href="https://github.com/smbcloudXYZ/smbcloud-cli/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/smbcloudXYZ/smbcloud-cli"></a>
@@ -48,6 +49,12 @@ brew install cli
49 npm install -g @smbcloud/cli
50 ```
51
52 +### .NET tool
53 +
54 +```sh
55 +dotnet tool install --global SmbCloud.Cli
56 +```
57 +
58 ### pip
59
60 ```sh
npm/smbcloud-cli/README.md
+7
@@ -18,6 +18,7 @@
18 </p>
19 <p align="center">
20 <a href="https://www.npmjs.com/package/@smbcloud/cli"><img alt="npm" src="https://img.shields.io/npm/v/@smbcloud/cli"></a>
21 + <a href="https://www.nuget.org/packages/SmbCloud.Cli"><img alt="NuGet" src="https://img.shields.io/nuget/v/SmbCloud.Cli"></a>
22 <a href="https://pypi.org/project/smbcloud-cli/"><img alt="PyPI" src="https://img.shields.io/pypi/v/smbcloud-cli"></a>
23 <a href="https://github.com/smbcloudXYZ/smbcloud-cli/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/smbcloudXYZ/smbcloud-cli"></a>
24 </p>
@@ -56,6 +57,12 @@ brew tap smbcloudXYZ/tap
57 brew install cli
58 ```
59
60 +### .NET tool
61 +
62 +```sh
63 +dotnet tool install --global SmbCloud.Cli
64 +```
65 +
66 ### pip
67
68 ```sh
nuget/.gitignore new
+4
@@ -0,0 +1,4 @@
1 +dist/
2 +smbcloud-cli/bin/
3 +smbcloud-cli/obj/
4 +smbcloud-cli/native/
nuget/smbcloud-cli/Program.cs new
+109
@@ -0,0 +1,109 @@
1 +using System.ComponentModel;
2 +using System.Diagnostics;
3 +using System.Runtime.InteropServices;
4 +
5 +internal static class Program
6 +{
7 + private const string CommandName = "smb";
8 +
9 + public static async Task<int> Main(string[] arguments)
10 + {
11 + try
12 + {
13 + string executablePath = ResolveExecutablePath();
14 + EnsureExecutablePermissions(executablePath);
15 + return await RunAsync(executablePath, arguments);
16 + }
17 + catch (Exception exception) when (
18 + exception is FileNotFoundException or
19 + PlatformNotSupportedException or
20 + Win32Exception)
21 + {
22 + Console.Error.WriteLine($"{CommandName}: {exception.Message}");
23 + return 1;
24 + }
25 + }
26 +
27 + private static string ResolveExecutablePath()
28 + {
29 + string runtimeIdentifier = GetRuntimeIdentifier();
30 + string executableName = OperatingSystem.IsWindows() ? $"{CommandName}.exe" : CommandName;
31 + string executablePath = Path.GetFullPath(
32 + Path.Combine(AppContext.BaseDirectory, "native", runtimeIdentifier, executableName));
33 +
34 + if (!File.Exists(executablePath))
35 + {
36 + throw new FileNotFoundException(
37 + $"The native {CommandName} executable for '{runtimeIdentifier}' is not bundled in this package.",
38 + executablePath);
39 + }
40 +
41 + return executablePath;
42 + }
43 +
44 + private static string GetRuntimeIdentifier()
45 + {
46 + string operatingSystem = OperatingSystem.IsWindows()
47 + ? "windows"
48 + : OperatingSystem.IsMacOS()
49 + ? "darwin"
50 + : OperatingSystem.IsLinux()
51 + ? "linux"
52 + : throw new PlatformNotSupportedException(
53 + $"{CommandName} does not support this operating system through the .NET tool package.");
54 +
55 + string architecture = RuntimeInformation.ProcessArchitecture switch
56 + {
57 + Architecture.X64 => "x64",
58 + Architecture.Arm64 => "arm64",
59 + _ => throw new PlatformNotSupportedException(
60 + $"{CommandName} does not support the '{RuntimeInformation.ProcessArchitecture}' architecture through the .NET tool package."),
61 + };
62 +
63 + return $"{operatingSystem}-{architecture}";
64 + }
65 +
66 + private static void EnsureExecutablePermissions(string executablePath)
67 + {
68 + if (OperatingSystem.IsWindows())
69 + {
70 + return;
71 + }
72 +
73 + UnixFileMode currentMode = File.GetUnixFileMode(executablePath);
74 + UnixFileMode requiredMode = UnixFileMode.UserRead |
75 + UnixFileMode.UserWrite |
76 + UnixFileMode.UserExecute |
77 + UnixFileMode.GroupRead |
78 + UnixFileMode.GroupExecute |
79 + UnixFileMode.OtherRead |
80 + UnixFileMode.OtherExecute;
81 +
82 + if ((currentMode & requiredMode) == requiredMode)
83 + {
84 + return;
85 + }
86 +
87 + File.SetUnixFileMode(executablePath, currentMode | requiredMode);
88 + }
89 +
90 + private static async Task<int> RunAsync(string executablePath, IReadOnlyList<string> arguments)
91 + {
92 + ProcessStartInfo startInfo = new(executablePath)
93 + {
94 + UseShellExecute = false,
95 + WorkingDirectory = Environment.CurrentDirectory,
96 + };
97 +
98 + foreach (string argument in arguments)
99 + {
100 + startInfo.ArgumentList.Add(argument);
101 + }
102 +
103 + using Process process = Process.Start(startInfo)
104 + ?? throw new Win32Exception($"Failed to start '{CommandName}'.");
105 +
106 + await process.WaitForExitAsync();
107 + return process.ExitCode;
108 + }
109 +}
nuget/smbcloud-cli/README.md new
+48
@@ -0,0 +1,48 @@
1 +# smbCloud CLI for .NET
2 +
3 +`smb` is the official command-line interface for [smbCloud](https://smbcloud.xyz/).
4 +
5 +## Install
6 +
7 +```sh
8 +dotnet tool install --global SmbCloud.Cli
9 +```
10 +
11 +## Update
12 +
13 +```sh
14 +dotnet tool update --global SmbCloud.Cli
15 +```
16 +
17 +## Quick start
18 +
19 +```sh
20 +smb login
21 +smb init
22 +smb deploy
23 +```
24 +
25 +## Platform support
26 +
27 +This .NET tool bundles native `smb` binaries for:
28 +
29 +- macOS `arm64`, `x64`
30 +- Linux `arm64`, `x64` (glibc)
31 +- Windows `arm64`, `x64`
32 +
33 +## Other installation methods
34 +
35 +- npm: `npm install -g @smbcloud/cli`
36 +- pip: `pip install smbcloud-cli`
37 +- Homebrew: `brew tap smbcloudXYZ/tap && brew install cli`
38 +- GitHub Releases: <https://github.com/smbcloudXYZ/smbcloud-cli/releases>
39 +
40 +## Source
41 +
42 +- Repository: <https://github.com/smbcloudXYZ/smbcloud-cli>
43 +- Documentation: <https://smbcloud.xyz/posts>
44 +- Issues: <https://github.com/smbcloudXYZ/smbcloud-cli/issues>
45 +
46 +## License
47 +
48 +Apache-2.0
nuget/smbcloud-cli/SmbCloud.Cli.csproj new
+26
@@ -0,0 +1,26 @@
1 +<Project Sdk="Microsoft.NET.Sdk">
2 + <PropertyGroup>
3 + <OutputType>Exe</OutputType>
4 + <TargetFramework>net8.0</TargetFramework>
5 + <ImplicitUsings>enable</ImplicitUsings>
6 + <Nullable>enable</Nullable>
7 + <PackAsTool>true</PackAsTool>
8 + <ToolCommandName>smb</ToolCommandName>
9 + <PackageId>SmbCloud.Cli</PackageId>
10 + <PackageVersion>0.0.0-local</PackageVersion>
11 + <Version>$(PackageVersion)</Version>
12 + <Authors>Seto Elkahfi</Authors>
13 + <Description>smbCloud command line interface.</Description>
14 + <PackageReadmeFile>README.md</PackageReadmeFile>
15 + <PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
16 + <PackageProjectUrl>https://smbcloud.xyz/</PackageProjectUrl>
17 + <RepositoryUrl>https://github.com/smbcloudXYZ/smbcloud-cli</RepositoryUrl>
18 + <PackageTags>cli;smbcloud;cloud;deployment;developer-tools;dotnet-tool</PackageTags>
19 + <PublishRepositoryUrl>true</PublishRepositoryUrl>
20 + </PropertyGroup>
21 +
22 + <ItemGroup>
23 + <None Include="README.md" Pack="true" PackagePath="" />
24 + <None Include="native/**/*" Pack="true" PackagePath="tools/$(TargetFramework)/any/native/%(RecursiveDir)%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" Visible="false" />
25 + </ItemGroup>
26 +</Project>
pypi/README.md
+7
@@ -19,6 +19,7 @@
19 <p align="center">
20 <a href="https://crates.io/crates/smbcloud-cli"><img alt="Crates.io" src="https://img.shields.io/crates/v/smbcloud-cli"></a>
21 <a href="https://www.npmjs.com/package/@smbcloud/cli"><img alt="npm" src="https://img.shields.io/npm/v/@smbcloud/cli"></a>
22 + <a href="https://www.nuget.org/packages/SmbCloud.Cli"><img alt="NuGet" src="https://img.shields.io/nuget/v/SmbCloud.Cli"></a>
23 <a href="https://github.com/smbcloudXYZ/homebrew-tap"><img alt="Homebrew" src="https://img.shields.io/badge/homebrew-tap-orange?logo=homebrew"></a>
24 <a href="https://pypi.org/project/smbcloud-cli/"><img alt="PyPI" src="https://img.shields.io/pypi/v/smbcloud-cli"></a>
25 <a href="https://github.com/smbcloudXYZ/smbcloud-cli/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/smbcloudXYZ/smbcloud-cli"></a>
@@ -64,6 +65,12 @@ brew install cli
65 npm install -g @smbcloud/cli
66 ```
67
68 +### .NET tool
69 +
70 +```sh
71 +dotnet tool install --global SmbCloud.Cli
72 +```
73 +
74 ### Shell (macOS / Linux)
75
76 ```sh