Claudinite scheduler #45
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 }); |