Skip to content

Commit fe94aa9

Browse files
committed
repomatic init no longer writes the running version's workflow content beside a pin the cooldown held back
1 parent 118f38a commit fe94aa9

3 files changed

Lines changed: 92 additions & 4 deletions

File tree

changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- Teach the `repomatic-ship` skill to reconcile bundled skills and agents as a third pass, judge a false-positive autofix PR against current `main` before writing a lint rule for it, and tell a superseded intra-cycle measurement from a genuine contradiction.
99
- The tool runner now retries a download up to 3 times on transient network failures, instead of failing the job on a one-off TLS or truncation error.
1010
- Standalone binary tests now run for every healthy target when a sibling build fails, instead of being skipped wholesale.
11+
- `repomatic init` no longer writes the running version's workflow content beside a pin the cooldown held back. A repository that already carries workflows keeps them untouched until the release is adopted, instead of receiving the new triggers, `concurrency` groups and `env:` blocks against the pinned release's reusable-workflow surface.
1112

1213
## [`7.9.0` (2026-08-10)](https://github.com/kdeldycke/repomatic/compare/v7.8.0...v7.9.0)
1314

repomatic/init_project.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -609,9 +609,12 @@ def resolve_default_pin(
609609
from the running version, so a ref naming an older release ships that
610610
content against an older reusable-workflow surface, which GitHub rejects as
611611
soon as the two disagree (see
612-
`test_thin_caller_workflow_call_inputs_stay_minimal`). Confining the
613-
step-back to a first-time adoption keeps that skew where no alternative
614-
exists.
612+
`test_thin_caller_workflow_call_inputs_stay_minimal`). Returning such a pin
613+
is therefore only half a decision: {func}`run_init` reads it back and, when
614+
the repository already carries workflows, skips regenerating them so the
615+
tree stays coherent at the pin it keeps. A first-time adoption has no tree
616+
to keep, so there the skew stands as the only alternative to writing no
617+
workflows at all.
615618
```
616619
617620
:param config: Repomatic config supplying the `minimum-release-age` window.
@@ -1059,12 +1062,42 @@ def run_init(
10591062
for name in selected
10601063
)
10611064
if cooldown and workflows_selected:
1065+
floor = _highest_upstream_pin(output_dir, repo)
10621066
version, commit_sha = resolve_default_pin(
10631067
config,
10641068
repo=repo,
10651069
warnings=result.warnings,
1066-
floor=_highest_upstream_pin(output_dir, repo),
1070+
floor=floor,
10671071
)
1072+
# A pin the cooldown held below the running version cannot carry
1073+
# this version's caller content. `init` renders bodies from the
1074+
# running wheel and substitutes only the ref, so writing them
1075+
# beside an older `uses:` pin ships half an adoption: the trigger
1076+
# blocks, `concurrency` groups and `env:` of the new release,
1077+
# against the reusable-workflow surface of the old one. Declining
1078+
# the pin has to decline the content with it, or the cooldown buys
1079+
# nothing while the caller half lands anyway, silently.
1080+
#
1081+
# Gated on *floor*, because only a repository that already carries
1082+
# workflows has somewhere to stand: leaving them untouched keeps a
1083+
# coherent tree at *floor*. A first-time adoption has no such
1084+
# fallback, since skipping would write no workflows at all, and is
1085+
# the one case `resolve_default_pin` documents the skew as
1086+
# unavoidable.
1087+
if floor is not None and is_newer(
1088+
_base_version(), version.removeprefix("v")
1089+
):
1090+
_note_cooldown(
1091+
result.warnings,
1092+
f"Leaving the workflows at {version}: their content belongs "
1093+
f"to repomatic {_base_version()}, which the pin above "
1094+
"declines. Pass --no-cooldown to adopt both together.",
1095+
)
1096+
selected -= {
1097+
name
1098+
for name in selected
1099+
if isinstance(COMPONENTS_BY_NAME.get(name), WorkflowComponent)
1100+
}
10681101
else:
10691102
version = default_version_pin()
10701103

tests/test_init_project.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,15 @@ def test_default_version_pin():
694694
Candidate("7.4.2", "2026-07-31", "v7.4.2"),
695695
]
696696

697+
# The same three releases, dated so the verdict holds whatever the real clock
698+
# says: 7.4.1 is always aged, 7.4.2 always fresh. Required by any test driving
699+
# `run_init`, which owns the cooldown comparison internally and exposes no
700+
# `today` hook to freeze, unlike `resolve_default_pin`.
701+
_CLOCK_ROBUST_CANDIDATES = [
702+
Candidate("7.4.1", "2020-01-01", "v7.4.1"),
703+
Candidate("7.4.2", "2999-01-01", "v7.4.2"),
704+
]
705+
697706

698707
@pytest.mark.parametrize(
699708
("base", "expected"),
@@ -965,6 +974,51 @@ def test_resolve_default_pin_held_at_floor_warns(pin_build):
965974
assert "7.4.2" in warnings[0] and "v7.4.1" in warnings[0]
966975

967976

977+
def test_run_init_held_at_floor_leaves_workflows_untouched(tmp_path, pin_build):
978+
"""Declining the pin declines the caller content with it.
979+
980+
`init` renders workflow bodies from the running wheel and substitutes only
981+
the `uses:` ref, so regenerating them under a held-back pin ships the new
982+
release's triggers, `concurrency` groups and `env:` against the old
983+
release's reusable-workflow surface. Half an adoption is worse than none:
984+
the caller half lands with nothing pinned to serve it.
985+
"""
986+
pin_build(version="7.4.1", sha="c" * 40)
987+
run_init(output_dir=tmp_path, components=("workflows",), cooldown=False)
988+
989+
# Running version moves inside the cooldown window; the repo stays at 7.4.1.
990+
pin_build(version="7.4.2", candidates=_CLOCK_ROBUST_CANDIDATES)
991+
result = run_init(output_dir=tmp_path, components=("workflows",))
992+
993+
touched = [path for path in result.created + result.updated if "workflows/" in path]
994+
assert not touched, f"held-back pin still rewrote {touched}"
995+
autofix = (tmp_path / ".github" / "workflows" / "autofix.yaml").read_text(
996+
encoding="UTF-8"
997+
)
998+
assert f"autofix.yaml@{'c' * 40} # v7.4.1" in autofix
999+
assert any("Leaving the workflows" in warning for warning in result.warnings)
1000+
1001+
1002+
def test_run_init_first_adoption_writes_workflows_despite_step_back(
1003+
tmp_path, pin_build
1004+
):
1005+
"""A repository with no pin yet still gets workflows when the cooldown bites.
1006+
1007+
The counterpart carve-out: holding the content back needs somewhere to
1008+
stand, and a first-time adoption has no prior tree to keep. Skipping here
1009+
would leave the repository with no workflows at all, so the pin/content
1010+
skew stands as the lesser outcome.
1011+
"""
1012+
pin_build(candidates=_CLOCK_ROBUST_CANDIDATES, tag_sha="b" * 40)
1013+
result = run_init(output_dir=tmp_path, components=("workflows",))
1014+
1015+
assert any("workflows/" in path for path in result.created)
1016+
autofix = (tmp_path / ".github" / "workflows" / "autofix.yaml").read_text(
1017+
encoding="UTF-8"
1018+
)
1019+
assert f"autofix.yaml@{'b' * 40} # v7.4.1" in autofix
1020+
1021+
9681022
def test_run_init_keeps_a_fresh_pin_already_on_disk(tmp_path, monkeypatch, pin_build):
9691023
"""Re-running `init` at the pinned version regenerates without downgrading.
9701024

0 commit comments

Comments
 (0)