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
162 changes: 162 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,34 @@ jobs:
cache-from: type=registry,ref=ghcr.io/${{ needs.build-base.outputs.repo_name }}:${{ matrix.variant }},mode=max
cache-to: type=inline

# Publish .NET Tool to NuGet
publish-nuget:
name: Publish NuGet
needs: [test-cli-integration, test-shell-functions, test-airlock]
runs-on: ubuntu-24.04
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Setup .NET
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
with:
global-json-file: global.json

- name: Extract version from shell script
id: version
run: |
VERSION=$(grep -m1 "^# Version:" copilot_here.sh | sed 's/# Version: //')
echo "version=$VERSION" >> $GITHUB_OUTPUT

- name: Pack as .NET tool
run: dotnet pack app/CopilotHere.csproj -c Release -p:PackAsTool=true -p:CopilotHereVersion=${{ steps.version.outputs.version }} --nologo

- name: Push to NuGet
if: ${{ secrets.NUGET_API_KEY != '' }}
run: dotnet nuget push "app/bin/Release/*.nupkg" --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate

# Summary job
publish-summary:
name: Publish Summary
Expand All @@ -570,6 +598,7 @@ jobs:
build-variants,
build-compound-variants,
release-cli,
publish-nuget,
]
if: always() && (needs.build-base.result != 'skipped' || needs.release-cli.result != 'skipped')
runs-on: ubuntu-24.04
Expand Down Expand Up @@ -612,6 +641,15 @@ jobs:
echo "- linux-x64, linux-arm64" >> $GITHUB_STEP_SUMMARY
echo "- osx-x64, osx-arm64" >> $GITHUB_STEP_SUMMARY
echo "- win-x64, win-arm64" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Package Managers:**" >> $GITHUB_STEP_SUMMARY
echo "- Homebrew tap updated" >> $GITHUB_STEP_SUMMARY
echo "- WinGet manifest submitted" >> $GITHUB_STEP_SUMMARY
Comment on lines +645 to +647
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow summary currently prints "Homebrew tap updated" and "WinGet manifest submitted" whenever release-cli succeeds, but both updates are conditional on secrets and may be skipped. This can produce misleading summaries; consider checking the relevant secrets or step outcomes (or exporting an output from those steps) before claiming they ran.

Suggested change
echo "**Package Managers:**" >> $GITHUB_STEP_SUMMARY
echo "- Homebrew tap updated" >> $GITHUB_STEP_SUMMARY
echo "- WinGet manifest submitted" >> $GITHUB_STEP_SUMMARY
echo "**Package Managers (conditional):**" >> $GITHUB_STEP_SUMMARY
echo "- Homebrew tap update (runs only when configured and enabled)" >> $GITHUB_STEP_SUMMARY
echo "- WinGet manifest update (runs only when configured and enabled)" >> $GITHUB_STEP_SUMMARY

Copilot uses AI. Check for mistakes.
fi

if [[ "${{ needs.publish-nuget.result }}" == "success" ]]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "**.NET Tool:** Published to nuget.org" >> $GITHUB_STEP_SUMMARY
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

publish-summary reports ".NET Tool: Published to nuget.org" when the publish-nuget job succeeds, but the actual push step is skipped if NUGET_API_KEY is not set. Since the job still succeeds in that case, the summary can incorrectly claim publication; consider gating the message on the presence of the secret and/or on a push-step output indicating a publish actually happened.

Suggested change
echo "**.NET Tool:** Published to nuget.org" >> $GITHUB_STEP_SUMMARY
echo "**.NET Tool:** NuGet publish job completed (push may be skipped if NUGET_API_KEY is not set)" >> $GITHUB_STEP_SUMMARY

Copilot uses AI. Check for mistakes.
fi

# Build native CLI binaries for all platforms
Expand Down Expand Up @@ -687,6 +725,11 @@ jobs:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Setup .NET
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
with:
global-json-file: global.json

- name: Download all artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
Expand Down Expand Up @@ -789,3 +832,122 @@ jobs:
draft: false
prerelease: false
make_latest: true

