diff --git a/packages/daemon/src/check-runner.ts b/packages/daemon/src/check-runner.ts index 9ea77a8..41eb139 100644 --- a/packages/daemon/src/check-runner.ts +++ b/packages/daemon/src/check-runner.ts @@ -1,5 +1,5 @@ import { createHash } from 'node:crypto' -import { mkdir, readFile, rm, writeFile } from 'node:fs/promises' +import { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises' import { join } from 'node:path' import { XrpcError } from '@radial/atproto' import { COLLECTIONS, type CheckrunRecord, type CheckrunResult, type ProjectCheck, type StrongRef } from '@radial/core' @@ -150,6 +150,14 @@ export async function runCheckRun(input: CheckRunInput, deps: CheckRunDeps): Pro await mkdir(workDir, { recursive: true }) await mkdir(checksDir, { recursive: true }) await mkdir(resultsDir, { recursive: true }) + // Widen the results dir (and the checkout top dir) so the uid-1000 container can write into the + // bind mounts even when the daemon (which created these dirs) runs as a different host uid — + // the same reason the turn path widens its socket dir. Safe: both are per-run throwaways under + // runDir, deleted in `finally`. (Full-tree writability of an existing checkout for a build that + // rewrites tracked files is a Linux uid-mapping / image concern for the operator, not solved + // here; results-writing, which every run needs, is.) + await chmod(resultsDir, 0o777) + await chmod(workDir, 0o777) const doCheckout = deps.checkout ?? checkoutCommit try { diff --git a/packages/daemon/test/docker-smoke.test.mjs b/packages/daemon/test/docker-smoke.test.mjs index 2dd5b69..cce5f87 100644 --- a/packages/daemon/test/docker-smoke.test.mjs +++ b/packages/daemon/test/docker-smoke.test.mjs @@ -13,6 +13,7 @@ // `radial` is the @radial/sidecar CLI, always on PATH in the image), so this test is about the // container/socket/mount/network plumbing, not about the model. import assert from 'node:assert/strict' +import { spawnSync } from 'node:child_process' import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -20,7 +21,7 @@ import { describe, it } from 'node:test' import { createSession, CredentialClient } from '../../atproto/dist/index.js' import { COLLECTIONS } from '../../core/dist/index.js' import { LocalPds } from '../../atproto/test/local-pds.mjs' -import { DockerRunner, TurnSocketServer } from '../dist/index.js' +import { DockerRunner, TurnSocketServer, checkrunRkey, runCheckRun } from '../dist/index.js' const RUN_DOCKER_TESTS = process.env.RADIAL_DOCKER_TESTS === '1' const TURN_IMAGE = process.env.RADIAL_TURN_IMAGE ?? 'radial-turn:latest' @@ -171,3 +172,61 @@ describe('docker smoke: radial-turn against a real Docker daemon', { skip: !RUN_ assert.deepEqual(stillListed, []) }) }) + +describe('docker smoke: check runner against a real Docker daemon', { skip: !RUN_DOCKER_TESTS }, () => { + it('runs pass+fail checks against an exact commit and writes a checkrun with per-check results', async () => { + const { pds, client } = await makeSignedClient() + + await withTmpDir(async (dir) => { + // A real local repo the daemon-side checkoutCommit can shallow-fetch the exact sha from. + // `uploadpack.allowAnySHA1InWant` is what lets `git fetch ` serve an arbitrary sha + // over file://. + const repo = join(dir, 'repo') + await mkdir(repo, { recursive: true }) + const git = (...args) => { + const r = spawnSync('git', args, { cwd: repo, encoding: 'utf8' }) + assert.equal(r.status, 0, r.stderr) + return r + } + git('init', '--initial-branch=main') + git('config', 'user.email', 'test@example.test') + git('config', 'user.name', 'Radial Test') + git('config', 'uploadpack.allowAnySHA1InWant', 'true') + await writeFile(join(repo, 'README.md'), '# check smoke\n') + git('add', 'README.md') + git('commit', '-m', 'initial') + const commit = git('rev-parse', 'HEAD').stdout.trim() + + const artifact = { uri: 'at://did:plc:human/com.disnetdev.radial.artifact/check-smoke', cid: 'cid-check-smoke' } + const result = await runCheckRun( + { + artifact, + commit, + gitUrl: `file://${repo}`, + checks: [ + { name: 'pass', command: 'true' }, + { name: 'fail', command: 'false' }, + ], + client, + runDir: join(dir, 'run'), + image: TURN_IMAGE, // radial-turn ships `sh`; a dedicated check image is a config seam. + timeoutMs: 60_000, + allowedSchemes: ['https', 'file'], + }, + { runner: new DockerRunner() }, + ) + + assert.equal(result.outcome, 'done', 'expected a completed check run') + assert.ok(result.ref, 'expected a checkrun ref') + + const written = [...pds.records.values()].find((record) => record.value?.$type === COLLECTIONS.checkrun) + assert.ok(written, 'expected a com.disnetdev.radial.checkrun record in the fake PDS') + assert.equal(written.uri.startsWith(`at://${AGENT_DID}/`), true, 'expected the checkrun under the agent DID') + assert.equal(written.uri.split('/').pop(), checkrunRkey(artifact.uri, artifact.cid, commit)) + assert.equal(written.value.commit, commit) + assert.deepEqual(written.value.artifact, artifact) + const byName = Object.fromEntries(written.value.results.map((r) => [r.name, r.pass])) + assert.deepEqual(byName, { pass: true, fail: false }, 'pass check exits 0, fail check exits non-zero') + }) + }) +})