Hands-off Time: spec planner, auto-merge on Approve + green checks, next card, update workflow
- spec issues (label spec) become queued cards, parsed deterministically, seat fallback - TIME_AUTOMERGE=on: squash-merge when the latest trusted verdict is Approve and all checks pass - Object, red or missing checks, or protected paths go to needs-human - merged card frees the slot; the next unblocked queued card gets ready and dispatches in the same run - update.yml pulls the engine from teahubxyz/time into project repos via PR - ci.yml runs Time's own tests Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UpT7eWzeLhRwuYmsSQnbRb
This commit is contained in:
1 parent
9108adf4c6
commit
889e93799e
9 files changed
+379
-14
No files matched your search
@@ -0,0 +1,15 @@
|
||||
name: ci
|
||||
# Time's own tests. Also the check that lets Time auto-merge changes to itself.
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 3
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- run: python3 .github/time/test_time.py
|
||||
@@ -21,3 +21,4 @@ jobs:
|
||||
TIME_MODE: ${{ vars.TIME_MODE }}
|
||||
TIME_DAILY_CAP: ${{ vars.TIME_DAILY_CAP }}
|
||||
TIME_REVIEW: ${{ vars.TIME_REVIEW }}
|
||||
TIME_AUTOMERGE: ${{ vars.TIME_AUTOMERGE }}
|
||||
@@ -1,21 +1,32 @@
|
||||
name: heartbeat
|
||||
# Time's clock: hourly, and right away when a card gets the `ready` label (dry-run until TIME_MODE=live).
|
||||
# Time's clock: hourly, right away when a card gets `ready` or a spec gets `spec`, and when a PR is reviewed
|
||||
# (dry-run until TIME_MODE=live; merges only with TIME_AUTOMERGE=on).
|
||||
on:
|
||||
schedule:
|
||||
- cron: "7 * * * *"
|
||||
workflow_dispatch:
|
||||
issues:
|
||||
types: [labeled]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
permissions:
|
||||
contents: read
|
||||
contents: write # squash-merge approved, green PRs
|
||||
issues: write
|
||||
pull-requests: write
|
||||
checks: read
|
||||
concurrency:
|
||||
group: heartbeat
|
||||
cancel-in-progress: false
|
||||
jobs:
|
||||
tick:
|
||||
if: github.event_name != 'issues' || github.event.label.name == 'ready'
|
||||
# issues: only `ready`/`spec` labels. comments: only on PRs, only a reviewer verdict (Approve/Object).
|
||||
if: >-
|
||||
(github.event_name != 'issues' || contains(fromJSON('["ready","spec"]'), github.event.label.name)) &&
|
||||
(github.event_name != 'issue_comment' || (github.event.issue.pull_request &&
|
||||
(startsWith(github.event.comment.body, '**Approve') || startsWith(github.event.comment.body, 'Approve') ||
|
||||
startsWith(github.event.comment.body, '**Object') || startsWith(github.event.comment.body, 'Object'))))
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
@@ -32,3 +43,8 @@ jobs:
|
||||
TIME_CURSOR_MODEL: ${{ vars.TIME_CURSOR_MODEL }}
|
||||
TIME_REVIEW: ${{ vars.TIME_REVIEW }}
|
||||
TIME_STUCK_HOURS: ${{ vars.TIME_STUCK_HOURS }}
|
||||
TIME_AUTOMERGE: ${{ vars.TIME_AUTOMERGE }}
|
||||
TIME_REQUIRE_CHECKS: ${{ vars.TIME_REQUIRE_CHECKS }}
|
||||
TIME_PROTECTED: ${{ vars.TIME_PROTECTED }}
|
||||
TIME_PARALLEL: ${{ vars.TIME_PARALLEL }}
|
||||
TIME_PLANNER: ${{ vars.TIME_PLANNER }}
|
||||
@@ -0,0 +1,47 @@
|
||||
name: update
|
||||
# Pull a fresh Time engine from the central repo and open a PR with the diff (weekly, or run it by hand).
|
||||
# Upstream: TIME_UPSTREAM (default teahubxyz/time) at TIME_CHANNEL (default main; pin a tag like v1 for stability).
|
||||
# Workflow files can't be changed by Actions' own token, so if those differ the PR lists them for a human to copy.
|
||||
on:
|
||||
schedule:
|
||||
- cron: "17 9 * * 1"
|
||||
workflow_dispatch:
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
jobs:
|
||||
update:
|
||||
if: github.repository != (vars.TIME_UPSTREAM || 'teahubxyz/time')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 3
|
||||
env:
|
||||
UPSTREAM: ${{ vars.TIME_UPSTREAM || 'teahubxyz/time' }}
|
||||
CHANNEL: ${{ vars.TIME_CHANNEL || 'main' }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- run: git clone -q --depth 1 --branch "$CHANNEL" "https://github.com/$UPSTREAM" /tmp/up
|
||||
- name: copy engine, open PR if changed
|
||||
run: |
|
||||
set -e
|
||||
for f in .github/time/time.py .github/time/test_time.py .github/time/seats.json .claude/skills/time/SKILL.md; do
|
||||
mkdir -p "$(dirname "$f")"; cp "/tmp/up/$f" "$f"
|
||||
done
|
||||
python3 .github/time/test_time.py > /dev/null
|
||||
wf=$(for f in /tmp/up/.github/workflows/*.yml; do b=.github/workflows/$(basename "$f"); cmp -s "$f" "$b" || echo "- \`$b\`"; done)
|
||||
if git diff --quiet; then
|
||||
echo "Time engine is up to date." | tee -a "$GITHUB_STEP_SUMMARY"
|
||||
[ -n "$wf" ] && printf 'Workflow files differ upstream, copy by hand:\n%s\n' "$wf" | tee -a "$GITHUB_STEP_SUMMARY"
|
||||
exit 0
|
||||
fi
|
||||
sha=$(git -C /tmp/up rev-parse --short HEAD)
|
||||
branch="time-update-$sha"
|
||||
git switch -c "$branch"
|
||||
git -c user.name=time -c user.email=time@users.noreply.github.com commit -qam "Update Time to $UPSTREAM@$sha"
|
||||
git push -q origin "$branch"
|
||||
body="Fresh Time engine from \`$UPSTREAM@$CHANNEL\` ($sha). Offline tests pass."
|
||||
[ -n "$wf" ] && body="$body
|
||||
|
||||
These workflow files differ upstream. Actions can't edit workflows, so copy them by hand from https://github.com/$UPSTREAM/tree/$CHANNEL/.github/workflows:
|
||||
$wf"
|
||||
gh pr create --title "Update Time ($sha)" --body "$body" --head "$branch"
|
||||
Reference in new issue
Block a user