Skip to content

Keep the clock's state in the served repository - #3

Merged
jeffwall-curlewlabs merged 1 commit into
mainfrom
feat/clock-state-in-repo
Oct 5, 2026
Merged

jeffwall-curlewlabs merged 1 commit into
mainfrom
feat/clock-state-in-repo

Conversation

@jeffwall-curlewlabs

Copy link
Copy Markdown
Contributor

Closes #2.

Why

The clock kept what it had sent in a local state file. That tied a repository's on-time delivery to one host, and a second host would have sent every slot again.

What changes

  • Marks. Before sending a slot, a clock claims it by creating refs/github-cron-trigger-clock/<workflow file>/<cron line>/<slot> in the served repository (clock_marks.py).
    • Ref creation is atomic, so of several clocks ticking at once exactly one sends each slot.
    • A failed send releases its mark, so the next tick on any host retries.
    • A clock stopped between its claim and its send leaves that slot to GitHub's backstop.
  • Baselines without local memory.
    • A line with no marks is baselined.
    • A workflow that stops being enrolled, or a line that is gone, has its marks removed, so enrolling again baselines again.
    • Each line keeps its two newest marks, so a released claim still leaves one to retry from.
    • Marks of a workflow file that could not be read or judged this tick are left alone, so a broken commit to a workflow does not turn into a baseline once it is fixed.
  • --state is removed. A host keeps nothing. Without --send a tick is a dry run: it reports what it would do and writes nothing.
  • Shared ref primitives. slot_ledger gains create_ref, delete_ref and list_refs, and the ledger and the marks both use them. The ledger's behavior is unchanged.
  • CI.
    • clock-check.yml is a do-nothing workflow enrolled so the live check can run real clocks against this repository.
    • The live check starts two tick --send processes at once and checks that a line with no marks is baselined once with nothing sent, and that a due slot is sent exactly once. The clock reads the default branch, so on this pull request that part skips with a notice, since main has no clock-check.yml yet. On main and on scheduled runs it is required.
    • The live job runs one at a time across refs, since it claims and removes marks.
  • Version 0.2.0.

Checked

  • ruff, strict mypy and actionlint are clean.
  • The unit tests pass on 3.14 and 3.10. The clock tests use an in-memory repository whose claims are atomic, and cover:
    • two clocks racing a send, and racing a baseline;
    • a failed send, then its retry;
    • a slot the ledger already records;
    • enrolling and disenrolling;
    • pruning;
    • an unreadable file keeping its marks;
    • a dry run writing nothing;
    • marks that cannot be read, so nothing is sent.
  • Against the live API, in a private repository under a throwaway workflow name, removed afterwards:
    • a first claim creates the mark, and a second claim of the same slot is refused;
    • the marks list in slot order;
    • release is idempotent.
    • The ledger round trip passes unchanged after the refactor.
  • A dry-run tick against a real repository with 20 enrolled lines reported a baseline for each and wrote nothing.

The clock remembered what it had sent in a local state file, which tied
delivery to one host: while that host slept every slot waited for GitHub's
backstop, and a second host would have sent every slot again.

A clock now claims a slot before sending it, by creating a ref under
refs/github-cron-trigger-clock/ in the served repository. Ref creation is
atomic, so of any number of clocks ticking at once exactly one sends each
slot. A failed send releases its claim, so the next tick on any host
retries. A line with no marks is baselined; a workflow that stops being
enrolled loses its marks, so enrolling it again baselines it again; each
line keeps its two newest marks. Marks of a workflow file that could not be
read this tick are left alone.

`--state` is gone: a host keeps nothing. Without `--send` a tick is a dry
run that reports what it would do and writes nothing.

The ledger's ref primitives (create with a note, delete, list) are shared
with the marks. CI's live check runs two clocks at once against a workflow
enrolled for it, clock-check.yml, and checks each slot is sent once.

Closes #2.
@jeffwall-curlewlabs

Copy link
Copy Markdown
Contributor Author

This lands through #5, which is retargeted to main and carries this commit plus #4's. One CI run covers both, instead of finishing this run and then running #5's. Merging #5 merges this commit, and GitHub marks this pull request merged.

@jeffwall-curlewlabs
jeffwall-curlewlabs merged commit 1c8bff4 into main Oct 5, 2026
11 of 16 checks passed
@jeffwall-curlewlabs
jeffwall-curlewlabs deleted the feat/clock-state-in-repo branch October 5, 2026 20:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Clock: keep its state in each served repository, so several hosts can run clocks

1 participant