/** * End-to-end smoke test for the GitHub mutation primitives. * * Runs the full create-branch → commit → open-PR → query-PR → close-PR → * delete-branch lifecycle against a real repo so regressions in `github/*` * surface immediately. Safe to re-run — every iteration uses a unique * timestamped branch name, and cleanup runs in `finally`. * * Authentication: uses GitHub App installation credentials only. Configure * the target repo via `GITHUB_OWNER` / `GITHUB_REPO`; defaults to * acid-info/logos-co. */ import { getOctokit, __resetOctokitForTests } from '../src/github/client' import { __resetGithubConfigForTests, loadGithubConfigFromEnv, setGithubConfig, } from '../src/github/config' import { findOpenPullRequestsTouchingPath } from '../src/github/locks' import { branchExists, commitFiles, commitTextFile, createBranch, getBranchSha, } from '../src/github/mutations' import { createOrGetPullRequest, findPullRequestByBranch, } from '../src/github/pull-requests' type Step = { name: string run: () => Promise } const config = loadGithubConfigFromEnv() const { owner, repo, prBaseBranch: baseBranch } = config const stamp = new Date().toISOString().replace(/[:.]/g, '-').replace(/-Z$/, 'Z') const suffix = Math.random().toString(36).slice(2, 8) const branchName = `content/smoke-test-${stamp}-${suffix}` const filePath = `tmp/github-smoke/${stamp}-${suffix}.json` const fileContent = JSON.stringify( { stamp, suffix, note: 'smoke test artifact — safe to delete' }, null, 2 ) const main = async (): Promise => { __resetGithubConfigForTests() __resetOctokitForTests() setGithubConfig(config) console.log(`target : ${owner}/${repo}`) console.log(`base : ${baseBranch}`) console.log(`branch : ${branchName}`) console.log(`file : ${filePath}`) console.log('') const steps: Step[] = [ { name: 'getBranchSha(base)', run: async () => { const sha = await getBranchSha(baseBranch) return `base sha=${sha.slice(0, 7)}` }, }, { name: 'createBranch', run: () => createBranch({ newBranch: branchName, fromBranch: baseBranch }), }, { name: 'branchExists', run: async () => { const exists = await branchExists(branchName) if (!exists) throw new Error('branch should exist after create') return 'true' }, }, { name: 'commitTextFile (single)', run: () => commitTextFile({ branch: branchName, path: filePath, content: fileContent, message: `chore(smoke): commit single file ${suffix}`, }), }, { name: 'commitFiles (atomic multi)', run: async () => { const result = await commitFiles({ branch: branchName, message: `chore(smoke): commit multi files ${suffix}`, changes: [ { path: `${filePath.replace('.json', '')}-second.json`, content: JSON.stringify({ batch: 'second' }, null, 2), }, { path: `${filePath.replace('.json', '')}-third.json`, content: JSON.stringify({ batch: 'third' }, null, 2), }, ], }) return `commitSha=${result.commitSha.slice(0, 7)}` }, }, { name: 'createOrGetPullRequest', run: async () => { const pr = await createOrGetPullRequest({ branchName, title: `chore(smoke): GitHub primitives smoke test ${suffix}`, body: 'Automated smoke test of the `@repo/content` GitHub mutation ' + 'primitives. Closed automatically by the smoke script — if ' + 'this PR lingers in your inbox it means the cleanup phase ' + 'failed and the branch needs manual deletion.', draft: true, }) return `#${pr.number} ${pr.htmlUrl}` }, }, { name: 'findPullRequestByBranch (idempotency)', run: async () => { const pr = await findPullRequestByBranch(branchName) if (!pr) throw new Error('expected to find the PR by branch') return `#${pr.number}` }, }, { name: 'createOrGetPullRequest (returns existing, not duplicate)', run: async () => { const a = await createOrGetPullRequest({ branchName, title: 'should not create a duplicate', body: 'should not create a duplicate', }) return `#${a.number}` }, }, { name: 'findOpenPullRequestsTouchingPath', run: async () => { const matches = await findOpenPullRequestsTouchingPath(filePath) const ours = matches.find((pr) => pr.branchName === branchName) if (!ours) { throw new Error( `expected to find our smoke PR in lock query for ${filePath}; got [${matches .map((pr) => `#${pr.number}`) .join(', ')}]` ) } return `${matches.length} PR(s) touch the path; ours #${ours.number}` }, }, ] let failed = 0 for (const step of steps) { try { const result = await step.run() console.log(`ok ${step.name}${result ? ' — ' + result : ''}`) } catch (err) { failed++ const message = err instanceof Error ? err.message : String(err) console.error(`FAIL ${step.name}: ${message}`) break } } console.log('') console.log('cleanup phase') await cleanup() if (failed > 0) process.exit(1) } const cleanup = async (): Promise => { const octokit = getOctokit() // Close PR if it exists. try { const pr = await findPullRequestByBranch(branchName) if (pr) { await octokit.pulls.update({ owner, repo, pull_number: pr.number, state: 'closed', }) console.log(`ok closed PR #${pr.number}`) } } catch (err) { console.warn( `warn PR close skipped: ${err instanceof Error ? err.message : String(err)}` ) } // Delete the branch. try { await octokit.git.deleteRef({ owner, repo, ref: `heads/${branchName}`, }) console.log(`ok deleted branch ${branchName}`) } catch (err) { console.warn( `warn branch delete skipped: ${err instanceof Error ? err.message : String(err)}` ) } } main().catch(async (err) => { console.error(err) await cleanup() process.exit(1) })