From 49fe4c3a0e4db954813d62a62ba588368e3814c8 Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Tue, 13 Oct 2026 20:49:39 -0400 Subject: [PATCH 1/3] Add the caller and the commands that adopt an EnergyPlus release The steps, their order and the gate are declared for this project in governance/energyplus.toml of idfkit-conformance, read at governance-2026.24. The commands those steps name are added here. --- .github/workflows/adopt-energyplus.yml | 48 +++++ scripts/adopt_release.py | 269 +++++++++++++++++++++++++ 2 files changed, 317 insertions(+) create mode 100644 .github/workflows/adopt-energyplus.yml create mode 100644 scripts/adopt_release.py diff --git a/.github/workflows/adopt-energyplus.yml b/.github/workflows/adopt-energyplus.yml new file mode 100644 index 000000000..239fc03b1 --- /dev/null +++ b/.github/workflows/adopt-energyplus.yml @@ -0,0 +1,48 @@ +name: adopt-energyplus + +# Adopt one EnergyPlus release in this project, as a draft pull request on `energyplus/X.Y.Z`. +# +# This file holds no logic. The steps, their order, the command each one runs and the gate are +# declared for the project `idfkit-docs` in `governance/energyplus.toml` of idfkit-conformance, and +# the reusable workflow called below executes exactly those commands and no others. It never +# merges, publishes or converts: the caller grants `contents: write` and `pull-requests: write` only. +# +# THE TAG IS STATED TWICE, on `uses:` and as `governance_level`, because a called workflow cannot +# see its own ref. The reusable workflow refuses the run unless the two are the same string, so +# both are changed together or not at all. +# +# THE ORDER IS A RULE. The idfkit level is moved by bump-idfkit.yml and not here. The step that adds +# the release to TARGET_VERSIONS (`python -m scripts.adopt_release target-list`) exits with status +# 1 while the installed idfkit does not list the release, because idfkit returns the newest schema +# it holds for a release it does not list and the conversion would publish the wrong field +# metadata without failing. The report of documentation directories that no configured set matches +# (`python -m scripts.adopt_release doc-sets`) is written to the job summary for a person to accept. +# Converting and publishing the release remains convert-docs.yml, dispatched by a maintainer after +# the merge. +# +# It is dispatched by the watch in idfkit-conformance, and may be dispatched by hand. A second run +# for a release that is already carried changes nothing. +run-name: Adopt EnergyPlus ${{ inputs.release }} + +on: + workflow_dispatch: + inputs: + release: + description: The EnergyPlus release to adopt, as X.Y.Z. + required: true + type: string + +permissions: + contents: write + pull-requests: write + +jobs: + adopt: + uses: idfkit/idfkit-conformance/.github/workflows/adopt-energyplus.yml@governance-2026.24 + with: + project: idfkit-docs + governance_level: governance-2026.24 + release: ${{ inputs.release }} + secrets: + APP_ID: ${{ secrets.APP_ID }} + APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }} diff --git a/scripts/adopt_release.py b/scripts/adopt_release.py new file mode 100644 index 000000000..f7262a432 --- /dev/null +++ b/scripts/adopt_release.py @@ -0,0 +1,269 @@ +"""Commands that the adoption of an EnergyPlus release runs in this repository. + +The adoption procedure is held in idfkit-conformance (``energyplus/README.md``). Its version roster +declares two commands for this project, and both are defined here. + +``target-list TAG`` + Adds the release to ``TARGET_VERSIONS`` in ``scripts/config.py`` and designates it + ``LATEST_VERSION``. The command refuses, with exit status 1, when the installed idfkit does not + list the release. idfkit returns the newest schema it holds for a release it does not list, so + a conversion against an older level would publish the field metadata of the previous release + under the heading of the new one without failing. + +``doc-sets TAG`` + Reports how the documentation directories of the upstream release map onto ``DOC_SET_INFO``: + LaTeX directories that no configured set matches (converted under a generated title), Sphinx + directories that no configured set matches (omitted from the site), configured sets found in + neither tree, and sets whose source format differs from the preceding release. The command + changes no file and exits with status 0 whatever it finds. A person accepts the report. + +Both commands are idempotent. +""" + +# Each refusal states its own reason, so the messages are composed where they are raised. +# ruff: noqa: TRY003 + +from __future__ import annotations + +import argparse +import os +import re +import subprocess +import sys +import tempfile +from dataclasses import dataclass, field +from pathlib import Path + +from idfkit.versions import ENERGYPLUS_VERSIONS + +from scripts.config import DOC_SET_INFO, ENERGYPLUS_REPO, EXCLUDED_DIRS, TARGET_VERSIONS +from scripts.rst_doc_sets import SPHINX_EXCLUDED_DIRS, SPHINX_SUBDIR, discover_rst_doc_sets + +CONFIG_PATH = Path(__file__).parent / "config.py" + +_TAG = re.compile(r"v(\d+)\.(\d+)\.(\d+)") +_TARGET_BLOCK = re.compile( + r"(?P^TARGET_VERSIONS: list\[str\] = \[\n)(?P(?: \"v\d+\.\d+\.\d+\",\n)+)(?P\]\n)", + re.MULTILINE, +) +_LATEST_LINE = re.compile(r'^LATEST_VERSION: str = "v\d+\.\d+\.\d+"$', re.MULTILINE) + + +class AdoptionError(Exception): + """The command cannot be performed. The message states the reason.""" + + +@dataclass(frozen=True, order=True) +class ReleaseTag: + """An EnergyPlus release, written ``vMAJOR.MINOR.PATCH`` in this repository.""" + + major: int + minor: int + patch: int + + @classmethod + def parse(cls, text: str) -> ReleaseTag: + match = _TAG.fullmatch(text) + if match is None: + raise AdoptionError(f"'{text}' is not a release tag of the form vMAJOR.MINOR.PATCH") + return cls(int(match.group(1)), int(match.group(2)), int(match.group(3))) + + def as_tuple(self) -> tuple[int, int, int]: + return (self.major, self.minor, self.patch) + + def __str__(self) -> str: + return f"v{self.major}.{self.minor}.{self.patch}" + + +# ── The target list ─────────────────────────────────────────────────────────── + + +def require_library_lists(release: ReleaseTag) -> None: + """Raise unless the installed idfkit lists *release* among its supported versions.""" + if release.as_tuple() not in ENERGYPLUS_VERSIONS: + newest = ReleaseTag(*ENERGYPLUS_VERSIONS[-1]) + raise AdoptionError( + f"the installed idfkit does not list {release}; its newest release is {newest}. " + "Move the idfkit pin in pyproject.toml to a version that carries the release " + "(bump-idfkit.yml) before adding the release to the target list." + ) + + +def with_release(config_text: str, release: ReleaseTag) -> str: + """Return *config_text* with *release* in ``TARGET_VERSIONS`` and the newest entry as ``LATEST_VERSION``.""" + block = _TARGET_BLOCK.search(config_text) + if block is None: + raise AdoptionError("TARGET_VERSIONS was not found in scripts/config.py in the expected layout") + if len(_LATEST_LINE.findall(config_text)) != 1: + raise AdoptionError("LATEST_VERSION was not found exactly once in scripts/config.py") + listed = {ReleaseTag.parse(tag) for tag in re.findall(r'"(v\d+\.\d+\.\d+)"', block.group("body"))} + ordered = sorted(listed | {release}) + body = "".join(f' "{tag}",\n' for tag in ordered) + text = config_text[: block.start("body")] + body + config_text[block.end("body") :] + return _LATEST_LINE.sub(f'LATEST_VERSION: str = "{ordered[-1]}"', text) + + +def add_to_target_list(release: ReleaseTag, config_path: Path = CONFIG_PATH) -> bool: + """Write *release* into the configuration. Returns whether the file changed.""" + require_library_lists(release) + before = config_path.read_text(encoding="utf-8") + after = with_release(before, release) + if after == before: + return False + config_path.write_text(after, encoding="utf-8") + return True + + +# ── The documentation sets ──────────────────────────────────────────────────── + + +@dataclass(frozen=True) +class DocSetSurvey: + """How the documentation directories of one upstream release map onto ``DOC_SET_INFO``.""" + + release: ReleaseTag + #: Configured set name to the source format it is found in, ``latex`` or ``rst``. + formats: dict[str, str] = field(default_factory=dict) + #: LaTeX directories with a main document that no configured set names. + unmatched_latex: tuple[str, ...] = () + #: Sphinx directories with a main document that no configured set or alias matches. + unmatched_sphinx: tuple[str, ...] = () + + @property + def missing(self) -> tuple[str, ...]: + return tuple(name for name in DOC_SET_INFO if name not in self.formats) + + +def _directories(parent: Path, excluded: frozenset[str] | set[str], suffix: str) -> list[Path]: + if not parent.is_dir(): + return [] + return [ + entry + for entry in sorted(parent.iterdir()) + if entry.is_dir() + and entry.name not in excluded + and not entry.name.startswith(".") + and (entry / f"{entry.name}{suffix}").exists() + ] + + +def survey(source_dir: Path, release: ReleaseTag) -> DocSetSurvey: + """Survey a source tree with the rules ``discover_doc_sets`` applies in ``scripts/convert.py``. + + The LaTeX tree takes precedence. A Sphinx directory is used only for a configured set that the + LaTeX tree does not provide. + """ + doc_dir = source_dir / "doc" + if not doc_dir.is_dir(): + raise AdoptionError(f"no doc/ directory at {source_dir}") + latex = [entry.name for entry in _directories(doc_dir, EXCLUDED_DIRS, ".tex")] + formats = {name: "latex" for name in latex if name in DOC_SET_INFO} + matched_sphinx: set[str] = set() + for doc_set in discover_rst_doc_sets(source_dir, DOC_SET_INFO): + matched_sphinx.add(doc_set.source_dir.name) + formats.setdefault(doc_set.dir_name, "rst") + sphinx = [entry.name for entry in _directories(doc_dir / SPHINX_SUBDIR, SPHINX_EXCLUDED_DIRS, ".rst")] + return DocSetSurvey( + release=release, + formats=formats, + unmatched_latex=tuple(name for name in latex if name not in DOC_SET_INFO), + unmatched_sphinx=tuple(name for name in sphinx if name not in matched_sphinx), + ) + + +def fetch_entry_documents(release: ReleaseTag, destination: Path) -> Path: + """Check out the entry document of every documentation directory of *release*, and nothing else.""" + target = destination / str(release) + commands = [ + ["git", "clone", "--quiet", "--filter=blob:none", "--no-checkout", "--depth=1", "--branch", str(release), "--single-branch", ENERGYPLUS_REPO, str(target)], + ["git", "-C", str(target), "sparse-checkout", "set", "--no-cone", "/doc/*/*.tex", f"/doc/{SPHINX_SUBDIR}/*/*.rst"], + ["git", "-C", str(target), "checkout", "--quiet"], + ] # fmt: skip + for command in commands: + done = subprocess.run(command, capture_output=True, text=True, check=False) + if done.returncode != 0: + raise AdoptionError(f"{' '.join(command[:3])} failed for {release}: {done.stderr.strip()}") + return target + + +def preceding(release: ReleaseTag) -> ReleaseTag | None: + """The newest configured release older than *release*, or ``None``.""" + older = [tag for tag in map(ReleaseTag.parse, TARGET_VERSIONS) if tag < release] + return max(older) if older else None + + +def render(current: DocSetSurvey, previous: DocSetSurvey | None) -> str: + """The report a person accepts, as Markdown.""" + + def listing(names: tuple[str, ...] | list[str]) -> list[str]: + return [f"- `{name}`" for name in names] or ["- none"] + + lines = [f"### Documentation sets of EnergyPlus {current.release}", ""] + lines += [f"Configured sets found: {len(current.formats)} of {len(DOC_SET_INFO)}.", ""] + lines += ["**Sphinx directories that no configured set matches (omitted from the site):**", ""] + lines += [*listing(current.unmatched_sphinx), ""] + lines += ["**LaTeX directories that no configured set matches (converted under a generated title):**", ""] + lines += [*listing(current.unmatched_latex), ""] + lines += ["**Configured sets found in neither tree (absent from the build):**", ""] + lines += [*listing(current.missing), ""] + if previous is not None: + moved = [ + f"{name}: {previous.formats[name]} in {previous.release}, {fmt} in {current.release}" + for name, fmt in current.formats.items() + if name in previous.formats and previous.formats[name] != fmt + ] + new = [name for name in (*current.unmatched_latex, *current.unmatched_sphinx) if name not in (*previous.unmatched_latex, *previous.unmatched_sphinx)] # fmt: skip + lines += [f"**Sets whose source format differs from {previous.release}:**", "", *listing(moved), ""] + lines += [f"**Unmatched directories that {previous.release} did not have:**", "", *listing(new), ""] + lines += [ + "Accept when each unmatched directory is either configured in `DOC_SET_INFO` or `DOC_SET_ALIASES` " + "with a title, or stated as excluded, and each set that changed format has been converted once " + f"(`make convert VERSION={current.release}`) and read.", + ] + return "\n".join(lines) + "\n" + + +def report_doc_sets(release: ReleaseTag, source: Path | None) -> str: + with tempfile.TemporaryDirectory() as scratch: + current = survey(source or fetch_entry_documents(release, Path(scratch)), release) + before = preceding(release) + previous = survey(fetch_entry_documents(before, Path(scratch)), before) if before and source is None else None + return render(current, previous) + + +# ── The command line ────────────────────────────────────────────────────────── + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description="Commands of an EnergyPlus release adoption") + commands = parser.add_subparsers(dest="command", required=True) + target = commands.add_parser( + "target-list", help="Add a release to TARGET_VERSIONS and designate the newest as latest" + ) + target.add_argument("release", help="Release tag (e.g., v25.2.0)") + sets = commands.add_parser("doc-sets", help="Report how upstream documentation directories map onto DOC_SET_INFO") + sets.add_argument("release", help="Release tag (e.g., v25.2.0)") + sets.add_argument("--source", type=Path, help="An EnergyPlus source tree to survey instead of fetching the release") + args = parser.parse_args(argv) + + try: + release = ReleaseTag.parse(args.release) + if args.command == "target-list": + changed = add_to_target_list(release) + print(f"{release}: {'added to' if changed else 'already in'} scripts/config.py") + return 0 + report = report_doc_sets(release, args.source) + except AdoptionError as error: + print(f"error: {error}", file=sys.stderr) + return 1 + print(report) + # A command the adoption run executes has its output kept only on failure. The job summary is + # where a successful run shows the report. + if summary := os.environ.get("GITHUB_STEP_SUMMARY"): + with Path(summary).open("a", encoding="utf-8") as handle: + handle.write(report) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From bd406c4df8c82d4b0c2197cc3ebbae977028672c Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Tue, 13 Oct 2026 21:01:35 -0400 Subject: [PATCH 2/3] Refuse a second patch release of one MAJOR.MINOR, and correct the doc-sets report 1. target-list refuses when another patch release of the same MAJOR.MINOR is listed. Build and published paths are named vMAJOR.MINOR, so two such entries would overwrite each other. 2. The doc-sets report compares the LaTeX and Sphinx trees separately, so an unmatched directory that moves between trees under one name is reported, and states when no preceding release was surveyed. 3. Each git command of the upstream fetch has a time limit and is named in its failure message. 4. Comments and the module docstring state that the report reaches the pull request body and the job summary, and that the newest listed release is designated LATEST_VERSION. --- .github/workflows/adopt-energyplus.yml | 3 +- scripts/adopt_release.py | 78 ++++++++++++++++++++------ 2 files changed, 62 insertions(+), 19 deletions(-) diff --git a/.github/workflows/adopt-energyplus.yml b/.github/workflows/adopt-energyplus.yml index 239fc03b1..0926afdec 100644 --- a/.github/workflows/adopt-energyplus.yml +++ b/.github/workflows/adopt-energyplus.yml @@ -16,7 +16,8 @@ name: adopt-energyplus # 1 while the installed idfkit does not list the release, because idfkit returns the newest schema # it holds for a release it does not list and the conversion would publish the wrong field # metadata without failing. The report of documentation directories that no configured set matches -# (`python -m scripts.adopt_release doc-sets`) is written to the job summary for a person to accept. +# (`python -m scripts.adopt_release doc-sets`) is copied into the pull request body, and into the +# job summary, for a person to accept. # Converting and publishing the release remains convert-docs.yml, dispatched by a maintainer after # the merge. # diff --git a/scripts/adopt_release.py b/scripts/adopt_release.py index f7262a432..5c6f79371 100644 --- a/scripts/adopt_release.py +++ b/scripts/adopt_release.py @@ -4,11 +4,13 @@ declares two commands for this project, and both are defined here. ``target-list TAG`` - Adds the release to ``TARGET_VERSIONS`` in ``scripts/config.py`` and designates it - ``LATEST_VERSION``. The command refuses, with exit status 1, when the installed idfkit does not - list the release. idfkit returns the newest schema it holds for a release it does not list, so - a conversion against an older level would publish the field metadata of the previous release - under the heading of the new one without failing. + Adds the release to ``TARGET_VERSIONS`` in ``scripts/config.py`` and designates the newest + listed release ``LATEST_VERSION``. The command refuses, with exit status 1, when the installed + idfkit does not list the release. idfkit returns the newest schema it holds for a release it + does not list, so a conversion against an older level would publish the field metadata of the + previous release under the heading of the new one without failing. The command also refuses + when another patch release of the same ``MAJOR.MINOR`` is listed, because the build and the + published path of a release are named ``vMAJOR.MINOR`` and the two would overwrite each other. ``doc-sets TAG`` Reports how the documentation directories of the upstream release map onto ``DOC_SET_INFO``: @@ -33,10 +35,11 @@ import tempfile from dataclasses import dataclass, field from pathlib import Path +from typing import Literal from idfkit.versions import ENERGYPLUS_VERSIONS -from scripts.config import DOC_SET_INFO, ENERGYPLUS_REPO, EXCLUDED_DIRS, TARGET_VERSIONS +from scripts.config import DOC_SET_INFO, ENERGYPLUS_REPO, EXCLUDED_DIRS, TARGET_VERSIONS, version_to_short from scripts.rst_doc_sets import SPHINX_EXCLUDED_DIRS, SPHINX_SUBDIR, discover_rst_doc_sets CONFIG_PATH = Path(__file__).parent / "config.py" @@ -48,6 +51,10 @@ ) _LATEST_LINE = re.compile(r'^LATEST_VERSION: str = "v\d+\.\d+\.\d+"$', re.MULTILINE) +# Upper bound for one git command of the upstream fetch. A stalled connection otherwise holds the +# adoption job until its own limit. +_GIT_TIMEOUT_SECONDS = 600 + class AdoptionError(Exception): """The command cannot be performed. The message states the reason.""" @@ -97,6 +104,15 @@ def with_release(config_text: str, release: ReleaseTag) -> str: if len(_LATEST_LINE.findall(config_text)) != 1: raise AdoptionError("LATEST_VERSION was not found exactly once in scripts/config.py") listed = {ReleaseTag.parse(tag) for tag in re.findall(r'"(v\d+\.\d+\.\d+)"', block.group("body"))} + # Every build and published path is named by version_to_short, that is vMAJOR.MINOR. Two patch + # releases of one MAJOR.MINOR would be built into, and deployed to, the same directory. + same_minor = sorted(tag for tag in listed if (tag.major, tag.minor) == (release.major, release.minor)) + if same_minor and release not in listed: + raise AdoptionError( + f"{same_minor[-1]} is listed and shares the path {version_to_short(str(release))} with {release}. " + f"Remove {same_minor[-1]} from TARGET_VERSIONS in scripts/config.py if {release} replaces it, " + "then run the command again." + ) ordered = sorted(listed | {release}) body = "".join(f' "{tag}",\n' for tag in ordered) text = config_text[: block.start("body")] + body + config_text[block.end("body") :] @@ -117,13 +133,17 @@ def add_to_target_list(release: ReleaseTag, config_path: Path = CONFIG_PATH) -> # ── The documentation sets ──────────────────────────────────────────────────── +#: The source format a configured set is found in. +SourceFormat = Literal["latex", "rst"] + + @dataclass(frozen=True) class DocSetSurvey: """How the documentation directories of one upstream release map onto ``DOC_SET_INFO``.""" release: ReleaseTag #: Configured set name to the source format it is found in, ``latex`` or ``rst``. - formats: dict[str, str] = field(default_factory=dict) + formats: dict[str, SourceFormat] = field(default_factory=dict) #: LaTeX directories with a main document that no configured set names. unmatched_latex: tuple[str, ...] = () #: Sphinx directories with a main document that no configured set or alias matches. @@ -157,7 +177,7 @@ def survey(source_dir: Path, release: ReleaseTag) -> DocSetSurvey: if not doc_dir.is_dir(): raise AdoptionError(f"no doc/ directory at {source_dir}") latex = [entry.name for entry in _directories(doc_dir, EXCLUDED_DIRS, ".tex")] - formats = {name: "latex" for name in latex if name in DOC_SET_INFO} + formats: dict[str, SourceFormat] = {name: "latex" for name in latex if name in DOC_SET_INFO} matched_sphinx: set[str] = set() for doc_set in discover_rst_doc_sets(source_dir, DOC_SET_INFO): matched_sphinx.add(doc_set.source_dir.name) @@ -171,18 +191,33 @@ def survey(source_dir: Path, release: ReleaseTag) -> DocSetSurvey: ) +@dataclass(frozen=True) +class _GitCommand: + """One git command of the upstream fetch, with the name a failure is reported under.""" + + name: str + arguments: tuple[str, ...] + + def fetch_entry_documents(release: ReleaseTag, destination: Path) -> Path: """Check out the entry document of every documentation directory of *release*, and nothing else.""" target = destination / str(release) - commands = [ - ["git", "clone", "--quiet", "--filter=blob:none", "--no-checkout", "--depth=1", "--branch", str(release), "--single-branch", ENERGYPLUS_REPO, str(target)], - ["git", "-C", str(target), "sparse-checkout", "set", "--no-cone", "/doc/*/*.tex", f"/doc/{SPHINX_SUBDIR}/*/*.rst"], - ["git", "-C", str(target), "checkout", "--quiet"], - ] # fmt: skip + commands = ( + _GitCommand("git clone", ("git", "clone", "--quiet", "--filter=blob:none", "--no-checkout", "--depth=1", "--branch", str(release), "--single-branch", ENERGYPLUS_REPO, str(target))), + _GitCommand("git sparse-checkout", ("git", "-C", str(target), "sparse-checkout", "set", "--no-cone", "/doc/*/*.tex", f"/doc/{SPHINX_SUBDIR}/*/*.rst")), + _GitCommand("git checkout", ("git", "-C", str(target), "checkout", "--quiet")), + ) # fmt: skip for command in commands: - done = subprocess.run(command, capture_output=True, text=True, check=False) + try: + done = subprocess.run( + command.arguments, capture_output=True, text=True, check=False, timeout=_GIT_TIMEOUT_SECONDS + ) + except subprocess.TimeoutExpired as expired: + raise AdoptionError( + f"{command.name} did not finish within {_GIT_TIMEOUT_SECONDS} s for {release}" + ) from expired if done.returncode != 0: - raise AdoptionError(f"{' '.join(command[:3])} failed for {release}: {done.stderr.strip()}") + raise AdoptionError(f"{command.name} failed for {release}: {done.stderr.strip()}") return target @@ -212,9 +247,16 @@ def listing(names: tuple[str, ...] | list[str]) -> list[str]: for name, fmt in current.formats.items() if name in previous.formats and previous.formats[name] != fmt ] - new = [name for name in (*current.unmatched_latex, *current.unmatched_sphinx) if name not in (*previous.unmatched_latex, *previous.unmatched_sphinx)] # fmt: skip + # The two trees are compared separately. An unmatched directory that moves from the LaTeX + # tree to the Sphinx tree under the same name changes from converted to omitted. + new = [ + *(f"{name} (LaTeX)" for name in current.unmatched_latex if name not in previous.unmatched_latex), + *(f"{name} (Sphinx)" for name in current.unmatched_sphinx if name not in previous.unmatched_sphinx), + ] lines += [f"**Sets whose source format differs from {previous.release}:**", "", *listing(moved), ""] lines += [f"**Unmatched directories that {previous.release} did not have:**", "", *listing(new), ""] + else: + lines += ["No preceding release was surveyed, so no comparison with one is reported.", ""] lines += [ "Accept when each unmatched directory is either configured in `DOC_SET_INFO` or `DOC_SET_ALIASES` " "with a title, or stated as excluded, and each set that changed format has been converted once " @@ -257,8 +299,8 @@ def main(argv: list[str] | None = None) -> int: print(f"error: {error}", file=sys.stderr) return 1 print(report) - # A command the adoption run executes has its output kept only on failure. The job summary is - # where a successful run shows the report. + # The adoption run copies the output of this step into the pull request body. The job summary + # holds the same report rendered as Markdown. if summary := os.environ.get("GITHUB_STEP_SUMMARY"): with Path(summary).open("a", encoding="utf-8") as handle: handle.write(report) From 5ec57bce04161d48396f099b371ee4f80dcd5771 Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Wed, 14 Oct 2026 08:19:14 -0400 Subject: [PATCH 3/3] Pin the adoption caller to governance-2026.25 The reusable workflow at this tag mints one scoped token per purpose, reads the roster at the called tag, waits on a dispatched run instead of dispatching it again, and refuses to push when the branch has moved. Its call interface is unchanged. --- .github/workflows/adopt-energyplus.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/adopt-energyplus.yml b/.github/workflows/adopt-energyplus.yml index 0926afdec..d28e44b49 100644 --- a/.github/workflows/adopt-energyplus.yml +++ b/.github/workflows/adopt-energyplus.yml @@ -39,10 +39,10 @@ permissions: jobs: adopt: - uses: idfkit/idfkit-conformance/.github/workflows/adopt-energyplus.yml@governance-2026.24 + uses: idfkit/idfkit-conformance/.github/workflows/adopt-energyplus.yml@governance-2026.25 with: project: idfkit-docs - governance_level: governance-2026.24 + governance_level: governance-2026.25 release: ${{ inputs.release }} secrets: APP_ID: ${{ secrets.APP_ID }}