Bump OMG Pilot #84
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: PR | |
| # Pull requests only: pushes and tags are covered by the CircleCI config, and | |
| # this workflow deliberately mirrors only its build-test workflow, not release. | |
| on: | |
| pull_request: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: pr-${{ github.event.pull_request.number }} | |
| cancel-in-progress: true | |
| env: | |
| # The OMG training corpus is not vendored. Requiring it here turns "corpus | |
| # absent" into a test failure instead of a skip, so the gate cannot pass green | |
| # without actually running (see internal/core/model/training_examples_test.go). | |
| OPENSYSML_REQUIRE_TRAINING_CORPUS: "1" | |
| # Same for the three pinned OMG pilot corpora, which are downloaded below and | |
| # gated by internal/core/model/pilot_corpora_test.go. | |
| OPENSYSML_REQUIRE_PILOT_CORPORA: "1" | |
| # z3 is installed below, so "no solver, therefore skip" would exercise nothing: | |
| # this turns an absent solver into a failure (see internal/core/solve). | |
| OPENSYSML_REQUIRE_SMT: "1" | |
| jobs: | |
| # Which areas the pull request touches. A path no area claims turns every area | |
| # on, so a new directory is over-tested rather than untested. | |
| changes: | |
| name: Changed areas | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| outputs: | |
| go: ${{ steps.areas.outputs.go }} | |
| docs: ${{ steps.areas.outputs.docs }} | |
| node: ${{ steps.areas.outputs.node }} | |
| python: ${{ steps.areas.outputs.python }} | |
| java: ${{ steps.areas.outputs.java }} | |
| rust: ${{ steps.areas.outputs.rust }} | |
| vscode: ${{ steps.areas.outputs.vscode }} | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Test the area filter | |
| run: scripts/ci-changed-areas-test.sh | |
| - name: Decide which areas to run | |
| id: areas | |
| env: | |
| BASE_REF: ${{ github.base_ref }} | |
| run: scripts/ci-changed-areas.sh "origin/$BASE_REF" HEAD | tee -a "$GITHUB_OUTPUT" | |
| race-tests: | |
| name: Go race tests | |
| needs: changes | |
| if: needs.changes.outputs.go == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 40 | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Go | |
| uses: actions/setup-go@v5 | |
| with: | |
| go-version-file: go.mod | |
| cache: true | |
| - name: Download Go modules | |
| run: go mod download | |
| # Pinned by digest, so a substituted asset is not run. The release binary rather | |
| # than `go run` keeps buf's own module tree out of the checksum-database path. | |
| - name: Install buf | |
| run: | | |
| curl -fsSL -o /tmp/buf \ | |
| https://github.com/bufbuild/buf/releases/download/v1.57.2/buf-Linux-x86_64 | |
| echo "644e6fa4a62afc2b0d425d3ac9538d2f512947c0f2e82fc95c41a5eab6729414 /tmp/buf" | sha256sum --check --strict | |
| sudo install -m 0755 /tmp/buf /usr/local/bin/buf | |
| buf --version | |
| # One buf template generates both the Go stubs and the Java client's message | |
| # classes, so without this check the committed stubs can drift from the schema. | |
| - name: Verify the committed Go and Java stubs are current | |
| run: | | |
| make proto-buf BUF=buf | |
| git diff --exit-code api/proto \ | |
| clients/java/opensysml-client/src/main/java/io/opensysml/proto | |
| # The solver is an external process, never linked in: the solver-dependent | |
| # tests need one on PATH to run rather than skip. | |
| - name: Install Z3 | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y z3 | |
| z3 --version | |
| # The second verified backend, for the portability gate below: cvc5 has no | |
| # apt package, so the official static release is what CI runs, pinned by | |
| # digest so a substituted asset is not run. | |
| - name: Install cvc5 | |
| run: | | |
| curl -fsSL -o /tmp/cvc5.zip \ | |
| https://github.com/cvc5/cvc5/releases/download/cvc5-1.3.4/cvc5-Linux-x86_64-static.zip | |
| echo "dcdbfada0ce493ee98259c0816e0daafc561c223aadb3af298c2968e73ea39c6 /tmp/cvc5.zip" | sha256sum --check --strict | |
| unzip -p /tmp/cvc5.zip '*/bin/cvc5' > /tmp/cvc5-bin | |
| sudo install -m 0755 /tmp/cvc5-bin /usr/local/bin/cvc5 | |
| cvc5 --version | head -1 | |
| # Keyed on the download script and the shared pilot pin, so a change to the | |
| # pinned tag or the layout invalidates the cache. A restored cache is still | |
| # verified below. | |
| - name: Cache the OMG training corpus | |
| uses: actions/cache@v4 | |
| with: | |
| path: examples/sysml-v2-training | |
| key: training-corpus-${{ hashFiles('scripts/download-training-examples.sh', 'scripts/pilot-pin.sh') }} | |
| - name: Download the OMG training corpus | |
| run: ./scripts/download-training-examples.sh | |
| - name: Verify the OMG training corpus | |
| run: | | |
| count=$(find examples/sysml-v2-training -name '*.sysml' | wc -l) | |
| echo "training corpus: $count .sysml files" | |
| if [ "$count" -eq 0 ]; then | |
| echo "error: the training corpus is empty; the corpus gate would not run" >&2 | |
| exit 1 | |
| fi | |
| # Same keying as the training corpus: the download script plus the shared | |
| # pilot pin. A restored cache is verified below rather than trusted. | |
| - name: Cache the OMG pilot corpora | |
| uses: actions/cache@v4 | |
| with: | |
| path: examples/pilot-corpora | |
| key: pilot-corpora-${{ hashFiles('scripts/download-pilot-corpora.sh', 'scripts/pilot-pin.sh') }} | |
| - name: Download the OMG pilot corpora | |
| run: ./scripts/download-pilot-corpora.sh | |
| - name: Verify the OMG pilot corpora | |
| run: | | |
| for root in sysml-examples sysml-validation kerml-examples; do | |
| count=$(find "examples/pilot-corpora/$root" -type f \( -name '*.sysml' -o -name '*.kerml' \) | wc -l) | |
| echo "pilot corpus $root: $count model files" | |
| if [ "$count" -eq 0 ]; then | |
| echo "error: pilot corpus $root is empty; the corpora gate would not run" >&2 | |
| exit 1 | |
| fi | |
| done | |
| # Per-package timeout: under -race, passes and model run within 1% of go's 10m | |
| # default. Matches `make test`. | |
| - name: Run Go race tests | |
| run: make test | |
| - name: Upload coverage artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: coverage | |
| path: coverage.txt | |
| if-no-files-found: error | |
| static-and-integrity: | |
| name: Go static and integrity checks | |
| needs: changes | |
| if: needs.changes.outputs.go == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Go | |
| uses: actions/setup-go@v5 | |
| with: | |
| go-version-file: go.mod | |
| cache: true | |
| - name: Download Go modules | |
| run: go mod download | |
| - name: Install Z3 | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y z3 | |
| z3 --version | |
| - name: Install cvc5 | |
| run: | | |
| curl -fsSL -o /tmp/cvc5.zip \ | |
| https://github.com/cvc5/cvc5/releases/download/cvc5-1.3.4/cvc5-Linux-x86_64-static.zip | |
| echo "dcdbfada0ce493ee98259c0816e0daafc561c223aadb3af298c2968e73ea39c6 /tmp/cvc5.zip" | sha256sum --check --strict | |
| unzip -p /tmp/cvc5.zip '*/bin/cvc5' > /tmp/cvc5-bin | |
| sudo install -m 0755 /tmp/cvc5-bin /usr/local/bin/cvc5 | |
| cvc5 --version | head -1 | |
| - name: Cache the OMG training corpus | |
| uses: actions/cache@v4 | |
| with: | |
| path: examples/sysml-v2-training | |
| key: training-corpus-${{ hashFiles('scripts/download-training-examples.sh', 'scripts/pilot-pin.sh') }} | |
| - name: Download the OMG training corpus | |
| run: ./scripts/download-training-examples.sh | |
| - name: Verify the OMG training corpus | |
| run: | | |
| count=$(find examples/sysml-v2-training -name '*.sysml' | wc -l) | |
| echo "training corpus: $count .sysml files" | |
| if [ "$count" -eq 0 ]; then | |
| echo "error: the training corpus is empty; the corpus gate would not run" >&2 | |
| exit 1 | |
| fi | |
| - name: Cache the OMG pilot corpora | |
| uses: actions/cache@v4 | |
| with: | |
| path: examples/pilot-corpora | |
| key: pilot-corpora-${{ hashFiles('scripts/download-pilot-corpora.sh', 'scripts/pilot-pin.sh') }} | |
| - name: Download the OMG pilot corpora | |
| run: ./scripts/download-pilot-corpora.sh | |
| - name: Verify the OMG pilot corpora | |
| run: | | |
| for root in sysml-examples sysml-validation kerml-examples; do | |
| count=$(find "examples/pilot-corpora/$root" -type f \( -name '*.sysml' -o -name '*.kerml' \) | wc -l) | |
| echo "pilot corpus $root: $count model files" | |
| if [ "$count" -eq 0 ]; then | |
| echo "error: pilot corpus $root is empty; the corpora gate would not run" >&2 | |
| exit 1 | |
| fi | |
| done | |
| - name: Check Go formatting | |
| run: | | |
| unformatted=$(gofmt -l .) | |
| if [ -n "$unformatted" ]; then | |
| echo "The following files are not gofmt'd:" | |
| echo "$unformatted" | |
| exit 1 | |
| fi | |
| - name: Run Go vet | |
| run: go vet ./... | |
| - name: Run static analysis (staticcheck + gosec) | |
| run: make lint | |
| - name: Check grpc-go stays out of production code | |
| run: ./scripts/check-grpc-imports.sh | |
| # Re-run the corpus gate on its own so its verdict is legible in the log | |
| # and a skip is impossible to miss. TestCorpusGates is the shared | |
| # cache-independence case over all four OMG roots, which skips with them. | |
| - name: Run training corpus gate | |
| run: | | |
| set -o pipefail | |
| go test -count=1 -v ./internal/core/model -run 'TestTrainingExamples|TestCorpusGates' | tee corpus-gate.log | |
| grep -E 'training files clean' corpus-gate.log | |
| if grep -qE '^\s*--- SKIP' corpus-gate.log; then | |
| echo "error: the corpus gate skipped" >&2 | |
| exit 1 | |
| fi | |
| # The pilot corpora gate on its own, for the same reason: its per-root | |
| # verdict lines are the reviewable output, and a skip must not pass. | |
| # It gates our own diagnostics only; the comparison against the reference | |
| # implementation (cmd/pilot-diff) needs Java validators and is not run here. | |
| - name: Run pilot corpora gate | |
| run: | | |
| set -o pipefail | |
| go test -count=1 -v ./internal/core/model -run 'TestPilotCorpora' | tee pilot-corpora-gate.log | |
| grep -E 'pilot corpus files clean' pilot-corpora-gate.log | |
| if grep -qE '^\s*--- SKIP' pilot-corpora-gate.log; then | |
| echo "error: the pilot corpora gate skipped" >&2 | |
| exit 1 | |
| fi | |
| # Re-run the solver gate on its own, as the corpus gate is, so a skip | |
| # cannot hide behind a green suite. | |
| - name: Run solver gate (Z3) | |
| run: | | |
| set -o pipefail | |
| go test -count=1 -v ./internal/core/solve ./internal/repl -run 'TestSolver|TestDiscovery|TestSolved|TestDivisor|TestCheck|TestSolve|TestConfigure|TestSynthesis|TestPinned|TestFixed|TestOptimize|TestOptimum' | tee solver-gate.log | |
| if grep -qE '^\s*--- SKIP' solver-gate.log; then | |
| echo "error: a solver-dependent test skipped" >&2 | |
| exit 1 | |
| fi | |
| # The differential gate compares the solver against the normative | |
| # evaluator; its summary counts make coverage drift reviewable in the log. | |
| - name: Run solver differential gate | |
| run: | | |
| set -o pipefail | |
| go test -count=1 -v ./internal/core/solve -run TestDifferential | tee differential-gate.log | |
| grep -E 'differential gate .*: .* elements:' differential-gate.log | |
| if grep -qE '^\s*--- SKIP' differential-gate.log; then | |
| echo "error: a differential gate skipped" >&2 | |
| exit 1 | |
| fi | |
| # Portability: the same subset run against both verified backends, so a | |
| # feature one of them refuses is reported rather than discovered by a user. | |
| # OPENSYSML_REQUIRE_SMT makes an absent solver a failure, not a skip. | |
| - name: Run solver portability gate | |
| run: | | |
| set -o pipefail | |
| for solver in z3 cvc5; do | |
| OPENSYSML_SMT="$solver" go test -count=1 -v ./internal/core/solve \ | |
| -run 'TestPortability' | tee "portability-$solver.log" | |
| grep -E "portability of $solver" "portability-$solver.log" | |
| if grep -qE '^\s*--- SKIP' "portability-$solver.log"; then | |
| echo "error: the portability gate skipped for $solver" >&2 | |
| exit 1 | |
| fi | |
| done | |
| # The solver gate again against the second backend, not only the | |
| # portability subset: what z3 answers, cvc5 must answer too. | |
| - name: Run solver gate (cvc5) | |
| run: | | |
| set -o pipefail | |
| OPENSYSML_SMT=cvc5 go test -count=1 -v ./internal/core/solve ./internal/repl \ | |
| -run 'TestSolver|TestDiscovery|TestSolved|TestDivisor|TestCheck|TestSolve|TestConfigure|TestSynthesis|TestPinned|TestFixed|TestExplain|TestCore' \ | |
| | tee cvc5-gate.log | |
| if grep -qE '^\s*--- SKIP' cvc5-gate.log; then | |
| echo "error: a solver-dependent test skipped under cvc5" >&2 | |
| exit 1 | |
| fi | |
| # The other half of the contract: with no solver to be found, every | |
| # solver-dependent test must skip with a reason and the absent-solver paths | |
| # must still report one. OPENSYSML_SMT names an executable that does not | |
| # exist, which is discovery's absent case, and the requirement flag is | |
| # dropped so a skip is the expected outcome here. | |
| - name: Run solver-absent gate | |
| run: | | |
| set -o pipefail | |
| env -u OPENSYSML_REQUIRE_SMT OPENSYSML_SMT=/nonexistent/no-such-solver \ | |
| go test -count=1 -v ./internal/core/solve ./internal/repl | tee no-solver-gate.log | |
| for name in TestCheckReportsAnAbsentSolver TestExplainReportsAnAbsentSolver \ | |
| TestSolveReportsAnAbsentSolver TestConfigureReportsAnAbsentSolver \ | |
| TestOptimizeReportsAnAbsentSolver; do | |
| grep -qE "^\s*--- PASS: $name" no-solver-gate.log || { | |
| echo "error: $name did not run without a solver" >&2 | |
| exit 1 | |
| } | |
| done | |
| # The client jobs download this binary rather than building one, so it runs | |
| # whenever the service or any client does. | |
| build: | |
| name: Build binaries | |
| needs: changes | |
| if: | | |
| needs.changes.outputs.go == 'true' || | |
| needs.changes.outputs.node == 'true' || | |
| needs.changes.outputs.python == 'true' || | |
| needs.changes.outputs.java == 'true' || | |
| needs.changes.outputs.rust == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Go | |
| uses: actions/setup-go@v5 | |
| with: | |
| go-version-file: go.mod | |
| cache: true | |
| - name: Download Go modules | |
| run: go mod download | |
| - name: Build binaries | |
| run: | | |
| make build \ | |
| VERSION="pr-${{ github.event.pull_request.number }}" \ | |
| COMMIT="$(git rev-parse --short HEAD)" \ | |
| BUILD_TIME="$(date -u '+%Y-%m-%d_%H:%M:%S')" \ | |
| GO_VERSION="$(go version | awk '{print $3}')" | |
| - name: Verify binaries | |
| run: | | |
| ./bin/sysml --version | |
| ./bin/sysml-lsp --version | |
| ls -lh bin/ | |
| - name: Upload binary artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: binaries | |
| path: bin/ | |
| if-no-files-found: error | |
| # A gated job that did not run reports `skipped`, which this gate accepts; a job | |
| # skipped by a failed dependency is caught through that dependency's own result. | |
| build-and-test: | |
| name: Build and test | |
| if: always() | |
| needs: | |
| - changes | |
| - build | |
| - docs | |
| - python-test | |
| - race-tests | |
| - rust-test | |
| - static-and-integrity | |
| - vscode-extension | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Verify required checks | |
| env: | |
| CHANGES_RESULT: ${{ needs.changes.result }} | |
| BUILD_RESULT: ${{ needs.build.result }} | |
| DOCS_RESULT: ${{ needs.docs.result }} | |
| PYTHON_TEST_RESULT: ${{ needs.python-test.result }} | |
| RACE_TESTS_RESULT: ${{ needs.race-tests.result }} | |
| RUST_TEST_RESULT: ${{ needs.rust-test.result }} | |
| STATIC_AND_INTEGRITY_RESULT: ${{ needs.static-and-integrity.result }} | |
| VSCODE_EXTENSION_RESULT: ${{ needs.vscode-extension.result }} | |
| run: | | |
| ok() { [ "$1" = success ] || [ "$1" = skipped ]; } | |
| if [ "$CHANGES_RESULT" != success ] || | |
| ! ok "$BUILD_RESULT" || | |
| ! ok "$DOCS_RESULT" || | |
| ! ok "$PYTHON_TEST_RESULT" || | |
| ! ok "$RACE_TESTS_RESULT" || | |
| ! ok "$RUST_TEST_RESULT" || | |
| ! ok "$STATIC_AND_INTEGRITY_RESULT" || | |
| ! ok "$VSCODE_EXTENSION_RESULT"; then | |
| echo "changes=$CHANGES_RESULT" | |
| echo "build=$BUILD_RESULT" | |
| echo "docs=$DOCS_RESULT" | |
| echo "python-test=$PYTHON_TEST_RESULT" | |
| echo "race-tests=$RACE_TESTS_RESULT" | |
| echo "rust-test=$RUST_TEST_RESULT" | |
| echo "static-and-integrity=$STATIC_AND_INTEGRITY_RESULT" | |
| echo "vscode-extension=$VSCODE_EXTENSION_RESULT" | |
| exit 1 | |
| fi | |
| vscode-extension: | |
| name: VS Code extension | |
| needs: changes | |
| if: needs.changes.outputs.vscode == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| cache: npm | |
| cache-dependency-path: editors/vscode/package-lock.json | |
| # Type-check, bundle and package. The generated TextMate grammars are | |
| # gated by go test ./editors/... in the job above. | |
| - name: Package the VS Code extension | |
| run: make vscode-package | |
| - name: Upload VS Code extension artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: vscode-extension | |
| path: editors/vscode/opensysml-sysml.vsix | |
| if-no-files-found: error | |
| docs: | |
| name: Documentation site | |
| needs: changes | |
| if: needs.changes.outputs.docs == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.12' | |
| cache: pip | |
| cache-dependency-path: docs-requirements.txt | |
| - name: Install documentation toolchain | |
| run: make docs-install | |
| # --strict: a link or anchor a moved page left behind fails here rather than | |
| # publishing a 404 from main. | |
| - name: Build documentation site | |
| run: make docs | |
| node-test: | |
| name: Node client tests | |
| needs: [changes, build-and-test] | |
| if: needs.changes.outputs.node == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| name: binaries | |
| path: bin | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| cache: npm | |
| cache-dependency-path: clients/node/package-lock.json | |
| # The tests and the conformance runner start private children of this | |
| # binary; without it they would build one from the checkout, needing Go. | |
| - name: Install the sysml-grpc binary | |
| run: | | |
| chmod +x bin/sysml-grpc | |
| echo "OPENSYSML_BINARY=$(pwd)/bin/sysml-grpc" >> "$GITHUB_ENV" | |
| - name: Install the client's dependencies | |
| working-directory: clients/node | |
| run: npm ci | |
| - name: Build, typecheck and lint | |
| working-directory: clients/node | |
| run: | | |
| npm run build | |
| npm run typecheck | |
| npm run lint | |
| - name: Run the tests | |
| working-directory: clients/node | |
| run: npm test | |
| # --allow-skips because v1 covers 5 of the 15 RPCs; the skipped count is | |
| # reported and the artifact records which scenarios they were. | |
| - name: Run the conformance suite through the client | |
| working-directory: clients/node | |
| run: | | |
| npm run conformance -- --binary "$OPENSYSML_BINARY" --allow-skips \ | |
| --report node-conformance.json | |
| # A runner that passes against a deliberately broken client tests nothing. | |
| - name: Check the conformance runner catches a broken client | |
| working-directory: clients/node | |
| run: | | |
| for mutation in hide-capability drop-diagnostics blank-symbol-kind shift-integer drop-feature-values; do | |
| if npm run conformance -- --binary "$OPENSYSML_BINARY" --allow-skips \ | |
| --protocols connect --mutate "$mutation" > /dev/null 2>&1; then | |
| echo "error: the suite passed with the $mutation mutation applied" >&2 | |
| exit 1 | |
| fi | |
| echo "ok: $mutation is caught" | |
| done | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: node-conformance | |
| path: clients/node/node-conformance.json | |
| # Pinned by digest, as cvc5 is above, so a substituted asset is not run. | |
| - name: Install buf | |
| run: | | |
| curl -fsSL -o /tmp/buf \ | |
| https://github.com/bufbuild/buf/releases/download/v1.57.2/buf-Linux-x86_64 | |
| echo "644e6fa4a62afc2b0d425d3ac9538d2f512947c0f2e82fc95c41a5eab6729414 /tmp/buf" | sha256sum --check --strict | |
| sudo install -m 0755 /tmp/buf /usr/local/bin/buf | |
| buf --version | |
| # Committed stubs must be what buf generates, or the checked-in client and | |
| # the schema can drift apart silently. | |
| - name: Verify the committed TypeScript stubs are current | |
| run: | | |
| make proto-ts BUF=buf | |
| git diff --exit-code clients/node/src/generated | |
| python-test: | |
| name: Python client tests | |
| needs: [changes, build] | |
| if: needs.changes.outputs.python == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| - name: Download binary artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: binaries | |
| path: bin | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install sysml-grpc | |
| run: | | |
| mkdir -p ~/.opensysml/bin | |
| cp bin/sysml-grpc ~/.opensysml/bin/ | |
| chmod +x ~/.opensysml/bin/sysml-grpc | |
| - name: Install Python client | |
| run: | | |
| make python-install | |
| pip install pytest pytest-mock psutil | |
| # The integration tests connect to a service on the standard port with | |
| # auto_start=False, the explicit opt-in to one the client does not manage; | |
| # without it they skip, exercising nothing, so one runs here for the rest of | |
| # the job. Tests naming no address start a private child of their own. | |
| - name: Start sysml-grpc | |
| run: nohup ~/.opensysml/bin/sysml-grpc -port 50051 > /tmp/sysml-grpc.log 2>&1 & | |
| - name: Wait for sysml-grpc | |
| run: | | |
| python3 - <<'PY' | |
| import socket, sys, time | |
| for _ in range(30): | |
| with socket.socket() as probe: | |
| probe.settimeout(1) | |
| if probe.connect_ex(("localhost", 50051)) == 0: | |
| print("sysml-grpc is listening on 50051") | |
| sys.exit(0) | |
| time.sleep(1) | |
| sys.exit("sysml-grpc never listened on 50051") | |
| PY | |
| # OPENSYSML_REQUIRE_SERVICE turns "no service, so skip" into a failure: a | |
| # service is provided here, so its absence is the bug, not a pass. | |
| - name: Run Python client tests | |
| env: | |
| OPENSYSML_REQUIRE_SERVICE: 1 | |
| run: make python-test | |
| - name: Verify Python client import | |
| run: python3 -c "import opensysml; print(f'opensysml {opensysml.__version__} imported successfully')" | |
| rust-test: | |
| name: Rust client tests | |
| needs: [changes, build] | |
| if: needs.changes.outputs.rust == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # Reuse the service built by the Go job rather than installing Go again | |
| # in this Rust-focused gate. | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| name: binaries | |
| path: bin | |
| # The artifact is a zip, which does not carry the executable bit. | |
| - name: Install the sysml-grpc binary | |
| run: chmod +x bin/sysml-grpc | |
| - name: Install Rust 1.83 | |
| run: | | |
| rustup toolchain install 1.83.0 --profile minimal | |
| rustup default 1.83.0 | |
| rustup component add rustfmt clippy --toolchain 1.83.0 | |
| rustc --version | |
| cargo --version | |
| - name: Check Rust formatting | |
| working-directory: rust | |
| run: cargo fmt --all -- --check | |
| - name: Run Rust clippy | |
| working-directory: rust | |
| run: cargo clippy --all-targets -- -D warnings | |
| # A missing service must fail rather than silently skipping integration | |
| # tests, because the downloaded binary is required by this job. | |
| - name: Run Rust tests | |
| working-directory: rust | |
| env: | |
| OPENSYSML_GRPC_BINARY: ${{ github.workspace }}/bin/sysml-grpc | |
| OPENSYSML_REQUIRE_SERVICE: 1 | |
| run: cargo test --workspace | |
| # Invoke the runner directly so this job does not rebuild the service | |
| # through make conformance-rust, which would require Go here. | |
| - name: Run Rust conformance | |
| env: | |
| OPENSYSML_GRPC_BINARY: ${{ github.workspace }}/bin/sysml-grpc | |
| run: | | |
| cargo run --manifest-path rust/Cargo.toml -p opensysml-conformance -- \ | |
| -binary "$GITHUB_WORKSPACE/bin/sysml-grpc" \ | |
| -report bin/conformance-report-rust.json | |
| - name: Package Rust client | |
| working-directory: rust | |
| run: cargo package -p opensysml | |
| # The digest pins the generator binary so a substituted release cannot | |
| # change the committed Rust artifacts. | |
| - name: Install buf | |
| run: | | |
| curl -fsSL -o /tmp/buf \ | |
| https://github.com/bufbuild/buf/releases/download/v1.57.2/buf-Linux-x86_64 | |
| echo "644e6fa4a62afc2b0d425d3ac9538d2f512947c0f2e82fc95c41a5eab6729414 /tmp/buf" | sha256sum --check --strict | |
| chmod +x /tmp/buf | |
| /tmp/buf --version | |
| - name: Verify Rust generated artifacts | |
| run: | | |
| make BUF=/tmp/buf proto-rust | |
| git diff --exit-code rust/opensysml/src/proto rust/conformance/sysml.descriptor.binpb | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: conformance-report-rust | |
| path: bin/conformance-report-rust.json | |
| # Mirrors the CircleCI java-test job: unit tests plus the conformance suite over | |
| # both Connect encodings, on the client's Java 17 baseline. | |
| java-test: | |
| name: Java client tests | |
| needs: [changes, build] | |
| if: needs.changes.outputs.java == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| name: binaries | |
| path: bin | |
| # 17 is the client's compile target, so a 21-only construct fails here. | |
| - uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: '17' | |
| cache: maven | |
| - name: Make the service binary executable | |
| run: chmod +x bin/sysml-grpc | |
| # opensysml.requireService turns "no binary, so skip" into a failure, and | |
| # `install` builds the sources and javadoc jars a release needs on every commit. | |
| - name: Test and install the client | |
| run: mvn -B -f clients/java/pom.xml install -Dopensysml.requireService=true | |
| - name: Run the conformance suite through the client | |
| run: | | |
| mvn -B -q -f clients/java/pom.xml -pl opensysml-conformance \ | |
| dependency:build-classpath -Dmdep.outputFile=/tmp/cp.txt | |
| java -cp "clients/java/opensysml-conformance/target/classes:$(cat /tmp/cp.txt)" \ | |
| io.opensysml.conformance.Main \ | |
| -binary bin/sysml-grpc -protocols connect,connect-json -allow-skips \ | |
| -report conformance-report-java.json | |
| - uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: java-conformance-report | |
| path: conformance-report-java.json | |
| conformance-pkg: | |
| name: Conformance through pkg/opensysml | |
| needs: changes | |
| if: needs.changes.outputs.go == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version-file: go.mod | |
| cache: true | |
| - name: Download modules | |
| run: go mod download | |
| # The runner builds and starts sysml-grpc itself; the pkg protocol runs | |
| # in-process and pkg-connect dials that service, so this gate proves the | |
| # public Go API answers as the wire does. Scenarios outside its v1 | |
| # surface are counted skips, not hidden. | |
| - name: Conformance suite through pkg/opensysml | |
| run: make conformance-pkg | |
| # if: always() — the report matters most when the gate failed and halted the job. | |
| - uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: conformance-pkg-report | |
| path: bin/conformance-pkg-report.json |