diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f5beca4..d40d337 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,23 @@ permissions: contents: read jobs: + formatting: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - name: Check formatting + shell: bash + run: | + unformatted=$(gofmt -l .) + if [ -n "$unformatted" ]; then + echo "The following files need gofmt:" + echo "$unformatted" + exit 1 + fi + test: strategy: fail-fast: false @@ -20,10 +37,20 @@ jobs: - uses: actions/setup-go@v5 with: go-version-file: go.mod - - run: go test -count=1 ./... - - run: go vet ./... + - name: Unit tests + run: go test -count=1 ./... + + vet: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - name: Vet + run: go vet ./... - quality: + race: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -32,11 +59,27 @@ jobs: go-version-file: go.mod - name: Race detector run: go test -race -count=1 ./... + + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod - name: Coverage report run: | go test -coverprofile=coverage.out ./... go tool cover -func=coverage.out go tool cover -func=coverage.out | awk '/^total:/ { gsub("%", "", $3); if ($3 + 0 < 80) { print "coverage below 80%: " $3 "%"; exit 1 } }' + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod - name: Installer syntax run: sh -n install.sh - name: Cross-compile supported targets @@ -50,19 +93,94 @@ jobs: CGO_ENABLED=0 GOOS=$os GOARCH=$arch go build -trimpath -o "/tmp/remote-${os}-${arch}${extension}" ./cmd/remote done - windows-installer: + install-linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - name: Build release fixture + shell: bash + run: | + asset_dir="$RUNNER_TEMP/releases/latest/download" + mkdir -p "$asset_dir" + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ + -trimpath -ldflags "-s -w -X main.version=ci-smoke" \ + -o "$asset_dir/remote-linux-amd64" ./cmd/remote + (cd "$asset_dir" && sha256sum remote-linux-amd64 > checksums.txt) + - name: Run installer and installed CLI + shell: bash + run: | + python3 -m http.server 8765 --bind 127.0.0.1 \ + --directory "$RUNNER_TEMP/releases" >/tmp/remote-installer-http.log 2>&1 & + server_pid=$! + trap 'kill "$server_pid"' EXIT + + for attempt in 1 2 3 4 5; do + curl -fsS http://127.0.0.1:8765/latest/download/checksums.txt >/dev/null && break + if [ "$attempt" -eq 5 ]; then + cat /tmp/remote-installer-http.log + exit 1 + fi + sleep 1 + done + + REMOTE_RELEASE_BASE_URL=http://127.0.0.1:8765 \ + REMOTE_INSTALL_DIR="$RUNNER_TEMP/remote-bin" \ + sh ./install.sh + test -x "$RUNNER_TEMP/remote-bin/remote" + test "$("$RUNNER_TEMP/remote-bin/remote" version)" = "ci-smoke" + + install-windows: runs-on: windows-latest steps: - uses: actions/checkout@v4 - - name: Parse PowerShell installer + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - name: Build release fixture shell: pwsh run: | - $tokens = $null - $errors = $null - [System.Management.Automation.Language.Parser]::ParseFile( - (Resolve-Path "./install.ps1"), [ref]$tokens, [ref]$errors - ) > $null - if ($errors.Count) { - $errors | ForEach-Object { Write-Error $_ } - exit 1 + $AssetDir = Join-Path $env:RUNNER_TEMP "releases\latest\download" + New-Item -ItemType Directory -Force -Path $AssetDir | Out-Null + $Asset = "remote-windows-amd64.exe" + go build -trimpath -ldflags "-s -w -X main.version=ci-smoke" ` + -o (Join-Path $AssetDir $Asset) ./cmd/remote + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + $Hash = (Get-FileHash -Algorithm SHA256 (Join-Path $AssetDir $Asset)).Hash.ToLowerInvariant() + "$Hash $Asset" | Set-Content -Encoding ascii (Join-Path $AssetDir "checksums.txt") + - name: Run installer and installed CLI + shell: pwsh + run: | + $ReleaseRoot = Join-Path $env:RUNNER_TEMP "releases" + $Log = Join-Path $env:RUNNER_TEMP "remote-installer-http.log" + $Server = Start-Process python -ArgumentList @( + "-m", "http.server", "8765", "--bind", "127.0.0.1", "--directory", $ReleaseRoot + ) -RedirectStandardOutput $Log -RedirectStandardError "$Log.err" -PassThru + try { + $Ready = $false + foreach ($Attempt in 1..5) { + try { + Invoke-WebRequest -UseBasicParsing ` + http://127.0.0.1:8765/latest/download/checksums.txt | Out-Null + $Ready = $true + break + } catch { + Start-Sleep -Seconds 1 + } + } + if (-not $Ready) { throw "release fixture server did not start" } + + $env:REMOTE_RELEASE_BASE_URL = "http://127.0.0.1:8765" + $env:REMOTE_INSTALL_DIR = Join-Path $env:RUNNER_TEMP "remote-bin" + & ./install.ps1 + $Installed = Join-Path $env:REMOTE_INSTALL_DIR "remote.exe" + if (-not (Test-Path $Installed)) { throw "installer did not create $Installed" } + $InstalledVersion = & $Installed version + if ($LASTEXITCODE -ne 0 -or $InstalledVersion -ne "ci-smoke") { + throw "installed CLI failed: expected ci-smoke, got '$InstalledVersion'" + } + } finally { + Stop-Process -Id $Server.Id -Force -ErrorAction SilentlyContinue } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 756fba4..3bcf6eb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ permissions: contents: write jobs: - release: + build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -22,20 +22,6 @@ jobs: go test ./... go vet ./... sh -n install.sh - - name: Validate Windows installer - shell: pwsh - run: | - $tokens = $null - $errors = $null - [System.Management.Automation.Language.Parser]::ParseFile( - (Resolve-Path "./install.ps1"), - [ref]$tokens, - [ref]$errors - ) > $null - if ($errors.Count) { - $errors | ForEach-Object { Write-Error $_ } - exit 1 - } - name: Build release binaries run: | mkdir -p dist @@ -52,6 +38,106 @@ jobs: done cd dist sha256sum remote-* > checksums.txt + - name: Store release assets + uses: actions/upload-artifact@v4 + with: + name: release-assets + path: dist/ + if-no-files-found: error + + install-smoke: + needs: build + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Download release assets + uses: actions/download-artifact@v4 + with: + name: release-assets + path: dist + - name: Install and execute release on Linux + if: runner.os == 'Linux' + shell: bash + run: | + fixture="$RUNNER_TEMP/releases/download/$GITHUB_REF_NAME" + install_dir="$RUNNER_TEMP/remote-bin" + mkdir -p "$fixture" + cp dist/* "$fixture/" + + python3 -m http.server 8765 --bind 127.0.0.1 \ + --directory "$RUNNER_TEMP/releases" >/tmp/remote-release-http.log 2>&1 & + server_pid=$! + trap 'kill "$server_pid"' EXIT + for attempt in 1 2 3 4 5; do + curl -fsS "http://127.0.0.1:8765/download/$GITHUB_REF_NAME/checksums.txt" >/dev/null && break + if [ "$attempt" -eq 5 ]; then + cat /tmp/remote-release-http.log + exit 1 + fi + sleep 1 + done + + REMOTE_VERSION="$GITHUB_REF_NAME" \ + REMOTE_RELEASE_BASE_URL=http://127.0.0.1:8765 \ + REMOTE_INSTALL_DIR="$install_dir" \ + sh ./install.sh + test -x "$install_dir/remote" + test "$("$install_dir/remote" version)" = "$GITHUB_REF_NAME" + - name: Install and execute release on Windows + if: runner.os == 'Windows' + shell: pwsh + run: | + $Fixture = Join-Path $env:RUNNER_TEMP "releases\download\$env:GITHUB_REF_NAME" + New-Item -ItemType Directory -Force -Path $Fixture | Out-Null + Copy-Item "dist\*" $Fixture + + $ReleaseRoot = Join-Path $env:RUNNER_TEMP "releases" + $Log = Join-Path $env:RUNNER_TEMP "remote-release-http.log" + $Server = Start-Process python -ArgumentList @( + "-m", "http.server", "8765", "--bind", "127.0.0.1", "--directory", $ReleaseRoot + ) -RedirectStandardOutput $Log -RedirectStandardError "$Log.err" -PassThru + try { + $Ready = $false + foreach ($Attempt in 1..5) { + try { + Invoke-WebRequest -UseBasicParsing ` + "http://127.0.0.1:8765/download/$env:GITHUB_REF_NAME/checksums.txt" | Out-Null + $Ready = $true + break + } catch { + Start-Sleep -Seconds 1 + } + } + if (-not $Ready) { throw "release fixture server did not start" } + + $env:REMOTE_VERSION = $env:GITHUB_REF_NAME + $env:REMOTE_RELEASE_BASE_URL = "http://127.0.0.1:8765" + $env:REMOTE_INSTALL_DIR = Join-Path $env:RUNNER_TEMP "remote-bin" + & ./install.ps1 + $Installed = Join-Path $env:REMOTE_INSTALL_DIR "remote.exe" + if (-not (Test-Path $Installed)) { throw "installer did not create $Installed" } + $InstalledVersion = & $Installed version + if ($LASTEXITCODE -ne 0 -or $InstalledVersion -ne $env:GITHUB_REF_NAME) { + throw "installed CLI failed: expected $env:GITHUB_REF_NAME, got '$InstalledVersion'" + } + } finally { + Stop-Process -Id $Server.Id -Force -ErrorAction SilentlyContinue + } + + publish: + needs: [build, install-smoke] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Download tested release assets + uses: actions/download-artifact@v4 + with: + name: release-assets + path: dist - name: Publish GitHub release env: GH_TOKEN: ${{ github.token }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cc3e74c..7327096 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,9 +11,11 @@ make coverage go vet ./... ``` -CI runs tests and vet on Linux, macOS, and Windows, validates both installers, -runs the race detector, cross-compiles every supported OS/architecture pair, -and rejects total statement coverage below 80%. +CI runs tests on Linux, macOS, and Windows. Independent jobs run formatting, +vet, the race detector, coverage, cross-compilation, and real installer smoke +tests in parallel. The installer jobs download checksummed fixture releases, +install them, and execute the resulting Linux or Windows binary. CI rejects +total statement coverage below 80%. ## Testing changes