Skip to content

fix(ci): add jsonschema, ephemeral GPG key for ledger tests #14

fix(ci): add jsonschema, ephemeral GPG key for ledger tests

fix(ci): add jsonschema, ephemeral GPG key for ledger tests #14

Workflow file for this run

# .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