Skip to content

Commit c6548ca

Browse files
authored
fix(framework): make the GitHub Actions run target actually run end to end (#1050) (#1082)
* fix(framework): grant id-token: write so the agent workflow can authenticate anthropics/claude-code-action@v1 exchanges an OIDC token to authenticate the subscription OAuth token (claude_code_oauth_token). Without id-token: write in the workflow permissions every run fails with "Could not fetch an OIDC token. Did you remember to add id-token: write to your workflow permissions?", so the agent step never runs. Refs #1050 * fix(framework): make the Actions run correlation id unique per process The correlation id seeded a module-level session counter, so a fresh `framework run` process (the daemon spawns one per run) restarted it at 1 and every run's first turn was `actions-1-turn-1`. A new run could then match an earlier, identically named run still in the recent-runs window and return its stale conclusion. Mix a random tag into the session id so a run only ever finds its own workflow run. Refs #1050
1 parent b4905df commit c6548ca

4 files changed

Lines changed: 33 additions & 4 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@gemstack/framework": patch
3+
---
4+
5+
Give each GitHub Actions run (`--run-on actions`, #1050) a correlation id that is unique across driver processes. The id seeded a per-process counter, so a fresh `framework run` process restarted it at 1 and every run's first turn was `actions-1-turn-1`; a new run could then match an earlier, identically named run still in the recent-runs window and report its stale result. The session id now mixes in a random tag, so a run only ever finds its own workflow run.

.github/workflows/framework-agent.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,13 @@ on:
2222
description: A prior agent session id to continue instead of starting fresh.
2323
required: false
2424

25-
# The agent pushes a branch and may open a PR; nothing here needs write access to anything else.
25+
# The agent pushes a branch and may open a PR. `id-token: write` is not optional: the
26+
# action exchanges an OIDC token to authenticate the subscription OAuth token, and without
27+
# it every run fails with "Could not fetch an OIDC token".
2628
permissions:
2729
contents: write
2830
pull-requests: write
31+
id-token: write
2932

3033
jobs:
3134
agent:

packages/framework/src/driver/actions.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,14 @@ test('ActionsDriver dispatches the prompt with a correlation id and the framing
163163
assert.equal(body.inputs['correlation_id'], `${session.id}-turn-1`)
164164
})
165165

166+
test('ActionsDriver gives each session a unique correlation prefix so a fresh process never matches a stale run (#1050)', async () => {
167+
// The daemon spawns a fresh process per run, restarting the session counter, so without a
168+
// random tag two runs would both be `actions-1-...` and one could latch onto the other's run.
169+
const a = await makeDriver().driver.start({ cwd: '/ws' })
170+
const b = await makeDriver().driver.start({ cwd: '/ws' })
171+
assert.notEqual(a.id, b.id)
172+
})
173+
166174
test('ActionsDriver runs the next turn on the branch the last one pushed (#610)', async () => {
167175
const { driver, calls } = makeDriver()
168176
const session = await driver.start({ cwd: '/ws' })

packages/framework/src/driver/actions.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { randomUUID } from 'node:crypto'
12
import { StreamJsonParser } from './claude-code.js'
23
import { readZip } from './actions-zip.js'
34
import { combineFraming, makeEmit } from './session-support.js'
@@ -71,13 +72,22 @@ export interface ActionsDriverOptions {
7172
now?: () => number
7273
/** Sleep override for tests. Default a real timer. */
7374
sleep?: (ms: number) => Promise<void>
75+
/**
76+
* Unique tag mixed into the correlation id. Default a random token; injected in tests for
77+
* a stable id. Without it a fresh driver process restarts the session counter at 1, so
78+
* every run's first turn is `actions-1-turn-1` and runs collide (see {@link ActionsSession}).
79+
*/
80+
runTag?: () => string
7481
}
7582

7683
/** The slice of `fetch` this driver uses. */
7784
export type FetchLike = (url: string, init?: RequestInit) => Promise<Response>
7885

7986
let sessionCounter = 0
8087

88+
/** A short random tag so correlation ids stay unique across driver processes. */
89+
const randomRunTag = (): string => randomUUID().slice(0, 8)
90+
8191
/** One Actions-backed session. Each `prompt` is one workflow run. */
8292
export class ActionsSession implements DriverSession {
8393
readonly id: string
@@ -93,7 +103,9 @@ export class ActionsSession implements DriverSession {
93103
private readonly startOpts: DriverStartOptions,
94104
) {
95105
this.cwd = startOpts.cwd
96-
this.id = `actions-${++sessionCounter}`
106+
// The counter reads well in logs within one process; the random tag is what keeps the
107+
// correlation id unique across processes, since the daemon spawns a fresh one per run.
108+
this.id = `actions-${++sessionCounter}-${(config.runTag ?? randomRunTag)()}`
97109
this.lastSessionId = startOpts.resumeSessionId
98110
}
99111

@@ -107,8 +119,9 @@ export class ActionsSession implements DriverSession {
107119
const prompt = framing ? `${framing}\n\n${text}` : text
108120
emit({ type: 'start', prompt })
109121

110-
// Deterministic, so no clock or randomness leaks into the correlation: the
111-
// dispatch API returns no run id, and this is how we find our run among others.
122+
// How we find our run: the dispatch API returns no run id, so the workflow echoes this
123+
// into its run-name and artifact name and we match on it. It must be unique per run (the
124+
// session's random tag) so a fresh process never latches onto a stale same-named run.
112125
const correlationId = `${this.id}-turn-${++this.turnCounter}`
113126
const resume = opts.resume ? this.lastSessionId : undefined
114127

0 commit comments

Comments
 (0)