test: isolate Windows lifecycle from unavailable Docker #39
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Phase acceptance | |
| on: | |
| push: | |
| pull_request: | |
| workflow_dispatch: | |
| inputs: | |
| release_tag: | |
| description: Optional Contexa CLI prerelease tag to verify | |
| required: false | |
| type: string | |
| jobs: | |
| installer-locale: | |
| name: Installer locale / ${{ matrix.os }} | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 45 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest, windows-latest] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| - name: Validate installer syntax (POSIX) | |
| if: runner.os != 'Windows' | |
| shell: bash | |
| run: sh -n install.sh | |
| - name: Validate installer syntax (PowerShell 5.1) | |
| if: runner.os == 'Windows' | |
| shell: powershell | |
| run: | | |
| $tokens = $null | |
| $errors = $null | |
| [System.Management.Automation.Language.Parser]::ParseFile( | |
| (Resolve-Path '.\install.ps1'), [ref]$tokens, [ref]$errors) | Out-Null | |
| if ($errors.Count) { throw ($errors | Out-String) } | |
| - name: Validate installer syntax (PowerShell 7) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| $tokens = $null | |
| $errors = $null | |
| [System.Management.Automation.Language.Parser]::ParseFile( | |
| (Resolve-Path '.\install.ps1'), [ref]$tokens, [ref]$errors) | Out-Null | |
| if ($errors.Count) { throw ($errors | Out-String) } | |
| - name: Run installer fault and locale acceptance | |
| env: | |
| CONTEXA_LIFECYCLE_REPEATS: '20' | |
| CONTEXA_FAULT_REPEATS: '5' | |
| run: >- | |
| node -e "const f=require('fs'),c=require('child_process');const r=c.spawnSync(process.execPath,['--test','test/installer-runtime.test.js'],{encoding:'utf8',windowsHide:true});const out=(r.stdout||'')+(r.stderr||'');f.writeFileSync('installer-runtime.tap',out);process.stdout.write(out);process.exit(r.status===null?1:r.status)" | |
| - name: Run the full installer fault matrix with PowerShell 7 | |
| if: runner.os == 'Windows' | |
| env: | |
| CONTEXA_TEST_POWERSHELL: pwsh.exe | |
| CONTEXA_LIFECYCLE_REPEATS: '20' | |
| CONTEXA_FAULT_REPEATS: '5' | |
| run: >- | |
| node -e "const f=require('fs'),c=require('child_process');const r=c.spawnSync(process.execPath,['--test','test/installer-runtime.test.js'],{encoding:'utf8',windowsHide:true});const out=(r.stdout||'')+(r.stderr||'');f.writeFileSync('installer-runtime-pwsh.tap',out);process.stdout.write(out);process.exit(r.status===null?1:r.status)" | |
| - name: Run installer source and release-contract acceptance | |
| run: node --test test/installers.test.js | |
| - name: Publish installer test failure detail | |
| if: failure() | |
| shell: bash | |
| run: | | |
| node -e "const f=require('fs');const paths=['installer-runtime.tap','installer-runtime-pwsh.tap'];const s=paths.map(p=>f.existsSync(p)?p+'\n'+f.readFileSync(p,'utf8'):'').join('\n').slice(-12000)||'installer TAP output is missing';const e=s.replace(/%/g,'%25').replace(/\r/g,'%0D').replace(/\n/g,'%0A');console.log('::error file=test/installer-runtime.test.js,line=1,title=Installer runtime failure::'+e)" | |
| - uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: installer-runtime-evidence-${{ matrix.os }} | |
| path: | | |
| installer-runtime.tap | |
| installer-runtime-pwsh.tap | |
| - name: Resolve current and previous prerelease tags | |
| env: | |
| INPUT_RELEASE_TAG: ${{ inputs.release_tag }} | |
| GITHUB_TOKEN: ${{ github.token }} | |
| shell: bash | |
| run: | | |
| node <<'NODE' | |
| const fs = require('fs'); | |
| (async () => { | |
| const requested = (process.env.INPUT_RELEASE_TAG || '').trim(); | |
| const response = await fetch('https://api.github.com/repos/contexa-security/contexa-cli/releases?per_page=30', { | |
| headers: { | |
| Accept: 'application/vnd.github+json', | |
| Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, | |
| 'User-Agent': 'install-ctxa-phase-acceptance', | |
| }, | |
| }); | |
| if (!response.ok) throw new Error(`release lookup failed: ${response.status}`); | |
| const releases = (await response.json()).filter(release => release.prerelease && !release.draft); | |
| const current = requested | |
| ? releases.find(release => release.tag_name === requested) | |
| : releases[0]; | |
| if (!current) throw new Error(`requested prerelease not found: ${requested || 'latest'}`); | |
| const currentIndex = releases.findIndex(release => release.tag_name === current.tag_name); | |
| const previous = releases.slice(currentIndex + 1) | |
| .find(release => release.tag_name !== current.tag_name); | |
| if (!previous) throw new Error(`previous prerelease not found for ${current.tag_name}`); | |
| fs.appendFileSync(process.env.GITHUB_ENV, | |
| `CONTEXA_PHASE_TAG=${current.tag_name}\nCONTEXA_PREVIOUS_TAG=${previous.tag_name}\n`); | |
| })().catch(error => { | |
| console.error(error); | |
| process.exitCode = 1; | |
| }); | |
| NODE | |
| - name: Verify actual install, update, no-op, rollback and init/reset (POSIX) | |
| if: runner.os != 'Windows' | |
| shell: bash | |
| env: | |
| CONTEXA_INSTALL_DIR: ${{ runner.temp }}/contexa-bin | |
| CONTEXA_SKIP_PATH_UPDATE: '1' | |
| run: | | |
| set -euo pipefail | |
| test "$(id -u)" -ne 0 | |
| expected_current="${CONTEXA_PHASE_TAG#v}" | |
| expected_previous="${CONTEXA_PREVIOUS_TAG#v}" | |
| CONTEXA_VERSION="$CONTEXA_PREVIOUS_TAG" sh ./install.sh | |
| test "$("$CONTEXA_INSTALL_DIR/contexa" --version)" = "$expected_previous" | |
| CONTEXA_VERSION="$CONTEXA_PHASE_TAG" sh ./install.sh | |
| test "$("$CONTEXA_INSTALL_DIR/contexa" --version)" = "$expected_current" | |
| test "$("$CONTEXA_INSTALL_DIR/contexa.previous" --version)" = "$expected_previous" | |
| CONTEXA_VERSION="$CONTEXA_PHASE_TAG" sh ./install.sh | |
| CONTEXA_INSTALL_ACTION=rollback sh ./install.sh | |
| test "$("$CONTEXA_INSTALL_DIR/contexa" --version)" = "$expected_previous" | |
| CONTEXA_VERSION="$CONTEXA_PHASE_TAG" sh ./install.sh | |
| fixture="$RUNNER_TEMP/contexa-installer-smoke" | |
| mkdir -p "$fixture/src/main/java/example" "$fixture/src/main/resources" | |
| printf '%s\n' "plugins { id 'org.springframework.boot' version '3.3.0' }" \ | |
| 'repositories { mavenCentral() }' \ | |
| "dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' }" \ | |
| > "$fixture/build.gradle" | |
| printf "%s\n" "rootProject.name = 'installer-smoke'" > "$fixture/settings.gradle" | |
| printf '%s\n' 'server:' ' port: 9080' > "$fixture/src/main/resources/application.yml" | |
| printf '%s\n' \ | |
| 'package example;' \ | |
| 'import org.springframework.boot.autoconfigure.SpringBootApplication;' \ | |
| '@SpringBootApplication' \ | |
| 'public class InstallerSmokeApplication {}' \ | |
| > "$fixture/src/main/java/example/InstallerSmokeApplication.java" | |
| "$CONTEXA_INSTALL_DIR/contexa" init --yes --dir "$fixture" | |
| "$CONTEXA_INSTALL_DIR/contexa" reset --yes --dir "$fixture" | |
| test ! -f "$fixture/contexa/manifest.json" | |
| CONTEXA_INSTALL_ACTION=uninstall sh ./install.sh | |
| test ! -e "$CONTEXA_INSTALL_DIR/contexa" | |
| test ! -e "$CONTEXA_INSTALL_DIR/contexa.previous" | |
| - name: Verify actual install, update, no-op, rollback and init/reset (Windows) | |
| if: runner.os == 'Windows' | |
| shell: powershell | |
| env: | |
| CONTEXA_INSTALL_DIR: ${{ runner.temp }}\contexa-bin | |
| CONTEXA_SKIP_PATH_UPDATE: '1' | |
| run: | | |
| $ErrorActionPreference = 'Stop' | |
| trap { | |
| $message = $_.Exception.ToString().Replace('%', '%25').Replace("`r", '%0D').Replace("`n", '%0A') | |
| Write-Output ('::error file=.github/workflows/phase-acceptance.yml,line=132,title=Windows actual installer failure::' + $message) | |
| exit 1 | |
| } | |
| $binary = Join-Path $env:CONTEXA_INSTALL_DIR 'contexa.exe' | |
| $env:CONTEXA_VERSION = $env:CONTEXA_PREVIOUS_TAG | |
| & .\install.ps1 | |
| if ((& $binary --version).Trim() -ne $env:CONTEXA_PREVIOUS_TAG.TrimStart('v')) { throw 'previous install mismatch' } | |
| $env:CONTEXA_VERSION = $env:CONTEXA_PHASE_TAG | |
| & .\install.ps1 | |
| if ((& $binary --version).Trim() -ne $env:CONTEXA_PHASE_TAG.TrimStart('v')) { throw 'current update mismatch' } | |
| & .\install.ps1 | |
| $env:CONTEXA_INSTALL_ACTION = 'rollback' | |
| & .\install.ps1 | |
| Remove-Item Env:CONTEXA_INSTALL_ACTION | |
| if ((& $binary --version).Trim() -ne $env:CONTEXA_PREVIOUS_TAG.TrimStart('v')) { throw 'rollback mismatch' } | |
| $env:CONTEXA_VERSION = $env:CONTEXA_PHASE_TAG | |
| & .\install.ps1 | |
| $fixture = Join-Path $env:RUNNER_TEMP 'contexa-installer-smoke' | |
| New-Item -ItemType Directory -Force -Path (Join-Path $fixture 'src\main\resources') | Out-Null | |
| New-Item -ItemType Directory -Force -Path (Join-Path $fixture 'src\main\java\example') | Out-Null | |
| $utf8 = New-Object System.Text.UTF8Encoding($false) | |
| [IO.File]::WriteAllText((Join-Path $fixture 'build.gradle'), (@( | |
| "plugins { id 'org.springframework.boot' version '3.3.0' }", | |
| 'repositories { mavenCentral() }', | |
| "dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' }" | |
| ) -join "`n") + "`n", $utf8) | |
| [IO.File]::WriteAllText((Join-Path $fixture 'settings.gradle'), "rootProject.name = 'installer-smoke'`n", $utf8) | |
| [IO.File]::WriteAllText((Join-Path $fixture 'src\main\resources\application.yml'), "server:`n port: 9080`n", $utf8) | |
| [IO.File]::WriteAllText((Join-Path $fixture 'src\main\java\example\InstallerSmokeApplication.java'), (@( | |
| 'package example;', | |
| 'import org.springframework.boot.autoconfigure.SpringBootApplication;', | |
| '@SpringBootApplication', | |
| 'public class InstallerSmokeApplication {}' | |
| ) -join "`n") + "`n", $utf8) | |
| & $binary init --yes --no-docker --dir $fixture | |
| & $binary reset --yes --dir $fixture | |
| if (Test-Path -LiteralPath (Join-Path $fixture 'contexa\manifest.json')) { throw 'reset left manifest behind' } | |
| $env:CONTEXA_INSTALL_ACTION = 'uninstall' | |
| & .\install.ps1 | |
| if (Test-Path -LiteralPath $binary) { throw 'uninstall left the final binary behind' } | |
| if (Test-Path -LiteralPath ($binary + '.previous')) { throw 'uninstall left the previous binary behind' } |