@rigkit/provider-freestyle 0.2.3 → 0.2.5
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/README.md +9 -3
- package/package.json +8 -5
- package/src/host-auth.test.ts +375 -0
- package/src/host-auth.ts +591 -0
- package/src/index.ts +37 -59
- package/src/provider.test.ts +188 -113
- package/src/provider.ts +113 -378
- package/src/terminal-session.test.ts +239 -7
- package/src/terminal-session.ts +1181 -332
- package/src/version.ts +1 -1
package/src/index.ts
CHANGED
|
@@ -3,30 +3,37 @@ import {
|
|
|
3
3
|
type WorkflowProviderDefinition,
|
|
4
4
|
} from "@rigkit/sdk";
|
|
5
5
|
import type { BaseProviderPlugin } from "@rigkit/engine";
|
|
6
|
-
import type { WorkflowProviderController } from "@rigkit/engine";
|
|
7
|
-
import { Freestyle } from "freestyle";
|
|
8
6
|
import * as z from "zod/v4-mini";
|
|
9
7
|
import { freestyleIdentityId, freestyleToken, freestyleTokenId } from "./auth.ts";
|
|
8
|
+
import {
|
|
9
|
+
createFreestyleAuthenticatedClient,
|
|
10
|
+
createFreestyleProxyFetch,
|
|
11
|
+
type FreestyleProviderAuthConfig,
|
|
12
|
+
} from "./host-auth.ts";
|
|
10
13
|
import {
|
|
11
14
|
FREESTYLE_PROVIDER_ID,
|
|
12
15
|
FREESTYLE_TERMINAL_PROVIDER_ID,
|
|
13
16
|
createFreestyleTerminalController,
|
|
14
17
|
createFreestyleWorkflowProvider,
|
|
15
|
-
isFreestyleVmSnapshotRef,
|
|
16
18
|
} from "./provider.ts";
|
|
17
19
|
import type { FreestyleRuntime, FreestyleTerminalRuntime } from "./provider.ts";
|
|
18
|
-
import { createFreestyleStore } from "./store.ts";
|
|
19
20
|
|
|
20
21
|
const freestyleProviderConfigSchema = z.object({
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
auth: z.optional(z.object({
|
|
23
|
+
apiKey: z.optional(z.string().check(z.minLength(1))),
|
|
24
|
+
profile: z.optional(z.string().check(z.minLength(1))),
|
|
25
|
+
teamId: z.optional(z.string().check(z.minLength(1))),
|
|
26
|
+
apiUrl: z.optional(z.string().check(z.minLength(1))),
|
|
27
|
+
dashboardUrl: z.optional(z.string().check(z.minLength(1))),
|
|
28
|
+
stackApiUrl: z.optional(z.string().check(z.minLength(1))),
|
|
29
|
+
stackAppUrl: z.optional(z.string().check(z.minLength(1))),
|
|
30
|
+
stackProjectId: z.optional(z.string().check(z.minLength(1))),
|
|
31
|
+
stackPublishableClientKey: z.optional(z.string().check(z.minLength(1))),
|
|
32
|
+
})),
|
|
27
33
|
});
|
|
28
34
|
|
|
29
35
|
export type FreestyleProviderConfig = z.output<typeof freestyleProviderConfigSchema>;
|
|
36
|
+
export type { FreestyleProviderAuthConfig };
|
|
30
37
|
|
|
31
38
|
export type FreestyleProviderDefinition = WorkflowProviderDefinition<
|
|
32
39
|
typeof FREESTYLE_PROVIDER_ID,
|
|
@@ -41,7 +48,7 @@ export type FreestyleTerminalProviderDefinition = WorkflowProviderDefinition<
|
|
|
41
48
|
>;
|
|
42
49
|
|
|
43
50
|
export function provider(
|
|
44
|
-
config: FreestyleProviderDefinition["config"],
|
|
51
|
+
config: FreestyleProviderDefinition["config"] = {},
|
|
45
52
|
): FreestyleProviderDefinition {
|
|
46
53
|
return defineProvider(FREESTYLE_PROVIDER_ID, config, freestyleProviderPlugin);
|
|
47
54
|
}
|
|
@@ -59,50 +66,18 @@ export const defineFreestyleProvider = provider;
|
|
|
59
66
|
|
|
60
67
|
export const freestyleProviderPlugin: BaseProviderPlugin = {
|
|
61
68
|
providerId: FREESTYLE_PROVIDER_ID,
|
|
62
|
-
createProvider({ provider,
|
|
69
|
+
async createProvider({ provider, hostStorage, local }) {
|
|
63
70
|
const config = parseFreestyleProviderConfig(provider.config);
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const savedIdentity = store.getIdentity();
|
|
75
|
-
if (savedIdentity) {
|
|
76
|
-
return createFreestyleWorkflowProvider({
|
|
77
|
-
apiKey,
|
|
78
|
-
identityId: savedIdentity.identityId,
|
|
79
|
-
token: savedIdentity.token,
|
|
80
|
-
vm,
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const client = new Freestyle({ apiKey });
|
|
85
|
-
const { identity, identityId } = await client.identities.create();
|
|
86
|
-
const { token, tokenId } = await identity.tokens.create();
|
|
87
|
-
const createdIdentity = store.saveIdentity({
|
|
88
|
-
identityId: freestyleIdentityId(identityId),
|
|
89
|
-
tokenId: freestyleTokenId(tokenId),
|
|
90
|
-
token: freestyleToken(token),
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
return createFreestyleWorkflowProvider({
|
|
94
|
-
apiKey,
|
|
95
|
-
identityId: createdIdentity.identityId,
|
|
96
|
-
token: createdIdentity.token,
|
|
97
|
-
vm,
|
|
98
|
-
});
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
return {
|
|
102
|
-
providerId: FREESTYLE_PROVIDER_ID,
|
|
103
|
-
runtime: async (context) => await (await load()).runtime(context),
|
|
104
|
-
validateArtifact: (ref) => isFreestyleVmSnapshotRef(ref),
|
|
105
|
-
} satisfies WorkflowProviderController<FreestyleRuntime>;
|
|
71
|
+
const authenticated = await createFreestyleAuthenticatedClient({
|
|
72
|
+
auth: config.auth,
|
|
73
|
+
hostStorage,
|
|
74
|
+
local,
|
|
75
|
+
});
|
|
76
|
+
return createFreestyleWorkflowProvider({
|
|
77
|
+
client: authenticated.client,
|
|
78
|
+
identityId: authenticated.identityId,
|
|
79
|
+
token: authenticated.token,
|
|
80
|
+
});
|
|
106
81
|
},
|
|
107
82
|
};
|
|
108
83
|
|
|
@@ -113,6 +88,10 @@ export const freestyleTerminalPlugin: BaseProviderPlugin = {
|
|
|
113
88
|
},
|
|
114
89
|
};
|
|
115
90
|
|
|
91
|
+
export {
|
|
92
|
+
createFreestyleAuthenticatedClient,
|
|
93
|
+
createFreestyleProxyFetch,
|
|
94
|
+
} from "./host-auth.ts";
|
|
116
95
|
export {
|
|
117
96
|
freestyleIdentityId,
|
|
118
97
|
freestyleToken,
|
|
@@ -124,24 +103,23 @@ export {
|
|
|
124
103
|
export {
|
|
125
104
|
FREESTYLE_PROVIDER_ID,
|
|
126
105
|
FREESTYLE_TERMINAL_PROVIDER_ID,
|
|
127
|
-
createFreestyleProvider,
|
|
128
106
|
createFreestyleTerminalController,
|
|
129
107
|
createFreestyleWorkflowController,
|
|
130
108
|
createFreestyleWorkflowProvider,
|
|
131
|
-
isFreestyleVmSnapshotRef,
|
|
132
109
|
} from "./provider.ts";
|
|
133
110
|
export { createFreestyleStore } from "./store.ts";
|
|
134
111
|
export { createFreestyleTerminalSession } from "./terminal-session.ts";
|
|
135
112
|
export { RIGKIT_PROVIDER_FREESTYLE_VERSION } from "./version.ts";
|
|
113
|
+
export { Freestyle, VmBaseImage, VmSpec, VmWith, VmWithInstance } from "freestyle";
|
|
114
|
+
export type { CreateVmOptions } from "freestyle";
|
|
136
115
|
export type {
|
|
137
116
|
FreestyleCmuxSshOptions,
|
|
138
117
|
FreestyleCmuxSshOptionsInput,
|
|
139
118
|
FreestyleRuntime,
|
|
119
|
+
FreestyleSdkVm,
|
|
120
|
+
FreestyleSshInput,
|
|
140
121
|
FreestyleTerminalRuntime,
|
|
141
122
|
FreestyleVscodeUrlOptions,
|
|
142
|
-
FreestyleVmConfig,
|
|
143
|
-
FreestyleVmRuntime,
|
|
144
|
-
FreestyleVmSnapshotRef,
|
|
145
123
|
} from "./provider.ts";
|
|
146
124
|
export type { FreestyleGitRelationship, FreestyleIdentity } from "./store.ts";
|
|
147
125
|
|
package/src/provider.test.ts
CHANGED
|
@@ -1,82 +1,61 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from "
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { Freestyle } from "freestyle";
|
|
3
|
+
import type { ProviderInteractionSession, ProviderRuntimeContext } from "@rigkit/engine";
|
|
4
|
+
import { freestyleIdentityId, freestyleToken } from "./auth.ts";
|
|
5
|
+
import {
|
|
6
|
+
buildInteractiveSshCommand,
|
|
7
|
+
createFreestyleTerminalController,
|
|
8
|
+
createFreestyleWorkflowController,
|
|
9
|
+
} from "./provider.ts";
|
|
10
|
+
|
|
11
|
+
const previousFetch = globalThis.fetch;
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
globalThis.fetch = previousFetch;
|
|
15
|
+
});
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
describe("Freestyle provider host adapters", () => {
|
|
18
|
+
test("creates SSH options and grants VM access internally", async () => {
|
|
19
|
+
const requests: string[] = [];
|
|
20
|
+
globalThis.fetch = (async (resource, init) => {
|
|
21
|
+
requests.push(`${init?.method ?? "GET"} ${String(resource)}`);
|
|
22
|
+
return Response.json({});
|
|
23
|
+
}) as typeof fetch;
|
|
24
|
+
|
|
25
|
+
const runtime = await createFreestyleWorkflowController({
|
|
26
|
+
client: new Freestyle({ apiKey: "test-key" }),
|
|
27
|
+
identityId: freestyleIdentityId("identity-stream"),
|
|
28
|
+
token: freestyleToken("token"),
|
|
29
|
+
}).runtime(providerContext());
|
|
30
|
+
|
|
31
|
+
await expect(runtime.createSSHOptions({ vmId: "vm-stream" })).resolves.toEqual({
|
|
32
|
+
kind: "ssh",
|
|
33
|
+
host: "vm-ssh.freestyle.sh",
|
|
34
|
+
username: "vm-stream+root",
|
|
35
|
+
auth: { type: "token", token: "token" },
|
|
36
|
+
command: "ssh vm-stream+root:token@vm-ssh.freestyle.sh",
|
|
20
37
|
});
|
|
21
|
-
|
|
22
|
-
expect(wrapped).toContain("export HOME=${HOME:-/root}");
|
|
23
|
-
expect(wrapped).toContain("export HOME='\\''/workspace/home'\\''");
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test("streams VM command output through run events without replaying buffered output", async () => {
|
|
27
|
-
const chunks: ExecOutputChunk[] = [];
|
|
28
|
-
const events: WorkflowEvent[] = [];
|
|
29
|
-
const provider = new StreamingProvider();
|
|
30
|
-
const controller = createFreestyleWorkflowController(provider);
|
|
31
|
-
const runtime = await controller.runtime(providerContext(events));
|
|
32
|
-
const vm = runtime.vms.fromId("vm-stream");
|
|
33
|
-
|
|
34
|
-
const result = await vm.exec("printf ready", {
|
|
35
|
-
name: "stream command",
|
|
36
|
-
onOutput: (chunk) => {
|
|
37
|
-
chunks.push(chunk);
|
|
38
|
-
},
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
expect(result.stdout).toBe("ready\n");
|
|
42
|
-
expect(chunks).toEqual([{ stream: "stdout", data: "ready\n" }]);
|
|
43
|
-
expect(events).toEqual([
|
|
44
|
-
{
|
|
45
|
-
type: "command.started",
|
|
46
|
-
nodePath: "workflow.step",
|
|
47
|
-
commandName: "stream command",
|
|
48
|
-
command: "printf ready",
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
type: "command.output",
|
|
52
|
-
nodePath: "workflow.step",
|
|
53
|
-
commandName: "stream command",
|
|
54
|
-
stream: "stdout",
|
|
55
|
-
data: "ready\n",
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
type: "command.completed",
|
|
59
|
-
nodePath: "workflow.step",
|
|
60
|
-
commandName: "stream command",
|
|
61
|
-
exitCode: 0,
|
|
62
|
-
},
|
|
63
|
-
]);
|
|
38
|
+
expect(requests).toContain("POST https://api.freestyle.sh/identity/v1/identities/identity-stream/permissions/vm/vm-stream");
|
|
64
39
|
});
|
|
65
40
|
|
|
66
41
|
test("creates cmux ssh options with Freestyle-owned ssh settings", async () => {
|
|
67
|
-
|
|
68
|
-
const controller = createFreestyleWorkflowController(provider);
|
|
69
|
-
const runtime = await controller.runtime(providerContext([]));
|
|
70
|
-
const vm = runtime.vms.fromId("vm-stream");
|
|
42
|
+
globalThis.fetch = (async () => Response.json({})) as unknown as typeof fetch;
|
|
71
43
|
|
|
72
|
-
const
|
|
44
|
+
const runtime = await createFreestyleWorkflowController({
|
|
45
|
+
client: new Freestyle({ apiKey: "test-key" }),
|
|
46
|
+
identityId: freestyleIdentityId("identity-stream"),
|
|
47
|
+
token: freestyleToken("token"),
|
|
48
|
+
}).runtime(providerContext());
|
|
49
|
+
|
|
50
|
+
const ssh = await runtime.cmux.createSshOptions({
|
|
51
|
+
vmId: "vm-stream",
|
|
73
52
|
sshOptions: ["ServerAliveInterval=15"],
|
|
74
53
|
skipDaemonBootstrap: true,
|
|
75
54
|
});
|
|
76
55
|
|
|
77
56
|
expect(ssh).toEqual({
|
|
78
57
|
kind: "ssh",
|
|
79
|
-
destination: "root,token@
|
|
58
|
+
destination: "vm-stream+root,token@vm-ssh.freestyle.sh",
|
|
80
59
|
skipDaemonBootstrap: true,
|
|
81
60
|
sshOptions: [
|
|
82
61
|
"StrictHostKeyChecking=no",
|
|
@@ -90,71 +69,168 @@ describe("Freestyle provider command wrapper", () => {
|
|
|
90
69
|
});
|
|
91
70
|
});
|
|
92
71
|
|
|
72
|
+
test("treats existing VM permissions as idempotent for cmux ssh options", async () => {
|
|
73
|
+
const calls: string[] = [];
|
|
74
|
+
const runtime = await createFreestyleWorkflowController({
|
|
75
|
+
client: {
|
|
76
|
+
identities: {
|
|
77
|
+
ref: () => ({
|
|
78
|
+
permissions: {
|
|
79
|
+
vms: {
|
|
80
|
+
grant: async () => {
|
|
81
|
+
calls.push("grant");
|
|
82
|
+
throw new Error("PERMISSION_ALREADY_EXISTS: Permission already exists");
|
|
83
|
+
},
|
|
84
|
+
update: async () => {
|
|
85
|
+
calls.push("update");
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
}),
|
|
90
|
+
},
|
|
91
|
+
} as unknown as Freestyle,
|
|
92
|
+
identityId: freestyleIdentityId("identity-stream"),
|
|
93
|
+
token: freestyleToken("token"),
|
|
94
|
+
}).runtime(providerContext());
|
|
95
|
+
|
|
96
|
+
await expect(runtime.cmux.createSshOptions({ vmId: "vm-stream" })).resolves.toMatchObject({
|
|
97
|
+
destination: "vm-stream+root,token@vm-ssh.freestyle.sh",
|
|
98
|
+
});
|
|
99
|
+
expect(calls).toEqual(["grant", "update"]);
|
|
100
|
+
});
|
|
101
|
+
|
|
93
102
|
test("creates VS Code URLs using the Freestyle ssh authority", async () => {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const runtime = await
|
|
97
|
-
|
|
103
|
+
globalThis.fetch = (async () => Response.json({})) as unknown as typeof fetch;
|
|
104
|
+
|
|
105
|
+
const runtime = await createFreestyleWorkflowController({
|
|
106
|
+
client: new Freestyle({ apiKey: "test-key" }),
|
|
107
|
+
identityId: freestyleIdentityId("identity-stream"),
|
|
108
|
+
token: freestyleToken("token"),
|
|
109
|
+
}).runtime(providerContext());
|
|
98
110
|
|
|
99
|
-
const url = await runtime.vscode.createUrl(vm,
|
|
111
|
+
const url = await runtime.vscode.createUrl({ vmId: "vm-stream", cwd: "/workspace/site" });
|
|
100
112
|
|
|
101
113
|
expect(url).toBe(
|
|
102
|
-
"vscode://vscode-remote/ssh-remote+
|
|
114
|
+
"vscode://vscode-remote/ssh-remote+vm-stream%2Broot%3Atoken%40vm-ssh.freestyle.sh/workspace/site?windowId=_blank",
|
|
103
115
|
);
|
|
104
116
|
});
|
|
105
|
-
});
|
|
106
117
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
118
|
+
test("honors explicit SSH users", async () => {
|
|
119
|
+
globalThis.fetch = (async () => Response.json({})) as unknown as typeof fetch;
|
|
120
|
+
|
|
121
|
+
const runtime = await createFreestyleWorkflowController({
|
|
122
|
+
client: new Freestyle({ apiKey: "test-key" }),
|
|
123
|
+
identityId: freestyleIdentityId("identity-stream"),
|
|
124
|
+
token: freestyleToken("token"),
|
|
125
|
+
}).runtime(providerContext());
|
|
126
|
+
|
|
127
|
+
await expect(runtime.createSSHOptions({ vmId: "vm-stream", user: "ubuntu" })).resolves.toMatchObject({
|
|
128
|
+
username: "vm-stream+ubuntu",
|
|
129
|
+
command: "ssh vm-stream+ubuntu:token@vm-ssh.freestyle.sh",
|
|
130
|
+
});
|
|
131
|
+
});
|
|
114
132
|
|
|
115
|
-
|
|
116
|
-
|
|
133
|
+
test("runs terminal commands as SSH remote commands instead of typed startup input", async () => {
|
|
134
|
+
let html = "";
|
|
135
|
+
const runtime = await createFreestyleTerminalController().runtime({
|
|
136
|
+
...providerContext(),
|
|
137
|
+
interaction: {
|
|
138
|
+
present: async <Result>(session: ProviderInteractionSession<Result>) => {
|
|
139
|
+
const response = await fetch(session.url);
|
|
140
|
+
html = await response.text();
|
|
141
|
+
session.stop();
|
|
142
|
+
return { finished: true } as Result;
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
});
|
|
117
146
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
147
|
+
await expect(runtime.open("GitHub auth", {
|
|
148
|
+
ssh: {
|
|
149
|
+
kind: "ssh",
|
|
150
|
+
host: "vm-ssh.freestyle.sh",
|
|
151
|
+
username: "vm-stream+root",
|
|
152
|
+
auth: { type: "token", token: "token" },
|
|
153
|
+
command: "ssh vm-stream+root:token@vm-ssh.freestyle.sh",
|
|
154
|
+
},
|
|
155
|
+
command: "gh auth login --hostname github.com",
|
|
156
|
+
})).resolves.toEqual({ finished: true });
|
|
121
157
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
158
|
+
expect(html).toContain("gh auth login --hostname github.com");
|
|
159
|
+
expect(html).toContain("const startupInput = null;");
|
|
160
|
+
expect(html).toContain("const canFinishWhileRunning = false;");
|
|
161
|
+
});
|
|
125
162
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
163
|
+
test("sets remote browser open fallbacks for SSH terminal commands", () => {
|
|
164
|
+
const command = buildInteractiveSshCommand(
|
|
165
|
+
{
|
|
166
|
+
kind: "ssh",
|
|
167
|
+
host: "vm-ssh.freestyle.sh",
|
|
168
|
+
username: "vm-stream+root",
|
|
169
|
+
auth: { type: "token", token: "token" },
|
|
170
|
+
command: "ssh vm-stream+root:token@vm-ssh.freestyle.sh",
|
|
171
|
+
},
|
|
172
|
+
"gh auth login --hostname github.com --web",
|
|
173
|
+
);
|
|
130
174
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
175
|
+
expect(command).toContain('export BROWSER="${BROWSER:-true}"');
|
|
176
|
+
expect(command).toContain('export GH_BROWSER="${GH_BROWSER:-$BROWSER}"');
|
|
177
|
+
expect(command).toContain("gh auth login --hostname github.com --web");
|
|
178
|
+
});
|
|
134
179
|
|
|
135
|
-
|
|
180
|
+
test("can keep an SSH terminal open after a successful remote command", () => {
|
|
181
|
+
const command = buildInteractiveSshCommand(
|
|
182
|
+
{
|
|
183
|
+
kind: "ssh",
|
|
184
|
+
host: "vm-ssh.freestyle.sh",
|
|
185
|
+
username: "vm-stream+root",
|
|
186
|
+
auth: { type: "token", token: "token" },
|
|
187
|
+
command: "ssh vm-stream+root:token@vm-ssh.freestyle.sh",
|
|
188
|
+
},
|
|
189
|
+
"gh auth status -h github.com",
|
|
190
|
+
{ keepOpenAfterCommand: true },
|
|
191
|
+
);
|
|
136
192
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
193
|
+
expect(command).toContain("gh auth status -h github.com");
|
|
194
|
+
expect(command).toContain("status=$?");
|
|
195
|
+
expect(command).toContain('if [ "$status" -ne 0 ]; then exit "$status"; fi');
|
|
196
|
+
expect(command).toContain('exec "${SHELL:-/bin/bash}" -l');
|
|
197
|
+
});
|
|
140
198
|
|
|
141
|
-
async
|
|
142
|
-
|
|
143
|
-
|
|
199
|
+
test("allows finishing while a keep-open SSH command is running", async () => {
|
|
200
|
+
let html = "";
|
|
201
|
+
const runtime = await createFreestyleTerminalController().runtime({
|
|
202
|
+
...providerContext(),
|
|
203
|
+
interaction: {
|
|
204
|
+
present: async <Result>(session: ProviderInteractionSession<Result>) => {
|
|
205
|
+
const response = await fetch(session.url);
|
|
206
|
+
html = await response.text();
|
|
207
|
+
session.stop();
|
|
208
|
+
return { finished: true } as Result;
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
});
|
|
144
212
|
|
|
145
|
-
|
|
146
|
-
|
|
213
|
+
await runtime.open("GitHub auth", {
|
|
214
|
+
ssh: {
|
|
215
|
+
kind: "ssh",
|
|
216
|
+
host: "vm-ssh.freestyle.sh",
|
|
217
|
+
username: "vm-stream+root",
|
|
218
|
+
auth: { type: "token", token: "token" },
|
|
219
|
+
command: "ssh vm-stream+root:token@vm-ssh.freestyle.sh",
|
|
220
|
+
},
|
|
221
|
+
command: "gh auth login --hostname github.com",
|
|
222
|
+
keepOpenAfterCommand: true,
|
|
223
|
+
});
|
|
147
224
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
225
|
+
expect(html).toContain("const canFinishWhileRunning = true;");
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
function providerContext(): ProviderRuntimeContext {
|
|
152
230
|
return {
|
|
153
231
|
workflow: "workflow",
|
|
154
232
|
nodePath: "workflow.step",
|
|
155
|
-
emit: (
|
|
156
|
-
events.push(event);
|
|
157
|
-
},
|
|
233
|
+
emit: () => {},
|
|
158
234
|
interaction: {
|
|
159
235
|
present: async () => {
|
|
160
236
|
throw new Error("unexpected interaction");
|
|
@@ -162,7 +238,6 @@ function providerContext(
|
|
|
162
238
|
},
|
|
163
239
|
local: {
|
|
164
240
|
open: async () => {},
|
|
165
|
-
...local,
|
|
166
241
|
},
|
|
167
242
|
metadata: () => {},
|
|
168
243
|
};
|