Skip to content

Commit 34ad1b9

Browse files
committed
add release bootstrap smoke test
1 parent a09601c commit 34ad1b9

3 files changed

Lines changed: 205 additions & 4 deletions

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,13 @@
4747
"dev": "turbo run dev",
4848
"dev:cli": "bun run apps/cli/src/main.ts",
4949
"test": "turbo run test",
50+
"test:release:bootstrap": "vitest run tests/release-bootstrap-smoke.test.ts",
5051
"typecheck": "bun run --filter='*' typecheck",
5152
"lint": "oxlint -c .oxlintrc.jsonc . --deny-warnings",
5253
"lint:fix": "oxlint -c .oxlintrc.jsonc --fix .",
5354
"changeset": "changeset",
5455
"changeset:version": "changeset version && bun install --lockfile-only",
55-
"release:check": "bun run --cwd apps/cli typecheck && bun run release:publish:dry-run",
56+
"release:check": "bun run --cwd apps/cli typecheck && bun run test:release:bootstrap && bun run release:publish:dry-run",
5657
"release:pre:enter": "changeset pre enter beta",
5758
"release:pre:exit": "changeset pre exit",
5859
"release:beta:start": "changeset pre enter beta",

packages/plugins/onepassword/src/sdk/service.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Context, Duration, Effect } from "effect";
2-
import { createClient, DesktopAuth } from "@1password/sdk";
32
import * as op from "@1password/op-js";
43

54
import { OnePasswordError } from "./errors";
@@ -53,6 +52,17 @@ export type ResolvedAuth =
5352
// ---------------------------------------------------------------------------
5453

5554
const DEFAULT_TIMEOUT_MS = 15_000;
55+
type OnePasswordSdkModule = typeof import("@1password/sdk");
56+
57+
const loadOnePasswordSdk = (): Effect.Effect<OnePasswordSdkModule, OnePasswordError> =>
58+
Effect.tryPromise({
59+
try: () => import("@1password/sdk"),
60+
catch: (cause) =>
61+
new OnePasswordError({
62+
operation: "sdk module load",
63+
message: cause instanceof Error ? cause.message : String(cause),
64+
}),
65+
});
5666

