@quenty/nevermore-cli 4.28.1 → 4.29.0-canary.697.26191410126.0
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/CHANGELOG.md +20 -0
- package/dist/commands/batch-command/batch-deploy-command.js +9 -2
- package/dist/commands/batch-command/batch-deploy-command.js.map +1 -1
- package/dist/commands/batch-command/batch-test-command.d.ts +1 -0
- package/dist/commands/batch-command/batch-test-command.d.ts.map +1 -1
- package/dist/commands/batch-command/batch-test-command.js +14 -4
- package/dist/commands/batch-command/batch-test-command.js.map +1 -1
- package/dist/commands/deploy-command/index.d.ts.map +1 -1
- package/dist/commands/deploy-command/index.js +20 -20
- package/dist/commands/deploy-command/index.js.map +1 -1
- package/dist/commands/deploy-command/select-target.d.ts +10 -0
- package/dist/commands/deploy-command/select-target.d.ts.map +1 -0
- package/dist/commands/deploy-command/select-target.js +53 -0
- package/dist/commands/deploy-command/select-target.js.map +1 -0
- package/dist/commands/test-command/test-command.d.ts +1 -0
- package/dist/commands/test-command/test-command.d.ts.map +1 -1
- package/dist/commands/test-command/test-command.js +42 -27
- package/dist/commands/test-command/test-command.js.map +1 -1
- package/dist/utils/batch/batch-runner.js +1 -1
- package/dist/utils/batch/batch-runner.js.map +1 -1
- package/dist/utils/build/deploy-config.d.ts +4 -3
- package/dist/utils/build/deploy-config.d.ts.map +1 -1
- package/dist/utils/build/deploy-config.js +7 -3
- package/dist/utils/build/deploy-config.js.map +1 -1
- package/dist/utils/build/deploy-config.test.d.ts +2 -0
- package/dist/utils/build/deploy-config.test.d.ts.map +1 -0
- package/dist/utils/build/deploy-config.test.js +41 -0
- package/dist/utils/build/deploy-config.test.js.map +1 -0
- package/dist/utils/job-context/cloud-job-context.d.ts.map +1 -1
- package/dist/utils/job-context/cloud-job-context.js +5 -2
- package/dist/utils/job-context/cloud-job-context.js.map +1 -1
- package/dist/utils/open-cloud/open-cloud-client.d.ts +2 -1
- package/dist/utils/open-cloud/open-cloud-client.d.ts.map +1 -1
- package/dist/utils/open-cloud/open-cloud-client.js +39 -14
- package/dist/utils/open-cloud/open-cloud-client.js.map +1 -1
- package/package.json +6 -6
- package/src/commands/batch-command/batch-deploy-command.ts +8 -4
- package/src/commands/batch-command/batch-test-command.ts +15 -5
- package/src/commands/deploy-command/index.ts +21 -24
- package/src/commands/deploy-command/select-target.ts +80 -0
- package/src/commands/test-command/test-command.ts +48 -33
- package/src/utils/batch/batch-runner.ts +1 -1
- package/src/utils/build/deploy-config.test.ts +51 -0
- package/src/utils/build/deploy-config.ts +7 -3
- package/src/utils/job-context/cloud-job-context.ts +10 -2
- package/src/utils/open-cloud/open-cloud-client.ts +54 -14
- package/templates/game-template/.github/workflows/{deploy.yml → integration.yml} +30 -26
- package/templates/game-template/.github/workflows/tests.yml +30 -20
- package/templates/game-template/README.md +22 -1
- package/templates/game-template/aftman.toml +1 -0
- package/templates/game-template/default.project.json +3 -0
- package/templates/game-template/package.json +7 -2
- package/templates/game-template/src/modules/jest.config.lua +4 -0
- package/templates/game-template/src/scripts/Server/ServerMain.server.lua +6 -0
- package/templates/plugin-template/.github/workflows/{deploy.yml → integration.yml} +30 -26
- package/templates/plugin-template/.github/workflows/tests.yml +18 -13
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -29,6 +29,7 @@ export interface TestProjectArgs extends NevermoreGlobalArgs {
|
|
|
29
29
|
scriptTemplate?: string;
|
|
30
30
|
scriptText?: string;
|
|
31
31
|
output?: string;
|
|
32
|
+
timeout?: number;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export class TestProjectCommand<T>
|
|
@@ -74,42 +75,48 @@ export class TestProjectCommand<T>
|
|
|
74
75
|
describe: 'Write JSON results to this file',
|
|
75
76
|
type: 'string',
|
|
76
77
|
});
|
|
78
|
+
args.option('timeout', {
|
|
79
|
+
describe:
|
|
80
|
+
'Max script execution time in seconds. Sent to the Open Cloud API so Roblox cancels server-side on overrun (default: 120)',
|
|
81
|
+
type: 'number',
|
|
82
|
+
});
|
|
77
83
|
|
|
78
84
|
return args as Argv<TestProjectArgs>;
|
|
79
85
|
};
|
|
80
86
|
|
|
81
87
|
public handler = async (args: TestProjectArgs) => {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const showLogs = args.logs ?? false;
|
|
87
|
-
const useSpinner = process.stdout.isTTY && !args.verbose;
|
|
88
|
+
const cwd = process.cwd();
|
|
89
|
+
const packageName = (await readPackageNameAsync(cwd)) ?? path.basename(cwd);
|
|
90
|
+
const showLogs = args.logs ?? false;
|
|
91
|
+
const useSpinner = process.stdout.isTTY && !args.verbose;
|
|
88
92
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return reporters;
|
|
93
|
+
const reporter = new CompositeReporter(
|
|
94
|
+
[packageName],
|
|
95
|
+
(state: LiveStateTracker) => {
|
|
96
|
+
const reporters: Reporter[] = [
|
|
97
|
+
useSpinner
|
|
98
|
+
? new SpinnerReporter(state, {
|
|
99
|
+
showLogs,
|
|
100
|
+
actionVerb: 'Testing',
|
|
101
|
+
})
|
|
102
|
+
: new SimpleReporter(state, {
|
|
103
|
+
alwaysShowLogs: showLogs,
|
|
104
|
+
verbose: args.verbose,
|
|
105
|
+
successMessage: 'Tests passed!',
|
|
106
|
+
failureMessage:
|
|
107
|
+
'Tests failed! See output above for more information.',
|
|
108
|
+
}),
|
|
109
|
+
];
|
|
110
|
+
if (args.output) {
|
|
111
|
+
reporters.push(new JsonFileReporter(state, args.output));
|
|
109
112
|
}
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
return reporters;
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
await reporter.startAsync();
|
|
112
117
|
|
|
118
|
+
let exitCode = 0;
|
|
119
|
+
try {
|
|
113
120
|
const context = args.cloud
|
|
114
121
|
? new CloudJobContext(
|
|
115
122
|
reporter,
|
|
@@ -126,6 +133,8 @@ export class TestProjectCommand<T>
|
|
|
126
133
|
packagePath: cwd,
|
|
127
134
|
packageName,
|
|
128
135
|
scriptText: args.scriptText,
|
|
136
|
+
timeoutMs:
|
|
137
|
+
args.timeout !== undefined ? args.timeout * 1000 : undefined,
|
|
129
138
|
});
|
|
130
139
|
} finally {
|
|
131
140
|
await context.disposeAsync();
|
|
@@ -140,13 +149,19 @@ export class TestProjectCommand<T>
|
|
|
140
149
|
? { kind: 'test-counts', ...result.testCounts }
|
|
141
150
|
: undefined,
|
|
142
151
|
});
|
|
143
|
-
|
|
144
|
-
await reporter.stopAsync();
|
|
152
|
+
if (!result.success) exitCode = 1;
|
|
145
153
|
} catch (err) {
|
|
146
|
-
|
|
147
|
-
|
|
154
|
+
reporter.onPackageResult({
|
|
155
|
+
packageName,
|
|
156
|
+
success: false,
|
|
157
|
+
logs: '',
|
|
158
|
+
durationMs: 0,
|
|
159
|
+
error: OutputHelper.formatErrorChain(err),
|
|
160
|
+
});
|
|
161
|
+
exitCode = 1;
|
|
148
162
|
}
|
|
149
163
|
|
|
150
|
-
|
|
164
|
+
await reporter.stopAsync();
|
|
165
|
+
process.exit(exitCode);
|
|
151
166
|
};
|
|
152
167
|
}
|
|
@@ -112,7 +112,7 @@ async function _runOneAsync<TResult extends PackageResult>(
|
|
|
112
112
|
const durationMs = partial.durationMs ?? Date.now() - startMs;
|
|
113
113
|
return { ...partial, durationMs } as TResult;
|
|
114
114
|
} catch (err) {
|
|
115
|
-
const errorMessage =
|
|
115
|
+
const errorMessage = OutputHelper.formatErrorChain(err);
|
|
116
116
|
const currentPhase = stateTracker?.getCurrentPhase(pkg.name);
|
|
117
117
|
const failedPhase =
|
|
118
118
|
currentPhase &&
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
type DeployConfig,
|
|
4
|
+
type DeployTarget,
|
|
5
|
+
resolveDefaultTargetName,
|
|
6
|
+
} from './deploy-config.js';
|
|
7
|
+
|
|
8
|
+
function makeTarget(placeId: number): DeployTarget {
|
|
9
|
+
return {
|
|
10
|
+
universeId: 1,
|
|
11
|
+
placeId,
|
|
12
|
+
project: 'default.project.json',
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function makeConfig(targets: Record<string, DeployTarget>): DeployConfig {
|
|
17
|
+
return { targets };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('resolveDefaultTargetName', () => {
|
|
21
|
+
it('returns the only target when there is just one', () => {
|
|
22
|
+
const config = makeConfig({ prod: makeTarget(1) });
|
|
23
|
+
expect(resolveDefaultTargetName(config)).toBe('prod');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('prefers integration over test when both exist', () => {
|
|
27
|
+
const config = makeConfig({
|
|
28
|
+
test: makeTarget(1),
|
|
29
|
+
integration: makeTarget(2),
|
|
30
|
+
});
|
|
31
|
+
expect(resolveDefaultTargetName(config)).toBe('integration');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('falls back to test when integration is absent', () => {
|
|
35
|
+
const config = makeConfig({ test: makeTarget(1), dev: makeTarget(2) });
|
|
36
|
+
expect(resolveDefaultTargetName(config)).toBe('test');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('returns integration when present alongside non-test targets', () => {
|
|
40
|
+
const config = makeConfig({
|
|
41
|
+
integration: makeTarget(1),
|
|
42
|
+
dev: makeTarget(2),
|
|
43
|
+
});
|
|
44
|
+
expect(resolveDefaultTargetName(config)).toBe('integration');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('throws when no preferred target exists and multiple options are present', () => {
|
|
48
|
+
const config = makeConfig({ foo: makeTarget(1), bar: makeTarget(2) });
|
|
49
|
+
expect(() => resolveDefaultTargetName(config)).toThrowError(/foo, bar/);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -88,9 +88,10 @@ export function resolveDeployTarget(
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
|
-
* Pick a target name when the user did not specify one
|
|
92
|
-
*
|
|
93
|
-
*
|
|
91
|
+
* Pick a target name when the user did not specify one and we cannot prompt
|
|
92
|
+
* (non-TTY / CI). Single target wins; otherwise prefer "integration" over
|
|
93
|
+
* "test" so `--publish` does not silently target the test place. Throws when
|
|
94
|
+
* neither is present.
|
|
94
95
|
*/
|
|
95
96
|
export function resolveDefaultTargetName(config: DeployConfig): string {
|
|
96
97
|
const availableTargets = Object.keys(config.targets);
|
|
@@ -98,6 +99,9 @@ export function resolveDefaultTargetName(config: DeployConfig): string {
|
|
|
98
99
|
if (availableTargets.length === 1) {
|
|
99
100
|
return availableTargets[0]!;
|
|
100
101
|
}
|
|
102
|
+
if (config.targets['integration']) {
|
|
103
|
+
return 'integration';
|
|
104
|
+
}
|
|
101
105
|
if (config.targets['test']) {
|
|
102
106
|
return 'test';
|
|
103
107
|
}
|
|
@@ -80,9 +80,14 @@ export class CloudJobContext extends BaseJobContext {
|
|
|
80
80
|
cloudDeployment.universeId,
|
|
81
81
|
cloudDeployment.placeId,
|
|
82
82
|
cloudDeployment.version,
|
|
83
|
-
scriptContent
|
|
83
|
+
scriptContent,
|
|
84
|
+
timeoutMs
|
|
84
85
|
);
|
|
85
86
|
|
|
87
|
+
// Give the server-side timeout a head start so we observe the cancelled
|
|
88
|
+
// task state instead of bailing out on the client first.
|
|
89
|
+
const CLIENT_TIMEOUT_GRACE_MS = 30_000;
|
|
90
|
+
|
|
86
91
|
let pollCount = 0;
|
|
87
92
|
const completedTask = await Promise.race([
|
|
88
93
|
this._openCloudClient!.pollTaskCompletionAsync(task.path, (state) => {
|
|
@@ -98,7 +103,10 @@ export class CloudJobContext extends BaseJobContext {
|
|
|
98
103
|
label,
|
|
99
104
|
});
|
|
100
105
|
}),
|
|
101
|
-
timeoutAsync(
|
|
106
|
+
timeoutAsync(
|
|
107
|
+
timeoutMs + CLIENT_TIMEOUT_GRACE_MS,
|
|
108
|
+
`Test timed out after ${timeoutMs / 1000}s`
|
|
109
|
+
),
|
|
102
110
|
]);
|
|
103
111
|
|
|
104
112
|
cloudDeployment.taskPath = task.path;
|
|
@@ -19,6 +19,7 @@ export interface LuauTask {
|
|
|
19
19
|
| 'COMPLETE'
|
|
20
20
|
| 'FAILED';
|
|
21
21
|
script: string;
|
|
22
|
+
timeout?: string;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export interface OpenCloudClientOptions {
|
|
@@ -209,18 +210,30 @@ export class OpenCloudClient {
|
|
|
209
210
|
universeId: number,
|
|
210
211
|
placeId: number,
|
|
211
212
|
placeVersion: number,
|
|
212
|
-
script: string
|
|
213
|
+
script: string,
|
|
214
|
+
timeoutMs?: number
|
|
213
215
|
): Promise<LuauTask> {
|
|
214
216
|
const apiKey = await this._resolveApiKeyAsync();
|
|
215
217
|
const url = `https://apis.roblox.com/cloud/v2/universes/${universeId}/places/${placeId}/versions/${placeVersion}/luau-execution-session-tasks`;
|
|
216
218
|
|
|
219
|
+
const body: {
|
|
220
|
+
script: string;
|
|
221
|
+
timeout?: string;
|
|
222
|
+
enableBinaryOutput?: boolean;
|
|
223
|
+
} = { script, enableBinaryOutput: false };
|
|
224
|
+
if (timeoutMs !== undefined) {
|
|
225
|
+
// Roblox encodes durations as Google AIP duration strings (e.g. "120s").
|
|
226
|
+
// The server uses this to cancel runaway scripts on its end.
|
|
227
|
+
body.timeout = `${Math.max(1, Math.ceil(timeoutMs / 1000))}s`;
|
|
228
|
+
}
|
|
229
|
+
|
|
217
230
|
const response = await this._rateLimiter.fetchAsync(url, {
|
|
218
231
|
method: 'POST',
|
|
219
232
|
headers: {
|
|
220
233
|
'Content-Type': 'application/json',
|
|
221
234
|
'X-API-Key': apiKey,
|
|
222
235
|
},
|
|
223
|
-
body: JSON.stringify(
|
|
236
|
+
body: JSON.stringify(body),
|
|
224
237
|
});
|
|
225
238
|
|
|
226
239
|
if (!response.ok) {
|
|
@@ -313,26 +326,53 @@ export class OpenCloudClient {
|
|
|
313
326
|
|
|
314
327
|
private async _fetchRawLogsAsync(taskPath: string): Promise<string> {
|
|
315
328
|
const apiKey = await this._resolveApiKeyAsync();
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
329
|
+
const messages: string[] = [];
|
|
330
|
+
let pageToken: string | undefined;
|
|
331
|
+
|
|
332
|
+
do {
|
|
333
|
+
const url = new URL(`https://apis.roblox.com/cloud/v2/${taskPath}/logs`);
|
|
334
|
+
url.searchParams.set('view', 'STRUCTURED');
|
|
335
|
+
if (pageToken) {
|
|
336
|
+
url.searchParams.set('pageToken', pageToken);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const response = await this._rateLimiter.fetchAsync(url.toString(), {
|
|
319
340
|
method: 'GET',
|
|
320
341
|
headers: {
|
|
321
342
|
'X-API-Key': apiKey,
|
|
322
343
|
},
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
if (!response.ok) {
|
|
347
|
+
const text = await response.text();
|
|
348
|
+
throw new Error(`Get logs failed: ${response.status}: ${text}`);
|
|
323
349
|
}
|
|
324
|
-
);
|
|
325
350
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
351
|
+
const data = (await response.json()) as {
|
|
352
|
+
luauExecutionSessionTaskLogs?: Array<{
|
|
353
|
+
messages?: string[];
|
|
354
|
+
structuredMessages?: Array<{
|
|
355
|
+
message: string;
|
|
356
|
+
createTime: string;
|
|
357
|
+
messageType: string;
|
|
358
|
+
}>;
|
|
359
|
+
}>;
|
|
360
|
+
nextPageToken?: string;
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
for (const entry of data.luauExecutionSessionTaskLogs ?? []) {
|
|
364
|
+
if (entry.structuredMessages?.length) {
|
|
365
|
+
for (const msg of entry.structuredMessages) {
|
|
366
|
+
messages.push(msg.message);
|
|
367
|
+
}
|
|
368
|
+
} else if (entry.messages?.length) {
|
|
369
|
+
messages.push(...entry.messages);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
330
372
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
};
|
|
373
|
+
pageToken = data.nextPageToken || undefined;
|
|
374
|
+
} while (pageToken);
|
|
334
375
|
|
|
335
|
-
const messages = data.luauExecutionSessionTaskLogs?.[0]?.messages ?? [];
|
|
336
376
|
return messages.join('\n');
|
|
337
377
|
}
|
|
338
378
|
|
|
@@ -1,45 +1,44 @@
|
|
|
1
|
-
# Roblox Deploy
|
|
1
|
+
# Roblox Integration Deploy
|
|
2
2
|
#
|
|
3
|
-
# Builds and uploads the project to your Roblox place.
|
|
3
|
+
# Builds and uploads the project to your Roblox integration place on every pull request.
|
|
4
4
|
# This workflow is inactive until you complete the setup below.
|
|
5
5
|
#
|
|
6
6
|
# Setup:
|
|
7
|
-
# 1. Run `nevermore deploy init` to create deploy.nevermore.json
|
|
7
|
+
# 1. Run `nevermore deploy init` to create deploy.nevermore.json (with an `integration` target)
|
|
8
8
|
# 2. Add ROBLOX_OPEN_CLOUD_API_KEY as a repository secret
|
|
9
9
|
# (Create one at https://create.roblox.com/dashboard/credentials)
|
|
10
|
-
# 3. Deploys will run automatically
|
|
11
|
-
# 4. You can also trigger a deploy manually from the Actions tab
|
|
10
|
+
# 3. Deploys will run automatically on every pull request
|
|
12
11
|
#
|
|
13
12
|
# Docs: https://quenty.github.io/NevermoreEngine/docs/testing/integration-testing
|
|
14
13
|
|
|
15
|
-
name:
|
|
16
|
-
on:
|
|
17
|
-
push:
|
|
18
|
-
branches:
|
|
19
|
-
- main
|
|
20
|
-
workflow_dispatch:
|
|
14
|
+
name: integration
|
|
15
|
+
on: [pull_request]
|
|
21
16
|
jobs:
|
|
22
|
-
|
|
17
|
+
integration:
|
|
23
18
|
runs-on: ubuntu-latest
|
|
24
|
-
|
|
25
|
-
enabled: $\{{ steps.check.outputs.enabled }}
|
|
19
|
+
environment: integration
|
|
26
20
|
steps:
|
|
27
21
|
- id: check
|
|
22
|
+
name: Check config
|
|
28
23
|
run: |
|
|
29
|
-
if [ -n "$KEY" ]; then
|
|
30
|
-
|
|
24
|
+
if [ -n "$KEY" ]; then
|
|
25
|
+
echo "enabled=true" >> "$GITHUB_OUTPUT"
|
|
26
|
+
else
|
|
27
|
+
echo "::notice::Integration skipped — ROBLOX_OPEN_CLOUD_API_KEY not configured. See top of this file for setup."
|
|
28
|
+
fi
|
|
31
29
|
env:
|
|
32
30
|
KEY: $\{{ secrets.ROBLOX_OPEN_CLOUD_API_KEY }}
|
|
33
31
|
|
|
34
|
-
deploy:
|
|
35
|
-
needs: config-check
|
|
36
|
-
if: needs.config-check.outputs.enabled == 'true'
|
|
37
|
-
runs-on: ubuntu-latest
|
|
38
|
-
steps:
|
|
39
32
|
- name: Checkout repository
|
|
33
|
+
if: steps.check.outputs.enabled == 'true'
|
|
40
34
|
uses: actions/checkout@v6
|
|
41
35
|
|
|
36
|
+
- name: Fetch base branch for changeset diff
|
|
37
|
+
if: steps.check.outputs.enabled == 'true'
|
|
38
|
+
run: git fetch origin $\{{ github.base_ref }} --depth=1
|
|
39
|
+
|
|
42
40
|
- name: Setup Aftman
|
|
41
|
+
if: steps.check.outputs.enabled == 'true'
|
|
43
42
|
uses: ok-nick/setup-aftman@v0.4.2
|
|
44
43
|
with:
|
|
45
44
|
version: 'v0.3.0'
|
|
@@ -47,11 +46,13 @@ jobs:
|
|
|
47
46
|
cache: true
|
|
48
47
|
|
|
49
48
|
- name: Setup pnpm
|
|
49
|
+
if: steps.check.outputs.enabled == 'true'
|
|
50
50
|
uses: pnpm/action-setup@v4
|
|
51
51
|
with:
|
|
52
52
|
cache: true
|
|
53
53
|
|
|
54
54
|
- name: Setup npm for GitHub Packages
|
|
55
|
+
if: steps.check.outputs.enabled == 'true'
|
|
55
56
|
run: |
|
|
56
57
|
if [[ -n "$NPM_GITHUB_QUENTYSTUDIOS_TOKEN" ]]; then
|
|
57
58
|
echo "//npm.pkg.github.com/:_authToken=$NPM_GITHUB_QUENTYSTUDIOS_TOKEN" >> .npmrc
|
|
@@ -67,16 +68,19 @@ jobs:
|
|
|
67
68
|
NPM_TOKEN: $\{{ secrets.NPM_TOKEN }}
|
|
68
69
|
|
|
69
70
|
- name: Link and install packages
|
|
71
|
+
if: steps.check.outputs.enabled == 'true'
|
|
70
72
|
run: pnpm install --frozen-lockfile
|
|
71
73
|
|
|
72
|
-
- name: Deploy
|
|
73
|
-
id: deploy
|
|
74
|
-
|
|
74
|
+
- name: Deploy integration
|
|
75
|
+
id: deploy-integration
|
|
76
|
+
if: steps.check.outputs.enabled == 'true'
|
|
77
|
+
run: npx @quenty/nevermore-cli batch deploy --target integration --publish --base origin/$\{{ github.base_ref }} --output integration-results.json --yes --verbose --logs
|
|
75
78
|
env:
|
|
76
79
|
ROBLOX_OPEN_CLOUD_API_KEY: $\{{ secrets.ROBLOX_OPEN_CLOUD_API_KEY }}
|
|
80
|
+
GITHUB_TOKEN: $\{{ secrets.GITHUB_TOKEN }}
|
|
77
81
|
|
|
78
82
|
- name: Post deploy results
|
|
79
|
-
if: always()
|
|
80
|
-
run: npx @quenty/nevermore-cli tools post-deploy-results
|
|
83
|
+
if: always() && steps.check.outputs.enabled == 'true'
|
|
84
|
+
run: npx @quenty/nevermore-cli tools post-deploy-results integration-results.json --yes --run-outcome $\{{ steps.deploy-integration.outcome }}
|
|
81
85
|
env:
|
|
82
86
|
GITHUB_TOKEN: $\{{ secrets.GITHUB_TOKEN }}
|
|
@@ -7,47 +7,46 @@
|
|
|
7
7
|
# 1. Run `nevermore deploy init` to create deploy.nevermore.json with a test target
|
|
8
8
|
# 2. Add ROBLOX_OPEN_CLOUD_API_KEY as a repository secret
|
|
9
9
|
# (Create one at https://create.roblox.com/dashboard/credentials)
|
|
10
|
-
# 3. Tests will run automatically on pull requests
|
|
10
|
+
# 3. Tests will run automatically on pull requests and on push to main
|
|
11
11
|
#
|
|
12
12
|
# Docs: https://quenty.github.io/NevermoreEngine/docs/testing
|
|
13
13
|
|
|
14
14
|
name: tests
|
|
15
15
|
on:
|
|
16
16
|
pull_request:
|
|
17
|
+
push:
|
|
18
|
+
branches: [main]
|
|
17
19
|
jobs:
|
|
18
|
-
|
|
20
|
+
tests:
|
|
19
21
|
runs-on: ubuntu-latest
|
|
20
|
-
outputs:
|
|
21
|
-
enabled: $\{{ steps.check.outputs.enabled }}
|
|
22
22
|
steps:
|
|
23
23
|
- id: check
|
|
24
|
+
name: Check config
|
|
24
25
|
run: |
|
|
25
|
-
if [ -n "$KEY" ]; then
|
|
26
|
-
|
|
26
|
+
if [ -n "$KEY" ]; then
|
|
27
|
+
echo "enabled=true" >> "$GITHUB_OUTPUT"
|
|
28
|
+
else
|
|
29
|
+
echo "::notice::Tests skipped — ROBLOX_OPEN_CLOUD_API_KEY not configured. See top of this file for setup."
|
|
30
|
+
fi
|
|
27
31
|
env:
|
|
28
32
|
KEY: $\{{ secrets.ROBLOX_OPEN_CLOUD_API_KEY }}
|
|
29
33
|
|
|
30
|
-
tests:
|
|
31
|
-
needs: config-check
|
|
32
|
-
if: needs.config-check.outputs.enabled == 'true'
|
|
33
|
-
runs-on: ubuntu-latest
|
|
34
|
-
steps:
|
|
35
34
|
- name: Checkout repository
|
|
35
|
+
if: steps.check.outputs.enabled == 'true'
|
|
36
36
|
uses: actions/checkout@v6
|
|
37
37
|
|
|
38
|
-
- name:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
version: 'v0.3.0'
|
|
42
|
-
token: $\{{ secrets.GITHUB_TOKEN }}
|
|
43
|
-
cache: true
|
|
38
|
+
- name: Fetch base branch for changeset diff
|
|
39
|
+
if: steps.check.outputs.enabled == 'true' && github.event_name == 'pull_request'
|
|
40
|
+
run: git fetch origin $\{{ github.base_ref }} --depth=1
|
|
44
41
|
|
|
45
42
|
- name: Setup pnpm
|
|
43
|
+
if: steps.check.outputs.enabled == 'true'
|
|
46
44
|
uses: pnpm/action-setup@v4
|
|
47
45
|
with:
|
|
48
46
|
cache: true
|
|
49
47
|
|
|
50
48
|
- name: Setup npm for GitHub Packages
|
|
49
|
+
if: steps.check.outputs.enabled == 'true'
|
|
51
50
|
run: |
|
|
52
51
|
if [[ -n "$NPM_GITHUB_QUENTYSTUDIOS_TOKEN" ]]; then
|
|
53
52
|
echo "//npm.pkg.github.com/:_authToken=$NPM_GITHUB_QUENTYSTUDIOS_TOKEN" >> .npmrc
|
|
@@ -63,20 +62,31 @@ jobs:
|
|
|
63
62
|
NPM_TOKEN: $\{{ secrets.NPM_TOKEN }}
|
|
64
63
|
|
|
65
64
|
- name: Link and install packages
|
|
65
|
+
if: steps.check.outputs.enabled == 'true'
|
|
66
66
|
run: pnpm install --frozen-lockfile
|
|
67
67
|
|
|
68
|
+
- name: Setup Aftman
|
|
69
|
+
if: steps.check.outputs.enabled == 'true'
|
|
70
|
+
uses: ok-nick/setup-aftman@v0.4.2
|
|
71
|
+
with:
|
|
72
|
+
version: 'v0.3.0'
|
|
73
|
+
token: $\{{ secrets.GITHUB_TOKEN }}
|
|
74
|
+
cache: true
|
|
75
|
+
|
|
68
76
|
- name: Run tests
|
|
69
77
|
id: run-tests
|
|
70
|
-
|
|
78
|
+
if: steps.check.outputs.enabled == 'true'
|
|
79
|
+
run: npx @quenty/nevermore-cli batch test --cloud --aggregated $\{{ github.event_name == 'pull_request' && format('--base origin/{0}', github.base_ref) || '' }} --output test-results.json --yes --verbose --logs
|
|
71
80
|
env:
|
|
72
81
|
ROBLOX_OPEN_CLOUD_API_KEY: $\{{ secrets.ROBLOX_OPEN_CLOUD_API_KEY }}
|
|
82
|
+
GITHUB_TOKEN: $\{{ secrets.GITHUB_TOKEN }}
|
|
73
83
|
|
|
74
84
|
- name: Build sourcemap for annotation resolution
|
|
75
|
-
if: always()
|
|
85
|
+
if: always() && steps.check.outputs.enabled == 'true'
|
|
76
86
|
run: npm run build:sourcemap
|
|
77
87
|
|
|
78
88
|
- name: Post test results
|
|
79
|
-
if: always()
|
|
89
|
+
if: always() && steps.check.outputs.enabled == 'true'
|
|
80
90
|
run: npx @quenty/nevermore-cli tools post-test-results test-results.json --yes --run-outcome $\{{ steps.run-tests.outcome }}
|
|
81
91
|
env:
|
|
82
92
|
GITHUB_TOKEN: $\{{ secrets.GITHUB_TOKEN }}
|
|
@@ -56,15 +56,36 @@ npm run format
|
|
|
56
56
|
|
|
57
57
|
Note that you should also configure your editor to automatically format files using Stylua.
|
|
58
58
|
|
|
59
|
+
# Tests
|
|
60
|
+
|
|
61
|
+
Tests live next to the code they cover and end in `.spec.lua`. They run on Roblox via [`@quenty/nevermore-test-runner`](https://www.npmjs.com/package/@quenty/nevermore-test-runner) using [jest-lua](https://github.com/jsdotlua/jest-lua).
|
|
62
|
+
|
|
63
|
+
Run tests locally against the cloud:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm test
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The matcher and runner config live in [`src/modules/jest.config.lua`](src/modules/jest.config.lua).
|
|
70
|
+
|
|
71
|
+
# Deploying
|
|
72
|
+
|
|
73
|
+
Deploy targets are defined in `deploy.nevermore.json`. Run `npx @quenty/nevermore-cli deploy init` to scaffold one. To push an integration build manually:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npm run deploy
|
|
77
|
+
```
|
|
78
|
+
|
|
59
79
|
# CI/CD
|
|
60
80
|
|
|
61
81
|
This project includes GitHub Actions workflows in `.github/workflows/`:
|
|
62
82
|
|
|
63
83
|
- **Linting** (`linting.yml`) — Runs automatically on PRs and push to main. Posts inline annotations on PRs for luau-lsp, stylua, selene, and moonwave issues.
|
|
64
84
|
- **Tests** (`tests.yml`) — Runs on PRs when configured. Set up with `nevermore deploy init` to create a `deploy.nevermore.json`, then add `ROBLOX_OPEN_CLOUD_API_KEY` as a repository secret.
|
|
85
|
+
- **Integration** (`integration.yml`) — Deploys PRs to an `integration` target so reviewers can play the change. Same setup as tests; optionally create a GitHub Environment named `integration` to scope the secret.
|
|
65
86
|
- **Deploy** (`deploy.yml`) — Runs on push to main when configured. Same setup as tests: `nevermore deploy init` + `ROBLOX_OPEN_CLOUD_API_KEY` secret.
|
|
66
87
|
|
|
67
|
-
Tests and deploy workflows are inactive until configured — they skip cleanly with a notice annotation explaining the setup steps.
|
|
88
|
+
Tests, integration, and deploy workflows are inactive until configured — they skip cleanly with a notice annotation explaining the setup steps.
|
|
68
89
|
|
|
69
90
|
# Getting Luau-lsp to work in VSCode
|
|
70
91
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# For more information, see https://github.com/LPGhatguy/aftman
|
|
3
3
|
[tools]
|
|
4
4
|
luau-lsp = "Quenty/luau-lsp@1.58.0-quenty.1"
|
|
5
|
+
lune = "lune-org/lune@0.10.4"
|
|
5
6
|
moonwave-extractor = "UpliftGames/moonwave@1.3.0"
|
|
6
7
|
rojo = "quenty/rojo@7.7.0-rc.1-quenty.4"
|
|
7
8
|
selene = "Kampfkarren/selene@0.29.0"
|
|
@@ -12,15 +12,20 @@
|
|
|
12
12
|
"scripts": {
|
|
13
13
|
"build:sourcemap": "rojo sourcemap default.project.json --output sourcemap.json --absolute",
|
|
14
14
|
"format": "stylua --config-path=stylua.toml src",
|
|
15
|
-
"lint:luau": "luau-lsp analyze --sourcemap=sourcemap.json --base-luaurc=.luaurc --defs=globalTypes.d.lua --flag:LuauSolverV2=false --ignore=**/node_modules/** --ignore=**/*.story.lua --ignore=**/*.client.lua --ignore=**/*.server.lua src",
|
|
15
|
+
"lint:luau": "luau-lsp analyze --sourcemap=sourcemap.json --base-luaurc=.luaurc --defs=globalTypes.d.lua --flag:LuauSolverV2=false --ignore=**/node_modules/** --ignore=**/*.spec.lua --ignore=**/*.story.lua --ignore=**/*.client.lua --ignore=**/*.server.lua src",
|
|
16
16
|
"lint:moonwave": "moonwave-extractor extract src",
|
|
17
17
|
"lint:selene": "selene --no-summary --config=selene.toml src",
|
|
18
18
|
"lint:stylua": "stylua --config-path=stylua.toml --check src",
|
|
19
19
|
"prelint:selene": "selene generate-roblox-std",
|
|
20
20
|
"prelint:luau": "npm run build:sourcemap && npx @quenty/nevermore-cli tools download-roblox-types",
|
|
21
|
+
"test": "npx @quenty/nevermore-cli test",
|
|
22
|
+
"deploy": "npx @quenty/nevermore-cli deploy --target integration",
|
|
21
23
|
"preinstall": "npx only-allow pnpm"
|
|
22
24
|
},
|
|
23
|
-
"dependencies": {
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@quenty/nevermore-test-runner": "^1.4.0",
|
|
27
|
+
"@quentystudios/jest-lua": "3.10.0-quenty.2"
|
|
28
|
+
},
|
|
24
29
|
"publishConfig": {
|
|
25
30
|
"access": "restricted"
|
|
26
31
|
}
|
|
@@ -8,6 +8,12 @@ local root = ServerScriptService.{{gameNameProper}}
|
|
|
8
8
|
local loader = root:FindFirstChild("LoaderUtils", true).Parent
|
|
9
9
|
local require = require(loader).bootstrapGame(root)
|
|
10
10
|
|
|
11
|
+
local NevermoreTestRunnerUtils = require("NevermoreTestRunnerUtils")
|
|
12
|
+
|
|
13
|
+
if NevermoreTestRunnerUtils.runTestsIfNeededAsync(root.game) then
|
|
14
|
+
return
|
|
15
|
+
end
|
|
16
|
+
|
|
11
17
|
local serviceBag = require("ServiceBag").new()
|
|
12
18
|
serviceBag:GetService(require("{{gameNameProper}}Service"))
|
|
13
19
|
serviceBag:Init()
|