Retrospective: M4 (GitHub repository and project infrastructure)#

Status: complete (2026-09-27) on the branch m4-infra, which ran in parallel with M1b on main and is merged by the orchestrator. Steps 1-2 each appended their section; Step 3 (finish) hardened the workflows, made the workflow rules permanent checks, ran the final verification and added the milestone summary: what was built, the acceptance record, the lessons and the list of author actions. An independent review then found 29 problems; the last section, “Review fixes”, records how each was fixed or why it was deferred.

Step 1: community files, templates and repository settings (2026-09-27)#

Done#

  • Community files (PLAN Section 10.1): CODE_OF_CONDUCT.md re-checked against the official Contributor Covenant 3.0 text (downloaded from contributor-covenant.org; only the two placeholders differ: the reporting contact sm.shovan@gmail.com and the removed enforcement note); SECURITY.md with supported versions, private vulnerability reporting, what to include, and the disclosure process (acknowledgement within 7 days, a fix in a private advisory fork, coordinated disclosure within 90 days, credit); SUPPORT.md; MAINTAINERS.md (new); a complete CONTRIBUTING.md (where to start, setup, presets, test labels and the local gate, parity, performance, style and headers, Doxygen, commit conventions, DCO, the pull-request process with the review rules of Section 8.9, adding or porting an algorithm).

  • GitHub metadata: eight issue forms (bug, feature, new_algorithm, port_research_code, parity_regression, api_change, performance_regression, docs) and config.yml (no blank issues; links to Discussions, private vulnerability reporting, the docs and the Code of Conduct); PULL_REQUEST_TEMPLATE.md with the checklist of Section 8.9; CODEOWNERS (@SMShovan owns everything; core block and a per-algorithm block); .github/dco.yml; .github/labels.yml (46 labels) and the labels workflow that applies it; docs/developer/labels.md.

  • Validation: the issue forms and config.yml validate against the SchemaStore schemas (github-issue-forms.json, github-issue-config.json, checked once with jsonschema in a scratch virtual environment; negative mutations are rejected). ci/github_meta_check.py (a new pre-commit hook, run with --self-test) checks what the schema does not: unique ids and labels per form, dropdown options (no reserved “None”, no unquoted yes/no), every label a form applies exists in labels.yml, the label file itself, CODEOWNERS syntax and paths, and the PR template’s checklist. actionlint 1.7.12 passes on every workflow; the two actions of labels.yml are pinned by SHA and the SHAs were checked with git ls-remote.

  • Read the Docs: .readthedocs.yaml (not connected; checkpoint A4). It validates against the Read the Docs v2 schema, and its pre_build commands (configure-only cpu-only preset, then ci/docs.sh for the Doxygen XML) were run in a fresh clone.

  • Repository settings guide: docs/developer/repository_settings.md, fourteen account-level steps with click paths (organization 2FA, About, features, squash merges, Actions permissions, security, the DCO app, labels, the main ruleset with the exact required check names, a release-tag ruleset, environment protections, Discussions, and later Read the Docs and Zenodo).

Deviations from the plan (pragmatic choices, same intent)#

Plan

What was done

Why

Section 8.9: “DCO sign-off on every commit”

sign-off required from external contributors; members of dyng-dev are exempt (.github/dco.yml, require.members: false)

the M4 brief asks for DCO sign-off for external contributors; the maintainers’ commits already carry their identity and a Co-Authored-By trailer, and the history has no Signed-off-by lines

Section 10.2 label help-wanted

help wanted (GitHub’s standard name), and a generic good first issue next to the plan’s good-first-issue:<category> labels

GitHub lists issues with exactly these two names on the repository’s /contribute page

Section 10.1 issue forms

also performance_regression.yml and docs.yml

the M4 acceptance criteria ask for them

no label workflow in Section 8.8

.github/workflows/labels.yml applies labels.yml (dry run on pull requests)

otherwise the author would have to create 46 labels by hand; the file stays the single source of truth

Section 4.2 lists “three scripts” of tooling

one more checker, ci/github_meta_check.py, as a pre-commit hook

GitHub validates issue forms, CODEOWNERS and labels only after a push; a broken form silently falls back to a blank issue

Section 8.9 rule 6: the CODEOWNERS algorithm block is generated from the manifests

written by hand, marked as the block scripts/regen.py will generate from M3 on

scripts/regen.py does not exist yet

Section 9.2 (Read the Docs installs the docs requirements)

.readthedocs.yaml uses the conda environment environment.yml

the M4 brief asks for the same pinned tool versions as environment.yml, including Doxygen 1.18.0

Notes for the next steps#

  • Docs site (Step 2): .readthedocs.yaml expects docs/conf.py and the Doxygen XML in build/doxygen/xml (what ci/docs.sh writes, after cmake --preset cpu-only -DDYNG_BUILD_TESTS=OFF ...); keep docs.yml and .readthedocs.yaml in step. Add the documented site-build command to CONTRIBUTING.md (“Documentation and Doxygen”). The new pages docs/developer/labels.md and docs/developer/repository_settings.md belong in the Developer section of the toctree; they use no in-page anchor links (MyST would need myst_heading_anchors).

  • Required checks: when docs.yml exists, put its job names into the table of docs/developer/repository_settings.md (step 9), which now says “every job of the workflow”. Any action from a new owner must be added to the Actions allow list (step 5).

  • Roadmap: CONTRIBUTING.md and the new_algorithm form link docs/roadmap.md, which Step 3 writes.

  • ci/github_meta_check.py has REQUIRED_FORMS and REQUIRED_LABELS; update them when a form or a plan label is added or renamed.

