@ghl-ai/aw 0.1.81 → 0.1.83
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/push.mjs +99 -15
- package/package.json +1 -1
package/commands/push.mjs
CHANGED
|
@@ -431,6 +431,10 @@ async function removeTrackedAwSymlink(cloneDir) {
|
|
|
431
431
|
return false;
|
|
432
432
|
}
|
|
433
433
|
|
|
434
|
+
// `.aw` is always kept inside the sparse-checkout cone (see AW_DOCS_CACHE_PATH
|
|
435
|
+
// in ensureAwDocsRepoClone) so this remove works even on blobless/sparse
|
|
436
|
+
// clones. Without that, git rejects the rm with "outside sparse-checkout
|
|
437
|
+
// definition" against repos that track a real .aw/ tree (e.g. ghl-aw-docs).
|
|
434
438
|
await execFile('git', ['rm', '-f', '.aw'], {
|
|
435
439
|
cwd: cloneDir,
|
|
436
440
|
encoding: 'utf8',
|
|
@@ -572,32 +576,97 @@ async function getProjectSourceRepo(projectRoot) {
|
|
|
572
576
|
return `local/${basename(projectRoot)}`;
|
|
573
577
|
}
|
|
574
578
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
579
|
+
// Network git ops against the docs repo always disable interactive credential
|
|
580
|
+
// prompts (so a missing/incorrect credential fails fast instead of hanging
|
|
581
|
+
// forever) and are time-bounded (so a stalled connection aborts instead of
|
|
582
|
+
// blocking the whole publish). Override the caps with AW_DOCS_GIT_TIMEOUT_MS.
|
|
583
|
+
const AW_DOCS_GIT_ENV = { ...process.env, GIT_TERMINAL_PROMPT: '0', GCM_INTERACTIVE: 'never' };
|
|
584
|
+
const AW_DOCS_CLONE_TIMEOUT_MS = Number(process.env.AW_DOCS_GIT_TIMEOUT_MS) || 180000;
|
|
585
|
+
const AW_DOCS_NET_TIMEOUT_MS = Number(process.env.AW_DOCS_GIT_TIMEOUT_MS) || 120000;
|
|
586
|
+
// `.aw` is always kept in the sparse-checkout cone. The docs repo may track a
|
|
587
|
+
// real `.aw/` tree (e.g. ghl-aw-docs tracks `.aw/.aw_rules/**`), and the
|
|
588
|
+
// cache-management code (removeTrackedAwSymlink, selfHeal*) runs write git ops
|
|
589
|
+
// on `.aw`. Excluding it from the cone makes those ops fail with "outside
|
|
590
|
+
// sparse-checkout definition" and breaks every publish against such a repo.
|
|
591
|
+
const AW_DOCS_CACHE_PATH = '.aw';
|
|
592
|
+
|
|
593
|
+
async function awDocsSparseEnabled(cloneDir) {
|
|
594
|
+
try {
|
|
595
|
+
const { stdout } = await execFile('git', ['config', '--get', 'core.sparseCheckout'], {
|
|
596
|
+
cwd: cloneDir,
|
|
580
597
|
encoding: 'utf8',
|
|
581
598
|
});
|
|
599
|
+
return stdout.trim() === 'true';
|
|
600
|
+
} catch {
|
|
601
|
+
return false;
|
|
582
602
|
}
|
|
603
|
+
}
|
|
583
604
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
605
|
+
async function ensureAwDocsRepoClone(home, publishConfig, sparseIncludePaths = []) {
|
|
606
|
+
const cloneDir = process.env.AW_DOCS_WORKTREE || join(home, '.aw-ghl-aw-docs');
|
|
607
|
+
const wantSparse = sparseIncludePaths.length > 0;
|
|
608
|
+
const freshClone = !existsSync(join(cloneDir, '.git'));
|
|
609
|
+
|
|
610
|
+
if (freshClone) {
|
|
611
|
+
// A full clone of ghl-aw-docs pulls the entire org's published docs
|
|
612
|
+
// (hundreds of MB / ~170k objects) and is the dominant cause of
|
|
613
|
+
// first-publish timeouts. Clone blobless (no file contents up front) and,
|
|
614
|
+
// when we know which subtree this publish touches, defer checkout and
|
|
615
|
+
// sparse-checkout only that path — turning hundreds of MB into a few MB.
|
|
616
|
+
// Degrades gracefully: if the remote does not support partial clone git
|
|
617
|
+
// ignores the filter, and if sparse-checkout is unavailable the branch
|
|
618
|
+
// checkout below still materialises the full tree.
|
|
619
|
+
const cloneArgs = ['clone', '--filter=blob:none'];
|
|
620
|
+
if (wantSparse) cloneArgs.push('--no-checkout');
|
|
621
|
+
cloneArgs.push(publishConfig.repoUrl, cloneDir);
|
|
622
|
+
await execFile('git', cloneArgs, {
|
|
623
|
+
encoding: 'utf8',
|
|
624
|
+
env: AW_DOCS_GIT_ENV,
|
|
625
|
+
timeout: AW_DOCS_CLONE_TIMEOUT_MS,
|
|
626
|
+
});
|
|
627
|
+
if (wantSparse) {
|
|
628
|
+
await execFile('git', ['sparse-checkout', 'set', '--cone', ...sparseIncludePaths, AW_DOCS_CACHE_PATH], {
|
|
629
|
+
cwd: cloneDir,
|
|
630
|
+
encoding: 'utf8',
|
|
631
|
+
}).catch(() => { /* sparse unsupported → full checkout below */ });
|
|
632
|
+
}
|
|
587
633
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
634
|
+
|
|
635
|
+
// Only validate cleanliness for a REUSED clone. A fresh blobless
|
|
636
|
+
// --no-checkout clone intentionally has no working tree yet (it is
|
|
637
|
+
// materialised by the branch checkout below), which would otherwise
|
|
638
|
+
// register as "dirty".
|
|
639
|
+
if (!freshClone) {
|
|
640
|
+
let status = await getGitStatus(cloneDir);
|
|
641
|
+
if (status.trim() && await selfHealManagedAwCacheDirtiness(cloneDir, status)) {
|
|
642
|
+
status = await getGitStatus(cloneDir);
|
|
643
|
+
}
|
|
644
|
+
if (status.trim()) {
|
|
645
|
+
throw new Error([
|
|
646
|
+
`AW docs repo worktree is dirty: ${cloneDir}`,
|
|
647
|
+
status.trim(),
|
|
648
|
+
'Clean the cached docs repo or set AW_DOCS_WORKTREE to a clean clone before publishing.',
|
|
649
|
+
].join('\n'));
|
|
650
|
+
}
|
|
594
651
|
}
|
|
595
652
|
|
|
596
653
|
await execFile('git', ['fetch', 'origin'], {
|
|
597
654
|
cwd: cloneDir,
|
|
598
655
|
encoding: 'utf8',
|
|
656
|
+
env: AW_DOCS_GIT_ENV,
|
|
657
|
+
timeout: AW_DOCS_NET_TIMEOUT_MS,
|
|
599
658
|
});
|
|
600
659
|
|
|
660
|
+
// Keep the current publish's subtree inside the sparse cone. Only applies to
|
|
661
|
+
// clones that already use sparse-checkout (i.e. the blobless clones we
|
|
662
|
+
// created) — legacy full clones are left untouched so there is no regression.
|
|
663
|
+
if (wantSparse && await awDocsSparseEnabled(cloneDir)) {
|
|
664
|
+
await execFile('git', ['sparse-checkout', 'add', ...sparseIncludePaths, AW_DOCS_CACHE_PATH], {
|
|
665
|
+
cwd: cloneDir,
|
|
666
|
+
encoding: 'utf8',
|
|
667
|
+
}).catch(() => { /* best effort — checkout below still works */ });
|
|
668
|
+
}
|
|
669
|
+
|
|
601
670
|
const hasPublishBranch = await remoteBranchExists(cloneDir, publishConfig.branch);
|
|
602
671
|
const checkoutStart = hasPublishBranch
|
|
603
672
|
? `origin/${publishConfig.branch}`
|
|
@@ -608,9 +677,13 @@ async function ensureAwDocsRepoClone(home, publishConfig) {
|
|
|
608
677
|
}
|
|
609
678
|
|
|
610
679
|
try {
|
|
680
|
+
// With a blobless clone, checkout lazily fetches the missing blobs for the
|
|
681
|
+
// sparse cone — a network op, so bound it like the other remote calls.
|
|
611
682
|
await execFile('git', ['checkout', '-B', publishConfig.branch, checkoutStart], {
|
|
612
683
|
cwd: cloneDir,
|
|
613
684
|
encoding: 'utf8',
|
|
685
|
+
env: AW_DOCS_GIT_ENV,
|
|
686
|
+
timeout: AW_DOCS_NET_TIMEOUT_MS,
|
|
614
687
|
});
|
|
615
688
|
} catch (e) {
|
|
616
689
|
const healedPath = isCheckoutOverwrittenByAwPathError(e)
|
|
@@ -622,6 +695,8 @@ async function ensureAwDocsRepoClone(home, publishConfig) {
|
|
|
622
695
|
await execFile('git', ['checkout', '-B', publishConfig.branch, checkoutStart], {
|
|
623
696
|
cwd: cloneDir,
|
|
624
697
|
encoding: 'utf8',
|
|
698
|
+
env: AW_DOCS_GIT_ENV,
|
|
699
|
+
timeout: AW_DOCS_NET_TIMEOUT_MS,
|
|
625
700
|
});
|
|
626
701
|
}
|
|
627
702
|
|
|
@@ -629,6 +704,8 @@ async function ensureAwDocsRepoClone(home, publishConfig) {
|
|
|
629
704
|
await execFile('git', ['pull', '--ff-only', 'origin', publishConfig.branch], {
|
|
630
705
|
cwd: cloneDir,
|
|
631
706
|
encoding: 'utf8',
|
|
707
|
+
env: AW_DOCS_GIT_ENV,
|
|
708
|
+
timeout: AW_DOCS_NET_TIMEOUT_MS,
|
|
632
709
|
});
|
|
633
710
|
}
|
|
634
711
|
await removeTrackedAwSymlink(cloneDir);
|
|
@@ -729,6 +806,8 @@ async function commitAndPushAwDocsRepo(docsRepoDir, { message, branch }) {
|
|
|
729
806
|
await execFile('git', ['push', '-u', 'origin', `HEAD:${branch}`], {
|
|
730
807
|
cwd: docsRepoDir,
|
|
731
808
|
encoding: 'utf8',
|
|
809
|
+
env: AW_DOCS_GIT_ENV,
|
|
810
|
+
timeout: AW_DOCS_NET_TIMEOUT_MS,
|
|
732
811
|
});
|
|
733
812
|
}
|
|
734
813
|
|
|
@@ -823,7 +902,12 @@ async function publishProjectAwDocs(cwd, home, dryRun, scope = null) {
|
|
|
823
902
|
const s = fmt.spinner();
|
|
824
903
|
s.start(`Publishing ${files.length} AW doc${files.length > 1 ? 's' : ''} to ${publishConfig.repo}...`);
|
|
825
904
|
try {
|
|
826
|
-
|
|
905
|
+
// Scope the (blobless) docs clone to only the subtree this publish writes,
|
|
906
|
+
// so a fresh clone fetches a few MB instead of the whole org's docs.
|
|
907
|
+
const sparseIncludePaths = isRootScope
|
|
908
|
+
? [`${publishConfig.dest}/${scope.relPrefix}`]
|
|
909
|
+
: [`${publishConfig.dest}/${repoSlug}/${githubUsername}`];
|
|
910
|
+
const docsRepoDir = await ensureAwDocsRepoClone(home, publishConfig, sparseIncludePaths);
|
|
827
911
|
const deleteTarget = isRootScope
|
|
828
912
|
? join(docsRepoDir, publishConfig.dest, scope.relPrefix)
|
|
829
913
|
: scope?.relPrefix
|