@usequark/quark-create-app 1.23.12 → 1.23.13
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/package.json +13 -1
- package/src/dependabot-config.test.js +0 -128
- package/src/deploy/adapters/railway.test.js +0 -318
- package/src/deploy/deploy.integration.test.js +0 -183
- package/src/deploy/deploy.test.js +0 -105
- package/src/deploy/discovery.test.js +0 -141
- package/src/mobile-template.test.js +0 -232
- package/src/prepare-hooks.test.js +0 -535
- package/src/scaffold-guards.test.js +0 -140
- package/src/scaffold-output.test.js +0 -278
- package/src/template-scripts.test.js +0 -160
- package/src/utils.test.js +0 -95
- package/src/worker-dockerfile.test.js +0 -85
package/package.json
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usequark/quark-create-app",
|
|
3
|
-
"version": "1.23.
|
|
3
|
+
"version": "1.23.13",
|
|
4
4
|
"description": "CLI to scaffold new Quark projects with Next.js, Prisma, BullMQ, and Railway deployment",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"quark",
|
|
7
|
+
"scaffolding",
|
|
8
|
+
"scaffolder",
|
|
9
|
+
"cli",
|
|
10
|
+
"nextjs",
|
|
11
|
+
"prisma",
|
|
12
|
+
"bullmq",
|
|
13
|
+
"railway",
|
|
14
|
+
"self-hosted"
|
|
15
|
+
],
|
|
5
16
|
"type": "module",
|
|
6
17
|
"homepage": "https://github.com/usequark/quark#readme",
|
|
7
18
|
"repository": {
|
|
@@ -21,6 +32,7 @@
|
|
|
21
32
|
},
|
|
22
33
|
"files": [
|
|
23
34
|
"src",
|
|
35
|
+
"!src/**/*.test.js",
|
|
24
36
|
"templates",
|
|
25
37
|
"README.md"
|
|
26
38
|
],
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Guard the scaffold template's dependabot config.
|
|
5
|
-
*
|
|
6
|
-
* The template used to group by `dependency-type` alone, which produced one
|
|
7
|
-
* PR bundling patch, minor and major bumps together. That is how several
|
|
8
|
-
* breaking changes reached this repo unnoticed - bullmq 6 removed
|
|
9
|
-
* `queue.client` and shipped two silent production regressions inside a
|
|
10
|
-
* 35-package diff.
|
|
11
|
-
*
|
|
12
|
-
* This checks the shipped template still separates majors, so a scaffolded
|
|
13
|
-
* project does not inherit the problem.
|
|
14
|
-
*
|
|
15
|
-
* Run with: node --test packages/cli/src/dependabot-config.test.js
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import assert from "node:assert/strict";
|
|
19
|
-
import fs from "node:fs";
|
|
20
|
-
import path from "node:path";
|
|
21
|
-
import { describe, it } from "node:test";
|
|
22
|
-
import { fileURLToPath } from "node:url";
|
|
23
|
-
|
|
24
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
-
const REPO_ROOT = path.resolve(__dirname, "../../..");
|
|
26
|
-
|
|
27
|
-
const CONFIGS = [
|
|
28
|
-
["monorepo", ".github/dependabot.yml"],
|
|
29
|
-
[
|
|
30
|
-
"scaffold template",
|
|
31
|
-
"packages/cli/templates/base-project/.github/dependabot.yml",
|
|
32
|
-
],
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
function readConfig(relativePath) {
|
|
36
|
-
return fs.readFileSync(path.join(REPO_ROOT, relativePath), "utf8");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
describe("dependabot config", () => {
|
|
40
|
-
for (const [label, relativePath] of CONFIGS) {
|
|
41
|
-
it(`${label} isolates major bumps into their own group`, () => {
|
|
42
|
-
const text = readConfig(relativePath);
|
|
43
|
-
|
|
44
|
-
assert.match(
|
|
45
|
-
text,
|
|
46
|
-
/production-majors:[\s\S]*?dependency-type:\s*production[\s\S]*?update-types:\s*\["major"\]/,
|
|
47
|
-
"production majors must be grouped separately so a breaking change is reviewed alone",
|
|
48
|
-
);
|
|
49
|
-
assert.match(
|
|
50
|
-
text,
|
|
51
|
-
/development-majors:[\s\S]*?dependency-type:\s*development[\s\S]*?update-types:\s*\["major"\]/,
|
|
52
|
-
"development majors must be grouped separately too",
|
|
53
|
-
);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it(`${label} does not lump every production bump into one group`, () => {
|
|
57
|
-
const text = readConfig(relativePath);
|
|
58
|
-
|
|
59
|
-
// A bare `production:` group with no update-types collects majors,
|
|
60
|
-
// minors and patches into a single reviewable-looking PR.
|
|
61
|
-
assert.doesNotMatch(
|
|
62
|
-
text,
|
|
63
|
-
/\n {6}production:\n {8}dependency-type:\s*production\n/,
|
|
64
|
-
"a catch-all production group re-creates the 35-package PR problem",
|
|
65
|
-
);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it(`${label} keeps a PR limit that can hold the extra groups`, () => {
|
|
69
|
-
const text = readConfig(relativePath);
|
|
70
|
-
const limit = Number(
|
|
71
|
-
(text.match(/open-pull-requests-limit:\s*(\d+)/) || [])[1],
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
assert.ok(
|
|
75
|
-
Number.isInteger(limit) && limit >= 6,
|
|
76
|
-
`open-pull-requests-limit should be at least 6 for the split groups (got ${limit})`,
|
|
77
|
-
);
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* The auto-merge workflow is the other half of the split, and the only part
|
|
84
|
-
* that actually merges anything.
|
|
85
|
-
*
|
|
86
|
-
* Splitting dependabot into per-severity groups changes what a human sees. It
|
|
87
|
-
* does not change what merges unattended - that is gated solely by
|
|
88
|
-
* `update-type == 'version-update:semver-patch'`. A dependency that ships a
|
|
89
|
-
* breaking change in a *patch* passes straight through, and that is exactly how
|
|
90
|
-
* bullmq 6's `queue.client` removal reached this repo. Majors already need a
|
|
91
|
-
* human by virtue of not matching; this test keeps it that way.
|
|
92
|
-
*
|
|
93
|
-
* The monorepo runs this workflow; the template ships an equivalent one, so a
|
|
94
|
-
* scaffolded project inherits the same gate.
|
|
95
|
-
*/
|
|
96
|
-
const AUTO_MERGE_WORKFLOWS = [
|
|
97
|
-
["monorepo", ".github/workflows/dependabot-auto-merge.yml"],
|
|
98
|
-
[
|
|
99
|
-
"scaffold template",
|
|
100
|
-
"packages/cli/templates/base-project/.github/workflows/dependabot-auto-merge.yml",
|
|
101
|
-
],
|
|
102
|
-
];
|
|
103
|
-
|
|
104
|
-
describe("dependabot auto-merge gate", () => {
|
|
105
|
-
for (const [label, relativePath] of AUTO_MERGE_WORKFLOWS) {
|
|
106
|
-
it(`${label} gates auto-merge on the semver update type`, () => {
|
|
107
|
-
const text = readConfig(relativePath);
|
|
108
|
-
|
|
109
|
-
assert.match(
|
|
110
|
-
text,
|
|
111
|
-
/version-update:semver-patch/,
|
|
112
|
-
"the auto-merge gate must key on the semver patch update type, so minors and majors still need a human",
|
|
113
|
-
);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it(`${label} does not merge every dependabot update`, () => {
|
|
117
|
-
const text = readConfig(relativePath);
|
|
118
|
-
|
|
119
|
-
// An unconditional `gh pr merge` would auto-merge majors unattended,
|
|
120
|
-
// which is precisely how the bullmq break nearly landed.
|
|
121
|
-
assert.doesNotMatch(
|
|
122
|
-
text,
|
|
123
|
-
/if:\s*github\.actor\s*==\s*'dependabot\[bot\]'\n\s*steps:/,
|
|
124
|
-
"auto-merge must be gated on the update type, not just the actor",
|
|
125
|
-
);
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
});
|
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
// biome-ignore-all lint/suspicious/noTemplateCurlyInString: this file exists to
|
|
2
|
-
// exercise Railway's own `${{Service.VAR}}` template syntax and the escaper that
|
|
3
|
-
// neutralises a literal `${}`. Both are plain strings by design, and the rule
|
|
4
|
-
// cannot tell them apart from a missed template interpolation.
|
|
5
|
-
import assert from "node:assert/strict";
|
|
6
|
-
import fs from "node:fs/promises";
|
|
7
|
-
import os from "node:os";
|
|
8
|
-
import path from "node:path";
|
|
9
|
-
import { after, test } from "node:test";
|
|
10
|
-
|
|
11
|
-
const temporaryDirectories = [];
|
|
12
|
-
|
|
13
|
-
after(async () => {
|
|
14
|
-
await Promise.all(
|
|
15
|
-
temporaryDirectories.map((dir) =>
|
|
16
|
-
fs.rm(dir, { recursive: true, force: true }),
|
|
17
|
-
),
|
|
18
|
-
);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
async function makeTempDir() {
|
|
22
|
-
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "railway-test-"));
|
|
23
|
-
temporaryDirectories.push(dir);
|
|
24
|
-
return dir;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// --- Mock execa ---
|
|
28
|
-
// We inject a mock by rewriting the module's execa reference via
|
|
29
|
-
// import.meta mocking. Since node:test doesn't support that natively,
|
|
30
|
-
// we test the adapter functions by providing our own execa replacement.
|
|
31
|
-
// The module imports execa from "execa"; for testing we verify behavior
|
|
32
|
-
// by calling internal helpers with mocked execa.
|
|
33
|
-
|
|
34
|
-
// Instead, test the CLI wrapper functions using a utility that replaces execa.
|
|
35
|
-
// We'll test the public interface by calling the real functions against
|
|
36
|
-
// a fixture directory and testing the helper functions.
|
|
37
|
-
|
|
38
|
-
test("checkRailwayCLI returns null when railway is not installed", async () => {
|
|
39
|
-
const { checkRailwayCLI } = await import("./railway.js");
|
|
40
|
-
// On a system without railway, this returns null.
|
|
41
|
-
// On a system with railway, it returns a version string.
|
|
42
|
-
// We just verify it returns either null or a non-empty string.
|
|
43
|
-
const result = await checkRailwayCLI();
|
|
44
|
-
if (result === null) {
|
|
45
|
-
assert.equal(result, null);
|
|
46
|
-
} else {
|
|
47
|
-
assert.ok(typeof result === "string" && result.length > 0);
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("checkRailwayLogin returns null when not logged in", async () => {
|
|
52
|
-
const { checkRailwayLogin } = await import("./railway.js");
|
|
53
|
-
const result = await checkRailwayLogin();
|
|
54
|
-
if (result === null) {
|
|
55
|
-
assert.equal(result, null);
|
|
56
|
-
} else {
|
|
57
|
-
assert.ok(typeof result === "string" && result.length > 0);
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("isProjectLinked returns false when .railway directory does not exist", async () => {
|
|
62
|
-
const { isProjectLinked } = await import("./railway.js");
|
|
63
|
-
const tmpDir = await makeTempDir();
|
|
64
|
-
const result = await isProjectLinked(tmpDir);
|
|
65
|
-
assert.equal(result, false);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test("isProjectLinked returns true when .railway directory contains config", async () => {
|
|
69
|
-
const { isProjectLinked } = await import("./railway.js");
|
|
70
|
-
const tmpDir = await makeTempDir();
|
|
71
|
-
const railwayDir = path.join(tmpDir, ".railway");
|
|
72
|
-
await fs.mkdir(railwayDir, { recursive: true });
|
|
73
|
-
await fs.writeFile(
|
|
74
|
-
path.join(railwayDir, "config.json"),
|
|
75
|
-
JSON.stringify({ projectId: "test" }),
|
|
76
|
-
);
|
|
77
|
-
const result = await isProjectLinked(tmpDir);
|
|
78
|
-
assert.equal(result, true);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test("isProjectLinked returns false when .railway directory is empty", async () => {
|
|
82
|
-
const { isProjectLinked } = await import("./railway.js");
|
|
83
|
-
const tmpDir = await makeTempDir();
|
|
84
|
-
const railwayDir = path.join(tmpDir, ".railway");
|
|
85
|
-
await fs.mkdir(railwayDir, { recursive: true });
|
|
86
|
-
const result = await isProjectLinked(tmpDir);
|
|
87
|
-
assert.equal(result, false);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test("RailwayError has correct shape", async () => {
|
|
91
|
-
const { RailwayError, DIAGNOSTIC_CODES } = await import("./railway.js");
|
|
92
|
-
const error = new RailwayError(
|
|
93
|
-
"test error",
|
|
94
|
-
DIAGNOSTIC_CODES.RAILWAY_CLI_NOT_FOUND,
|
|
95
|
-
{ detail: "x" },
|
|
96
|
-
);
|
|
97
|
-
assert.equal(error.name, "RailwayError");
|
|
98
|
-
assert.equal(error.code, DIAGNOSTIC_CODES.RAILWAY_CLI_NOT_FOUND);
|
|
99
|
-
assert.equal(error.message, "test error");
|
|
100
|
-
assert.deepEqual(error.meta, { detail: "x" });
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test("DIAGNOSTIC_CODES are frozen", async () => {
|
|
104
|
-
const { DIAGNOSTIC_CODES } = await import("./railway.js");
|
|
105
|
-
assert.throws(() => {
|
|
106
|
-
DIAGNOSTIC_CODES.NEW_CODE = "test";
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// --- IaC adapter tests ---
|
|
111
|
-
|
|
112
|
-
test("generateIacFile creates .railway/railway.ts with preserve() for secrets", async () => {
|
|
113
|
-
const { generateIacFile } = await import("./iac.js");
|
|
114
|
-
const tmpDir = await makeTempDir();
|
|
115
|
-
const iacPath = path.join(tmpDir, ".railway", "railway.ts");
|
|
116
|
-
|
|
117
|
-
await generateIacFile({
|
|
118
|
-
iacPath,
|
|
119
|
-
services: [
|
|
120
|
-
{ name: "web", kind: "web", relativeRootDir: "apps/web" },
|
|
121
|
-
{ name: "worker", kind: "worker", relativeRootDir: "apps/worker" },
|
|
122
|
-
],
|
|
123
|
-
secrets: {
|
|
124
|
-
AUTH_SECRET: "preserve()",
|
|
125
|
-
NEXTAUTH_SECRET: "preserve()",
|
|
126
|
-
},
|
|
127
|
-
variableRefs: {
|
|
128
|
-
DATABASE_URL: "${{Postgres.DATABASE_URL}}",
|
|
129
|
-
REDIS_URL: "${{Redis.REDIS_URL}}",
|
|
130
|
-
},
|
|
131
|
-
serviceVars: {
|
|
132
|
-
worker: { WORKER_CONCURRENCY: '"5"' },
|
|
133
|
-
},
|
|
134
|
-
projectName: "test-project",
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
const content = await fs.readFile(iacPath, "utf8");
|
|
138
|
-
assert.ok(content.includes("import { defineRailway"));
|
|
139
|
-
assert.ok(content.includes('service("web"'));
|
|
140
|
-
assert.ok(content.includes('service("worker"'));
|
|
141
|
-
assert.ok(content.includes("preserve()"));
|
|
142
|
-
assert.ok(content.includes("${{Postgres.DATABASE_URL}}"));
|
|
143
|
-
assert.ok(content.includes("${{Redis.REDIS_URL}}"));
|
|
144
|
-
assert.ok(content.includes("pnpm db:migrate:deploy"));
|
|
145
|
-
assert.ok(content.includes("/api/health"));
|
|
146
|
-
assert.ok(content.includes("WORKER_CONCURRENCY"));
|
|
147
|
-
assert.ok(content.includes('"test-project"'));
|
|
148
|
-
// Must NOT contain github() source — CLI deploys manage settings, not sources
|
|
149
|
-
assert.ok(
|
|
150
|
-
!content.includes("github("),
|
|
151
|
-
"Generated IaC must not reference github()",
|
|
152
|
-
);
|
|
153
|
-
assert.ok(
|
|
154
|
-
!content.includes("source:"),
|
|
155
|
-
"Generated IaC must not include source field",
|
|
156
|
-
);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
test("escapeTsString escapes dangerous characters", async () => {
|
|
160
|
-
const { escapeTsString } = await import("./iac.js");
|
|
161
|
-
|
|
162
|
-
assert.strictEqual(escapeTsString('hello"world'), 'hello\\"world');
|
|
163
|
-
assert.strictEqual(escapeTsString("back\\slash"), "back\\\\slash");
|
|
164
|
-
assert.strictEqual(escapeTsString("line\nbreak"), "line\\nbreak");
|
|
165
|
-
assert.strictEqual(escapeTsString("dollar${}"), "dollar\\${}");
|
|
166
|
-
assert.strictEqual(escapeTsString("cr\rreturn"), "cr\\rreturn");
|
|
167
|
-
assert.strictEqual(escapeTsString("safe-value"), "safe-value");
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
test("generateIacFile escapes service names with special chars", async () => {
|
|
171
|
-
const { generateIacFile } = await import("./iac.js");
|
|
172
|
-
const tmpDir = await makeTempDir();
|
|
173
|
-
const iacPath = path.join(tmpDir, ".railway", "railway.ts");
|
|
174
|
-
|
|
175
|
-
await generateIacFile({
|
|
176
|
-
iacPath,
|
|
177
|
-
services: [
|
|
178
|
-
{ name: 'my "web" app', kind: "web", relativeRootDir: "apps/web" },
|
|
179
|
-
],
|
|
180
|
-
projectName: 'project "name"',
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
const content = await fs.readFile(iacPath, "utf8");
|
|
184
|
-
// Variable name uses sanitized identifier (underscores, not escaped quotes)
|
|
185
|
-
assert.ok(content.includes('const my_web_app = service("my \\"web\\" app"'));
|
|
186
|
-
// Project name is escaped in string, not double-escaped
|
|
187
|
-
assert.ok(content.includes('project("project \\"name\\""'));
|
|
188
|
-
// Resources list uses identifiers
|
|
189
|
-
assert.ok(content.includes("resources: [my_web_app]"));
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
// --- Environment flag forwarding tests ---
|
|
193
|
-
// These use a mock railway CLI on PATH to verify --environment is
|
|
194
|
-
// forwarded to execa for each function that supports it.
|
|
195
|
-
|
|
196
|
-
async function withMockRailway(fn) {
|
|
197
|
-
const tmpDir = await makeTempDir();
|
|
198
|
-
const railwayPath = path.join(tmpDir, "railway");
|
|
199
|
-
const argsLog = path.join(tmpDir, "args.log");
|
|
200
|
-
|
|
201
|
-
await fs.writeFile(
|
|
202
|
-
railwayPath,
|
|
203
|
-
`#!/bin/sh
|
|
204
|
-
echo "$*" >> "${argsLog}"
|
|
205
|
-
case "$*" in
|
|
206
|
-
*"deployment list"*)
|
|
207
|
-
echo '[{"id":"dep_mock_123","status":"SUCCESS"}]'
|
|
208
|
-
;;
|
|
209
|
-
*"list"*)
|
|
210
|
-
echo '[]'
|
|
211
|
-
;;
|
|
212
|
-
*)
|
|
213
|
-
echo '{"deploymentId":"dep_mock_123"}'
|
|
214
|
-
;;
|
|
215
|
-
esac
|
|
216
|
-
`,
|
|
217
|
-
);
|
|
218
|
-
await fs.chmod(railwayPath, 0o755);
|
|
219
|
-
|
|
220
|
-
const origPath = process.env.PATH;
|
|
221
|
-
process.env.PATH = `${tmpDir}:${origPath}`;
|
|
222
|
-
|
|
223
|
-
try {
|
|
224
|
-
await fn(tmpDir, argsLog);
|
|
225
|
-
} finally {
|
|
226
|
-
process.env.PATH = origPath;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
test("deployService forwards --environment flag", async () => {
|
|
231
|
-
await withMockRailway(async (tmpDir, argsLog) => {
|
|
232
|
-
const { deployService } = await import("./railway.js");
|
|
233
|
-
|
|
234
|
-
await deployService("web", { cwd: tmpDir, environment: "staging" });
|
|
235
|
-
|
|
236
|
-
const log = await fs.readFile(argsLog, "utf-8");
|
|
237
|
-
assert.ok(
|
|
238
|
-
log.includes("--environment"),
|
|
239
|
-
`Expected --environment in args: ${log}`,
|
|
240
|
-
);
|
|
241
|
-
assert.ok(log.includes("staging"), `Expected staging in args: ${log}`);
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
test("setProjectVariable forwards --environment flag", async () => {
|
|
246
|
-
await withMockRailway(async (tmpDir, argsLog) => {
|
|
247
|
-
const { setProjectVariable } = await import("./railway.js");
|
|
248
|
-
|
|
249
|
-
await setProjectVariable("KEY", "val", {
|
|
250
|
-
cwd: tmpDir,
|
|
251
|
-
environment: "staging",
|
|
252
|
-
serviceName: "web",
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
const log = await fs.readFile(argsLog, "utf-8");
|
|
256
|
-
assert.ok(
|
|
257
|
-
log.includes("--environment"),
|
|
258
|
-
`Expected --environment in args: ${log}`,
|
|
259
|
-
);
|
|
260
|
-
assert.ok(log.includes("staging"), `Expected staging in args: ${log}`);
|
|
261
|
-
});
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
test("setProjectVariables forwards --environment flag", async () => {
|
|
265
|
-
await withMockRailway(async (tmpDir, argsLog) => {
|
|
266
|
-
const { setProjectVariables } = await import("./railway.js");
|
|
267
|
-
|
|
268
|
-
await setProjectVariables(
|
|
269
|
-
[
|
|
270
|
-
{ key: "A", value: "1" },
|
|
271
|
-
{ key: "B", value: "2" },
|
|
272
|
-
],
|
|
273
|
-
{ cwd: tmpDir, environment: "staging", serviceName: "web" },
|
|
274
|
-
);
|
|
275
|
-
|
|
276
|
-
const log = await fs.readFile(argsLog, "utf-8");
|
|
277
|
-
assert.ok(
|
|
278
|
-
log.includes("--environment"),
|
|
279
|
-
`Expected --environment in args: ${log}`,
|
|
280
|
-
);
|
|
281
|
-
assert.ok(log.includes("staging"), `Expected staging in args: ${log}`);
|
|
282
|
-
});
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
test("getExistingVariable forwards --environment flag", async () => {
|
|
286
|
-
await withMockRailway(async (tmpDir, argsLog) => {
|
|
287
|
-
const { getExistingVariable } = await import("./railway.js");
|
|
288
|
-
|
|
289
|
-
// Mock returns empty JSON array for variable list
|
|
290
|
-
await getExistingVariable("AUTH_SECRET", {
|
|
291
|
-
cwd: tmpDir,
|
|
292
|
-
environment: "staging",
|
|
293
|
-
serviceName: "web",
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
const log = await fs.readFile(argsLog, "utf-8");
|
|
297
|
-
assert.ok(
|
|
298
|
-
log.includes("--environment"),
|
|
299
|
-
`Expected --environment in args: ${log}`,
|
|
300
|
-
);
|
|
301
|
-
assert.ok(log.includes("staging"), `Expected staging in args: ${log}`);
|
|
302
|
-
});
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
test("deleteService forwards --environment flag", async () => {
|
|
306
|
-
await withMockRailway(async (tmpDir, argsLog) => {
|
|
307
|
-
const { deleteService } = await import("./railway.js");
|
|
308
|
-
|
|
309
|
-
await deleteService("web", { cwd: tmpDir, environment: "staging" });
|
|
310
|
-
|
|
311
|
-
const log = await fs.readFile(argsLog, "utf-8");
|
|
312
|
-
assert.ok(
|
|
313
|
-
log.includes("--environment"),
|
|
314
|
-
`Expected --environment in args: ${log}`,
|
|
315
|
-
);
|
|
316
|
-
assert.ok(log.includes("staging"), `Expected staging in args: ${log}`);
|
|
317
|
-
});
|
|
318
|
-
});
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { execSync } from "node:child_process";
|
|
3
|
-
import fs from "node:fs/promises";
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import { after, test } from "node:test";
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
8
|
-
|
|
9
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
const temporaryDirectories = [];
|
|
11
|
-
|
|
12
|
-
after(async () => {
|
|
13
|
-
await Promise.all(
|
|
14
|
-
temporaryDirectories.map((dir) =>
|
|
15
|
-
fs.rm(dir, { recursive: true, force: true, maxRetries: 3 }),
|
|
16
|
-
),
|
|
17
|
-
);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
async function makeTempDir() {
|
|
21
|
-
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "deploy-int-"));
|
|
22
|
-
temporaryDirectories.push(dir);
|
|
23
|
-
return dir;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async function writeJson(filePath, value) {
|
|
27
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
28
|
-
await fs.writeFile(filePath, JSON.stringify(value, null, "\t"));
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Create a fixture that looks like a Quark project with web + worker.
|
|
33
|
-
*/
|
|
34
|
-
async function createQuarkFixture() {
|
|
35
|
-
const projectDir = await makeTempDir();
|
|
36
|
-
|
|
37
|
-
await writeJson(path.join(projectDir, "package.json"), {
|
|
38
|
-
name: "@test/quark-app",
|
|
39
|
-
private: true,
|
|
40
|
-
type: "module",
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
await writeJson(path.join(projectDir, "apps", "web", "package.json"), {
|
|
44
|
-
name: "@test/quark-web",
|
|
45
|
-
private: true,
|
|
46
|
-
type: "module",
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
await writeJson(path.join(projectDir, "apps", "worker", "package.json"), {
|
|
50
|
-
name: "@test/quark-worker",
|
|
51
|
-
private: true,
|
|
52
|
-
type: "module",
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
return projectDir;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function runQuarkCli(args) {
|
|
59
|
-
const cliEntry = path.join(__dirname, "../../src/index.js");
|
|
60
|
-
try {
|
|
61
|
-
const result = execSync(
|
|
62
|
-
`${process.execPath} ${cliEntry} ${args.join(" ")}`,
|
|
63
|
-
{
|
|
64
|
-
encoding: "utf8",
|
|
65
|
-
timeout: 10_000,
|
|
66
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
67
|
-
},
|
|
68
|
-
);
|
|
69
|
-
return result;
|
|
70
|
-
} catch (error) {
|
|
71
|
-
// execSync throws on non-zero exit — return stdout if available
|
|
72
|
-
if (error.stdout) return error.stdout.toString("utf8");
|
|
73
|
-
throw error;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// ---------------------------------------------------------------------------
|
|
78
|
-
// Level 2: Integration - deployToRailway with missing Railway CLI
|
|
79
|
-
// ---------------------------------------------------------------------------
|
|
80
|
-
|
|
81
|
-
test("checkRailwayCLI returns null when railway is not on PATH", async () => {
|
|
82
|
-
const { checkRailwayCLI } = await import("./adapters/railway.js");
|
|
83
|
-
const origPath = process.env.PATH;
|
|
84
|
-
process.env.PATH = "/dev/null";
|
|
85
|
-
try {
|
|
86
|
-
const result = await checkRailwayCLI();
|
|
87
|
-
assert.equal(result, null);
|
|
88
|
-
} finally {
|
|
89
|
-
process.env.PATH = origPath;
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
// ---------------------------------------------------------------------------
|
|
94
|
-
// Level 2: Integration - adapter functions with mocked project
|
|
95
|
-
// ---------------------------------------------------------------------------
|
|
96
|
-
|
|
97
|
-
test("isProjectLinked returns false for unlinked project", async () => {
|
|
98
|
-
const { isProjectLinked } = await import("./adapters/railway.js");
|
|
99
|
-
const projectDir = await createQuarkFixture();
|
|
100
|
-
const result = await isProjectLinked(projectDir);
|
|
101
|
-
assert.equal(result, false);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test("isProjectLinked returns true after creating .railway directory with config", async () => {
|
|
105
|
-
const { isProjectLinked } = await import("./adapters/railway.js");
|
|
106
|
-
const projectDir = await createQuarkFixture();
|
|
107
|
-
const railwayDir = path.join(projectDir, ".railway");
|
|
108
|
-
await fs.mkdir(railwayDir, { recursive: true });
|
|
109
|
-
await fs.writeFile(
|
|
110
|
-
path.join(railwayDir, "config.json"),
|
|
111
|
-
JSON.stringify({ projectId: "test-123" }),
|
|
112
|
-
);
|
|
113
|
-
const result = await isProjectLinked(projectDir);
|
|
114
|
-
assert.equal(result, true);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
// ---------------------------------------------------------------------------
|
|
118
|
-
// Level 2: Integration - resolveQuarkDeployProject with fixtures
|
|
119
|
-
// ---------------------------------------------------------------------------
|
|
120
|
-
|
|
121
|
-
test("resolveQuarkDeployProject discovers web + worker in fixture", async () => {
|
|
122
|
-
const { resolveQuarkDeployProject } = await import("./discovery.js");
|
|
123
|
-
const projectDir = await createQuarkFixture();
|
|
124
|
-
const discovery = await resolveQuarkDeployProject(projectDir);
|
|
125
|
-
|
|
126
|
-
assert.equal(discovery.kind, "quark");
|
|
127
|
-
assert.equal(discovery.services.length, 2);
|
|
128
|
-
assert.equal(discovery.services[0].kind, "web");
|
|
129
|
-
assert.equal(discovery.services[1].kind, "worker");
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test("resolveQuarkDeployProject fails when web is missing", async () => {
|
|
133
|
-
const { resolveQuarkDeployProject } = await import("./discovery.js");
|
|
134
|
-
const projectDir = await makeTempDir();
|
|
135
|
-
|
|
136
|
-
await writeJson(path.join(projectDir, "package.json"), {
|
|
137
|
-
name: "@test/quark-app",
|
|
138
|
-
private: true,
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
await assert.rejects(() => resolveQuarkDeployProject(projectDir), {
|
|
142
|
-
code: "QUARK_DEPLOY_DISCOVERY_FAILED",
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
// ---------------------------------------------------------------------------
|
|
147
|
-
// Level 2: Integration - inspectProject with fixture
|
|
148
|
-
// ---------------------------------------------------------------------------
|
|
149
|
-
|
|
150
|
-
test("inspectProject loads and inspects fixture without error", async () => {
|
|
151
|
-
const { inspectProject } = await import("./inspect.js");
|
|
152
|
-
const projectDir = await createQuarkFixture();
|
|
153
|
-
|
|
154
|
-
// Suppress stdout — inspectProject prints a report via console.log, and
|
|
155
|
-
// unstructured text on the test runner's stdout pipe can corrupt the
|
|
156
|
-
// binary v8-serialized result frames (nodejs/node#64061), flaking CI.
|
|
157
|
-
await inspectProject({ cwd: projectDir, output: () => {} });
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
// ---------------------------------------------------------------------------
|
|
161
|
-
// Level 2: CLI command routing
|
|
162
|
-
// ---------------------------------------------------------------------------
|
|
163
|
-
|
|
164
|
-
test("quark deploy --help shows subcommands", () => {
|
|
165
|
-
const stdout = runQuarkCli(["deploy", "--help"]);
|
|
166
|
-
|
|
167
|
-
assert.match(stdout, /railway/);
|
|
168
|
-
assert.match(stdout, /inspect/);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
test("quark deploy railway --help shows options", () => {
|
|
172
|
-
const stdout = runQuarkCli(["deploy", "railway", "--help"]);
|
|
173
|
-
|
|
174
|
-
assert.match(stdout, /project-name/);
|
|
175
|
-
assert.match(stdout, /project-id/);
|
|
176
|
-
assert.match(stdout, /dry-run/);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
test("quark deploy inspect --help shows help", () => {
|
|
180
|
-
const stdout = runQuarkCli(["deploy", "inspect", "--help"]);
|
|
181
|
-
|
|
182
|
-
assert.ok(stdout.length > 0);
|
|
183
|
-
});
|