Contributing¶
How to make a change to FLAF (or an analysis) and get it merged. The same workflow applies to the shared submodules (Corrections, StatInference, PlotKit).
Branch, don't commit to main¶
Always work on a topic branch and open a pull request — never commit directly to main.
Nearly all PRs come from personal forks: pushing a branch to cms-flaf/<repo> itself needs
write access to that repository. Fork the repository on GitHub once and add the fork as a remote:
git remote add fork git@github.com:<your-user>/<repo>.git # once per checkout
git fetch origin
git checkout -b my-short-topic-name origin/main
# ... make changes ...
git commit -m "short, clear one-line description"
git push fork my-short-topic-name # then open a PR against cms-flaf/<repo> on GitHub
If your change spans repositories (e.g. FLAF and an analysis), use the same branch name in each affected repo so reviewers can find the matching pieces, and test them together by listing the other PRs in the bot comment (see Integration pipeline).
Changing FLAF or Corrections from an analysis checkout
To run an analysis against your edited framework, point FLAF_PATH/CORRECTIONS_PATH at the
edited copy before sourcing env.sh — see
The environment. Mind its two
warnings: the edited copy's directory must itself be named FLAF (or Corrections),
because env.sh puts its parent directory on PYTHONPATH; and Setup reads the framework
configuration from <analysis>/FLAF/config rather than from FLAF_PATH, so make
configuration edits in the submodule copy.
Format your commits¶
Formatting is CI-enforced (GitHub Actions). apply_format.sh applies all
formatters at once, but only to the files changed by the commits on your branch
(git log origin/main..HEAD), so the order is commit, format, amend. Run it from the root of the
repository you changed, with the environment active:
source env.sh # in the analysis checkout
git commit -m "..."
bash FLAF/run_tools/apply_format.sh # from an analysis; in FLAF itself: bash run_tools/apply_format.sh
git commit --amend --no-edit # if it changed anything
black (Python) and clang-format (C++) fix the files in place; yamllint (YAML) only reports
problems, which you fix by hand. bash FLAF/run_tools/apply_format.sh --dry-run checks without
changing anything. You can also run the tools on individual files — see
GitHub Actions.
Re-index after adding a task¶
If you added, renamed or moved a LAW task class, refresh the index so it can be found:
Writing code for several eras¶
A stage over several eras (--era A,B) is one workflow whose branches each belong to one era
(Eras); the second model, a workflow without era
branches, is described below. The base class Task in
FLAF/run_tools/law_customizations.py keeps the two scopes apart, and a task class follows these
rules:
- Class flags.
allow_multi_era = Truelets a class be constructed over several eras (without itTask.__init__raises<Task> takes one era).era_branches = Truemarks a workflow whose branches carry the era: its branch map over several eras is the union of the eras' own maps, numberedera id × 1 000 000 + the number in the era's own map, with per-era requirements and per-era submission state. It needsallow_multi_era. Every stage class ofAnaProd/tasks.pyandAnalysis/tasks.pysets both. - Read per-era values through
only_era, neverself.era[0].self.only_erais the era of a task over one era and raisesEraScopeErroron a task over several;self.setupand the paths and names built from it behave the same. Over several eras usesetup_for(era),single(era)(this stage over one era) orcommon(getter, what)for a setting that holds for all the eras, which raisesEraDependentValueErrorwhen they disagree (the settings read this way are listed in Eras).fs_defaultandcmssw_envare readable on any task: the first is checked to agree when the task is constructed, the second does not depend on the era. - Branch-level code runs on one era. A branch of a run over several eras is the branch task of
its era's own workflow, so
output(),run(), a branch'srequires()and the payload calls see one era and that era's own branch numbers. - The requirements of a stage go in
stage_workflow_requires(), not inworkflow_requires(): the latter is a dispatcher that, over several eras, asks each era that still needs work for itsstage_workflow_requires()and merges the answers into one requirement per stage. Callsuper().stage_workflow_requires()for the bundles. - Build the branch map through
cached_branch_map:create_branch_mapreturnsself.cached_branch_map(self._build_branch_map), and the union over the eras is made from the single-era maps. - A workflow over several eras without era branches (
allow_multi_eraonly; StatInference's card stages are the example) keeps law's own branches, and every branch task keeps all the eras of the run, so branch-level code reads an era's values throughsetup_for(era)and run-wide ones throughcommon(). FLAF'sstore_parts()andremote_log_dir_target()are per era and refuse several: override both so that the job file and the logs do not depend on--era(a later run over more eras then resumes the same jobs). Build its map withoutcached_branch_map, which refuses several eras without era branches, and keep anything that must refuse an era set out of__init__: FLAF constructs the workflow over each single era for its luigi resource tokens. Bundles (and with them CRAB) read per-era configuration and are not available to such a workflow.test/test_era_jobs.py(EraCardWorkflow) guards this shape. - Name eras in requirements only through
with_eras(eras, branches), which derives this stage over other eras and hands on the per-task values (--tasks-per-job,--max-runtime,--n-cpus,--crab-memory).run_tools/check_era_rename.pyreports anerakeyword inX.req(...),req_params,req_branchorreq_workflowoutsidewith_eras, andself.era[...]in a task class outsideonly_era; a deliberate exception carries# noqa: era-scopeon the line (the rename findings use# noqa: era-rename). - Deriving a task of the task's own family states what it is:
X.req(task)withtaskanXpasses_skip_task_excludes=Truefor the same task or_skip_task_excludes=Falsefor a separate one with its own defaults;Task.reqrefuses it otherwise (HTCondor options). - A producer that reads AnaTuples checks the era of their events:
Utilities.book_era_check(df, era)before the event loop andUtilities.check_era(count, era, what)after it, asAnaTupleMergeTask,HistTupleProducerTaskandAnalysisCacheTaskdo.
Validate config changes¶
- Edited
datasets.yaml? Run the consistency check. - Added an era or changed config loading? Make sure
Setupstill loads for every era — see Run the tests. CI'stest-setup-loadingdoes this only on analysis PRs, so a FLAF change is not checked there.
Run the tests¶
Most of the framework cannot be tested without CERN infrastructure, but a few checks run locally,
from the analysis checkout with env.sh sourced:
- Config loading —
python3 FLAF/test/test_setup_loading.py Run3_2022 Run3_2022EE …loadsSetupfor each listed era, astest-setup-loadingdoes in CI. It finds the analysis from its own location, so run the copy inside the analysis checkout. - Smoke test —
python3 FLAF/test/test_hello_world.py --version <v> --workflow local(orhtcondororcrab; add--bundleto go through a bundle,--force-failto check the log transfer of a failing job,--user-custom <file>for an extra configuration file) runsFLAF.test.hello_world_task.HelloWorldTaskfor one branch and printsPASSorFAIL.--eradefaults toRun3_2022EE; with several (--era Run3_2022,Run3_2023) the run is one workflow over all of them, with branch 0 of each era selected by its number in that run, and every era's log and output are checked where a run over that era alone puts them. It first deletesdata/<v>/and the task's remote log, so use a throwaway version name. - Every stage over several eras —
python3 FLAF/test/test_era_dimension.py Run3_2022 Run3_2022EE …(from the analysis checkout, at least two eras) constructs every FLAF stage over those eras on the analysis's own configuration, with each workflow type it supports, and takes it through what the scheduler does before anything runs (proxy, resources, requirements, outputs, completeness). It fails on a setting that differs between the eras where it has to agree, or on a per-era value read on a task over several eras. No grid is needed: thefs_*are redirected to a local directory, and ROOT and Corrections are stand-ins. The reusable GitHub workflowtest-era-dimension.yamlruns it (GitHub Actions). - Unit tests — the other
FLAF/test/test_*.pyfiles (path cache, bundle hashing, stitching variables, cost model, …), and thetest/test_*.pyof an analysis, are pytest modules, mostlyunittestclasses; see Unit tests for where they go, how to mark the ones that need ROOT, CVMFS or a grid proxy, and how to run them. Run the ones covering the code you changed, and extend them when you change that code. - Undefined names —
run_tools/apply_format.sh, when run in FLAF, also runsflake8 --select=F821,F811over the whole package, asformatting-checkdoes in CI.
Unit tests¶
A unit test goes into test/test_<topic>.py of the repository whose code it covers, FLAF or an
analysis. The unit-tests workflow collects every test/**/test_*.py there, so a new test needs
no change under .github/ (a PR that changes .github/ can be
tested by the bot on a comment by kandrosov only). The GitHub runner has only pyyaml, luigi, law and pytest (9.0.3, as in the LCG view): no ROOT,
no /cvmfs, no grid proxy. A test that needs one of them declares it with a marker, and CI
deselects it (GitHub Actions):
| Marker | The test needs |
|---|---|
needs_root |
ROOT or another package of the analysis environment (numpy, uproot, awkward, Corrections) |
needs_cvmfs |
/cvmfs, e.g. the CRAB client or SITECONF |
needs_proxy |
a valid grid proxy |
import pytest
pytestmark = pytest.mark.needs_root # the whole module
@pytest.mark.needs_cvmfs # or one class, or one test method
class TheCrabClientItself(unittest.TestCase):
...
- A test that uses no ROOT itself but reaches it through an import (
FLAF.Common.SetupandFLAF.Common.Utilitiesimport ROOT at module level, and so doesrun_tools/law_customizations.pythrough them) takes no marker. It imports through a stand-in where ROOT is missing, astest_crab_credentials.pyandtest_bundle_hash.pydo, and so it runs in CI. Keepneeds_rootfor tests that use ROOT themselves. - A module that cannot be imported without what it needs (
import ROOTat the top, for the tests' own use) declares the marker module-wide withpytestmark. The marker is read from the source, and the failed import is reported as one test that carries it, so the marker deselects the module. A module without a marker that fails to import stops the run. - The test modules share one process, so a stand-in serves the import only and is removed from
sys.modulesright after: a module collected later must still get the real ROOT, or fail to import it. A module that leaves a stand-in for ROOT behind stops the run. FLAF/test/conftest.pydefines the markers and these rules. Itscollect_ignorelists the scripts namedtest_*.pythat are not unit tests (test_setup_loading.py,test_era_dimension.py,test_hello_world.py,GenProcess/).
Run them from the root of the repository:
python -m pytest # FLAF: every test, which needs an analysis environment
python -m pytest -m "not needs_root and not needs_cvmfs and not needs_proxy" # what CI runs
python -m pytest -p FLAF.test.conftest test # an analysis: its test/, with the pinned FLAF's markers
The marked tests run in an analysis environment: in the analysis checkout, after
source env.sh, cd FLAF && python -m pytest -m "needs_root or needs_cvmfs or needs_proxy"
(test_shape_weight_registry.py takes Corrections from ANALYSIS_PATH). The end of the output
lists the deselected test files with their markers and every test file that ran no test.
Open the PR and run the checks¶
On the pull request:
- The GitHub Actions checks run automatically: repository sanity on every PR, formatting on PRs
to
mainin every repo, the unit tests on PRs tomainin FLAF and the analyses,test-setup-loadingon analysis PRs tomain, the cross-section and dataset checks on FLAF PRs tomainthat change those files, and the docs build when the docs change — see GitHub Actions. - For a real physics check, a member of the
cms-flafGitHub organisation triggers the full pipeline with a@cms-flaf-bot please testcomment (if the PR changes.github/, onlykandrosovcan) — see Integration pipeline.
Pre-PR checklist¶
- On a topic branch from a fresh
origin/main(notmain) -
apply_format.shrun on the committed changes, and the result amended into the commit -
law index --verboserun if you added/renamed a task - dataset consistency check run if you touched
datasets.yaml -
test_setup_loading.pyrun if you changed configuration or its loading - the unit tests for the code you changed pass, including the marked ones CI deselects
- no binary files staged (use Git LFS for those)
- docs updated if behaviour or interfaces changed (see below)
Editing the documentation¶
These docs are MkDocs with the
Material theme; the sources are the Markdown files
under docs/ and the navigation is in mkdocs.yml. To preview locally:
pip install mkdocs-material # once, e.g. in a throwaway venv
mkdocs serve # live preview at http://127.0.0.1:8000
mkdocs build --strict # what to run before committing: fails on broken links
mkdocs build --strict catches broken internal links, missing nav entries and missing assets —
run it before you commit doc changes. The same strict build runs on every PR that touches the
docs, and merging to main publishes the site. There is a edit action on
every page that takes you straight to the source file on GitHub.
Guidelines for docs changes:
- Keep framework-wide material here in FLAF; put analysis-specific material in that analysis's
docs/(see Analyses). Link rather than duplicate. - Prefer concrete, copy-pasteable commands, and flag caveats with admonitions
(
!!! warning,!!! tip). - Remember the audience includes physicists new to the tooling — define terms or link the Glossary.