ci(cutover): no Closes listed twice, room for the worst case, the checkout pinned (D332)
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in 14s
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in 14s
From the second review: - A PR whose merge commit main already has is not listed again. edge's tip at a cutover is often that PR's merge commit, and its merged_at can be a moment after the cut-off's committer date, so its issues would have had the next release's comment too. - timeout-minutes 240: the worst case is about 200, and a timeout must never cut off the step that starts rust-carbon again. - A test pins the checkout: edge, except on a dry dispatch that names code_ref. Dry run (CI run 54, before these): every gate refused for the right reason. Public Rust is still the 22 October build, which lacks the three fields edge carries for November, and gates 1 to 4 each saw it; the panel key wrote to, restarted and read rust-carbon; the live half ran 266 pass, 0 fail; nothing was opened, edited, commented or merged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -92,8 +92,9 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
cutover:
|
cutover:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Carbon's restart and navmesh, the live half (about an hour), the PR checks.
|
# Carbon's restart and navmesh, the live half (about an hour), the PR checks: about 100 minutes, 200 at the
|
||||||
timeout-minutes: 180
|
# worst. Room for that, so a timeout never cuts off the step that starts rust-carbon again.
|
||||||
|
timeout-minutes: 240
|
||||||
env:
|
env:
|
||||||
WORK: ${{ github.workspace }}/cutover-work
|
WORK: ${{ github.workspace }}/cutover-work
|
||||||
STAGING_WORK: ${{ github.workspace }}/cutover-staging
|
STAGING_WORK: ${{ github.workspace }}/cutover-staging
|
||||||
|
|||||||
@@ -451,11 +451,21 @@ async function closesSinceCutOff(g, deps) {
|
|||||||
try {
|
try {
|
||||||
since = Date.parse(deps.git('log', '-1', '--format=%cI', deps.git('merge-base', 'origin/main', 'origin/edge'))) || 0
|
since = Date.parse(deps.git('log', '-1', '--format=%cI', deps.git('merge-base', 'origin/main', 'origin/edge'))) || 0
|
||||||
} catch {}
|
} catch {}
|
||||||
|
// A PR whose merge commit main already has shipped in an earlier cutover, even when its merged_at is a moment
|
||||||
|
// after the cut-off's committer date (edge's tip is often that very merge commit).
|
||||||
|
const shipped = (sha) => {
|
||||||
|
if (!sha) return false
|
||||||
|
try {
|
||||||
|
return /\borigin\/main\b/.test(deps.git('branch', '-r', '--contains', sha, '--list', 'origin/main'))
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
const bodies = []
|
const bodies = []
|
||||||
for (let page = 1; page <= 10; page++) {
|
for (let page = 1; page <= 10; page++) {
|
||||||
const prs = await g.call('GET', `/pulls?state=closed&limit=50&page=${page}`)
|
const prs = await g.call('GET', `/pulls?state=closed&limit=50&page=${page}`)
|
||||||
for (const pr of prs) {
|
for (const pr of prs) {
|
||||||
if (pr.merged && pr.base && pr.base.ref === 'edge' && Date.parse(pr.merged_at) > since) bodies.push(pr.body)
|
if (pr.merged && pr.base && pr.base.ref === 'edge' && Date.parse(pr.merged_at) > since && !shipped(pr.merge_commit_sha)) bodies.push(pr.body)
|
||||||
}
|
}
|
||||||
if (prs.length < 50) break
|
if (prs.length < 50) break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -676,6 +676,24 @@ test('the Closes list is the PRs merged into edge after the newest edge commit a
|
|||||||
assert.ok(asked.includes('log -1 --format=%cI deadbeef'))
|
assert.ok(asked.includes('log -1 --format=%cI deadbeef'))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('a PR whose merge commit main already has is not listed again, though merged a moment after the cut-off', async () => {
|
||||||
|
const prs = [
|
||||||
|
// edge's tip at the last cutover: its merged_at is a second after the merge commit's committer date.
|
||||||
|
{ merged: true, base: { ref: 'edge' }, merged_at: '2026-11-05T19:00:01Z', merge_commit_sha: 'shipped1', body: 'Closes #20' },
|
||||||
|
{ merged: true, base: { ref: 'edge' }, merged_at: '2026-11-20T00:00:00Z', merge_commit_sha: 'new1', body: 'Closes #30' },
|
||||||
|
]
|
||||||
|
const deps = fakeDeps({
|
||||||
|
handler: (m, p) => (p.startsWith('/pulls?state=closed') ? { json: prs } : {}),
|
||||||
|
git: (...a) => {
|
||||||
|
if (a[0] === 'merge-base') return 'shipped1'
|
||||||
|
if (a[0] === 'log') return '2026-11-05T19:00:00+00:00'
|
||||||
|
if (a[0] === 'branch') return a[3] === 'shipped1' ? ' origin/main' : ''
|
||||||
|
return ''
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.deepEqual(await c.closesSinceCutOff(c.gitea('t', deps), deps), [30])
|
||||||
|
})
|
||||||
|
|
||||||
test('rust-carbon is started again only when it was running at the start and is offline now, and nothing else is touched', async () => {
|
test('rust-carbon is started again only when it was running at the start and is offline now, and nothing else is touched', async () => {
|
||||||
const dir = tmp()
|
const dir = tmp()
|
||||||
const powered = []
|
const powered = []
|
||||||
@@ -744,6 +762,13 @@ test('and the outputs the logic depends on are all read by the workflow', () =>
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('the code checked out is edge, except on a dry dispatch that names code_ref', () => {
|
||||||
|
const refs = yml.split('\n').filter((l) => /^\s*ref:/.test(l)).map((l) => l.trim())
|
||||||
|
assert.deepEqual(refs, [
|
||||||
|
"ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run != 'false' && github.event.inputs.code_ref || 'edge' }}",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
test('RUNICNPC_TOKEN is in the merge step alone, and a dispatch is dry unless dry_run is exactly false', () => {
|
test('RUNICNPC_TOKEN is in the merge step alone, and a dispatch is dry unless dry_run is exactly false', () => {
|
||||||
const uses = yml.split('\n').filter((l) => !l.trim().startsWith('#') && l.includes('RUNICNPC_TOKEN'))
|
const uses = yml.split('\n').filter((l) => !l.trim().startsWith('#') && l.includes('RUNICNPC_TOKEN'))
|
||||||
assert.equal(uses.length, 1)
|
assert.equal(uses.length, 1)
|
||||||
@@ -754,5 +779,5 @@ test('RUNICNPC_TOKEN is in the merge step alone, and a dispatch is dry unless dr
|
|||||||
assert.ok(!/dry_run\s*[=!]=\s*'true'/.test(code), 'dry_run must be compared with false, never true')
|
assert.ok(!/dry_run\s*[=!]=\s*'true'/.test(code), 'dry_run must be compared with false, never true')
|
||||||
assert.ok((code.match(/dry_run != 'false'/g) || []).length >= 6)
|
assert.ok((code.match(/dry_run != 'false'/g) || []).length >= 6)
|
||||||
for (const key of ['LC_ALL: C', "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: '1'"]) assert.equal(code.split(key).length - 1, 2, `${key} on both static halves`)
|
for (const key of ['LC_ALL: C', "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: '1'"]) assert.equal(code.split(key).length - 1, 2, `${key} on both static halves`)
|
||||||
assert.match(code, /timeout-minutes: 180/)
|
assert.match(code, /timeout-minutes: 240/)
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user