From 235d5127e96cf0cd119ba06d21a53851437eab2a Mon Sep 17 00:00:00 2001
From: Leon Grave <l.grave@gmail.com>
Date: Fri, 10 Nov 2023 17:27:15 +0100
Subject: [PATCH] feat(manager/dockerfile): Add syntax statement support
 (#25530)

Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
Co-authored-by: Rhys Arkins <rhys@arkins.net>
Co-authored-by: Johannes Feichtner <343448+Churro@users.noreply.github.com>
---
 .../manager/dockerfile/extract.spec.ts        | 64 +++++++++++++++++++
 lib/modules/manager/dockerfile/extract.ts     | 32 +++++++++-
 2 files changed, 95 insertions(+), 1 deletion(-)

diff --git a/lib/modules/manager/dockerfile/extract.spec.ts b/lib/modules/manager/dockerfile/extract.spec.ts
index 9c47215ae7..fab901c893 100644
--- a/lib/modules/manager/dockerfile/extract.spec.ts
+++ b/lib/modules/manager/dockerfile/extract.spec.ts
@@ -941,6 +941,16 @@ describe('modules/manager/dockerfile/extract', () => {
     it('handles an alternative escape character', () => {
       const res = extractPackageFile(d4, '', {})?.deps;
       expect(res).toEqual([
+        {
+          autoReplaceStringTemplate:
+            '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}',
+          currentDigest: undefined,
+          currentValue: '1',
+          datasource: 'docker',
+          depName: 'docker/dockerfile',
+          depType: 'syntax',
+          replaceString: 'docker/dockerfile:1',
+        },
         {
           autoReplaceStringTemplate:
             ' ARG `\n' +
@@ -1152,6 +1162,60 @@ describe('modules/manager/dockerfile/extract', () => {
     });
   });
 
+  it('handles # syntax statements', () => {
+    const res = extractPackageFile(
+      '# syntax=docker/dockerfile:1.1.7\n' + 'FROM alpine:3.13.5\n',
+      '',
+      {},
+    );
+    expect(res).toEqual({
+      deps: [
+        {
+          autoReplaceStringTemplate:
+            '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}',
+          currentDigest: undefined,
+          currentValue: '1.1.7',
+          datasource: 'docker',
+          depName: 'docker/dockerfile',
+          depType: 'syntax',
+          replaceString: 'docker/dockerfile:1.1.7',
+        },
+        {
+          autoReplaceStringTemplate:
+            '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}',
+          currentDigest: undefined,
+          currentValue: '3.13.5',
+          datasource: 'docker',
+          depName: 'alpine',
+          depType: 'final',
+          replaceString: 'alpine:3.13.5',
+        },
+      ],
+    });
+  });
+
+  it('ignores # syntax statements after first line', () => {
+    const res = extractPackageFile(
+      'FROM alpine:3.13.5\n' + '# syntax=docker/dockerfile:1.1.7\n',
+      '',
+      {},
+    );
+    expect(res).toEqual({
+      deps: [
+        {
+          autoReplaceStringTemplate:
+            '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}',
+          currentDigest: undefined,
+          currentValue: '3.13.5',
+          datasource: 'docker',
+          depName: 'alpine',
+          depType: 'final',
+          replaceString: 'alpine:3.13.5',
+        },
+      ],
+    });
+  });
+
   describe('getDep()', () => {
     it('rejects null', () => {
       expect(getDep(null)).toEqual({ skipReason: 'invalid-value' });
diff --git a/lib/modules/manager/dockerfile/extract.ts b/lib/modules/manager/dockerfile/extract.ts
index 2841052c8d..fdce4db052 100644
--- a/lib/modules/manager/dockerfile/extract.ts
+++ b/lib/modules/manager/dockerfile/extract.ts
@@ -251,6 +251,7 @@ export function extractPackageFile(
 
   let escapeChar = '\\\\';
   let lookForEscapeChar = true;
+  let lookForSyntaxDirective = true;
 
   const lineFeed = content.indexOf('\r\n') >= 0 ? '\r\n' : '\n';
   const lines = content.split(newlineRegex);
@@ -272,6 +273,33 @@ export function extractPackageFile(
       }
     }
 
+    if (lookForSyntaxDirective) {
+      const syntaxRegex = regEx(
+        '^#[ \\t]*syntax[ \\t]*=[ \\t]*(?<image>\\S+)',
+        'im',
+      );
+      const syntaxMatch = instruction.match(syntaxRegex);
+      if (syntaxMatch?.groups?.image) {
+        const syntaxImage = syntaxMatch.groups.image;
+        const lineNumberRanges: number[][] = [
+          [lineNumberInstrStart, lineNumber],
+        ];
+        const dep = getDep(syntaxImage, true, config.registryAliases);
+        dep.depType = 'syntax';
+        processDepForAutoReplace(dep, lineNumberRanges, lines, lineFeed);
+        logger.trace(
+          {
+            depName: dep.depName,
+            currentValue: dep.currentValue,
+            currentDigest: dep.currentDigest,
+          },
+          'Dockerfile # syntax',
+        );
+        deps.push(dep);
+      }
+      lookForSyntaxDirective = false;
+    }
+
     const lineContinuationRegex = regEx(escapeChar + '[ \\t]*$|^[ \\t]*#', 'm');
     let lineLookahead = instruction;
     while (
@@ -400,7 +428,9 @@ export function extractPackageFile(
     return null;
   }
   for (const d of deps) {
-    d.depType = 'stage';
+    if (!d.depType) {
+      d.depType = 'stage';
+    }
   }
   deps[deps.length - 1].depType = 'final';
   return { deps };
-- 
GitLab