@shopify/oxygen-cli 1.5.0 → 1.7.0
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +2 -0
- package/dist/commands/oxygen/deploy.d.ts +9 -9
- package/dist/commands/oxygen/deploy.js +57 -44
- package/dist/deploy/build-project.js +9 -13
- package/dist/deploy/build-project.test.js +20 -13
- package/dist/deploy/health-check.d.ts +12 -0
- package/dist/deploy/health-check.js +44 -0
- package/dist/deploy/health-check.test.d.ts +2 -0
- package/dist/deploy/health-check.test.js +92 -0
- package/dist/deploy/index.js +20 -4
- package/dist/deploy/types.d.ts +10 -6
- package/dist/deploy/types.js +3 -1
- package/dist/utils/test-helper.js +2 -1
- package/dist/utils/utils.js +1 -0
- package/oclif.manifest.json +51 -34
- package/package.json +7 -7
package/README.md
CHANGED
@@ -42,6 +42,8 @@ oxygen:deploy [options]
|
|
42
42
|
- -o, --workerOnly: Worker only deployment.
|
43
43
|
- -s, --skipBuild: Skip running build command.
|
44
44
|
- -b, --buildCommand <buildCommand>: Build command (default: `yarn build`).
|
45
|
+
- -h, --skipHealthCheck: Skip running the health check on the deployment
|
46
|
+
- -d, --healthCheckMaxDuration: The maximum duration (in seconds) that the health check is allowed to run before it is considered failed. Accepts values between 10 and 300.
|
45
47
|
- --publicDeployment: set the deployment to be publicly accessible.
|
46
48
|
- --metadataUrl <metadataUrl>: URL that links to the deployment.
|
47
49
|
- --metadataUser <metadataUser>: User that initiated the deployment.
|
@@ -1,20 +1,21 @@
|
|
1
1
|
import * as _oclif_core_lib_interfaces_parser_js from '@oclif/core/lib/interfaces/parser.js';
|
2
2
|
import { Command } from '@oclif/core';
|
3
|
-
import { DeploymentConfig } from '../../deploy/types.js';
|
4
3
|
|
5
4
|
declare class Deploy extends Command {
|
6
5
|
static description: string;
|
7
6
|
static hidden: boolean;
|
8
7
|
static flags: {
|
9
|
-
token: _oclif_core_lib_interfaces_parser_js.OptionFlag<string, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
10
|
-
path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
11
|
-
environmentTag: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
12
|
-
workerFolder: _oclif_core_lib_interfaces_parser_js.OptionFlag<string, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
13
8
|
assetsFolder: _oclif_core_lib_interfaces_parser_js.OptionFlag<string, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
14
|
-
workerOnly: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
|
15
|
-
skipBuild: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
|
16
9
|
buildCommand: _oclif_core_lib_interfaces_parser_js.OptionFlag<string, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
10
|
+
environmentTag: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
11
|
+
healthCheckMaxDuration: _oclif_core_lib_interfaces_parser_js.OptionFlag<number, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
12
|
+
path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
17
13
|
publicDeployment: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
|
14
|
+
skipBuild: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
|
15
|
+
skipHealthCheck: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
|
16
|
+
token: _oclif_core_lib_interfaces_parser_js.OptionFlag<string, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
17
|
+
workerFolder: _oclif_core_lib_interfaces_parser_js.OptionFlag<string, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
18
|
+
workerOnly: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
|
18
19
|
metadataUrl: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
19
20
|
metadataUser: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
20
21
|
metadataVersion: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
@@ -22,6 +23,5 @@ declare class Deploy extends Command {
|
|
22
23
|
static hasCustomBuildCommand: boolean;
|
23
24
|
run(): Promise<void>;
|
24
25
|
}
|
25
|
-
declare function runInit(config: DeploymentConfig): void;
|
26
26
|
|
27
|
-
export { Deploy
|
27
|
+
export { Deploy };
|
@@ -3,52 +3,18 @@ import { consoleError } from '@shopify/cli-kit/node/output';
|
|
3
3
|
import { normalizePath } from '@shopify/cli-kit/node/path';
|
4
4
|
import { createDeploy } from '../../deploy/index.js';
|
5
5
|
import { deployDefaults, parseToken, verifyConfig, getBuildCommandFromLockFile } from '../../utils/utils.js';
|
6
|
+
import { HealthCheckError } from '../../deploy/types.js';
|
6
7
|
|
7
8
|
class Deploy extends Command {
|
8
9
|
static description = "Creates a deployment to Oxygen";
|
9
10
|
static hidden = false;
|
10
11
|
static flags = {
|
11
|
-
token: Flags.string({
|
12
|
-
char: "t",
|
13
|
-
description: "Oxygen deployment token",
|
14
|
-
env: "OXYGEN_DEPLOYMENT_TOKEN",
|
15
|
-
required: true
|
16
|
-
}),
|
17
|
-
path: Flags.string({
|
18
|
-
char: "p",
|
19
|
-
description: "Root path",
|
20
|
-
default: "./",
|
21
|
-
required: false
|
22
|
-
}),
|
23
|
-
environmentTag: Flags.string({
|
24
|
-
char: "e",
|
25
|
-
description: "Tag of the environment to deploy to",
|
26
|
-
required: false
|
27
|
-
}),
|
28
|
-
workerFolder: Flags.string({
|
29
|
-
char: "w",
|
30
|
-
description: "Worker folder",
|
31
|
-
default: String(deployDefaults.workerDirDefault),
|
32
|
-
required: false
|
33
|
-
}),
|
34
12
|
assetsFolder: Flags.string({
|
35
13
|
char: "a",
|
36
14
|
description: "Assets folder",
|
37
15
|
default: String(deployDefaults.assetsDirDefault),
|
38
16
|
required: false
|
39
17
|
}),
|
40
|
-
workerOnly: Flags.boolean({
|
41
|
-
char: "o",
|
42
|
-
description: "Worker only deployment",
|
43
|
-
default: false,
|
44
|
-
required: false
|
45
|
-
}),
|
46
|
-
skipBuild: Flags.boolean({
|
47
|
-
char: "s",
|
48
|
-
description: "Skip running build command",
|
49
|
-
required: false,
|
50
|
-
default: false
|
51
|
-
}),
|
52
18
|
buildCommand: Flags.string({
|
53
19
|
char: "b",
|
54
20
|
description: "Build command",
|
@@ -59,12 +25,61 @@ class Deploy extends Command {
|
|
59
25
|
return Promise.resolve(input);
|
60
26
|
}
|
61
27
|
}),
|
28
|
+
environmentTag: Flags.string({
|
29
|
+
char: "e",
|
30
|
+
description: "Tag of the environment to deploy to",
|
31
|
+
required: false
|
32
|
+
}),
|
33
|
+
healthCheckMaxDuration: Flags.integer({
|
34
|
+
char: "d",
|
35
|
+
description: "the maximum duration (in seconds) that the health check is allowed to run before it is considered failed.",
|
36
|
+
min: 10,
|
37
|
+
max: 300,
|
38
|
+
required: false,
|
39
|
+
default: deployDefaults.healthCheckMaxDurationDefault
|
40
|
+
}),
|
41
|
+
path: Flags.string({
|
42
|
+
char: "p",
|
43
|
+
description: "Root path",
|
44
|
+
default: "./",
|
45
|
+
required: false
|
46
|
+
}),
|
62
47
|
publicDeployment: Flags.boolean({
|
63
48
|
env: "OXYGEN_PUBLIC_DEPLOYMENT",
|
64
49
|
description: "Marks a preview deployment as publicly accessible.",
|
65
50
|
required: false,
|
66
51
|
default: false
|
67
52
|
}),
|
53
|
+
skipBuild: Flags.boolean({
|
54
|
+
char: "s",
|
55
|
+
description: "Skip running build command",
|
56
|
+
required: false,
|
57
|
+
default: false
|
58
|
+
}),
|
59
|
+
skipHealthCheck: Flags.boolean({
|
60
|
+
char: "h",
|
61
|
+
description: "Skip running deployment health check",
|
62
|
+
required: false,
|
63
|
+
default: false
|
64
|
+
}),
|
65
|
+
token: Flags.string({
|
66
|
+
char: "t",
|
67
|
+
description: "Oxygen deployment token",
|
68
|
+
env: "OXYGEN_DEPLOYMENT_TOKEN",
|
69
|
+
required: true
|
70
|
+
}),
|
71
|
+
workerFolder: Flags.string({
|
72
|
+
char: "w",
|
73
|
+
description: "Worker folder",
|
74
|
+
default: String(deployDefaults.workerDirDefault),
|
75
|
+
required: false
|
76
|
+
}),
|
77
|
+
workerOnly: Flags.boolean({
|
78
|
+
char: "o",
|
79
|
+
description: "Worker only deployment",
|
80
|
+
default: false,
|
81
|
+
required: false
|
82
|
+
}),
|
68
83
|
metadataUrl: Flags.string({
|
69
84
|
description: "URL that links to the deployment. Will be saved and displayed in the Shopify admin",
|
70
85
|
required: false,
|
@@ -92,10 +107,10 @@ class Deploy extends Command {
|
|
92
107
|
const config = {
|
93
108
|
assetsDir: normalizePath(flags.assetsFolder),
|
94
109
|
buildCommand: flags.buildCommand,
|
95
|
-
buildOutput: true,
|
96
110
|
deploymentToken: parseToken(flags.token),
|
97
111
|
environmentTag: flags.environmentTag,
|
98
112
|
deploymentUrl,
|
113
|
+
healthCheckMaxDuration: flags.healthCheckMaxDuration,
|
99
114
|
metadata: {
|
100
115
|
url: flags.metadataUrl,
|
101
116
|
user: flags.metadataUser,
|
@@ -104,6 +119,7 @@ class Deploy extends Command {
|
|
104
119
|
publicDeployment: flags.publicDeployment,
|
105
120
|
rootPath: normalizePath(flags.path),
|
106
121
|
skipBuild: flags.skipBuild,
|
122
|
+
skipHealthCheck: flags.skipHealthCheck,
|
107
123
|
workerDir: normalizePath(flags.workerFolder),
|
108
124
|
workerOnly: flags.workerOnly
|
109
125
|
};
|
@@ -111,19 +127,16 @@ class Deploy extends Command {
|
|
111
127
|
if (!Deploy.hasCustomBuildCommand && !config.skipBuild) {
|
112
128
|
config.buildCommand = getBuildCommandFromLockFile(config);
|
113
129
|
}
|
114
|
-
|
130
|
+
await createDeploy({ config });
|
115
131
|
} catch (error) {
|
116
|
-
if (error instanceof Error) {
|
132
|
+
if (!(error instanceof Error)) {
|
133
|
+
consoleError(error);
|
134
|
+
} else if (!(error instanceof HealthCheckError)) {
|
117
135
|
consoleError(error.message);
|
118
|
-
} else {
|
119
|
-
console.error(error);
|
120
136
|
}
|
121
137
|
this.exit(1);
|
122
138
|
}
|
123
139
|
}
|
124
140
|
}
|
125
|
-
function runInit(config) {
|
126
|
-
createDeploy({ config });
|
127
|
-
}
|
128
141
|
|
129
|
-
export { Deploy
|
142
|
+
export { Deploy };
|
@@ -2,12 +2,19 @@ import { spawn } from 'child_process';
|
|
2
2
|
|
3
3
|
async function buildProject(options) {
|
4
4
|
const { config, assetPath, hooks } = options;
|
5
|
-
hooks?.
|
5
|
+
if (hooks?.buildFunction) {
|
6
|
+
try {
|
7
|
+
await hooks.buildFunction(assetPath);
|
8
|
+
} catch (error) {
|
9
|
+
throw new Error(`Build function failed with error: ${error}`);
|
10
|
+
}
|
11
|
+
return;
|
12
|
+
}
|
6
13
|
const assetPathEnvironment = assetPath ? { HYDROGEN_ASSET_BASE_URL: assetPath } : {};
|
7
14
|
try {
|
8
15
|
await new Promise((resolve, reject) => {
|
9
16
|
const buildCommand = spawn(config.buildCommand, [], {
|
10
|
-
stdio:
|
17
|
+
stdio: ["inherit", process.stderr, "inherit"],
|
11
18
|
env: {
|
12
19
|
// eslint-disable-next-line no-process-env
|
13
20
|
...process.env,
|
@@ -16,22 +23,11 @@ async function buildProject(options) {
|
|
16
23
|
cwd: config.rootPath,
|
17
24
|
shell: true
|
18
25
|
});
|
19
|
-
let stderrOutput = "";
|
20
|
-
if (buildCommand.stderr) {
|
21
|
-
buildCommand.stderr.on("data", (data) => {
|
22
|
-
stderrOutput += data;
|
23
|
-
});
|
24
|
-
}
|
25
|
-
if (config.buildOutput && buildCommand.stdout) {
|
26
|
-
buildCommand.stdout.pipe(process.stderr);
|
27
|
-
}
|
28
26
|
buildCommand.on("close", (code) => {
|
29
27
|
if (code !== 0) {
|
30
|
-
hooks?.onBuildError?.(new Error(stderrOutput));
|
31
28
|
reject(code);
|
32
29
|
return;
|
33
30
|
}
|
34
|
-
hooks?.onBuildComplete?.();
|
35
31
|
resolve(code);
|
36
32
|
});
|
37
33
|
});
|
@@ -30,32 +30,39 @@ test("BuildProject builds the project successfully", async () => {
|
|
30
30
|
buildCommand: "npm run build"
|
31
31
|
};
|
32
32
|
const assetPath = "https://example.com/assets";
|
33
|
-
|
34
|
-
onBuildStart: vi.fn(),
|
35
|
-
onBuildComplete: vi.fn()
|
36
|
-
};
|
37
|
-
await buildProject({ config, assetPath, hooks });
|
33
|
+
await buildProject({ config, assetPath });
|
38
34
|
expect(spawn).toBeCalledWith("npm run build", [], {
|
39
35
|
cwd: "rootFolder",
|
40
36
|
shell: true,
|
41
|
-
stdio: ["inherit",
|
37
|
+
stdio: ["inherit", process.stderr, "inherit"],
|
42
38
|
env: {
|
43
39
|
// eslint-disable-next-line no-process-env
|
44
40
|
...process.env,
|
45
41
|
HYDROGEN_ASSET_BASE_URL: "https://example.com/assets"
|
46
42
|
}
|
47
43
|
});
|
48
|
-
expect(hooks.onBuildStart).toBeCalled();
|
49
|
-
expect(hooks.onBuildComplete).toBeCalled();
|
50
44
|
});
|
51
45
|
test("should throw error on build command failure", async () => {
|
52
46
|
returnCode = 1;
|
53
|
-
const hooks = {
|
54
|
-
onBuildError: vi.fn()
|
55
|
-
};
|
56
47
|
const assetPath = "https://example.com/assets";
|
57
48
|
await expect(
|
58
|
-
() => buildProject({ config: testConfig, assetPath
|
49
|
+
() => buildProject({ config: testConfig, assetPath })
|
59
50
|
).rejects.toThrow("Build failed with error code: 1");
|
60
|
-
|
51
|
+
});
|
52
|
+
test("should call buildFunction hook if provided", async () => {
|
53
|
+
const buildFunction = vi.fn().mockResolvedValue(void 0);
|
54
|
+
const hooks = { buildFunction };
|
55
|
+
const assetPath = "https://example.com/assets";
|
56
|
+
await buildProject({ config: testConfig, assetPath, hooks });
|
57
|
+
expect(buildFunction).toBeCalledWith(assetPath);
|
58
|
+
});
|
59
|
+
test("should throw error if buildFunction hook fails", async () => {
|
60
|
+
const buildFunction = vi.fn().mockRejectedValue(new Error("Oops! I tripped over a cable."));
|
61
|
+
const hooks = { buildFunction };
|
62
|
+
const assetPath = "https://example.com/assets";
|
63
|
+
await expect(
|
64
|
+
() => buildProject({ config: testConfig, assetPath, hooks })
|
65
|
+
).rejects.toThrow(
|
66
|
+
"Build function failed with error: Error: Oops! I tripped over a cable."
|
67
|
+
);
|
61
68
|
});
|
@@ -0,0 +1,12 @@
|
|
1
|
+
import { Logger } from '@shopify/cli-kit/node/output';
|
2
|
+
import { DeploymentConfig, DeploymentHooks } from './types.js';
|
3
|
+
|
4
|
+
interface HealthCheckOptions {
|
5
|
+
config: DeploymentConfig;
|
6
|
+
hooks?: DeploymentHooks;
|
7
|
+
logger: Logger;
|
8
|
+
url: string;
|
9
|
+
}
|
10
|
+
declare function healthCheck(options: HealthCheckOptions): Promise<void>;
|
11
|
+
|
12
|
+
export { healthCheck };
|
@@ -0,0 +1,44 @@
|
|
1
|
+
import { fetch } from '@shopify/cli-kit/node/http';
|
2
|
+
import { outputInfo } from '@shopify/cli-kit/node/output';
|
3
|
+
import { HealthCheckError } from './types.js';
|
4
|
+
|
5
|
+
async function healthCheck(options) {
|
6
|
+
const { config, url, logger, hooks } = options;
|
7
|
+
hooks?.onHealthCheckStart?.();
|
8
|
+
outputInfo("Performing health check on the deployment...", logger);
|
9
|
+
let attempts = 0;
|
10
|
+
let delay = 0;
|
11
|
+
const startTime = Date.now();
|
12
|
+
const handleInterval = async () => {
|
13
|
+
if (attempts < 10) {
|
14
|
+
delay = 500;
|
15
|
+
} else if (attempts % 5 === 0) {
|
16
|
+
delay += 5e3;
|
17
|
+
}
|
18
|
+
const elapsedTime = (Date.now() - startTime) / 1e3;
|
19
|
+
if (elapsedTime + delay / 1e3 > config.healthCheckMaxDuration) {
|
20
|
+
const error = new HealthCheckError("Unable to verify deployment health.");
|
21
|
+
hooks?.onHealthCheckError?.(error);
|
22
|
+
throw error;
|
23
|
+
}
|
24
|
+
attempts++;
|
25
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
26
|
+
await check();
|
27
|
+
};
|
28
|
+
const check = async () => {
|
29
|
+
try {
|
30
|
+
const response = await fetch(`${url}/.oxygen/deployment`);
|
31
|
+
if (response.status === 200) {
|
32
|
+
outputInfo("Deployment health check passed", logger);
|
33
|
+
hooks?.onHealthCheckComplete?.();
|
34
|
+
return Promise.resolve();
|
35
|
+
}
|
36
|
+
await handleInterval();
|
37
|
+
} catch {
|
38
|
+
await handleInterval();
|
39
|
+
}
|
40
|
+
};
|
41
|
+
await check();
|
42
|
+
}
|
43
|
+
|
44
|
+
export { healthCheck };
|
@@ -0,0 +1,92 @@
|
|
1
|
+
import { vi, describe, beforeEach, it, expect } from 'vitest';
|
2
|
+
import { fetch } from '@shopify/cli-kit/node/http';
|
3
|
+
import { Response } from 'node-fetch';
|
4
|
+
import { stderrLogger } from '../utils/utils.js';
|
5
|
+
import { createTestConfig } from '../utils/test-helper.js';
|
6
|
+
import { HealthCheckError } from './types.js';
|
7
|
+
import { healthCheck } from './health-check.js';
|
8
|
+
|
9
|
+
vi.mock("@shopify/cli-kit/node/http", async () => {
|
10
|
+
const actual = await vi.importActual("@shopify/cli-kit/node/http");
|
11
|
+
return {
|
12
|
+
...actual,
|
13
|
+
fetch: vi.fn()
|
14
|
+
};
|
15
|
+
});
|
16
|
+
vi.mock("@shopify/cli-kit/node/output");
|
17
|
+
const testConfig = createTestConfig("rootFolder");
|
18
|
+
describe("healthCheck", () => {
|
19
|
+
let setTimeoutSpy;
|
20
|
+
const hooks = {
|
21
|
+
onHealthCheckError: vi.fn(),
|
22
|
+
onHealthCheckStart: vi.fn(),
|
23
|
+
onHealthCheckComplete: vi.fn()
|
24
|
+
};
|
25
|
+
const url = "http://example.com";
|
26
|
+
beforeEach(() => {
|
27
|
+
vi.mocked(fetch).mockReset();
|
28
|
+
setTimeoutSpy = vi.spyOn(global, "setTimeout").mockImplementation((cb, _delay) => {
|
29
|
+
cb();
|
30
|
+
return {};
|
31
|
+
});
|
32
|
+
});
|
33
|
+
it("resolves when URL is accessible", async () => {
|
34
|
+
const response = new Response();
|
35
|
+
vi.mocked(fetch).mockResolvedValueOnce(response);
|
36
|
+
await healthCheck({ config: testConfig, url, logger: stderrLogger, hooks });
|
37
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
38
|
+
expect(fetch).toHaveBeenCalledWith(`${url}/.oxygen/deployment`);
|
39
|
+
expect(hooks.onHealthCheckStart).toBeCalled();
|
40
|
+
expect(hooks.onHealthCheckComplete).toBeCalled();
|
41
|
+
});
|
42
|
+
it("repeats request until URL is accessible with progressive timeout", async () => {
|
43
|
+
const responseFail = new Response(null, { status: 404 });
|
44
|
+
const responseSuccess = new Response(null, { status: 200 });
|
45
|
+
let attempts = 0;
|
46
|
+
vi.mocked(fetch).mockImplementation(() => {
|
47
|
+
if (attempts === 30) {
|
48
|
+
return Promise.resolve(responseSuccess);
|
49
|
+
}
|
50
|
+
attempts++;
|
51
|
+
return Promise.resolve(responseFail);
|
52
|
+
});
|
53
|
+
await new Promise((resolve, reject) => {
|
54
|
+
healthCheck({ config: testConfig, url, logger: stderrLogger, hooks }).then(() => {
|
55
|
+
expect(fetch).toHaveBeenCalledTimes(attempts + 1);
|
56
|
+
expect(fetch).toHaveBeenCalledWith(`${url}/.oxygen/deployment`);
|
57
|
+
expect(setTimeoutSpy).toHaveBeenCalledTimes(attempts);
|
58
|
+
expect(hooks.onHealthCheckStart).toBeCalled();
|
59
|
+
expect(hooks.onHealthCheckComplete).toBeCalled();
|
60
|
+
let expectedDuration = 500;
|
61
|
+
setTimeoutSpy.mock.calls.forEach(
|
62
|
+
(call, index) => {
|
63
|
+
if (index >= 10 && index % 5 === 0) {
|
64
|
+
expectedDuration += 5e3;
|
65
|
+
}
|
66
|
+
expect(call[1]).toBe(expectedDuration);
|
67
|
+
}
|
68
|
+
);
|
69
|
+
resolve();
|
70
|
+
}).catch((error) => {
|
71
|
+
reject(error);
|
72
|
+
});
|
73
|
+
});
|
74
|
+
});
|
75
|
+
it("throws an error after max duration", async () => {
|
76
|
+
vi.useFakeTimers();
|
77
|
+
const responseFail = new Response(null, { status: 404 });
|
78
|
+
vi.mocked(fetch).mockResolvedValue(responseFail);
|
79
|
+
const healthCheckPromise = healthCheck({
|
80
|
+
config: testConfig,
|
81
|
+
url,
|
82
|
+
logger: stderrLogger,
|
83
|
+
hooks
|
84
|
+
});
|
85
|
+
vi.setSystemTime(Date.now() + testConfig.healthCheckMaxDuration * 1e3);
|
86
|
+
await expect(healthCheckPromise).rejects.toThrow(HealthCheckError);
|
87
|
+
expect(fetch).toHaveBeenCalledOnce();
|
88
|
+
expect(hooks.onHealthCheckStart).toBeCalled();
|
89
|
+
expect(hooks.onHealthCheckError).toBeCalled();
|
90
|
+
vi.useRealTimers();
|
91
|
+
});
|
92
|
+
});
|
package/dist/deploy/index.js
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
import { outputSuccess, outputInfo, outputWarn
|
1
|
+
import { outputSuccess, outputInfo, outputWarn } from '@shopify/cli-kit/node/output';
|
2
2
|
import { stderrLogger, verifyConfig } from '../utils/utils.js';
|
3
3
|
export { parseToken } from '../utils/utils.js';
|
4
4
|
import { buildInitiate } from './build-initiate.js';
|
@@ -6,8 +6,10 @@ import { buildCancel } from './build-cancel.js';
|
|
6
6
|
import { getUploadFiles } from './get-upload-files.js';
|
7
7
|
import { deploymentInitiate } from './deployment-initiate.js';
|
8
8
|
import { deploymentComplete } from './deployment-complete.js';
|
9
|
+
import { healthCheck } from './health-check.js';
|
9
10
|
import { deploymentCancel, DeploymentCancelReason } from './deployment-cancel.js';
|
10
11
|
import { uploadFiles } from './upload-files.js';
|
12
|
+
import { HealthCheckError } from './types.js';
|
11
13
|
import { buildProject } from './build-project.js';
|
12
14
|
import { getMetadata, createLabels, getEnvironmentInput } from './metadata.js';
|
13
15
|
|
@@ -47,11 +49,24 @@ async function createDeploy(options) {
|
|
47
49
|
input: deploymentInitiateInput,
|
48
50
|
logger
|
49
51
|
});
|
50
|
-
await uploadFiles({
|
52
|
+
await uploadFiles({
|
53
|
+
config,
|
54
|
+
targets: deployment.deploymentTargets,
|
55
|
+
hooks,
|
56
|
+
logger
|
57
|
+
});
|
51
58
|
const deploymentCompleteOp = await deploymentComplete(
|
52
59
|
config,
|
53
60
|
deployment.deployment.id
|
54
61
|
);
|
62
|
+
if (!config.skipHealthCheck) {
|
63
|
+
await healthCheck({
|
64
|
+
config,
|
65
|
+
url: deploymentCompleteOp.deployment.url,
|
66
|
+
logger,
|
67
|
+
hooks
|
68
|
+
});
|
69
|
+
}
|
55
70
|
const urlMessage = config.publicDeployment ? "Public" : "Private";
|
56
71
|
outputSuccess(
|
57
72
|
`Deployment complete.
|
@@ -67,7 +82,9 @@ ${urlMessage} preview URL: ${deploymentCompleteOp.deployment.url}`,
|
|
67
82
|
console.error("Unknown error", error);
|
68
83
|
return Promise.reject(new Error("Unknown error"));
|
69
84
|
}
|
70
|
-
if (
|
85
|
+
if (error instanceof HealthCheckError) {
|
86
|
+
outputWarn(error.message, logger);
|
87
|
+
} else if (build.id && !buildCompleted) {
|
71
88
|
outputWarn(
|
72
89
|
`Build failed with: ${error.message}, cancelling build.`,
|
73
90
|
logger
|
@@ -98,7 +115,6 @@ ${urlMessage} preview URL: ${deploymentCompleteOp.deployment.url}`,
|
|
98
115
|
}
|
99
116
|
});
|
100
117
|
}
|
101
|
-
consoleError(error.message);
|
102
118
|
return Promise.reject(error);
|
103
119
|
}
|
104
120
|
}
|
package/dist/deploy/types.d.ts
CHANGED
@@ -6,19 +6,20 @@ interface ClientError extends Error {
|
|
6
6
|
statusCode: number;
|
7
7
|
}
|
8
8
|
interface DeploymentHooks {
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
buildFunction?: (urlPath?: string) => Promise<void>;
|
10
|
+
onHealthCheckStart?: () => void;
|
11
|
+
onHealthCheckComplete?: () => void;
|
12
|
+
onHealthCheckError?: (error: Error) => void;
|
12
13
|
onUploadFilesStart?: () => void;
|
13
14
|
onUploadFilesComplete?: () => void;
|
14
15
|
}
|
15
16
|
interface DeploymentConfig {
|
16
17
|
assetsDir?: string;
|
17
|
-
buildCommand
|
18
|
-
buildOutput: boolean;
|
18
|
+
buildCommand?: string;
|
19
19
|
deploymentToken: DeploymentToken;
|
20
20
|
deploymentUrl: string;
|
21
21
|
environmentTag?: string;
|
22
|
+
healthCheckMaxDuration: number;
|
22
23
|
metadata: {
|
23
24
|
user?: string;
|
24
25
|
version?: string;
|
@@ -27,6 +28,7 @@ interface DeploymentConfig {
|
|
27
28
|
publicDeployment: boolean;
|
28
29
|
rootPath?: string;
|
29
30
|
skipBuild: boolean;
|
31
|
+
skipHealthCheck: boolean;
|
30
32
|
workerDir?: string;
|
31
33
|
workerOnly: boolean;
|
32
34
|
}
|
@@ -57,5 +59,7 @@ declare enum FileType {
|
|
57
59
|
interface OxygenError {
|
58
60
|
message: string;
|
59
61
|
}
|
62
|
+
declare class HealthCheckError extends Error {
|
63
|
+
}
|
60
64
|
|
61
|
-
export { Build, ClientError, DeploymentConfig, DeploymentHooks, DeploymentManifestFile, DeploymentToken, EnvironmentInput, FileType, OxygenError };
|
65
|
+
export { Build, ClientError, DeploymentConfig, DeploymentHooks, DeploymentManifestFile, DeploymentToken, EnvironmentInput, FileType, HealthCheckError, OxygenError };
|
package/dist/deploy/types.js
CHANGED
@@ -13,14 +13,15 @@ function createTestConfig(rootFolder) {
|
|
13
13
|
return {
|
14
14
|
assetsDir: "/assets/",
|
15
15
|
buildCommand: String(deployDefaults.buildCommandDefault),
|
16
|
-
buildOutput: true,
|
17
16
|
deploymentToken: testToken,
|
18
17
|
environmentTag: "environment",
|
19
18
|
deploymentUrl: "https://localhost:3000",
|
19
|
+
healthCheckMaxDuration: 300,
|
20
20
|
metadata: {},
|
21
21
|
rootPath: rootFolder,
|
22
22
|
publicDeployment: false,
|
23
23
|
skipBuild: false,
|
24
|
+
skipHealthCheck: false,
|
24
25
|
workerDir: "/worker/",
|
25
26
|
workerOnly: false
|
26
27
|
};
|
package/dist/utils/utils.js
CHANGED
@@ -6,6 +6,7 @@ import { AbortError } from '@shopify/cli-kit/node/error';
|
|
6
6
|
const deployDefaults = {
|
7
7
|
assetsDirDefault: "dist/client/",
|
8
8
|
buildCommandDefault: "yarn build",
|
9
|
+
healthCheckMaxDurationDefault: 180,
|
9
10
|
maxUploadAttempts: 3,
|
10
11
|
maxResumabeUploadAttempts: 9,
|
11
12
|
workerDirDefault: "dist/worker/"
|
package/oclif.manifest.json
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
{
|
2
|
-
"version": "1.
|
2
|
+
"version": "1.7.0",
|
3
3
|
"commands": {
|
4
4
|
"oxygen:deploy": {
|
5
5
|
"id": "oxygen:deploy",
|
@@ -11,22 +11,23 @@
|
|
11
11
|
"hidden": false,
|
12
12
|
"aliases": [],
|
13
13
|
"flags": {
|
14
|
-
"
|
15
|
-
"name": "
|
14
|
+
"assetsFolder": {
|
15
|
+
"name": "assetsFolder",
|
16
16
|
"type": "option",
|
17
|
-
"char": "
|
18
|
-
"description": "
|
19
|
-
"required":
|
20
|
-
"multiple": false
|
17
|
+
"char": "a",
|
18
|
+
"description": "Assets folder",
|
19
|
+
"required": false,
|
20
|
+
"multiple": false,
|
21
|
+
"default": "dist/client/"
|
21
22
|
},
|
22
|
-
"
|
23
|
-
"name": "
|
23
|
+
"buildCommand": {
|
24
|
+
"name": "buildCommand",
|
24
25
|
"type": "option",
|
25
|
-
"char": "
|
26
|
-
"description": "
|
26
|
+
"char": "b",
|
27
|
+
"description": "Build command",
|
27
28
|
"required": false,
|
28
29
|
"multiple": false,
|
29
|
-
"default": "
|
30
|
+
"default": "yarn build"
|
30
31
|
},
|
31
32
|
"environmentTag": {
|
32
33
|
"name": "environmentTag",
|
@@ -36,29 +37,28 @@
|
|
36
37
|
"required": false,
|
37
38
|
"multiple": false
|
38
39
|
},
|
39
|
-
"
|
40
|
-
"name": "
|
40
|
+
"healthCheckMaxDuration": {
|
41
|
+
"name": "healthCheckMaxDuration",
|
41
42
|
"type": "option",
|
42
|
-
"char": "
|
43
|
-
"description": "
|
43
|
+
"char": "d",
|
44
|
+
"description": "the maximum duration (in seconds) that the health check is allowed to run before it is considered failed.",
|
44
45
|
"required": false,
|
45
46
|
"multiple": false,
|
46
|
-
"default":
|
47
|
+
"default": 180
|
47
48
|
},
|
48
|
-
"
|
49
|
-
"name": "
|
49
|
+
"path": {
|
50
|
+
"name": "path",
|
50
51
|
"type": "option",
|
51
|
-
"char": "
|
52
|
-
"description": "
|
52
|
+
"char": "p",
|
53
|
+
"description": "Root path",
|
53
54
|
"required": false,
|
54
55
|
"multiple": false,
|
55
|
-
"default": "
|
56
|
+
"default": "./"
|
56
57
|
},
|
57
|
-
"
|
58
|
-
"name": "
|
58
|
+
"publicDeployment": {
|
59
|
+
"name": "publicDeployment",
|
59
60
|
"type": "boolean",
|
60
|
-
"
|
61
|
-
"description": "Worker only deployment",
|
61
|
+
"description": "Marks a preview deployment as publicly accessible.",
|
62
62
|
"required": false,
|
63
63
|
"allowNo": false
|
64
64
|
},
|
@@ -70,19 +70,36 @@
|
|
70
70
|
"required": false,
|
71
71
|
"allowNo": false
|
72
72
|
},
|
73
|
-
"
|
74
|
-
"name": "
|
73
|
+
"skipHealthCheck": {
|
74
|
+
"name": "skipHealthCheck",
|
75
|
+
"type": "boolean",
|
76
|
+
"char": "h",
|
77
|
+
"description": "Skip running deployment health check",
|
78
|
+
"required": false,
|
79
|
+
"allowNo": false
|
80
|
+
},
|
81
|
+
"token": {
|
82
|
+
"name": "token",
|
75
83
|
"type": "option",
|
76
|
-
"char": "
|
77
|
-
"description": "
|
84
|
+
"char": "t",
|
85
|
+
"description": "Oxygen deployment token",
|
86
|
+
"required": true,
|
87
|
+
"multiple": false
|
88
|
+
},
|
89
|
+
"workerFolder": {
|
90
|
+
"name": "workerFolder",
|
91
|
+
"type": "option",
|
92
|
+
"char": "w",
|
93
|
+
"description": "Worker folder",
|
78
94
|
"required": false,
|
79
95
|
"multiple": false,
|
80
|
-
"default": "
|
96
|
+
"default": "dist/worker/"
|
81
97
|
},
|
82
|
-
"
|
83
|
-
"name": "
|
98
|
+
"workerOnly": {
|
99
|
+
"name": "workerOnly",
|
84
100
|
"type": "boolean",
|
85
|
-
"
|
101
|
+
"char": "o",
|
102
|
+
"description": "Worker only deployment",
|
86
103
|
"required": false,
|
87
104
|
"allowNo": false
|
88
105
|
},
|
package/package.json
CHANGED
@@ -5,7 +5,7 @@
|
|
5
5
|
"@shopify:registry": "https://registry.npmjs.org"
|
6
6
|
},
|
7
7
|
"license": "MIT",
|
8
|
-
"version": "1.
|
8
|
+
"version": "1.7.0",
|
9
9
|
"type": "module",
|
10
10
|
"scripts": {
|
11
11
|
"build": "tsup --clean --config ./tsup.config.ts && oclif manifest",
|
@@ -31,8 +31,8 @@
|
|
31
31
|
"/oclif.manifest.json"
|
32
32
|
],
|
33
33
|
"dependencies": {
|
34
|
-
"@oclif/core": "2.
|
35
|
-
"@shopify/cli-kit": "^3.
|
34
|
+
"@oclif/core": "2.9.4",
|
35
|
+
"@shopify/cli-kit": "^3.47.5",
|
36
36
|
"async": "^3.2.4"
|
37
37
|
},
|
38
38
|
"devDependencies": {
|
@@ -40,15 +40,15 @@
|
|
40
40
|
"@shopify/eslint-plugin": "^42.1.0",
|
41
41
|
"@shopify/prettier-config": "^1.1.2",
|
42
42
|
"@types/async": "^3.2.18",
|
43
|
-
"@types/node": "^20.
|
43
|
+
"@types/node": "^20.4.4",
|
44
44
|
"@types/prettier": "^2.7.3",
|
45
|
-
"eslint": "^8.
|
45
|
+
"eslint": "^8.45.0",
|
46
46
|
"node-fetch": "^3.3.1",
|
47
47
|
"oclif": "^3",
|
48
48
|
"tsup": "^7.1.0",
|
49
49
|
"typescript": "^5.1.3",
|
50
|
-
"vite": "^4.
|
51
|
-
"vitest": "^0.
|
50
|
+
"vite": "^4.4.6",
|
51
|
+
"vitest": "^0.33.0"
|
52
52
|
},
|
53
53
|
"prettier": "@shopify/prettier-config",
|
54
54
|
"oclif": {
|