feat(chapel): Wave 2 — chapel-multilocale gate (-nl 2 via gasnet+smp, #87 option A) #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
| # SPDX-License-Identifier: MPL-2.0 | |
| # | |
| # chapel-ci — strict CI gates for the OPTIONAL Chapel mass-panic harness. | |
| # | |
| # The Rust binary stands alone (USB-stick-portable, single-machine). chapel/ is | |
| # a detachable multi-machine harness on top. This workflow exercises the harness | |
| # and the Rust↔Chapel contract surface. | |
| # | |
| # **Why an aggregator gate?** The 6 gate jobs are path-filtered (they only do | |
| # real work when chapel/** or the Rust contract files change). But path-filtered | |
| # workflows that don't trigger leave required status checks "expected but not | |
| # reported" — which blocks unrelated PRs from merging when the gates are in | |
| # the Base ruleset. Solution: a single `chapel-ci-gate` job that ALWAYS runs | |
| # and aggregates. The ruleset requires only the gate. The gate reports: | |
| # - SUCCESS immediately if no chapel-relevant paths changed. | |
| # - SUCCESS if all 6 underlying jobs succeeded on a relevant change. | |
| # - FAILURE if any underlying job failed. | |
| # | |
| # Seven strict jobs (no continue-on-error anywhere): | |
| # 1. chapel-parse-check — chpl --parse-only on every module | |
| # 2. chapel-build — chpl build of mass-panic + smoke (no toolbox) | |
| # 3. chapel-smoke — chapel/smoke/two_repo_smoke (Chapel data flow) | |
| # 4. chapel-e2e — mass-panic -nl 1 on a synthetic 2-repo manifest | |
| # 5. chapel-cli-contract — panic-attack describe-contract vs expected fixture | |
| # 6. chapel-rust-diff — rayon assemblyline vs Chapel single-locale parity | |
| # 7. chapel-multilocale — mass-panic -nl 2 on the same synthetic 2-repo | |
| # corpus, against a Chapel built from source with | |
| # CHPL_COMM=gasnet + CHPL_LAUNCHER=smp (single-host | |
| # oversubscription). The source build is cached on | |
| # $CHPL_HOME; cold ~30-40 min, warm ~30s restore. | |
| # Closes the gap left by Wave 1 (issue #87). | |
| # | |
| # Plus the always-on aggregator: `chapel-ci-gate`. | |
| # | |
| # Wave 2 hardening tracker: SHA-pin the Chapel 2.8.0 .deb + source tarball | |
| # downloads. Today the workflow trusts the HTTPS endpoints at chapel-lang/chapel | |
| # releases. | |
| name: chapel-ci | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: chapel-ci-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| CHAPEL_VERSION: "2.8.0" | |
| CHAPEL_DEB_URL: "https://github.com/chapel-lang/chapel/releases/download/2.8.0/chapel-2.8.0-1.ubuntu22.amd64.deb" | |
| # Source tarball used by chapel-multilocale to build with CHPL_COMM=gasnet. | |
| CHAPEL_SRC_URL: "https://github.com/chapel-lang/chapel/releases/download/2.8.0/chapel-2.8.0.tar.gz" | |
| # $CHPL_HOME for the multilocale build. Cache key bumps via CHAPEL_MULTILOCALE_CACHE_GEN. | |
| CHAPEL_MULTILOCALE_HOME: /opt/chapel-multilocale | |
| CHAPEL_MULTILOCALE_CACHE_GEN: "v1" | |
| jobs: | |
| detect-relevant-changes: | |
| name: detect-relevant-changes | |
| runs-on: ubuntu-22.04 | |
| outputs: | |
| relevant: ${{ steps.f.outputs.relevant }} | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 2 | |
| - id: f | |
| name: Detect chapel-relevant paths | |
| run: | | |
| set -euo pipefail | |
| BASE="${{ github.event.pull_request.base.sha || github.event.before }}" | |
| if [[ -z "$BASE" || "$BASE" == "0000000000000000000000000000000000000000" ]]; then | |
| # First push or detached state — be safe and run the full gate. | |
| echo "relevant=true" >> "$GITHUB_OUTPUT" | |
| echo "detect: BASE missing/zero — treating as relevant" | |
| exit 0 | |
| fi | |
| git fetch origin "$BASE" --depth=1 2>/dev/null || true | |
| CHANGED=$(git diff --name-only "$BASE" HEAD || true) | |
| echo "Changed files:" | |
| echo "$CHANGED" | |
| if echo "$CHANGED" | grep -qE '^(chapel/|Justfile$|\.github/workflows/chapel-ci\.yml$|src/main\.rs$|src/types\.rs$|Cargo\.toml$|Cargo\.lock$)'; then | |
| echo "relevant=true" >> "$GITHUB_OUTPUT" | |
| echo "detect: chapel-relevant paths changed — running gates" | |
| else | |
| echo "relevant=false" >> "$GITHUB_OUTPUT" | |
| echo "detect: no chapel-relevant paths — gates skipped via if-guard" | |
| fi | |
| chapel-parse-check: | |
| name: chapel-parse-check | |
| needs: detect-relevant-changes | |
| if: needs.detect-relevant-changes.outputs.relevant == 'true' | |
| runs-on: ubuntu-22.04 | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Install Chapel ${{ env.CHAPEL_VERSION }} | |
| run: | | |
| set -euo pipefail | |
| sudo apt-get update -qq | |
| sudo apt-get install -y libunwind-dev | |
| curl -fsSL --retry 3 -o /tmp/chapel.deb "${{ env.CHAPEL_DEB_URL }}" | |
| sudo apt-get install -y /tmp/chapel.deb | |
| chpl --version | |
| - name: Parse every Chapel module | |
| working-directory: chapel | |
| run: | | |
| set -euo pipefail | |
| chpl --parse-only src/MassPanic.chpl src/Protocol.chpl src/Imaging.chpl src/Temporal.chpl | |
| chpl --parse-only smoke/two_repo_smoke.chpl src/Protocol.chpl src/Imaging.chpl | |
| chapel-build: | |
| name: chapel-build | |
| needs: [detect-relevant-changes, chapel-parse-check] | |
| if: needs.detect-relevant-changes.outputs.relevant == 'true' | |
| runs-on: ubuntu-22.04 | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Install Chapel ${{ env.CHAPEL_VERSION }} | |
| run: | | |
| set -euo pipefail | |
| sudo apt-get update -qq | |
| sudo apt-get install -y libunwind-dev | |
| curl -fsSL --retry 3 -o /tmp/chapel.deb "${{ env.CHAPEL_DEB_URL }}" | |
| sudo apt-get install -y /tmp/chapel.deb | |
| chpl --version | |
| - name: Build mass-panic + smoke (no toolbox) | |
| working-directory: chapel | |
| run: | | |
| set -euo pipefail | |
| chpl src/MassPanic.chpl src/Protocol.chpl src/Imaging.chpl src/Temporal.chpl -o mass-panic | |
| chpl smoke/two_repo_smoke.chpl src/Protocol.chpl src/Imaging.chpl -o smoke/two_repo_smoke | |
| - name: Upload Chapel artefacts | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: chapel-binaries | |
| path: | | |
| chapel/mass-panic | |
| chapel/smoke/two_repo_smoke | |
| retention-days: 1 | |
| chapel-smoke: | |
| name: chapel-smoke | |
| needs: [detect-relevant-changes, chapel-build] | |
| if: needs.detect-relevant-changes.outputs.relevant == 'true' | |
| runs-on: ubuntu-22.04 | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Install Chapel ${{ env.CHAPEL_VERSION }} | |
| run: | | |
| set -euo pipefail | |
| sudo apt-get update -qq | |
| sudo apt-get install -y libunwind-dev | |
| curl -fsSL --retry 3 -o /tmp/chapel.deb "${{ env.CHAPEL_DEB_URL }}" | |
| sudo apt-get install -y /tmp/chapel.deb | |
| - name: Download Chapel artefacts | |
| uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 | |
| with: | |
| name: chapel-binaries | |
| path: chapel/ | |
| - name: Restore exec bits | |
| run: chmod +x chapel/mass-panic chapel/smoke/two_repo_smoke | |
| - name: Run two_repo_smoke | |
| run: ./chapel/smoke/two_repo_smoke | |
| chapel-e2e: | |
| name: chapel-e2e | |
| needs: [detect-relevant-changes, chapel-build] | |
| if: needs.detect-relevant-changes.outputs.relevant == 'true' | |
| runs-on: ubuntu-22.04 | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Install Chapel ${{ env.CHAPEL_VERSION }} | |
| run: | | |
| set -euo pipefail | |
| sudo apt-get update -qq | |
| sudo apt-get install -y libunwind-dev | |
| curl -fsSL --retry 3 -o /tmp/chapel.deb "${{ env.CHAPEL_DEB_URL }}" | |
| sudo apt-get install -y /tmp/chapel.deb | |
| - name: Download Chapel artefacts | |
| uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 | |
| with: | |
| name: chapel-binaries | |
| path: chapel/ | |
| - name: Restore exec bits | |
| run: chmod +x chapel/mass-panic | |
| - name: End-to-end -nl 1 exercise | |
| run: | | |
| set -euo pipefail | |
| WORK=$(mktemp -d /tmp/chapel-e2e-XXXXXX) | |
| trap 'rm -rf "$WORK"' EXIT | |
| mkdir -p "$WORK/corpus/repo-alpha/src" "$WORK/corpus/repo-beta/src" | |
| echo 'pub unsafe fn a() {}' > "$WORK/corpus/repo-alpha/src/lib.rs" | |
| echo 'pub unsafe fn b() {}' > "$WORK/corpus/repo-beta/src/lib.rs" | |
| for d in repo-alpha repo-beta; do | |
| (cd "$WORK/corpus/$d" && git init -q && git add -A && git -c user.email=ci@example.com -c user.name=ci commit -q -m init) | |
| done | |
| ./chapel/mass-panic --repoDirectory="$WORK/corpus" --numLocales=1 --quiet --outputDir="$WORK/out" | |
| ls "$WORK/out"/system-image-*.json >/dev/null | |
| echo "chapel-e2e: PASS" | |
| chapel-cli-contract: | |
| name: chapel-cli-contract | |
| needs: detect-relevant-changes | |
| if: needs.detect-relevant-changes.outputs.relevant == 'true' | |
| runs-on: ubuntu-22.04 | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable | |
| - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| target | |
| key: ${{ runner.os }}-cargo-chapel-cli-contract-${{ hashFiles('Cargo.lock') }} | |
| - name: Build panic-attack | |
| run: cargo build --release --locked | |
| - name: Run contract gate | |
| run: ./chapel/tests/contract_check.sh | |
| chapel-rust-diff: | |
| name: chapel-rust-diff | |
| needs: [detect-relevant-changes, chapel-build] | |
| if: needs.detect-relevant-changes.outputs.relevant == 'true' | |
| runs-on: ubuntu-22.04 | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable | |
| - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| target | |
| key: ${{ runner.os }}-cargo-chapel-rust-diff-${{ hashFiles('Cargo.lock') }} | |
| - name: Install Chapel ${{ env.CHAPEL_VERSION }} | |
| run: | | |
| set -euo pipefail | |
| sudo apt-get update -qq | |
| sudo apt-get install -y libunwind-dev | |
| curl -fsSL --retry 3 -o /tmp/chapel.deb "${{ env.CHAPEL_DEB_URL }}" | |
| sudo apt-get install -y /tmp/chapel.deb | |
| - name: Download Chapel artefacts | |
| uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 | |
| with: | |
| name: chapel-binaries | |
| path: chapel/ | |
| - name: Restore exec bits | |
| run: chmod +x chapel/mass-panic | |
| - name: Build panic-attack | |
| run: cargo build --release --locked | |
| - name: rayon vs Chapel single-locale aggregate parity | |
| run: ./chapel/tests/rayon_vs_chapel_diff.sh | |
| chapel-multilocale: | |
| name: chapel-multilocale | |
| needs: detect-relevant-changes | |
| if: needs.detect-relevant-changes.outputs.relevant == 'true' | |
| runs-on: ubuntu-22.04 | |
| timeout-minutes: 75 | |
| env: | |
| CHPL_HOME: /opt/chapel-multilocale | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| # Cache the entire built-from-source Chapel tree. Key is stable across | |
| # PRs as long as the version, conduit, launcher and cache-gen marker | |
| # don't change. Cold build is ~30-40 min on a 2-core runner; warm | |
| # restore is ~30s. | |
| - name: Cache multilocale Chapel ($CHPL_HOME) | |
| id: chapel-cache | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 | |
| with: | |
| path: ${{ env.CHAPEL_MULTILOCALE_HOME }} | |
| key: ${{ runner.os }}-chapel-multilocale-${{ env.CHAPEL_VERSION }}-gasnet-smp-${{ env.CHAPEL_MULTILOCALE_CACHE_GEN }} | |
| - name: Install Chapel build dependencies | |
| if: steps.chapel-cache.outputs.cache-hit != 'true' | |
| run: | | |
| set -euo pipefail | |
| sudo apt-get update -qq | |
| sudo apt-get install -y --no-install-recommends \ | |
| build-essential gcc g++ make perl python3 \ | |
| m4 autoconf automake libtool libunwind-dev pkg-config | |
| - name: Build Chapel from source with CHPL_COMM=gasnet | |
| if: steps.chapel-cache.outputs.cache-hit != 'true' | |
| run: | | |
| set -euo pipefail | |
| curl -fsSL --retry 3 -o /tmp/chapel-src.tar.gz "${{ env.CHAPEL_SRC_URL }}" | |
| sudo mkdir -p /opt | |
| sudo tar -xzf /tmp/chapel-src.tar.gz -C /opt | |
| sudo mv "/opt/chapel-${{ env.CHAPEL_VERSION }}" "${{ env.CHAPEL_MULTILOCALE_HOME }}" | |
| sudo chown -R "$(id -u):$(id -g)" "${{ env.CHAPEL_MULTILOCALE_HOME }}" | |
| cd "${{ env.CHAPEL_MULTILOCALE_HOME }}" | |
| # Configure for single-host oversubscribed multilocale: | |
| # CHPL_COMM=gasnet — multilocale communication layer | |
| # CHPL_COMM_SUBSTRATE=smp — shared-memory substrate (no NIC needed) | |
| # CHPL_LAUNCHER=smp — spawn locales as local processes | |
| export CHPL_HOME="${{ env.CHAPEL_MULTILOCALE_HOME }}" | |
| export CHPL_COMM=gasnet | |
| export CHPL_COMM_SUBSTRATE=smp | |
| export CHPL_LAUNCHER=smp | |
| export CHPL_TARGET_COMPILER=gnu | |
| # setchplenv.bash references ${MANPATH} unconditionally; GH runners | |
| # don't export MANPATH by default, so seed it before sourcing. | |
| export MANPATH="${MANPATH:-}" | |
| source util/setchplenv.bash | |
| # Build chpl + runtime + GASNet+smp substrate | |
| make -j"$(nproc)" | |
| # Sanity: confirm we have a multilocale chpl | |
| ./bin/*/chpl --about | grep -E 'CHPL_COMM:\s+gasnet' | |
| - name: Activate multilocale Chapel | |
| id: activate | |
| run: | | |
| set -euo pipefail | |
| export CHPL_HOME="${{ env.CHAPEL_MULTILOCALE_HOME }}" | |
| export MANPATH="${MANPATH:-}" | |
| source "$CHPL_HOME/util/setchplenv.bash" | |
| # Persist env to subsequent steps via GITHUB_ENV | |
| { | |
| echo "CHPL_HOME=$CHPL_HOME" | |
| echo "CHPL_COMM=gasnet" | |
| echo "CHPL_COMM_SUBSTRATE=smp" | |
| echo "CHPL_LAUNCHER=smp" | |
| echo "CHPL_TARGET_COMPILER=gnu" | |
| echo "PATH=$CHPL_HOME/bin/$(uname -s)-$(uname -m):$PATH" | |
| } >> "$GITHUB_ENV" | |
| chpl --version | |
| chpl --about | grep CHPL_COMM | |
| - name: Build mass-panic against multilocale Chapel | |
| working-directory: chapel | |
| run: | | |
| set -euo pipefail | |
| chpl src/MassPanic.chpl src/Protocol.chpl src/Imaging.chpl src/Temporal.chpl -o mass-panic | |
| - name: End-to-end -nl 2 exercise (oversubscribed locales on single runner) | |
| run: | | |
| set -euo pipefail | |
| WORK=$(mktemp -d /tmp/chapel-multilocale-XXXXXX) | |
| trap 'rm -rf "$WORK"' EXIT | |
| mkdir -p "$WORK/corpus/repo-alpha/src" "$WORK/corpus/repo-beta/src" | |
| echo 'pub unsafe fn a() {}' > "$WORK/corpus/repo-alpha/src/lib.rs" | |
| echo 'pub unsafe fn b() {}' > "$WORK/corpus/repo-beta/src/lib.rs" | |
| for d in repo-alpha repo-beta; do | |
| (cd "$WORK/corpus/$d" && git init -q && git add -A && git -c user.email=ci@example.com -c user.name=ci commit -q -m init) | |
| done | |
| # The smp launcher spawns N processes on the local host. -nl 2 is | |
| # the minimum non-trivial multilocale exercise; oversubscription | |
| # is fine for verification (latency, not throughput, matters here). | |
| ./chapel/mass-panic \ | |
| --repoDirectory="$WORK/corpus" \ | |
| --numLocales=2 \ | |
| --quiet \ | |
| --outputDir="$WORK/out" | |
| # Two-locale run produced a system image | |
| ls "$WORK/out"/system-image-*.json >/dev/null | |
| # And that image references both repos (cross-locale aggregation) | |
| grep -q 'repo-alpha' "$WORK/out"/system-image-*.json | |
| grep -q 'repo-beta' "$WORK/out"/system-image-*.json | |
| echo "chapel-multilocale: PASS (-nl 2, gasnet+smp)" | |
| # Always-on aggregator. This is the ONLY job listed in the Base ruleset's | |
| # required_status_checks rule. If detect-relevant-changes determined nothing | |
| # in this PR touches Chapel-relevant paths, the gate passes immediately | |
| # (the seven per-task jobs above skip via their `if:` guard). If a relevant | |
| # change is present, the gate inspects each job's result and only passes | |
| # when ALL seven succeeded. | |
| chapel-ci-gate: | |
| name: chapel-ci-gate | |
| needs: | |
| - detect-relevant-changes | |
| - chapel-parse-check | |
| - chapel-build | |
| - chapel-smoke | |
| - chapel-e2e | |
| - chapel-cli-contract | |
| - chapel-rust-diff | |
| - chapel-multilocale | |
| if: always() | |
| runs-on: ubuntu-22.04 | |
| steps: | |
| - name: Aggregate chapel-ci results | |
| env: | |
| RELEVANT: ${{ needs.detect-relevant-changes.outputs.relevant }} | |
| R_PARSE: ${{ needs.chapel-parse-check.result }} | |
| R_BUILD: ${{ needs.chapel-build.result }} | |
| R_SMOKE: ${{ needs.chapel-smoke.result }} | |
| R_E2E: ${{ needs.chapel-e2e.result }} | |
| R_CLI: ${{ needs.chapel-cli-contract.result }} | |
| R_DIFF: ${{ needs.chapel-rust-diff.result }} | |
| R_MULTILOCALE: ${{ needs.chapel-multilocale.result }} | |
| run: | | |
| set -euo pipefail | |
| echo "detect-relevant-changes.outputs.relevant=$RELEVANT" | |
| printf 'parse-check=%s\nbuild=%s\nsmoke=%s\ne2e=%s\ncli-contract=%s\nrust-diff=%s\nmultilocale=%s\n' \ | |
| "$R_PARSE" "$R_BUILD" "$R_SMOKE" "$R_E2E" "$R_CLI" "$R_DIFF" "$R_MULTILOCALE" | |
| if [[ "$RELEVANT" != "true" ]]; then | |
| echo "chapel-ci-gate: SKIP (no chapel-relevant paths changed) → PASS" | |
| exit 0 | |
| fi | |
| # If detect itself failed, we never confirmed relevance — fail safe. | |
| if [[ "${{ needs.detect-relevant-changes.result }}" != "success" ]]; then | |
| echo "chapel-ci-gate: detect-relevant-changes did not succeed → FAIL" | |
| exit 1 | |
| fi | |
| fail=0 | |
| for r in "$R_PARSE" "$R_BUILD" "$R_SMOKE" "$R_E2E" "$R_CLI" "$R_DIFF" "$R_MULTILOCALE"; do | |
| case "$r" in | |
| success) ;; | |
| *) fail=$((fail + 1)) ;; | |
| esac | |
| done | |
| if [[ $fail -gt 0 ]]; then | |
| echo "chapel-ci-gate: $fail dependent job(s) did not succeed → FAIL" | |
| exit 1 | |
| fi | |
| echo "chapel-ci-gate: all seven gates green → PASS" |