Skip to content

promote: when the canonical copy is gone or excluded, a by-name or --toc run silently makes a stale copy canonical and reverts the pool #24

Open

Description

@mmcky

Follow-up from the review of #14. Line numbers are at #14's head as it merges.

What happens

resolve_sources judges whether a copy is stale only while the entry's recorded canonical series is eligible in the run (tools/promote:719). When the canonical copy is gone upstream, or its series is excluded from the run, nothing is judged. A by-name run (promote SLUG) or --toc of the series holding the stale copy then anchors on that copy and makes it canonical. The pool goes back to the earlier version and loses the canonical copy's later edits. The run exits 0 with an ordinary promoted line and no warning.

The refresh refuses the same entry, naming the removal, so the two modes disagree. The by-name run also hides what happened. The rewritten entry no longer records the removed series, so the drift-check goes clean, and its canonical-removed issue closes itself. That issue is the one asking for a decision about whether the lecture was retired, renamed or moved.

Reproduction (with the tools/tests harness)

repo = make_repo(["advanced", "dp-test"])
for series in ("advanced", "dp-test"):
    repo.lecture(series, "amss", lecture_text("AMSS"))
assert repo.promote("amss").returncode == 0                 # sources: advanced, dp-test
repo.lecture("advanced", "amss", lecture_text("AMSS", "A fix upstream."))
repo.move_pin("advanced")
assert repo.promote("--refresh").returncode == 0            # dp-test kept as a stale source
assert "A fix upstream." in repo.pool("amss.md").read_text()

(repo.root / "mirror" / "advanced" / "lectures" / "amss.md").unlink()
repo.move_pin("advanced", 3)                                 # advanced removes (or renames) amss.md
assert "[canonical-removed] lectures/amss.md" in repo.drift_check().stdout
assert repo.promote("--refresh").returncode == 1            # the refresh refuses, naming the removal

res = repo.promote("amss")                                   # a by-name (or --toc dp-test) run
assert res.returncode == 0 and "WARN" not in res.stdout
assert repo.entry("amss")["canonical"] == "dp-test"
assert "A fix upstream." not in repo.pool("amss.md").read_text()   # the pool lost the fix
assert repo.drift_check().returncode == 0                   # and the drift-check goes quiet

The same happens to promote amss --exclude advanced while advanced still holds its copy.

When it bites

Only on a manual run after a canonical copy has been removed or renamed upstream. CI's refresh refuses the entry, so the run has to be by name or by toc: a whole-estate promotion, the --toc dp re-homing at QuantEcon/project-monorepo#24, or an operator re-promoting a lecture. The renames in QuantEcon/project-monorepo#76 are the first realistic trigger. Main behaves the same way.

Suggested fix

When the entry's recorded canonical series no longer holds the lecture (or is excluded from the run), and the run would anchor on a recorded source still at an earlier digest, refuse the entry. The refusal should say so and point at the canonical-removed decision, unless sync/canonical.yml names that series. Take care not to refuse a source that is the same lecture only after normalisation: one test is whether promoting it would change the pool file at all. A test like the one above, inverted, covers it.

Activity

  1. added
    bugSomething isn't working
    on Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions