@shopify/oxygen-cli 1.0.2
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 +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 };
|