Skip to content

Claudinite scheduler #42

Claudinite scheduler

Claudinite scheduler #42

# Claudinite per-repo task scheduler — the repo's ONLY cron
# (per-project-scheduling DESIGN §3). A thin shim: all logic lives in the engine,
# so this file changes rarely. It runs hourly on this repo's stable hashed minute
# constrained to :10–:50 (spreading the fleet, dodging GitHub's :00 stampede, and
# staying clear of the hour boundary the slot math anchors on).
#
# The canon variant: unlike a vendored consumer — whose shim calls
# `.claudinite/shared/engine/scheduler/run.mjs` from its mount — the canon runs its
# OWN engine at the repo root (`engine/scheduler/run.mjs`); it has no `.claudinite/
# shared/` mount because it IS the source. The minute (44) is
# `hashedMinute('missingbulb/Claudinite')` and is re-derivable by
# `node engine/scheduler/hash-minute.mjs missingbulb/Claudinite`.
name: Claudinite scheduler
on:
schedule:
- cron: '44 * * * *'
workflow_dispatch:
inputs:
overrides:
description: 'Manual-run overrides: comma-separated KEY=value. FORCE_TASKS=<task ids> runs those tasks unconditionally, even when their slot has passed, e.g. FORCE_TASKS=baselining.'
required: false
default: ''
concurrency:
group: claudinite-scheduler
cancel-in-progress: false
permissions:
contents: write # read the repo; migrations-retire's TTL archiver moves records + baselining's deliver() (n/a on canon — self-skips)
issues: write # file / label / close dispatch issues
pull-requests: write # migrations-retire opens the archive PR; agentful tasks' delivery
actions: read # read the run ledger (last successful run) for due-slot math
jobs:
schedule:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: 24
- name: Evaluate schedule and dispatch due tasks
env:
GITHUB_TOKEN: ${{ github.token }}
FLEET_GITHUB_TOKEN: ${{ secrets.FLEET_GITHUB_TOKEN }}
# Empty on every scheduled run — `inputs` is only populated by
# workflow_dispatch. The engine reads only FORCE_TASKS out of it.
CLAUDINITE_OVERRIDES: ${{ inputs.overrides }}
run: node engine/scheduler/run.mjs
# A scheduled run reaches no one when it goes red, so escalate a whole-run
# failure to a human-visible `workflow-failure` issue (DESIGN §3.6,
# gha/scheduled-failure-escalation). Converges to ONE open issue by a fixed
# title (create-or-comment), and ensures the label first since a first-run
# failure may precede the scheduler's own label-ensure.
report-failure:
needs: schedule
if: failure()
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/github-script@v7
with:
script: |
const { owner, repo } = context.repo;
const title = 'Claudinite scheduler run failed';
await github.rest.issues.createLabel({ owner, repo, name: 'workflow-failure', color: 'b60205', description: 'Claudinite scheduler: a scheduler run or task failed' }).catch(() => {});
const run = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;
const body = `The Claudinite scheduler run failed: ${run}`;
const found = await github.rest.search.issuesAndPullRequests({ q: `repo:${owner}/${repo} is:issue is:open in:title "${title}"` });
if (found.data.total_count > 0) await github.rest.issues.createComment({ owner, repo, issue_number: found.data.items[0].number, body });
else await github.rest.issues.create({ owner, repo, title, labels: ['workflow-failure'], body });