Skip to content

Commit 85a6e91

Browse files
authored
Search all files for app.yaml candidates (#308)
Fixes #303
1 parent 3c081bf commit 85a6e91

File tree

3 files changed

+202
-40
lines changed

3 files changed

+202
-40
lines changed

dist/index.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/main.ts

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
isPinnedToHead,
4646
KVPair,
4747
parseBoolean,
48+
parseCSV,
4849
parseFlags,
4950
parseKVString,
5051
pinnedToHeadWarning,
@@ -83,7 +84,7 @@ export async function run(): Promise<void> {
8384
// Get action inputs.
8485
const projectId = presence(getInput('project_id'));
8586
const cwd = presence(getInput('working_directory'));
86-
const deliverables = (presence(getInput('deliverables')) || 'app.yaml').split(' ');
87+
const deliverables = parseDeliverables(getInput('deliverables') || 'app.yaml');
8788
const buildEnvVars = parseKVString(getInput('build_env_vars'));
8889
const envVars = parseKVString(getInput('env_vars'));
8990
const imageUrl = presence(getInput('image_url'));
@@ -113,7 +114,7 @@ export async function run(): Promise<void> {
113114
.catch((err) => {
114115
const rejection =
115116
`Deliverable ${deliverable} not found or the ` +
116-
`caller does not have permission, check "working_direcotry" ` +
117+
`caller does not have permission, check "working_directory" ` +
117118
`and "deliverables" inputs: ${err}`;
118119
reject(new Error(rejection));
119120
});
@@ -128,7 +129,7 @@ export async function run(): Promise<void> {
128129
) {
129130
logDebug(`Updating env_variables or build_env_variables`);
130131

131-
originalAppYamlPath = findAppYaml(deliverables);
132+
originalAppYamlPath = await findAppYaml(deliverables);
132133
originalAppYamlContents = await fs.readFile(originalAppYamlPath, 'utf8');
133134
const parsed = YAML.parse(originalAppYamlContents);
134135

@@ -277,22 +278,31 @@ async function computeGcloudVersion(str: string): Promise<string> {
277278

278279
/**
279280
* findAppYaml finds the best app.yaml or app.yml file in the list of
280-
* deliverables. It returns a tuple of the index and the path. If no file is
281-
* found, it throws an error.
281+
* deliverables. It returns the file's path. If no file is found, it throws an
282+
* error.
282283
*
283-
* @return [number, string]
284+
* @return [string]
284285
*/
285-
export function findAppYaml(list: string[]): string {
286-
const idx = list.findIndex((item) => {
287-
return item.endsWith('app.yml') || item.endsWith('app.yaml');
288-
});
289-
290-
const pth = list[idx];
291-
if (!pth) {
292-
throw new Error(`Could not find "app.yml" file`);
286+
export async function findAppYaml(list: string[]): Promise<string> {
287+
for (let i = 0; i < list.length; i++) {
288+
const pth = list[i];
289+
290+
try {
291+
const contents = await fs.readFile(pth, 'utf8');
292+
const parsed = YAML.parse(contents);
293+
294+
// Per https://cloud.google.com/appengine/docs/standard/reference/app-yaml,
295+
// the only required fields are "runtime" and "service".
296+
if (parsed && parsed['runtime'] && parsed['service']) {
297+
return pth;
298+
}
299+
} catch (err) {
300+
const msg = errorMessage(err);
301+
logDebug(`Failed to parse ${pth} as YAML: ${msg}`);
302+
}
293303
}
294304

295-
return pth;
305+
throw new Error(`Could not find an appyaml in [${list.join(', ')}]`);
296306
}
297307

298308
/**
@@ -307,6 +317,32 @@ export function updateEnvVars(existing: KVPair, envVars: KVPair): KVPair {
307317
return Object.assign({}, existing, envVars);
308318
}
309319

320+
/**
321+
* parseDeliverables parses the given input string as a space-separated or
322+
* comma-separated list of deliverables.
323+
*
324+
* @param input The given input
325+
* @return [string[]]
326+
*/
327+
export function parseDeliverables(input: string): string[] {
328+
const onSpaces = input.split(' ');
329+
330+
const final: string[] = [];
331+
for (let i = 0; i < onSpaces.length; i++) {
332+
const entry = onSpaces[i].trim();
333+
if (entry !== '') {
334+
const entries = parseCSV(entry);
335+
for (let j = 0; j < entries.length; j++) {
336+
const csvEntry = entries[j];
337+
if (csvEntry !== '') {
338+
final.push(csvEntry);
339+
}
340+
}
341+
}
342+
}
343+
return final;
344+
}
345+
310346
// Execute this as the entrypoint when requested.
311347
if (require.main === module) {
312348
run();

tests/main.test.ts

Lines changed: 150 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,22 @@ import { expect } from 'chai';
1919
import * as sinon from 'sinon';
2020

2121
import YAML from 'yaml';
22+
import * as path from 'path';
23+
import * as fs from 'fs/promises';
2224

2325
import * as core from '@actions/core';
2426
import * as exec from '@actions/exec';
2527
import * as setupGcloud from '@google-github-actions/setup-cloud-sdk';
2628
import { TestToolCache } from '@google-github-actions/setup-cloud-sdk';
27-
import { errorMessage, KVPair } from '@google-github-actions/actions-utils';
29+
import {
30+
errorMessage,
31+
forceRemove,
32+
KVPair,
33+
randomFilepath,
34+
writeSecureFile,
35+
} from '@google-github-actions/actions-utils';
2836

29-
import { run, findAppYaml, updateEnvVars } from '../src/main';
37+
import { run, findAppYaml, updateEnvVars, parseDeliverables } from '../src/main';
3038

3139
// These are mock data for github actions inputs, where camel case is expected.
3240
const fakeInputs: { [key: string]: string } = {
@@ -198,49 +206,120 @@ describe('#run', function () {
198206
});
199207

200208
describe('#findAppYaml', () => {
209+
beforeEach(async function () {
210+
this.parent = randomFilepath();
211+
await fs.mkdir(this.parent, { recursive: true });
212+
});
213+
214+
afterEach(async function () {
215+
if (this.parent) {
216+
forceRemove(this.parent);
217+
}
218+
});
219+
201220
const cases: {
202221
only?: boolean;
203222
name: string;
204-
list: string[];
223+
files: Record<string, string>;
205224
expected?: string;
206225
error?: string;
207226
}[] = [
208227
{
209-
name: 'empty list',
210-
list: [],
211-
error: 'Could not find',
228+
name: 'no deployables',
229+
files: {},
230+
error: 'could not find an appyaml',
212231
},
213232
{
214-
name: 'non-existent',
215-
list: ['a', 'b', 'c'],
216-
error: 'Could not find',
233+
name: 'no appyaml single',
234+
files: {
235+
'my-file': `
236+
this is a file
237+
`,
238+
},
239+
error: 'could not find an appyaml',
217240
},
218241
{
219-
name: 'finds app.yml',
220-
list: ['a', 'b', 'c', 'app.yml'],
221-
expected: 'app.yml',
242+
name: 'no appyaml multiple',
243+
files: {
244+
'my-file': `
245+
this is a file
246+
`,
247+
'my-other-file': `
248+
this is another file
249+
`,
250+
},
251+
error: 'could not find an appyaml',
222252
},
223253
{
224-
name: 'finds app.yaml',
225-
list: ['a', 'b', 'c', 'app.yaml'],
226-
expected: 'app.yaml',
254+
name: 'single appyaml',
255+
files: {
256+
'app-dev.yaml': `
257+
runtime: 'node'
258+
service: 'my-service'
259+
`,
260+
},
261+
expected: 'app-dev.yaml',
227262
},
228263
{
229-
name: 'finds nested',
230-
list: ['foo/bar/app.yaml'],
231-
expected: 'foo/bar/app.yaml',
264+
name: 'multiple files with appyaml',
265+
files: {
266+
'my-file': `
267+
this is a file
268+
`,
269+
'my-other-file': `
270+
this is another file
271+
`,
272+
'app-prod.yaml': `
273+
runtime: 'node'
274+
service: 'my-service'
275+
`,
276+
},
277+
expected: 'app-prod.yaml',
278+
},
279+
{
280+
name: 'multiple appyaml uses first',
281+
files: {
282+
'app.yaml': `
283+
runtime: 'node'
284+
service: 'my-service'
285+
`,
286+
'app-dev.yaml': `
287+
runtime: 'node'
288+
service: 'my-service'
289+
`,
290+
'app-prod.yaml': `
291+
runtime: 'node'
292+
service: 'my-service'
293+
`,
294+
},
295+
expected: 'app.yaml',
232296
},
233297
];
234298

235299
cases.forEach((tc) => {
236300
const fn = tc.only ? it.only : it;
237-
fn(tc.name, () => {
301+
fn(tc.name, async function () {
302+
Object.keys(tc.files).map((key) => {
303+
const newKey = path.join(this.parent, key);
304+
tc.files[newKey] = tc.files[key];
305+
delete tc.files[key];
306+
});
307+
308+
await Promise.all(
309+
Object.entries(tc.files).map(async ([pth, contents]) => {
310+
await writeSecureFile(pth, contents);
311+
}),
312+
);
313+
314+
const filepaths = Object.keys(tc.files);
238315
if (tc.error) {
239-
expect(() => {
240-
findAppYaml(tc.list);
241-
}).to.throw(tc.error);
242-
} else {
243-
expect(findAppYaml(tc.list)).to.eql(tc.expected);
316+
expectError(async () => {
317+
await findAppYaml(filepaths);
318+
}, tc.error);
319+
} else if (tc.expected) {
320+
const expected = path.join(this.parent, tc.expected);
321+
const result = await findAppYaml(filepaths);
322+
expect(result).to.eql(expected);
244323
}
245324
});
246325
});
@@ -333,6 +412,53 @@ describe('#updateEnvVars', () => {
333412
});
334413
});
335414

415+
describe('#parseDeliverables', () => {
416+
const cases: {
417+
only?: boolean;
418+
name: string;
419+
input: string;
420+
expected?: string[];
421+
}[] = [
422+
{
423+
name: 'empty',
424+
input: '',
425+
expected: [],
426+
},
427+
{
428+
name: 'single',
429+
input: 'app.yaml',
430+
expected: ['app.yaml'],
431+
},
432+
{
433+
name: 'multi space',
434+
input: 'app.yaml foo.yaml',
435+
expected: ['app.yaml', 'foo.yaml'],
436+
},
437+
{
438+
name: 'multi comma',
439+
input: 'app.yaml, foo.yaml',
440+
expected: ['app.yaml', 'foo.yaml'],
441+
},
442+
{
443+
name: 'multi comma space',
444+
input: 'app.yaml,foo.yaml, bar.yaml',
445+
expected: ['app.yaml', 'foo.yaml', 'bar.yaml'],
446+
},
447+
{
448+
name: 'multi-line comma space',
449+
input: 'app.yaml,\nfoo.yaml, bar.yaml',
450+
expected: ['app.yaml', 'foo.yaml', 'bar.yaml'],
451+
},
452+
];
453+
454+
cases.forEach((tc) => {
455+
const fn = tc.only ? it.only : it;
456+
fn(tc.name, () => {
457+
expect(parseDeliverables(tc.input)).to.eql(tc.expected);
458+
});
459+
});
460+
});
461+
336462
async function expectError(fn: () => Promise<void>, want: string) {
337463
try {
338464
await fn();

0 commit comments

Comments
 (0)