Open items for the author#

  • Make the account-level settings of docs/developer/repository_settings.md (steps 1-12 now; 13-14 at checkpoint A4). The main ruleset needs one pull request on which every workflow has run.

  • Decide whether milestone merges keep being pushed directly to main (a ruleset bypass for repository admins, recorded in GOVERNANCE.md) or go through pull requests from now on (PLAN Section 8.9, rule 1; the recommended default).

Step 2: documentation site (2026-09-27)#

Done#

  • Sphinx site (PLAN Sections 9.1-9.6, D14): docs/conf.py with MyST Markdown, the pydata-sphinx-theme, Breathe over the Doxygen XML, sphinx-copybutton, sphinx-design and sphinxcontrib-bibtex; nitpicky references and warnings as errors. Diataxis sections: getting started (install and build, a first update in C++), tutorials (dynamic shortest paths on MOSP files, quoting examples/cpp/sssp_update.cpp by text anchors), how-to guides (choose a backend, profile an update, read and write files, run parity, build the docs), explanation (the update model: the thesis Chapter 3 template, G1-G10, the template card, the two families, the four challenges, a worked sssp example; backends and resources; results and versions), reference (the algorithm catalogue, the curated C++ API with one doxygengroup page per Doxygen group, the file formats), developer (the plan, contributing, writing documentation, the parity guide included from parity/README.md, labels, repository settings, the name reservation, the retrospectives), the ADRs (docs/adr/index.md includes the README’s table; the ADRs are globbed), roadmap, how to cite (the bibliography rendered from docs/references.bib, the BibTeX and CITATION.cff), changelog (included from CHANGELOG.md) and a glossary. A landing page with the tagline, the algorithm table (sssp now, the others marked planned with their release), install and build, a first update, and “How to cite”. Every existing page (algorithms/sssp.md, api/file_formats.md, the ADRs, the retrospectives, labels.md, repository_settings.md, pypi_name_reservation.md) is part of the site.

  • docs/_ext/dyng_breathe.py, a small local extension for three Breathe limitations found while making the reference build warning-free: (1) on group pages Breathe declares free functions, enumerations and aliases in the global scope (to_vector, not dyng::to_vector), so 134 references did not resolve; the extension qualifies them with Doxygen’s <qualifiedname>; (2) Doxygen links @ref sssp and header names to group and file pages that a group-based reference does not have; they are resolved to the page that renders the group (a header: its @ingroup group), and a Doxygen group without a page is a warning, so a new group cannot go missing; (3) Doxygen 1.18 emits constexpr twice for constexpr constructors, which Sphinx cannot parse.

  • ci/docs.sh now builds everything: Doxygen with warnings as errors and ci/doxygen_coverage.py (unchanged), sphinx-build -W --keep-going -n -E into build/docs/html, and the linkcheck builder for internal links (documents, anchors, local files; external links only with DYNG_LINKCHECK_EXTERNAL=1). --doxygen-only keeps the old behaviour for lint.yml, the CMake target docs-doxygen and the Read the Docs pre-build; --no-linkcheck skips step 3. It removes stale Doxygen XML before each run.

  • .github/workflows/docs.yml: job site on pushes to main, pull requests and on demand (micromamba installs environment.yml, so the versions are those of the local environment and of .readthedocs.yaml; configure-only cpu-only preset; ci/docs.sh; the site uploaded as the artifact docs-html); job external-links weekly and on demand (DYNG_LINKCHECK_EXTERNAL=1), not a required check. permissions: contents: read, persist-credentials: false, actions pinned by SHA.

  • environment.yml pins Sphinx 8.2.3, myst-parser 5.1.0, linkify-it-py 2.2.0, pydata-sphinx-theme 0.22.0, Breathe 5.0.0, sphinx-copybutton 0.5.2, sphinx-design 0.7.0 and sphinxcontrib-bibtex 2.7.0; the dyng-dev environment was updated and the conda cache cleaned.

  • docs/developer/plan.md (the short roadmap, from PLAN Sections 0, 1, 11, 12 and Appendix E, with the M1a re-estimate) and docs/roadmap.md (the public roadmap, PLAN Section 10.5).

  • Follow-ups of Step 1: the site-build command in CONTRIBUTING.md; the docs.yml required check (site) and the micromamba action in the Actions allow list of docs/developer/repository_settings.md; labels.md and repository_settings.md in the Developer toctree.

Deviations from the plan (pragmatic choices, same intent)#

Plan

What was done

Why

the newest Sphinx

Sphinx 8.2.3 (not 9.1)

Breathe 5.0.0, the newest, requires Sphinx < 9 (the conda solver refused 9.1)

Section 9.2 lists myst-nb

not installed yet

there are no notebooks before M5/M6; it is added with the first notebook

Section 9.2 lists no bibliography or linkify extension

sphinxcontrib-bibtex and linkify-it-py

“How to cite” renders docs/references.bib, the single source of citations, instead of a hand-copied list; bare URLs in the existing pages become links

Breathe renders the Doxygen XML as is

docs/_ext/dyng_breathe.py works around three Breathe limitations

without it the curated group pages (Section 9.2) cannot build with -W -n; the alternative, per-namespace pages, loses the curation

