Skip to content

Commit e1fd386

Browse files
accesswatchGitHub Workshop Botclaude
authored
fix(provisioning): wait for asynchronous template copy before verification (#235)
GitHub's generate-from-template API returns before repository contents finish copying. The verification gate ran immediately and marked every freshly created learner failed (all 10 rooms in run 28615646165 were created fine but flagged missing workflows; the healing re-run passed). Verification now retries for up to 30 seconds on fresh creates and heals; already-exists verification stays single-shot. Co-authored-by: GitHub Workshop Bot <workshop@example.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent c4eb618 commit e1fd386

2 files changed

Lines changed: 75 additions & 5 deletions

File tree

.github/scripts/provisioning/__tests__/provision-core.test.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,58 @@ test('heals an incomplete repo when seedContent is available', async () => {
102102
assert.equal(summary.healed, 1);
103103
});
104104

105+
test('waits for asynchronous template copy before verifying a fresh repo', async () => {
106+
// GitHub's generate-from-template API returns before the content copy
107+
// finishes; verification must wait instead of marking the learner failed.
108+
const { client, state } = makeClient();
109+
client.createFromTemplate = async ({ repo }) => {
110+
state.calls.create += 1;
111+
state.repos.set(repo, { workflows: [] });
112+
return { full_name: repo };
113+
};
114+
let listCalls = 0;
115+
client.listWorkflowPaths = async ({ repo }) => {
116+
listCalls += 1;
117+
if (listCalls >= 3) state.repos.get(repo).workflows = [...REQUIRED];
118+
return state.repos.get(repo).workflows;
119+
};
120+
let slept = 0;
121+
const countingSleep = async () => { slept += 1; };
122+
123+
const roster = rosterWith({ github_handle: 'dana', cohort_id: 'c1' });
124+
const { roster: out, summary } = await provisionRoster({
125+
roster,
126+
client,
127+
config,
128+
sleep: countingSleep
129+
});
130+
131+
assert.equal(out.learners[0].provision_state, 'provisioned');
132+
assert.equal(summary.created, 1);
133+
assert.equal(summary.error, 0);
134+
assert.ok(slept >= 1, 'should have waited between verification attempts');
135+
});
136+
137+
test('still fails when content never arrives after creation', async () => {
138+
const { client, state } = makeClient();
139+
client.createFromTemplate = async ({ repo }) => {
140+
state.calls.create += 1;
141+
state.repos.set(repo, { workflows: [] });
142+
return { full_name: repo };
143+
};
144+
const roster = rosterWith({ github_handle: 'erin', cohort_id: 'c1' });
145+
const { roster: out, log, summary } = await provisionRoster({
146+
roster,
147+
client,
148+
config: { ...config, verifyRetries: 2, verifyDelayMs: 0 },
149+
sleep: noSleep
150+
});
151+
152+
assert.equal(out.learners[0].provision_state, 'failed');
153+
assert.equal(summary.error, 1);
154+
assert.match(log[0].error_detail, /Verification failed/);
155+
});
156+
105157
test('fails loudly when repo exists but incomplete and cannot heal', async () => {
106158
const { client, state } = makeClient();
107159
state.repos.set('learning-room-c1-carol', { workflows: ['pr-validation-bot.yml'] });

.github/scripts/provisioning/provision-core.js

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ const sleepReal = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
4040
* repoNameFor, // (handle, cohortId) => name; defaults to defaultRepoName
4141
* delayMs, // delay between entries (default 1500)
4242
* maxRetries, // backoff attempts on secondary rate limit (default 5)
43+
* verifyRetries, // verification attempts after create/heal (default 10)
44+
* verifyDelayMs, // delay between verification attempts (default 3000)
4345
* collaboratorPermission // default 'push'
4446
* }
4547
*
@@ -60,6 +62,8 @@ async function provisionRoster({
6062
const repoNameFor = config.repoNameFor || defaultRepoName;
6163
const delayMs = config.delayMs == null ? 1500 : config.delayMs;
6264
const maxRetries = config.maxRetries == null ? 5 : config.maxRetries;
65+
const verifyRetries = config.verifyRetries == null ? 10 : config.verifyRetries;
66+
const verifyDelayMs = config.verifyDelayMs == null ? 3000 : config.verifyDelayMs;
6367
const permission = config.collaboratorPermission || 'push';
6468

6569
const log = [];
@@ -85,7 +89,10 @@ async function provisionRoster({
8589
templateRepo,
8690
repoName,
8791
requiredWorkflows,
88-
permission
92+
permission,
93+
verifyRetries,
94+
verifyDelayMs,
95+
sleep
8996
}),
9097
{ maxRetries, sleep, logger }
9198
);
@@ -135,7 +142,10 @@ async function provisionOne({
135142
templateRepo,
136143
repoName,
137144
requiredWorkflows,
138-
permission
145+
permission,
146+
verifyRetries = 10,
147+
verifyDelayMs = 3000,
148+
sleep = sleepReal
139149
}) {
140150
const exists = await client.repoExists({ owner: studentOwner, repo: repoName });
141151

@@ -180,9 +190,17 @@ async function provisionOne({
180190
permission
181191
});
182192

183-
// Final verification gate: required workflows must be present.
184-
const present = await client.listWorkflowPaths({ owner: studentOwner, repo: repoName });
185-
const stillMissing = missingWorkflows(requiredWorkflows, present);
193+
// Final verification gate: required workflows must be present. On a fresh
194+
// create (or heal), GitHub copies template content asynchronously after the
195+
// API returns, so give the copy time to land instead of failing the learner.
196+
const attempts = result === 'already-exists' ? 1 : Math.max(1, verifyRetries);
197+
let stillMissing = requiredWorkflows;
198+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
199+
const present = await client.listWorkflowPaths({ owner: studentOwner, repo: repoName });
200+
stillMissing = missingWorkflows(requiredWorkflows, present);
201+
if (stillMissing.length === 0) break;
202+
if (attempt < attempts) await sleep(verifyDelayMs);
203+
}
186204
if (stillMissing.length > 0) {
187205
throw new Error(
188206
`Verification failed for ${studentOwner}/${repoName}: missing ${stillMissing.join(', ')}`

0 commit comments

Comments
 (0)