@happier-dev/stack 0.1.0-preview.17.1 → 0.1.0-preview.21.1
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/docs/server-flavors.md +6 -6
- package/node_modules/@happier-dev/cli-common/dist/index.d.ts +2 -0
- package/node_modules/@happier-dev/cli-common/dist/index.d.ts.map +1 -1
- package/node_modules/@happier-dev/cli-common/dist/index.js +2 -0
- package/node_modules/@happier-dev/cli-common/dist/index.js.map +1 -1
- package/node_modules/@happier-dev/cli-common/dist/providers/index.d.ts +51 -0
- package/node_modules/@happier-dev/cli-common/dist/providers/index.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/providers/index.js +129 -0
- package/node_modules/@happier-dev/cli-common/dist/providers/index.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/index.d.ts +5 -0
- package/node_modules/@happier-dev/cli-common/dist/service/index.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/index.js +5 -0
- package/node_modules/@happier-dev/cli-common/dist/service/index.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/launchd.d.ts +19 -0
- package/node_modules/@happier-dev/cli-common/dist/service/launchd.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/launchd.js +117 -0
- package/node_modules/@happier-dev/cli-common/dist/service/launchd.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/manager.d.ts +55 -0
- package/node_modules/@happier-dev/cli-common/dist/service/manager.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/manager.js +302 -0
- package/node_modules/@happier-dev/cli-common/dist/service/manager.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/systemd.d.ts +12 -0
- package/node_modules/@happier-dev/cli-common/dist/service/systemd.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/systemd.js +75 -0
- package/node_modules/@happier-dev/cli-common/dist/service/systemd.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/windows.d.ts +8 -0
- package/node_modules/@happier-dev/cli-common/dist/service/windows.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/windows.js +29 -0
- package/node_modules/@happier-dev/cli-common/dist/service/windows.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/package.json +11 -0
- package/node_modules/@happier-dev/release-runtime/dist/assets.d.ts +22 -0
- package/node_modules/@happier-dev/release-runtime/dist/assets.d.ts.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/assets.js +44 -0
- package/node_modules/@happier-dev/release-runtime/dist/assets.js.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/checksums.d.ts +5 -0
- package/node_modules/@happier-dev/release-runtime/dist/checksums.d.ts.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/checksums.js +21 -0
- package/node_modules/@happier-dev/release-runtime/dist/checksums.js.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/extractPlan.d.ts +14 -0
- package/node_modules/@happier-dev/release-runtime/dist/extractPlan.d.ts.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/extractPlan.js +39 -0
- package/node_modules/@happier-dev/release-runtime/dist/extractPlan.js.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/github.d.ts +20 -0
- package/node_modules/@happier-dev/release-runtime/dist/github.d.ts.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/github.js +60 -0
- package/node_modules/@happier-dev/release-runtime/dist/github.js.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/index.d.ts +7 -0
- package/node_modules/@happier-dev/release-runtime/dist/index.d.ts.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/index.js +7 -0
- package/node_modules/@happier-dev/release-runtime/dist/index.js.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/minisign.d.ts +7 -0
- package/node_modules/@happier-dev/release-runtime/dist/minisign.d.ts.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/minisign.js +92 -0
- package/node_modules/@happier-dev/release-runtime/dist/minisign.js.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/verifiedDownload.d.ts +26 -0
- package/node_modules/@happier-dev/release-runtime/dist/verifiedDownload.d.ts.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/verifiedDownload.js +53 -0
- package/node_modules/@happier-dev/release-runtime/dist/verifiedDownload.js.map +1 -0
- package/node_modules/@happier-dev/release-runtime/package.json +38 -0
- package/package.json +4 -2
- package/scripts/auth.mjs +3 -2
- package/scripts/auth_copy_from_pglite_lock_in_use.integration.test.mjs +1 -0
- package/scripts/auth_copy_from_runCapture.integration.test.mjs +8 -1
- package/scripts/auth_login_guided_server_no_expo.test.mjs +2 -0
- package/scripts/build.mjs +3 -18
- package/scripts/bundleWorkspaceDeps.mjs +5 -1
- package/scripts/bundleWorkspaceDeps.test.mjs +42 -1
- package/scripts/mobile.mjs +30 -2
- package/scripts/mobile_dev_client.mjs +7 -32
- package/scripts/mobile_dev_client_help_smoke.test.mjs +24 -0
- package/scripts/mobile_prebuild_happyDir_defined.test.mjs +47 -0
- package/scripts/mobile_prebuild_sets_rct_metro_port.test.mjs +81 -0
- package/scripts/mobile_run_ios_passes_port.integration.test.mjs +103 -0
- package/scripts/mobile_run_ios_uses_long_port_flag.test.mjs +106 -0
- package/scripts/providers_cmd.mjs +262 -0
- package/scripts/release_binary_smoke.integration.test.mjs +45 -37
- package/scripts/remote_cmd.mjs +352 -0
- package/scripts/self_host_daemon.real.integration.test.mjs +296 -0
- package/scripts/self_host_launchd.real.integration.test.mjs +211 -0
- package/scripts/self_host_runtime.mjs +1829 -327
- package/scripts/self_host_runtime.test.mjs +523 -1
- package/scripts/self_host_schtasks.real.integration.test.mjs +217 -0
- package/scripts/self_host_service_e2e_harness.mjs +93 -0
- package/scripts/self_host_systemd.real.integration.test.mjs +8 -86
- package/scripts/service.mjs +156 -26
- package/scripts/stack/command_arguments.mjs +1 -0
- package/scripts/stack/help_text.mjs +3 -1
- package/scripts/stack.mjs +2 -1
- package/scripts/stack_daemon_cmd.integration.test.mjs +37 -0
- package/scripts/stack_happy_cmd.integration.test.mjs +36 -0
- package/scripts/stack_pr_help_cmd.test.mjs +38 -0
- package/scripts/stop.mjs +2 -3
- package/scripts/utils/auth/credentials_paths.mjs +9 -9
- package/scripts/utils/auth/credentials_paths.test.mjs +8 -0
- package/scripts/utils/auth/orchestrated_stack_auth_flow.mjs +64 -3
- package/scripts/utils/auth/stable_scope_id.mjs +1 -1
- package/scripts/utils/cli/cli_registry.mjs +21 -0
- package/scripts/utils/cli/progress.mjs +8 -1
- package/scripts/utils/cli/progress.test.mjs +43 -0
- package/scripts/utils/dev/expo_dev.buildEnv.test.mjs +17 -0
- package/scripts/utils/dev/expo_dev.mjs +35 -5
- package/scripts/utils/dev/expo_dev_restart_port_reservation.test.mjs +180 -1
- package/scripts/utils/dev/expo_dev_runtime_metadata.test.mjs +126 -0
- package/scripts/utils/dev/expo_dev_verbose_logs.test.mjs +9 -2
- package/scripts/utils/mobile/dev_client_install_invocation.mjs +68 -0
- package/scripts/utils/mobile/dev_client_install_invocation.test.mjs +27 -0
- package/scripts/utils/server/port.mjs +20 -2
- package/scripts/utils/service/service_manager.definition.test.mjs +66 -0
- package/scripts/utils/service/service_manager.mjs +96 -0
- package/scripts/utils/service/service_manager.plan.test.mjs +37 -0
- package/scripts/utils/service/service_manager.test.mjs +20 -0
- package/scripts/utils/service/systemd_service_unit.mjs +1 -0
- package/scripts/utils/service/systemd_service_unit.test.mjs +42 -0
- package/scripts/utils/service/windows_schtasks_wrapper.mjs +1 -0
- package/scripts/utils/service/windows_schtasks_wrapper.test.mjs +25 -0
- package/scripts/utils/ui/ui_export_env.mjs +29 -0
- package/scripts/utils/ui/ui_export_env.test.mjs +25 -0
- package/scripts/worktrees.mjs +3 -0
- package/scripts/worktrees_status_default_target.test.mjs +56 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { cp, mkdir, mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const SELF_HOST_INSTALL_TIMEOUT_MS = 420_000;
|
|
9
|
+
|
|
10
|
+
import { commandExists, extractBinaryFromArtifact, reserveLocalhostPort, run, waitForHealth } from './self_host_service_e2e_harness.mjs';
|
|
11
|
+
|
|
12
|
+
function currentTarget() {
|
|
13
|
+
if (process.platform !== 'win32') return '';
|
|
14
|
+
if (process.arch === 'x64') return 'windows-x64';
|
|
15
|
+
return '';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readTail(path) {
|
|
19
|
+
const escaped = String(path ?? '').replaceAll("'", "''");
|
|
20
|
+
return run(
|
|
21
|
+
'powershell',
|
|
22
|
+
['-NoProfile', '-Command', `Get-Content -LiteralPath '${escaped}' -Tail 200 -ErrorAction SilentlyContinue`],
|
|
23
|
+
{ label: 'self-host-schtasks', allowFail: true, timeoutMs: 20_000 }
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test(
|
|
28
|
+
'compiled hstack self-host install/uninstall works on Windows schtasks host without repo checkout',
|
|
29
|
+
{ timeout: 15 * 60_000 },
|
|
30
|
+
async (t) => {
|
|
31
|
+
if (process.platform !== 'win32') {
|
|
32
|
+
t.skip(`windows-only test (current: ${process.platform})`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const target = currentTarget();
|
|
36
|
+
if (!target) {
|
|
37
|
+
t.skip(`unsupported Windows runner architecture: ${process.arch}`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!commandExists('schtasks')) {
|
|
41
|
+
t.skip('schtasks is required');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (!commandExists('powershell')) {
|
|
45
|
+
t.skip('powershell is required');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (!commandExists('bun')) {
|
|
49
|
+
t.skip('bun is required to build compiled binaries');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const repoRoot = resolve(fileURLToPath(new URL('../../..', import.meta.url)));
|
|
54
|
+
const version = `0.0.0-schtasks.${Date.now()}`;
|
|
55
|
+
|
|
56
|
+
run(
|
|
57
|
+
process.execPath,
|
|
58
|
+
[
|
|
59
|
+
'scripts/release/build-hstack-binaries.mjs',
|
|
60
|
+
'--channel=preview',
|
|
61
|
+
`--version=${version}`,
|
|
62
|
+
`--targets=${target}`,
|
|
63
|
+
],
|
|
64
|
+
{
|
|
65
|
+
label: 'self-host-schtasks',
|
|
66
|
+
cwd: repoRoot,
|
|
67
|
+
env: { ...process.env },
|
|
68
|
+
timeoutMs: 10 * 60_000,
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
run(
|
|
72
|
+
process.execPath,
|
|
73
|
+
[
|
|
74
|
+
'scripts/release/build-server-binaries.mjs',
|
|
75
|
+
'--channel=preview',
|
|
76
|
+
`--version=${version}`,
|
|
77
|
+
`--targets=${target}`,
|
|
78
|
+
],
|
|
79
|
+
{
|
|
80
|
+
label: 'self-host-schtasks',
|
|
81
|
+
cwd: repoRoot,
|
|
82
|
+
env: { ...process.env },
|
|
83
|
+
timeoutMs: 10 * 60_000,
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const hstackArtifact = join(repoRoot, 'dist', 'release-assets', 'stack', `hstack-v${version}-${target}.tar.gz`);
|
|
88
|
+
const serverArtifact = join(repoRoot, 'dist', 'release-assets', 'server', `happier-server-v${version}-${target}.tar.gz`);
|
|
89
|
+
|
|
90
|
+
const extractedHstack = await extractBinaryFromArtifact({ label: 'self-host-schtasks', artifactPath: hstackArtifact, binaryName: 'hstack.exe' });
|
|
91
|
+
const extractedServer = await extractBinaryFromArtifact({ label: 'self-host-schtasks', artifactPath: serverArtifact, binaryName: 'happier-server.exe' });
|
|
92
|
+
|
|
93
|
+
t.after(async () => {
|
|
94
|
+
await rm(extractedHstack.extractDir, { recursive: true, force: true });
|
|
95
|
+
await rm(extractedServer.extractDir, { recursive: true, force: true });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const sandboxDir = await mkdtemp(join(tmpdir(), 'happier-self-host-schtasks-'));
|
|
99
|
+
t.after(async () => {
|
|
100
|
+
await rm(sandboxDir, { recursive: true, force: true });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const installRoot = join(sandboxDir, 'self-host');
|
|
104
|
+
const binDir = join(sandboxDir, 'bin');
|
|
105
|
+
const configDir = join(sandboxDir, 'config');
|
|
106
|
+
const dataDir = join(sandboxDir, 'data');
|
|
107
|
+
const logDir = join(sandboxDir, 'logs');
|
|
108
|
+
await mkdir(binDir, { recursive: true });
|
|
109
|
+
|
|
110
|
+
const hstackPath = join(binDir, 'hstack.exe');
|
|
111
|
+
await cp(extractedHstack.binaryPath, hstackPath);
|
|
112
|
+
|
|
113
|
+
const serviceName = `happier-server-e2e-${Date.now().toString(36).slice(-6)}`;
|
|
114
|
+
const serverPort = await reserveLocalhostPort();
|
|
115
|
+
const commonEnv = {
|
|
116
|
+
PATH: process.env.PATH ?? '',
|
|
117
|
+
HAPPIER_SELF_HOST_INSTALL_ROOT: installRoot,
|
|
118
|
+
HAPPIER_SELF_HOST_BIN_DIR: binDir,
|
|
119
|
+
HAPPIER_SELF_HOST_CONFIG_DIR: configDir,
|
|
120
|
+
HAPPIER_SELF_HOST_DATA_DIR: dataDir,
|
|
121
|
+
HAPPIER_SELF_HOST_LOG_DIR: logDir,
|
|
122
|
+
HAPPIER_SELF_HOST_SERVICE_NAME: serviceName,
|
|
123
|
+
HAPPIER_SELF_HOST_SERVER_BINARY: extractedServer.binaryPath,
|
|
124
|
+
HAPPIER_SELF_HOST_AUTO_UPDATE: '0',
|
|
125
|
+
HAPPIER_SELF_HOST_HEALTH_TIMEOUT_MS: '240000',
|
|
126
|
+
HAPPIER_NONINTERACTIVE: '1',
|
|
127
|
+
HAPPIER_WITH_CLI: '0',
|
|
128
|
+
HAPPIER_SERVER_PORT: String(serverPort),
|
|
129
|
+
HAPPIER_SERVER_HOST: '127.0.0.1',
|
|
130
|
+
};
|
|
131
|
+
const serverOutLog = join(logDir, 'server.out.log');
|
|
132
|
+
const serverErrLog = join(logDir, 'server.err.log');
|
|
133
|
+
|
|
134
|
+
const taskName = `Happier\\${serviceName}`;
|
|
135
|
+
|
|
136
|
+
let installSucceeded = false;
|
|
137
|
+
t.after(() => {
|
|
138
|
+
if (!installSucceeded) return;
|
|
139
|
+
run(
|
|
140
|
+
hstackPath,
|
|
141
|
+
['self-host', 'uninstall', '--channel=preview', '--mode=user', '--yes', '--purge-data', '--json'],
|
|
142
|
+
{
|
|
143
|
+
env: commonEnv,
|
|
144
|
+
allowFail: true,
|
|
145
|
+
timeoutMs: 180_000,
|
|
146
|
+
stdio: 'ignore',
|
|
147
|
+
cwd: sandboxDir,
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const installResult = run(
|
|
153
|
+
hstackPath,
|
|
154
|
+
['self-host', 'install', '--channel=preview', '--mode=user', '--no-auto-update', '--non-interactive', '--without-cli', '--json'],
|
|
155
|
+
{
|
|
156
|
+
label: 'self-host-schtasks',
|
|
157
|
+
env: commonEnv,
|
|
158
|
+
timeoutMs: SELF_HOST_INSTALL_TIMEOUT_MS,
|
|
159
|
+
allowFail: true,
|
|
160
|
+
cwd: sandboxDir,
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
if ((installResult.status ?? 1) !== 0) {
|
|
164
|
+
const recoveredHealth = await waitForHealth(`http://127.0.0.1:${serverPort}/v1/version`, 120_000);
|
|
165
|
+
if (!recoveredHealth) {
|
|
166
|
+
const statusResult = run(
|
|
167
|
+
hstackPath,
|
|
168
|
+
['self-host', 'status', '--channel=preview', '--mode=user', '--json'],
|
|
169
|
+
{ label: 'self-host-schtasks', env: commonEnv, allowFail: true, timeoutMs: 60_000, cwd: sandboxDir }
|
|
170
|
+
);
|
|
171
|
+
const schtasksQuery = run('schtasks', ['/Query', '/TN', taskName, '/FO', 'LIST', '/V'], { label: 'self-host-schtasks', allowFail: true, timeoutMs: 20_000 });
|
|
172
|
+
const outTail = readTail(serverOutLog);
|
|
173
|
+
const errTail = readTail(serverErrLog);
|
|
174
|
+
throw new Error(
|
|
175
|
+
[
|
|
176
|
+
'[self-host-schtasks] self-host install failed and service never became healthy',
|
|
177
|
+
`install status: ${String(installResult.status ?? 'null')}`,
|
|
178
|
+
`install stdout:\n${String(installResult.stdout ?? '').trim()}`,
|
|
179
|
+
`install stderr:\n${String(installResult.stderr ?? '').trim()}`,
|
|
180
|
+
`self-host status:\n${String(statusResult.stdout ?? '').trim()}\n${String(statusResult.stderr ?? '').trim()}`,
|
|
181
|
+
`schtasks query (${taskName}):\n${String(schtasksQuery.stdout ?? '').trim()}\n${String(schtasksQuery.stderr ?? '').trim()}`,
|
|
182
|
+
`server out tail (${serverOutLog}):\n${String(outTail.stdout ?? '').trim()}\n${String(outTail.stderr ?? '').trim()}`,
|
|
183
|
+
`server err tail (${serverErrLog}):\n${String(errTail.stdout ?? '').trim()}\n${String(errTail.stderr ?? '').trim()}`,
|
|
184
|
+
].join('\n\n')
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
installSucceeded = true;
|
|
189
|
+
|
|
190
|
+
const healthOk = await waitForHealth(`http://127.0.0.1:${serverPort}/v1/version`, 120_000);
|
|
191
|
+
assert.equal(healthOk, true, 'self-host service health endpoint did not become ready');
|
|
192
|
+
|
|
193
|
+
const status = run(
|
|
194
|
+
hstackPath,
|
|
195
|
+
['self-host', 'status', '--channel=preview', '--mode=user', '--json'],
|
|
196
|
+
{ label: 'self-host-schtasks', env: commonEnv, timeoutMs: 60_000, cwd: sandboxDir }
|
|
197
|
+
);
|
|
198
|
+
const statusPayload = JSON.parse(String(status.stdout ?? '').trim());
|
|
199
|
+
assert.equal(statusPayload?.ok, true);
|
|
200
|
+
assert.equal(statusPayload?.service?.name, serviceName);
|
|
201
|
+
assert.equal(statusPayload?.service?.active, true);
|
|
202
|
+
assert.equal(statusPayload?.healthy, true);
|
|
203
|
+
|
|
204
|
+
const schtasksQueryAfter = run('schtasks', ['/Query', '/TN', taskName], { label: 'self-host-schtasks', allowFail: true, timeoutMs: 20_000 });
|
|
205
|
+
assert.equal(schtasksQueryAfter.status, 0, 'schtasks query should succeed after install');
|
|
206
|
+
|
|
207
|
+
run(
|
|
208
|
+
hstackPath,
|
|
209
|
+
['self-host', 'uninstall', '--channel=preview', '--mode=user', '--yes', '--purge-data', '--json'],
|
|
210
|
+
{ label: 'self-host-schtasks', env: commonEnv, timeoutMs: 180_000, cwd: sandboxDir }
|
|
211
|
+
);
|
|
212
|
+
installSucceeded = false;
|
|
213
|
+
|
|
214
|
+
const schtasksAfterUninstall = run('schtasks', ['/Query', '/TN', taskName], { label: 'self-host-schtasks', allowFail: true, timeoutMs: 20_000 });
|
|
215
|
+
assert.notEqual(schtasksAfterUninstall.status, 0, 'scheduled task should not remain after uninstall');
|
|
216
|
+
}
|
|
217
|
+
);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { mkdtemp, readdir } from 'node:fs/promises';
|
|
4
|
+
import { createServer } from 'node:net';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
const SELF_HOST_PORT_MIN = 20_000;
|
|
9
|
+
const SELF_HOST_PORT_MAX = 29_999;
|
|
10
|
+
const SELF_HOST_PORT_ATTEMPTS = 40;
|
|
11
|
+
|
|
12
|
+
export function commandExists(cmd) {
|
|
13
|
+
const name = String(cmd ?? '').trim();
|
|
14
|
+
if (!name) return false;
|
|
15
|
+
if (process.platform === 'win32') {
|
|
16
|
+
return spawnSync('where', [name], { stdio: 'ignore' }).status === 0;
|
|
17
|
+
}
|
|
18
|
+
return spawnSync('sh', ['-lc', `command -v ${name} >/dev/null 2>&1`], { stdio: 'ignore' }).status === 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function run(cmd, args, { label, cwd, env, timeoutMs = 0, allowFail = false, stdio = 'pipe' } = {}) {
|
|
22
|
+
const prefix = label ? `[${label}] ` : '';
|
|
23
|
+
const result = spawnSync(cmd, args, {
|
|
24
|
+
cwd,
|
|
25
|
+
env: env ?? process.env,
|
|
26
|
+
encoding: 'utf-8',
|
|
27
|
+
stdio,
|
|
28
|
+
timeout: timeoutMs || undefined,
|
|
29
|
+
});
|
|
30
|
+
const timedOut = result.error && result.error.code === 'ETIMEDOUT';
|
|
31
|
+
if (!allowFail && (timedOut || (result.status ?? 1) !== 0)) {
|
|
32
|
+
const stderr = String(result.stderr ?? '').trim();
|
|
33
|
+
const stdout = String(result.stdout ?? '').trim();
|
|
34
|
+
const reason = timedOut ? 'timed out' : `exited with status ${result.status}`;
|
|
35
|
+
throw new Error(`${prefix}${cmd} ${args.join(' ')} ${reason}\nstdout:\n${stdout}\nstderr:\n${stderr}`);
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function extractBinaryFromArtifact({ artifactPath, binaryName, label } = {}) {
|
|
41
|
+
const path = String(artifactPath ?? '').trim();
|
|
42
|
+
const name = String(binaryName ?? '').trim();
|
|
43
|
+
assert.ok(path, 'artifactPath is required');
|
|
44
|
+
assert.ok(name, 'binaryName is required');
|
|
45
|
+
const extractDir = await mkdtemp(join(tmpdir(), 'happier-self-host-artifact-'));
|
|
46
|
+
run('tar', ['-xzf', path, '-C', extractDir], { label, timeoutMs: 60_000 });
|
|
47
|
+
const roots = await readdir(extractDir);
|
|
48
|
+
assert.ok(roots.length > 0, `expected extracted root directory for ${path}`);
|
|
49
|
+
return {
|
|
50
|
+
extractDir,
|
|
51
|
+
binaryPath: join(extractDir, roots[0], name),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function waitForHealth(url, timeoutMs = 60_000) {
|
|
56
|
+
const started = Date.now();
|
|
57
|
+
while (Date.now() - started < timeoutMs) {
|
|
58
|
+
try {
|
|
59
|
+
const response = await fetch(String(url), {
|
|
60
|
+
headers: { accept: 'application/json' },
|
|
61
|
+
});
|
|
62
|
+
if (response.ok) {
|
|
63
|
+
const payload = await response.json().catch(() => ({}));
|
|
64
|
+
if (payload?.ok === true) return true;
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// keep polling until timeout
|
|
68
|
+
}
|
|
69
|
+
await new Promise((resolveSleep) => setTimeout(resolveSleep, 1500));
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function canBindLocalhostPort(port) {
|
|
75
|
+
return await new Promise((resolvePort) => {
|
|
76
|
+
const server = createServer();
|
|
77
|
+
server.unref();
|
|
78
|
+
server.once('error', () => resolvePort(false));
|
|
79
|
+
server.listen(port, '127.0.0.1', () => {
|
|
80
|
+
server.close((error) => resolvePort(!error));
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function reserveLocalhostPort() {
|
|
86
|
+
for (let attempt = 0; attempt < SELF_HOST_PORT_ATTEMPTS; attempt += 1) {
|
|
87
|
+
const candidate =
|
|
88
|
+
SELF_HOST_PORT_MIN + Math.floor(Math.random() * (SELF_HOST_PORT_MAX - SELF_HOST_PORT_MIN + 1));
|
|
89
|
+
if (await canBindLocalhostPort(candidate)) return candidate;
|
|
90
|
+
}
|
|
91
|
+
throw new Error('failed to reserve localhost port from non-ephemeral range');
|
|
92
|
+
}
|
|
93
|
+
|
|
@@ -1,38 +1,13 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
-
import {
|
|
3
|
-
import { chmod, cp, mkdir, mkdtemp, readdir, rm } from 'node:fs/promises';
|
|
4
|
-
import { createServer } from 'node:net';
|
|
2
|
+
import { chmod, cp, mkdir, mkdtemp, rm } from 'node:fs/promises';
|
|
5
3
|
import { tmpdir } from 'node:os';
|
|
6
4
|
import { join, resolve } from 'node:path';
|
|
7
5
|
import test from 'node:test';
|
|
8
6
|
import { fileURLToPath } from 'node:url';
|
|
9
7
|
|
|
10
|
-
const SELF_HOST_PORT_MIN = 20_000;
|
|
11
|
-
const SELF_HOST_PORT_MAX = 29_999;
|
|
12
|
-
const SELF_HOST_PORT_ATTEMPTS = 40;
|
|
13
8
|
const SELF_HOST_INSTALL_TIMEOUT_MS = 420_000;
|
|
14
9
|
|
|
15
|
-
|
|
16
|
-
return spawnSync('bash', ['-lc', `command -v ${cmd} >/dev/null 2>&1`], { stdio: 'ignore' }).status === 0;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function run(cmd, args, { cwd, env, timeoutMs = 0, allowFail = false, stdio = 'pipe' } = {}) {
|
|
20
|
-
const result = spawnSync(cmd, args, {
|
|
21
|
-
cwd,
|
|
22
|
-
env: env ?? process.env,
|
|
23
|
-
encoding: 'utf-8',
|
|
24
|
-
stdio,
|
|
25
|
-
timeout: timeoutMs || undefined,
|
|
26
|
-
});
|
|
27
|
-
const timedOut = result.error && result.error.code === 'ETIMEDOUT';
|
|
28
|
-
if (!allowFail && (timedOut || (result.status ?? 1) !== 0)) {
|
|
29
|
-
const stderr = String(result.stderr ?? '').trim();
|
|
30
|
-
const stdout = String(result.stdout ?? '').trim();
|
|
31
|
-
const reason = timedOut ? 'timed out' : `exited with status ${result.status}`;
|
|
32
|
-
throw new Error(`[self-host-systemd] ${cmd} ${args.join(' ')} ${reason}\nstdout:\n${stdout}\nstderr:\n${stderr}`);
|
|
33
|
-
}
|
|
34
|
-
return result;
|
|
35
|
-
}
|
|
10
|
+
import { commandExists, extractBinaryFromArtifact, reserveLocalhostPort, run, waitForHealth } from './self_host_service_e2e_harness.mjs';
|
|
36
11
|
|
|
37
12
|
function runAsRoot(cmd, args, { cwd, env, timeoutMs = 0, allowFail = false, stdio = 'pipe' } = {}) {
|
|
38
13
|
const mergedEnv = {
|
|
@@ -40,13 +15,14 @@ function runAsRoot(cmd, args, { cwd, env, timeoutMs = 0, allowFail = false, stdi
|
|
|
40
15
|
...(env ?? {}),
|
|
41
16
|
};
|
|
42
17
|
if (typeof process.getuid === 'function' && process.getuid() === 0) {
|
|
43
|
-
return run(cmd, args, { cwd, env: mergedEnv, timeoutMs, allowFail, stdio });
|
|
18
|
+
return run(cmd, args, { label: 'self-host-systemd', cwd, env: mergedEnv, timeoutMs, allowFail, stdio });
|
|
44
19
|
}
|
|
45
20
|
if (!commandExists('sudo')) {
|
|
46
21
|
throw new Error('[self-host-systemd] sudo is required for systemd self-host test');
|
|
47
22
|
}
|
|
48
23
|
const envArgs = Object.entries(mergedEnv).map(([key, value]) => `${key}=${String(value ?? '')}`);
|
|
49
24
|
return run('sudo', ['-E', 'env', ...envArgs, cmd, ...args], {
|
|
25
|
+
label: 'self-host-systemd',
|
|
50
26
|
cwd,
|
|
51
27
|
env: process.env,
|
|
52
28
|
timeoutMs,
|
|
@@ -55,62 +31,6 @@ function runAsRoot(cmd, args, { cwd, env, timeoutMs = 0, allowFail = false, stdi
|
|
|
55
31
|
});
|
|
56
32
|
}
|
|
57
33
|
|
|
58
|
-
async function extractBinaryFromArtifact({ artifactPath, binaryName }) {
|
|
59
|
-
const extractDir = await mkdtemp(join(tmpdir(), 'happier-self-host-systemd-artifact-'));
|
|
60
|
-
run('tar', ['-xzf', artifactPath, '-C', extractDir], { timeoutMs: 30_000 });
|
|
61
|
-
const roots = await readdir(extractDir);
|
|
62
|
-
assert.ok(roots.length > 0, `expected extracted root directory for ${artifactPath}`);
|
|
63
|
-
return {
|
|
64
|
-
extractDir,
|
|
65
|
-
binaryPath: join(extractDir, roots[0], binaryName),
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function waitForHealth(url, timeoutMs = 60_000) {
|
|
70
|
-
const started = Date.now();
|
|
71
|
-
while (Date.now() - started < timeoutMs) {
|
|
72
|
-
try {
|
|
73
|
-
const response = await fetch(url, {
|
|
74
|
-
headers: { accept: 'application/json' },
|
|
75
|
-
});
|
|
76
|
-
if (response.ok) {
|
|
77
|
-
const payload = await response.json().catch(() => ({}));
|
|
78
|
-
if (payload?.ok === true) {
|
|
79
|
-
return true;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
} catch {
|
|
83
|
-
// keep polling until timeout
|
|
84
|
-
}
|
|
85
|
-
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
86
|
-
}
|
|
87
|
-
return false;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async function canBindLocalhostPort(port) {
|
|
91
|
-
return await new Promise((resolvePort) => {
|
|
92
|
-
const server = createServer();
|
|
93
|
-
server.unref();
|
|
94
|
-
server.once('error', () => resolvePort(false));
|
|
95
|
-
server.listen(port, '127.0.0.1', () => {
|
|
96
|
-
server.close((error) => {
|
|
97
|
-
resolvePort(!error);
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async function reserveLocalhostPort() {
|
|
104
|
-
for (let attempt = 0; attempt < SELF_HOST_PORT_ATTEMPTS; attempt += 1) {
|
|
105
|
-
const candidate =
|
|
106
|
-
SELF_HOST_PORT_MIN + Math.floor(Math.random() * (SELF_HOST_PORT_MAX - SELF_HOST_PORT_MIN + 1));
|
|
107
|
-
if (await canBindLocalhostPort(candidate)) {
|
|
108
|
-
return candidate;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
throw new Error('failed to reserve localhost port from non-ephemeral range');
|
|
112
|
-
}
|
|
113
|
-
|
|
114
34
|
test(
|
|
115
35
|
'compiled hstack self-host install/uninstall works on systemd host without repo checkout',
|
|
116
36
|
{ timeout: 15 * 60_000 },
|
|
@@ -148,6 +68,7 @@ test(
|
|
|
148
68
|
'--targets=linux-x64',
|
|
149
69
|
],
|
|
150
70
|
{
|
|
71
|
+
label: 'self-host-systemd',
|
|
151
72
|
cwd: repoRoot,
|
|
152
73
|
env: { ...process.env },
|
|
153
74
|
timeoutMs: 8 * 60_000,
|
|
@@ -162,6 +83,7 @@ test(
|
|
|
162
83
|
'--targets=linux-x64',
|
|
163
84
|
],
|
|
164
85
|
{
|
|
86
|
+
label: 'self-host-systemd',
|
|
165
87
|
cwd: repoRoot,
|
|
166
88
|
env: { ...process.env },
|
|
167
89
|
timeoutMs: 8 * 60_000,
|
|
@@ -171,8 +93,8 @@ test(
|
|
|
171
93
|
const hstackArtifact = join(repoRoot, 'dist', 'release-assets', 'stack', `hstack-v${version}-linux-x64.tar.gz`);
|
|
172
94
|
const serverArtifact = join(repoRoot, 'dist', 'release-assets', 'server', `happier-server-v${version}-linux-x64.tar.gz`);
|
|
173
95
|
|
|
174
|
-
const extractedHstack = await extractBinaryFromArtifact({ artifactPath: hstackArtifact, binaryName: 'hstack' });
|
|
175
|
-
const extractedServer = await extractBinaryFromArtifact({ artifactPath: serverArtifact, binaryName: 'happier-server' });
|
|
96
|
+
const extractedHstack = await extractBinaryFromArtifact({ label: 'self-host-systemd', artifactPath: hstackArtifact, binaryName: 'hstack' });
|
|
97
|
+
const extractedServer = await extractBinaryFromArtifact({ label: 'self-host-systemd', artifactPath: serverArtifact, binaryName: 'happier-server' });
|
|
176
98
|
|
|
177
99
|
t.after(async () => {
|
|
178
100
|
await rm(extractedHstack.extractDir, { recursive: true, force: true });
|