Skip to content

Commit cb4920a

Browse files
committed
test: prepare pre-push smoke source
1 parent b17eb50 commit cb4920a

2 files changed

Lines changed: 235 additions & 33 deletions

File tree

packages/lib/src/core/templates-entrypoint/git.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,18 @@ while read -r local_ref local_sha remote_ref remote_sha; do
255255
done
256256
EOF
257257
chmod 0755 "$PRE_PUSH_HOOK"
258+
259+
cat <<'EOF' >> "$PRE_PUSH_HOOK"
260+
261+
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
262+
cd "$REPO_ROOT"
263+
264+
if [ "${"${"}DOCKER_GIT_SKIP_SESSION_BACKUP:-}" != "1" ]; then
265+
if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
266+
node scripts/session-backup-gist.js --verbose || echo "[session-backup] Warning: session backup failed (non-fatal)"
267+
fi
268+
fi
269+
EOF
258270
git config --system core.hooksPath "$HOOKS_DIR" || true
259271
git config --global core.hooksPath "$HOOKS_DIR" || true`
260272

scripts/session-backup-gist.js

Lines changed: 223 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -118,48 +118,216 @@ const execCommand = (command, options = {}) => {
118118
};
119119

120120
/**
121-
* Get current git branch name
122-
* @returns {string|null} Branch name or null
121+
* Execute gh CLI command and return result
122+
* @param {string[]} args - Command arguments
123+
* @returns {{success: boolean, stdout: string, stderr: string}}
123124
*/
124-
const getCurrentBranch = () => {
125-
return execCommand("git rev-parse --abbrev-ref HEAD");
125+
const ghCommand = (args) => {
126+
const result = spawnSync("gh", args, {
127+
encoding: "utf8",
128+
stdio: ["pipe", "pipe", "pipe"],
129+
});
130+
131+
return {
132+
success: result.status === 0,
133+
stdout: (result.stdout || "").trim(),
134+
stderr: (result.stderr || "").trim(),
135+
};
126136
};
127137

128138
/**
129-
* Get repository owner/name from git remote
139+
* Parse a GitHub repository from a remote URL
140+
* @param {string} remoteUrl - Remote URL
130141
* @returns {string|null} Repository in owner/repo format or null
131142
*/
132-
const getRepoFromRemote = () => {
133-
const remoteUrl = execCommand("git remote get-url origin");
134-
if (!remoteUrl) return null;
143+
const parseGitHubRepoFromRemoteUrl = (remoteUrl) => {
144+
if (!remoteUrl) {
145+
return null;
146+
}
135147

136-
// Handle SSH format: git@github.com:owner/repo.git
137148
const sshMatch = remoteUrl.match(/git@github\.com:([^/]+\/[^.]+)(?:\.git)?$/);
138-
if (sshMatch) return sshMatch[1];
149+
if (sshMatch) {
150+
return sshMatch[1];
151+
}
139152

140-
// Handle HTTPS format: https://github.com/owner/repo.git
141153
const httpsMatch = remoteUrl.match(/https:\/\/github\.com\/([^/]+\/[^.]+)(?:\.git)?$/);
142-
if (httpsMatch) return httpsMatch[1];
154+
if (httpsMatch) {
155+
return httpsMatch[1];
156+
}
143157

144158
return null;
145159
};
146160

161+
const rankRemoteName = (remoteName) => {
162+
if (remoteName === "upstream") {
163+
return 0;
164+
}
165+
if (remoteName === "origin") {
166+
return 1;
167+
}
168+
return 2;
169+
};
170+
171+
/**
172+
* Get current git branch name
173+
* @returns {string|null} Branch name or null
174+
*/
175+
const getCurrentBranch = () => {
176+
return execCommand("git rev-parse --abbrev-ref HEAD");
177+
};
178+
179+
/**
180+
* Get HEAD commit sha
181+
* @returns {string|null} Commit sha or null
182+
*/
183+
const getHeadCommitSha = () => {
184+
return execCommand("git rev-parse HEAD");
185+
};
186+
187+
/**
188+
* Get repository candidates from git remotes
189+
* @param {string|null} explicitRepo - Explicit repository override
190+
* @param {boolean} verbose - Whether to log verbosely
191+
* @returns {string[]} Candidate repositories in owner/repo format
192+
*/
193+
const getRepoCandidates = (explicitRepo, verbose) => {
194+
if (explicitRepo) {
195+
return [explicitRepo];
196+
}
197+
198+
const remoteOutput = execCommand("git remote -v");
199+
if (!remoteOutput) {
200+
return [];
201+
}
202+
203+
const remotes = [];
204+
const seenRepos = new Set();
205+
206+
for (const line of remoteOutput.split("\n")) {
207+
const match = line.match(/^(\S+)\s+(\S+)\s+\((fetch|push)\)$/);
208+
if (!match || match[3] !== "fetch") {
209+
continue;
210+
}
211+
212+
const [, remoteName, remoteUrl] = match;
213+
const repo = parseGitHubRepoFromRemoteUrl(remoteUrl);
214+
if (!repo || seenRepos.has(repo)) {
215+
continue;
216+
}
217+
218+
remotes.push({ remoteName, repo });
219+
seenRepos.add(repo);
220+
}
221+
222+
remotes.sort((left, right) => {
223+
const rankDiff = rankRemoteName(left.remoteName) - rankRemoteName(right.remoteName);
224+
return rankDiff !== 0 ? rankDiff : left.remoteName.localeCompare(right.remoteName);
225+
});
226+
227+
const repos = remotes.map(({ repo }) => repo);
228+
if (repos.length > 0) {
229+
log(verbose, `Repository candidates: ${repos.join(", ")}`);
230+
}
231+
return repos;
232+
};
233+
147234
/**
148235
* Get PR number from current branch
149236
* @param {string} repo - Repository in owner/repo format
150237
* @param {string} branch - Branch name
151238
* @returns {number|null} PR number or null
152239
*/
153240
const getPrNumberFromBranch = (repo, branch) => {
154-
const result = execCommand(
155-
`gh pr list --repo ${repo} --head ${branch} --json number --jq '.[0].number'`
156-
);
157-
if (result && !isNaN(parseInt(result, 10))) {
158-
return parseInt(result, 10);
241+
const result = ghCommand([
242+
"pr",
243+
"list",
244+
"--repo",
245+
repo,
246+
"--head",
247+
branch,
248+
"--json",
249+
"number",
250+
"--jq",
251+
".[0].number",
252+
]);
253+
254+
if (result.success && result.stdout && !isNaN(parseInt(result.stdout, 10))) {
255+
return parseInt(result.stdout, 10);
159256
}
160257
return null;
161258
};
162259

260+
/**
261+
* Check whether a PR exists in a repository
262+
* @param {string} repo - Repository in owner/repo format
263+
* @param {number} prNumber - PR number
264+
* @returns {boolean} Whether the PR exists
265+
*/
266+
const prExists = (repo, prNumber) => {
267+
const result = ghCommand([
268+
"pr",
269+
"view",
270+
prNumber.toString(),
271+
"--repo",
272+
repo,
273+
"--json",
274+
"number",
275+
"--jq",
276+
".number",
277+
]);
278+
279+
return result.success && result.stdout === prNumber.toString();
280+
};
281+
282+
/**
283+
* Extract a PR number from a docker-git workspace branch
284+
* @param {string} branch - Branch name
285+
* @returns {number|null} PR number or null
286+
*/
287+
const getPrNumberFromWorkspaceBranch = (branch) => {
288+
const match = branch.match(/^pr-refs-pull-([0-9]+)-head$/);
289+
if (!match) {
290+
return null;
291+
}
292+
293+
const prNumber = parseInt(match[1], 10);
294+
return Number.isNaN(prNumber) ? null : prNumber;
295+
};
296+
297+
/**
298+
* Find an open PR for the current branch across repo candidates
299+
* @param {string[]} repos - Candidate repositories
300+
* @param {string} branch - Branch name
301+
* @param {boolean} verbose - Whether to log verbosely
302+
* @returns {{repo: string, prNumber: number} | null} PR context or null
303+
*/
304+
const findPrContext = (repos, branch, verbose) => {
305+
for (const repo of repos) {
306+
log(verbose, `Checking open PR in ${repo} for branch ${branch}`);
307+
const prNumber = getPrNumberFromBranch(repo, branch);
308+
if (prNumber !== null) {
309+
return { repo, prNumber };
310+
}
311+
}
312+
313+
const workspacePrNumber = getPrNumberFromWorkspaceBranch(branch);
314+
if (workspacePrNumber === null) {
315+
return null;
316+
}
317+
318+
for (const repo of repos) {
319+
log(
320+
verbose,
321+
`Checking workspace PR #${workspacePrNumber} in ${repo} for branch ${branch}`
322+
);
323+
if (prExists(repo, workspacePrNumber)) {
324+
return { repo, prNumber: workspacePrNumber };
325+
}
326+
}
327+
328+
return null;
329+
};
330+
163331
/**
164332
* Find session directories to backup
165333
* @param {string|null} explicitPath - Explicit session directory path
@@ -322,13 +490,15 @@ const createGist = (files, description, dryRun, verbose) => {
322490
* @param {boolean} verbose - Whether to log verbosely
323491
* @returns {boolean} Whether the comment was posted successfully
324492
*/
325-
const postPrComment = (repo, prNumber, gistUrl, dryRun, verbose) => {
493+
const postPrComment = (repo, prNumber, gistUrl, commitSha, dryRun, verbose) => {
326494
const timestamp = new Date().toISOString();
495+
const commitLine = commitSha ? `**Commit:** \`${commitSha}\`\n\n` : "";
496+
const commitMarker = commitSha ? `\n<!-- docker-git-session-backup:${commitSha} -->` : "";
327497
const comment = `## AI Session Backup
328498
329499
A snapshot of the AI agent session has been saved to a private gist:
330500
331-
**Gist URL:** ${gistUrl}
501+
${commitLine}**Gist URL:** ${gistUrl}
332502
333503
To resume this session, you can use:
334504
\`\`\`bash
@@ -345,7 +515,7 @@ gemini --resume <session-id>
345515
For extracting session dialogs, see: https://github.com/ProverCoderAI/context-doc
346516
347517
---
348-
*Backup created at: ${timestamp}*`;
518+
*Backup created at: ${timestamp}*${commitMarker}`;
349519

350520
if (dryRun) {
351521
console.log(`[dry-run] Would post comment to PR #${prNumber} in ${repo}:`);
@@ -389,11 +559,12 @@ const main = () => {
389559
log(verbose, "Starting session backup...");
390560

391561
// Get repository info
392-
const repo = args.repo || getRepoFromRemote();
393-
if (!repo) {
562+
const repoCandidates = getRepoCandidates(args.repo, verbose);
563+
if (repoCandidates.length === 0) {
394564
console.error("[session-backup] Could not determine repository. Use --repo option.");
395565
process.exit(1);
396566
}
567+
const repo = repoCandidates[0];
397568
log(verbose, `Repository: ${repo}`);
398569

399570
// Get current branch
@@ -405,15 +576,17 @@ const main = () => {
405576
log(verbose, `Branch: ${branch}`);
406577

407578
// Get PR number
408-
let prNumber = args.prNumber;
409-
if (!prNumber && args.postComment) {
410-
prNumber = getPrNumberFromBranch(repo, branch);
411-
if (!prNumber) {
412-
log(verbose, "No PR found for current branch, skipping comment");
413-
}
579+
let prContext = null;
580+
if (args.prNumber !== null) {
581+
prContext = { repo, prNumber: args.prNumber };
582+
} else if (args.postComment) {
583+
prContext = findPrContext(repoCandidates, branch, verbose);
414584
}
415-
if (prNumber) {
416-
log(verbose, `PR number: ${prNumber}`);
585+
586+
if (prContext !== null) {
587+
log(verbose, `PR number: ${prContext.prNumber} (${prContext.repo})`);
588+
} else if (args.postComment) {
589+
log(verbose, "No PR found for current branch, skipping comment");
417590
}
418591

419592
// Find session directories
@@ -438,7 +611,17 @@ const main = () => {
438611
log(verbose, `Total files to backup: ${allFiles.length}`);
439612

440613
// Create gist
441-
const description = `AI Session Backup - ${repo} - ${branch} - ${new Date().toISOString()}`;
614+
const commitSha = getHeadCommitSha();
615+
const descriptionParts = [
616+
"AI Session Backup",
617+
prContext !== null ? prContext.repo : repo,
618+
branch,
619+
];
620+
if (commitSha) {
621+
descriptionParts.push(commitSha.slice(0, 12));
622+
}
623+
descriptionParts.push(new Date().toISOString());
624+
const description = descriptionParts.join(" - ");
442625
const gistUrl = createGist(allFiles, description, args.dryRun, verbose);
443626

444627
if (!gistUrl) {
@@ -449,8 +632,15 @@ const main = () => {
449632
console.log(`[session-backup] Created gist: ${gistUrl}`);
450633

451634
// Post PR comment
452-
if (args.postComment && prNumber) {
453-
postPrComment(repo, prNumber, gistUrl, args.dryRun, verbose);
635+
if (args.postComment && prContext !== null) {
636+
postPrComment(
637+
prContext.repo,
638+
prContext.prNumber,
639+
gistUrl,
640+
commitSha,
641+
args.dryRun,
642+
verbose
643+
);
454644
}
455645

456646
console.log("[session-backup] Session backup complete");

0 commit comments

Comments
 (0)