every reference resolves

nitpick_ignore_regex for the standard library, CUstream_st, dyng::detail and the namespace names

no external inventory is loaded (the build must not need the network); detail is private by design (Section 9.1); Breathe links namespace prefixes that group pages do not declare

docs/Doxyfile.in with CITE_BIB_FILES

docs/Doxyfile unchanged: it already writes XML (GENERATE_XML = YES) with the custom aliases; no CITE_BIB_FILES

Doxygen’s @cite needs bibtex and Perl at build time; the @paper paragraphs name the BibTeX keys instead and the site renders the bibliography

conda env update -f environment.yml

conda install of exactly the new pins into dyng-dev (docutils 0.23 -> 0.21.2, required by Sphinx 8.2; nothing else changed)

M1b was building and timing with the same environment at the same time; env update may also upgrade CMake, Ninja or clang-format. The resulting environment satisfies environment.yml

.readthedocs.yaml pre-build runs ci/docs.sh

runs ci/docs.sh --doxygen-only

Read the Docs builds the Sphinx site itself; the link check stays in ci/docs.sh and docs.yml

docs.yml: “pinned pip packages + a pinned Doxygen binary” or micromamba

micromamba with environment.yml (the whole development environment, including clang-tools)

one source of truth for every version; the environment is cached between runs

existing files untouched

two comment edits: docs/references.bib (its header comment contained an at sign, which pybtex reads as an entry) and cpp/include/dyng/core/error.hpp (Doxygen turned the text file:line into a link to the URL file:line, found by the link check)

the comments now say the same in words that parse

release.yml untouched in this step

grouped three >> "${GITHUB_OUTPUT}" lines into one redirect

actionlint with shellcheck (SC2129) flagged it; the M4 criterion asks for every workflow to pass actionlint

Validation#

Check

Result

ci/docs.sh in the dyng-dev environment

Doxygen + coverage OK (96 compounds); Sphinx -W -n with no warning; link check OK; about 10 s

negative tests (a page edited, then restored)

a link to a missing page, an unknown {doc}, a missing heading anchor, a link to a missing file and a page outside every toctree each fail the build; a raw HTML link is not checked (recorded in developer/documentation.md)

the link check found a real defect

Doxygen had turned file:line in two comments of error.hpp into links to the URL file:line (fixed)

fresh environment from environment.yml (what docs.yml and Read the Docs install)

created in 28 s; Sphinx 8.2.3, Breathe 5.0.0, myst-parser 5.1.0, pydata-sphinx-theme 0.22.0, Doxygen 1.18.0; configure-only preset + ci/docs.sh pass

ci/check.sh in a fresh clone of m4-infra (under the shared lock, niced)

all checks passed in 74 s: clang-format, cpu-only 196/196 and dev 202/202, clang-tidy, REUSE, provenance, harness 10/10, docs (Doxygen, Sphinx, link check), pre-commit

code on the pages

the “first update in C++” program compiled against the fresh build prints invalidated 2, distances (0, 4, 1, 2) and parents (-1, 0, 0, 2), as the page says; the tutorial’s commands produce files identical to the original’s (cmp)

actionlint 1.7.12 with shellcheck (temporary venv)

all five workflows pass (after the release.yml fix)

action pins

every uses: is a full SHA; each SHA equals its version tag by git ls-remote (mamba-org/setup-micromamba v3.2.1 = f457c30a..., actions/upload-artifact v7.0.1 = 043fb46d..., and the six pins of the other workflows)

reuse lint, pre-commit run --all-files

pass

machine-specific paths

none in the diff

