Skip to content

fix(ci): airlock smoke test runs on PRs and binds broker to IPAddress… #528

fix(ci): airlock smoke test runs on PRs and binds broker to IPAddress…

fix(ci): airlock smoke test runs on PRs and binds broker to IPAddress… #528

Workflow file for this run

# This workflow builds and publishes Docker images and native CLI binaries.

Check failure on line 1 in .github/workflows/publish.yml

View workflow run for this annotation

GitHub Actions / .github/workflows/publish.yml

Invalid workflow file

(Line: 579, Col: 3): 'integration-tests-airlock' is already defined
# Tests run first on all triggers. Publishing only happens on main branch.
name: Build and Publish
run-name: "Build #${{ github.run_number }} (${{ github.event_name }})"
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
schedule:
# Runs nightly at 3:15 AM UTC to check for package updates.
- cron: "15 3 * * *"
workflow_dispatch: # Allows the workflow to be run manually from the Actions tab
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
# Sets the minimum required permissions for the entire workflow for security.
permissions:
contents: write
packages: write
jobs:
# Run CLI tests on all platforms
test-cli:
name: Test CLI (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, macos-latest, macos-15-intel, windows-latest]
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: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Test
run: dotnet test --no-build --verbosity normal
# CLI integration tests on all platforms
test-cli-integration:
name: Integration Tests (${{ matrix.os }})
needs: [test-cli]
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, macos-latest, macos-15-intel, windows-latest]
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: Make test script executable (Unix)
if: runner.os != 'Windows'
run: chmod +x tests/integration/test_cli.sh
- name: Run CLI integration tests (Unix)
if: runner.os != 'Windows'
run: ./tests/integration/test_cli.sh
- name: Run CLI integration tests (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: ./tests/integration/test_cli.ps1
# Test shell function scripts can be sourced and call the native binary
test-shell-functions:
name: Shell Functions (${{ matrix.os }})
needs: [test-cli]
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, macos-latest, macos-15-intel, windows-latest]
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: Build CLI binary
run: dotnet publish app/CopilotHere.csproj -c Release -o publish/shell-test --nologo
- name: Test Bash script syntax
if: runner.os != 'Windows'
run: bash -n copilot_here.sh
- name: Test Bash source and copilot_here --help
if: runner.os != 'Windows'
shell: bash
run: |
export COPILOT_HERE_BIN="$PWD/publish/shell-test/copilot_here"
chmod +x "$COPILOT_HERE_BIN"
source ./copilot_here.sh
# Run --help and verify output contains expected text
OUTPUT=$(copilot_here --help 2>&1)
echo "$OUTPUT"
echo "$OUTPUT" | grep -q "GitHub Copilot CLI"
- name: Test Bash copilot_here --version
if: runner.os != 'Windows'
shell: bash
run: |
export COPILOT_HERE_BIN="$PWD/publish/shell-test/copilot_here"
source ./copilot_here.sh
OUTPUT=$(copilot_here --version 2>&1)
echo "$OUTPUT"
# Verify version format YYYY.MM.DD
echo "$OUTPUT" | grep -qE "^[0-9]{4}\.[0-9]{2}\.[0-9]{2}"
- name: Test Zsh source and copilot_here --help
if: runner.os == 'macOS'
shell: zsh {0}
run: |
export COPILOT_HERE_BIN="$PWD/publish/shell-test/copilot_here"
source ./copilot_here.sh
OUTPUT=$(copilot_here --help 2>&1)
echo "$OUTPUT"
echo "$OUTPUT" | grep -q "GitHub Copilot CLI"
- name: Test PowerShell script syntax
if: runner.os == 'Windows'
shell: pwsh
run: |
$errors = $null
$null = [System.Management.Automation.Language.Parser]::ParseFile("$PWD/copilot_here.ps1", [ref]$null, [ref]$errors)
if ($errors.Count -gt 0) {
$errors | ForEach-Object { Write-Error $_.Message }
exit 1
}
Write-Host "✓ PowerShell syntax is valid"
- name: Test PowerShell source and Copilot-Here --help
if: runner.os == 'Windows'
shell: pwsh
run: |
$env:COPILOT_HERE_BIN = "$PWD\publish\shell-test\copilot_here.exe"
. .\copilot_here.ps1
$output = & $env:COPILOT_HERE_BIN --help 2>&1 | Out-String
Write-Host $output
if ($output -notmatch "GitHub Copilot CLI") {
Write-Error "Expected 'GitHub Copilot CLI' in output"
exit 1
}
- name: Test PowerShell copilot_here --version
if: runner.os == 'Windows'
shell: pwsh
run: |
$env:COPILOT_HERE_BIN = "$PWD\publish\shell-test\copilot_here.exe"
. .\copilot_here.ps1
$output = & $env:COPILOT_HERE_BIN --version 2>&1 | Out-String
Write-Host $output
if ($output -notmatch "^\d{4}\.\d{2}\.\d{2}") {
Write-Error "Expected version format YYYY.MM.DD"
exit 1
}
# Airlock tests - Linux only (Docker with Linux containers required)
test-airlock:
name: Test Airlock
runs-on: ubuntu-24.04
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: Build images locally
run: |
chmod +x ./dev-build.sh
./dev-build.sh
- name: Make test script executable
run: chmod +x tests/integration/test_airlock.sh
- name: Run airlock integration tests
run: ./tests/integration/test_airlock.sh --use-local
# Live-Docker smoke tests for the brokered Docker socket (--dind).
# Runs on every PR and push: starts the real DockerSocketBroker against
# the runner's Docker daemon, executes docker commands inside a workload
# container that points DOCKER_HOST at the broker, and asserts both the
# happy path (docker version, docker run alpine) AND the Phase 2 body
# inspection rejections (privileged, forbidden host binds).
#
# No copilot_here images required — uses the public docker:cli + alpine
# images so this stage is fast (~1 minute) and unblocked from the main
# build matrices.
integration-tests-broker:
name: Integration Tests (Broker)
needs: [test-cli]
runs-on: ubuntu-24.04
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: Pre-pull test images
run: |
docker pull docker:cli
docker pull alpine:3.21
- name: Run integration tests
env:
RUN_LIVE_DOCKER_TESTS: "1"
working-directory: tests/CopilotHere.IntegrationTests
run: dotnet run --configuration Release
# Build proxy Rust binary natively on each architecture (much faster than QEMU/Docker)
build-proxy-binary:
name: Build Proxy Binary (${{ matrix.arch }})
needs: [test-cli-integration, test-shell-functions, test-airlock, integration-tests-broker]
runs-on: ${{ matrix.runner }}
if: github.ref == 'refs/heads/main'
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
runner: ubuntu-24.04
target: x86_64-unknown-linux-musl
- arch: arm64
runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Install Rust
uses: dtolnay/rust-toolchain@6d9817901c499d6b02debbb57edb38d33daa680b # stable
- name: Install musl tools
run: |
sudo apt-get update
sudo apt-get install -y musl-tools musl-dev
- name: Add musl target
run: rustup target add ${{ matrix.target }}
- name: Build proxy binary
working-directory: proxy
run: |
cargo build --release --target ${{ matrix.target }}
cp target/${{ matrix.target }}/release/secure-proxy ../secure-proxy-${{ matrix.arch }}
- name: Upload proxy binary
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: proxy-binary-${{ matrix.arch }}
path: secure-proxy-${{ matrix.arch }}
if-no-files-found: error
# Build proxy Docker image using pre-built binaries
build-proxy:
name: Build Proxy Image
needs: [build-proxy-binary]
runs-on: ubuntu-24.04
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Set lowercase repository name
id: repo
run: echo "name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
- name: Download proxy binaries
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
pattern: proxy-binary-*
path: proxy-binaries
merge-multiple: true
- name: Prepare binaries for Docker
run: |
mv proxy-binaries/secure-proxy-amd64 proxy-amd64
mv proxy-binaries/secure-proxy-arm64 proxy-arm64
chmod +x proxy-amd64 proxy-arm64
ls -la proxy-*
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
- name: Log in to the Container registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push proxy image (amd64)
uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5
with:
context: .
file: ./docker/Dockerfile.proxy-runtime
platforms: linux/amd64
push: true
provenance: false
sbom: false
tags: ghcr.io/${{ steps.repo.outputs.name }}:proxy-amd64
labels: project=copilot_here
- name: Build and push proxy image (arm64)
uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5
with:
context: .
file: ./docker/Dockerfile.proxy-runtime
platforms: linux/arm64
push: true
provenance: false
sbom: false
tags: ghcr.io/${{ steps.repo.outputs.name }}:proxy-arm64
labels: project=copilot_here
- name: Create and push multi-arch manifest
run: |
docker manifest create ghcr.io/${{ steps.repo.outputs.name }}:proxy \
ghcr.io/${{ steps.repo.outputs.name }}:proxy-amd64 \
ghcr.io/${{ steps.repo.outputs.name }}:proxy-arm64
docker manifest push ghcr.io/${{ steps.repo.outputs.name }}:proxy
docker manifest create ghcr.io/${{ steps.repo.outputs.name }}:proxy-sha-${{ github.sha }} \
ghcr.io/${{ steps.repo.outputs.name }}:proxy-amd64 \
ghcr.io/${{ steps.repo.outputs.name }}:proxy-arm64
docker manifest push ghcr.io/${{ steps.repo.outputs.name }}:proxy-sha-${{ github.sha }}
# Verify generated Dockerfiles are up to date
verify-generated-dockerfiles:
name: Verify Generated Dockerfiles
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Verify generated Dockerfiles match config
run: pwsh docker/verify-generated.ps1
# Fetch latest dependency versions for image builds
prepare-versions:
name: Prepare Versions
needs: [test-cli-integration, test-shell-functions, test-airlock, integration-tests-broker]
runs-on: ubuntu-24.04
if: github.ref == 'refs/heads/main'
outputs:
repo_name: ${{ steps.repo.outputs.name }}
copilot_version: ${{ steps.versions.outputs.copilot_version }}
playwright_version: ${{ steps.versions.outputs.playwright_version }}
csharp_ls_version: ${{ steps.versions.outputs.csharp_ls_version }}
dotnet_8_version: ${{ steps.versions.outputs.dotnet_8_version }}
dotnet_9_version: ${{ steps.versions.outputs.dotnet_9_version }}
dotnet_10_version: ${{ steps.versions.outputs.dotnet_10_version }}
steps:
- name: Set lowercase repository name
id: repo
run: echo "name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
- name: Get latest dependency versions
id: versions
run: |
COPILOT_VERSION=$(npm view @github/copilot version)
echo "copilot_version=$COPILOT_VERSION" >> $GITHUB_OUTPUT
PLAYWRIGHT_VERSION=$(npm view playwright version)
echo "playwright_version=$PLAYWRIGHT_VERSION" >> $GITHUB_OUTPUT
DOTNET_8_VERSION=$(curl -s https://dotnetcli.blob.core.windows.net/dotnet/release-metadata/8.0/releases.json | jq -r '."latest-sdk"')
echo "dotnet_8_version=$DOTNET_8_VERSION" >> $GITHUB_OUTPUT
DOTNET_9_VERSION=$(curl -s https://dotnetcli.blob.core.windows.net/dotnet/release-metadata/9.0/releases.json | jq -r '."latest-sdk"')
echo "dotnet_9_version=$DOTNET_9_VERSION" >> $GITHUB_OUTPUT
DOTNET_10_VERSION=$(curl -s https://dotnetcli.blob.core.windows.net/dotnet/release-metadata/10.0/releases.json | jq -r '."latest-sdk"')
echo "dotnet_10_version=$DOTNET_10_VERSION" >> $GITHUB_OUTPUT
CSHARP_LS_VERSION=$(curl -s https://api.nuget.org/v3-flatcontainer/csharp-ls/index.json | jq -r '.versions[-1]')
echo "csharp_ls_version=$CSHARP_LS_VERSION" >> $GITHUB_OUTPUT
# Build all container images in parallel (each builds independently from node:20-slim)
build-images:
name: Build ${{ matrix.image }}
needs: [prepare-versions, verify-generated-dockerfiles]
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
include:
- image: default
dockerfile: ./docker/generated/Dockerfile.default
tags_extra: |
ghcr.io/${{ needs.prepare-versions.outputs.repo_name }}:latest
ghcr.io/${{ needs.prepare-versions.outputs.repo_name }}:copilot-latest
build_args: "COPILOT_VERSION=$COPILOT_VERSION"
- image: rust
dockerfile: ./docker/generated/Dockerfile.rust
build_args: "COPILOT_VERSION=$COPILOT_VERSION"
- image: golang
dockerfile: ./docker/generated/Dockerfile.golang
build_args: "COPILOT_VERSION=$COPILOT_VERSION"
- image: playwright
dockerfile: ./docker/generated/Dockerfile.playwright
build_args: "COPILOT_VERSION=$COPILOT_VERSION\nPLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION"
- image: dotnet-8
dockerfile: ./docker/generated/Dockerfile.dotnet-8
build_args: "COPILOT_VERSION=$COPILOT_VERSION\nDOTNET_SDK_8_VERSION=$DOTNET_8_VERSION"
- image: dotnet-9
dockerfile: ./docker/generated/Dockerfile.dotnet-9
build_args: "COPILOT_VERSION=$COPILOT_VERSION\nDOTNET_SDK_9_VERSION=$DOTNET_9_VERSION"
- image: dotnet-10
dockerfile: ./docker/generated/Dockerfile.dotnet-10
build_args: "COPILOT_VERSION=$COPILOT_VERSION\nDOTNET_SDK_10_VERSION=$DOTNET_10_VERSION\nCSHARP_LS_VERSION=$CSHARP_LS_VERSION"
- image: dotnet
dockerfile: ./docker/generated/Dockerfile.dotnet
build_args: "COPILOT_VERSION=$COPILOT_VERSION\nDOTNET_SDK_8_VERSION=$DOTNET_8_VERSION\nDOTNET_SDK_9_VERSION=$DOTNET_9_VERSION\nDOTNET_SDK_10_VERSION=$DOTNET_10_VERSION\nCSHARP_LS_VERSION=$CSHARP_LS_VERSION"
- image: dotnet-playwright
dockerfile: ./docker/generated/Dockerfile.dotnet-playwright
build_args: "COPILOT_VERSION=$COPILOT_VERSION\nDOTNET_SDK_8_VERSION=$DOTNET_8_VERSION\nDOTNET_SDK_9_VERSION=$DOTNET_9_VERSION\nDOTNET_SDK_10_VERSION=$DOTNET_10_VERSION\nPLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION\nCSHARP_LS_VERSION=$CSHARP_LS_VERSION"
- image: dotnet-rust
dockerfile: ./docker/generated/Dockerfile.dotnet-rust
build_args: "COPILOT_VERSION=$COPILOT_VERSION\nDOTNET_SDK_8_VERSION=$DOTNET_8_VERSION\nDOTNET_SDK_9_VERSION=$DOTNET_9_VERSION\nDOTNET_SDK_10_VERSION=$DOTNET_10_VERSION\nCSHARP_LS_VERSION=$CSHARP_LS_VERSION"
- image: java
dockerfile: ./docker/generated/Dockerfile.java
build_args: "COPILOT_VERSION=$COPILOT_VERSION"
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
- name: Log in to the Container registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Prepare build args
id: args
run: |
ARGS='${{ matrix.build_args }}'
ARGS="${ARGS//\$COPILOT_VERSION/${{ needs.prepare-versions.outputs.copilot_version }}}"
ARGS="${ARGS//\$PLAYWRIGHT_VERSION/${{ needs.prepare-versions.outputs.playwright_version }}}"
ARGS="${ARGS//\$DOTNET_8_VERSION/${{ needs.prepare-versions.outputs.dotnet_8_version }}}"
ARGS="${ARGS//\$DOTNET_9_VERSION/${{ needs.prepare-versions.outputs.dotnet_9_version }}}"
ARGS="${ARGS//\$DOTNET_10_VERSION/${{ needs.prepare-versions.outputs.dotnet_10_version }}}"
ARGS="${ARGS//\$CSHARP_LS_VERSION/${{ needs.prepare-versions.outputs.csharp_ls_version }}}"
echo "build_args<<EOF" >> $GITHUB_OUTPUT
echo -e "$ARGS" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Prepare tags
id: tags
run: |
REPO="${{ needs.prepare-versions.outputs.repo_name }}"
IMAGE="${{ matrix.image }}"
SHA="${{ github.sha }}"
TAGS="ghcr.io/${REPO}:copilot-${IMAGE}
ghcr.io/${REPO}:copilot-${IMAGE}-sha-${SHA}"
# Add variant shorthand tag (e.g., :rust, :dotnet)
if [[ "$IMAGE" != "default" ]]; then
TAGS="${TAGS}
ghcr.io/${REPO}:${IMAGE}"
fi
# Add extra tags (e.g., :latest for the default image)
EXTRA_TAGS="${{ matrix.tags_extra }}"
if [[ -n "$EXTRA_TAGS" ]]; then
TAGS="${TAGS}
${EXTRA_TAGS}"
fi
echo "tags<<EOF" >> $GITHUB_OUTPUT
echo "$TAGS" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Build and push ${{ matrix.image }} image
uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5
with:
context: .
file: ${{ matrix.dockerfile }}
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.tags.outputs.tags }}
labels: project=copilot_here
build-args: ${{ steps.args.outputs.build_args }}
cache-from: type=registry,ref=ghcr.io/${{ needs.prepare-versions.outputs.repo_name }}:copilot-${{ matrix.image }},mode=max
cache-to: type=inline
# Full airlock + DinD smoke test using the freshly published proxy and
# default app images. This is the unblocker test from issue #20: it spins
# up a real airlock compose project, points the workload at the broker via
# the proxy bridge, and verifies that spawned siblings get the airlock
# NetworkMode injected via Phase 2 body inspection.
#
# Only runs on main because it depends on build-proxy and build-images
# having pushed the canonical sha-tagged images. PRs are covered by the
# cheaper integration-tests-broker job above.
integration-tests-airlock:
name: Integration Tests (Airlock)
needs: [build-proxy, build-images, prepare-versions]
runs-on: ubuntu-24.04
if: 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: Log in to the Container registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Pre-pull test images
run: |
docker pull alpine:3.21
docker pull ghcr.io/${{ needs.prepare-versions.outputs.repo_name }}:proxy-sha-${{ github.sha }}
docker pull ghcr.io/${{ needs.prepare-versions.outputs.repo_name }}:copilot-default-sha-${{ github.sha }}
- name: Run integration tests (airlock + dind)
env:
RUN_LIVE_DOCKER_TESTS: "1"
COPILOT_HERE_PROXY_IMAGE: ghcr.io/${{ needs.prepare-versions.outputs.repo_name }}:proxy-sha-${{ github.sha }}
COPILOT_HERE_APP_IMAGE: ghcr.io/${{ needs.prepare-versions.outputs.repo_name }}:copilot-default-sha-${{ github.sha }}
working-directory: tests/CopilotHere.IntegrationTests
run: dotnet run --configuration Release
# Full airlock + DinD smoke test. Spins up a real airlock compose project,
# points the workload at the broker via the proxy bridge, and verifies that
# spawned siblings get the airlock NetworkMode injected via Phase 2 body
# inspection. This is the unblocker test from issue #20.
#
# Builds the proxy + default app images LOCALLY via dev-build.sh so the job
# can run on every PR without depending on the published ghcr tags (which
# are main-only). On the order of ~5 minutes for the image build, plus
# ~30 seconds for the test itself.
integration-tests-airlock:
name: Integration Tests (Airlock)
needs: [test-cli]
runs-on: ubuntu-24.04
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: Pre-pull alpine (sibling target)
run: docker pull alpine:3.21
- name: Build proxy + default app images locally
run: |
chmod +x ./dev-build.sh
./dev-build.sh
- name: Run integration tests (airlock + dind)
env:
RUN_LIVE_DOCKER_TESTS: "1"
# dev-build.sh tags the proxy as :proxy and the default app image
# as :latest / :copilot-latest. Point the smoke test at those
# local tags via the override env vars (the test skips when both
# are unset to avoid running against random pulls).
COPILOT_HERE_PROXY_IMAGE: ghcr.io/gordonbeeming/copilot_here:proxy
COPILOT_HERE_APP_IMAGE: ghcr.io/gordonbeeming/copilot_here:latest
working-directory: tests/CopilotHere.IntegrationTests
run: dotnet run --configuration Release
# Compute version from VERSION file + run number as revision
compute-version:
name: Compute Version
runs-on: ubuntu-24.04
outputs:
version: ${{ steps.compute.outputs.version }}
short_sha: ${{ steps.compute.outputs.short_sha }}
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Compute version with revision
id: compute
run: |
BASE_VERSION=$(cat VERSION | tr -d '[:space:]')
VERSION="${BASE_VERSION}.${{ github.run_number }}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
SHORT_SHA="${GITHUB_SHA:0:7}"
echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
echo "Computed version: $VERSION (run_number=${{ github.run_number }})"
# Publish .NET Tool to NuGet
publish-nuget:
name: Publish NuGet
needs: [test-cli-integration, test-shell-functions, test-airlock, integration-tests-broker, compute-version]
runs-on: ubuntu-24.04
if: github.event_name != 'schedule' && 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: Pack as .NET tool
run: dotnet pack app/CopilotHere.csproj -c Release -p:PackAsTool=true -p:CopilotHereVersion=${{ needs.compute-version.outputs.version }} --nologo
- name: Push to NuGet
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
needs:
[
build-images,
build-proxy,
integration-tests-airlock,
release-cli,
publish-nuget,
update-winget,
]
if: always() && (needs.build-images.result != 'skipped' || needs.release-cli.result != 'skipped')
runs-on: ubuntu-24.04
steps:
- name: Workflow Summary
run: |
echo "## Workflow Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Trigger:** ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY
echo "**Branch:** ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
echo "**Tests:** Passed (required before build)" >> $GITHUB_STEP_SUMMARY
if [[ "${{ needs.build-images.result }}" == "success" ]]; then
echo "**Images Published:**" >> $GITHUB_STEP_SUMMARY
echo "- \`latest\`, \`copilot-latest\` (default)" >> $GITHUB_STEP_SUMMARY
echo "- \`proxy\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Variants (with copilot- prefix):**" >> $GITHUB_STEP_SUMMARY
echo "- \`rust\`, \`copilot-rust\`" >> $GITHUB_STEP_SUMMARY
echo "- \`golang\`, \`copilot-golang\`" >> $GITHUB_STEP_SUMMARY
echo "- \`playwright\`, \`copilot-playwright\`" >> $GITHUB_STEP_SUMMARY
echo "- \`dotnet\`, \`copilot-dotnet\`" >> $GITHUB_STEP_SUMMARY
echo "- \`dotnet-8\`, \`copilot-dotnet-8\`" >> $GITHUB_STEP_SUMMARY
echo "- \`dotnet-9\`, \`copilot-dotnet-9\`" >> $GITHUB_STEP_SUMMARY
echo "- \`dotnet-10\`, \`copilot-dotnet-10\`" >> $GITHUB_STEP_SUMMARY
echo "- \`dotnet-playwright\`, \`copilot-dotnet-playwright\`" >> $GITHUB_STEP_SUMMARY
echo "- \`dotnet-rust\`, \`copilot-dotnet-rust\`" >> $GITHUB_STEP_SUMMARY
echo "- \`java\`, \`copilot-java\`" >> $GITHUB_STEP_SUMMARY
else
echo "**Images:** No changes or skipped publishing" >> $GITHUB_STEP_SUMMARY
fi
if [[ "${{ needs.release-cli.result }}" == "success" ]]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "**CLI Binaries Published:**" >> $GITHUB_STEP_SUMMARY
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
fi
if [[ "${{ needs.publish-nuget.result }}" == "success" ]]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "**.NET Tool:** Published to nuget.org" >> $GITHUB_STEP_SUMMARY
fi
# Build native CLI binaries for all platforms
build-cli:
name: Build CLI ${{ matrix.rid }}
needs: [test-cli-integration, test-shell-functions, test-airlock, integration-tests-broker, compute-version]
runs-on: ${{ matrix.os }}
if: github.event_name != 'schedule' && github.ref == 'refs/heads/main'
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-24.04
rid: linux-x64
binary: copilot_here
- os: ubuntu-24.04-arm
rid: linux-arm64
binary: copilot_here
- os: macos-15-intel
rid: osx-x64
binary: copilot_here
- os: macos-latest
rid: osx-arm64
binary: copilot_here
- os: windows-latest
rid: win-x64
binary: copilot_here.exe
- os: windows-11-arm
rid: win-arm64
binary: copilot_here.exe
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: Stamp version into source files
shell: bash
run: |
chmod +x scripts/stamp-version.sh
./scripts/stamp-version.sh "${{ needs.compute-version.outputs.version }}"
- name: Publish AOT binary
run: dotnet publish app/CopilotHere.csproj -c Release -r ${{ matrix.rid }} -p:CopilotHereVersion=${{ needs.compute-version.outputs.version }} -o publish/${{ matrix.rid }}
- name: Upload artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: copilot_here-${{ matrix.rid }}
path: publish/${{ matrix.rid }}/${{ matrix.binary }}
if-no-files-found: error
# Create release with CLI binaries
release-cli:
name: Release CLI
needs: [build-cli, compute-version]
runs-on: ubuntu-24.04
if: github.event_name != 'schedule' && 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: Download all artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
path: artifacts
- name: Prepare release assets
run: |
mkdir -p release
for rid in linux-x64 linux-arm64 osx-x64 osx-arm64 win-x64 win-arm64; do
if [[ "$rid" == win-* ]]; then
binary="copilot_here.exe"
else
binary="copilot_here"
fi
# Create archive
cd artifacts/copilot_here-$rid
if [[ "$rid" == win-* ]]; then
zip ../../release/copilot_here-$rid.zip $binary
else
chmod +x $binary
tar -czvf ../../release/copilot_here-$rid.tar.gz $binary
fi
cd ../..
done
# Include shell scripts and install scripts in release
cp copilot_here.sh release/
cp copilot_here.ps1 release/
cp install.sh release/
cp install.ps1 release/
ls -la release/
- name: Stamp version into release shell scripts
run: |
chmod +x scripts/stamp-version.sh
./scripts/stamp-version.sh "${{ needs.compute-version.outputs.version }}"
- name: Create versioned release
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2
with:
tag_name: cli-v${{ needs.compute-version.outputs.version }}-${{ needs.compute-version.outputs.short_sha }}
name: CLI v${{ needs.compute-version.outputs.version }} (${{ needs.compute-version.outputs.short_sha }})
body: |
## Native AOT CLI Binary
Commit: ${{ github.sha }}
### Download
| Platform | Architecture | Download |
|----------|--------------|----------|
| Linux | x64 | [copilot_here-linux-x64.tar.gz](https://github.com/${{ github.repository }}/releases/download/cli-v${{ needs.compute-version.outputs.version }}-${{ needs.compute-version.outputs.short_sha }}/copilot_here-linux-x64.tar.gz) |
| Linux | ARM64 | [copilot_here-linux-arm64.tar.gz](https://github.com/${{ github.repository }}/releases/download/cli-v${{ needs.compute-version.outputs.version }}-${{ needs.compute-version.outputs.short_sha }}/copilot_here-linux-arm64.tar.gz) |
| macOS | x64 | [copilot_here-osx-x64.tar.gz](https://github.com/${{ github.repository }}/releases/download/cli-v${{ needs.compute-version.outputs.version }}-${{ needs.compute-version.outputs.short_sha }}/copilot_here-osx-x64.tar.gz) |
| macOS | ARM64 | [copilot_here-osx-arm64.tar.gz](https://github.com/${{ github.repository }}/releases/download/cli-v${{ needs.compute-version.outputs.version }}-${{ needs.compute-version.outputs.short_sha }}/copilot_here-osx-arm64.tar.gz) |
| Windows | x64 | [copilot_here-win-x64.zip](https://github.com/${{ github.repository }}/releases/download/cli-v${{ needs.compute-version.outputs.version }}-${{ needs.compute-version.outputs.short_sha }}/copilot_here-win-x64.zip) |
| Windows | ARM64 | [copilot_here-win-arm64.zip](https://github.com/${{ github.repository }}/releases/download/cli-v${{ needs.compute-version.outputs.version }}-${{ needs.compute-version.outputs.short_sha }}/copilot_here-win-arm64.zip) |
### Installation
See [README](https://github.com/${{ github.repository }}#installation) for installation instructions.
files: release/*
draft: false
prerelease: false
make_latest: false
- name: Delete existing cli-latest tag
run: |
gh release delete cli-latest --yes || true
git push origin :refs/tags/cli-latest || true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create latest release (for easy download)
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2
with:
tag_name: cli-latest
name: CLI v${{ needs.compute-version.outputs.version }}
body: |
## Latest Native AOT CLI Binary
**Version:** ${{ needs.compute-version.outputs.version }}
**Commit:** ${{ github.sha }}
This release is automatically updated with the latest CLI build.
For versioned releases, see the [releases page](https://github.com/${{ github.repository }}/releases).
### Installation
See [README](https://github.com/${{ github.repository }}#installation) for installation instructions.
files: release/*
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
env:
HOMEBREW_TAP_DEPLOY_KEY: ${{ secrets.HOMEBREW_TAP_DEPLOY_KEY }}
run: |
TAG="cli-v${{ needs.compute-version.outputs.version }}-${{ needs.compute-version.outputs.short_sha }}"
VERSION="${{ needs.compute-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"
# Clone the tap repo
git clone git@github.com:GordonBeeming/homebrew-tap.git /tmp/homebrew-tap
# 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
def install
bin.install "copilot_here"
end
def caveats
<<~EOS
copilot_here requires Docker, Podman, or OrbStack to be installed and running.
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
# Generate the cask (macOS only, bypasses CLT check)
mkdir -p /tmp/homebrew-tap/Casks
cat > /tmp/homebrew-tap/Casks/copilot-here.rb << CASK_EOF
# typed: false
# frozen_string_literal: true
cask "copilot-here" do
version "$VERSION"
on_arm do
url "https://github.com/GordonBeeming/copilot_here/releases/download/$TAG/copilot_here-osx-arm64.tar.gz"
sha256 "${{ steps.sha256.outputs.osx_arm64 }}"
end
on_intel do
url "https://github.com/GordonBeeming/copilot_here/releases/download/$TAG/copilot_here-osx-x64.tar.gz"
sha256 "${{ steps.sha256.outputs.osx_x64 }}"
end
name "copilot_here"
desc "Run GitHub Copilot CLI in a sandboxed Docker container"
homepage "https://github.com/GordonBeeming/copilot_here"
binary "copilot_here"
caveats <<~EOS
copilot_here requires Docker, Podman, or OrbStack to be installed and running.
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
CASK_EOF
# Remove leading whitespace from cask heredoc
sed -i 's/^ //' /tmp/homebrew-tap/Casks/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 Casks/copilot-here.rb
git commit -m "Update copilot_here to $VERSION"
git push
# Clean up
rm -f ~/.ssh/homebrew_tap_key
# Update WinGet manifest (requires Windows for wingetcreate)
update-winget:
name: Update WinGet
needs: [release-cli, compute-version]
runs-on: windows-latest
if: github.event_name != 'schedule' && github.ref == 'refs/heads/main'
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Update WinGet manifest
shell: pwsh
env:
WINGET_PAT: ${{ secrets.WINGET_PAT }}
run: |
iwr https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe
$tag = "cli-v${{ needs.compute-version.outputs.version }}-${{ needs.compute-version.outputs.short_sha }}"
$version = "${{ needs.compute-version.outputs.version }}"
$urlX64 = "https://github.com/${{ github.repository }}/releases/download/$tag/copilot_here-win-x64.zip"
$urlArm64 = "https://github.com/${{ github.repository }}/releases/download/$tag/copilot_here-win-arm64.zip"
# wingetcreate update requires the manifest to already exist in microsoft/winget-pkgs.
# The first submission must be done manually (see packaging/SETUP.md).
.\wingetcreate.exe update GordonBeeming.CopilotHere `
--version $version `
--urls $urlX64 $urlArm64 `
--submit --token $env:WINGET_PAT