Skip to content

Commit

Permalink
Search all files for app.yaml candidates (#308)
Browse files Browse the repository at this point in the history
Fixes #303
  • Loading branch information
sethvargo authored Mar 12, 2023
1 parent 3c081bf commit 85a6e91
Show file tree
Hide file tree
Showing 3 changed files with 202 additions and 40 deletions.
2 changes: 1 addition & 1 deletion dist/index.js

Large diffs are not rendered by default.

66 changes: 51 additions & 15 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
isPinnedToHead,
KVPair,
parseBoolean,
parseCSV,
parseFlags,
parseKVString,
pinnedToHeadWarning,
Expand Down Expand Up @@ -83,7 +84,7 @@ export async function run(): Promise<void> {
// Get action inputs.
const projectId = presence(getInput('project_id'));
const cwd = presence(getInput('working_directory'));
const deliverables = (presence(getInput('deliverables')) || 'app.yaml').split(' ');
const deliverables = parseDeliverables(getInput('deliverables') || 'app.yaml');
const buildEnvVars = parseKVString(getInput('build_env_vars'));
const envVars = parseKVString(getInput('env_vars'));
const imageUrl = presence(getInput('image_url'));
Expand Down Expand Up @@ -113,7 +114,7 @@ export async function run(): Promise<void> {
.catch((err) => {
const rejection =
`Deliverable ${deliverable} not found or the ` +
`caller does not have permission, check "working_direcotry" ` +
`caller does not have permission, check "working_directory" ` +
`and "deliverables" inputs: ${err}`;
reject(new Error(rejection));
});
Expand All @@ -128,7 +129,7 @@ export async function run(): Promise<void> {
) {
logDebug(`Updating env_variables or build_env_variables`);

originalAppYamlPath = findAppYaml(deliverables);
originalAppYamlPath = await findAppYaml(deliverables);
originalAppYamlContents = await fs.readFile(originalAppYamlPath, 'utf8');
const parsed = YAML.parse(originalAppYamlContents);

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

/**
* findAppYaml finds the best app.yaml or app.yml file in the list of
* deliverables. It returns a tuple of the index and the path. If no file is
* found, it throws an error.
* deliverables. It returns the file's path. If no file is found, it throws an
* error.
*
* @return [number, string]
* @return [string]
*/
export function findAppYaml(list: string[]): string {
const idx = list.findIndex((item) => {
return item.endsWith('app.yml') || item.endsWith('app.yaml');
});

const pth = list[idx];
if (!pth) {
throw new Error(`Could not find "app.yml" file`);
export async function findAppYaml(list: string[]): Promise<string> {
for (let i = 0; i < list.length; i++) {
const pth = list[i];

try {
const contents = await fs.readFile(pth, 'utf8');
const parsed = YAML.parse(contents);

// Per https://cloud.google.com/appengine/docs/standard/reference/app-yaml,
// the only required fields are "runtime" and "service".
if (parsed && parsed['runtime'] && parsed['service']) {
return pth;
}
} catch (err) {
const msg = errorMessage(err);
logDebug(`Failed to parse ${pth} as YAML: ${msg}`);
}
}

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

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

/**
* parseDeliverables parses the given input string as a space-separated or
* comma-separated list of deliverables.
*
* @param input The given input
* @return [string[]]
*/
export function parseDeliverables(input: string): string[] {
const onSpaces = input.split(' ');

const final: string[] = [];
for (let i = 0; i < onSpaces.length; i++) {
const entry = onSpaces[i].trim();
if (entry !== '') {
const entries = parseCSV(entry);
for (let j = 0; j < entries.length; j++) {
const csvEntry = entries[j];
if (csvEntry !== '') {
final.push(csvEntry);
}
}
}
}
return final;
}

// Execute this as the entrypoint when requested.
if (require.main === module) {
run();
Expand Down
174 changes: 150 additions & 24 deletions tests/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,22 @@ import { expect } from 'chai';
import * as sinon from 'sinon';

import YAML from 'yaml';
import * as path from 'path';
import * as fs from 'fs/promises';

import * as core from '@actions/core';
import * as exec from '@actions/exec';
import * as setupGcloud from '@google-github-actions/setup-cloud-sdk';
import { TestToolCache } from '@google-github-actions/setup-cloud-sdk';
import { errorMessage, KVPair } from '@google-github-actions/actions-utils';
import {
errorMessage,
forceRemove,
KVPair,
randomFilepath,
writeSecureFile,
} from '@google-github-actions/actions-utils';

import { run, findAppYaml, updateEnvVars } from '../src/main';
import { run, findAppYaml, updateEnvVars, parseDeliverables } from '../src/main';

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

describe('#findAppYaml', () => {
beforeEach(async function () {
this.parent = randomFilepath();
await fs.mkdir(this.parent, { recursive: true });
});

afterEach(async function () {
if (this.parent) {
forceRemove(this.parent);
}
});

const cases: {
only?: boolean;
name: string;
list: string[];
files: Record<string, string>;
expected?: string;
error?: string;
}[] = [
{
name: 'empty list',
list: [],
error: 'Could not find',
name: 'no deployables',
files: {},
error: 'could not find an appyaml',
},
{
name: 'non-existent',
list: ['a', 'b', 'c'],
error: 'Could not find',
name: 'no appyaml single',
files: {
'my-file': `
this is a file
`,
},
error: 'could not find an appyaml',
},
{
name: 'finds app.yml',
list: ['a', 'b', 'c', 'app.yml'],
expected: 'app.yml',
name: 'no appyaml multiple',
files: {
'my-file': `
this is a file
`,
'my-other-file': `
this is another file
`,
},
error: 'could not find an appyaml',
},
{
name: 'finds app.yaml',
list: ['a', 'b', 'c', 'app.yaml'],
expected: 'app.yaml',
name: 'single appyaml',
files: {
'app-dev.yaml': `
runtime: 'node'
service: 'my-service'
`,
},
expected: 'app-dev.yaml',
},
{
name: 'finds nested',
list: ['foo/bar/app.yaml'],
expected: 'foo/bar/app.yaml',
name: 'multiple files with appyaml',
files: {
'my-file': `
this is a file
`,
'my-other-file': `
this is another file
`,
'app-prod.yaml': `
runtime: 'node'
service: 'my-service'
`,
},
expected: 'app-prod.yaml',
},
{
name: 'multiple appyaml uses first',
files: {
'app.yaml': `
runtime: 'node'
service: 'my-service'
`,
'app-dev.yaml': `
runtime: 'node'
service: 'my-service'
`,
'app-prod.yaml': `
runtime: 'node'
service: 'my-service'
`,
},
expected: 'app.yaml',
},
];

cases.forEach((tc) => {
const fn = tc.only ? it.only : it;
fn(tc.name, () => {
fn(tc.name, async function () {
Object.keys(tc.files).map((key) => {
const newKey = path.join(this.parent, key);
tc.files[newKey] = tc.files[key];
delete tc.files[key];
});

await Promise.all(
Object.entries(tc.files).map(async ([pth, contents]) => {
await writeSecureFile(pth, contents);
}),
);

const filepaths = Object.keys(tc.files);
if (tc.error) {
expect(() => {
findAppYaml(tc.list);
}).to.throw(tc.error);
} else {
expect(findAppYaml(tc.list)).to.eql(tc.expected);
expectError(async () => {
await findAppYaml(filepaths);
}, tc.error);
} else if (tc.expected) {
const expected = path.join(this.parent, tc.expected);
const result = await findAppYaml(filepaths);
expect(result).to.eql(expected);
}
});
});
Expand Down Expand Up @@ -333,6 +412,53 @@ describe('#updateEnvVars', () => {
});
});

describe('#parseDeliverables', () => {
const cases: {
only?: boolean;
name: string;
input: string;
expected?: string[];
}[] = [
{
name: 'empty',
input: '',
expected: [],
},
{
name: 'single',
input: 'app.yaml',
expected: ['app.yaml'],
},
{
name: 'multi space',
input: 'app.yaml foo.yaml',
expected: ['app.yaml', 'foo.yaml'],
},
{
name: 'multi comma',
input: 'app.yaml, foo.yaml',
expected: ['app.yaml', 'foo.yaml'],
},
{
name: 'multi comma space',
input: 'app.yaml,foo.yaml, bar.yaml',
expected: ['app.yaml', 'foo.yaml', 'bar.yaml'],
},
{
name: 'multi-line comma space',
input: 'app.yaml,\nfoo.yaml, bar.yaml',
expected: ['app.yaml', 'foo.yaml', 'bar.yaml'],
},
];

cases.forEach((tc) => {
const fn = tc.only ? it.only : it;
fn(tc.name, () => {
expect(parseDeliverables(tc.input)).to.eql(tc.expected);
});
});
});

async function expectError(fn: () => Promise<void>, want: string) {
try {
await fn();
Expand Down

0 comments on commit 85a6e91

Please sign in to comment.