Skip to content

Commit 50aa77a

Browse files
verbanicmsethvargo
andauthored
feat: Enable machine parsable output with --format json (#283)
* feat: enable machine parsable output with format json Co-authored-by: Seth Vargo <[email protected]>
1 parent 91a24f8 commit 50aa77a

File tree

6 files changed

+838
-226
lines changed

6 files changed

+838
-226
lines changed

package-lock.json

Lines changed: 18 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"@actions/core": "^1.6.0",
3333
"@actions/exec": "^1.1.0",
3434
"@actions/tool-cache": "^1.7.1",
35+
"@google-github-actions/actions-utils": "^0.1.2",
3536
"@google-github-actions/setup-cloud-sdk": "^0.3.1"
3637
},
3738
"devDependencies": {
@@ -45,9 +46,9 @@
4546
"@typescript-eslint/parser": "^5.7.0",
4647
"@zeit/ncc": "^0.22.3",
4748
"chai": "^4.3.4",
49+
"eslint": "^8.4.1",
4850
"eslint-config-prettier": "^8.3.0",
4951
"eslint-plugin-prettier": "^4.0.0",
50-
"eslint": "^8.4.1",
5152
"google-auth-library": "^7.10.4",
5253
"googleapis": "^92.0.0",
5354
"lodash": "^4.17.21",

src/deploy-cloudrun.ts

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,26 @@ import { getExecOutput } from '@actions/exec';
1919
import * as toolCache from '@actions/tool-cache';
2020
import * as setupGcloud from '@google-github-actions/setup-cloud-sdk';
2121
import path from 'path';
22+
import { parseDeployResponse, parseUpdateTrafficResponse } from './output-parser';
2223

2324
export const GCLOUD_METRICS_ENV_VAR = 'CLOUDSDK_METRICS_ENVIRONMENT';
2425
export const GCLOUD_METRICS_LABEL = 'github-actions-deploy-cloudrun';
2526

27+
/**
28+
* DeployCloudRunOutputs are the common GitHub action outputs created by this action
29+
*/
30+
export interface DeployCloudRunOutputs {
31+
url?: string | null | undefined; // Type required to match run_v1.Schema$Service.status.url
32+
}
33+
34+
/**
35+
* ResponseTypes are the gcloud command response formats
36+
*/
37+
enum ResponseTypes {
38+
DEPLOY,
39+
UPDATE_TRAFFIC,
40+
}
41+
2642
/**
2743
* Executes the main action. It includes the main business logic and is the
2844
* primary entry point. It is documented inline.
@@ -59,8 +75,10 @@ export async function run(): Promise<void> {
5975
);
6076
}
6177

78+
let responseType = ResponseTypes.DEPLOY; // Default response type for output parsing
6279
let installBeta = false; // Flag for installing gcloud beta components
6380
let cmd;
81+
6482
// Throw errors if inputs aren't valid
6583
if (revTraffic && tagTraffic) {
6684
throw new Error('Both `revision_traffic` and `tag_traffic` inputs set - Please select one.');
@@ -71,6 +89,9 @@ export async function run(): Promise<void> {
7189

7290
// Find base command
7391
if (revTraffic || tagTraffic) {
92+
// Set response type for output parsing
93+
responseType = ResponseTypes.UPDATE_TRAFFIC;
94+
7495
// Update traffic
7596
cmd = [
7697
'run',
@@ -181,6 +202,9 @@ export async function run(): Promise<void> {
181202
cmd.unshift('beta');
182203
}
183204

205+
// Set output format to json
206+
cmd.push('--format', 'json');
207+
184208
const toolCommand = setupGcloud.getToolCommand();
185209
const options = { silent: true };
186210
const commandString = `${toolCommand} ${cmd.join(' ')}`;
@@ -193,25 +217,24 @@ export async function run(): Promise<void> {
193217
throw new Error(`failed to execute gcloud command \`${commandString}\`: ${errMsg}`);
194218
}
195219

196-
// gcloud outputs status information to stderr.
197-
// TODO: update this to use JSON or YAML machine-readable output instead.
198-
setUrlOutput(output.stdout + output.stderr);
220+
// Map outputs by response type
221+
const outputs: DeployCloudRunOutputs =
222+
responseType === ResponseTypes.UPDATE_TRAFFIC
223+
? parseUpdateTrafficResponse(output.stdout)
224+
: parseDeployResponse(output.stdout, { tag: tag });
225+
226+
// Map outputs to GitHub actions output
227+
setActionOutputs(outputs);
199228
} catch (error) {
200229
core.setFailed(convertUnknown(error));
201230
}
202231
}
203232

204-
export function setUrlOutput(output: string): string | undefined {
205-
// regex to match Cloud Run URLs
206-
const urlMatch = output.match(/https:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.app/g);
207-
if (!urlMatch) {
208-
core.warning('Can not find URL.');
209-
return undefined;
210-
}
211-
// Match "tagged" URL or default to service URL
212-
const url = urlMatch!.length > 1 ? urlMatch![1] : urlMatch![0];
213-
core.setOutput('url', url);
214-
return url;
233+
// Map output response to GitHub Action outputs
234+
export function setActionOutputs(outputs: DeployCloudRunOutputs): void {
235+
Object.keys(outputs).forEach((key: string) => {
236+
core.setOutput(key, outputs[key as keyof DeployCloudRunOutputs]);
237+
});
215238
}
216239

217240
export function parseFlags(flags: string): RegExpMatchArray {

src/output-parser.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* Copyright 2022 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { DeployCloudRunOutputs } from './deploy-cloudrun';
18+
import { run_v1 } from 'googleapis';
19+
import { errorMessage, presence } from '@google-github-actions/actions-utils';
20+
21+
/**
22+
* ParseInputs are the input values from GitHub actions used for parsing logic
23+
*/
24+
export interface ParseInputs {
25+
[key: string]: string | boolean;
26+
}
27+
28+
/**
29+
* UpdateTrafficItem is the response type for the gcloud run services update-traffic command
30+
*/
31+
interface UpdateTrafficItem {
32+
displayPercent: string;
33+
displayRevisionId: string;
34+
displayTags: string;
35+
key: string;
36+
latestRevision: boolean;
37+
revisionName: string;
38+
serviceUrl: string;
39+
specPercent: string;
40+
specTags: string;
41+
statusPercent: string;
42+
statusTags: string;
43+
tags: string[];
44+
urls: string[];
45+
}
46+
47+
/**
48+
* parseUpdateTrafficResponse parses the gcloud command response for update-traffic
49+
* into a common DeployCloudRunOutputs object
50+
*
51+
* @param stdout
52+
* @returns DeployCloudRunOutputs
53+
*/
54+
export function parseUpdateTrafficResponse(stdout: string | undefined): DeployCloudRunOutputs {
55+
try {
56+
stdout = presence(stdout);
57+
if (!stdout || stdout === '{}' || stdout === '[]') {
58+
throw new Error(`invalid input received`);
59+
}
60+
61+
const outputJSON: UpdateTrafficItem[] = JSON.parse(stdout);
62+
63+
// Default to service url
64+
const responseItem = outputJSON[0];
65+
let url = responseItem?.serviceUrl;
66+
67+
// Maintain current logic to use first tag URL if present
68+
for (const item of outputJSON) {
69+
if (item?.urls?.length) {
70+
url = item.urls[0];
71+
break;
72+
}
73+
}
74+
75+
const outputs: DeployCloudRunOutputs = { url: url };
76+
77+
return outputs;
78+
} catch (err) {
79+
const msg = errorMessage(err);
80+
throw new Error(`failed to parse update traffic response: ${msg}, stdout: ${stdout}`);
81+
}
82+
}
83+
84+
/**
85+
* parseDeployResponse parses the gcloud command response for gcloud run deploy/replace
86+
* into a common DeployCloudRunOutputs object
87+
*
88+
* @param stdout Standard output from gcloud command
89+
* @param inputs Action inputs used in parsing logic
90+
* @returns DeployCloudRunOutputs
91+
*/
92+
export function parseDeployResponse(
93+
stdout: string | undefined,
94+
inputs: ParseInputs | undefined,
95+
): DeployCloudRunOutputs {
96+
try {
97+
stdout = presence(stdout);
98+
if (!stdout || stdout === '{}' || stdout === '[]') {
99+
throw new Error(`invalid input received`);
100+
}
101+
102+
const outputJSON: run_v1.Schema$Service = JSON.parse(stdout);
103+
104+
// Set outputs
105+
const outputs: DeployCloudRunOutputs = {
106+
url: outputJSON?.status?.url,
107+
};
108+
109+
// Maintain current logic to use tag url if provided
110+
if (inputs?.tag) {
111+
const tagInfo = outputJSON?.status?.traffic?.find((t) => t.tag === inputs.tag);
112+
if (tagInfo) {
113+
outputs.url = tagInfo.url;
114+
}
115+
}
116+
117+
return outputs;
118+
} catch (err) {
119+
const msg = errorMessage(err);
120+
throw new Error(
121+
`failed to parse deploy response: ${msg}, stdout: ${stdout}, inputs: ${JSON.stringify(
122+
inputs,
123+
)}`,
124+
);
125+
}
126+
}

0 commit comments

Comments
 (0)