Skip to content

feat(chapel): Wave 2 — chapel-multilocale gate (-nl 2 via gasnet+smp, #87 option A) #40

feat(chapel): Wave 2 — chapel-multilocale gate (-nl 2 via gasnet+smp, #87 option A)

feat(chapel): Wave 2 — chapel-multilocale gate (-nl 2 via gasnet+smp, #87 option A) #40

Workflow file for this run

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