# --- Homebrew tap update ---
- name: Compute SHA256 for Homebrew
id: sha256
run: |
echo "osx_arm64=$(sha256sum release/copilot_here-osx-arm64.tar.gz | awk '{print $1}')" >> $GITHUB_OUTPUT
echo "osx_x64=$(sha256sum release/copilot_here-osx-x64.tar.gz | awk '{print $1}')" >> $GITHUB_OUTPUT
echo "linux_arm64=$(sha256sum release/copilot_here-linux-arm64.tar.gz | awk '{print $1}')" >> $GITHUB_OUTPUT
echo "linux_x64=$(sha256sum release/copilot_here-linux-x64.tar.gz | awk '{print $1}')" >> $GITHUB_OUTPUT

- name: Update Homebrew formula
if: ${{ secrets.HOMEBREW_TAP_DEPLOY_KEY != '' }}
env:
HOMEBREW_TAP_DEPLOY_KEY: ${{ secrets.HOMEBREW_TAP_DEPLOY_KEY }}
run: |
TAG="cli-v${{ steps.version.outputs.version }}-${{ steps.version.outputs.short_sha }}"
VERSION="${{ steps.version.outputs.version }}"

# Configure SSH for deploy key
mkdir -p ~/.ssh
echo "$HOMEBREW_TAP_DEPLOY_KEY" > ~/.ssh/homebrew_tap_key
chmod 600 ~/.ssh/homebrew_tap_key
ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null
export GIT_SSH_COMMAND="ssh -i ~/.ssh/homebrew_tap_key -o StrictHostKeyChecking=no"
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Homebrew tap update disables SSH host key checking (StrictHostKeyChecking=no) even though known_hosts is populated. This weakens protection against MITM and is avoidable here; prefer relying on a pinned host key in known_hosts (or at least remove StrictHostKeyChecking=no) so the deploy key cannot be used against an unexpected host.

Suggested change
export GIT_SSH_COMMAND="ssh -i ~/.ssh/homebrew_tap_key -o StrictHostKeyChecking=no"
export GIT_SSH_COMMAND="ssh -i ~/.ssh/homebrew_tap_key"

Copilot uses AI. Check for mistakes.

# Clone the tap repo
git clone git@github.com:GordonBeeming/homebrew-tap.git /tmp/homebrew-tap

Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tap update assumes /tmp/homebrew-tap/Formula/ already exists. If the tap repo is newly created or doesn't have that directory, the cat > /tmp/homebrew-tap/Formula/copilot_here.rb step will fail. Consider adding a mkdir -p /tmp/homebrew-tap/Formula before writing the file to make the workflow robust.

Suggested change
# Ensure Formula directory exists
mkdir -p /tmp/homebrew-tap/Formula

Copilot uses AI. Check for mistakes.
# Generate the formula
cat > /tmp/homebrew-tap/Formula/copilot_here.rb << FORMULA_EOF
# typed: false
# frozen_string_literal: true

class CopilotHere < Formula
desc "Run GitHub Copilot CLI in a sandboxed Docker container"
homepage "https://github.com/GordonBeeming/copilot_here"
version "$VERSION"
license "FSL-1.1-MIT"

on_macos do
if Hardware::CPU.arm?
url "https://github.com/GordonBeeming/copilot_here/releases/download/$TAG/copilot_here-osx-arm64.tar.gz"
sha256 "${{ steps.sha256.outputs.osx_arm64 }}"
else
url "https://github.com/GordonBeeming/copilot_here/releases/download/$TAG/copilot_here-osx-x64.tar.gz"
sha256 "${{ steps.sha256.outputs.osx_x64 }}"
end
end

on_linux do
if Hardware::CPU.arm?
url "https://github.com/GordonBeeming/copilot_here/releases/download/$TAG/copilot_here-linux-arm64.tar.gz"
sha256 "${{ steps.sha256.outputs.linux_arm64 }}"
else
url "https://github.com/GordonBeeming/copilot_here/releases/download/$TAG/copilot_here-linux-x64.tar.gz"
sha256 "${{ steps.sha256.outputs.linux_x64 }}"
end
end

