Repository: iterative/cml
Pull request #1309 (2023-01-11 14:01:29)

Add the sha of the the current commit to the watermark as the fragment of the watermark image url.
This PR also updates the way updatable comments are determined - the matcher should ignore the sha in
the watermark.

This PR supercedes #1304.

github-actions[bot]: ## Test Comment

![](https://asset.cml.dev/65c49ca187aaddca0d979dab472f1d0a077b2bd0?cml=png&cache-bypass=b34f8f55-464f-4827-a73e-72f557dcdd77)

![](https://cml.dev/watermark.png "CML watermark")

github-actions[bot]: ## Test Comment

![](https://cml.dev/watermark.png "CML watermark")

github-actions[bot]: ## Test Comment

![](https://asset.cml.dev/65c49ca187aaddca0d979dab472f1d0a077b2bd0?cml=png&cache-bypass=4bb1c63b-6647-45ee-9968-82f8746e84fb)

![](https://cml.dev/watermark.png "CML watermark")

github-actions[bot]: ## Test Comment

![](https://cml.dev/watermark.png#nosha! "CML watermark")

github-actions[bot]: ## Test Comment

![](https://asset.cml.dev/65c49ca187aaddca0d979dab472f1d0a077b2bd0?cml=png&cache-bypass=bf6797ff-679b-49df-9c3f-71f6f9e4d68b)

![](https://cml.dev/watermark.png#nosha! "CML watermark")

github-actions[bot]: ## Test Comment

![](https://cml.dev/watermark.png#506e4e8d60e48e2ee4e99cfe9ff38ad6ec6a8f7f "CML watermark")

github-actions[bot]: ## Test Comment

![](https://asset.cml.dev/65c49ca187aaddca0d979dab472f1d0a077b2bd0?cml=png&cache-bypass=6564009c-3ec5-4f11-8e2d-1c8a033c0f19)

![](https://cml.dev/watermark.png#506e4e8d60e48e2ee4e99cfe9ff38ad6ec6a8f7f "CML watermark")

github-actions[bot]: ## Test Comment

![](https://cml.dev/watermark.png#506e4e8d60e48e2ee4e99cfe9ff38ad6ec6a8f7f "CML watermark")

github-actions[bot]: ## Test Comment

![](https://asset.cml.dev/65c49ca187aaddca0d979dab472f1d0a077b2bd0?cml=png&cache-bypass=abe9ae52-f7ec-4a8c-924f-a7725c20b174)

![](https://cml.dev/watermark.png#506e4e8d60e48e2ee4e99cfe9ff38ad6ec6a8f7f "CML watermark")

github-actions[bot]: ## Test Comment

![](https://cml.dev/watermark.png#506e4e8d60e48e2ee4e99cfe9ff38ad6ec6a8f7f "CML watermark")

github-actions[bot]: ## Test Comment

![](https://asset.cml.dev/65c49ca187aaddca0d979dab472f1d0a077b2bd0?cml=png&cache-bypass=4e1e95d2-1b84-44eb-9561-64b2d1647a4c)

![](https://cml.dev/watermark.png#506e4e8d60e48e2ee4e99cfe9ff38ad6ec6a8f7f "CML watermark")


Patch:
diff --git a/src/cml.js b/src/cml.js
index dcf2177..06560ab 100644
--- a/src/cml.js
+++ b/src/cml.js
@@ -21,10 +21,9 @@ const {
   preventcacheUri,
   waitForever
 } = require('./utils');
-
+const { Watermark } = require('./watermark');
 const { GITHUB_REPOSITORY, CI_PROJECT_URL, BITBUCKET_REPO_UUID } = process.env;
 
-const WATERMARK_IMAGE = 'https://cml.dev/watermark.png';
 const GIT_USER_NAME = 'Olivaw[bot]';
 const GIT_USER_EMAIL = 'olivaw@iterative.ai';
 const GIT_REMOTE = 'origin';
@@ -181,36 +180,16 @@ class CML {
     const drv = this.getDriver();
 
     if (rmWatermark && update)
-      throw new Error('watermarks are mandatory for updateable comments');
+      throw new Error('watermarks are mandatory for updatable comments');
 
     // Create the watermark.
-    const genWatermark = (opts = {}) => {
-      const { label = '', workflow, run } = opts;
-      // Replace {workflow} and {run} placeholders in label with actual values.
-      const lbl = label.replace('{workflow}', workflow).replace('{run}', run);
-
-      let title = `CML watermark ${lbl}`.trim();
-      // Github appears to escape underscores and asterisks in markdown content.
-      // Without escaping them, the watermark content in comments retrieved
-      // from github will not match the input.
-      const patterns = [
-        [/_/g, '\\_'], // underscore
-        [/\*/g, '\\*'], // asterisk
-        [/\[/g, '\\['], // opening square bracket
-        [/</g, '\\<'] // opening angle bracket
-      ];
-      title = patterns.reduce(
-        (label, pattern) => label.replace(pattern[0], pattern[1]),
-        title
-      );
-      return `![](${WATERMARK_IMAGE} "${title}")`;
-    };
     const watermark = rmWatermark
-      ? ''
-      : genWatermark({
+      ? null
+      : new Watermark({
           label: watermarkTitle,
           workflow: drv.workflowId,
-          run: drv.runId
+          run: drv.runId,
+          sha: commitSha || (await this.triggerSha())
         });
 
     let userReport = testReport;
@@ -222,7 +201,10 @@ class CML {
       if (!watch) throw err;
     }
 
-    let report = `${userReport}\n\n${watermark}`;
+    let report = userReport;
+    if (watermark) {
+      report = watermark.appendTo(userReport);
+    }
 
     const publishLocalFiles = async (tree) => {
       const nodes = [];
@@ -294,7 +276,7 @@ class CML {
     let comment;
     const updatableComment = (comments) => {
       return comments.reverse().find(({ body }) => {
-        return body.includes(watermark);
+        return !watermark || watermark.isIn(body);
       });
     };
 
diff --git a/src/watermark.js b/src/watermark.js
new file mode 100644
index 0000000..dbc37e4
--- /dev/null
+++ b/src/watermark.js
@@ -0,0 +1,65 @@
+const WATERMARK_IMAGE = 'https://cml.dev/watermark.png';
+
+class Watermark {
+  constructor(opts = {}) {
+    const { label = '', workflow, run, sha = false } = opts;
+    // Replace {workflow} and {run} placeholders in label with actual values.
+    this.label = label.replace('{workflow}', workflow).replace('{run}', run);
+
+    this.sha = sha;
+  }
+
+  // Appends the watermark (in markdown) to the report.
+  appendTo(report) {
+    return `${report}\n\n${this.toString()}`;
+  }
+
+  // Returns whether the watermark is present in the specified text.
+  // When checking for presence, the commit sha in the watermark is ignored.
+  isIn(text) {
+    const title = escapeRegExp(this.title);
+    const url = escapeRegExp(this.url({ sha: false }));
+    const pattern = `!\\[\\]\\(${url}#[0-9a-fA-F]+ "${title}"\\)`;
+    const re = new RegExp(pattern);
+    return re.test(text);
+  }
+
+  // String representation of the watermark.
+  toString() {
+    // Replace {workflow} and {run} placeholders in label with actual values.
+    return `![](${this.url()} "${this.title}")`;
+  }
+
+  get title() {
+    let title = `CML watermark ${this.label}`.trim();
+    // Github appears to escape underscores and asterisks in markdown content.
+    // Without escaping them, the watermark content in comments retrieved
+    // from github will not match the input.
+    const patterns = [
+      [/_/g, '\\_'], // underscore
+      [/\*/g, '\\*'], // asterisk
+      [/\[/g, '\\['], // opening square bracket
+      [/</g, '\\<'] // opening angle bracket
+    ];
+    title = patterns.reduce(
+      (label, pattern) => label.replace(pattern[0], pattern[1]),
+      title
+    );
+    return title;
+  }
+
+  url(opts = {}) {
+    const { sha = this.sha } = opts;
+    const watermarkUrl = new URL(WATERMARK_IMAGE);
+    if (sha) {
+      watermarkUrl.hash = sha;
+    }
+    return watermarkUrl.toString();
+  }
+}
+
+function escapeRegExp(str) {
+  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+}
+
+module.exports = { Watermark };


Test patch:
diff --git a/src/watermark.test.js b/src/watermark.test.js
new file mode 100644
index 0000000..c01327f
--- /dev/null
+++ b/src/watermark.test.js
@@ -0,0 +1,118 @@
+const { Watermark } = require('../src/watermark');
+
+describe('watermark tests', () => {
+  beforeEach(() => {
+    jest.resetModules();
+  });
+
+  test('append watermark to report', async () => {
+    const watermark = new Watermark({
+      workflow: 'workflow-id',
+      run: 'run-id',
+      sha: 'deadbeef'
+    });
+    const report = watermark.appendTo('some report');
+    expect(report).toEqual(
+      'some report\n\n![](https://cml.dev/watermark.png#deadbeef "CML watermark")'
+    );
+  });
+
+  test('append watermark with label to report', async () => {
+    const watermark = new Watermark({
+      label: 'some-label',
+      workflow: 'workflow-id',
+      run: 'run-id',
+      sha: 'deadbeef'
+    });
+    const report = watermark.appendTo('some report');
+    expect(report).toEqual(
+      'some report\n\n![](https://cml.dev/watermark.png#deadbeef "CML watermark some-label")'
+    );
+  });
+
+  test('append watermark with workflow placeholder to report', async () => {
+    const watermark = new Watermark({
+      label: 'some-label-{workflow}',
+      workflow: 'workflow-id',
+      run: 'run-id',
+      sha: 'deadbeef'
+    });
+    const report = watermark.appendTo('some report');
+    expect(report).toEqual(
+      'some report\n\n![](https://cml.dev/watermark.png#deadbeef "CML watermark some-label-workflow-id")'
+    );
+  });
+
+  test('append watermark with run placeholder to report', async () => {
+    const watermark = new Watermark({
+      label: 'some-label-{run}',
+      workflow: 'workflow-id',
+      run: 'run-id',
+      sha: 'deadbeef'
+    });
+    const report = watermark.appendTo('some report');
+    expect(report).toEqual(
+      'some report\n\n![](https://cml.dev/watermark.png#deadbeef "CML watermark some-label-run-id")'
+    );
+  });
+
+  test('check for presence of the watermark in a report', async () => {
+    const watermark = new Watermark({
+      workflow: 'workflow-id',
+      run: 'run-id',
+      sha: 'deadbeef'
+    });
+    const report = watermark.appendTo('some report');
+    expect(watermark.isIn(report)).toEqual(true);
+  });
+
+  test('check for presence of the watermark with special chars in a report', async () => {
+    const watermark = new Watermark({
+      label: 'custom_1[*]-vm',
+      workflow: 'workflow-id',
+      run: 'run-id',
+      sha: 'deadbeef'
+    });
+    const report = watermark.appendTo('some report');
+    expect(watermark.isIn(report)).toEqual(true);
+  });
+
+  test('check for presence of the watermark in a report, different sha', async () => {
+    const watermark = new Watermark({
+      label: 'some-label-{run}',
+      run: 'run-id',
+      sha: 'deadbeef'
+    });
+    const report = watermark.appendTo('some report');
+
+    // Watermark with a different sha, created by a CI run on
+    // a different HEAD commit.
+    const newWatermark = new Watermark({
+      label: 'some-label-{run}',
+      run: 'run-id',
+      sha: 'abcd'
+    });
+
+    expect(newWatermark.isIn(report)).toEqual(true);
+  });
+
+  test('check for watermark mismatch', async () => {
+    const watermark = new Watermark({
+      label: 'some-label-{workflow}',
+      workflow: 'workflow-id',
+      sha: 'deadbeef'
+    });
+    const report = watermark.appendTo('some report');
+
+    // Watermark with a different sha and different
+    // workflow.
+    const newWatermark = new Watermark({
+      workflow: 'different-workflow-id',
+      label: 'some-label-{workflow}',
+      run: 'run-id',
+      sha: 'abcd'
+    });
+
+    expect(newWatermark.isIn(report)).toEqual(false);
+  });
+});