-
Notifications
You must be signed in to change notification settings - Fork 449
Add typed JSON output to hydrogen deploy #4098
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,18 @@ | ||
| import {Flags} from '@oclif/core'; | ||
| import Command from '@shopify/cli-kit/node/base-command'; | ||
| import colors from '@shopify/cli-kit/node/colors'; | ||
| import {writeJsonResult, isJsonOutput} from '../../lib/json-output.js'; | ||
| import { | ||
| flushStdout, | ||
| outputContent, | ||
| outputInfo, | ||
| outputWarn, | ||
| Logger, | ||
| LogLevel, | ||
| } from '@shopify/cli-kit/node/output'; | ||
| import {emitCommandEvent} from '@shopify/cli-kit/node/command-events'; | ||
| import {jsonFlag} from '@shopify/cli-kit/node/cli'; | ||
| import {deployJsonOutputSchema} from '../../lib/deployment/types.js'; | ||
| import {Flags} from '@oclif/core'; | ||
| import Command from '@shopify/cli-kit/node/base-command'; | ||
| import colors from '@shopify/cli-kit/node/colors'; | ||
| import {type PackageJson} from '@shopify/cli-kit/node/node-package-manager'; | ||
| import {readAndParseDotEnv} from '@shopify/cli-kit/node/dot-env'; | ||
| import {AbortError} from '@shopify/cli-kit/node/error'; | ||
|
|
@@ -73,9 +78,14 @@ export const deploymentLogger: Logger = ( | |
| }; | ||
|
|
||
| export default class Deploy extends Command { | ||
| static get jsonOutputSchema(): typeof deployJsonOutputSchema { | ||
| return deployJsonOutputSchema; | ||
| } | ||
|
|
||
| static descriptionWithMarkdown = `Builds and deploys your Hydrogen storefront to Oxygen. Requires an Oxygen deployment token to be set with the \`--token\` flag or an environment variable (\`SHOPIFY_HYDROGEN_DEPLOYMENT_TOKEN\`). If the storefront is [linked](https://shopify.dev/docs/api/shopify-cli/hydrogen/hydrogen-link) then the Oxygen deployment token for the linked storefront will be used automatically.`; | ||
| static description = 'Builds and deploys a Hydrogen storefront to Oxygen.'; | ||
| static flags: any = { | ||
| static description = this.descriptionForHelp(); | ||
| static flags = { | ||
| ...jsonFlag, | ||
| ...commonFlags.entry, | ||
| ...commonFlags.env, | ||
| ...commonFlags.envBranch, | ||
|
|
@@ -190,6 +200,7 @@ export default class Deploy extends Command { | |
| // The Remix compiler hangs due to a bug in ESBuild: | ||
| // https://github.com/evanw/esbuild/issues/2727 | ||
| // The actual build has already finished so we can kill the process | ||
| await flushStdout(); | ||
| process.exit(0); | ||
| } | ||
|
|
||
|
|
@@ -207,6 +218,7 @@ export default class Deploy extends Command { | |
| } | ||
|
|
||
| interface OxygenDeploymentOptions { | ||
| json?: boolean; | ||
| authBypassTokenDuration?: string; | ||
| authBypassToken: boolean; | ||
| buildCommand?: string; | ||
|
|
@@ -255,9 +267,60 @@ function createUnexpectedAbortError(message?: string): AbortError { | |
| ); | ||
| } | ||
|
|
||
| export async function runDeploy( | ||
| export async function runDeploy(options: OxygenDeploymentOptions) { | ||
| const completedDeployment = await executeDeploy(options); | ||
| const isCI = ciPlatform().isCI; | ||
| const {jsonOutput} = options; | ||
| if (!completedDeployment) { | ||
| writeJsonResult(deployJsonOutputSchema, null, options.json); | ||
| return; | ||
| } | ||
| if (isCI && jsonOutput) { | ||
| await writeFile( | ||
| DEPLOY_OUTPUT_FILE_HANDLE, | ||
| JSON.stringify(completedDeployment), | ||
| ); | ||
| } | ||
| if ( | ||
| !writeJsonResult(deployJsonOutputSchema, completedDeployment, options.json) | ||
| ) { | ||
| const nextSteps: (string | {subdued: string} | {link: {url: string}})[][] = | ||
| []; | ||
|
|
||
| if (isCI) { | ||
| if (jsonOutput) { | ||
| nextSteps.push([ | ||
| 'View the deployment information in', | ||
| {subdued: DEPLOY_OUTPUT_FILE_HANDLE}, | ||
| ]); | ||
| } | ||
| } else { | ||
| nextSteps.push([ | ||
| 'Open', | ||
| {link: {url: completedDeployment!.url}}, | ||
| 'in your browser to view your deployment.', | ||
| ]); | ||
|
|
||
| if (completedDeployment?.authBypassToken) { | ||
| nextSteps.push([ | ||
| 'Use the', | ||
| {subdued: completedDeployment.authBypassToken}, | ||
| 'token to perform end-to-end tests against the deployment.', | ||
| ]); | ||
| } | ||
| } | ||
|
|
||
| renderSuccess({ | ||
| body: ['Successfully deployed to Oxygen'], | ||
| nextSteps, | ||
| }); | ||
| } | ||
| return completedDeployment; | ||
| } | ||
|
|
||
| export async function executeDeploy( | ||
| options: OxygenDeploymentOptions, | ||
| ): Promise<void> { | ||
| ): Promise<CompletedDeployment | undefined> { | ||
| const { | ||
| authBypassTokenDuration, | ||
| authBypassToken: generateAuthBypassToken, | ||
|
|
@@ -585,9 +648,9 @@ Continue?`.value, | |
|
|
||
| let deployError: AbortError | null = null; | ||
| let buildError: Error | null = null; | ||
| let resolveDeploy: () => void; | ||
| let resolveDeploy: (result: CompletedDeployment) => void; | ||
| let rejectDeploy: (reason?: AbortError) => void; | ||
| const deployPromise = new Promise<void>((resolve, reject) => { | ||
| const deployPromise = new Promise<CompletedDeployment>((resolve, reject) => { | ||
| resolveDeploy = resolve; | ||
| rejectDeploy = reject; | ||
| }); | ||
|
|
@@ -622,14 +685,43 @@ Continue?`.value, | |
|
|
||
| if (buildCommand || shouldUseDefaultBuildCommand) { | ||
| if (forceClientSourcemap) { | ||
| console.log(''); | ||
| if (!isJsonOutput()) console.log(''); | ||
| renderInfo({ | ||
| headline: | ||
| 'The `--force-client-sourcemap` flag is not supported with a custom build command', | ||
| body: 'Client sourcemaps will not be generated.', | ||
| }); | ||
| } | ||
| config.buildCommand = buildCommand ?? DEFAULT_BUILD_COMMAND; | ||
| if (isJsonOutput()) { | ||
| hooks.buildFunction = async (assetPath) => { | ||
| try { | ||
| const {stdout, stderr} = await execAsync(config.buildCommand!, { | ||
| cwd: root, | ||
| env: { | ||
| ...process.env, | ||
| ...(assetPath ? {HYDROGEN_ASSET_BASE_URL: assetPath} : {}), | ||
| }, | ||
| maxBuffer: 64 * 1024 * 1024, | ||
| }); | ||
| if (stdout.trim()) | ||
| emitCommandEvent({ | ||
| type: 'diagnostic', | ||
| level: 'info', | ||
| message: stdout.trim(), | ||
| }); | ||
| if (stderr.trim()) | ||
| emitCommandEvent({ | ||
| type: 'diagnostic', | ||
| level: 'warning', | ||
| message: stderr.trim(), | ||
| }); | ||
| } catch (error) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. non-blocking: when the build fails we drop whatever it printed. Some smaller tradeoffs vs oxygen-cli's own runner, which already pipes all build output to stderr, so stdout was never at risk. Fine if these are deliberate:
|
||
| buildError = error as Error; | ||
| throw error; | ||
| } | ||
| }; | ||
| } | ||
| } else { | ||
| hooks.buildFunction = async ( | ||
| assetPath: string | undefined, | ||
|
|
@@ -688,47 +780,7 @@ Continue?`.value, | |
| return; | ||
| } | ||
|
|
||
| const nextSteps: ( | ||
| | string | ||
| | {subdued: string} | ||
| | {link: {url: string}} | ||
| )[][] = []; | ||
|
|
||
| if (isCI) { | ||
| if (jsonOutput) { | ||
| nextSteps.push([ | ||
| 'View the deployment information in', | ||
| {subdued: DEPLOY_OUTPUT_FILE_HANDLE}, | ||
| ]); | ||
| } | ||
| } else { | ||
| nextSteps.push([ | ||
| 'Open', | ||
| {link: {url: completedDeployment!.url}}, | ||
| 'in your browser to view your deployment.', | ||
| ]); | ||
|
|
||
| if (completedDeployment?.authBypassToken) { | ||
| nextSteps.push([ | ||
| 'Use the', | ||
| {subdued: completedDeployment.authBypassToken}, | ||
| 'token to perform end-to-end tests against the deployment.', | ||
| ]); | ||
| } | ||
| } | ||
|
|
||
| renderSuccess({ | ||
| body: ['Successfully deployed to Oxygen'], | ||
| nextSteps, | ||
| }); | ||
| // in CI environments, output to a file so consequent steps can access the deployment details | ||
| if (isCI && jsonOutput) { | ||
| await writeFile( | ||
| DEPLOY_OUTPUT_FILE_HANDLE, | ||
| JSON.stringify(completedDeployment), | ||
| ); | ||
| } | ||
| resolveDeploy(); | ||
| resolveDeploy(completedDeployment); | ||
| }) | ||
| .catch((error) => { | ||
| rejectDeploy(deployError || buildError || error); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import { | ||
| defineJsonOutputSchema, | ||
| type InferJsonOutputSchema, | ||
| } from '@shopify/cli-kit/node/json-output-schema'; | ||
| import {zod} from '@shopify/cli-kit/node/schema'; | ||
|
|
||
| const Deployment = zod.object({ | ||
| url: zod.string(), | ||
| authBypassToken: zod.string().optional(), | ||
| }); | ||
| export const deployJsonOutputSchema = defineJsonOutputSchema({ | ||
| name: 'HydrogenDeployResult', | ||
| schema: Deployment.nullable(), | ||
| definitions: {Deployment}, | ||
| }); | ||
| export type DeployResult = InferJsonOutputSchema<typeof deployJsonOutputSchema>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
non-blocking: the JSON-mode
buildFunctionfor--build-commandis the main new behaviour in this PR, but nothing tests it. Let's add a test that runs withbuildCommand+json: trueand checks the build output ends up as diagnostic events rather than on stdout. The newisJsonOutput()throws ingetOxygenDeploymentDataandrenderMissingStorefrontcould use a quick test each too (get-oxygen-deployment-data.test.tsalready exists).