depends_on "docker" => :recommended

def install
bin.install "copilot_here"
end

def caveats
<<~EOS
To enable the shell function wrapper, run:
copilot_here --install-shells

Or manually source the shell script in your profile:
Bash/Zsh: source "\$(brew --prefix)/share/copilot_here/copilot_here.sh"
EOS
end

test do
assert_match version.to_s, shell_output("\#{bin}/copilot_here --version")
end
end
FORMULA_EOF

# Remove leading whitespace from heredoc
sed -i 's/^ //' /tmp/homebrew-tap/Formula/copilot_here.rb

# Commit and push
cd /tmp/homebrew-tap
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Formula/copilot_here.rb
git commit -m "Update copilot_here to $VERSION"
git push
Comment on lines +924 to +925
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Homebrew tap commit/push will fail the workflow when there are no changes (e.g., rerunning the workflow for the same version/tag), because git commit exits non-zero on a clean index. Consider checking git diff --quiet (and skipping commit/push) or allowing a no-op commit step so reruns don’t break publishing.

Suggested change
git commit -m "Update copilot_here to $VERSION"
git push
if git diff --cached --quiet; then
echo "No changes to commit in Homebrew tap; skipping commit and push."
else
git commit -m "Update copilot_here to $VERSION"
git push
fi

Copilot uses AI. Check for mistakes.

# Clean up
rm -f ~/.ssh/homebrew_tap_key

# --- WinGet manifest update ---
- name: Compute SHA256 for WinGet
id: winget_sha256
run: |
echo "win_x64=$(sha256sum release/copilot_here-win-x64.zip | awk '{print $1}')" >> $GITHUB_OUTPUT
echo "win_arm64=$(sha256sum release/copilot_here-win-arm64.zip | awk '{print $1}')" >> $GITHUB_OUTPUT

- name: Update WinGet manifest
if: ${{ secrets.WINGET_PAT != '' }}
env:
WINGET_PAT: ${{ secrets.WINGET_PAT }}
run: |
TAG="cli-v${{ steps.version.outputs.version }}-${{ steps.version.outputs.short_sha }}"
VERSION="${{ steps.version.outputs.version }}"
URL_X64="https://github.com/${{ github.repository }}/releases/download/$TAG/copilot_here-win-x64.zip"
URL_ARM64="https://github.com/${{ github.repository }}/releases/download/$TAG/copilot_here-win-arm64.zip"

# Install wingetcreate as .NET tool
dotnet tool install --global wingetcreate

wingetcreate update GordonBeeming.CopilotHere \
--version "$VERSION" \
--urls "$URL_X64" "$URL_ARM64" \
--submit --token "$WINGET_PAT"
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ FSL-1.1-MIT

## Notice

Copyright 2025 Gordon Beeming
Copyright 2026 Gordon Beeming

## Terms and Conditions

Expand Down
25 changes: 21 additions & 4 deletions app/CopilotHere.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,24 @@
<!-- Disable automatic +sha suffix, we add it with . instead -->
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>

<PublishAot>true</PublishAot>
<!-- .NET Tool packaging (enabled via -p:PackAsTool=true during dotnet pack) -->
<ToolCommandName>copilot_here</ToolCommandName>
<PackageId>copilot_here</PackageId>
<Authors>Gordon Beeming</Authors>
<Description>Run GitHub Copilot CLI in a sandboxed Docker container</Description>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageProjectUrl>https://github.com/GordonBeeming/copilot_here</PackageProjectUrl>
<RepositoryUrl>https://github.com/GordonBeeming/copilot_here</RepositoryUrl>
<PackageTags>copilot;docker;cli;github</PackageTags>
<PackageReadmeFile>README.md</PackageReadmeFile>

<!-- AOT properties are incompatible with PackAsTool; disable when packing as .NET tool -->
<PublishAot Condition="'$(PackAsTool)' != 'true'">true</PublishAot>

