-
Notifications
You must be signed in to change notification settings - Fork 0
190 lines (183 loc) · 9.44 KB
/
Copy pathharness-drift.yml
File metadata and controls
190 lines (183 loc) · 9.44 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# Harness drift monitor: watches the upstream @deepseek-ai/* release channel
# (`alpha` for alpha, `next` for rc, `latest` for stable) daily and, when the whole watched set moves to a newer
# same-minor line, performs the bump automatically:
#
# detect (script/harness-drift.mjs, deterministic, free)
# ├─ SYNC quiet green
# ├─ PARTIAL issue — wait for the whole set (version.spec forbids
# │ a split tree; upstream ships the monorepo in lockstep)
# ├─ MINOR_JUMP issue — R1 ruling: never chase jumps past the minor
# └─ BUMP_READY ─┐
# ▼
# bump: dsh --profile headless agent edits files ONLY (the task prompt is
# generated by script/harness-drift-task.mjs; hard rules: no git, no
# .github/, version.spec is the completeness proof), then the workflow
# takes over deterministically: full gates → only if ALL green →
# commit as github-actions[bot], force-push automation/harness-bump,
# upsert the PR.
#
# Why the gates live in THIS workflow instead of riding the PR: events
# produced by GITHUB_TOKEN (the push, the PR creation) do not trigger new
# workflow runs — a GITHUB_TOKEN-opened PR never starts ci.yml on its own.
# Running the gates here means the PR only ever appears in a green state;
# the human merge push then triggers main's ci.yml as the final check.
# (PAT evolution note: if branch-protection required checks ever appear,
# switch the push to a PAT secret and the PR gains native runs.)
#
# Why the agent may never touch .github/: GITHUB_TOKEN cannot push commits
# that modify workflow files — GitHub rejects the push outright. ci.yml's
# CLI pin is derived from HARNESS_LINE (script/harness-line.mjs), so a line
# bump has no reason to touch a workflow file at all.
#
# The dispatch `apply` input is an explicit human intent: it may be run
# from any branch (that is the rehearsal path — pin the tree back to an
# old line on a branch, dispatch apply, watch the full pipeline). The
# schedule only ever runs on the default branch.
name: harness-drift
on:
schedule:
- cron: '23 1 * * *' # daily 01:23 UTC / 09:23 Beijing; off the :00 wall
workflow_dispatch:
inputs:
apply:
description: 'apply the bump (agent + gates + PR); default check-only'
type: boolean
default: false
permissions:
contents: write # push automation/harness-bump
pull-requests: write # PR upsert
issues: write # drift and gate-failure tracking
concurrency:
group: harness-drift
cancel-in-progress: false
env:
PRIMARY_NODE_VERSION: '24'
jobs:
detect:
if: github.repository == 'Ephemeral-AI-Lab/mayfly'
runs-on: ubuntu-latest
outputs:
state: ${{ steps.classify.outputs.state }}
current: ${{ steps.classify.outputs.current }}
target: ${{ steps.classify.outputs.target }}
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: ${{ env.PRIMARY_NODE_VERSION }}
- id: classify
run: |
# Capture the exit code WITHOUT a pipeline: the default shell has
# no pipefail, so `node | tee || code=$?` reads tee's 0 and turns
# a BUMP_READY (10) into a bogus sync (caught in the first
# apply rehearsal — run 32561189281).
code=0
node script/harness-drift.mjs > drift-report.txt || code=$?
cat drift-report.txt
case "$code" in
0) state=sync ;;
10) state=bump-ready ;;
20) state=minor-jump ;;
30) state=partial ;;
*) echo "detect failed with exit $code"; exit 1 ;;
esac
current=$(node script/harness-line.mjs)
target=$(sed -n 's/^DRIFT_TARGET=//p' drift-report.txt)
echo "state=$state" >> "$GITHUB_OUTPUT"
echo "current=$current" >> "$GITHUB_OUTPUT"
echo "target=${target:-none}" >> "$GITHUB_OUTPUT"
echo "detect: state=$state current=$current target=${target:-none}"
bump:
needs: detect
if: needs.detect.outputs.state == 'bump-ready'
&& (github.event_name == 'schedule' || inputs.apply == true)
runs-on: ubuntu-latest
timeout-minutes: 45
env:
GH_TOKEN: ${{ github.token }}
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
DRIFT_CURRENT: ${{ needs.detect.outputs.current }}
DRIFT_TARGET: ${{ needs.detect.outputs.target }}
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:
node-version: ${{ env.PRIMARY_NODE_VERSION }}
cache: pnpm
# The agent runs version.spec against package-name entry points, so
# the workspace needs a frozen install and a build before it starts.
- run: pnpm install --frozen-lockfile
- run: pnpm build
# The headless CLI rides the CURRENT pin (it is tooling, not the bump
# target); the version pin is single-sourced like ci.yml.
- run: npm install -g "@deepseek-ai/dsh@$(node script/harness-line.mjs)"
- name: Run the bump agent (files only — no git, no .github/)
run: dsh --profile headless "$(node script/harness-drift-task.mjs)"
- name: The agent must have changed something
run: |
if git status --porcelain | grep -q .; then
echo "agent tree diff:"; git status --porcelain
else
echo "the bump agent made no changes"; exit 1
fi
# Deterministic takeover: nothing the agent claims is trusted until
# the full gate set passes on its tree.
- run: pnpm install --no-frozen-lockfile --config.minimumReleaseAge=0
- run: pnpm typecheck && pnpm lint && pnpm diagrams:check && pnpm build && pnpm check:lib
- run: pnpm test:coverage
- run: npm install -g "@deepseek-ai/dsh@${{ env.DRIFT_TARGET }}"
- run: pnpm smoke:happy
- name: Push automation branch and upsert the PR (all gates green)
run: |
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
git checkout -B automation/harness-bump
git add -A
git commit -m "chore(harness): bump the pinned line ${DRIFT_CURRENT} -> ${DRIFT_TARGET} (harness-drift auto)"
git push --force origin automation/harness-bump
run_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
title="harness-drift: bump ${DRIFT_CURRENT} → ${DRIFT_TARGET} (auto)"
body="Automated harness-line bump. The full gate set (typecheck / lint / diagrams / build / check:lib / test:coverage / smoke:happy against ${DRIFT_TARGET}) ran INSIDE the drift run — a GITHUB_TOKEN-opened PR does not trigger ci.yml on its own, so the PR's checks tab stays empty by design; see ${run_url}. The human merge push re-triggers main's ci.yml as the final check. Review the narrative edits (roadmap R1 row, session-title bridge verdict) alongside the pins."
number=$(gh pr list --head automation/harness-bump --state open --json number --jq '.[0].number')
if [ -n "$number" ]; then
gh pr edit "$number" --title "$title" --body "$body"
gh pr comment "$number" --body "rebuilt for ${DRIFT_TARGET}: ${run_url}"
else
gh pr create --base main --head automation/harness-bump --title "$title" --body "$body"
fi
alert:
needs: [detect, bump]
if: always()
&& (needs.detect.outputs.state == 'minor-jump'
|| needs.detect.outputs.state == 'partial'
|| needs.detect.result == 'failure'
|| needs.bump.result == 'failure')
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }}
# No checkout in this job: every gh call carries the repo explicitly
# (without a git context gh fails with "not a git repository").
GH_REPO: ${{ github.repository }}
steps:
- name: Upsert the drift tracking issue
run: |
state="${{ needs.detect.outputs.state }}"
current="${{ needs.detect.outputs.current }}"
target="${{ needs.detect.outputs.target }}"
run_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
case "$state" in
minor-jump) note="The monitored registry tags moved to ${target}, past the pinned ${current}'s minor (or rolled back). R1 ruling: never chase it automatically — human ruling required." ;;
partial) note="The watched packages' monitored tags disagree (pinned ${current}). Upstream ships in lockstep, so this is usually a publish-window transient; the next daily run retries. If a package stays stuck, prune its exclude entry or shrink the watch set in script/harness-drift.mjs." ;;
*) note="The harness-drift run failed (state=${state:-unknown}, detect=${{ needs.detect.result }}, bump=${{ needs.bump.result }}). See the run log; a failed gate means the bump agent's tree did not pass and no PR was opened." ;;
esac
title="harness drift: ${state:-failure} (pinned ${current}, registry ${target})"
body="${note}
Run: ${run_url}
The daily schedule retries automatically; this issue tracks until a human closes it."
number=$(gh issue list --state open --search "harness drift in:title" --json number --jq '.[0].number')
if [ -n "$number" ]; then
gh issue comment "$number" --body "${state:-failure} @ ${target:-?}: ${run_url}"
else
gh issue create --title "$title" --body "$body"
fi