@shopify/oxygen-cli 1.5.0 → 1.7.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/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": {
|