Notes for the next steps#

  • Merging M1b: every new page under docs/ must be in a toctree (ADRs, retrospectives and algorithm pages are globbed); a new Doxygen group needs a docs/api/cpp/<group>.md page (the build warns); new ADRs still need a row in docs/adr/README.md. After the merge, run ci/docs.sh once: M1b’s new public headers (CUDA streams, memory resources) are the first real test of the Breathe workaround beyond the M1a headers.

  • docs/getting_started/*, how_to/choose_a_backend.md and the landing page say that CUDA arrives in M1b; update them when it has.

  • The algorithm table exists three times (README, landing page, algorithms/index.md) until scripts/regen.py generates it from the manifests (M3).

  • M5 adds the Python API (sphinx-autoapi over the committed stubs; pin it in environment.yml), myst-nb with the first notebook, the CLI page and the history pages.

Open items for the author#

  • Add mamba-org/setup-micromamba to the Actions allow list and site to the required checks (both in docs/developer/repository_settings.md, steps 5 and 9) once docs.yml has run on a pull request.

  • Read the Docs remains untested end to end (checkpoint A4): whether its conda build of environment.yml fits the time limit, and whether it adds its own Sphinx next to the pins.

Step 3: finish (2026-09-27)#

Done#

  • actionlint 1.7.12 with shellcheck 0.11.0 (actionlint-py and shellcheck-py in a temporary virtual environment) passes on all five workflows. zizmor 1.30.1, a security auditor for workflows, was run as a second opinion and found what actionlint does not check:

    • six actions/checkout steps (cpu.yml, the four jobs of lint.yml, release.yml) left the job token in .git/config (audit artipacked); all now set persist-credentials: false, as docs.yml and labels.yml already did;

    • release.yml expanded two step outputs (project_dir, version) inside shell scripts (template-injection; the values are computed from the tag, so this was hardening, not an exploitable hole); they now reach the scripts through env:, and the smoke-test virtual environment moved from /tmp to RUNNER_TEMP;

    • dependabot.yml proposed a new action release as soon as it appeared (dependabot-cooldown); it now waits 7 days.

    The two per-job permission grants of labels.yml gained comments saying why they are needed. With the default persona zizmor reports nothing; the remaining pedantic notes are listed under deviations.

  • Least privilege, checked: every workflow has a top-level permissions: contents: read; the only write permissions are issues: write in the sync job of labels.yml and id-token: write (Trusted Publishing) in the two publish jobs of release.yml, which get no contents permission at all. No workflow uses pull_request_target or secrets.

  • Pins verified: all eight actions are pinned to a full SHA with a # vX.Y.Z comment, and each SHA equals the commit of its tag by git ls-remote (annotated tags peeled). Each pin is also the newest release tag of its action today:

    Action

    Tag

    SHA

    actions/cache

    v6.1.0

    55cc8345863c7cc4c66a329aec7e433d2d1c52a9

    actions/checkout

    v7.0.1

    3d3c42e5aac5ba805825da76410c181273ba90b1

    actions/download-artifact

    v8.0.1

    3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c

    actions/setup-python

    v7.0.0

    5fda3b95a4ea91299a34e894583c3862153e4b97

    actions/upload-artifact

    v7.0.1

    043fb46d1a93c77aae656e7c1c64a875d1fc6a0a

    crazy-max/ghaction-github-labeler

    v6.0.0

    548a7c3603594ec17c819e1239f281a3b801ab4d

    mamba-org/setup-micromamba

    v3.2.1

    f457c30a868e4760d3a6fcea5f25dc655b8edf39

    pypa/gh-action-pypi-publish

    v1.14.2

    dc37677b2e1c63e2034f94d8a5b11f265b73ba33

  • The checks are permanent now, not a one-off manual run:

    • ci/github_meta_check.py checks every workflow: a top-level permissions block granting read at most, no pull_request_target, every remote action pinned to a full SHA with a version comment, persist-credentials: false on every checkout, and no ${{ }} expression in a run: script except matrix.*, env.* and runner.*. Its self-test gained nine workflow mutations, each of which must be rejected; run on the pre-fix workflows it reports exactly the eight findings above. --verify-pins repeats the git ls-remote comparison (network, so not part of pre-commit).

    • pre-commit runs actionlint (with shellcheck) and zizmor --offline on the workflows, so ci/check.sh and the pre-commit job of lint.yml enforce them. A deliberately broken workflow (an unquoted variable) was rejected by the hook with shellcheck’s SC2086, which shows that shellcheck is found inside the hook environment.

    • CONTRIBUTING.md (Style) states the workflow rules and the --verify-pins command.

  • Final verification (all under flock -s ... nice -n 10, GPU 1 visible only):

    • a fresh clone of m4-infra at b060762 passes ci/check.sh in 73 s: clang-format, cpu-only 196/196, dev 202/202, clang-tidy, REUSE, provenance (63 files), harness, docs (Doxygen coverage of 96 compounds, the Sphinx site with -W -n, the internal link check) and pre-commit with the new hooks;

    • reuse lint: 1067/1067 files compliant (REUSE 3.3);

    • trial merge: in a scratch clone, main (M1b at 5eb1b61) merged into m4-infra without conflicts (CHANGELOG.md auto-merged), and the merged tree passes the whole ci/check.sh (221 CPU tests in the dev preset; the docs build picks up ADR 0015 and the M1b retrospective through the globs). This is only a forecast: M1b is still committing;

    • the final retrospective commit was checked again with ci/docs.sh and pre-commit.

  • CHANGELOG: one additive Unreleased bullet (workflow hardening).

Deviations from the plan (pragmatic choices, same intent)#

Plan

What was done

Why

Section 8.7/8.8 hooks: pre-commit, REUSE, regen check

two more pre-commit hooks, actionlint (with shellcheck-py) and zizmor (offline); fetched at a pinned rev like the other hooks

the M4 criterion asks that every workflow passes actionlint; a hook keeps it true after M4. zizmor found six credential-persisting checkouts that actionlint does not look for

Section 8.8: “never trigger on pull_request from forks automatically … a label-gated pull_request_target job” for a future self-hosted GPU runner

ci/github_meta_check.py rejects pull_request_target everywhere (relaxed under “Review fixes”: allowed in a workflow with no checkout and no run: step, for welcome.yml)

no workflow needs it today; if gpu-test.yml is ever written that way, the check needs an explicit, reviewed exemption for that one file

Section 10.1: dependabot.yml for “GitHub Actions and pip”

GitHub Actions only, plus a 7-day cooldown

there is no pip manifest with dependencies yet (the name-reservation package has none); add the pip entry with the root pyproject.toml in M5

zizmor pedantic: anonymous-definition (jobs without name:)

not changed

a job’s name is its required-check name (docs/developer/repository_settings.md, step 9); renaming jobs now would only churn that list

zizmor pedantic: ${{ matrix.* }} in cpu.yml scripts

allowed (the checker allows matrix.*, env.*, runner.*)

matrix values are fixed in the workflow file, not attacker-controlled; keeping cpu.yml unchanged apart from the checkout option keeps the merge with M1b small

Lessons#

  • Turn each manual validation into a check. Steps 1 and 2 validated the issue forms, the pins and actionlint by hand; the pins and actionlint are now hooks or a script option, so they stay true after the next workflow edit. The self-test’s rule “every mutation must be rejected” also guards the test itself: a mutation that no longer applies (the fixture text moved) leaves the input unchanged, is accepted, and fails the self-test.

  • Use two linters for workflows. actionlint checks syntax, types and shell; zizmor checks security properties (credentials, injection, permissions, supply chain). Each found things the other does not.

  • Required checks couple to job names. Renaming a job, or giving it a name:, silently turns a required check into one that never reports, which blocks every pull request. The settings guide lists the exact names; keep it in the same pull request as any rename.

  • Trial-merge a parallel branch early. Merging main into a scratch clone and running the full gate costs two minutes and turns “the merge may break the docs build” into evidence.

Milestone summary#

What M4 built#

  • Community files: CODE_OF_CONDUCT.md (Contributor Covenant 3.0, contact sm.shovan@gmail.com), SECURITY.md, SUPPORT.md, MAINTAINERS.md, a complete CONTRIBUTING.md (now with the workflow rules).

  • GitHub metadata: eight issue forms and the chooser, the pull request template with the Section 8.9 checklist, CODEOWNERS, dco.yml, labels.yml (46 labels, including the Section 10.2 categories) with the labels workflow and docs/developer/labels.md, dependabot.yml with a cooldown.

  • The documentation site (Sphinx, MyST, pydata-sphinx-theme, Breathe; Diataxis sections; the short plan and the public roadmap), ci/docs.sh, docs.yml, .readthedocs.yaml (not connected), and the repository settings guide.

  • Checks: ci/github_meta_check.py (issue forms, labels, CODEOWNERS, PR template, workflows; --verify-pins), the actionlint and zizmor hooks, and the hardened workflows.

Acceptance record#

#

Criterion

Evidence

Status

1

a fresh clone passes ci/check.sh; the site builds with the documented command (MyST, pydata-sphinx-theme, Breathe over Doxygen XML, -W, Diataxis incl. developer and ADRs); existing pages in the site; internal linkcheck

Step 3 final verification (fresh clone at b060762, 73 s, all steps); CONTRIBUTING.md and docs/how_to/build_the_docs.md give cmake --preset cpu-only + ci/docs.sh; Step 2 negative tests

met

2

community files, issue forms (bug, feature, new algorithm, performance regression, docs, + config.yml), PR template, CODEOWNERS (@SMShovan), labels taxonomy with the good-first-issue categories

Step 1; SchemaStore validation of the forms; ci/github_meta_check.py in pre-commit

met (rendering on GitHub not yet seen)

3

docs.yml on hosted runners with the environment.yml versions; actionlint; least privilege; SHA pins matching their version comments

Step 2 (docs.yml via micromamba and environment.yml, reproduced in a fresh conda environment); Step 3 (actionlint, zizmor, the pin table, the checker)

met locally; not yet run on GitHub

4

.readthedocs.yaml; docs/developer/plan.md; docs/roadmap.md

Steps 1-2 (.readthedocs.yaml validated against the v2 schema; its commands run in a fresh clone)

met (Read the Docs not connected: checkpoint A4)

5

docs/developer/repository_settings.md with exact click paths

Step 1, updated in Step 2 (site check, micromamba in the allow list)

met

6

reuse lint; no machine-specific paths or personal data beyond the contact address; this retrospective

reuse lint 1067/1067; a grep of the whole M4 diff finds no absolute paths and no e-mail address other than sm.shovan@gmail.com and the your.email@example.org placeholder of the DCO instructions. Corrected in “Review fixes”: the grep covered the M4 diff only; the files a newcomer is sent to have defaults of the author’s layout, now documented and listed there

met for the M4 diff; defaults documented

Author actions (account level; nothing below can be done from the repository)#

After the orchestrator pushes, in this order (the click paths are in docs/developer/repository_settings.md; the step numbers refer to it):

  1. Organization security (step 1): require 2FA for dyng-dev; base permission Read.

  2. About (step 2): description, website, topics.

  3. Features (step 3): Issues and Discussions on, wiki off.

  4. Pull requests (step 4): squash merges only; delete head branches automatically.

  5. Actions (step 5): allow GitHub’s actions plus pypa/gh-action-pypi-publish, crazy-max/ghaction-github-labeler and mamba-org/setup-micromamba; require full-SHA pinning; require approval for all external contributors; read-only default token.

  6. Security (step 6): enable private vulnerability reporting (Appendix E, O13), Dependabot alerts and security updates, secret protection with push protection; optionally CodeQL default setup.

  7. DCO app (step 7): install it for dyng-dev/dyng only.

  8. Labels (step 8): run the labels workflow once (Actions → labels → Run workflow), or let the first push to main that touches .github/labels.yml do it.

  9. Decide how milestone work reaches main: through pull requests (PLAN 8.9 rule 1, the default; the empty bypass list of step 9) or direct pushes for a while (a repository-admin bypass, recorded in GOVERNANCE.md).

  10. Ruleset for main (step 9), after one pull request on which every workflow has run: the required checks pre-commit, doxygen, harness, name-reservation, the nine cpu matrix jobs, site and DCO.

  11. Ruleset for release tags (step 10).

  12. Environments (step 11): protection rules on pypi and testpypi (names unchanged; the trusted publishers are bound to them and to release.yml).

  13. Discussions (step 12): categories and the pinned roadmap post.

  14. Check the first runs on GitHub: docs.yml (site green, the docs-html artifact), the labels sync, the issue chooser and each form rendering, and the CODEOWNERS file view (GitHub marks invalid lines there).

  15. Checkpoint A4, later: connect Read the Docs (step 13) and Zenodo (step 14).

Open items#

  • Nothing of M4 has run on GitHub yet; all workflow, form, label and CODEOWNERS checks are local (schemas, actionlint, zizmor, ci/github_meta_check.py).

  • Read the Docs remains untested end to end (checkpoint A4): the time limit for the conda build of environment.yml, and whether it installs its own Sphinx next to the pins.

  • After merging M1b (a trial merge with main at 5eb1b61 passed ci/check.sh): any workflow M1b adds (for example cuda-build.yml) must satisfy the new checks, that is a read-only top-level permissions, SHA pins with version comments, persist-credentials: false, and values passed to scripts through env:; pre-commit run --all-files reports the exact lines. Its jobs also go into the required checks of the settings guide (step 9). The pages that say CUDA arrives in M1b need updating (Step 2 notes).

  • dependabot.yml gets a pip entry with the root pyproject.toml (M5).

  • The algorithm table exists three times until scripts/regen.py (M3); CODEOWNERS’ algorithm block is hand-written until then.

Review fixes (2026-09-27)#

Independent reviewers (newcomer, docs quality, governance and legal, workflows) confirmed 29 problems at 7e70b7b; two pairs were duplicates. All are fixed, except the parts listed under “Deferred”, each with its reason.

Fixed#

Finding

Fix

Commit

clang-tidy naming step ran in no workflow, although CONTRIBUTING and install.md said CI runs the same steps (ADR 0004 and M1a had moved the hosted job to M4)

tidy job in lint.yml: the clang-tidy of environment.yml (micromamba), a configure-only cpu-only tree, DYNG_CHECK_ONLY=tidy ci/check.sh; a missing clang-tidy fails in CI instead of skipping; tidy replaces doxygen in the required checks

0c2a2b7

--verify-pins ran nowhere automatically

a step of the pre-commit job of lint.yml

0c2a2b7

only docs.yml jobs had timeout-minutes

every job has one (cpu 60, lint 15-30, labels 10, release 30, docs 30, welcome 5); github_meta_check.py requires it

0c2a2b7

the weekly external-link run and pushes to main shared a concurrency group with cancel-in-progress

the group includes the event name; only pull-request runs cancel superseded runs (cpu, lint, docs)

0c2a2b7

the micromamba environment cache was keyed on environment.yml only, freezing transitive packages in CI

cache-environment-key with the ISO week, so CI re-solves weekly like a local env update

0c2a2b7

Doxygen pinned twice by hand (the lint doxygen job duplicated docs.yml site)

the lint job is removed (docs.yml site runs the same ci/docs.sh step with the environment.yml Doxygen); github_meta_check.py now checks that every micromamba-version agrees and that CMake pins installed with pip equal environment.yml’s

0c2a2b7

PLAN 10.2’s first-interaction welcome bot missing and unrecorded (two findings)

.github/workflows/welcome.yml (actions/first-interaction v3.1.0, SHA-pinned; issues: write / pull-requests: write in the job only). It needs pull_request_target for fork PRs; the checker now allows that trigger only in a workflow with no checkout and no run: step (self-tested), and zizmor’s dangerous-triggers finding is suppressed on that line with the reason

0c2a2b7

parity commands failed without the author’s ~/Projects/<original> clones; SSH upstream URLs

DYNG_ORIGINALS_DIR (default $HOME/Projects, unchanged for the author) expanded in references.toml; https upstreams; a missing clone prints the exact git clone command; the clone step in CONTRIBUTING, README, parity/README.md and run_parity.md

7254e81

dyng_breathe.py: docstring numbered 1, 3, 2; colon-fenced doxygengroup pages not recognized

renumbered; the pattern accepts backtick and colon fences

f189ba5

step 3 of ci/docs.sh (“internal link check”) checked nothing; links to repository files never checked

ci/docs_links.py: every github.com/dyng-dev/dyng/{blob,tree,edit,raw}/main/<path> in the tracked text files must exist, and every relative link and #anchor of the built HTML must resolve (6 tests in ci/tests, run by the harness step); the Sphinx linkcheck builder runs only for external links. The wording of ci/docs.sh, ci/check.sh, CONTRIBUTING, docs.yml and the how-to says what each step checks

a84266e

the weekly external-link job could never pass (admin-only and login-only URLs return 404 to anonymous clients; about 19 minutes under GitHub rate limits)

linkcheck_ignore for the settings, “new” form, package-management and Read the Docs URLs when external checking is on; 8 workers; linkcheck_rate_limit_timeout = 60

a84266e

“Edit this page” on the include-only pages opened the stub (two findings)

an html-page-context hook in docs/conf.py points the button of a page that consists of one {include} at the included file (CHANGELOG.md, parity/README.md, docs/adr/README.md)

a84266e

no gate compiled the C++ on the docs pages; the landing snippet did not compile

examples/cpp/first_update.cpp, built by the dev preset and run by example.first_update.<backend>, which matches the exact printed output; both pages quote it with literalinclude text anchors

b26420e

tutorial: “both CPU backends” but one command; CTest compared only the tree file

the commands loop over both backends; new test example.sssp_update.<backend>.distances_match_original

b26420e

no figure for the worked sssp example (PLAN 9.2)

a committed SVG (no new dependency) of the tree before and after the batch, with the invalidated subtree

e9d3b38, 14b9dc9

“API review checklist” required in four places but did not exist

docs/developer/api_review_checklist.md (from ADR 0004, ADR 0006 and the stability policy), linked from CONTRIBUTING, the PR template, the API-change form and the label description

8321a1d

docs/developer/provenance.md missing, yet the short plan marked M0 “provenance” done

written (commit identities and counts per pinned original, placeholder and AI-assisted commits, the basis for publishing, the third-party rule and table; no e-mail addresses or host names); the M0 row corrected

8321a1d

CoC: the only reporting channel was the single moderator (two findings); CoC pointed to a missing GOVERNANCE contact

an escalation paragraph (reports about the lead maintainer go to GitHub’s own reporting until a second moderator exists; attribution note updated; REUSE.toml comment); GOVERNANCE “Contacts” section

d0ed2a3

GOVERNANCE said every change goes through a PR, but main was pushed directly

“Reviews and merges” states the interim practice and that the route is an open decision; “Open decisions” lists it

d0ed2a3

O3 (NOTICE institution line and years) never confirmed

listed as an open decision in GOVERNANCE and the short plan; author action below

d0ed2a3, 8321a1d

DCO fix contradicted “no force-push during review”; remediation commits undocumented; no upstream remote

CONTRIBUTING: upstream remote; rebase onto upstream/main before review, a remediation commit (exact text) during review

0843ccb

web-editor commits fail the required DCO check

settings guide step 7: Require contributors to sign off on web-based commits

0843ccb

DCO summary left out clause (d)

all four statements, (d) spelled out (name and e-mail kept publicly and indefinitely)

0843ccb

commit-subject style versus PR-title style unexplained

“Commits” explains that branch commits use <area>: ... and only the Conventional-Commits PR title reaches main

0843ccb

porting guidance would strip MIT/BSD notices; no path for unlicensed code with permission

CONTRIBUTING (Style, Adding an algorithm) and the port form: outside code keeps its notices (third_party/, REUSE, LICENSES/, NOTICE if required, provenance.md); the checkbox accepts the written agreement of all copyright holders

0843ccb

script defaults assume the author’s layout, unexplained

CONTRIBUTING has a table of the locations dev_env.sh exports, their defaults and how to move them; the defaults themselves are listed under “Deferred”

0843ccb

newcomer files cite “PLAN Section N” of an unpublished document

removed from CONTRIBUTING, the PR template, the issue forms, MAINTAINERS and the non-retrospective docs pages (links to CONTRIBUTING or site pages instead); plan.md says what the citations in maintainer records (ADRs, retrospectives, settings guide, workflow comments) refer to

609fd43, 8321a1d

labels: “exactly one kind”, but the parity form sets two

“at least one kind”; port, parity, api-change may come on top

609fd43

roadmap: HIP both “not planned” and “open to contributors”

one wording in both lists and in the short plan: the maintainers will not build it; a contributor-led backend is welcome through the feature form

609fd43, 8321a1d

Deviations from the plan (pragmatic choices, same intent)#

Plan

What was done

Why

Section 8.8: clang-tidy on host TUs in cpu.yml (the tooling table: “separate CI job”)

a separate tidy job in lint.yml, naming rules only (the checks ADR 0004 enforces)

it needs the pinned clang-tidy of environment.yml, which lint.yml’s micromamba setup provides; cpu.yml uses apt compilers. The bugprone-*, performance-*, modernize-* sets stay advisory (ADR 0004) until a milestone cleans their findings

Section 8.8: no pull_request_target (M4 checker rule)

allowed only for a workflow without checkout and run: steps (welcome.yml)

the greeting must comment on fork pull requests, whose pull_request token is read-only; the workflow runs no code from the pull request

Section 9.6: the README quickstarts are extracted and run by ci/docs.sh

not yet: the docs quote examples/cpp/first_update.cpp, which CTest runs; the README’s C++ block is still hand-written

the README’s build and quickstart sections are being changed by M1b at the same time; extraction (and the Python quickstart) lands with M5, when both quickstarts exist

the linkcheck builder as the internal link check (Step 2)

ci/docs_links.py; the builder only for external links

the builder never checks in-page anchors and ignores URLs; MyST resolution in the -W -n build already covers links between pages

Deferred (with reasons)#

  • Neutral defaults for the scratch and toolkit locations. DYNG_SCRATCH defaults to ~/Projects/dyng-work in scripts/dev_env.sh, ci/check.sh, parity/*.py, parity/*.sh and tools/compat/CMakeLists.txt (goldens); parity/references.toml and parity/export_patches/MOSP-CUDA/build.sh name /usr/local/cuda-13.1/bin/nvcc; dev_env.sh prefers /usr/local/cuda-13.1; parity/perf_ab.py sets OMP_NUM_THREADS=28. These are the lead maintainer’s layout (Appendix E records it as the author’s location; the plan’s generic default was ~/scratch/dyng-parity). Changing them now would touch the parity files M1b is editing and move the author’s goldens; they are documented in CONTRIBUTING instead. Follow-up for the parity owner after the M1b merge: ${XDG_CACHE_HOME:-~/.cache}/dyng (or keep the author’s default) and ${CUDACXX:-nvcc} from PATH / CUDA_HOME.

  • README quickstart extraction (Section 9.6): M5, see the deviation above.

  • The external-link job end to end. Not run locally again (about 20 minutes against GitHub’s rate limits, and the blob/main links of files that reach main only with this merge would fail until then); first check it with Run workflow after the merge.

Validation#

Check

Result

fresh clone of m4-infra at d5c0298, ci/check.sh (shared lock, niced, GPU 1 only)

all checks passed in 73 s: clang-format; cpu-only 200/200 and dev 206/206 (+4: first_update and distances_match_original on two backends); clang-tidy; REUSE; provenance; harness 16/16 (10 parity + 6 ci/tests); docs (Doxygen, Sphinx -W -n, docs_links); pre-commit (actionlint, zizmor, github-meta --self-test)

hosted tidy job’s command in a fresh clone (configure-only cpu-only, CI=true DYNG_CHECK_ONLY=tidy)

passes; with a struct BadName added to a library source it fails with readability-identifier-naming

github_meta_check.py --verify-pins

all nine pins equal their tags (new: actions/first-interaction v3.1.0 = 1c4688942c71f71d4f5502a26ea67c331730fa4d)

zizmor on welcome.yml without the suppression

reports dangerous-triggers (so the suppression is what silences it)

negative tests

a typo’d blob/main URL fails ci/docs.sh; a colon-fenced group page builds; a wrong expected distance fails both first_update tests; self-test mutations for timeouts, pull_request_target with checkout or run:, two micromamba versions, a CMake pin that differs from environment.yml

parity harness from a fresh https clone

with DYNG_ORIGINALS_DIR pointing at a scratch clone, build_reference.sh --variant unpatched MOSP-OpenMP builds; with an empty HOME it prints the clone command and exits 1

edit links of the include-only pages

edit/main/CHANGELOG.md, edit/main/parity/README.md, edit/main/docs/adr/README.md

trial merge of main (M1b at 4cdf33c)

two textual conflicts that already existed before these fixes (CONTRIBUTING.md: keep m4-infra’s file and add M1b’s CUDA presets block to “Build”; README.md build section: keep both paragraphs); after the merge github_meta_check.py reports M1b’s cuda-build.yml (checkout without persist-credentials: false, no timeout-minutes), to be fixed in the merge

Author actions added by the fixes#

  • Confirm or correct the NOTICE institution line and the years (O3), then log it in GOVERNANCE.md (Open decisions → approvals log).

  • Confirm who made the 22 placeholder-identity commits and whether any co-author wrote code outside git (docs/developer/provenance.md, AUTHORS.md).

  • Settings guide step 7: tick Require contributors to sign off on web-based commits.

  • Required checks (step 9): tidy instead of doxygen; welcome is not a required check.

  • After the merge, run the docs workflow once with Run workflow to see the external-link job.

Lessons#

  • A check that cannot fail is not a check. The linkcheck step printed “OK” while checking nothing; reading the builder’s output.json counts (all unchecked or ignored) would have shown it. Every new gate step now comes with a negative test.

  • “CI runs the same steps” has to be checked step by step. A table of ci/check.sh steps against workflow jobs (now in the header of ci/check.sh) found the missing tidy job.

  • Grep what the newcomer reads, not only the diff. The machine-specific defaults were in files M4 did not change but sent contributors to.

Acceptance fixes (2026-09-27)#

The independent acceptance check (at 402a467) passed criteria 1-3, 5 and 6 and failed criterion 4. The second check (at f902800) passed criteria 1 and 3-6 and failed criterion 2 on one sentence of CONTRIBUTING.

Fixed#

Finding

Fix

Commit

docs/developer/plan.md said the plan’s figures stand after 0.1 (0.1.x 4-6 weeks, 0.2 and 0.3 8-12 each), contradicting the M1a retrospective, which re-estimated them

the estimate table carries the retrospective’s rows: 0.1.x 1.5-2 weeks focused / 3-4 calendar, 0.2.0 and 0.3.0 3-4 / 6-8 each, up to 0.3.0 about 12-16 / 22-30 (plan: 32-47)

d722388

informational: the weekly external-link job hit GitHub’s rate limit (HTTP 429) and a 404 for a file not yet on main

the external linkcheck ignores github.com/dyng-dev/dyng/{blob,tree,edit,raw}/main/...; ci/docs_links.py already checks each of these paths against the checked-out tree on every build, so no link goes unchecked

c1ca843

the external check reported a permanent redirect for GitHub’s private-vulnerability-reporting docs page

the settings guide links the new URL

d93efb9

second acceptance check (at f902800): CONTRIBUTING’s workflow rules said “pull_request_target is not used”, but welcome.yml uses it (the exception recorded under “Review fixes”)

the rule states what ci/github_meta_check.py enforces: the trigger only in a workflow that neither checks out nor runs code from the pull request (today welcome.yml)

ceacb06

cosmetic: the docs.yml header comment had a 125-column line

rewrapped to 100 columns

40f1581

Lessons#

  • A summary must be checked against its source number by number. The short plan reproduced the retrospective’s table only up to the row the plan text was written from; a diff of the two tables would have caught it.

  • When a rule gets an exception, grep every place that states the rule. The checker and the retrospective were updated for welcome.yml, the contributor guide was not.