/
githubmirror
/
next.js
Обзор
Документация
Войти
/
githubmirror
/
next.js
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
v16.3.0
scripts/github-utils/signed-commit.js
399 строк
9 KB
Sebastian "Sebbie" Silbermann
[cd] add ad-hoc preview release cut from canary (#95086)
24 июн 2026, 17:33
Не верифицирован
24 июн 2026, 17:33
6819405
Код
Авторство
О чём код?
// @ts-check const execa = require('execa') /** * Call the GitHub REST API and include response bodies in thrown errors so * workflow failures show actionable details. */ async function githubRequest(token, method, path, body) { const response = await fetch(`https://api.github.com${path}`, { method, headers: { Accept: 'application/vnd.github+json', Authorization: `Bearer ${token}`, 'X-GitHub-Api-Version': '2022-11-28', ...(body ? { 'Content-Type': 'application/json' } : {}), }, body: body ? JSON.stringify(body) : undefined, }) if (!response.ok) { const responseText = await response.text() throw new Error( `GitHub API ${method} ${path} failed (${response.status}): ${responseText}` ) } if (response.status === 204) { return null } return response.json() } async function git(args, options = {}) { const { captureOutput = false, ...execaOptions } = options const { stdout } = await execa('git', args, { stdio: captureOutput ? 'pipe' : 'inherit', ...execaOptions, }) return typeof stdout === 'string' ? stdout.trim() : stdout } /** * List paths changed between two commits, using "\0" delimiters so unusual * file names do not affect parsing. */ async function getChangedFiles(baseSha, headSha) { const stdout = await git( ['diff-tree', '-r', '--name-only', '--no-renames', '-z', baseSha, headSha], { captureOutput: true, encoding: 'utf8' } ) return String(stdout).split('\0').filter(Boolean) } /** * Read the Git tree metadata for a path so recreated tree entries preserve * file modes, blob types, and submodule commit pointers. */ async function getTreeEntry(commitSha, filePath) { const stdout = await git(['ls-tree', '-z', commitSha, '--', filePath], { captureOutput: true, encoding: 'utf8', }) if (!stdout) { return null } // git ls-tree -z emits "<mode> <type> <object>\t<path>\0". const match = /^(\d{6}) (\w+) ([0-9a-f]{40})\t/.exec(String(stdout)) if (!match) { throw new Error(`Failed to parse git tree entry for ${filePath}`) } return { mode: match[1], type: match[2], sha: match[3], } } /** * Upload one file from a local commit as a GitHub blob and return the blob * SHA for the recreated tree entry. * * `request` defaults to the real `githubRequest`; callers can inject a mock * (e.g. a dry-run logger) to exercise the flow without hitting the API. */ async function createBlobForFile( token, repoApiPath, commitSha, filePath, request = githubRequest ) { const content = await git(['show', `${commitSha}:${filePath}`], { captureOutput: true, encoding: null, stripFinalNewline: false, maxBuffer: 1024 * 1024 * 100, }) const blob = await request(token, 'POST', `${repoApiPath}/git/blobs`, { content: Buffer.from(content).toString('base64'), encoding: 'base64', }) return blob.sha } /** * Build a GitHub tree matching a local commit, creating new blob objects for * changed files in parallel while preserving deletions and submodules. */ async function createTreeFromLocalCommit({ token, repoApiPath, diffBaseSha, baseTreeSha, localCommitSha, allowEmpty = false, request = githubRequest, }) { const changedFiles = await getChangedFiles(diffBaseSha, localCommitSha) if (changedFiles.length === 0) { if (allowEmpty) { // Nothing changed — reuse the parent tree so the resulting commit is // a no-op. return baseTreeSha } throw new Error(`Commit ${localCommitSha} has no file changes`) } const tree = await Promise.all( changedFiles.map(async (filePath) => { const treeEntry = await getTreeEntry(localCommitSha, filePath) if (!treeEntry) { return { path: filePath, sha: null, } } if (treeEntry.type === 'commit') { return { path: filePath, mode: treeEntry.mode, type: treeEntry.type, sha: treeEntry.sha, } } const blobSha = await createBlobForFile( token, repoApiPath, localCommitSha, filePath, request ) return { path: filePath, mode: treeEntry.mode, type: treeEntry.type, sha: blobSha, } }) ) const createdTree = await request(token, 'POST', `${repoApiPath}/git/trees`, { base_tree: baseTreeSha, tree, }) return createdTree.sha } /** * Build a GitHub tree from the local commit's diff against `baseSha`, then * create a GitHub-signed commit on top of `baseSha`. Returns the new commit * payload (including `sha`). Verifies `verification.verified`. */ async function createSignedCommit({ token, owner, repo, baseSha, localCommitSha, message, allowEmpty = false, }) { const repoApiPath = `/repos/${owner}/${repo}` const baseTreeSha = await git(['rev-parse', `${baseSha}^{tree}`], { captureOutput: true, }) const treeSha = await createTreeFromLocalCommit({ token, repoApiPath, diffBaseSha: baseSha, baseTreeSha, localCommitSha, allowEmpty, }) const commit = await githubRequest( token, 'POST', `${repoApiPath}/git/commits`, { message, tree: treeSha, parents: [baseSha], } ) if (!commit.verification?.verified) { throw new Error( `GitHub API created unsigned commit ${commit.sha}: ${commit.verification?.reason}` ) } return commit } /** * Replay every local commit between `fromBaseSha` (exclusive) and * `toLocalSha` (inclusive) as a chain of GitHub-signed commits whose root is * `fromBaseSha` on the remote. Each replayed commit's tree is built by * applying that local commit's file-tree diff (against its local parent) on * top of the previous signed commit's tree. The local commit message is * preserved. * * Returns `{ headSha, signedCommits }` where `headSha` is the final signed * commit and `signedCommits` maps each local commit to its signed counterpart * (`{ localSha, signedSha }`, in replay order). */ async function replayLocalCommitsAsSigned({ token, owner, repo, fromBaseSha, toLocalSha, allowEmpty = false, request = githubRequest, }) { const repoApiPath = `/repos/${owner}/${repo}` const revListOutput = await git( ['rev-list', '--reverse', `${fromBaseSha}..${toLocalSha}`], { captureOutput: true } ) const localCommits = String(revListOutput).split('\n').filter(Boolean) if (localCommits.length === 0) { throw new Error( `No commits to replay between ${fromBaseSha} and ${toLocalSha}` ) } let parentSha = fromBaseSha let parentTreeSha = await git(['rev-parse', `${fromBaseSha}^{tree}`], { captureOutput: true, }) const signedCommits = [] for (const localSha of localCommits) { const localParentSha = await git(['rev-parse', `${localSha}^`], { captureOutput: true, }) const message = await git(['log', '-1', '--pretty=%B', localSha], { captureOutput: true, }) const treeSha = await createTreeFromLocalCommit({ token, repoApiPath, diffBaseSha: localParentSha, baseTreeSha: parentTreeSha, localCommitSha: localSha, allowEmpty, request, }) const commit = await request(token, 'POST', `${repoApiPath}/git/commits`, { message, tree: treeSha, parents: [parentSha], }) if (!commit.verification?.verified) { throw new Error( `GitHub API created unsigned commit ${commit.sha}: ${commit.verification?.reason}` ) } parentSha = commit.sha parentTreeSha = treeSha signedCommits.push({ localSha, signedSha: commit.sha }) } return { headSha: parentSha, signedCommits } } /** * Create the branch ref if it does not exist, otherwise fast-forward (or * `force`-update) it to the given commit SHA. */ async function upsertBranchRef({ token, owner, repo, branch, sha, force = false, }) { const repoApiPath = `/repos/${owner}/${repo}` try { await githubRequest(token, 'POST', `${repoApiPath}/git/refs`, { ref: `refs/heads/${branch}`, sha, }) return { created: true } } catch (error) { const errMessage = error instanceof Error ? error.message : String(error) if (!errMessage.includes('Reference already exists')) { throw error } await githubRequest( token, 'PATCH', `${repoApiPath}/git/refs/heads/${branch}`, { sha, force, } ) return { created: false } } } /** * Refresh local refs after API writes so subsequent steps see the * GitHub-signed commit instead of the unsigned local commit. Optionally also * fetches a freshly created tag. */ async function alignLocalBranchWithSignedCommit( branch, commitSha, options = {} ) { const { tagName } = options if (tagName) { const tagExists = await execa( 'git', ['show-ref', '--verify', '--quiet', `refs/tags/${tagName}`], { stdio: 'ignore', reject: false, } ) if (tagExists.exitCode === 0) { await git(['tag', '-d', tagName]) } } const fetchRefs = [`refs/heads/${branch}:refs/remotes/origin/${branch}`] if (tagName) { fetchRefs.push(`refs/tags/${tagName}:refs/tags/${tagName}`) } await git(['fetch', 'origin', ...fetchRefs]) await git(['reset', '--hard', commitSha]) } module.exports = { githubRequest, getChangedFiles, getTreeEntry, createBlobForFile, createTreeFromLocalCommit, createSignedCommit, replayLocalCommitsAsSigned, upsertBranchRef, alignLocalBranchWithSignedCommit, }