Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# PR / main: linux-x64 Payload + pack pointer + ghx.linux-x64. Full RID matrix is publish.yml.
# PR / main: linux-x64 Payload + pack pointer + gh.linux-x64. Full RID matrix is publish.yml.

name: build
on:
Expand Down Expand Up @@ -59,23 +59,23 @@ jobs:
- name: 🧪 test
run: dnx --yes retest -- --no-build

- name: 📦 ghx
- name: 📦 gh
run: |
$ErrorActionPreference = 'Stop'
dotnet pack src/ghx/ghx.csproj -c $env:Configuration -p:GeneratePackageOnBuild=false -bl:pointer-pack-ghx.binlog
if ($LASTEXITCODE -ne 0) { throw "ghx pointer pack failed with exit code $LASTEXITCODE." }
dotnet pack src/ghx/ghx.csproj -c $env:Configuration -r linux-x64 -p:GeneratePackageOnBuild=false -bl:pack-ghx-linux-x64.binlog
if ($LASTEXITCODE -ne 0) { throw "ghx linux-x64 pack failed with exit code $LASTEXITCODE." }
dotnet pack src/gh/gh.csproj -c $env:Configuration -p:GeneratePackageOnBuild=false -bl:pointer-pack-gh.binlog
if ($LASTEXITCODE -ne 0) { throw "gh pointer pack failed with exit code $LASTEXITCODE." }
dotnet pack src/gh/gh.csproj -c $env:Configuration -r linux-x64 -p:GeneratePackageOnBuild=false -bl:pack-gh-linux-x64.binlog
if ($LASTEXITCODE -ne 0) { throw "gh linux-x64 pack failed with exit code $LASTEXITCODE." }

