Repository: cdklabs/cdk-pipelines-github
Pull request #63 (2022-01-12 00:14:09)

chore: create workflow path if it does not exist
Super minor, but it bothers me that I need to create `.github/workflows` myself or `cdk synth` will fail. See:

![Screen Shot 2022-01-11 at 7 02 39 PM](https://user-images.githubusercontent.com/36202692/149040457-ab71f1bc-529c-4c71-a9a8-f2ec4c49de5d.png)

I think its low-hanging fruit to solve this, so I whipped up this PR.

Separately, I think it makes sense to enforce that `workflowPath.includes('.github/workflows');`. Ideally, I think we can enforce `workflowPath.startsWith('.github/workflows/')` but this would mean modifying the tests, as they would fail this check.

The actual success of the command I tested is based on the fact that the tests with ```workflowPath: `${mkoutdir()}/.github/workflows/deploy.yml` ``` succeed even though there is no `.github/workflows` structure in place.
 


Patch:
diff --git a/package.json b/package.json
index c8585ea..4580bb1 100644
--- a/package.json
+++ b/package.json
@@ -38,7 +38,7 @@
     "organization": false
   },
   "devDependencies": {
-    "@types/jest": "^26.0.24",
+    "@types/jest": "^27.4.0",
     "@types/node": "^12",
     "@typescript-eslint/eslint-plugin": "^5",
     "@typescript-eslint/parser": "^5",
diff --git a/src/pipeline.ts b/src/pipeline.ts
index 0fdffd6..69205e0 100644
--- a/src/pipeline.ts
+++ b/src/pipeline.ts
@@ -1,4 +1,4 @@
-import { writeFileSync } from 'fs';
+import { mkdirSync, writeFileSync } from 'fs';
 import * as path from 'path';
 import { Stage } from 'aws-cdk-lib';
 import { EnvironmentPlaceholders } from 'aws-cdk-lib/cx-api';
@@ -112,6 +112,9 @@ export class GitHubWorkflow extends PipelineBase {
     if (!this.workflowPath.endsWith('.yml') && !this.workflowPath.endsWith('.yaml')) {
       throw new Error('workflow file is expected to be a yaml file');
     }
+    if (!this.workflowPath.includes('.github/workflows/')) {
+      throw new Error('workflow files must be stored in the \'.github/workflows\' directory of your repository');
+    }
 
     this.workflowName = props.workflowName ?? 'deploy';
     this.workflowTriggers = props.workflowTriggers ?? {
@@ -177,6 +180,7 @@ export class GitHubWorkflow extends PipelineBase {
 
     // eslint-disable-next-line no-console
     console.error(`writing ${this.workflowPath}`);
+    mkdirSync(path.dirname(this.workflowPath), { recursive: true });
     writeFileSync(this.workflowPath, yaml);
   }
 
diff --git a/yarn.lock b/yarn.lock
index bf5fa95..6885333 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -509,17 +509,6 @@
     source-map "^0.6.1"
     write-file-atomic "^3.0.0"
 
-"@jest/types@^26.6.2":
-  version "26.6.2"
-  resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e"
-  integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==
-  dependencies:
-    "@types/istanbul-lib-coverage" "^2.0.0"
-    "@types/istanbul-reports" "^3.0.0"
-    "@types/node" "*"
-    "@types/yargs" "^15.0.0"
-    chalk "^4.0.0"
-
 "@jest/types@^27.4.2":
   version "27.4.2"
   resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.4.2.tgz#96536ebd34da6392c2b7c7737d693885b5dd44a5"
@@ -746,13 +735,13 @@
   dependencies:
     "@types/istanbul-lib-report" "*"
 
-"@types/jest@^26.0.24":
-  version "26.0.24"
-  resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.24.tgz#943d11976b16739185913a1936e0de0c4a7d595a"
-  integrity sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w==
+"@types/jest@^27.4.0":
+  version "27.4.0"
+  resolved "https://registry.npmjs.org/@types/jest/-/jest-27.4.0.tgz#037ab8b872067cae842a320841693080f9cb84ed"
+  integrity sha512-gHl8XuC1RZ8H2j5sHv/JqsaxXkDDM9iDOgu0Wp8sjs4u/snb2PVehyWXJPr+ORA0RPpgw231mnutWI1+0hgjIQ==
   dependencies:
-    jest-diff "^26.0.0"
-    pretty-format "^26.0.0"
+    jest-diff "^27.0.0"
+    pretty-format "^27.0.0"
 
 "@types/json-schema@^7.0.9":
   version "7.0.9"
@@ -799,13 +788,6 @@
   resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.1.tgz#3b9ce2489919d9e4fea439b76916abc34b2df129"
   integrity sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==
 
-"@types/yargs@^15.0.0":
-  version "15.0.14"
-  resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.14.tgz#26d821ddb89e70492160b66d10a0eb6df8f6fb06"
-  integrity sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ==
-  dependencies:
-    "@types/yargs-parser" "*"
-
 "@types/yargs@^16.0.0":
   version "16.0.4"
   resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.4.tgz#26aad98dd2c2a38e421086ea9ad42b9e51642977"
@@ -997,7 +979,7 @@ ansi-escapes@^4.2.1:
   dependencies:
     type-fest "^0.21.3"
 
-ansi-regex@^5.0.0, ansi-regex@^5.0.1:
+ansi-regex@^5.0.1:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
   integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
@@ -1902,11 +1884,6 @@ detect-newline@^3.0.0, detect-newline@^3.1.0:
   resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
   integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
 
-diff-sequences@^26.6.2:
-  version "26.6.2"
-  resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1"
-  integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==
-
 diff-sequences@^27.4.0:
   version "27.4.0"
   resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.4.0.tgz#d783920ad8d06ec718a060d00196dfef25b132a5"
@@ -3266,17 +3243,7 @@ jest-config@^27.4.7:
     pretty-format "^27.4.6"
     slash "^3.0.0"
 
-jest-diff@^26.0.0:
-  version "26.6.2"
-  resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394"
-  integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==
-  dependencies:
-    chalk "^4.0.0"
-    diff-sequences "^26.6.2"
-    jest-get-type "^26.3.0"
-    pretty-format "^26.6.2"
-
-jest-diff@^27.4.6:
+jest-diff@^27.0.0, jest-diff@^27.4.6:
   version "27.4.6"
   resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.4.6.tgz#93815774d2012a2cbb6cf23f84d48c7a2618f98d"
   integrity sha512-zjaB0sh0Lb13VyPsd92V7HkqF6yKRH9vm33rwBt7rPYrpQvS1nCvlIy2pICbKta+ZjWngYLNn4cCK4nyZkjS/w==
@@ -3329,11 +3296,6 @@ jest-environment-node@^27.4.6:
     jest-mock "^27.4.6"
     jest-util "^27.4.2"
 
-jest-get-type@^26.3.0:
-  version "26.3.0"
-  resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0"
-  integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==
-
 jest-get-type@^27.4.0:
   version "27.4.0"
   resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.4.0.tgz#7503d2663fffa431638337b3998d39c5e928e9b5"
@@ -4699,17 +4661,7 @@ prepend-http@^2.0.0:
   resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
   integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
 
-pretty-format@^26.0.0, pretty-format@^26.6.2:
-  version "26.6.2"
-  resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93"
-  integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==
-  dependencies:
-    "@jest/types" "^26.6.2"
-    ansi-regex "^5.0.0"
-    ansi-styles "^4.0.0"
-    react-is "^17.0.1"
-
-pretty-format@^27.4.6:
+pretty-format@^27.0.0, pretty-format@^27.4.6:
   version "27.4.6"
   resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.4.6.tgz#1b784d2f53c68db31797b2348fa39b49e31846b7"
   integrity sha512-NblstegA1y/RJW2VyML+3LlpFjzx62cUrtBIKIWDXEDkjNeleA7Od7nrzcs/VLQvAeV4CgSYhrN39DRN88Qi/g==


Test patch:
diff --git a/test/github.test.ts b/test/github.test.ts
index 676c9a0..8fb2123 100644
--- a/test/github.test.ts
+++ b/test/github.test.ts
@@ -14,7 +14,7 @@ test('pipeline with only a synth step', () => {
   const app = new TestApp();
 
   const github = new GitHubWorkflow(app, 'Pipeline', {
-    workflowPath: `${mkoutdir()}/deploy.yml`,
+    workflowPath: `${mkoutdir()}/.github/workflows/deploy.yml`,
     synth: new ShellStep('Build', {
       installCommands: ['yarn'],
       commands: ['yarn build'],
@@ -30,7 +30,7 @@ test('single wave/stage/stack', () => {
   const app = new TestApp();
 
   const pipeline = new GitHubWorkflow(app, 'Pipeline', {
-    workflowPath: `${mkoutdir()}/deploy.yml`,
+    workflowPath: `${mkoutdir()}/.github/workflows/deploy.yml`,
     synth: new ShellStep('Build', {
       commands: [],
     }),
@@ -66,6 +66,34 @@ test('example app', () => {
   expect(readFileSync(join(repoDir, '.github/workflows/deploy.yml'), 'utf-8')).toMatchSnapshot();
 });
 
+describe('workflow path', () => {
+  test('invalid workflow path fails', () => {
+    const app = new TestApp();
+
+    expect(() => {
+      new GitHubWorkflow(app, 'Pipeline', {
+        workflowPath: 'deploy.yml',
+        synth: new ShellStep('Build', {
+          commands: [],
+        }),
+      });
+    }).toThrowError("workflow files must be stored in the '.github/workflows' directory of your repository");
+  });
+
+  test('workflow path must be a yaml file', () => {
+    const app = new TestApp();
+
+    expect(() => {
+      new GitHubWorkflow(app, 'Pipeline', {
+        workflowPath: '.github/workflows/deploy.json',
+        synth: new ShellStep('Build', {
+          commands: [],
+        }),
+      });
+    }).toThrowError('workflow file is expected to be a yaml file');
+  });
+});
+
 function mkoutdir() {
   return mkdtempSync(join(tmpdir(), 'cdk-pipelines-github-'));
 }
\ No newline at end of file