@shopify/oxygen-cli 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. package/README.md +91 -0
  2. package/dist/commands/oxygen/deploy.d.ts +26 -0
  3. package/dist/commands/oxygen/deploy.js +121 -0
  4. package/dist/deploy/build-cancel.d.ts +6 -0
  5. package/dist/deploy/build-cancel.js +36 -0
  6. package/dist/deploy/build-cancel.test.d.ts +2 -0
  7. package/dist/deploy/build-cancel.test.js +73 -0
  8. package/dist/deploy/build-initiate.d.ts +6 -0
  9. package/dist/deploy/build-initiate.js +38 -0
  10. package/dist/deploy/build-initiate.test.d.ts +2 -0
  11. package/dist/deploy/build-initiate.test.js +81 -0
  12. package/dist/deploy/build-project.d.ts +5 -0
  13. package/dist/deploy/build-project.js +32 -0
  14. package/dist/deploy/build-project.test.d.ts +2 -0
  15. package/dist/deploy/build-project.test.js +53 -0
  16. package/dist/deploy/deployment-cancel.d.ts +6 -0
  17. package/dist/deploy/deployment-cancel.js +36 -0
  18. package/dist/deploy/deployment-cancel.test.d.ts +2 -0
  19. package/dist/deploy/deployment-cancel.test.js +78 -0
  20. package/dist/deploy/deployment-complete.d.ts +6 -0
  21. package/dist/deploy/deployment-complete.js +33 -0
  22. package/dist/deploy/deployment-complete.test.d.ts +2 -0
  23. package/dist/deploy/deployment-complete.test.js +77 -0
  24. package/dist/deploy/deployment-initiate.d.ts +17 -0
  25. package/dist/deploy/deployment-initiate.js +40 -0
  26. package/dist/deploy/deployment-initiate.test.d.ts +2 -0
  27. package/dist/deploy/deployment-initiate.test.js +136 -0
  28. package/dist/deploy/get-upload-files.d.ts +5 -0
  29. package/dist/deploy/get-upload-files.js +65 -0
  30. package/dist/deploy/get-upload-files.test.d.ts +2 -0
  31. package/dist/deploy/get-upload-files.test.js +56 -0
  32. package/dist/deploy/graphql/build-cancel.d.ts +14 -0
  33. package/dist/deploy/graphql/build-cancel.js +14 -0
  34. package/dist/deploy/graphql/build-initiate.d.ts +15 -0
  35. package/dist/deploy/graphql/build-initiate.js +15 -0
  36. package/dist/deploy/graphql/deployment-cancel.d.ts +14 -0
  37. package/dist/deploy/graphql/deployment-cancel.js +14 -0
  38. package/dist/deploy/graphql/deployment-complete.d.ts +17 -0
  39. package/dist/deploy/graphql/deployment-complete.js +16 -0
  40. package/dist/deploy/graphql/deployment-initiate.d.ts +28 -0
  41. package/dist/deploy/graphql/deployment-initiate.js +25 -0
  42. package/dist/deploy/index.d.ts +5 -0
  43. package/dist/deploy/index.js +74 -0
  44. package/dist/deploy/metadata.d.ts +11 -0
  45. package/dist/deploy/metadata.js +65 -0
  46. package/dist/deploy/metadata.test.d.ts +2 -0
  47. package/dist/deploy/metadata.test.js +131 -0
  48. package/dist/deploy/types.d.ts +52 -0
  49. package/dist/deploy/types.js +7 -0
  50. package/dist/deploy/upload-files.d.ts +6 -0
  51. package/dist/deploy/upload-files.js +156 -0
  52. package/dist/deploy/upload-files.test.d.ts +2 -0
  53. package/dist/deploy/upload-files.test.js +194 -0
  54. package/dist/index.d.ts +3 -0
  55. package/dist/index.js +11 -0
  56. package/dist/oxygen-cli.d.ts +1 -0
  57. package/dist/oxygen-cli.js +5 -0
  58. package/dist/utils/test-helper.d.ts +14 -0
  59. package/dist/utils/test-helper.js +27 -0
  60. package/dist/utils/utils.d.ts +20 -0
  61. package/dist/utils/utils.js +126 -0
  62. package/dist/utils/utils.test.d.ts +2 -0
  63. package/dist/utils/utils.test.js +154 -0
  64. package/oclif.manifest.json +108 -0
  65. 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,6 @@
1
+ import { DeployConfig } from './types.js';
2
+ import { BuildCancelResponse } from './graphql/build-cancel.js';
3
+
4
+ declare function buildCancel(config: DeployConfig, buildId: string, reason: string): Promise<BuildCancelResponse>;
5
+
6
+ export { buildCancel };
@@ -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,2 @@
1
+
2
+ export { }
@@ -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,2 @@
1
+
2
+ export { }
@@ -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,5 @@
1
+ import { DeployConfig } from './types.js';
2
+
3
+ declare function buildProject(config: DeployConfig, assetPath?: string): Promise<void>;
4
+
5
+ export { buildProject };
@@ -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,2 @@
1
+
2
+ export { }
@@ -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 };
@@ -0,0 +1,2 @@
1
+
2
+ export { }