diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index a0017d87f..e610f455d 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -1,8 +1,9 @@ name: conformance # COST: the cross-port conformance matrix + full Java reactor + drift/mutation gates -# are heavy, so they do NOT run on every push/PR — release gate (tag `v*`), a NIGHTLY -# schedule, and on-demand (workflow_dispatch). Run them LOCALLY before opening/merging +# are heavy, so they do NOT run on every push or PR (pr-ci.yml runs the touched ports' +# lanes on a PR) — release gate (tag `v*`), a NIGHTLY schedule, and on-demand +# (workflow_dispatch). Run them LOCALLY before opening/merging # a PR: # scripts/ci-local.sh # full parity (this + integration suite + drift) # scripts/ci-local.sh --quick # everything except the docker integration suite diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml new file mode 100644 index 000000000..c1467e937 --- /dev/null +++ b/.github/workflows/pr-ci.yml @@ -0,0 +1,193 @@ +name: pr-ci + +# PR CI: runs the tests of the ports a pull request touches, on GitHub-hosted runners +# (free for a public repo). hygiene.yml still owns the `leak-scan` status; this workflow +# adds the port lanes that used to run only on main, nightly and release tags. +# +# THIN WRAPPER: scripts/ci-local.sh is the single definition of every check here. Each +# job installs the toolchain its lane needs and calls the script with that lane's +# `--only` selector and `--no-integration`, so a local run is the same check. Change a +# check in the script, never here. +# +# WHICH PORTS: `detect` maps the PR's changed paths to ports with +# scripts/ci-affected-ports.sh, the same mapper local-ci.yml uses. A change under a +# port's directory selects that port; a change to fixtures/, spec/, scripts/, .github/, +# agent-context/ or anything unrecognised selects ALL ports (the mapper fails open); a +# docs-only change selects none and runs only the offline `gates` lane. +# +# WHAT RUNS PER PORT: unit tests plus the conformance corpora, not the docker +# integration suites (integration-tests.yml owns those, on tags and on demand): +# ts — ts-fast (build + typecheck, conformance, completeness) and ts-unit +# (the per-package suites) +# java — java-slow: the full reactor build with tests on, which already holds the +# java and kotlin conformance classes +# python / csharp — the port's full test suite and conformance +# +# THE REQUIRED CHECK: the final job, `pr-ci`, is the name to put in branch protection +# when a maintainer decides to (it is not required yet; `leak-scan` is). It always runs +# and is green only when every lane that was selected passed, so the set of lanes can +# change per PR without touching branch protection. Do not rename it. +# +# SECURITY: this is a `pull_request` workflow on GitHub-hosted runners with a read-only +# token and no secrets, so fork-submitted code cannot reach anything. It must NEVER move +# onto the self-hosted runner (see local-ci.yml, which must never gain this trigger). + +on: + pull_request: + +permissions: + contents: read + +# A new push to a PR cancels the superseded run. +concurrency: + group: pr-ci-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + detect: + runs-on: ubuntu-latest + outputs: + ports: ${{ steps.sel.outputs.ports }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: false + - id: sel + run: | + # HEAD is the PR's merge commit; its first parent is the base tip the merge was + # built on, so HEAD^1..HEAD is exactly the PR's change even after main moves. + ports="$(scripts/ci-affected-ports.sh HEAD^1 HEAD)" + echo "selected ports: '$ports'" + echo "ports=$ports" >> "$GITHUB_OUTPUT" + + gates: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # the version gates diff against origin/main + persist-credentials: false + - uses: oven-sh/setup-bun@v2 + with: + bun-version: '1.3.14' + - name: Cache Bun install cache + uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun- + - name: Gates + env: + MO_CI_LEAK_BASE: origin/${{ github.base_ref }} + run: scripts/ci-local.sh --only gates --strict-toolchains + + ts: + needs: detect + if: contains(format(' {0} ', needs.detect.outputs.ports), ' ts ') + runs-on: ubuntu-latest + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + lane: [ts-fast, ts-unit] + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: oven-sh/setup-bun@v2 + with: + bun-version: '1.3.14' + - name: Cache Bun install cache + uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun- + - name: ${{ matrix.lane }} + env: + LANE: ${{ matrix.lane }} + run: scripts/ci-local.sh --only "$LANE" --no-integration --strict-toolchains + + java: + needs: detect + if: contains(format(' {0} ', needs.detect.outputs.ports), ' java ') + runs-on: ubuntu-latest + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + # java-slow = the full reactor with tests on, which already holds every java and + # kotlin conformance class (java-fast would run the same classes a second time). + lane: [java-slow] + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + cache: maven + - name: ${{ matrix.lane }} + env: + LANE: ${{ matrix.lane }} + run: | + METAOBJECTS_CI_M2_REPO="$HOME/.m2/repository" \ + scripts/ci-local.sh --only "$LANE" --no-integration --strict-toolchains + + python: + needs: detect + if: contains(format(' {0} ', needs.detect.outputs.ports), ' python ') + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + - run: scripts/ci-local.sh --only python --no-integration --strict-toolchains + + csharp: + needs: detect + if: contains(format(' {0} ', needs.detect.outputs.ports), ' csharp ') + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('server/csharp/**/*.csproj') }} + restore-keys: | + ${{ runner.os }}-nuget- + - run: scripts/ci-local.sh --only csharp --no-integration --strict-toolchains + + # The stable required-check name. Runs even when lanes are skipped or failed. + pr-ci: + if: always() + needs: [detect, gates, ts, java, python, csharp] + runs-on: ubuntu-latest + steps: + - name: Every selected lane passed + env: + RESULTS: ${{ join(needs.*.result, ' ') }} + run: | + echo "lane results: $RESULTS" + for r in $RESULTS; do + case "$r" in + success|skipped) ;; + *) echo "a lane finished '$r'" >&2; exit 1 ;; + esac + done diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 55dd7647f..709cb8c33 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,10 +28,10 @@ conformance corpora. This guide covers how to propose changes. 1. **Fork** the repo and create a branch from `main`. 2. Make your change following the discipline below — **tests first (TDD)**. 3. Run the gates locally with **`scripts/ci-local.sh --quick`** and make sure it's - green. **This matters:** on a pull request GitHub Actions runs only the leak scan - (`hygiene.yml`); the conformance and integration lanes run on `main`, nightly and on - release tags, so your local run is the only check of the rest before merge — see the - *Local CI* section below. + green. On a pull request GitHub Actions runs the leak scan (`hygiene.yml`) and the + unit and conformance tests of the ports your change touches (`pr-ci.yml`); the docker + integration suites run on release tags and on manual dispatch — see the *Local CI* + section below. 4. Open a **pull request** against `main` and fill out the PR template. 5. A maintainer reviews the change and the checks on the pull request before merge. Who can approve what, and which changes need a core maintainer, is in @@ -84,9 +84,15 @@ GitHub Actions is enabled on this repository. The check workflows in `.github/workflows/` are thin wrappers over `scripts/ci-local.sh`, the single definition of the checks: each wrapper checks out, installs the toolchains its lane needs, and calls the script, so a local run is the same check a workflow -run would be. A pull request runs only the leak scan; the other lanes run on -`main`, nightly and on release tags. Run the script locally before opening or -merging a PR, so nothing red reaches `main`: +run would be. A pull request runs the leak scan (`hygiene.yml`) and `pr-ci.yml`, which +runs the unit tests and conformance corpora of only the ports the PR touches (a change +under `fixtures/`, `spec/`, `scripts/`, `.github/` or `agent-context/`, or to any file +outside a port's directory that the selector does not recognise, touches every port; a +change only to `docs/` or to Markdown files outside those directories runs just the +offline gates). Its `pr-ci` job is green only when every selected lane is, and a newer +push cancels the superseded run. The docker integration suites run on release tags and on +manual dispatch. Run the script locally before opening or merging a PR, so nothing red +reaches `main`: ```bash scripts/ci-local.sh # full parity: hygiene.yml's leak-scan, all-port diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 2d1f493a0..32fae092d 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -131,6 +131,12 @@ license does not grant use of the MetaObjects name or trademark. Paid work or sponsorship does not grant a role, a vote or an exception to review. A change built for a paying customer goes through the same review as any other change. +One commitment goes with the roles: a port maintainer gets first call on paid work in the port +they maintain, as listed in [MAINTAINERS.md](MAINTAINERS.md). When the creator or Doug Mealing LLC +has paid work that falls in a port, they offer it to that port's maintainer before anyone else; work +that spans several ports is offered to each of those ports' maintainers. This is an offer of work +only. It grants no role, vote or review exception, and the maintainer may decline it. + ## Succession The creator has stepped away when they say so in writing, or when they have not committed, diff --git a/scripts/ci-local.sh b/scripts/ci-local.sh index 097b89fed..11b50b23e 100755 --- a/scripts/ci-local.sh +++ b/scripts/ci-local.sh @@ -118,7 +118,8 @@ # so use it wherever a skip would mean a real gap rather than an absent toolchain: # the pre-release run, and the no-mistakes gate's two commands (.no-mistakes.yaml). # -# Called by: hygiene.yml (--only leak-scan) · conformance.yml (--only gates / +# Called by: hygiene.yml (--only leak-scan) · pr-ci.yml (the touched ports' lanes, with +# --no-integration) · conformance.yml (--only gates / # ts-fast / csharp / java-fast / python, and java-slow for the reactor, all with # --no-integration) · integration-tests.yml (--integration-only, one job per lane) · # local-ci.yml (one job per lane).