fix(ci): add jsonschema, ephemeral GPG key for ledger tests #14
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
| # .github/workflows/build.yml | |
| name: Build | |
| on: | |
| pull_request: | |
| branches: | |
| - if/main | |
| - if/staging | |
| paths-ignore: | |
| - "docs/**" | |
| - "*.md" | |
| push: | |
| branches: | |
| - if/main | |
| - if/staging | |
| workflow_dispatch: | |
| concurrency: | |
| group: build-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| checks: write | |
| id-token: write | |
| env: | |
| IF_REPO_ROOT: ${{ github.workspace }} | |
| jobs: | |
| # --------------------------------------------------------------------------- | |
| # lint-tools: Python tooling quality gate (ubuntu runner) | |
| # --------------------------------------------------------------------------- | |
| lint-tools: | |
| name: Lint Python tools | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Install lint tools | |
| # jsonschema is required by tools/check-config-schema.py for full | |
| # draft 2020-12 validation (without it the validator falls back to a | |
| # type-only stdlib check that misses enum constraints, so e.g. | |
| # schema_version=99 would be incorrectly accepted). | |
| run: pip install ruff mypy pytest pyyaml types-PyYAML jsonschema | |
| - name: Set up ephemeral GPG signing key for ledger tests | |
| # tools/ledger-event.py fails closed in CI environments when git | |
| # commit signing fails (HIGH-2 fix). Tests that exercise the real | |
| # ledger-event subprocess therefore need a GPG key on the runner. | |
| # Generate an ephemeral throw-away key valid for one day; it never | |
| # leaves the runner and is overwritten on every CI run. | |
| run: | | |
| set -euo pipefail | |
| cat > /tmp/gpg-batch <<'EOF' | |
| %no-protection | |
| Key-Type: ed25519 | |
| Key-Curve: ed25519 | |
| Name-Real: CI Test | |
| Name-Email: ci-test@example.com | |
| Expire-Date: 1 | |
| %commit | |
| EOF | |
| gpg --batch --gen-key /tmp/gpg-batch | |
| rm /tmp/gpg-batch | |
| KEY_ID=$(gpg --list-secret-keys --keyid-format=long ci-test@example.com | awk '/^sec/ {split($2,a,"/"); print a[2]; exit}') | |
| echo "GPG key id: $KEY_ID" | |
| git config --global user.name "CI Test" | |
| git config --global user.email "ci-test@example.com" | |
| git config --global user.signingkey "$KEY_ID" | |
| git config --global commit.gpgsign true | |
| - name: ruff check | |
| run: ruff check tools/ tests/ | |
| # mypy --strict applies to tools/ only (per pyproject.toml | |
| # [tool.mypy] files=["tools/*.py"]). Test code is intentionally not | |
| # type-strict — tests use yaml.safe_load() returns and parametrize | |
| # with untyped values where strict typing would add noise without | |
| # catching real bugs. | |
| - name: mypy --strict (tools) | |
| run: mypy --strict tools/ | |
| - name: pytest tools unit tests | |
| run: pytest tests/ -v --tb=short | |
| # --------------------------------------------------------------------------- | |
| # lint-yaml: YAML / actionlint gate (ubuntu runner) | |
| # --------------------------------------------------------------------------- | |
| lint-yaml: | |
| name: Lint YAML / actionlint | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python (for PyYAML parse check) | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Install PyYAML | |
| run: pip install pyyaml | |
| - name: Validate all workflow YAML parses | |
| run: | | |
| python - <<'EOF' | |
| import sys, glob, yaml | |
| errors = [] | |
| for path in glob.glob(".github/workflows/*.yml"): | |
| try: | |
| with open(path) as f: | |
| yaml.safe_load(f) | |
| print(f"OK: {path}") | |
| except yaml.YAMLError as e: | |
| errors.append(f"FAIL: {path}: {e}") | |
| if errors: | |
| for err in errors: | |
| print(err, file=sys.stderr) | |
| sys.exit(1) | |
| EOF | |
| - name: actionlint | |
| continue-on-error: true | |
| run: | | |
| if command -v actionlint &>/dev/null; then | |
| actionlint .github/workflows/*.yml | |
| else | |
| echo "actionlint not installed; skipping (install from https://github.com/rhysd/actionlint)" | |
| fi | |
| # --------------------------------------------------------------------------- | |
| # build-wpf: Windows matrix build (x64 + arm64) | |
| # --------------------------------------------------------------------------- | |
| build-wpf: | |
| name: Build WPF (${{ matrix.arch }}) | |
| runs-on: windows-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| arch: [x64, arm64] | |
| # arm64 cross-compile runs on windows-latest x64 runner; runs-on already pins to Windows. | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Cache NuGet packages | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.nuget/packages | |
| key: nuget-win-${{ matrix.arch }}-${{ hashFiles('**/packages.lock.json', 'global.json') }} | |
| restore-keys: nuget-win-${{ matrix.arch }}- | |
| - name: Set up .NET SDK | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| global-json-file: global.json | |
| - name: Install Strawberry Perl (idempotent) | |
| # windows-latest pre-installs strawberryperl (currently v5.42+). | |
| # Skip choco install if perl is already on PATH; otherwise install. | |
| run: | | |
| if (Get-Command perl -ErrorAction SilentlyContinue) { | |
| Write-Host "Strawberry Perl already installed: $(perl --version | Select-String -Pattern 'This is perl')" | |
| } else { | |
| choco install strawberryperl -y --no-progress | |
| } | |
| shell: powershell | |
| # Placeholder: upstream wpf submodule / dotnet/wpf source checkout. | |
| # TODO (wired by packaging beads): add real submodule reference or | |
| # sparse-checkout of upstream source tree. | |
| - name: Checkout upstream WPF source (placeholder) | |
| run: | | |
| echo "Upstream dotnet/wpf source will be wired by packaging beads." | |
| echo "Placeholder: no-op in this bead." | |
| shell: bash | |
| # Placeholder: build InitialForce.WPF packaging project. | |
| # TODO (wired by packaging beads): replace paths below with real project paths. | |
| - name: Build InitialForce.WPF packaging projects (placeholder) | |
| run: | | |
| echo "Build step placeholder — packaging project paths wired by other beads." | |
| echo "Expected commands:" | |
| echo " dotnet build packaging/InitialForce.WPF/InitialForce.WPF.csproj -c Release /p:Platform=${{ matrix.arch }}" | |
| echo " dotnet build packaging/InitialForce.WPF.RuntimeOverride/InitialForce.WPF.RuntimeOverride.csproj -c Release /p:Platform=${{ matrix.arch }}" | |
| shell: bash | |
| - name: Validate ledger schema | |
| run: python tools/ledger-validate.py --ledger .if-fork/patch-ledger.jsonl | |
| shell: bash | |
| # Phase-0 placeholder: build step doesn't yet produce real nupkgs. | |
| # Emit a sentinel marker so downstream smoke/perf jobs (which gate on | |
| # this artifact existing) can run their own placeholder steps end-to-end. | |
| # Once real packaging beads land, this block is replaced by the real | |
| # nupkg upload above. | |
| - name: Emit placeholder build marker | |
| run: | | |
| mkdir -p artifacts/packages | |
| echo "phase-0 placeholder build for ${{ matrix.arch }} @ ${{ github.sha }}" > artifacts/packages/PLACEHOLDER-${{ matrix.arch }}.txt | |
| shell: bash | |
| - name: Upload build artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: build-${{ matrix.arch }} | |
| path: | | |
| artifacts/packages/**/*.nupkg | |
| artifacts/packages/**/*.snupkg | |
| artifacts/packages/PLACEHOLDER-*.txt | |
| if-no-files-found: error | |
| retention-days: 14 | |
| - name: Upload binary logs on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: binlogs-${{ matrix.arch }} | |
| path: artifacts/log/**/*.binlog | |
| retention-days: 7 | |
| # --------------------------------------------------------------------------- | |
| # smoke: 22-scenario smoke harness (needs build-wpf) | |
| # --------------------------------------------------------------------------- | |
| smoke: | |
| name: Smoke (${{ matrix.arch }}) | |
| runs-on: windows-latest | |
| needs: build-wpf | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| arch: [x64, arm64] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: build-${{ matrix.arch }} | |
| path: artifacts/packages/ | |
| - name: Set up .NET SDK | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| global-json-file: global.json | |
| # Placeholder: run 22-scenario smoke harness. | |
| # TODO (wired by smoke-harness beads): replace with real test invocation. | |
| - name: Run smoke harness (placeholder) | |
| run: | | |
| echo "Smoke harness placeholder — test/InitialForce.WpfSmoke/ wired by other beads." | |
| echo "Expected command:" | |
| echo " dotnet test test/InitialForce.WpfSmoke/ -c Release --logger trx --results-directory smoke-results/" | |
| mkdir -p smoke-results | |
| echo '<?xml version="1.0" encoding="UTF-8"?><TestRun id="placeholder"><Results/></TestRun>' > smoke-results/smoke-placeholder.xml | |
| shell: bash | |
| - name: Upload smoke results | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: smoke-${{ matrix.arch }}.xml | |
| path: smoke-results/ | |
| retention-days: 14 | |
| # --------------------------------------------------------------------------- | |
| # perf: BenchmarkDotNet perf gate (needs build-wpf) | |
| # --------------------------------------------------------------------------- | |
| perf: | |
| name: Perf (${{ matrix.arch }}) | |
| runs-on: windows-latest | |
| needs: build-wpf | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| arch: [x64, arm64] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: build-${{ matrix.arch }} | |
| path: artifacts/packages/ | |
| - name: Set up .NET SDK | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| global-json-file: global.json | |
| # Placeholder: run BenchmarkDotNet benchmarks. | |
| # TODO (wired by perf beads): replace with real benchmark invocation. | |
| - name: Run BenchmarkDotNet (placeholder) | |
| run: | | |
| echo "BenchmarkDotNet placeholder — wired by other beads." | |
| echo "Expected command:" | |
| echo " dotnet run --project test/InitialForce.WpfSmoke/ -c Release -- --filter '*Bench*' --exporters json" | |
| echo " python tools/check-regression.py --current-sha ${{ github.sha }} --series perf/series.jsonl" | |
| shell: bash | |
| - name: Upload perf results | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: perf-${{ matrix.arch }} | |
| path: BenchmarkDotNet.Artifacts/ | |
| if-no-files-found: warn | |
| retention-days: 14 | |
| # --------------------------------------------------------------------------- | |
| # aggregate: diff smoke results, final gate (needs all prior jobs) | |
| # --------------------------------------------------------------------------- | |
| aggregate: | |
| name: Aggregate results | |
| runs-on: ubuntu-latest | |
| needs: [lint-tools, lint-yaml, smoke, perf] | |
| if: always() | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Install Python deps | |
| run: pip install pyyaml | |
| - name: Download smoke x64 results | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: smoke-x64.xml | |
| path: /tmp/smoke-results/x64/ | |
| continue-on-error: true | |
| - name: Download smoke arm64 results | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: smoke-arm64.xml | |
| path: /tmp/smoke-results/arm64/ | |
| continue-on-error: true | |
| - name: Download baseline smoke artifact (if available) | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: smoke-baseline | |
| path: /tmp/smoke-baseline/ | |
| continue-on-error: true | |
| - name: Diff smoke results (if baseline available) | |
| run: | | |
| if [ -d /tmp/smoke-baseline ] && [ "$(ls -A /tmp/smoke-baseline)" ]; then | |
| python tools/diff-smoke-results.py \ | |
| --upstream /tmp/smoke-baseline/ \ | |
| --fork /tmp/smoke-results/x64/ \ | |
| --output /tmp/smoke-diff-report.md | |
| echo "Smoke diff report:" | |
| cat /tmp/smoke-diff-report.md | |
| else | |
| echo "No baseline artifact found; skipping diff (first run or baseline not published)." | |
| fi | |
| shell: bash | |
| - name: Check all required jobs passed | |
| run: | | |
| LINT_TOOLS="${{ needs.lint-tools.result }}" | |
| LINT_YAML="${{ needs.lint-yaml.result }}" | |
| SMOKE="${{ needs.smoke.result }}" | |
| PERF="${{ needs.perf.result }}" | |
| echo "lint-tools: $LINT_TOOLS" | |
| echo "lint-yaml: $LINT_YAML" | |
| echo "smoke: $SMOKE" | |
| echo "perf: $PERF" | |
| FAILED=0 | |
| for result in "$LINT_TOOLS" "$LINT_YAML" "$SMOKE" "$PERF"; do | |
| if [ "$result" != "success" ] && [ "$result" != "skipped" ]; then | |
| FAILED=1 | |
| fi | |
| done | |
| if [ "$FAILED" -eq 1 ]; then | |
| echo "::error::One or more required jobs failed. See details above." | |
| exit 1 | |
| fi | |
| echo "All required jobs passed." | |
| shell: bash |