diff --git a/.github/actions/cli/build-test-archive/action.yml b/.github/actions/cli/build-test-archive/action.yml new file mode 100644 index 000000000..df4fdf403 --- /dev/null +++ b/.github/actions/cli/build-test-archive/action.yml @@ -0,0 +1,39 @@ +name: 'Build opentaint test archive' +description: 'Builds the opentaint CLI and packages it into the per-OS archive plus checksums.txt that the installers expect at the download URL' + +inputs: + working-directory: + description: 'Directory containing the Go module to build' + required: false + default: 'cli' + +runs: + using: 'composite' + steps: + - name: Build archive (linux) + if: runner.os == 'Linux' + shell: bash + working-directory: ${{ inputs.working-directory }} + run: | + go build -o opentaint . + tar -czf opentaint-full_linux_amd64.tar.gz opentaint + sha256sum opentaint-full_linux_amd64.tar.gz > checksums.txt + + - name: Build archive (macos) + if: runner.os == 'macOS' + shell: bash + working-directory: ${{ inputs.working-directory }} + run: | + go build -o opentaint . + tar -czf opentaint-full_darwin_arm64.tar.gz opentaint + shasum -a 256 opentaint-full_darwin_arm64.tar.gz > checksums.txt + + - name: Build archive (windows) + if: runner.os == 'Windows' + shell: pwsh + working-directory: ${{ inputs.working-directory }} + run: | + go build -o opentaint.exe . + Compress-Archive -Path opentaint.exe -DestinationPath opentaint-full_windows_amd64.zip + $hash = (Get-FileHash -Path opentaint-full_windows_amd64.zip -Algorithm SHA256).Hash.ToLower() + "$hash opentaint-full_windows_amd64.zip" | Out-File -FilePath checksums.txt -Encoding utf8NoBOM diff --git a/.github/actions/cli/serve-archive/action.yml b/.github/actions/cli/serve-archive/action.yml new file mode 100644 index 000000000..f31124211 --- /dev/null +++ b/.github/actions/cli/serve-archive/action.yml @@ -0,0 +1,43 @@ +name: 'Serve test archive over HTTP (unix only)' +description: | + Starts a Python http.server in a directory and waits until it is reachable. + Linux/macOS only: on Windows the GitHub Actions Job Object kills background + processes at step end, so the server would not survive into the next step. + Windows callers must inline the server start, readiness poll, and consumer + command in a single step. + +inputs: + archive-dir: + description: 'Directory whose contents to serve' + required: true + port: + description: 'Port to listen on' + required: false + default: '8080' + timeout-seconds: + description: 'How long to wait for the server to become ready' + required: false + default: '30' + +runs: + using: 'composite' + steps: + - name: Reject Windows runner + if: runner.os == 'Windows' + shell: pwsh + run: | + Write-Error 'cli/serve-archive does not support Windows runners. Inline the server start, readiness poll, and the consumer command in a single step.' + exit 1 + + - name: Start server + if: runner.os != 'Windows' + shell: bash + working-directory: ${{ inputs.archive-dir }} + env: + SERVE_PORT: ${{ inputs.port }} + SERVE_TIMEOUT: ${{ inputs.timeout-seconds }} + run: | + LOG="${RUNNER_TEMP}/http-server.log" + python3 -m http.server "$SERVE_PORT" > "$LOG" 2>&1 & + bash "${GITHUB_WORKSPACE}/scripts/ci/wait-for-http-server.sh" \ + "http://127.0.0.1:${SERVE_PORT}/" "$SERVE_TIMEOUT" "$LOG" diff --git a/.github/actions/cli/verify-installed-binary/action.yml b/.github/actions/cli/verify-installed-binary/action.yml new file mode 100644 index 000000000..8f31c1e99 --- /dev/null +++ b/.github/actions/cli/verify-installed-binary/action.yml @@ -0,0 +1,30 @@ +name: 'Verify installed opentaint binary' +description: 'Runs ` --version`, falling back to the standard per-OS install location when no binary path is provided' + +inputs: + binary-path: + description: 'Path to the opentaint binary (typically from an install step output). Falls back to the standard per-OS install location when empty.' + required: false + default: '' + +runs: + using: 'composite' + steps: + - name: Verify (unix) + if: runner.os != 'Windows' + shell: bash + env: + BINARY_PATH: ${{ inputs.binary-path }} + run: | + if [ -z "$BINARY_PATH" ]; then BINARY_PATH="$HOME/.opentaint/install/opentaint"; fi + "$BINARY_PATH" --version + + - name: Verify (windows) + if: runner.os == 'Windows' + shell: pwsh + env: + BINARY_PATH: ${{ inputs.binary-path }} + run: | + $binaryPath = $env:BINARY_PATH + if ([string]::IsNullOrEmpty($binaryPath)) { $binaryPath = "$env:LOCALAPPDATA\opentaint\install\opentaint.exe" } + & $binaryPath --version diff --git a/.github/workflows/ci-cli.yaml b/.github/workflows/ci-cli.yaml index c14735a35..a4e382d9f 100644 --- a/.github/workflows/ci-cli.yaml +++ b/.github/workflows/ci-cli.yaml @@ -323,26 +323,24 @@ jobs: with: go-version: ${{ env.GO_VERSION }} cache-dependency-path: cli/go.sum + - uses: actions/setup-python@v5 + with: + python-version: '3.x' - name: Build test archive - working-directory: cli - run: | - go build -o opentaint . - tar -czf opentaint-full_linux_amd64.tar.gz opentaint - sha256sum opentaint-full_linux_amd64.tar.gz > checksums.txt - - name: Start file server - working-directory: cli - run: python3 -m http.server 8080 & + uses: ./.github/actions/cli/build-test-archive + - name: Serve test archive + uses: ./.github/actions/cli/serve-archive + with: + archive-dir: cli - name: Run install.sh id: install-linux env: - OPENTAINT_DOWNLOAD_BASE_URL: http://localhost:8080 - run: | - echo "OPENTAINT_BINARY_PATH=$(bash scripts/install/install.sh | grep ^OPENTAINT_BINARY_PATH= | cut -d= -f2)" >> $GITHUB_OUTPUT + OPENTAINT_DOWNLOAD_BASE_URL: http://127.0.0.1:8080 + run: bash scripts/ci/run-installer-sh.sh scripts/install/install.sh - name: Verify installation - run: | - BINARY_PATH="${{ steps.install-linux.outputs.OPENTAINT_BINARY_PATH }}" - if [ -z "$BINARY_PATH" ]; then BINARY_PATH="$HOME/.opentaint/install/opentaint"; fi - "$BINARY_PATH" --version + uses: ./.github/actions/cli/verify-installed-binary + with: + binary-path: ${{ steps.install-linux.outputs.OPENTAINT_BINARY_PATH }} test-install-sh-macos: runs-on: macos-latest @@ -352,26 +350,24 @@ jobs: with: go-version: ${{ env.GO_VERSION }} cache-dependency-path: cli/go.sum + - uses: actions/setup-python@v5 + with: + python-version: '3.x' - name: Build test archive - working-directory: cli - run: | - go build -o opentaint . - tar -czf opentaint-full_darwin_arm64.tar.gz opentaint - shasum -a 256 opentaint-full_darwin_arm64.tar.gz > checksums.txt - - name: Start file server - working-directory: cli - run: python3 -m http.server 8080 & + uses: ./.github/actions/cli/build-test-archive + - name: Serve test archive + uses: ./.github/actions/cli/serve-archive + with: + archive-dir: cli - name: Run install.sh id: install-macos env: - OPENTAINT_DOWNLOAD_BASE_URL: http://localhost:8080 - run: | - echo "OPENTAINT_BINARY_PATH=$(bash scripts/install/install.sh | grep ^OPENTAINT_BINARY_PATH= | cut -d= -f2)" >> $GITHUB_OUTPUT + OPENTAINT_DOWNLOAD_BASE_URL: http://127.0.0.1:8080 + run: bash scripts/ci/run-installer-sh.sh scripts/install/install.sh - name: Verify installation - run: | - BINARY_PATH="${{ steps.install-macos.outputs.OPENTAINT_BINARY_PATH }}" - if [ -z "$BINARY_PATH" ]; then BINARY_PATH="$HOME/.opentaint/install/opentaint"; fi - "$BINARY_PATH" --version + uses: ./.github/actions/cli/verify-installed-binary + with: + binary-path: ${{ steps.install-macos.outputs.OPENTAINT_BINARY_PATH }} test-install-ps1: runs-on: windows-latest @@ -381,35 +377,46 @@ jobs: with: go-version: ${{ env.GO_VERSION }} cache-dependency-path: cli/go.sum + - uses: actions/setup-python@v5 + with: + python-version: '3.x' - name: Build test archive - shell: pwsh - working-directory: cli - run: | - go build -o opentaint.exe . - Compress-Archive -Path opentaint.exe -DestinationPath opentaint-full_windows_amd64.zip - $hash = (Get-FileHash -Path opentaint-full_windows_amd64.zip -Algorithm SHA256).Hash.ToLower() - "$hash opentaint-full_windows_amd64.zip" | Out-File -FilePath checksums.txt -Encoding utf8NoBOM - - name: Run install.ps1 + uses: ./.github/actions/cli/build-test-archive + # Server start, readiness poll and install must share a single step: + # on windows-latest the runner's Job Object kills background processes + # at step end, so the python server cannot live across steps. + - name: Serve archive and run install.ps1 id: install-windows shell: pwsh env: OPENTAINT_DOWNLOAD_BASE_URL: http://127.0.0.1:8080 + SERVE_PORT: '8080' + SERVE_TIMEOUT: '30' run: | - Start-Process -NoNewWindow python -ArgumentList "-m", "http.server", "8080" -WorkingDirectory cli - Start-Sleep -Seconds 2 - $output = pwsh -File scripts/install/install.ps1 2>&1 | Out-String - Write-Host $output - if ($LASTEXITCODE -ne 0) { - throw "install.ps1 exited with code $LASTEXITCODE" - } - if ($output -notmatch 'OPENTAINT_BINARY_PATH=(.+)') { - throw "install.ps1 did not emit OPENTAINT_BINARY_PATH" + $port = [int]$env:SERVE_PORT + $logOut = Join-Path $env:RUNNER_TEMP 'http-server.out.log' + $logErr = Join-Path $env:RUNNER_TEMP 'http-server.err.log' + $server = Start-Process -NoNewWindow -PassThru ` + -FilePath python ` + -ArgumentList '-m', 'http.server', "$port" ` + -WorkingDirectory cli ` + -RedirectStandardOutput $logOut ` + -RedirectStandardError $logErr + try { + & "$env:GITHUB_WORKSPACE/scripts/ci/wait-for-http-server.ps1" ` + -Url "http://127.0.0.1:$port/" ` + -TimeoutSec ([int]$env:SERVE_TIMEOUT) ` + -Process $server ` + -StdoutLog $logOut ` + -StderrLog $logErr + & "$env:GITHUB_WORKSPACE/scripts/ci/run-installer-pwsh.ps1" ` + -Script scripts/install/install.ps1 + } finally { + if ($server -and -not $server.HasExited) { + Stop-Process -Id $server.Id -Force -ErrorAction SilentlyContinue + } } - $binaryPath = $Matches[1].Trim() - "OPENTAINT_BINARY_PATH=$binaryPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 - name: Verify installation - shell: pwsh - run: | - $binaryPath = "${{ steps.install-windows.outputs.OPENTAINT_BINARY_PATH }}" - if ([string]::IsNullOrEmpty($binaryPath)) { $binaryPath = "$env:LOCALAPPDATA\opentaint\install\opentaint.exe" } - & $binaryPath --version + uses: ./.github/actions/cli/verify-installed-binary + with: + binary-path: ${{ steps.install-windows.outputs.OPENTAINT_BINARY_PATH }} diff --git a/.gitignore b/.gitignore index 30765f8d9..27bf36d6d 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ config.local.* # Optional: Allow the .gitignore file itself to be tracked !.gitignore +!.github diff --git a/scripts/ci/run-installer-pwsh.ps1 b/scripts/ci/run-installer-pwsh.ps1 new file mode 100644 index 000000000..e0be11d3e --- /dev/null +++ b/scripts/ci/run-installer-pwsh.ps1 @@ -0,0 +1,22 @@ +# Run an opentaint PowerShell install script and propagate its +# OPENTAINT_BINARY_PATH line to $env:GITHUB_OUTPUT so a verify step can +# consume it. +[CmdletBinding()] +param( + [Parameter(Mandatory)] [string]$Script +) + +$ErrorActionPreference = 'Stop' + +$output = pwsh -File $Script 2>&1 | Out-String +Write-Host $output + +if ($LASTEXITCODE -ne 0) { + throw "$Script exited with code $LASTEXITCODE" +} +if ($output -notmatch 'OPENTAINT_BINARY_PATH=(.+)') { + throw "$Script did not emit OPENTAINT_BINARY_PATH" +} + +$binaryPath = $Matches[1].Trim() +"OPENTAINT_BINARY_PATH=$binaryPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 diff --git a/scripts/ci/run-installer-sh.sh b/scripts/ci/run-installer-sh.sh new file mode 100644 index 000000000..8104e460c --- /dev/null +++ b/scripts/ci/run-installer-sh.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Run an opentaint POSIX install script and propagate its OPENTAINT_BINARY_PATH +# line to $GITHUB_OUTPUT so a verify step can consume it. +# Usage: run-installer-sh.sh PATH_TO_INSTALL_SCRIPT +set -euo pipefail + +SCRIPT="${1:?usage: $0 PATH_TO_INSTALL_SCRIPT}" + +OUTPUT=$(bash "$SCRIPT") +echo "$OUTPUT" + +BINARY_PATH=$(echo "$OUTPUT" | grep '^OPENTAINT_BINARY_PATH=' | cut -d= -f2-) +if [ -z "$BINARY_PATH" ]; then + echo "$SCRIPT did not emit OPENTAINT_BINARY_PATH" >&2 + exit 1 +fi + +echo "OPENTAINT_BINARY_PATH=$BINARY_PATH" >> "$GITHUB_OUTPUT" diff --git a/scripts/ci/wait-for-http-server.ps1 b/scripts/ci/wait-for-http-server.ps1 new file mode 100644 index 000000000..62acb9ccc --- /dev/null +++ b/scripts/ci/wait-for-http-server.ps1 @@ -0,0 +1,28 @@ +# Poll an HTTP URL until it responds; fail and dump the server log on timeout. +# Returns to the caller on success; throws on timeout or early server exit. +[CmdletBinding()] +param( + [Parameter(Mandatory)] [string]$Url, + [int]$TimeoutSec = 30, + [System.Diagnostics.Process]$Process, + [string]$StdoutLog, + [string]$StderrLog +) + +$ErrorActionPreference = 'Stop' + +for ($i = 1; $i -le $TimeoutSec; $i++) { + if ($Process -and $Process.HasExited) { break } + try { + Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 2 -ErrorAction Stop | Out-Null + Write-Host "Server ready after ${i}s" + return + } catch { + Start-Sleep -Seconds 1 + } +} + +Write-Host "Server did not become ready at $Url within ${TimeoutSec}s." +if ($StdoutLog) { Write-Host '--- stdout ---'; Get-Content $StdoutLog -ErrorAction SilentlyContinue } +if ($StderrLog) { Write-Host '--- stderr ---'; Get-Content $StderrLog -ErrorAction SilentlyContinue } +throw "Local HTTP server did not become ready at $Url" diff --git a/scripts/ci/wait-for-http-server.sh b/scripts/ci/wait-for-http-server.sh new file mode 100644 index 000000000..88a9a43f2 --- /dev/null +++ b/scripts/ci/wait-for-http-server.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Poll an HTTP URL until it responds; fail and dump the server log on timeout. +# Usage: wait-for-http-server.sh URL [TIMEOUT_SECONDS] [LOG_PATH] +set -euo pipefail + +URL="${1:?usage: $0 URL [TIMEOUT_SECONDS] [LOG_PATH]}" +TIMEOUT="${2:-30}" +LOG="${3:-}" + +for i in $(seq 1 "$TIMEOUT"); do + if curl -fsS "$URL" -o /dev/null 2>/dev/null; then + echo "Server ready after ${i}s" + exit 0 + fi + sleep 1 +done + +echo "Server did not become ready at ${URL} within ${TIMEOUT}s." +if [ -n "$LOG" ] && [ -f "$LOG" ]; then + echo '--- log ---' + cat "$LOG" +fi +exit 1