From 7fd8cadab9d0174fd6633f5eb0a79aba86138d9d Mon Sep 17 00:00:00 2001 From: Jake Sanders Date: Wed, 5 Aug 2026 11:59:43 -0700 Subject: [PATCH 1/3] ci: add presubmit GitHub Actions workflow Lints bash (shellcheck + bash -n), PowerShell (parser + PSScriptAnalyzer), and data files (JSON/TOML/mobileconfig), lints the workflows themselves with actionlint, smoke-tests both package-firewall generators with dummy credentials, and runs the package-firewall test suites on ubuntu/macos/ windows once package-firewall/tests/ lands on a branch. Also fixes the one shellcheck finding (SC1007 in render.sh) so the lint gate starts green. --- .github/workflows/ci.yml | 261 +++++++++++++++++++++++++++++ agent-governance/scripts/render.sh | 2 +- 2 files changed, 262 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0b33fc2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,261 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +defaults: + run: + shell: bash + +jobs: + lint-bash: + name: Lint bash + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # Hand-written scripts get full shellcheck. Generated examples under out/ + # are excluded: they intentionally define config blocks that not every + # script consumes (SC2034 by design) and are covered by bash -n below. + - name: shellcheck (hand-written scripts) + run: | + set -uo pipefail + fail=0 + while IFS= read -r -d '' f; do + args=(--severity=warning) + # Library/template fragments have no shebang; tell shellcheck the dialect. + if ! head -n1 "$f" | grep -q '^#!'; then + args+=(--shell=bash) + fi + if shellcheck "${args[@]}" "$f"; then + echo "ok: $f" + else + fail=1 + fi + done < <(find . -name '*.sh' -not -path './.git/*' -not -path '*/out/*' -print0) + exit "$fail" + + - name: bash -n (all shell files, incl. generated examples) + run: | + set -uo pipefail + fail=0 + while IFS= read -r -d '' f; do + if bash -n "$f"; then + echo "ok: $f" + else + fail=1 + fi + done < <(find . -name '*.sh' -not -path './.git/*' -print0) + exit "$fail" + + lint-powershell: + name: Lint PowerShell + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Parse-check all PowerShell files + shell: pwsh + run: | + $bad = 0 + Get-ChildItem -Recurse -Filter *.ps1 | ForEach-Object { + $tokens = $null; $errors = $null + [System.Management.Automation.Language.Parser]::ParseFile($_.FullName, [ref]$tokens, [ref]$errors) | Out-Null + if ($errors.Count -gt 0) { + $bad++ + foreach ($e in $errors) { Write-Host "PARSE ERROR: $($_.FullName): $($e.Message)" } + } else { + Write-Host "ok: $($_.FullName)" + } + } + if ($bad -gt 0) { Write-Error "$bad file(s) with parse errors" } + + # Gate on Error severity only; warnings are printed but non-blocking + # (generated-script templates trip stylistic rules by design). + - name: PSScriptAnalyzer (hand-written scripts) + shell: pwsh + run: | + if (-not (Get-Module -ListAvailable -Name PSScriptAnalyzer)) { + Install-Module PSScriptAnalyzer -Force -Scope CurrentUser + } + $paths = @( + 'package-firewall/powershell/generate.ps1', + 'package-firewall/powershell/lib', + 'package-firewall/powershell/templates', + 'agent-governance/scripts' + ) + $results = @() + foreach ($p in $paths) { + $results += Invoke-ScriptAnalyzer -Path $p -Recurse -Severity @('Error', 'Warning') + } + $warnings = @($results | Where-Object Severity -eq 'Warning') + $errors = @($results | Where-Object Severity -eq 'Error') + if ($warnings.Count -gt 0) { + Write-Host "PSScriptAnalyzer warnings ($($warnings.Count), non-blocking):" + $warnings | Format-Table RuleName, ScriptName, Line, Message -AutoSize | Out-String -Width 200 | Write-Host + } + if ($errors.Count -gt 0) { + $errors | Format-Table RuleName, ScriptName, Line, Message -AutoSize | Out-String -Width 200 | Write-Host + Write-Error "PSScriptAnalyzer found $($errors.Count) error(s)" + } + Write-Host 'PSScriptAnalyzer: no errors.' + + validate-data: + name: Validate JSON / TOML / mobileconfig + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Validate JSON + run: | + set -uo pipefail + fail=0 + while IFS= read -r -d '' f; do + if jq empty "$f"; then + echo "ok: $f" + else + echo "INVALID JSON: $f" + fail=1 + fi + done < <(find . -name '*.json' -not -path './.git/*' -print0) + exit "$fail" + + - name: Validate TOML + run: | + set -uo pipefail + fail=0 + while IFS= read -r -d '' f; do + if python3 -c 'import sys, tomllib; tomllib.load(open(sys.argv[1], "rb"))' "$f"; then + echo "ok: $f" + else + echo "INVALID TOML: $f" + fail=1 + fi + done < <(find . -name '*.toml' -not -path './.git/*' -print0) + exit "$fail" + + - name: Validate mobileconfig (XML plists) + run: | + set -uo pipefail + if ! command -v xmllint >/dev/null 2>&1; then + sudo apt-get update -qq + sudo apt-get install -y -qq libxml2-utils + fi + fail=0 + while IFS= read -r -d '' f; do + if xmllint --noout "$f"; then + echo "ok: $f" + else + echo "INVALID XML: $f" + fail=1 + fi + done < <(find . -name '*.mobileconfig' -not -path './.git/*' -print0) + exit "$fail" + + lint-workflows: + name: Lint workflows + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Run actionlint + run: | + bash <(curl -sSfL https://raw.githubusercontent.com/rhysd/actionlint/v1.7.7/scripts/download-actionlint.bash) 1.7.7 + ./actionlint -color + + generator-smoke: + name: Generator smoke test + runs-on: ubuntu-latest + env: + ENDOR_NAMESPACE: ci-smoke + ENDOR_API_KEY_ID: ci-smoke-key-id + ENDOR_API_SECRET: ci-smoke-secret + steps: + - uses: actions/checkout@v4 + + - name: Generate bash scripts and syntax-check output + run: | + set -uo pipefail + bash package-firewall/bash/generate.sh + shopt -s nullglob + files=(package-firewall/bash/out/ci-smoke/*.sh) + if [ "${#files[@]}" -lt 6 ]; then + echo "expected at least 6 generated scripts, got ${#files[@]}" + exit 1 + fi + for f in "${files[@]}"; do + bash -n "$f" + echo "ok: $f" + done + + - name: Generate PowerShell scripts and parse-check output + shell: pwsh + run: | + & ./package-firewall/powershell/generate.ps1 + if ($LASTEXITCODE -ne 0) { Write-Error "generate.ps1 exited with $LASTEXITCODE" } + # On non-Windows the generator writes to a directory literally named + # 'out\ci-smoke', so match on the namespace rather than the path. + $files = @(Get-ChildItem -Path package-firewall/powershell -Recurse -Filter *.ps1 | + Where-Object { $_.FullName -like '*ci-smoke*' }) + if ($files.Count -lt 6) { Write-Error "expected at least 6 generated scripts, got $($files.Count)" } + $bad = 0 + foreach ($f in $files) { + $tokens = $null; $errors = $null + [System.Management.Automation.Language.Parser]::ParseFile($f.FullName, [ref]$tokens, [ref]$errors) | Out-Null + if ($errors.Count -gt 0) { + $bad++ + Write-Host "PARSE ERROR: $($f.FullName): $($errors[0].Message)" + } else { + Write-Host "ok: $($f.FullName)" + } + } + if ($bad -gt 0) { Write-Error "$bad generated file(s) with parse errors" } + + # The package-firewall test suites live in package-firewall/tests/ (currently + # on an unmerged branch). These jobs run them once the directory exists and + # log a skip notice until then. + tests: + name: Tests (${{ matrix.os }}) + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - name: Run package-firewall test suites + run: | + set -uo pipefail + if [ -d package-firewall/tests ]; then + cd package-firewall/tests || exit 1 + bash ./run-all.sh + else + echo "No package-firewall/tests directory on this branch yet - skipping." + fi + + tests-windows: + name: Tests (windows-latest) + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - name: Run package-firewall PowerShell test suites + shell: pwsh + run: | + if (Test-Path 'package-firewall/tests/run-all.ps1') { + Set-Location package-firewall/tests + ./run-all.ps1 + exit $LASTEXITCODE + } else { + Write-Host 'No package-firewall/tests directory on this branch yet - skipping.' + } diff --git a/agent-governance/scripts/render.sh b/agent-governance/scripts/render.sh index 117b3df..643dfab 100755 --- a/agent-governance/scripts/render.sh +++ b/agent-governance/scripts/render.sh @@ -37,7 +37,7 @@ # render.sh --agent claude --target-os windows --api-key K --api-secret S --namespace NS set -eu -SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +SCRIPT_DIR=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) DEFAULT_API_URL="https://api.endorlabs.com" die() { echo "render.sh: error: $*" >&2; exit 1; } From 5043da655875841fa7284957d1bad5b37b320707 Mon Sep 17 00:00:00 2001 From: Jake Sanders Date: Wed, 5 Aug 2026 12:03:36 -0700 Subject: [PATCH 2/3] ci: pin actions/checkout to commit SHA (org policy requires full-SHA pins) --- .github/workflows/ci.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b33fc2..48cb7c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: name: Lint bash runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 # Hand-written scripts get full shellcheck. Generated examples under out/ # are excluded: they intentionally define config blocks that not every @@ -61,7 +61,7 @@ jobs: name: Lint PowerShell runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Parse-check all PowerShell files shell: pwsh @@ -113,7 +113,7 @@ jobs: name: Validate JSON / TOML / mobileconfig runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Validate JSON run: | @@ -165,7 +165,7 @@ jobs: name: Lint workflows runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Run actionlint run: | @@ -180,7 +180,7 @@ jobs: ENDOR_API_KEY_ID: ci-smoke-key-id ENDOR_API_SECRET: ci-smoke-secret steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Generate bash scripts and syntax-check output run: | @@ -231,7 +231,7 @@ jobs: os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Run package-firewall test suites run: | @@ -247,7 +247,7 @@ jobs: name: Tests (windows-latest) runs-on: windows-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Run package-firewall PowerShell test suites shell: pwsh From 6b3b6131d98075e627e72b21c7cfc798513e3065 Mon Sep 17 00:00:00 2001 From: Jake Sanders Date: Wed, 5 Aug 2026 12:06:30 -0700 Subject: [PATCH 3/3] ci: drop bogus LASTEXITCODE check after in-process generate.ps1 invocation --- .github/workflows/ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 48cb7c1..7b40bed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -200,10 +200,11 @@ jobs: - name: Generate PowerShell scripts and parse-check output shell: pwsh run: | + # Runs in-process; generate.ps1 sets ErrorActionPreference=Stop and + # exits non-zero on validation failure, either of which fails the step. & ./package-firewall/powershell/generate.ps1 - if ($LASTEXITCODE -ne 0) { Write-Error "generate.ps1 exited with $LASTEXITCODE" } - # On non-Windows the generator writes to a directory literally named - # 'out\ci-smoke', so match on the namespace rather than the path. + # Match on the namespace rather than a fixed path in case path + # separators differ across platforms. $files = @(Get-ChildItem -Path package-firewall/powershell -Recurse -Filter *.ps1 | Where-Object { $_.FullName -like '*ci-smoke*' }) if ($files.Count -lt 6) { Write-Error "expected at least 6 generated scripts, got $($files.Count)" }