5767
const makeTimeoutMessage = (operation: string, timeoutMs: number): string =>
5868
[
@@ -71,13 +81,23 @@ export const makeNativeSdkService = (
7181
): Effect.Effect<OnePasswordService, OnePasswordError> =>
7282
Effect.gen(function* () {
7383
const timeout = Duration.millis(timeoutMs);
84+
const sdk = yield* loadOnePasswordSdk().pipe(
85+
Effect.timeoutFail({
86+
duration: timeout,
87+
onTimeout: () =>
88+
new OnePasswordError({
89+
operation: "sdk module load",
90+
message: makeTimeoutMessage("sdk module load", timeoutMs),
91+
}),
92+
}),
93+
);
7494

7595
const client = yield* Effect.tryPromise({
7696
try: () =>
77-
createClient({
97+
sdk.createClient({
7898
auth:
7999
auth.kind === "desktop-app"
80-
? new DesktopAuth(auth.accountName)
100+
? new sdk.DesktopAuth(auth.accountName)
81101
: auth.token,
82102
integrationName: "Executor",
83103
integrationVersion: "0.0.0",
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import { describe, expect, it } from "@effect/vitest";
2+
import { spawn } from "node:child_process";
3+
import { createServer } from "node:http";
4+
import { mkdtemp, readFile, readdir, rm, cp, writeFile } from "node:fs/promises";
5+
import { tmpdir } from "node:os";
6+
import { basename, join, resolve } from "node:path";
7+
import { fileURLToPath } from "node:url";
8+
9+
type CommandResult = {
10+
readonly exitCode: number;
11+
readonly stdout: string;
12+
readonly stderr: string;
13+
};
14+
15+
const repoRoot = resolve(dirnameOf(import.meta.url), "..");
16+
const cliRoot = join(repoRoot, "apps/cli");
17+
const distDir = join(cliRoot, "dist");
18+
19+
function dirnameOf(url: string): string {
20+
return resolve(fileURLToPath(new URL(".", url)));
21+
}
22+
23+
const runCommand = async (
24+
command: string,
25+
args: ReadonlyArray<string>,
26+
cwd: string,
27+
env: NodeJS.ProcessEnv = process.env,
28+
): Promise<CommandResult> => {
29+
const child = spawn(command, [...args], {
30+
cwd,
31+
env,
32+
stdio: ["ignore", "pipe", "pipe"],
33+
});
34+
35+
let stdout = "";
36+
let stderr = "";
37+
38+
child.stdout.setEncoding("utf8");
39+
child.stdout.on("data", (chunk) => {
40+
stdout += chunk;
41+
});
42+
43+
child.stderr.setEncoding("utf8");
44+
child.stderr.on("data", (chunk) => {
45+
stderr += chunk;
46+
});
47+
48+
const exitCode = await new Promise<number>((resolveExitCode, reject) => {
49+
child.once("error", reject);
50+
child.once("close", (code) => {
51+
resolveExitCode(code ?? -1);
52+
});
53+
});
54+
55+
return { exitCode, stdout, stderr };
56+
};
57+
58+
const listen = async (server: ReturnType<typeof createServer>): Promise<number> =>
59+
new Promise((resolvePort, reject) => {
60+
server.once("error", reject);
61+
server.listen(0, "127.0.0.1", () => {
62+
const address = server.address();
63+
if (!address || typeof address === "string") {
64+
reject(new Error("Failed to resolve local release server address"));
65+
return;
66+
}
67+
resolvePort(address.port);
68+
});
69+
});
70+
71+
const closeServer = async (server: ReturnType<typeof createServer>): Promise<void> =>
72+
new Promise((resolveClose, reject) => {
73+
server.close((error) => {
74+
if (error) {
75+
reject(error);
76+
return;
77+
}
78+
resolveClose();
79+
});
80+
});
81+
82+
const currentRuntimeBinaryName = process.platform === "win32" ? "executor.exe" : "executor";
83+
const isSupportedPlatform = ["darwin", "linux", "win32"].includes(process.platform) &&
84+
["x64", "arm64"].includes(process.arch);
85+
86+
describe("release bootstrap smoke", () => {
87+
it(
88+
"fresh wrapper install bootstraps locally hosted release assets and stays runnable",
89+
async () => {
90+
if (!isSupportedPlatform) {
91+
return;
92+
}
93+
94+
const build = await runCommand("bun", ["run", "src/build.ts", "binary", "--single"], cliRoot);
95+
expect(build.exitCode, build.stderr || build.stdout).toBe(0);
96+
97+
const assets = await runCommand("bun", ["run", "src/build.ts", "release-assets"], cliRoot);
98+
expect(assets.exitCode, assets.stderr || assets.stdout).toBe(0);
99+
100+
const wrapperDir = join(distDir, "executor");
101+
const assetNames = (await readdir(distDir))
102+
.filter((entry) => /^executor-.*\.(?:tar\.gz|zip)$/.test(entry))
103+
.sort();
104+
105+
expect(assetNames, `expected one current-platform asset in ${distDir}`).toHaveLength(1);
106+
107+
const assetName = assetNames[0]!;
108+
const assetPath = join(distDir, assetName);
109+
const tempRoot = await mkdtemp(join(tmpdir(), "executor-release-bootstrap-"));
110+
const installedPackageDir = join(tempRoot, "executor");
111+
112+
await cp(wrapperDir, installedPackageDir, { recursive: true });
113+
114+
const originalPackageJson = JSON.parse(
115+
await readFile(join(installedPackageDir, "package.json"), "utf8"),
116+
) as { version: string; homepage?: string };
117+
118+
const assetRoute = `/releases/download/v${originalPackageJson.version}/${assetName}`;
119+
const server = createServer(async (request, response) => {
120+
if (request.url !== assetRoute) {
121+
response.statusCode = 404;
122+
response.end("not found");
123+
return;
124+
}
125+
126+
const body = await readFile(assetPath);
127+
response.statusCode = 200;
128+
response.setHeader("content-length", String(body.byteLength));
129+
response.end(body);
130+
});
131+
132+
try {
133+
const port = await listen(server);
134+
const packageJsonPath = join(installedPackageDir, "package.json");
135+
await writeFile(
136+
packageJsonPath,
137+
JSON.stringify(
138+
{
139+
...originalPackageJson,
140+
homepage: `http://127.0.0.1:${port}`,
141+
},
142+
null,
143+
2,
144+
) + "\n",
145+
);
146+
147+
const firstRun = await runCommand(
148+
process.execPath,
149+
[join(installedPackageDir, "bin", "executor"), "--help"],
150+
installedPackageDir,
151+
);
152+
const combinedOutput = `${firstRun.stdout}\n${firstRun.stderr}`;
153+
154+
expect(firstRun.exitCode, combinedOutput).toBe(0);
155+
expect(combinedOutput).toContain("downloading release asset");
156+
expect(combinedOutput).toContain(`installed ${basename(assetName, ".zip").replace(/\.tar\.gz$/, "")}`);
157+
expect(combinedOutput).not.toContain("core_bg.wasm");
158+
expect(combinedOutput).not.toContain("ENOENT");
159+
160+
const installedBinaryPath = join(installedPackageDir, "bin", "runtime", currentRuntimeBinaryName);
161+
const installedBinaryStat = await readFile(installedBinaryPath);
162+
expect(installedBinaryStat.byteLength).toBeGreaterThan(0);
163+
164+
const secondRun = await runCommand(
165+
process.execPath,
166+
[join(installedPackageDir, "bin", "executor"), "--help"],
167+
installedPackageDir,
168+
);
169+
const secondCombinedOutput = `${secondRun.stdout}\n${secondRun.stderr}`;
170+
171+
expect(secondRun.exitCode, secondCombinedOutput).toBe(0);
172+
expect(secondCombinedOutput).not.toContain("downloading release asset");
173+
} finally {
174+
await closeServer(server);
175+
await rm(tempRoot, { recursive: true, force: true });
176+
}
177+
},
178+
180_000,
179+
);
180+
});

0 commit comments

Comments
 (0)