<InvariantGlobalization>true</InvariantGlobalization>
<TrimMode>full</TrimMode>
<StackTraceSupport>false</StackTraceSupport>
<OptimizationPreference>Size</OptimizationPreference>
<TrimMode Condition="'$(PackAsTool)' != 'true'">full</TrimMode>
<StackTraceSupport Condition="'$(PackAsTool)' != 'true'">false</StackTraceSupport>
<OptimizationPreference Condition="'$(PackAsTool)' != 'true'">Size</OptimizationPreference>
</PropertyGroup>

<!-- Add git SHA with . separator -->
Expand All @@ -37,6 +49,11 @@
<InternalsVisibleTo Include="CopilotHere.UnitTests" />
</ItemGroup>

<ItemGroup>
<None Include="..\README.md" Pack="true" PackagePath="\" />
<None Include="..\LICENSE" Pack="true" PackagePath="\" />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="Resources\docker-compose.airlock.yml.template" />
<EmbeddedResource Include="..\default-airlock-rules.json" Link="Resources\default-airlock-rules.json" />
Expand Down
87 changes: 87 additions & 0 deletions packaging/SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Package Manager Distribution Setup

This document describes the external setup steps required to enable automated package manager distribution.

## 1. NuGet (.NET Tool - Issue #50)

The CI workflow publishes the CLI as a .NET global tool to nuget.org.

### Setup steps

- [ ] Create/verify a nuget.org account at https://www.nuget.org/
- [ ] Generate an API key at https://www.nuget.org/account/apikeys
- Scope: **Push new packages and package versions**
- Glob pattern: `copilot_here`
- [ ] Add GitHub Actions secret on the `copilot_here` repo:
- Go to **Settings > Secrets and variables > Actions > New repository secret**
- Name: `NUGET_API_KEY`
- Value: the API key from nuget.org

### Usage

```bash
dotnet tool install -g copilot_here
copilot_here --version
```

---

## 2. Homebrew Tap (Issue #51)

The CI workflow updates a Homebrew formula in a separate tap repository after each release.

### Setup steps

