@shopify/oxygen-cli 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +91 -0
- package/dist/commands/oxygen/deploy.d.ts +26 -0
- package/dist/commands/oxygen/deploy.js +121 -0
- package/dist/deploy/build-cancel.d.ts +6 -0
- package/dist/deploy/build-cancel.js +36 -0
- package/dist/deploy/build-cancel.test.d.ts +2 -0
- package/dist/deploy/build-cancel.test.js +73 -0
- package/dist/deploy/build-initiate.d.ts +6 -0
- package/dist/deploy/build-initiate.js +38 -0
- package/dist/deploy/build-initiate.test.d.ts +2 -0
- package/dist/deploy/build-initiate.test.js +81 -0
- package/dist/deploy/build-project.d.ts +5 -0
- package/dist/deploy/build-project.js +32 -0
- package/dist/deploy/build-project.test.d.ts +2 -0
- package/dist/deploy/build-project.test.js +53 -0
- package/dist/deploy/deployment-cancel.d.ts +6 -0
- package/dist/deploy/deployment-cancel.js +36 -0
- package/dist/deploy/deployment-cancel.test.d.ts +2 -0
- package/dist/deploy/deployment-cancel.test.js +78 -0
- package/dist/deploy/deployment-complete.d.ts +6 -0
- package/dist/deploy/deployment-complete.js +33 -0
- package/dist/deploy/deployment-complete.test.d.ts +2 -0
- package/dist/deploy/deployment-complete.test.js +77 -0
- package/dist/deploy/deployment-initiate.d.ts +17 -0
- package/dist/deploy/deployment-initiate.js +40 -0
- package/dist/deploy/deployment-initiate.test.d.ts +2 -0
- package/dist/deploy/deployment-initiate.test.js +136 -0
- package/dist/deploy/get-upload-files.d.ts +5 -0
- package/dist/deploy/get-upload-files.js +65 -0
- package/dist/deploy/get-upload-files.test.d.ts +2 -0
- package/dist/deploy/get-upload-files.test.js +56 -0
- package/dist/deploy/graphql/build-cancel.d.ts +14 -0
- package/dist/deploy/graphql/build-cancel.js +14 -0
- package/dist/deploy/graphql/build-initiate.d.ts +15 -0
- package/dist/deploy/graphql/build-initiate.js +15 -0
- package/dist/deploy/graphql/deployment-cancel.d.ts +14 -0
- package/dist/deploy/graphql/deployment-cancel.js +14 -0
- package/dist/deploy/graphql/deployment-complete.d.ts +17 -0
- package/dist/deploy/graphql/deployment-complete.js +16 -0
- package/dist/deploy/graphql/deployment-initiate.d.ts +28 -0
- package/dist/deploy/graphql/deployment-initiate.js +25 -0
- package/dist/deploy/index.d.ts +5 -0
- package/dist/deploy/index.js +74 -0
- package/dist/deploy/metadata.d.ts +11 -0
- package/dist/deploy/metadata.js +65 -0
- package/dist/deploy/metadata.test.d.ts +2 -0
- package/dist/deploy/metadata.test.js +131 -0
- package/dist/deploy/types.d.ts +52 -0
- package/dist/deploy/types.js +7 -0
- package/dist/deploy/upload-files.d.ts +6 -0
- package/dist/deploy/upload-files.js +156 -0
- package/dist/deploy/upload-files.test.d.ts +2 -0
- package/dist/deploy/upload-files.test.js +194 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +11 -0
- package/dist/oxygen-cli.d.ts +1 -0
- package/dist/oxygen-cli.js +5 -0
- package/dist/utils/test-helper.d.ts +14 -0
- package/dist/utils/test-helper.js +27 -0
- package/dist/utils/utils.d.ts +20 -0
- package/dist/utils/utils.js +126 -0
- package/dist/utils/utils.test.d.ts +2 -0
- package/dist/utils/utils.test.js +154 -0
- package/oclif.manifest.json +108 -0
- package/package.json +68 -0
package/README.md
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
# Oxygen-cli
|
2
|
+
|
3
|
+
`@shopify/oxygen-cli` is a command-line tool for deploying your Oxygen projects. It provides a simple a way to deploy your Oxygen applications, including workers and assets, to the Oxygen platform.
|
4
|
+
|
5
|
+
## Getting Started
|
6
|
+
|
7
|
+
The oxygen-cli can be run locally, or it can be used from within CI/CD environments such as Github Actions.
|
8
|
+
|
9
|
+
The common route to launch oxygen-cli's `deploy` command locally is through [hydrogen-cli](https://www.npmjs.com/package/@shopify/hydrogen-cli). When this is installed globally, the command is available via the `h2 deploy`.
|
10
|
+
|
11
|
+
You can also install oxygen-cli as plugin for [@shopify/cli](https://www.npmjs.com/package/@shopify/cli). When this is installed globally, running the following will install oxygen-cli:
|
12
|
+
|
13
|
+
```
|
14
|
+
shopify plugins install @shopify/oxygen-cli
|
15
|
+
```
|
16
|
+
|
17
|
+
The CLI will now expose its commands under `shopify oxygen`.
|
18
|
+
|
19
|
+
Finally, it is also possible to install the oxygen-cli independently, which is preferable when run from within a CI/CD environment.
|
20
|
+
|
21
|
+
```
|
22
|
+
npm install -g @shopify/oxygen-cli
|
23
|
+
```
|
24
|
+
|
25
|
+
The CLI can then be activated using the command `oxygen-cli`.
|
26
|
+
|
27
|
+
## Command overview
|
28
|
+
|
29
|
+
- `oxygen deploy` command initiates a deployment to Oxygen.
|
30
|
+
|
31
|
+
```
|
32
|
+
oxygen:deploy [options]
|
33
|
+
```
|
34
|
+
|
35
|
+
### Options
|
36
|
+
|
37
|
+
- -t, --token <token>: (required) Oxygen deployment token. Can also be set using the `OXYGEN_DEPLOYMENT_TOKEN` environment variable (see below).
|
38
|
+
- -r, --rootPath <rootPath>: Root path (defaults to the current working directory).
|
39
|
+
- -e, --environmentTag <environmentTag>: Tag of the environment to deploy to. Defaults to branch name in supported CI environments (see below).
|
40
|
+
- -w, --workerFolder <workerFolder>: Worker folder (default: `dist/worker/`).
|
41
|
+
- -a, --assetsFolder <assetsFolder>: Assets folder (default: `dist/client/`).
|
42
|
+
- -o, --workerOnly: Worker only deployment.
|
43
|
+
- -s, --skipBuild: Skip running build command.
|
44
|
+
- -b, --buildCommand <buildCommand>: Build command (default: `yarn build`).
|
45
|
+
- --metadataUrl <metadataUrl>: URL that links to the deployment.
|
46
|
+
- --metadataUser <metadataUser>: User that initiated the deployment.
|
47
|
+
- --metadataVersion <metadataVersion>: A version identifier for the deployment.
|
48
|
+
|
49
|
+
**Note**: All metadata options (--metadataUrl, --metadataUser, and --metadataVersion) have a maximum character limit of 90.
|
50
|
+
|
51
|
+
### Example:
|
52
|
+
|
53
|
+
```
|
54
|
+
oxygen:deploy -t my-deployment-token -e staging --rootPath="/my-project" --workerOnly
|
55
|
+
```
|
56
|
+
|
57
|
+
This command will deploy your Oxygen project to the staging environment with the provided deployment token and root path. No static assets will be uploaded.
|
58
|
+
|
59
|
+
The `environmentTag` option and the `metadata` options serve as overrides for values that can be retrieved from supported CI environments, see the section on environment variables below.
|
60
|
+
|
61
|
+
## Environment variables
|
62
|
+
|
63
|
+
The Oxygen deployment token can be specified with the `token` option. Alternatively, you can use the environment variable `OXYGEN_DEPLOYMENT_TOKEN` for this. In CI environments this is recommended. For example, when using CircleCI you can define a custom environment variable in your project settings which holds the token. The `OXYGEN_DEPLOYMENT_TOKEN` can then be populated with this variable's value in your `config.yml` file:
|
64
|
+
|
65
|
+
```
|
66
|
+
environment:
|
67
|
+
OXYGEN_DEPLOYMENT_TOKEN: ${YOUR_ENV_VARIABLE}
|
68
|
+
```
|
69
|
+
|
70
|
+
When run from a supported CI/CD environment, the tool will automatically retrieve metadata from relevant environment variables. Metadata describes the author making the deployment (user), the version (commit Sha) and the deployment URL (workflow URL).
|
71
|
+
|
72
|
+
The following environment variables are relevant, depending on your environment:
|
73
|
+
|
74
|
+
|- | Bitbucket | CircleCI | Github | Gitlab |
|
75
|
+
|- | --------- | --------| ------ | ------ |
|
76
|
+
| User | BITBUCKET_COMMIT_AUTHOR | CIRCLE_USERNAME | GITHUB_ACTOR | GITLAB_USER_LOGIN |
|
77
|
+
| Version | BITBUCKET_COMMIT | CIRCLE_SHA1 | GITHUB_SHA | CI_COMMIT_SHA |
|
78
|
+
| URL | BITBUCKET_BUILD_URL | CIRCLE_BUILD_URL | `${GITHUB_SERVER_URL}${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}` | CI_PROJECT_URL |
|
79
|
+
|
80
|
+
These values can be overruled by the command line flags `metadataUser`, `metadataUrl` and `metadataVersion` respectively.
|
81
|
+
|
82
|
+
The tag of the environment to deploy to, will be derived from the following variables:
|
83
|
+
|
84
|
+
| Service | Branch |
|
85
|
+
| ------- | ------ |
|
86
|
+
| Bitbucket | BITBUCKET_BRANCH |
|
87
|
+
| CircleCI | CIRCLE_BRANCH |
|
88
|
+
| Github | GITHUB_REF_NAME |
|
89
|
+
| Gitlab | CI_COMMIT_REF_NAME |
|
90
|
+
|
91
|
+
The `environmentTag` command line option can be used to override this, and is required when the tool is run from outside a supported CI environment.
|
@@ -0,0 +1,26 @@
|
|
1
|
+
import * as _oclif_core_lib_interfaces_parser_js from '@oclif/core/lib/interfaces/parser.js';
|
2
|
+
import { Command } from '@oclif/core';
|
3
|
+
import { DeployConfig } from '../../deploy/types.js';
|
4
|
+
|
5
|
+
declare class Deploy extends Command {
|
6
|
+
static description: string;
|
7
|
+
static hidden: boolean;
|
8
|
+
static flags: {
|
9
|
+
token: _oclif_core_lib_interfaces_parser_js.OptionFlag<string, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
10
|
+
rootPath: _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
|
+
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
|
+
buildCommand: _oclif_core_lib_interfaces_parser_js.OptionFlag<string, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
17
|
+
metadataUrl: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
18
|
+
metadataUser: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
19
|
+
metadataVersion: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
20
|
+
};
|
21
|
+
static hasCustomBuildCommand: boolean;
|
22
|
+
run(): Promise<void>;
|
23
|
+
}
|
24
|
+
declare function runInit(options: DeployConfig): void;
|
25
|
+
|
26
|
+
export { Deploy, runInit };
|
@@ -0,0 +1,121 @@
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
2
|
+
import { consoleError } from '@shopify/cli-kit/node/output';
|
3
|
+
import { normalizePath } from '@shopify/cli-kit/node/path';
|
4
|
+
import { createDeploy } from '../../deploy/index.js';
|
5
|
+
import { deployDefaults, parseToken, verifyConfig, getBuildCommandFromLockFile } from '../../utils/utils.js';
|
6
|
+
|
7
|
+
class Deploy extends Command {
|
8
|
+
static description = "Creates a deployment to Oxygen";
|
9
|
+
static hidden = false;
|
10
|
+
static flags = {
|
11
|
+
token: Flags.string({
|
12
|
+
char: "t",
|
13
|
+
description: "Oxygen deployment token",
|
14
|
+
env: "OXYGEN_DEPLOYMENT_TOKEN",
|
15
|
+
required: true
|
16
|
+
}),
|
17
|
+
rootPath: Flags.string({
|
18
|
+
char: "r",
|
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
|
+
assetsFolder: Flags.string({
|
35
|
+
char: "a",
|
36
|
+
description: "Assets folder",
|
37
|
+
default: String(deployDefaults.assetsDirDefault),
|
38
|
+
required: false
|
39
|
+
}),
|
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
|
+
buildCommand: Flags.string({
|
53
|
+
char: "b",
|
54
|
+
description: "Build command",
|
55
|
+
required: false,
|
56
|
+
default: String(deployDefaults.buildCommandDefault),
|
57
|
+
parse: (input) => {
|
58
|
+
this.hasCustomBuildCommand = true;
|
59
|
+
return Promise.resolve(input);
|
60
|
+
}
|
61
|
+
}),
|
62
|
+
metadataUrl: Flags.string({
|
63
|
+
description: "URL that links to the deployment",
|
64
|
+
required: false,
|
65
|
+
env: "OXYGEN_METADATA_URL"
|
66
|
+
}),
|
67
|
+
metadataUser: Flags.string({
|
68
|
+
description: "User that initiated the deployment",
|
69
|
+
required: false,
|
70
|
+
env: "OXYGEN_METADATA_USER"
|
71
|
+
}),
|
72
|
+
metadataVersion: Flags.string({
|
73
|
+
description: "A version identifier for the deployment",
|
74
|
+
required: false,
|
75
|
+
env: "OXYGEN_METADATA_VERSION"
|
76
|
+
})
|
77
|
+
};
|
78
|
+
static hasCustomBuildCommand = false;
|
79
|
+
async run() {
|
80
|
+
try {
|
81
|
+
const { flags } = await this.parse(Deploy);
|
82
|
+
const deploymentUrl = (
|
83
|
+
// eslint-disable-next-line no-process-env
|
84
|
+
process.env.UNSAFE_OXYGEN_DEPLOYMENT_URL || "https://oxygen.shopifyapps.com"
|
85
|
+
);
|
86
|
+
const config = {
|
87
|
+
assetsDir: normalizePath(flags.assetsFolder),
|
88
|
+
buildCommand: flags.buildCommand,
|
89
|
+
deploymentToken: parseToken(flags.token),
|
90
|
+
environmentTag: flags.environmentTag,
|
91
|
+
deploymentUrl,
|
92
|
+
metadata: {
|
93
|
+
url: flags.metadataUrl,
|
94
|
+
user: flags.metadataUser,
|
95
|
+
version: flags.metadataVersion
|
96
|
+
},
|
97
|
+
rootPath: normalizePath(flags.rootPath),
|
98
|
+
skipBuild: flags.skipBuild,
|
99
|
+
workerDir: normalizePath(flags.workerFolder),
|
100
|
+
workerOnly: flags.workerOnly
|
101
|
+
};
|
102
|
+
await verifyConfig({ config });
|
103
|
+
if (!Deploy.hasCustomBuildCommand && !config.skipBuild) {
|
104
|
+
config.buildCommand = getBuildCommandFromLockFile(config);
|
105
|
+
}
|
106
|
+
runInit(config);
|
107
|
+
} catch (error) {
|
108
|
+
if (error instanceof Error) {
|
109
|
+
consoleError(error.message);
|
110
|
+
} else {
|
111
|
+
console.error(error);
|
112
|
+
}
|
113
|
+
this.exit(1);
|
114
|
+
}
|
115
|
+
}
|
116
|
+
}
|
117
|
+
function runInit(options) {
|
118
|
+
createDeploy(options);
|
119
|
+
}
|
120
|
+
|
121
|
+
export { Deploy, runInit };
|
@@ -0,0 +1,36 @@
|
|
1
|
+
import { graphqlRequest } from '@shopify/cli-kit/node/api/graphql';
|
2
|
+
import { AbortError } from '@shopify/cli-kit/node/error';
|
3
|
+
import { outputInfo } from '@shopify/cli-kit/node/output';
|
4
|
+
import { Header, errorHandler } from '../utils/utils.js';
|
5
|
+
import { BuildCancelQuery } from './graphql/build-cancel.js';
|
6
|
+
|
7
|
+
async function buildCancel(config, buildId, reason) {
|
8
|
+
const variables = {
|
9
|
+
buildId,
|
10
|
+
reason
|
11
|
+
};
|
12
|
+
try {
|
13
|
+
const response = await graphqlRequest({
|
14
|
+
query: BuildCancelQuery,
|
15
|
+
api: "Oxygen",
|
16
|
+
url: `${config.deploymentUrl}/api/v2/admin/graphql`,
|
17
|
+
token: config.deploymentToken.accessToken,
|
18
|
+
addedHeaders: {
|
19
|
+
[Header.OxygenNamespaceHandle]: config.deploymentToken.namespace
|
20
|
+
},
|
21
|
+
variables
|
22
|
+
});
|
23
|
+
if (response.buildCancel.userErrors.length >= 1) {
|
24
|
+
throw new AbortError(
|
25
|
+
`Failed to cancel build: ${response.buildCancel.userErrors[0]?.message}`
|
26
|
+
);
|
27
|
+
}
|
28
|
+
outputInfo(`Build with id ${buildId} cancelled.`);
|
29
|
+
return response.buildCancel;
|
30
|
+
} catch (error) {
|
31
|
+
errorHandler(error);
|
32
|
+
throw error;
|
33
|
+
}
|
34
|
+
}
|
35
|
+
|
36
|
+
export { buildCancel };
|
@@ -0,0 +1,73 @@
|
|
1
|
+
import { AbortError } from '@shopify/cli-kit/node/error';
|
2
|
+
import { graphqlRequest } from '@shopify/cli-kit/node/api/graphql';
|
3
|
+
import { vi, describe, test, expect } from 'vitest';
|
4
|
+
import { createTestConfig } from '../utils/test-helper.js';
|
5
|
+
import { Header } from '../utils/utils.js';
|
6
|
+
import { buildCancel } from './build-cancel.js';
|
7
|
+
|
8
|
+
vi.mock("@shopify/cli-kit/node/api/graphql");
|
9
|
+
vi.mock("@shopify/cli-kit/node/output", () => {
|
10
|
+
return {
|
11
|
+
outputInfo: vi.fn()
|
12
|
+
};
|
13
|
+
});
|
14
|
+
const testConfig = createTestConfig("/tmp/deploymentRoot/");
|
15
|
+
describe("BuildCancel", () => {
|
16
|
+
test("should cancel a build", async () => {
|
17
|
+
const response = {
|
18
|
+
buildCancel: {
|
19
|
+
userErrors: []
|
20
|
+
}
|
21
|
+
};
|
22
|
+
vi.mocked(graphqlRequest).mockResolvedValueOnce(response);
|
23
|
+
const cancelResponse = await buildCancel(testConfig, "build-1", "because");
|
24
|
+
expect(cancelResponse).toEqual(response.buildCancel);
|
25
|
+
expect(graphqlRequest).toHaveBeenCalledWith({
|
26
|
+
query: expect.any(String),
|
27
|
+
api: "Oxygen",
|
28
|
+
url: `${testConfig.deploymentUrl}/api/v2/admin/graphql`,
|
29
|
+
token: testConfig.deploymentToken.accessToken,
|
30
|
+
variables: {
|
31
|
+
buildId: "build-1",
|
32
|
+
reason: "because"
|
33
|
+
},
|
34
|
+
addedHeaders: {
|
35
|
+
[Header.OxygenNamespaceHandle]: `${testConfig.deploymentToken.namespace}`
|
36
|
+
}
|
37
|
+
});
|
38
|
+
});
|
39
|
+
test("should throw AbortError when buildCancel fails with error", async () => {
|
40
|
+
const response = {
|
41
|
+
buildCancel: {
|
42
|
+
userErrors: [
|
43
|
+
{
|
44
|
+
message: "Cannot cancel build."
|
45
|
+
}
|
46
|
+
]
|
47
|
+
}
|
48
|
+
};
|
49
|
+
vi.mocked(graphqlRequest).mockResolvedValueOnce(response);
|
50
|
+
await expect(buildCancel(testConfig, "build-1", "because")).rejects.toThrow(
|
51
|
+
new AbortError(
|
52
|
+
`Failed to cancel build: ${response.buildCancel.userErrors[0]?.message}`
|
53
|
+
)
|
54
|
+
);
|
55
|
+
});
|
56
|
+
test("should throw AbortError when unauthorized", async () => {
|
57
|
+
const error = {
|
58
|
+
statusCode: 401
|
59
|
+
};
|
60
|
+
vi.mocked(graphqlRequest).mockRejectedValueOnce(error);
|
61
|
+
try {
|
62
|
+
await expect(
|
63
|
+
buildCancel(testConfig, "build-1", "because")
|
64
|
+
).rejects.toThrow(
|
65
|
+
new AbortError(
|
66
|
+
"You are not authorized to perform this action. Please check your deployment token."
|
67
|
+
)
|
68
|
+
);
|
69
|
+
} catch (err) {
|
70
|
+
expect(error).toBeInstanceOf(AbortError);
|
71
|
+
}
|
72
|
+
});
|
73
|
+
});
|
@@ -0,0 +1,6 @@
|
|
1
|
+
import { DeployConfig, EnvironmentInput } from './types.js';
|
2
|
+
import { BuildInitiateResponse } from './graphql/build-initiate.js';
|
3
|
+
|
4
|
+
declare function buildInitiate(config: DeployConfig, environment?: EnvironmentInput, labels?: string[]): Promise<BuildInitiateResponse>;
|
5
|
+
|
6
|
+
export { buildInitiate };
|
@@ -0,0 +1,38 @@
|
|
1
|
+
import { graphqlRequest } from '@shopify/cli-kit/node/api/graphql';
|
2
|
+
import { AbortError } from '@shopify/cli-kit/node/error';
|
3
|
+
import { outputCompleted } from '@shopify/cli-kit/node/output';
|
4
|
+
import { Header, errorHandler } from '../utils/utils.js';
|
5
|
+
import { BuildInitiateQuery } from './graphql/build-initiate.js';
|
6
|
+
|
7
|
+
async function buildInitiate(config, environment, labels = []) {
|
8
|
+
const variables = {
|
9
|
+
environment,
|
10
|
+
labels
|
11
|
+
};
|
12
|
+
try {
|
13
|
+
const response = await graphqlRequest({
|
14
|
+
query: BuildInitiateQuery,
|
15
|
+
api: "Oxygen",
|
16
|
+
url: `${config.deploymentUrl}/api/v2/admin/graphql`,
|
17
|
+
token: config.deploymentToken.accessToken,
|
18
|
+
variables,
|
19
|
+
addedHeaders: {
|
20
|
+
[Header.OxygenNamespaceHandle]: config.deploymentToken.namespace
|
21
|
+
}
|
22
|
+
});
|
23
|
+
if (response.buildInitiate.userErrors.length >= 1) {
|
24
|
+
throw new AbortError(
|
25
|
+
`Failed to create build. ${response.buildInitiate.userErrors[0]?.message}`
|
26
|
+
);
|
27
|
+
}
|
28
|
+
outputCompleted(
|
29
|
+
`Build initiated successfully with id ${response.buildInitiate.build.id}.`
|
30
|
+
);
|
31
|
+
return response.buildInitiate;
|
32
|
+
} catch (error) {
|
33
|
+
errorHandler(error);
|
34
|
+
throw error;
|
35
|
+
}
|
36
|
+
}
|
37
|
+
|
38
|
+
export { buildInitiate };
|
@@ -0,0 +1,81 @@
|
|
1
|
+
import { AbortError } from '@shopify/cli-kit/node/error';
|
2
|
+
import { graphqlRequest } from '@shopify/cli-kit/node/api/graphql';
|
3
|
+
import { outputCompleted } from '@shopify/cli-kit/node/output';
|
4
|
+
import { vi, describe, test, expect } from 'vitest';
|
5
|
+
import { Header } from '../utils/utils.js';
|
6
|
+
import { createTestConfig } from '../utils/test-helper.js';
|
7
|
+
import { buildInitiate } from './build-initiate.js';
|
8
|
+
|
9
|
+
vi.mock("@shopify/cli-kit/node/api/graphql");
|
10
|
+
vi.mock("@shopify/cli-kit/node/output");
|
11
|
+
const testConfig = createTestConfig("/tmp/deploymentRoot/");
|
12
|
+
describe("BuildInitiate", () => {
|
13
|
+
test("should initiate a build", async () => {
|
14
|
+
const response = {
|
15
|
+
buildInitiate: {
|
16
|
+
build: {
|
17
|
+
id: "build-1"
|
18
|
+
},
|
19
|
+
userErrors: []
|
20
|
+
}
|
21
|
+
};
|
22
|
+
vi.mocked(graphqlRequest).mockResolvedValueOnce(response);
|
23
|
+
const initiateResponse = await buildInitiate(
|
24
|
+
testConfig,
|
25
|
+
{
|
26
|
+
tag: testConfig.environmentTag
|
27
|
+
},
|
28
|
+
[]
|
29
|
+
);
|
30
|
+
expect(initiateResponse).toEqual(response.buildInitiate);
|
31
|
+
expect(graphqlRequest).toHaveBeenCalledWith({
|
32
|
+
query: expect.any(String),
|
33
|
+
api: "Oxygen",
|
34
|
+
url: `${testConfig.deploymentUrl}/api/v2/admin/graphql`,
|
35
|
+
token: testConfig.deploymentToken.accessToken,
|
36
|
+
variables: { environment: { tag: testConfig.environmentTag }, labels: [] },
|
37
|
+
addedHeaders: {
|
38
|
+
[Header.OxygenNamespaceHandle]: `${testConfig.deploymentToken.namespace}`
|
39
|
+
}
|
40
|
+
});
|
41
|
+
expect(outputCompleted).toHaveBeenCalledWith(
|
42
|
+
"Build initiated successfully with id build-1."
|
43
|
+
);
|
44
|
+
});
|
45
|
+
test("should throw AbortError when build initiation fails due to user errors", async () => {
|
46
|
+
const response = {
|
47
|
+
buildInitiate: {
|
48
|
+
userErrors: [
|
49
|
+
{
|
50
|
+
message: "Error: cannot proceed with build."
|
51
|
+
}
|
52
|
+
]
|
53
|
+
}
|
54
|
+
};
|
55
|
+
vi.mocked(graphqlRequest).mockResolvedValueOnce(response);
|
56
|
+
await expect(
|
57
|
+
buildInitiate(testConfig, { tag: "preview" }, [])
|
58
|
+
).rejects.toThrow(
|
59
|
+
new AbortError(
|
60
|
+
`Failed to create build. ${response.buildInitiate.userErrors[0]?.message}`
|
61
|
+
)
|
62
|
+
);
|
63
|
+
});
|
64
|
+
test("should throw AbortError when unauthorized", async () => {
|
65
|
+
const error = {
|
66
|
+
statusCode: 401
|
67
|
+
};
|
68
|
+
vi.mocked(graphqlRequest).mockRejectedValueOnce(error);
|
69
|
+
try {
|
70
|
+
await expect(
|
71
|
+
buildInitiate(testConfig, { tag: "preview" }, [])
|
72
|
+
).rejects.toThrow(
|
73
|
+
new AbortError(
|
74
|
+
"You are not authorized to perform this action. Please check your deployment token."
|
75
|
+
)
|
76
|
+
);
|
77
|
+
} catch (err) {
|
78
|
+
expect(error).toBeInstanceOf(AbortError);
|
79
|
+
}
|
80
|
+
});
|
81
|
+
});
|
@@ -0,0 +1,32 @@
|
|
1
|
+
import { spawn } from 'child_process';
|
2
|
+
|
3
|
+
async function buildProject(config, assetPath) {
|
4
|
+
const assetPathEnvironment = assetPath ? { HYDROGEN_ASSET_BASE_URL: assetPath } : {};
|
5
|
+
await new Promise((resolve, reject) => {
|
6
|
+
const buildCommand = spawn(config.buildCommand, [], {
|
7
|
+
stdio: "inherit",
|
8
|
+
env: {
|
9
|
+
// eslint-disable-next-line no-process-env
|
10
|
+
...process.env,
|
11
|
+
...assetPathEnvironment
|
12
|
+
},
|
13
|
+
cwd: config.rootPath,
|
14
|
+
shell: true
|
15
|
+
});
|
16
|
+
if (buildCommand.stdout) {
|
17
|
+
buildCommand.stdout.on("data", (data) => {
|
18
|
+
console.log(`stdout: ${data}`);
|
19
|
+
});
|
20
|
+
}
|
21
|
+
buildCommand.on("close", (code) => {
|
22
|
+
if (code !== 0) {
|
23
|
+
reject(code);
|
24
|
+
}
|
25
|
+
resolve(code);
|
26
|
+
});
|
27
|
+
}).catch((error) => {
|
28
|
+
throw new Error(`Build failed with error code: ${error}`);
|
29
|
+
});
|
30
|
+
}
|
31
|
+
|
32
|
+
export { buildProject };
|
@@ -0,0 +1,53 @@
|
|
1
|
+
import { spawn } from 'child_process';
|
2
|
+
import { vi, test, expect } from 'vitest';
|
3
|
+
import { createTestConfig } from '../utils/test-helper.js';
|
4
|
+
import { buildProject } from './build-project.js';
|
5
|
+
|
6
|
+
let returnCode = 0;
|
7
|
+
vi.mock("child_process", () => {
|
8
|
+
const spawn2 = vi.fn();
|
9
|
+
spawn2.mockImplementation(() => {
|
10
|
+
return {
|
11
|
+
stdout: {
|
12
|
+
on: () => {
|
13
|
+
}
|
14
|
+
},
|
15
|
+
on: (event, callback) => {
|
16
|
+
if (event === "close") {
|
17
|
+
return callback(returnCode);
|
18
|
+
}
|
19
|
+
}
|
20
|
+
};
|
21
|
+
});
|
22
|
+
return { spawn: spawn2 };
|
23
|
+
});
|
24
|
+
const testConfig = createTestConfig("rootFolder");
|
25
|
+
test("BuildProject builds the project successfully", async () => {
|
26
|
+
const config = {
|
27
|
+
...testConfig,
|
28
|
+
buildCommand: "npm run build"
|
29
|
+
};
|
30
|
+
const assetPath = "https://example.com/assets";
|
31
|
+
await buildProject(config, assetPath);
|
32
|
+
expect(spawn).toBeCalledWith("npm run build", [], {
|
33
|
+
cwd: "rootFolder",
|
34
|
+
shell: true,
|
35
|
+
stdio: "inherit",
|
36
|
+
env: {
|
37
|
+
// eslint-disable-next-line no-process-env
|
38
|
+
...process.env,
|
39
|
+
HYDROGEN_ASSET_BASE_URL: "https://example.com/assets"
|
40
|
+
}
|
41
|
+
});
|
42
|
+
});
|
43
|
+
test("should throw error on build command failure", async () => {
|
44
|
+
returnCode = 1;
|
45
|
+
({
|
46
|
+
...testConfig,
|
47
|
+
buildCommand: "yarn build"
|
48
|
+
});
|
49
|
+
const assetPath = "https://example.com/assets";
|
50
|
+
await expect(() => buildProject(testConfig, assetPath)).rejects.toThrow(
|
51
|
+
"Build failed with error code: 1"
|
52
|
+
);
|
53
|
+
});
|
@@ -0,0 +1,6 @@
|
|
1
|
+
import { DeployConfig } from './types.js';
|
2
|
+
import { DeploymentCancelResponse } from './graphql/deployment-cancel.js';
|
3
|
+
|
4
|
+
declare function deploymentCancel(config: DeployConfig, deploymentId: string, reason: string): Promise<DeploymentCancelResponse>;
|
5
|
+
|
6
|
+
export { deploymentCancel };
|
@@ -0,0 +1,36 @@
|
|
1
|
+
import { graphqlRequest } from '@shopify/cli-kit/node/api/graphql';
|
2
|
+
import { AbortError } from '@shopify/cli-kit/node/error';
|
3
|
+
import { outputInfo } from '@shopify/cli-kit/node/output';
|
4
|
+
import { Header, errorHandler } from '../utils/utils.js';
|
5
|
+
import { DeploymentCancelQuery } from './graphql/deployment-cancel.js';
|
6
|
+
|
7
|
+
async function deploymentCancel(config, deploymentId, reason) {
|
8
|
+
const variables = {
|
9
|
+
deploymentId,
|
10
|
+
reason
|
11
|
+
};
|
12
|
+
try {
|
13
|
+
const response = await graphqlRequest({
|
14
|
+
query: DeploymentCancelQuery,
|
15
|
+
api: "Oxygen",
|
16
|
+
url: `${config.deploymentUrl}/api/v2/admin/graphql`,
|
17
|
+
token: config.deploymentToken.accessToken,
|
18
|
+
variables,
|
19
|
+
addedHeaders: {
|
20
|
+
[Header.OxygenNamespaceHandle]: config.deploymentToken.namespace
|
21
|
+
}
|
22
|
+
});
|
23
|
+
if (response.deploymentCancel.userErrors.length >= 1) {
|
24
|
+
throw new AbortError(
|
25
|
+
`Failed to cancel deployment: ${response.deploymentCancel.userErrors[0]?.message}`
|
26
|
+
);
|
27
|
+
}
|
28
|
+
outputInfo(`Deployment with id ${deploymentId} cancelled.`);
|
29
|
+
return response.deploymentCancel;
|
30
|
+
} catch (error) {
|
31
|
+
errorHandler(error);
|
32
|
+
throw error;
|
33
|
+
}
|
34
|
+
}
|
35
|
+
|
36
|
+
export { deploymentCancel };
|