Skip to content

Commit a87788b

Browse files
committed
feat(setup): detach from latest release
1 parent 7b6bad5 commit a87788b

2 files changed

Lines changed: 181 additions & 8 deletions

File tree

scripts/detach-instance.mjs

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22

33
import fs from "node:fs";
44
import path from "node:path";
5+
import { execFileSync } from "node:child_process";
6+
7+
import { latestStableTagFromLsRemote } from "./lib/upgrade-source.mjs";
58

69
const ROOT = process.cwd();
10+
const DEFAULT_REMOTE = "https://github.com/vanityURLs/code.git";
711
const INSTANCE_README_PATH = path.join(ROOT, "docs", "README.md");
812
const ROOT_README_PATH = path.join(ROOT, "README.md");
913
const DETACH_PATHS = [
@@ -21,6 +25,125 @@ const DETACH_PATHS = [
2125
"docs",
2226
"scripts/v8s.zsh"
2327
];
28+
const args = parseArgs(process.argv.slice(2));
29+
30+
function parseArgs(argv) {
31+
const parsed = {
32+
currentRef: false,
33+
help: false
34+
};
35+
36+
for (const arg of argv) {
37+
if (arg === "--current-ref") {
38+
parsed.currentRef = true;
39+
continue;
40+
}
41+
if (arg === "-h" || arg === "--help") {
42+
parsed.help = true;
43+
continue;
44+
}
45+
console.error(`[detach] Unknown option: ${arg}`);
46+
process.exit(1);
47+
}
48+
49+
return parsed;
50+
}
51+
52+
function printHelp() {
53+
console.log(`Usage: npm run detach -- [options]
54+
55+
Detach a vanityURLs product checkout into a standalone instance.
56+
57+
Options:
58+
--current-ref Detach the currently checked-out files instead of first switching
59+
to the latest stable vanityURLs release.
60+
-h, --help Show this help.`);
61+
}
62+
63+
function git(gitArgs, options = {}) {
64+
return execFileSync("git", gitArgs, {
65+
cwd: ROOT,
66+
encoding: "utf8",
67+
stdio: options.capture ? ["ignore", "pipe", "pipe"] : "pipe"
68+
}).trim();
69+
}
70+
71+
function gitQuiet(gitArgs) {
72+
execFileSync("git", gitArgs, {
73+
cwd: ROOT,
74+
stdio: "ignore"
75+
});
76+
}
77+
78+
function isGitCheckout() {
79+
try {
80+
git(["rev-parse", "--is-inside-work-tree"], { capture: true });
81+
return true;
82+
} catch {
83+
return false;
84+
}
85+
}
86+
87+
function remoteURL() {
88+
try {
89+
return git(["config", "--get", "remote.origin.url"], { capture: true }) || DEFAULT_REMOTE;
90+
} catch {
91+
return DEFAULT_REMOTE;
92+
}
93+
}
94+
95+
function worktreeStatus() {
96+
try {
97+
return git(["status", "--porcelain"], { capture: true });
98+
} catch {
99+
return "";
100+
}
101+
}
102+
103+
function checkoutLatestRelease() {
104+
if (args.currentRef) {
105+
console.log("[detach] Keeping the currently checked-out files.");
106+
return;
107+
}
108+
109+
if (!isGitCheckout()) {
110+
console.error("[detach] Refusing to switch releases: this directory is not a Git checkout.");
111+
console.error("[detach] Retry from a normal git clone, or run npm run detach -- --current-ref.");
112+
process.exit(1);
113+
}
114+
115+
const status = worktreeStatus();
116+
if (status) {
117+
console.error("[detach] Refusing to switch releases because the working tree is not clean.");
118+
console.error("[detach] Commit, stash, or discard local changes, or run npm run detach -- --current-ref.");
119+
process.exit(1);
120+
}
121+
122+
const remote = remoteURL();
123+
let tag = "";
124+
try {
125+
tag = latestStableTagFromLsRemote(git(["ls-remote", "--tags", "--refs", remote, "v*"], { capture: true }));
126+
} catch (error) {
127+
console.error(`[detach] Could not query vanityURLs releases: ${error.message}`);
128+
console.error("[detach] Retry when the network is available, or run npm run detach -- --current-ref.");
129+
process.exit(1);
130+
}
131+
132+
if (!tag) {
133+
console.error(`[detach] No stable vanityURLs release tag was found for ${remote}.`);
134+
console.error("[detach] Retry later, or run npm run detach -- --current-ref.");
135+
process.exit(1);
136+
}
137+
138+
gitQuiet(["fetch", "--depth", "1", remote, `refs/tags/${tag}:refs/tags/${tag}`]);
139+
gitQuiet(["checkout", "--detach", tag]);
140+
console.log(`[detach] Checked out vanityURLs release ${tag}.`);
141+
}
142+
143+
function initInstanceRepository() {
144+
gitQuiet(["init"]);
145+
console.log("[detach] Initialized a fresh Git repository for this instance.");
146+
}
24147

25148
function hasExpectedPackage() {
26149
const packagePath = path.join(ROOT, "package.json");
@@ -34,11 +157,18 @@ function hasExpectedPackage() {
34157
}
35158
}
36159

160+
if (args.help) {
161+
printHelp();
162+
process.exit(0);
163+
}
164+
37165
if (!hasExpectedPackage()) {
38166
console.error("[detach] Refusing to run: this directory does not look like a vanityURLs code checkout.");
39167
process.exit(1);
40168
}
41169

170+
checkoutLatestRelease();
171+
42172
if (fs.existsSync(INSTANCE_README_PATH)) {
43173
fs.copyFileSync(INSTANCE_README_PATH, ROOT_README_PATH);
44174
console.log("[detach] Replaced README.md with the instance README.");
@@ -58,4 +188,5 @@ for (const relativePath of DETACH_PATHS) {
58188
console.log(`[detach] Removed ${relativePath}`);
59189
}
60190

61-
console.log("[detach] Ready for git init in your own repository.");
191+
initInstanceRepository();
192+
console.log("[detach] Ready for your first instance commit.");

scripts/detach-instance.test.mjs

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ const ROOT = process.cwd();
1010

1111
function makeFixture() {
1212
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "v8s-detach-"));
13+
const sourceDir = path.join(tmpDir, "source");
14+
const fixtureDir = path.join(tmpDir, "fixture");
1315

14-
fs.cpSync(ROOT, tmpDir, {
16+
fs.cpSync(ROOT, sourceDir, {
1517
recursive: true,
1618
filter: (sourcePath) => {
1719
const relative = path.relative(ROOT, sourcePath);
@@ -20,18 +22,39 @@ function makeFixture() {
2022
}
2123
});
2224

23-
fs.mkdirSync(path.join(tmpDir, ".git"), { recursive: true });
24-
fs.mkdirSync(path.join(tmpDir, ".github", "workflows"), { recursive: true });
25-
fs.writeFileSync(path.join(tmpDir, "RELEASE.md"), "release notes\n");
26-
fs.writeFileSync(path.join(tmpDir, "scripts", "v8s.zsh"), "# legacy helper\n");
25+
fs.mkdirSync(path.join(sourceDir, ".github", "workflows"), { recursive: true });
26+
fs.writeFileSync(path.join(sourceDir, "RELEASE.md"), "release notes\n");
27+
fs.writeFileSync(path.join(sourceDir, "scripts", "v8s.zsh"), "# legacy helper\n");
2728

28-
return tmpDir;
29+
git(["init"], sourceDir);
30+
git(["config", "user.email", "tests@example.com"], sourceDir);
31+
git(["config", "user.name", "vanityURLs tests"], sourceDir);
32+
git(["add", "."], sourceDir);
33+
git(["commit", "-m", "test fixture release"], sourceDir);
34+
git(["tag", "v9.9.9"], sourceDir);
35+
fs.writeFileSync(path.join(sourceDir, "CURRENT_ONLY.txt"), "current branch only\n");
36+
git(["add", "CURRENT_ONLY.txt"], sourceDir);
37+
git(["commit", "-m", "test fixture current branch"], sourceDir);
38+
39+
execFileSync("git", ["clone", sourceDir, fixtureDir], {
40+
stdio: "pipe"
41+
});
42+
43+
return fixtureDir;
2944
}
3045

3146
function exists(fixture, relativePath) {
3247
return fs.existsSync(path.join(fixture, relativePath));
3348
}
3449

50+
function git(args, cwd, options = {}) {
51+
return execFileSync("git", args, {
52+
cwd,
53+
encoding: "utf8",
54+
stdio: options.capture ? ["ignore", "pipe", "pipe"] : "pipe"
55+
}).trim();
56+
}
57+
3558
{
3659
const fixture = makeFixture();
3760
const instanceReadmePath = path.join(fixture, "docs", "README.md");
@@ -44,9 +67,13 @@ function exists(fixture, relativePath) {
4467
});
4568

4669
assert.equal(fs.readFileSync(path.join(fixture, "README.md"), "utf8"), instanceReadme || originalReadme);
70+
assert.equal(
71+
exists(fixture, "CURRENT_ONLY.txt"),
72+
false,
73+
"detach should switch to the latest stable release by default"
74+
);
4775

4876
for (const relativePath of [
49-
".git",
5077
".github",
5178
".all-contributorsrc",
5279
".release-please-manifest.json",
@@ -66,6 +93,21 @@ function exists(fixture, relativePath) {
6693
assert.equal(exists(fixture, "package.json"), true);
6794
assert.equal(exists(fixture, "scripts/v8s.sh"), true);
6895
assert.equal(exists(fixture, "scripts/v8s-lnk"), true);
96+
assert.equal(exists(fixture, ".git"), true, "detach should initialize a fresh instance Git repository");
97+
assert.equal(git(["rev-parse", "--is-inside-work-tree"], fixture, { capture: true }), "true");
98+
assert.throws(() => git(["config", "--get", "remote.origin.url"], fixture, { capture: true }));
99+
}
100+
101+
{
102+
const fixture = makeFixture();
103+
104+
execFileSync(process.execPath, ["scripts/detach-instance.mjs", "--current-ref"], {
105+
cwd: fixture,
106+
stdio: "pipe"
107+
});
108+
109+
assert.equal(exists(fixture, "CURRENT_ONLY.txt"), true, "--current-ref should detach the current checkout");
110+
assert.equal(exists(fixture, ".git"), true, "--current-ref should still initialize a fresh instance Git repository");
69111
}
70112

71113
console.log("detach tests ok");

0 commit comments

Comments
 (0)