diff --git a/bin/cml/comment/create.js b/bin/cml/comment/create.js index 409b9df9c..eda8df3ff 100644 --- a/bin/cml/comment/create.js +++ b/bin/cml/comment/create.js @@ -68,6 +68,13 @@ exports.options = kebabcaseKeys({ 'Avoid watermark; CML needs a watermark to be able to distinguish CML comments from others', hidden: true, telemetryData: 'name' + }, + watermarkTitle: { + type: 'string', + description: + 'Hidden comment marker (used for targeting in subsequent `cml comment update`); "{workflow}" & "{run}" are auto-replaced', + default: '', + conflicts: ['rmWatermark'] } }); exports.DOCSURL = DOCSURL; diff --git a/bin/cml/comment/create.test.js b/bin/cml/comment/create.test.js index 4e36101e2..c4aebf9dc 100644 --- a/bin/cml/comment/create.test.js +++ b/bin/cml/comment/create.test.js @@ -3,7 +3,6 @@ const { exec } = require('../../../src/utils'); describe('Comment integration tests', () => { test('cml send-comment --help', async () => { const output = await exec(`node ./bin/cml.js send-comment --help`); - expect(output).toMatchInlineSnapshot(` "cml.js send-comment @@ -30,7 +29,10 @@ describe('Comment integration tests', () => { [string] [default: \\"https://asset.cml.dev\\"] --publish-native, --native Uses driver's native capabilities to upload assets instead of CML's storage; not available on GitHub - [boolean]" + [boolean] + --watermark-title Hidden comment marker (used for targeting in + subsequent \`cml comment update\`); \\"{workflow}\\" & + \\"{run}\\" are auto-replaced [string] [default: \\"\\"]" `); }); }); diff --git a/src/cml.js b/src/cml.js index 50d0d8601..f02abd299 100755 --- a/src/cml.js +++ b/src/cml.js @@ -23,6 +23,7 @@ const { 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'; @@ -164,26 +165,55 @@ class CML { const triggerSha = await this.triggerSha(); const { commitSha: inCommitSha = triggerSha, - rmWatermark, - update, + markdownFile, pr, publish, publishUrl, - markdownFile, report: testReport, + rmWatermark, + triggerFile, + update, watch, - triggerFile + watermarkTitle } = opts; + const drv = this.getDriver(); + const commitSha = (await this.revParse({ ref: inCommitSha })) || inCommitSha; if (rmWatermark && update) throw new Error('watermarks are mandatory for updateable 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 + [/ label.replace(pattern[0], pattern[1]), + title + ); + return `![](${WATERMARK_IMAGE} "${title}")`; + }; const watermark = rmWatermark ? '' - : '![CML watermark](https://raw.githubusercontent.com/iterative/cml/master/assets/watermark.svg)'; + : genWatermark({ + label: watermarkTitle, + workflow: drv.workflowId, + run: drv.runId + }); let userReport = testReport; try { @@ -195,15 +225,17 @@ class CML { } let report = `${userReport}\n\n${watermark}`; - const drv = this.getDriver(); const publishLocalFiles = async (tree) => { const nodes = []; visit(tree, ['definition', 'image', 'link'], (node) => nodes.push(node)); + const isWatermark = (node) => { + return node.title && node.title.startsWith('CML watermark'); + }; const visitor = async (node) => { - if (node.url && node.alt !== 'CML watermark') { + if (node.url && !isWatermark(node)) { const absolutePath = path.resolve( path.dirname(markdownFile), node.url @@ -264,7 +296,7 @@ class CML { let comment; const updatableComment = (comments) => { return comments.reverse().find(({ body }) => { - return body.includes('watermark.svg'); + return body.includes(watermark); }); }; diff --git a/src/drivers/bitbucket_cloud.js b/src/drivers/bitbucket_cloud.js index 0c6a668dd..9c160a558 100644 --- a/src/drivers/bitbucket_cloud.js +++ b/src/drivers/bitbucket_cloud.js @@ -8,8 +8,12 @@ const ProxyAgent = require('proxy-agent'); const { fetchUploadData, exec, gpuPresent, sleep } = require('../utils'); -const { BITBUCKET_COMMIT, BITBUCKET_BRANCH, BITBUCKET_PIPELINE_UUID } = - process.env; +const { + BITBUCKET_COMMIT, + BITBUCKET_BRANCH, + BITBUCKET_PIPELINE_UUID, + BITBUCKET_BUILD_NUMBER +} = process.env; class BitbucketCloud { constructor(opts = {}) { @@ -421,6 +425,14 @@ class BitbucketCloud { return command; } + get workflowId() { + return BITBUCKET_PIPELINE_UUID; + } + + get runId() { + return BITBUCKET_BUILD_NUMBER; + } + get sha() { return BITBUCKET_COMMIT; } diff --git a/src/drivers/github.js b/src/drivers/github.js index d36bdce25..273fa0a55 100644 --- a/src/drivers/github.js +++ b/src/drivers/github.js @@ -18,14 +18,15 @@ const CHECK_TITLE = 'CML Report'; process.env.RUNNER_ALLOW_RUNASROOT = 1; const { - GITHUB_REPOSITORY, - GITHUB_SHA, - GITHUB_REF, - GITHUB_HEAD_REF, + CI, GITHUB_EVENT_NAME, + GITHUB_HEAD_REF, + GITHUB_REF, + GITHUB_REPOSITORY, GITHUB_RUN_ID, + GITHUB_SHA, GITHUB_TOKEN, - CI, + GITHUB_WORKFLOW, TPI_TASK } = process.env; @@ -705,6 +706,14 @@ class Github { return command; } + get workflowId() { + return GITHUB_WORKFLOW; + } + + get runId() { + return GITHUB_RUN_ID; + } + warn(message) { console.error(`::warning::${message}`); } diff --git a/src/drivers/gitlab.js b/src/drivers/gitlab.js index afad6a368..6cd59fe0a 100644 --- a/src/drivers/gitlab.js +++ b/src/drivers/gitlab.js @@ -11,7 +11,8 @@ const winston = require('winston'); const { fetchUploadData, download, gpuPresent } = require('../utils'); -const { IN_DOCKER, CI_PIPELINE_ID } = process.env; +const { CI_JOB_ID, CI_PIPELINE_ID, IN_DOCKER } = process.env; + const API_VER = 'v4'; class Gitlab { constructor(opts = {}) { @@ -444,6 +445,14 @@ class Gitlab { return command; } + get workflowId() { + return CI_PIPELINE_ID; + } + + get runId() { + return CI_JOB_ID; + } + get sha() { return process.env.CI_COMMIT_SHA; } diff --git a/src/utils.js b/src/utils.js index a29aaa937..9fe41e45d 100644 --- a/src/utils.js +++ b/src/utils.js @@ -136,14 +136,14 @@ const isProcRunning = async (opts) => { }; const watermarkUri = ({ uri, type } = {}) => { - return uriParmam({ uri, param: 'cml', value: type }); + return uriParam({ uri, param: 'cml', value: type }); }; const preventcacheUri = ({ uri } = {}) => { - return uriParmam({ uri, param: 'cache-bypass', value: uuid.v4() }); + return uriParam({ uri, param: 'cache-bypass', value: uuid.v4() }); }; -const uriParmam = (opts = {}) => { +const uriParam = (opts = {}) => { const { uri, param, value } = opts; const url = new URL(uri); url.searchParams.set(param, value);