From 375e31328060ddc3c8e223715681a04b3aa6b961 Mon Sep 17 00:00:00 2001
From: CrazyMax <crazy-max@users.noreply.github.com>
Date: Mon, 5 Jul 2021 20:37:02 +0200
Subject: [PATCH] Allow global prefix/suffix on latest

Signed-off-by: CrazyMax <crazy-max@users.noreply.github.com>
---
 README.md                |  4 +-
 __tests__/flavor.test.ts | 64 ++++++++++++++++++++++++++++---
 __tests__/meta.test.ts   | 73 ++++++++++++++++++++++++++++++++++-
 dist/index.js            | 82 ++++++++++++++++++++++++++++------------
 src/flavor.ts            | 76 ++++++++++++++++++++++++++-----------
 src/meta.ts              |  3 +-
 src/tag.ts               |  2 +-
 7 files changed, 245 insertions(+), 59 deletions(-)

diff --git a/README.md b/README.md
index 66831d0..789822c 100644
--- a/README.md
+++ b/README.md
@@ -296,8 +296,8 @@ flavor: |
 ```
 
 * `latest=<auto|true|false>`: Handle [latest tag](#latest-tag) (default `auto`)
-* `prefix=<string>`: A global prefix for each generated tag
-* `suffix=<string>`: A global suffix for each generated tag
+* `prefix=<string>,onlatest=<true|false>`: A global prefix for each generated tag and optionnally for `latest`
+* `suffix=<string>,onlatest=<true|false>`: A global suffix for each generated tag and optionnally for `latest`
 
 ## `tags` input
 
diff --git a/__tests__/flavor.test.ts b/__tests__/flavor.test.ts
index fcd6642..373ab4b 100644
--- a/__tests__/flavor.test.ts
+++ b/__tests__/flavor.test.ts
@@ -32,7 +32,9 @@ describe('transform', () => {
       {
         latest: "true",
         prefix: "",
-        suffix: ""
+        prefixLatest: false,
+        suffix: "",
+        suffixLatest: false,
       } as Flavor,
       false
     ],
@@ -43,7 +45,9 @@ describe('transform', () => {
       {
         latest: "false",
         prefix: "",
-        suffix: ""
+        prefixLatest: false,
+        suffix: "",
+        suffixLatest: false,
       } as Flavor,
       false
     ],
@@ -54,7 +58,9 @@ describe('transform', () => {
       {
         latest: "auto",
         prefix: "",
-        suffix: ""
+        prefixLatest: false,
+        suffix: "",
+        suffixLatest: false,
       } as Flavor,
       false
     ],
@@ -72,7 +78,9 @@ describe('transform', () => {
       {
         latest: "auto",
         prefix: "sha-",
-        suffix: ""
+        prefixLatest: false,
+        suffix: "",
+        suffixLatest: false,
       } as Flavor,
       false
     ],
@@ -83,7 +91,9 @@ describe('transform', () => {
       {
         latest: "auto",
         prefix: "",
-        suffix: "-alpine"
+        prefixLatest: false,
+        suffix: "-alpine",
+        suffixLatest: false,
       } as Flavor,
       false
     ],
@@ -96,7 +106,49 @@ describe('transform', () => {
       {
         latest: "false",
         prefix: "dev-",
-        suffix: "-alpine"
+        prefixLatest: false,
+        suffix: "-alpine",
+        suffixLatest: false,
+      } as Flavor,
+      false
+    ],
+    [
+      [
+        `prefix=dev-,onlatest=true`,
+      ],
+      {
+        latest: "auto",
+        prefix: "dev-",
+        prefixLatest: true,
+        suffix: "",
+        suffixLatest: false,
+      } as Flavor,
+      false
+    ],
+    [
+      [
+        `suffix=-alpine,onlatest=true`,
+      ],
+      {
+        latest: "auto",
+        prefix: "",
+        prefixLatest: false,
+        suffix: "-alpine",
+        suffixLatest: true,
+      } as Flavor,
+      false
+    ],
+    [
+      [
+        `prefix=dev-,onlatest=true`,
+        `suffix=-alpine,onlatest=true`,
+      ],
+      {
+        latest: "auto",
+        prefix: "dev-",
+        prefixLatest: true,
+        suffix: "-alpine",
+        suffixLatest: true,
       } as Flavor,
       false
     ],
diff --git a/__tests__/meta.test.ts b/__tests__/meta.test.ts
index 2e0c3e9..a8ee660 100644
--- a/__tests__/meta.test.ts
+++ b/__tests__/meta.test.ts
@@ -1301,7 +1301,78 @@ describe('tag', () => {
         "org.opencontainers.image.revision=90dd6032fac8bda1b6c4436a2e65de27961ed071",
         "org.opencontainers.image.licenses=MIT"
       ]
-    ]
+    ],
+    [
+      'tag21',
+      'event_tag_v1.1.1.env',
+      {
+        images: ['org/app', 'ghcr.io/user/app'],
+        tags: [
+          `type=semver,pattern={{version}}`,
+          `type=semver,pattern={{major}}.{{minor}}.{{patch}}`
+        ],
+        flavor: [
+          `suffix=-dev,onlatest=true`
+        ]
+      } as Inputs,
+      {
+        main: '1.1.1-dev',
+        partial: [],
+        latest: true
+      } as Version,
+      [
+        'org/app:1.1.1-dev',
+        'org/app:latest-dev',
+        'ghcr.io/user/app:1.1.1-dev',
+        'ghcr.io/user/app:latest-dev'
+      ],
+      [
+        "org.opencontainers.image.title=Hello-World",
+        "org.opencontainers.image.description=This your first repo!",
+        "org.opencontainers.image.url=https://github.com/octocat/Hello-World",
+        "org.opencontainers.image.source=https://github.com/octocat/Hello-World",
+        "org.opencontainers.image.version=1.1.1-dev",
+        "org.opencontainers.image.created=2020-01-10T00:30:00.000Z",
+        "org.opencontainers.image.revision=90dd6032fac8bda1b6c4436a2e65de27961ed071",
+        "org.opencontainers.image.licenses=MIT"
+      ]
+    ],
+    [
+      'tag22',
+      'event_tag_v1.1.1.env',
+      {
+        images: ['org/app', 'ghcr.io/user/app'],
+        tags: [
+          `type=semver,pattern={{version}}`,
+          `type=semver,pattern={{major}}.{{minor}}.{{patch}}`
+        ],
+        flavor: [
+          `prefix=foo-,onlatest=true`,
+          `suffix=-dev,onlatest=true`
+        ]
+      } as Inputs,
+      {
+        main: 'foo-1.1.1-dev',
+        partial: [],
+        latest: true
+      } as Version,
+      [
+        'org/app:foo-1.1.1-dev',
+        'org/app:foo-latest-dev',
+        'ghcr.io/user/app:foo-1.1.1-dev',
+        'ghcr.io/user/app:foo-latest-dev'
+      ],
+      [
+        "org.opencontainers.image.title=Hello-World",
+        "org.opencontainers.image.description=This your first repo!",
+        "org.opencontainers.image.url=https://github.com/octocat/Hello-World",
+        "org.opencontainers.image.source=https://github.com/octocat/Hello-World",
+        "org.opencontainers.image.version=foo-1.1.1-dev",
+        "org.opencontainers.image.created=2020-01-10T00:30:00.000Z",
+        "org.opencontainers.image.revision=90dd6032fac8bda1b6c4436a2e65de27961ed071",
+        "org.opencontainers.image.licenses=MIT"
+      ]
+    ],
   ])('given %p with %p event', tagsLabelsTest);
 });
 
diff --git a/dist/index.js b/dist/index.js
index b68e1a5..6a4a7f6 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -129,45 +129,78 @@ var __importStar = (this && this.__importStar) || function (mod) {
     __setModuleDefault(result, mod);
     return result;
 };
+var __importDefault = (this && this.__importDefault) || function (mod) {
+    return (mod && mod.__esModule) ? mod : { "default": mod };
+};
 Object.defineProperty(exports, "__esModule", ({ value: true }));
 exports.Transform = void 0;
 const core = __importStar(__webpack_require__(2186));
+const sync_1 = __importDefault(__webpack_require__(8750));
 function Transform(inputs) {
     const flavor = {
         latest: 'auto',
         prefix: '',
-        suffix: ''
+        prefixLatest: false,
+        suffix: '',
+        suffixLatest: false
     };
     for (const input of inputs) {
-        const parts = input.split('=', 2);
-        if (parts.length == 1) {
-            throw new Error(`Invalid entry: ${input}`);
-        }
-        switch (parts[0]) {
-            case 'latest': {
-                flavor.latest = parts[1];
-                if (!['auto', 'true', 'false'].includes(flavor.latest)) {
-                    throw new Error(`Invalid latest flavor entry: ${input}`);
-                }
-                break;
-            }
-            case 'prefix': {
-                flavor.prefix = parts[1];
-                break;
+        const fields = sync_1.default(input, {
+            relaxColumnCount: true,
+            skipLinesWithEmptyValues: true
+        })[0];
+        let onlatestfor = '';
+        for (const field of fields) {
+            const parts = field.toString().split('=', 2);
+            if (parts.length == 1) {
+                throw new Error(`Invalid flavor entry: ${input}`);
             }
-            case 'suffix': {
-                flavor.suffix = parts[1];
-                break;
-            }
-            default: {
-                throw new Error(`Unknown entry: ${input}`);
+            switch (parts[0]) {
+                case 'latest': {
+                    flavor.latest = parts[1];
+                    if (!['auto', 'true', 'false'].includes(flavor.latest)) {
+                        throw new Error(`Invalid latest flavor entry: ${input}`);
+                    }
+                    break;
+                }
+                case 'prefix': {
+                    flavor.prefix = parts[1];
+                    onlatestfor = 'prefix';
+                    break;
+                }
+                case 'suffix': {
+                    flavor.suffix = parts[1];
+                    onlatestfor = 'suffix';
+                    break;
+                }
+                case 'onlatest': {
+                    if (!['true', 'false'].includes(parts[1])) {
+                        throw new Error(`Invalid value for onlatest attribute: ${parts[1]}`);
+                    }
+                    switch (onlatestfor) {
+                        case 'prefix': {
+                            flavor.prefixLatest = /true/i.test(parts[1]);
+                            break;
+                        }
+                        case 'suffix': {
+                            flavor.suffixLatest = /true/i.test(parts[1]);
+                            break;
+                        }
+                    }
+                    break;
+                }
+                default: {
+                    throw new Error(`Unknown flavor entry: ${input}`);
+                }
             }
         }
     }
     core.startGroup(`Processing flavor input`);
     core.info(`latest=${flavor.latest}`);
     core.info(`prefix=${flavor.prefix}`);
+    core.info(`prefixLatest=${flavor.prefixLatest}`);
     core.info(`suffix=${flavor.suffix}`);
+    core.info(`suffixLatest=${flavor.suffixLatest}`);
     core.endGroup();
     return flavor;
 }
@@ -501,7 +534,6 @@ class Meta {
         else {
             vraw = this.context.ref.replace(/^refs\/tags\//g, '').replace(/\//g, '-');
         }
-        let latest = false;
         let tmatch;
         const isRegEx = tag.attrs['pattern'].match(/^\/(.+)\/(.*)$/);
         if (isRegEx) {
@@ -633,7 +665,7 @@ class Meta {
                 tags.push(`${imageLc}:${partial}`);
             }
             if (this.version.latest) {
-                tags.push(`${imageLc}:latest`);
+                tags.push(`${imageLc}:${this.flavor.prefixLatest ? this.flavor.prefix : ''}latest${this.flavor.suffixLatest ? this.flavor.suffix : ''}`);
             }
         }
         return tags;
@@ -817,7 +849,7 @@ function Parse(s) {
             switch (key) {
                 case 'type': {
                     if (!Object.values(Type).includes(value)) {
-                        throw new Error(`Unknown type attribute: ${value}`);
+                        throw new Error(`Unknown tag type attribute: ${value}`);
                     }
                     tag.type = value;
                     break;
diff --git a/src/flavor.ts b/src/flavor.ts
index 40b02d3..c5f96cf 100644
--- a/src/flavor.ts
+++ b/src/flavor.ts
@@ -1,41 +1,71 @@
 import * as core from '@actions/core';
+import csvparse from 'csv-parse/lib/sync';
 
 export interface Flavor {
   latest: string;
   prefix: string;
+  prefixLatest: boolean;
   suffix: string;
+  suffixLatest: boolean;
 }
 
 export function Transform(inputs: string[]): Flavor {
   const flavor: Flavor = {
     latest: 'auto',
     prefix: '',
-    suffix: ''
+    prefixLatest: false,
+    suffix: '',
+    suffixLatest: false
   };
 
   for (const input of inputs) {
-    const parts = input.split('=', 2);
-    if (parts.length == 1) {
-      throw new Error(`Invalid entry: ${input}`);
-    }
-    switch (parts[0]) {
-      case 'latest': {
-        flavor.latest = parts[1];
-        if (!['auto', 'true', 'false'].includes(flavor.latest)) {
-          throw new Error(`Invalid latest flavor entry: ${input}`);
-        }
-        break;
-      }
-      case 'prefix': {
-        flavor.prefix = parts[1];
-        break;
+    const fields = csvparse(input, {
+      relaxColumnCount: true,
+      skipLinesWithEmptyValues: true
+    })[0];
+    let onlatestfor = '';
+    for (const field of fields) {
+      const parts = field.toString().split('=', 2);
+      if (parts.length == 1) {
+        throw new Error(`Invalid flavor entry: ${input}`);
       }
-      case 'suffix': {
-        flavor.suffix = parts[1];
-        break;
-      }
-      default: {
-        throw new Error(`Unknown entry: ${input}`);
+      switch (parts[0]) {
+        case 'latest': {
+          flavor.latest = parts[1];
+          if (!['auto', 'true', 'false'].includes(flavor.latest)) {
+            throw new Error(`Invalid latest flavor entry: ${input}`);
+          }
+          break;
+        }
+        case 'prefix': {
+          flavor.prefix = parts[1];
+          onlatestfor = 'prefix';
+          break;
+        }
+        case 'suffix': {
+          flavor.suffix = parts[1];
+          onlatestfor = 'suffix';
+          break;
+        }
+        case 'onlatest': {
+          if (!['true', 'false'].includes(parts[1])) {
+            throw new Error(`Invalid value for onlatest attribute: ${parts[1]}`);
+          }
+          switch (onlatestfor) {
+            case 'prefix': {
+              flavor.prefixLatest = /true/i.test(parts[1]);
+              break;
+            }
+            case 'suffix': {
+              flavor.suffixLatest = /true/i.test(parts[1]);
+              break;
+            }
+          }
+          break;
+        }
+        default: {
+          throw new Error(`Unknown flavor entry: ${input}`);
+        }
       }
     }
   }
@@ -43,7 +73,9 @@ export function Transform(inputs: string[]): Flavor {
   core.startGroup(`Processing flavor input`);
   core.info(`latest=${flavor.latest}`);
   core.info(`prefix=${flavor.prefix}`);
+  core.info(`prefixLatest=${flavor.prefixLatest}`);
   core.info(`suffix=${flavor.suffix}`);
+  core.info(`suffixLatest=${flavor.suffixLatest}`);
   core.endGroup();
 
   return flavor;
diff --git a/src/meta.ts b/src/meta.ts
index ebcb7a6..1c70893 100644
--- a/src/meta.ts
+++ b/src/meta.ts
@@ -159,7 +159,6 @@ export class Meta {
       vraw = this.context.ref.replace(/^refs\/tags\//g, '').replace(/\//g, '-');
     }
 
-    let latest: boolean = false;
     let tmatch;
     const isRegEx = tag.attrs['pattern'].match(/^\/(.+)\/(.*)$/);
     if (isRegEx) {
@@ -304,7 +303,7 @@ export class Meta {
         tags.push(`${imageLc}:${partial}`);
       }
       if (this.version.latest) {
-        tags.push(`${imageLc}:latest`);
+        tags.push(`${imageLc}:${this.flavor.prefixLatest ? this.flavor.prefix : ''}latest${this.flavor.suffixLatest ? this.flavor.suffix : ''}`);
       }
     }
     return tags;
diff --git a/src/tag.ts b/src/tag.ts
index b0c83df..4ac9d15 100644
--- a/src/tag.ts
+++ b/src/tag.ts
@@ -100,7 +100,7 @@ export function Parse(s: string): Tag {
       switch (key) {
         case 'type': {
           if (!Object.values(Type).includes(value)) {
-            throw new Error(`Unknown type attribute: ${value}`);
+            throw new Error(`Unknown tag type attribute: ${value}`);
           }
           tag.type = value;
           break;