- name: ✅ unix execute bits
run: |
$ErrorActionPreference = 'Stop'
$script = Join-Path $pwd 'src/GitHub.Cli/unix-exec.ps1'
$cli = Get-ChildItem bin -Filter "GitHub.Cli.linux-x64.*.nupkg" | Where-Object Name -notlike '*.symbols.*' | Select-Object -First 1
$ghx = Get-ChildItem bin -Filter "ghx.linux-x64.*.nupkg" | Where-Object Name -notlike '*.symbols.*' | Select-Object -First 1
$tool = Get-ChildItem bin -Filter "gh.linux-x64.*.nupkg" | Where-Object Name -notlike '*.symbols.*' | Select-Object -First 1
if (-not $cli) { throw "GitHub.Cli.linux-x64 nupkg not found" }
if (-not $ghx) { throw "ghx.linux-x64 nupkg not found" }
foreach ($pkg in @($cli, $ghx)) {
if (-not $tool) { throw "gh.linux-x64 nupkg not found" }
foreach ($pkg in @($cli, $tool)) {
& pwsh -NoProfile -File $script -Nupkg $pkg.FullName -Assert
if ($LASTEXITCODE -ne 0) { throw "unix execute bits missing in $($pkg.Name)" }
}
Expand Down
50 changes: 25 additions & 25 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,21 +56,21 @@ jobs:
if ($LASTEXITCODE -ne 0) { throw "GitHub.Cli pointer pack failed with exit code $LASTEXITCODE." }
dotnet pack src/GitHub.Cli/GitHub.Cli.csproj -c $env:Configuration -r $rid $version -p:GeneratePackageOnBuild=false -bl:"pack-$rid.binlog"
if ($LASTEXITCODE -ne 0) { throw "GitHub.Cli $rid pack failed with exit code $LASTEXITCODE." }
dotnet pack src/ghx/ghx.csproj -c $env:Configuration $version -p:GeneratePackageOnBuild=false -bl:pointer-pack-ghx.binlog
if ($LASTEXITCODE -ne 0) { throw "ghx pointer pack failed with exit code $LASTEXITCODE." }
dotnet pack src/ghx/ghx.csproj -c $env:Configuration -r $rid $version -p:GeneratePackageOnBuild=false -bl:"pack-ghx-$rid.binlog"
if ($LASTEXITCODE -ne 0) { throw "ghx $rid pack failed with exit code $LASTEXITCODE." }
dotnet pack src/gh/gh.csproj -c $env:Configuration $version -p:GeneratePackageOnBuild=false -bl:pointer-pack-gh.binlog
if ($LASTEXITCODE -ne 0) { throw "gh pointer pack failed with exit code $LASTEXITCODE." }
dotnet pack src/gh/gh.csproj -c $env:Configuration -r $rid $version -p:GeneratePackageOnBuild=false -bl:"pack-gh-$rid.binlog"
if ($LASTEXITCODE -ne 0) { throw "gh $rid pack failed with exit code $LASTEXITCODE." }

- name: ✅ unix execute bits
if: matrix.rid != 'win-x64'
run: |
$ErrorActionPreference = 'Stop'
$script = Join-Path $pwd 'src/GitHub.Cli/unix-exec.ps1'
$cli = Get-ChildItem bin -Filter "GitHub.Cli.${{ matrix.rid }}.*.nupkg" | Where-Object Name -notlike '*.symbols.*' | Select-Object -First 1
$ghx = Get-ChildItem bin -Filter "ghx.${{ matrix.rid }}.*.nupkg" | Where-Object Name -notlike '*.symbols.*' | Select-Object -First 1
$tool = Get-ChildItem bin -Filter "gh.${{ matrix.rid }}.*.nupkg" | Where-Object Name -notlike '*.symbols.*' | Select-Object -First 1
if (-not $cli) { throw "GitHub.Cli.${{ matrix.rid }} nupkg not found" }
if (-not $ghx) { throw "ghx.${{ matrix.rid }} nupkg not found" }
foreach ($pkg in @($cli, $ghx)) {
if (-not $tool) { throw "gh.${{ matrix.rid }} nupkg not found" }
foreach ($pkg in @($cli, $tool)) {
& pwsh -NoProfile -File $script -Nupkg $pkg.FullName -Assert
if ($LASTEXITCODE -ne 0) { throw "unix execute bits missing in $($pkg.Name)" }
}
Expand All @@ -83,7 +83,7 @@ jobs:
# into one GitHub.Cli.{version}.nupkg corrupts the zip (NUGET 400 / sleet EOCD).
path: |
bin/GitHub.Cli.${{ matrix.rid }}.*.nupkg
bin/ghx.${{ matrix.rid }}.*.nupkg
bin/gh.${{ matrix.rid }}.*.nupkg
retention-days: 30
if-no-files-found: error

Expand All @@ -94,34 +94,34 @@ jobs:
name: package-pointer
path: |
bin/GitHub.Cli.${{ env.Version }}.nupkg
bin/ghx.${{ env.Version }}.nupkg
bin/gh.${{ env.Version }}.nupkg
retention-days: 30
if-no-files-found: error

- name: 🧪 ghx --version
- name: 🧪 dotnet-gh --version
run: |
$ErrorActionPreference = 'Stop'
$nupkg = Get-ChildItem bin -Filter "ghx.${{ matrix.rid }}.*.nupkg" | Where-Object Name -notlike '*.symbols.*' | Select-Object -First 1
if (-not $nupkg) { throw "ghx.${{ matrix.rid }} nupkg not found" }
$extract = Join-Path $env:RUNNER_TEMP "ghx-${{ matrix.rid }}"
$nupkg = Get-ChildItem bin -Filter "gh.${{ matrix.rid }}.*.nupkg" | Where-Object Name -notlike '*.symbols.*' | Select-Object -First 1
if (-not $nupkg) { throw "gh.${{ matrix.rid }} nupkg not found" }
$extract = Join-Path $env:RUNNER_TEMP "gh-${{ matrix.rid }}"
$script = Join-Path $pwd 'src/GitHub.Cli/unix-exec.ps1'
& pwsh -NoProfile -File $script -Nupkg $nupkg.FullName -Destination $extract
if ($LASTEXITCODE -ne 0) { throw "nupkg extract failed for $($nupkg.Name)" }
$ghx = Get-ChildItem $extract -Recurse -File | Where-Object { $_.Name -eq 'ghx' -or $_.Name -eq 'ghx.exe' } | Select-Object -First 1
if (-not $ghx) { throw "ghx executable not found in $($nupkg.Name)" }
$tool = Get-ChildItem $extract -Recurse -File | Where-Object { $_.Name -eq 'dotnet-gh' -or $_.Name -eq 'dotnet-gh.exe' } | Select-Object -First 1
if (-not $tool) { throw "dotnet-gh executable not found in $($nupkg.Name)" }
if (-not $IsWindows) {
$gh = Get-ChildItem $extract -Recurse -File | Where-Object { $_.FullName.Replace('\', '/') -like '*/gh/bin/gh' } | Select-Object -First 1
if (-not $gh) { throw "gh/bin/gh not found in $($nupkg.Name)" }
$exec = [System.IO.UnixFileMode]::UserExecute
if (([System.IO.File]::GetUnixFileMode($ghx.FullName) -band $exec) -eq 0) {
throw "ghx is not executable after extract: $($ghx.FullName)"
if (([System.IO.File]::GetUnixFileMode($tool.FullName) -band $exec) -eq 0) {
throw "dotnet-gh is not executable after extract: $($tool.FullName)"
}
if (([System.IO.File]::GetUnixFileMode($gh.FullName) -band $exec) -eq 0) {
throw "gh/bin/gh is not executable after extract: $($gh.FullName)"
}
}
& $ghx.FullName --version
if ($LASTEXITCODE -ne 0) { throw "ghx --version failed with exit code $LASTEXITCODE." }
& $tool.FullName --version
if ($LASTEXITCODE -ne 0) { throw "dotnet-gh --version failed with exit code $LASTEXITCODE." }

- name: 🐛 logs
uses: actions/upload-artifact@v4
Expand Down Expand Up @@ -163,12 +163,12 @@ jobs:
"GitHub.Cli.linux-arm64.$env:Version.nupkg",
"GitHub.Cli.osx-x64.$env:Version.nupkg",
"GitHub.Cli.osx-arm64.$env:Version.nupkg",
"ghx.$env:Version.nupkg",
"ghx.win-x64.$env:Version.nupkg",
"ghx.linux-x64.$env:Version.nupkg",
"ghx.linux-arm64.$env:Version.nupkg",
"ghx.osx-x64.$env:Version.nupkg",
"ghx.osx-arm64.$env:Version.nupkg"
"gh.$env:Version.nupkg",
"gh.win-x64.$env:Version.nupkg",
"gh.linux-x64.$env:Version.nupkg",
"gh.linux-arm64.$env:Version.nupkg",
"gh.osx-x64.$env:Version.nupkg",
"gh.osx-arm64.$env:Version.nupkg"
)) {
if ($names -notcontains $need) { throw "Missing $need" }
}
Expand Down
10 changes: 5 additions & 5 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# GitHub.Cli

NuGet pointer and per-RID payloads that put a working GitHub CLI next to a .NET app or tool. Humans run that payload through **ghx**.
NuGet pointer and per-RID payloads that put a working GitHub CLI next to a .NET app or tool. Humans run that payload through the **gh** tool package.

## Language

Expand All @@ -20,12 +20,12 @@ _Avoid_: native files, native/, binaries, sidecar
The managed type in namespace `GitHub` whose `ResolvePath` returns the Payload's `gh` executable (`gh.exe` on Windows). Callers write `GitHub.Cli.ResolvePath()`.
_Avoid_: Gh, Ghx, GitHubCli, ResolveBinaryPath

**ghx**:
The passthrough .NET tool that execs the Payload `gh` with the same arguments. Primary human vehicle via `dnx`/`ndnx ghx`.
_Avoid_: GitHub CLI, gh, wrapper with its own GitHub verbs
**gh** (tool package):
The passthrough .NET tool. Package id `gh` so `dnx`/`ndnx gh`. `ToolCommandName` is `dotnet-gh`, so a locally/globally installed tool is `dotnet gh` and does not steal the `gh` command from GitHub's native CLI.
_Avoid_: installing a command named `gh`; wrapping with its own GitHub verbs

**Execute**:
ghx replacing itself with the Payload `gh`. Every argument is forwarded. The only exception is a lone `--version`, which prints ghx and `gh`.
The tool replacing itself with the Payload `gh`. Every argument is forwarded. The only exception is a lone `--version`, which prints the wrapper version line (`gh {version}`) then payload `gh --version`.
_Avoid_: wrap, shell out and wait as the product metaphor (implementation may still spawn)

**Upstream**:
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0002-nupkg-version-is-upstream.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Nupkg version is Upstream

`GitHub.Cli` and `ghx` use the GitHub CLI version (`2.98.0`) so `ndnx ghx@2.98.0` is that CLI. The pin is `github-cli.version`. CI dogfood stays `42.42.*`. Packaging-only republishes of the same CLI use a SemVer label, not a different major.
`GitHub.Cli` and the `gh` tool use the GitHub CLI version (`2.98.0`) so `ndnx gh@2.98.0` is that CLI. The pin is `github-cli.version`. CI dogfood stays `42.42.*`. Packaging-only republishes of the same CLI use a SemVer label, not a different major.
2 changes: 1 addition & 1 deletion ghx.slnx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<Solution>
<Project Path="src/GitHub.Cli/GitHub.Cli.csproj" />
<Project Path="src/ghx/ghx.csproj" />
<Project Path="src/gh/gh.csproj" />
<Project Path="src/Tests/Tests.csproj" />
</Solution>
12 changes: 7 additions & 5 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# ghx

[![Version](https://img.shields.io/nuget/vpre/ghx.svg?color=royalblue)](https://www.nuget.org/packages/ghx)
[![Downloads](https://img.shields.io/nuget/dt/ghx.svg?color=darkmagenta)](https://www.nuget.org/packages/ghx)
[![Version](https://img.shields.io/nuget/vpre/gh.svg?color=royalblue)](https://www.nuget.org/packages/gh)
[![Downloads](https://img.shields.io/nuget/dt/gh.svg?color=darkmagenta)](https://www.nuget.org/packages/gh)
[![EULA](https://img.shields.io/badge/EULA-OSMF-blue?labelColor=black&color=C9FF30)](https://github.com/devlooped/oss/blob/main/osmfeula.txt)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/devlooped/oss/blob/main/license.txt)

Expand All @@ -21,11 +21,13 @@ OSMF tier. A single fee covers all of [Devlooped packages](https://www.nuget.org
## Usage

```bash
ndnx ghx -- repo list
dnx ghx -- repo list
ghx --version # ghx version, then gh --version
ndnx gh -- repo list
dnx gh -- repo list
dotnet tool install gh && dotnet gh --version
```

`dnx`/`ndnx` use the package id `gh`. An installed tool is `dotnet gh` (`ToolCommandName` `dotnet-gh`) so it does not replace GitHub's native `gh` on PATH.

`PackageReference` `GitHub.Cli` and pack/publish for your RID to copy `gh/` next to the app. `GitHub.Cli.ResolvePath()` is the Payload `gh` executable.

<!-- #content -->
Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@

<!--
dotnet pack writes 0644 for every regular file. Unix RID nupkgs must carry
+x on gh and the ghx host or restore/unzip cannot exec them.
+x on gh and the dotnet-gh host or restore/unzip cannot exec them.
-->
<Target Name="StampUnixExecuteBitsOnNupkg"
AfterTargets="Pack"
Expand Down
2 changes: 1 addition & 1 deletion src/GitHub.Cli/unix-exec.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ function Test-UnixExecuteEntry([string] $Name) {
}
$slash = $n.LastIndexOf('/')
$leaf = if ($slash -lt 0) { $n } else { $n.Substring($slash + 1) }
if ($leaf -eq 'ghx') {
if ($leaf -eq 'dotnet-gh') {
return $true
}
if ($n -eq 'gh/bin/gh' -or $n.EndsWith('/gh/bin/gh')) {
Expand Down
33 changes: 17 additions & 16 deletions src/Tests/GhxTests.cs → src/Tests/NghTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,63 +2,64 @@

namespace Tests;

public class GhxTests
public class NghTests
{
[Fact]
public void Lone_version_prints_ghx_and_gh()
public void Lone_version_prints_gh_wrapper_and_payload()
{
var ghx = FindGhx();
if (ghx is null)
var tool = FindDotnetGh();
if (tool is null)
return;

var pin = File.ReadAllText(Path.Combine(FindRepoRoot(), "github-cli.version")).Trim();
var (exit, stdout, stderr) = Run(ghx, "--version");
var (exit, stdout, stderr) = Run(tool, "--version");
Assert.True(exit == 0, stderr);
Assert.Contains("ghx ", stdout, StringComparison.Ordinal);
var first = stdout.Split(["\r\n", "\n"], StringSplitOptions.None)[0];
Assert.StartsWith("gh ", first, StringComparison.Ordinal);
Assert.Contains("gh version", stdout, StringComparison.OrdinalIgnoreCase);
Assert.Contains(pin, stdout, StringComparison.Ordinal);
}

[Fact]
public void Other_args_are_passthrough()
{
var ghx = FindGhx();
if (ghx is null)
var tool = FindDotnetGh();
if (tool is null)
return;

var (exit, stdout, stderr) = Run(ghx, "version");
var (exit, stdout, stderr) = Run(tool, "version");
Assert.True(exit == 0, stderr);
Assert.DoesNotContain("ghx ", stdout, StringComparison.Ordinal);
Assert.StartsWith("gh version", stdout.Trim(), StringComparison.OrdinalIgnoreCase);
var pin = File.ReadAllText(Path.Combine(FindRepoRoot(), "github-cli.version")).Trim();
Assert.Contains(pin, stdout, StringComparison.Ordinal);
}

static (int Exit, string Stdout, string Stderr) Run(string ghx, params string[] args)
static (int Exit, string Stdout, string Stderr) Run(string tool, params string[] args)
{
var start = new ProcessStartInfo(ghx)
var start = new ProcessStartInfo(tool)
{
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
WorkingDirectory = Path.GetDirectoryName(ghx),
WorkingDirectory = Path.GetDirectoryName(tool),
};
foreach (var arg in args)
{
start.ArgumentList.Add(arg);
}

using var process = Process.Start(start)
?? throw new InvalidOperationException("Failed to start ghx.");
?? throw new InvalidOperationException("Failed to start dotnet-gh.");
var stdout = process.StandardOutput.ReadToEnd();
var stderr = process.StandardError.ReadToEnd();
Assert.True(process.WaitForExit(60_000));
return (process.ExitCode, stdout, stderr);
}

static string? FindGhx()
static string? FindDotnetGh()
{
var dir = AppContext.BaseDirectory;
var name = OperatingSystem.IsWindows() ? "ghx.exe" : "ghx";
var name = OperatingSystem.IsWindows() ? "dotnet-gh.exe" : "dotnet-gh";
var path = Path.Combine(dir, name);
return File.Exists(path) ? path : null;
}
Expand Down
Loading
Loading