- [ ] Create a public repository: [`GordonBeeming/homebrew-tap`](https://github.com/GordonBeeming/homebrew-tap)
- [ ] Copy the formula template from `packaging/homebrew/Formula/copilot_here.rb` to the new repo at `Formula/copilot_here.rb`
- [ ] Generate an SSH deploy key pair:
```bash
ssh-keygen -t ed25519 -C "homebrew-tap-deploy" -f homebrew_tap_key -N ""
```
- [ ] Add the **public** key (`homebrew_tap_key.pub`) as a deploy key on the `homebrew-tap` repo:
- Go to `homebrew-tap` repo **Settings > Deploy keys > Add deploy key**
- Title: `copilot_here CI`
- Check **Allow write access**
- [ ] Add the **private** key (`homebrew_tap_key`) as a GitHub Actions secret on the `copilot_here` repo:
- Name: `HOMEBREW_TAP_DEPLOY_KEY`
- Value: the full contents of the private key file

### Usage

```bash
brew tap gordonbeeming/tap
brew install copilot_here
copilot_here --version
```

---

## 3. WinGet (Issue #52)

The CI workflow auto-submits manifest updates to the `microsoft/winget-pkgs` repository using `wingetcreate`.

### Setup steps

- [ ] Generate a Personal Access Token (classic) at https://github.com/settings/tokens
- Scope: `public_repo` (needs to submit PRs to `microsoft/winget-pkgs`)
- [ ] Add GitHub Actions secret on the `copilot_here` repo:
- Name: `WINGET_PAT`
- Value: the PAT
- [ ] **First submission note:** After code changes are merged and a release is created, the CI will auto-submit the first PR to `microsoft/winget-pkgs`. This initial PR requires manual review/approval by Microsoft maintainers (typically takes 1-3 days). Subsequent version updates are auto-approved.

### Usage

```powershell
winget install GordonBeeming.CopilotHere
copilot_here --version
```

---

## Summary of required GitHub secrets

| Secret Name | Purpose | Where to get it |
|---|---|---|
| `NUGET_API_KEY` | Push .NET tool to nuget.org | https://www.nuget.org/account/apikeys |
| `HOMEBREW_TAP_DEPLOY_KEY` | Update Homebrew formula | SSH deploy key (write access) on `homebrew-tap` repo |
| `WINGET_PAT` | Submit WinGet manifest PRs | https://github.com/settings/tokens (scope: `public_repo`) |
49 changes: 49 additions & 0 deletions packaging/homebrew/Formula/copilot_here.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# typed: false
# frozen_string_literal: true

class CopilotHere < Formula
desc "Run GitHub Copilot CLI in a sandboxed Docker container"
homepage "https://github.com/GordonBeeming/copilot_here"
version "VERSION_PLACEHOLDER"
license "FSL-1.1-MIT"

on_macos do
if Hardware::CPU.arm?
url "https://github.com/GordonBeeming/copilot_here/releases/download/TAG_PLACEHOLDER/copilot_here-osx-arm64.tar.gz"
sha256 "SHA256_OSX_ARM64_PLACEHOLDER"
else
url "https://github.com/GordonBeeming/copilot_here/releases/download/TAG_PLACEHOLDER/copilot_here-osx-x64.tar.gz"
sha256 "SHA256_OSX_X64_PLACEHOLDER"
end
end

on_linux do
if Hardware::CPU.arm?
url "https://github.com/GordonBeeming/copilot_here/releases/download/TAG_PLACEHOLDER/copilot_here-linux-arm64.tar.gz"
sha256 "SHA256_LINUX_ARM64_PLACEHOLDER"
else
url "https://github.com/GordonBeeming/copilot_here/releases/download/TAG_PLACEHOLDER/copilot_here-linux-x64.tar.gz"
sha256 "SHA256_LINUX_X64_PLACEHOLDER"
end
end

depends_on "docker" => :recommended

def install
bin.install "copilot_here"
end

def caveats
<<~EOS
To enable the shell function wrapper, run:
copilot_here --install-shells

Or manually source the shell script in your profile:
Bash/Zsh: source "$(brew --prefix)/share/copilot_here/copilot_here.sh"
Comment on lines +38 to +42
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Homebrew formula caveats suggest sourcing a shell script from "$(brew --prefix)/share/copilot_here/copilot_here.sh", but this formula only installs the binary (no script is placed under share/). This instruction will fail for users; either install the shell script into the referenced path (e.g., under prefix/share) or remove the manual sourcing instruction and only recommend copilot_here --install-shells (which downloads the scripts itself).

Suggested change
To enable the shell function wrapper, run:
copilot_here --install-shells
Or manually source the shell script in your profile:
Bash/Zsh: source "$(brew --prefix)/share/copilot_here/copilot_here.sh"
To enable the shell function wrappers for your shell, run:
copilot_here --install-shells

Copilot uses AI. Check for mistakes.
EOS
end

test do
assert_match version.to_s, shell_output("#{bin}/copilot_here --version")
end
end
15 changes: 15 additions & 0 deletions packaging/winget/GordonBeeming.CopilotHere.installer.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
PackageIdentifier: GordonBeeming.CopilotHere
PackageVersion: 2026.02.19
InstallerType: zip
NestedInstallerType: portable
NestedInstallerFiles:
- RelativeFilePath: copilot_here.exe
Installers:
- Architecture: x64
InstallerUrl: https://github.com/GordonBeeming/copilot_here/releases/download/TAG_PLACEHOLDER/copilot_here-win-x64.zip
InstallerSha256: SHA256_WIN_X64_PLACEHOLDER
- Architecture: arm64
InstallerUrl: https://github.com/GordonBeeming/copilot_here/releases/download/TAG_PLACEHOLDER/copilot_here-win-arm64.zip
InstallerSha256: SHA256_WIN_ARM64_PLACEHOLDER
ManifestType: installer
ManifestVersion: 1.6.0
Loading