Skip to content

ci-pre.yml: rename from ci-precheck, job-level concurrency (janitor only), cache janitor on every push, PR-numbered cache keys #23

Closed

Description

@zackees

Summary

This records the owner decisions from 2026-09-28 that turn #6's proto "precheck" into a concrete, fleet-wide ci-pre.yml. It is also where the Actions-cache janitor now lives. The first implementation is in zackees/soldr (in progress; linked below when the PR is up).

1. Rename: ci-pre.yml, never "precheck"

The fast first workflow is ci-pre.yml. Nothing is called "precheck": not the file, workflow name (CI Pre), job IDs, concurrency groups, step names or docs.

To do in this repo:

2. Shape

  • on: workflow_call only. It is the first job of ci.yml, so it runs on every ci.yml trigger: PR, push to any branch, schedule, dispatch.
  • Fast: ≤ 30 s when there is nothing to delete, with timeout-minutes: 2.
  • No installs: stdlib python3 only; no setup-uv, uv run or pip.
  • The calling job grants actions: write + contents: read. On fork PRs GitHub downgrades the token to read-only, and ci-pre copes with that.

3. Concurrency: per job, not per workflow

# ci-pre.yml has NO workflow-level `concurrency:`
jobs:
  cache-janitor:
    concurrency: { group: cache-janitor, cancel-in-progress: false }   # repo-wide, serialized, never cancelled mid-delete
  cache-budget:      # no concurrency: runs immediately, in parallel
  # every other ci-pre check: no concurrency
  • Only the janitor is serialized. Two sweeps must not act on stale listings in parallel. GitHub keeps one pending run per group, so a burst of pushes collapses to the latest sweep.
  • Check jobs have no lock and must not needs: cache-janitor, so a queued sweep never delays a check.
  • The proto fixture's workflow-level concurrency with cancel-in-progress: true is removed: it would cancel sweeps mid-delete.
  • The caller ci.yml may keep its own per-ref cancel group.

4. Cache janitor (cache-janitor job)

It runs on every push (all branches), plus schedule and dispatch, whenever the token can write.

  1. Merged or closed PR caches are deleted.
    • A cache belongs to PR N if its key contains a delimited pr-<N> component, or its ref is refs/pull/<N>/merge.
    • Entries are grouped by N and each PR's state is looked up. Everything of a closed PR (merged or not) is deleted.
    • A failed lookup keeps the entries (fail safe).
    • Open PRs keep their caches, so repeat pushes stay warm.
  2. Family budgets converge.
    • Evictable families (evict: lru, e.g. experiment lanes) are LRU-evicted down to their budget.
    • Pinned-download families keep the newest entry per key prefix.
    • A non-evictable family whose declared steady state can't fit its budget is a static finding (row 8), never a permanently red cron.
  3. Safety. The janitor never deletes:
    • the newest entry that a required job restores;
    • any entry younger than a grace window (a named constant, e.g. 10 min), so it can't race a producer and its consumers.

A pull_request: closed trigger in the calling workflow deletes that PR's entries immediately, including on merge. A workflow-call-only file can't take that trigger. If the token can't delete, the job logs and exits 0, and the next push sweep catches it.

5. PR number in cache keys (new rule)

  • Every cache saved in a PR context embeds pr-<N> in its key, set in one place (e.g. setup-soldr cache-key-suffix). Main, nightly and non-PR keys carry no PR tag.
  • The ref alone isn't enough: pushes to a PR's branch save under refs/heads/<branch>.
  • Proposed lint: a PR-context save without the pr- key component is a finding (CACHE-*).

6. Budget verdict (cache-budget job)

  • It evaluates live state independently of the janitor.
  • Warn-only (exit 0) in PR context and on non-main pushes, for state the PR didn't create. Fails only for entries saved under the PR's own pr-<N> / ref, or a forecast breach from its own additions.
  • On main and schedule it is a hard failure if still over budget. A transient race with an in-flight sweep resolves on the next run.

Evidence (see the #6 comments):

7. Related fleet work done the same day

Acceptance criteria

Activity

  1. zackees commented on Sep 29, 2026

    @zackees
    OwnerAuthor

    Implemented by #60, squash-merged as af6d22d.

    ci-lint side of #23:

    • Rename: ci-pre.yml is the canonical name. GEN-014 flags a ci-precheck.yml file, a uses: of one, or an [allow].workflows key naming it. The example, every fixture (CT-004, GEN-008, CACHE-009, _e2e and the rest) and the docs are renamed.
    • Concurrency:
      • GEN-015 flags a workflow-level concurrency in ci-pre.yml. WF-002 now exempts that file.
      • GEN-016 flags a job other than cache-janitor that has concurrency or needs: cache-janitor.
      • GEN-017 flags a janitor that lacks { group: cache-janitor, cancel-in-progress: false }.
    • Installs: GEN-018 flags an install step in ci-pre.yml.
    • PR cache keys: CACHE-013 flags a PR-reachable cache save without a delimited pr-<N> key component. The planner has a new --github-output key, cache_key_pr, which holds pr-<N> on a PR and is empty elsewhere.
    • Classifier: it recognizes a delimited pr-<N> anywhere in a key, still accepts the legacy delta-v1-pr<N> form, and treats a refs/pull/<N>/merge ref as belonging to PR N.
    • Janitor:
      • Deletes every entry of a closed or merged PR. A failed lookup keeps them.
      • Evicts least-recently-used entries in families marked with the new evict = "lru" key.
      • Keeps the newest entry per key prefix for other families.
      • Never deletes the newest entry a required job restores.
      • Never deletes an entry younger than 10 minutes.
    • ci-lint cache budget:
      • PR context: warn-only, except for the PR's own pr-<N> entries.
      • Default branch, schedule and dispatch: a hard fail when over budget.

    Rule IDs are GEN-014..018 and CACHE-013 because GEN-012, GEN-013 and CACHE-010..012 were taken by #57, #61 and #62 while this was in flight.

    Validation: there is no CI in ci.yml, so the selftest pair is the validation (435 tests):

    • uv run --no-project --with pyyaml python3 -m ci_lint selftest: OK
    • python3 -S -m ci_lint selftest (PyYAML not importable): OK (skipped=63)

    The RED/GREEN output for every rule is in the PR body.

    Not done here:

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

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions