# Omega Pull Request Testing date: 2026/09/29 Contributors: - Xylar Asay-Davis - Claude ## Summary The Omega [pull request template](https://github.com/E3SM-Project/Omega/blob/develop/.github/pull_request_template.md) asks for the CTests and the Polaris `omega_pr` suite to pass on six machine and compiler combinations, with the results recorded in a `Testing` comment. For Omega#481, agents on four machines did this from a handoff written for that pull request alone. This design turns that process into a Polaris utility, `utils/omega/pr_testing`, that any developer can use. The developer who wants the results, the requester, gives the work two roles. An initiator pins the commits under test in a branch on the requester's fork, then reports on linting and the documentation build. A tester builds and runs both sides against that pin on each machine, then reports its rows, including any new compiler warnings. By default, one agent on the requester's own computer takes both roles and runs each machine's commands over ssh, through connections the requester opens by logging in. Frontier does not allow such connections, so an agent on Frontier tests its rows. Scripts do the steps whose mistakes would pass unnoticed: choosing the commits and producing the reports. Agent instructions cover the order of setup and what to do when something fails, using the tools Polaris already has. Success means that a reviewer can check each item of the checklist from the `Testing` comments alone: what was tested, against which baseline, and with what result. ## Requirements ### Requirement: Any developer can request testing. Nothing depends on one person's paths, fork, accounts or allocations. ### Requirement: Testing can start from the requester's computer or any supported machine. ### Requirement: GitHub credentials are needed on one computer only. Pushing the test branch and posting comments need the requester's credentials. Setting those up on every machine is a chore that should not be needed. ### Requirement: Every machine tests the same commits. ### Requirement: The baseline is Polaris' Omega submodule unless the requester chooses otherwise. Any other baseline, `develop` included, is stated in every report, with a reason that rests on evidence, not a guess. ### Requirement: Each report carries its own provenance. A report names the machine, compiler and build directories. It identifies the Polaris commit and the baseline and pull request Omega commits, each with a few commits of recent history. ### Requirement: Results are reported as Polaris and CTest write them. Anything an agent adds is kept apart from them. ### Requirement: The requester controls every outward-facing action. Pushing the test branch, submitting jobs and posting comments each need the requester's permission. ### Requirement: Testers do not tick the checklist. The requester ticks the boxes from the posted results. ### Requirement: Progress across machines is visible. The requester can see which rows have results, which were not run and why, and which results are for commits that are no longer current. ### Requirement: A matching baseline is reused wherever it was run. Before building a baseline, the tester looks for an existing `omega_pr` run that matches, whether or not this utility made it. A reused baseline is reported with its own provenance, like any other. ### Requirement: Linting and the documentation build are checked once. They do not depend on the machine, so they are checked once per pull request head rather than once per row. A result is posted only if Omega's CI does not already show it on the pull request. ### Requirement: Each row reports new compiler warnings. A report lists the warnings from the pull request build that the baseline build does not have. It says which of them are in files the pull request changes. ## Algorithm Design ### Algorithm Design: Any developer can request testing. The requester is usually the pull request's assignee, but may be its author or another reviewer. Their agent runs in their own Polaris checkout, and runs commands in their own checkouts on each machine. Settings come from a config file the requester writes once on their computer and once per machine, and the agent asks for any that are missing. The agent instructions are committed in Polaris and reviewed like code. The test branch carries data, not instructions. ### Algorithm Design: Testing can start from the requester's computer or any supported machine. The initiator only does git work, so any computer with an Omega clone and a Polaris environment will do. The initiator may also be the tester for any machine it can run commands on. ### Algorithm Design: GitHub credentials are needed on one computer only. The agent on the requester's computer runs the utility on each machine over ssh, using a shared connection (`ControlMaster`) that the requester opens by logging in, so the agent never handles a password or passcode. The connection lasts as long as their login session, unless they choose to keep it open longer (`ControlPersist`). The utility runs on the machine as it would for an agent there; nothing in it knows about ssh. The agent copies each report back unchanged and posts it from the requester's computer, where `gh` is logged in. An agent on a machine that the requester's computer cannot reach this way, such as Frontier, tests that machine's rows itself. It posts with `gh` if it is logged in there. Otherwise it gives the requester the report, which the agent on their computer can post. ### Algorithm Design: Every machine tests the same commits. The initiator makes the test commit by merging the pull request head into the head of its base branch, as GitHub's test merge does. Any extra merges the requester names, such as a build fix still in review, go on top. It pushes two branches to the requester's fork: ``` omega-pr-test/- test commit, then manifest commit omega-pr-test/--baseline baseline commit ``` The tip of the first branch adds a single file, the manifest `omega_pr_test.yaml`. Testers build its parent, so the manifest is not part of the tested tree. Before building anything, a tester checks that the parent contains the recorded pull request head and base branch head. Only the initiator runs `init`, since testers may already be using the branches. If the fork already has the test branch with a manifest that pins the same pull request head, base, baseline, Polaris commits, merges and rows, `init` leaves it alone rather than replace it with merges that differ only in their hashes. Test and baseline commits are compared by tree. ```yaml schema_version: 1 requester: pull_request: 553 pr_head: base_branch: develop base_head: test_commit: baseline: commit: source: polaris-submodule # or the ref the requester chose reason: null # required unless polaris-submodule polaris_commit: # polaris_commit by default polaris_commit: extra_merges: - {pull_request: 574, commit: , sides: [test]} rows: - {machine: aurora, compiler: intel, mpi: mpich, template: "aurora, intel, mpich"} notes: ``` Testers act only on the structured fields. `notes` is repeated in reports and never followed. An Omega pull request may need Polaris changes that the baseline Omega cannot run, such as renamed config options. Then `polaris_commit` is a test merge of the Polaris pull request, and the baseline runs from a second Polaris checkout at `baseline.polaris_commit`, without those changes. ### Algorithm Design: The baseline is Polaris' Omega submodule unless the requester chooses otherwise. By default, the baseline commit is the `e3sm_submodules/Omega` pin at `baseline.polaris_commit`, which is normally Polaris `main` when the branch is made. That pin states which Omega the Polaris code supports. So the Polaris checkout the baseline runs from must pin the same Omega commit, although it may be at a different Polaris commit, which is recorded. The pin can lag far enough behind that differences would be hard to blame on the pull request, as happened for Omega#481. In that case the requester can choose another baseline, such as `base_head`. Doing so overrides the normal practice, so the reason must rest on evidence: a merged PR's Testing comment showing that it changed `omega_pr` results, or something checked that suggests a merge may change them, stated as only that. A PR's title is not evidence. `init` prints the reason in its summary, so the requester sees it before the branch is pushed, and it goes in the manifest and in each report. ### Algorithm Design: Each report carries its own provenance. The Omega commit is recorded when Omega is built, not when a suite is set up. Otherwise a suite set up with `-p` on an existing build reports the source tree's HEAD at setup time, which may have moved since the build (the gap noted in #823, filed as #827 and fixed by #829). `_output_for_pr.md` gains the full hash and the last five first-parent commits of the Polaris checkout and of both Omega builds. The baseline's come from the baseline work directory's `provenance` file. The CTest utility writes a matching `ctest_output_for_pr.md` with the build directory and Omega commit. Human testers get this too, whether or not they use the utility. ### Algorithm Design: Results are reported as Polaris and CTest write them. Each machine gets one comment, with Perlmutter's pm-cpu and pm-gpu rows in one. A table at the top has a line for each row, generated from `_results.json`, the CTest counts and the warnings comparison. Below it, each row has a collapsed `
` block with its two `.md` files, pasted unchanged, and its warnings section. After the results, the agent may add a single `Notes` section for qualifications such as an extra merge or a known failure. When an agent writes the report, it ends with the signature that `AGENTS.md` prescribes. ### Algorithm Design: The requester controls every outward-facing action. Each outward-facing step prints what it would do, and acts only when given a flag: `init --push` and `setup --submit`. `lint` and `report` only write their comments. The `post` command posts one, once someone has read it, and is outward-facing in itself (see [Decisions](#decisions)). The agent passes one of these only when the requester has allowed it in that session. A manifest or any other document cannot grant permission. Building on the login node is not outward-facing. `polaris suite --build` initializes the submodules and builds. The two builds run one after the other, because concurrent CIME configures race. ### Algorithm Design: Progress across machines is visible. Each report starts with a hidden marker: ``` ``` The table uses the template's label for each row, so the requester can match it to a box. A row that cannot be run, for example because its queue is down, is reported with `"status": "not-run"`, and the table gives the reason. A report for a machine that is down entirely is written on the requester's computer. ### Algorithm Design: A matching baseline is reused wherever it was run. The tester searches the roots the requester lists, and always the utility's own `/baselines`. A candidate is any directory with both a `provenance` file and an `omega_pr_results.json`. A candidate matches only if all of the following hold: - It has the same machine, compiler and build type. - Its Omega commit, as recorded at build time, is the baseline commit. - Its Polaris commit is that of the checkout the baseline runs from, and neither tree was dirty. - It was set up as the `omega_pr` suite, with no extra config file. - Its results are complete, and no task failed or is pending. A run whose Omega commit was recorded only at setup never matches. The matcher lists the near misses and the test each one failed, so the agent can tell the requester what almost matched. If nothing matches, `setup` makes a new baseline in `/baselines`. A baseline there whose job is still queued or running becomes the pull request suite's dependency. With the submodule as the default baseline, pull requests tested against the same Polaris commit share one baseline per row. The warnings comparison needs only a build log. It can use the log from any complete build of the baseline commit with the same machine, compiler and build type, even when no suite run matches. A build is complete if it started from an empty build directory, which the build-time record notes. ### Algorithm Design: Linting and the documentation build are checked once. Omega's `omega-pr` workflow already runs pre-commit on the changed files and `make html-strict` on each pull request, in its "lint and test docs" job. When that job has passed on `pr_head`, the initiator tells the requester and posts nothing, since the pull request's checks already show the result. If the job has not run for `pr_head`, or has not passed, the initiator runs the same two commands on the test commit, in an `omega_dev` environment. It then posts a `Testing` comment that gives pre-commit and the documentation build each its own result, passing or failing. The comment's marker has `lint` as its only row. Checking that the rendered documentation "looks as expected" still needs a person. ### Algorithm Design: Each row reports new compiler warnings. The baseline and pull request builds each leave a complete build log. A warning is identified by its source path relative to the Omega tree, its message and its warning flag, but not by its line number. A pull request that adds lines above an existing warning therefore does not make that warning look new. The report lists each identity that the baseline build does not have, and marks the ones whose file is in `git diff --name-only ...`. In those files, it also lists an identity that occurs more often in the pull request build. In other files, a higher count says nothing about the pull request's code: a header's warning occurs once for each file that includes it, and a parallel build can interleave the output of two compilers so that one occurrence cannot be parsed. With the submodule as the baseline, warnings added on `develop` since the pin also appear as new. The mark separates those from the pull request's own warnings. Only complete build logs can be compared. When a build directory has to be reused because an earlier build failed, `setup` rebuilds it with `--clean_build` rather than building on top of the partial build. ## Implementation ### Implementation: Any developer can request testing. `utils/omega/pr_testing/` holds: - `AGENTS.md`: instructions for the initiator and tester roles, and the prompts the requester gives each one. - `README.md`: the same workflow, written for people running the commands. - `omega_pr_test.py`: the subcommands `init`, `lint`, `setup`, `report`, `post` and `status`, with the modules they use in `omega_pr_*.py`. The modules that do not depend on Omega PRs are in `utils/pr_testing/`, as `pr_test_*.py`, so that other PR testing utilities can share them: the settings, git and GitHub helpers, the rows, setting up and submitting suites and CTests, markers and the table of rows, status, and `pr_test_warnings.py`, the build log parser. `omega_pr_test.py` puts that directory on `sys.path`. The prefixes keep a module from shadowing a standard one, such as `warnings`. The settings for the requester's computer and each machine are in `utils/pr_testing/example.cfg`, shared with other PR testing utilities. Copy it to `~/.config/polaris_pr_test.cfg`, or pass another path with `-f`: ```ini [polaris_pr_test] work_base = /path/to/scratch/polaris_pr_test omega_repo = /path/to/a/clone/of/E3SM-Project/Omega # needed by the initiator only fork = git@github.com:/E3SM.git # needed by the initiator only if the PR needs Polaris changes polaris_fork = git@github.com:/polaris.git # needed by the initiator only if Omega's CI has not passed omega_dev_env = /path/to/conda/envs/omega_dev # other places to look for a baseline to reuse, besides work_base baseline_search_roots = /path/to/earlier/test/dirs # a deployed Polaris checkout tracking main, which baselines of main run # from (testers only) polaris_main = /path/to/polaris/main # on the requester's computer only: : [machines] chrysalis = chrys:/path/to/polaris ``` `init` ends by printing the prompt for an agent testing on a machine itself, such as Frontier, which is the whole handoff and the same on every machine. The utility's `AGENTS.md` shows its template, so a handoff never needs anything added: ``` Test Omega PR 553 from branch omega-pr-test/553-54456ec on git@github.com:xylar/E3SM.git, following utils/omega/pr_testing/AGENTS.md. ``` Each tester finds its rows with `status`, which names the manifest's rows for the machine it runs on, or, on the requester's computer, the rows for each machine. Perlmutter's pm-cpu and pm-gpu rows are tested from the same login nodes. When the PR needs Polaris changes, the prompt also names the Polaris branches for the PR and the baseline, on `polaris_fork`. ### Implementation: Testing can start from the requester's computer or any supported machine. ``` omega_pr_test.py init --pr 553 [--baseline --reason ] [--test-ref ] [--merge-pr ]... [--baseline-merge-pr ]... [--polaris-ref ] [--baseline-polaris-ref ] [--rows /,...] [--push] ``` `init` asks GitHub for the pull request head and base branch, fetches them from E3SM-Project/Omega, and makes the merges in a scratch worktree of `omega_repo`. `polaris_commit` is the current `main` of E3SM-Project/polaris, fetched rather than taken from the initiator's checkout, unless `--polaris-ref` names another. `baseline.polaris_commit` is `polaris_commit` unless `--baseline-polaris-ref` names another. If a merge conflicts, `init` stops, and the pull request's author must update the branch. Alternatively, the requester can resolve the conflicts in a merge of their own, and `--test-ref` names it, a branch on the fork or a commit. `init` checks that it contains the pull request head and the base branch head and uses it as the test commit. The default rows are the six in the template. ### Implementation: GitHub credentials are needed on one computer only. `AGENTS.md` has the agent run each command on a machine with `ssh -o BatchMode=yes bash -l -s`, the commands on standard input. Each command gets a fresh login shell, and `ssh` fails rather than prompting when the requester's connection is gone. `setup` and `./deploy.py` run under `nohup`, so that a dropped connection does not kill a build. The `[machines]` section of the config file on the requester's computer names each machine's ssh host and Polaris checkout. The utility checks that each names a Polaris machine and has that form, and `status` shows them with each machine's rows. Reports come back with `scp`, and `post` posts them (see below, under results). ### Implementation: Every machine tests the same commits. ``` omega_pr_test.py setup --fork --branch [--submit] [--baseline-dir ] [--baseline-load-script