From 2ecf523980e5f82e89a1518eaaced8cf540b8512 Mon Sep 17 00:00:00 2001 From: Helio Machado <0x2b3bfa0+git@googlemail.com> Date: Thu, 17 Aug 2023 20:12:11 +0200 Subject: [PATCH 1/9] fix: upgrade winston from 3.8.1 to 3.10.0 (#1414) Snyk has created this PR to upgrade winston from 3.8.1 to 3.10.0. See this package in npm: https://www.npmjs.com/package/winston See this project in Snyk: https://app.snyk.io/org/0x2b3bfa0/project/c72874ff-26c3-4f42-abed-4a4ce462ebbf?utm_source=github&utm_medium=referral&page=upgrade-pr Co-authored-by: snyk-bot --- package-lock.json | 13 +++++++++---- package.json | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 04b637055..ee8a9762c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,7 +45,7 @@ "unist-util-visit": "^2.0.3", "uuid": "^8.3.2", "which": "^2.0.2", - "winston": "^3.3.3", + "winston": "^3.10.0", "yargs": "^17.7.2" }, "bin": { @@ -7389,9 +7389,11 @@ } }, "node_modules/winston": { - "version": "3.8.1", - "license": "MIT", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.10.0.tgz", + "integrity": "sha512-nT6SIDaE9B7ZRO0u3UvdrimG0HkB7dSTAgInQnNR2SOPJ4bvq5q79+pXLftKmP52lJGW15+H5MCK0nM9D3KB/g==", "dependencies": { + "@colors/colors": "1.5.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", @@ -12368,8 +12370,11 @@ } }, "winston": { - "version": "3.8.1", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.10.0.tgz", + "integrity": "sha512-nT6SIDaE9B7ZRO0u3UvdrimG0HkB7dSTAgInQnNR2SOPJ4bvq5q79+pXLftKmP52lJGW15+H5MCK0nM9D3KB/g==", "requires": { + "@colors/colors": "1.5.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", diff --git a/package.json b/package.json index f8b10756e..c6aac9828 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ "unist-util-visit": "^2.0.3", "uuid": "^8.3.2", "which": "^2.0.2", - "winston": "^3.3.3", + "winston": "^3.10.0", "yargs": "^17.7.2" }, "devDependencies": { From 8211cf42ebe31d6fce93e7cd228c09d17e4d47ed Mon Sep 17 00:00:00 2001 From: Daniel Barnes Date: Fri, 25 Aug 2023 10:33:15 -0700 Subject: [PATCH 2/9] 0.20.0 (#1419) Co-authored-by: Olivaw[bot] <64868532+iterative-olivaw@users.noreply.github.com> --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ee8a9762c..9ac0576c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@dvcorg/cml", - "version": "0.19.1", + "version": "0.20.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@dvcorg/cml", - "version": "0.19.1", + "version": "0.20.0", "license": "Apache-2.0", "dependencies": { "@actions/core": "^1.9.1", diff --git a/package.json b/package.json index c6aac9828..fa4e60efd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@dvcorg/cml", - "version": "0.19.1", + "version": "0.20.0", "description": "

", "author": { "name": "Iterative Inc", From 27ea8ab5fd07f3e76d3117c769f9f2c49c060d76 Mon Sep 17 00:00:00 2001 From: Helio Machado <0x2b3bfa0+git@googlemail.com> Date: Wed, 13 Sep 2023 20:19:32 +0200 Subject: [PATCH 3/9] Use repository variables for non-sensitive fields (#1427) --- .github/workflows/images.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/images.yml b/.github/workflows/images.yml index b81f1fa7a..b2d29f8f2 100644 --- a/.github/workflows/images.yml +++ b/.github/workflows/images.yml @@ -90,7 +90,7 @@ jobs: - uses: docker/login-action@v2 with: registry: docker.io - username: ${{ secrets.DOCKERHUB_USERNAME }} + username: ${{ vars.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} - uses: docker/login-action@v2 with: From f775f7b385f5fb11313fb841fecbc9770780a0cd Mon Sep 17 00:00:00 2001 From: Daniel Barnes Date: Tue, 10 Oct 2023 06:24:33 -0700 Subject: [PATCH 4/9] Update Dockerfile (#1429) --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index bafd9cf1c..4849a8115 100644 --- a/Dockerfile +++ b/Dockerfile @@ -111,7 +111,7 @@ RUN add-apt-repository universe --yes \ && apt-get clean \ && rm --recursive --force /var/lib/apt/lists/* \ && npm config set user 0 \ - && npm install --global canvas@2 vega@5 vega-cli@5 vega-lite@5 + && npm install --global canvas@2 vega@5 vega-cli@5 vega-lite@5.14.1 # CONFIGURE RUNNER PATH ENV CML_RUNNER_PATH=/home/runner From 2675110b9e28ce9b23964c828b38b8872a1552e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Ng=E1=BB=8Dc=20Hoa?= <114990730+h2oa@users.noreply.github.com> Date: Fri, 10 May 2024 05:02:28 +0700 Subject: [PATCH 5/9] Quote `$GITHUB_HEAD_REF` on `release.yml` (#1448) * Update release.yml * Apply suggestions from code review --------- Co-authored-by: Helio Machado <0x2b3bfa0+git@googlemail.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 447843150..1dff6a100 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,7 +33,7 @@ jobs: - run: > gh release create --target ${{ github.event.pull_request.merge_commit_sha }} {--title=CML\ - ,}$(basename ${{ github.head_ref }}) --generate-notes --draft + ,}"$(basename "$GITHUB_HEAD_REF")" --generate-notes --draft env: GITHUB_TOKEN: ${{ secrets.ADMIN_GITHUB_TOKEN }} package: From e7d27a5af1960664470ba3614fe7faf43d46693b Mon Sep 17 00:00:00 2001 From: DavidGOrtega Date: Fri, 10 May 2024 00:04:13 +0200 Subject: [PATCH 6/9] Add exclusion list for environment variables (#802) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Restrict runner ENV access * Retrieve exclusion list from environment variable * Apply suggestions from code review * Fix “the blunder of the century” https://www.youtube.com/watch?v=vcFBwt1nu2U * Add warning for GitHub runners * Update github.js * Update github.js --------- Co-authored-by: Helio Machado <0x2b3bfa0+git@googlemail.com> --- src/cml.js | 9 ++++++++- src/drivers/bitbucket_cloud.js | 4 ++-- src/drivers/github.js | 9 +++++++-- src/drivers/gitlab.js | 5 +++-- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/cml.js b/src/cml.js index 59624fc5e..92b136080 100644 --- a/src/cml.js +++ b/src/cml.js @@ -421,7 +421,14 @@ class CML { } async startRunner(opts = {}) { - return await this.getDriver().startRunner(opts); + const env = {}; + const sensitive = [ + '_CML_RUNNER_SENSITIVE_ENV', + ...process.env._CML_RUNNER_SENSITIVE_ENV.split(':') + ]; + for (const variable in process.env) + if (!sensitive.includes(variable)) env[variable] = process.env[variable]; + return await this.getDriver().startRunner({ ...opts, env }); } async registerRunner(opts = {}) { diff --git a/src/drivers/bitbucket_cloud.js b/src/drivers/bitbucket_cloud.js index 4c56986f8..76680da79 100644 --- a/src/drivers/bitbucket_cloud.js +++ b/src/drivers/bitbucket_cloud.js @@ -166,7 +166,7 @@ class BitbucketCloud { async startRunner(opts) { const { projectPath } = this; - const { workdir, name, labels } = opts; + const { workdir, name, labels, env } = opts; winston.warn( `Bitbucket runner is working under /tmp folder and not under ${workdir} as expected` @@ -197,7 +197,7 @@ class BitbucketCloud { ${gpu ? '--runtime=nvidia -e NVIDIA_VISIBLE_DEVICES=all' : ''} \ docker-public.packages.atlassian.com/sox/atlassian/bitbucket-pipelines-runner:1`; - return spawn(command, { shell: true }); + return spawn(command, { shell: true, env }); } catch (err) { throw new Error(`Failed preparing runner: ${err.message}`); } diff --git a/src/drivers/github.js b/src/drivers/github.js index 70d216756..ad5e88c13 100644 --- a/src/drivers/github.js +++ b/src/drivers/github.js @@ -255,7 +255,11 @@ class Github { } async startRunner(opts) { - const { workdir, single, name, labels } = opts; + const { workdir, single, name, labels, env } = opts; + + this.warn( + 'cloud credentials are no longer available on self-hosted runner steps; please use step.env and secrets instead' + ); try { const runnerCfg = resolve(workdir, '.runner'); @@ -295,7 +299,8 @@ class Github { ); return spawn(resolve(workdir, 'run.sh'), { - shell: true + shell: true, + env }); } catch (err) { throw new Error(`Failed preparing GitHub runner: ${err.message}`); diff --git a/src/drivers/gitlab.js b/src/drivers/gitlab.js index 489f64d35..3360e39c7 100644 --- a/src/drivers/gitlab.js +++ b/src/drivers/gitlab.js @@ -183,7 +183,8 @@ class Gitlab { single, labels, name, - dockerVolumes = [] + dockerVolumes = [], + env } = opts; const gpu = await gpuPresent(); @@ -222,7 +223,7 @@ class Gitlab { ${dockerVolumesTpl} \ ${single ? '--max-builds 1' : ''}`; - return spawn(command, { shell: true }); + return spawn(command, { shell: true, env }); } catch (err) { if (err.message === 'Forbidden') err.message += From 1c49104205132b2b65564dceb8294e2c748cceb7 Mon Sep 17 00:00:00 2001 From: Helio Machado <0x2b3bfa0+git@googlemail.com> Date: Fri, 10 May 2024 00:05:37 +0200 Subject: [PATCH 7/9] Fix flawed `unist-util-visit` usage (#1447) * Fix issue on `publishLocalFiles` * Lint cml.js * Lint cml.js --- src/cml.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cml.js b/src/cml.js index 92b136080..fe971896d 100644 --- a/src/cml.js +++ b/src/cml.js @@ -213,7 +213,9 @@ class CML { const publishLocalFiles = async (tree) => { const nodes = []; - visit(tree, ['definition', 'image', 'link'], (node) => nodes.push(node)); + visit(tree, ['definition', 'image', 'link'], (node) => { + nodes.push(node); + }); const isWatermark = (node) => { return node.title && node.title.startsWith('CML watermark'); From 6638d381284bf7af9a6d86c5ff001c895a047056 Mon Sep 17 00:00:00 2001 From: Helio Machado <0x2b3bfa0+git@googlemail.com> Date: Fri, 10 May 2024 00:24:56 +0200 Subject: [PATCH 8/9] Cleanup project files (#1449) * Cleanup project files * fixup! Cleanup project files --- .eslintrc.js | 22 ---- .github/workflows/trigger-external.yml | 8 +- .gitignore | 142 ++++++++++++++++++++++--- .gitlab-ci.yml | 54 ---------- .nvmrc | 1 - README.md | 17 +-- assets/test.md | 4 +- bitbucket-pipelines.yml | 26 ----- package.json | 47 +++++++- prettier.config.js | 9 -- 10 files changed, 191 insertions(+), 139 deletions(-) delete mode 100644 .eslintrc.js delete mode 100644 .gitlab-ci.yml delete mode 100644 .nvmrc delete mode 100644 bitbucket-pipelines.yml delete mode 100644 prettier.config.js diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 50e21abcf..000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,22 +0,0 @@ -module.exports = { - env: { - browser: true, - commonjs: true, - es6: true, - jest: true - }, - extends: ['standard', 'prettier'], - globals: { - Atomics: 'readonly', - SharedArrayBuffer: 'readonly' - }, - parserOptions: { - ecmaVersion: 2020 - }, - ignorePatterns: ['assets/', 'dist/', 'node_modules/'], - rules: { - camelcase: [1, { properties: 'never' }], - 'prettier/prettier': 'error' - }, - plugins: ['prettier'] -}; diff --git a/.github/workflows/trigger-external.yml b/.github/workflows/trigger-external.yml index 104168c12..df5bdaa63 100644 --- a/.github/workflows/trigger-external.yml +++ b/.github/workflows/trigger-external.yml @@ -9,12 +9,12 @@ on: release: types: [published] pull_request_target: - branches: [master] + branches: [main] push: - branches: [master] + branches: [main] jobs: push: - if: ${{ github.event_name == 'push' && github.ref_name == 'master' }} + if: ${{ github.event_name == 'push' && github.ref_name == 'main' }} runs-on: ubuntu-latest strategy: matrix: @@ -26,7 +26,7 @@ jobs: --header "Authorization: token ${{ secrets.TEST_GITHUB_TOKEN }}" \ --header "Accept: application/vnd.github.v3+json" \ --url "https://api.github.com/repos/iterative/${{ matrix.repos }}/dispatches" \ - --data '{"event_type":"push", "client_payload": {"branch":"master"}}' + --data '{"event_type":"push", "client_payload": {"branch":"main"}}' pr: if: ${{ github.event_name == 'pull_request_target' }} runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 9727194aa..c6bba5913 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,130 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories node_modules/ -.terraform/ -.cml/ -.DS_Store - -main.tf -terraform.* -!terraform.js -!terraform.test.js -crash.log -/build -/coverage - -.idea/ \ No newline at end of file +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 70459d35e..000000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,54 +0,0 @@ -deploy-runner: - only: - refs: [master] - image: iterativeai/cml:0-dvc2-base1 - script: - - pip install awscli - - > - CREDENTIALS=($(aws sts assume-role-with-web-identity --region=us-west-1 - --role-arn=arn:aws:iam::342840881361:role/SandboxUser - --role-session-name=GitLab --duration-seconds=3600 - --web-identity-token="$CI_JOB_JWT_V2" - --query="Credentials.[AccessKeyId,SecretAccessKey,SessionToken]" - --output=text)) - - export AWS_ACCESS_KEY_ID="${CREDENTIALS[0]}" - - export AWS_SECRET_ACCESS_KEY="${CREDENTIALS[1]}" - - export AWS_SESSION_TOKEN="${CREDENTIALS[2]}" - - | - cml runner \ - --cloud=aws \ - --cloud-region=us-west \ - --cloud-type=g4dn.xlarge \ - --cloud-spot \ - --labels=cml-runner-gpu -test-runner: - needs: [deploy-runner] - only: - refs: [master] - tags: - - cml-runner-gpu - script: - - pip install tensorboard - - - npm ci - - npm run lint - - npm run test - - - nvidia-smi -test-container: - needs: [deploy-runner] - only: - refs: [master] - tags: - - cml-runner-gpu - image: iterativeai/cml:0-dvc2-base1-gpu - script: - - dvc --version - - cml --version - - pip install tensorboard - - - npm ci - - npm run lint - - npm run test - - - nvidia-smi diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index b6a7d89c6..000000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -16 diff --git a/README.md b/README.md index 0dd84e15e..c555ab5e7 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ jobs: ## Usage We helpfully provide CML and other useful libraries pre-installed on our -[custom Docker images](https://github.com/iterative/cml/blob/master/Dockerfile). +[custom Docker images](https://github.com/iterative/cml/blob/mains/Dockerfile). In the above example, uncommenting the field `container: ghcr.io/iterative/cml:0-dvc2-base1`) will make the runner pull the CML Docker image. The image already has NodeJS, Python 3, DVC and CML set up on @@ -215,7 +215,7 @@ git push origin experiment ``` 5. In GitHub, open up a pull request to compare the `experiment` branch to - `master`. + `main`. ![](https://static.iterative.ai/img/cml/make_pr.png) @@ -272,18 +272,18 @@ jobs: # Report metrics echo "## Metrics" >> report.md git fetch --prune - dvc metrics diff master --show-md >> report.md + dvc metrics diff main --show-md >> report.md # Publish confusion matrix diff echo "## Plots" >> report.md echo "### Class confusions" >> report.md - dvc plots diff --target classes.csv --template confusion -x actual -y predicted --show-vega master > vega.json + dvc plots diff --target classes.csv --template confusion -x actual -y predicted --show-vega main > vega.json vl2png vega.json -s 1.5 > confusion_plot.png echo "![](./confusion_plot.png)" >> report.md # Publish regularization function diff echo "### Effects of regularization" >> report.md - dvc plots diff --target estimators.csv -x Regularization --show-vega master > vega.json + dvc plots diff --target estimators.csv -x Regularization --show-vega main > vega.json vl2png vega.json -s 1.5 > plot.png echo "![](./plot.png)" >> report.md @@ -642,6 +642,9 @@ These are some example projects using CML. :key: needs a [PAT](#environment-variables). - # :warning: Maintenance :warning: -- ~2023-07 Nvidia has dropped container CUDA images with [10.x](https://hub.docker.com/r/nvidia/cuda/tags?page=1&name=10)/[cudnn7](https://hub.docker.com/r/nvidia/cuda/tags?page=1&name=cudnn7) and [11.2.1](https://hub.docker.com/r/nvidia/cuda/tags?page=1&name=11.2.1), CML images will be updated accrodingly + +- ~2023-07 Nvidia has dropped container CUDA images with + [10.x](https://hub.docker.com/r/nvidia/cuda/tags?page=1&name=10)/[cudnn7](https://hub.docker.com/r/nvidia/cuda/tags?page=1&name=cudnn7) + and [11.2.1](https://hub.docker.com/r/nvidia/cuda/tags?page=1&name=11.2.1), + CML images will be updated accrodingly diff --git a/assets/test.md b/assets/test.md index 116f12e7c..95a08f815 100644 --- a/assets/test.md +++ b/assets/test.md @@ -1,3 +1,3 @@ ### test -![embed](data:image/png;base64,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WZjgAetIhkwqRa52+8W6VZBgk32iUdFi5B4/vdMVymoeMtTvVaOIraxk/wDLP72PTd/hiuepiKcOtzGSudvqninTdJ3I8vmzj/llHyR9fT8a891jxDf61J+/k2QjgQoSF69/U1lMxZizElicknvSVwVa8qmnQlRSCiiisCgooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAfFLJDIJIpGjcdGQ4I/Gu68I3E1zpszzzSSsJiAXYsQNo9aKK78EbUjpBTqKK9AtmT4nmlt9AuJIZHjkBXDIxBHzDuK83nuZ7pw9xNJK4GA0jFjj05oorgxhlMiooorzzMKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//ZAHWWGjwAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjAtMDctMzBUMTI6MzI6MDIrMDA6MDDi40UVAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIwLTA3LTMwVDEyOjMyOjAyKzAwOjAwk779qQAAABp0RVh0ZXhpZjpCaXRzUGVyU2FtcGxlADgsIDgsIDgS7T4nAAAAEXRFWHRleGlmOkNvbG9yU3BhY2UAMQ+bAkkAAAAhdEVYdGV4aWY6RGF0ZVRpbWUAMjAyMDowNzowOSAxNjoxNDoxOWHSjSwAAAATdEVYdGV4aWY6RXhpZk9mZnNldAAxNzjc1lZ+AAAAFHRFWHRleGlmOkltYWdlTGVuZ3RoADE0M4D8vbsAAAATdEVYdGV4aWY6SW1hZ2VXaWR0aAAyNTDR1HOUAAAAGnRFWHRleGlmOlNvZnR3YXJlAEdJTVAgMi4xMC4yMAYQhsIAAAAkdEVYdGV4aWY6dGh1bWJuYWlsOkJpdHNQZXJTYW1wbGUAOCwgOCwgOCAb9FMAAAAcdEVYdGV4aWY6dGh1bWJuYWlsOkNvbXByZXNzaW9uADb5ZXBXAAAAHnRFWHRleGlmOnRodW1ibmFpbDpJbWFnZUxlbmd0aAAxNDZLLb8bAAAAHXRFWHRleGlmOnRodW1ibmFpbDpJbWFnZVdpZHRoADI1NogG+hQAAAAodEVYdGV4aWY6dGh1bWJuYWlsOkpQRUdJbnRlcmNoYW5nZUZvcm1hdAAzMDSsR89oAAAAL3RFWHRleGlmOnRodW1ibmFpbDpKUEVHSW50ZXJjaGFuZ2VGb3JtYXRMZW5ndGgANTMyOY6ptrMAAAAqdEVYdGV4aWY6dGh1bWJuYWlsOlBob3RvbWV0cmljSW50ZXJwcmV0YXRpb24ANhIVihoAAAAgdEVYdGV4aWY6dGh1bWJuYWlsOlNhbXBsZXNQZXJQaXhlbAAz4dfNWgAAABt0RVh0aWNjOmNvcHlyaWdodABQdWJsaWMgRG9tYWlutpExWwAAACJ0RVh0aWNjOmRlc2NyaXB0aW9uAEdJTVAgYnVpbHQtaW4gc1JHQkxnQRMAAAAVdEVYdGljYzptYW51ZmFjdHVyZXIAR0lNUEyekMoAAAAOdEVYdGljYzptb2RlbABzUkdCW2BJQwAAAABJRU5ErkJggg== -) + +![embed](data:image/png;base64,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WZjgAetIhkwqRa52+8W6VZBgk32iUdFi5B4/vdMVymoeMtTvVaOIraxk/wDLP72PTd/hiuepiKcOtzGSudvqninTdJ3I8vmzj/llHyR9fT8a891jxDf61J+/k2QjgQoSF69/U1lMxZizElicknvSVwVa8qmnQlRSCiiisCgooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAfFLJDIJIpGjcdGQ4I/Gu68I3E1zpszzzSSsJiAXYsQNo9aKK78EbUjpBTqKK9AtmT4nmlt9AuJIZHjkBXDIxBHzDuK83nuZ7pw9xNJK4GA0jFjj05oorgxhlMiooorzzMKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//ZAHWWGjwAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjAtMDctMzBUMTI6MzI6MDIrMDA6MDDi40UVAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIwLTA3LTMwVDEyOjMyOjAyKzAwOjAwk779qQAAABp0RVh0ZXhpZjpCaXRzUGVyU2FtcGxlADgsIDgsIDgS7T4nAAAAEXRFWHRleGlmOkNvbG9yU3BhY2UAMQ+bAkkAAAAhdEVYdGV4aWY6RGF0ZVRpbWUAMjAyMDowNzowOSAxNjoxNDoxOWHSjSwAAAATdEVYdGV4aWY6RXhpZk9mZnNldAAxNzjc1lZ+AAAAFHRFWHRleGlmOkltYWdlTGVuZ3RoADE0M4D8vbsAAAATdEVYdGV4aWY6SW1hZ2VXaWR0aAAyNTDR1HOUAAAAGnRFWHRleGlmOlNvZnR3YXJlAEdJTVAgMi4xMC4yMAYQhsIAAAAkdEVYdGV4aWY6dGh1bWJuYWlsOkJpdHNQZXJTYW1wbGUAOCwgOCwgOCAb9FMAAAAcdEVYdGV4aWY6dGh1bWJuYWlsOkNvbXByZXNzaW9uADb5ZXBXAAAAHnRFWHRleGlmOnRodW1ibmFpbDpJbWFnZUxlbmd0aAAxNDZLLb8bAAAAHXRFWHRleGlmOnRodW1ibmFpbDpJbWFnZVdpZHRoADI1NogG+hQAAAAodEVYdGV4aWY6dGh1bWJuYWlsOkpQRUdJbnRlcmNoYW5nZUZvcm1hdAAzMDSsR89oAAAAL3RFWHRleGlmOnRodW1ibmFpbDpKUEVHSW50ZXJjaGFuZ2VGb3JtYXRMZW5ndGgANTMyOY6ptrMAAAAqdEVYdGV4aWY6dGh1bWJuYWlsOlBob3RvbWV0cmljSW50ZXJwcmV0YXRpb24ANhIVihoAAAAgdEVYdGV4aWY6dGh1bWJuYWlsOlNhbXBsZXNQZXJQaXhlbAAz4dfNWgAAABt0RVh0aWNjOmNvcHlyaWdodABQdWJsaWMgRG9tYWlutpExWwAAACJ0RVh0aWNjOmRlc2NyaXB0aW9uAEdJTVAgYnVpbHQtaW4gc1JHQkxnQRMAAAAVdEVYdGljYzptYW51ZmFjdHVyZXIAR0lNUEyekMoAAAAOdEVYdGljYzptb2RlbABzUkdCW2BJQwAAAABJRU5ErkJggg==) diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml deleted file mode 100644 index 4bf120b8c..000000000 --- a/bitbucket-pipelines.yml +++ /dev/null @@ -1,26 +0,0 @@ -# This is an example Starter pipeline configuration -# Use a skeleton to build, test and deploy using manual and parallel steps -# ----- -# You can specify a custom docker image from Docker Hub as your build environment. - -# This is a sample build configuration for JavaScript. -# Check our guides at https://confluence.atlassian.com/x/14UWN for more examples. -# Only use spaces to indent your .yml configuration. -# ----- -# You can specify a custom docker image from Docker Hub as your build environment. -image: node:10.15.3 - -pipelines: - pull-requests: - '**': #this runs as default for any branch not elsewhere defined - - step: - caches: - - node - script: # Modify the commands below to build your repository. - - npm install - - echo "# My first CML report" > report.md - - echo - "![](https://static.boredpanda.com/blog/wp-content/uploads/2020/07/funny-expressive-dog-corgi-genthecorgi-1-1-5f0ea719ea38a__700.jpg)" - >> report.md - - echo "So much data viz" >> report.md - - node bin/cml-send-comment report.md diff --git a/package.json b/package.json index fa4e60efd..5d19b1802 100644 --- a/package.json +++ b/package.json @@ -28,10 +28,55 @@ "dvc" ], "license": "Apache-2.0", - "main": "index.js", "engines": { "node": ">=16.0.0" }, + "eslintConfig": { + "env": { + "browser": true, + "commonjs": true, + "es6": true, + "jest": true + }, + "extends": [ + "standard", + "prettier" + ], + "globals": { + "Atomics": "readonly", + "SharedArrayBuffer": "readonly" + }, + "parserOptions": { + "ecmaVersion": 2020 + }, + "ignorePatterns": [ + "assets/", + "dist/", + "node_modules/" + ], + "rules": { + "camelcase": [ + 1, + { + "properties": "never" + } + ], + "prettier/prettier": "error" + }, + "plugins": [ + "prettier" + ] + }, + "prettier": { + "arrowParens": "always", + "singleQuote": true, + "trailingComma": "none", + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "proseWrap": "always" + }, + "main": "index.js", "bin": { "cml": "bin/cml.js", "cml-send-github-check": "bin/legacy/link.js", diff --git a/prettier.config.js b/prettier.config.js deleted file mode 100644 index ac512f69b..000000000 --- a/prettier.config.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - arrowParens: 'always', - singleQuote: true, - trailingComma: 'none', - printWidth: 80, - tabWidth: 2, - useTabs: false, - proseWrap: 'always' -}; From 24ca0df3455f8d5ebf5b3f338ef997ef02129a97 Mon Sep 17 00:00:00 2001 From: Helio Machado <0x2b3bfa0+git@googlemail.com> Date: Fri, 10 May 2024 03:23:07 +0200 Subject: [PATCH 9/9] Fix CML (#1450) * Fix CML * fixup! Fix CML * fixup! Fix CML --- .github/workflows/test-deploy.yml | 8 ++-- bin/cml.js | 28 +----------- bin/cml.test.js | 1 - bin/cml/asset/publish.js | 4 +- bin/cml/comment/create.e2e.test.js | 14 ------ bin/cml/runner/launch.js | 58 ++++++++++++------------- bin/cml/tensorboard.js | 2 +- bin/cml/tensorboard/connect.e2e.test.js | 8 ++-- bin/cml/tensorboard/connect.js | 11 +++-- bin/legacy/deprecation.js | 4 +- package.json | 2 +- src/analytics.js | 10 ++--- src/cml.js | 24 +++++----- src/commenttarget.js | 10 ++--- src/drivers/bitbucket_cloud.e2e.test.js | 8 ++-- src/drivers/bitbucket_cloud.js | 16 +++---- src/drivers/github.js | 28 ++++++------ src/drivers/gitlab.js | 32 +++++++++----- src/logger.js | 32 ++++++++++++++ src/terraform.js | 4 +- src/terraform.test.js | 12 ----- src/utils.js | 8 ++-- 22 files changed, 160 insertions(+), 164 deletions(-) create mode 100644 src/logger.js diff --git a/.github/workflows/test-deploy.yml b/.github/workflows/test-deploy.yml index 9d7d9b5d9..446354a6a 100644 --- a/.github/workflows/test-deploy.yml +++ b/.github/workflows/test-deploy.yml @@ -65,10 +65,10 @@ jobs: TEST_GITLAB_REPO: https://gitlab.com/iterative.ai/cml_qa_tests_dummy TEST_GITLAB_SHA: f8b8b49a253243830ef59a7f090eb887157b2b67 TEST_GITLAB_ISSUE: 1 - TEST_BBCLOUD_TOKEN: ${{ secrets.TEST_BBCLOUD_TOKEN }} - TEST_BBCLOUD_REPO: https://bitbucket.org/iterative-ai/cml-qa-tests-dummy - TEST_BBCLOUD_SHA: b511535a89f76d3d311b1c15e3e712b15c0b94e3 - TEST_BBCLOUD_ISSUE: 1 + TEST_BITBUCKET_TOKEN: ${{ secrets.TEST_BITBUCKET_TOKEN }} + TEST_BITBUCKET_REPO: https://bitbucket.org/iterative-ai/cml-qa-tests-dummy + TEST_BITBUCKET_SHA: b511535a89f76d3d311b1c15e3e712b15c0b94e3 + TEST_BITBUCKET_ISSUE: 1 test-os: needs: authorize name: test-${{ matrix.system }} diff --git a/bin/cml.js b/bin/cml.js index 4d8358cd5..5c15e15c8 100755 --- a/bin/cml.js +++ b/bin/cml.js @@ -5,7 +5,7 @@ const { pseudoexec } = require('pseudoexec'); const kebabcaseKeys = require('kebabcase-keys'); const which = require('which'); -const winston = require('winston'); +const { logger, setupLogger } = require('../src/logger'); const yargs = require('yargs'); const CML = require('../src/cml').default; @@ -72,30 +72,6 @@ const setupOpts = (opts) => { opts.cml = new CML(opts); }; -const setupLogger = (opts) => { - const { log: level } = opts; - - winston.configure({ - format: process.stdout.isTTY - ? winston.format.combine( - winston.format.colorize({ all: true }), - winston.format.simple() - ) - : winston.format.combine( - winston.format.errors({ stack: true }), - winston.format.json() - ), - transports: [ - new winston.transports.Console({ - stderrLevels: Object.keys(winston.config.npm.levels), - handleExceptions: true, - handleRejections: true, - level - }) - ] - }); -}; - const setupTelemetry = async (opts, yargs) => { const { cml, _: command } = opts; @@ -201,7 +177,7 @@ const handleError = (message, error) => { const event = { ...telemetryEvent, error: err.message }; await send({ event }); } - winston.error(err); + logger.error(err); process.exit(1); } })(); diff --git a/bin/cml.test.js b/bin/cml.test.js index 06d19cedc..b41e6a9bd 100644 --- a/bin/cml.test.js +++ b/bin/cml.test.js @@ -13,7 +13,6 @@ describe('command-line interface tests', () => { cml.js comment Manage comments cml.js pr Manage pull requests cml.js runner Manage self-hosted (cloud & on-premise) CI runners - cml.js tensorboard Manage tensorboard.dev connections cml.js workflow Manage CI workflows cml.js ci Prepare Git repository for CML operations diff --git a/bin/cml/asset/publish.js b/bin/cml/asset/publish.js index fa07e165d..ea1f49994 100644 --- a/bin/cml/asset/publish.js +++ b/bin/cml/asset/publish.js @@ -1,6 +1,6 @@ const fs = require('fs').promises; const kebabcaseKeys = require('kebabcase-keys'); -const winston = require('winston'); +const { logger } = require('../../../src/logger'); const { CML } = require('../../../src/cml'); @@ -12,7 +12,7 @@ exports.description = `${DESCRIPTION}\n${DOCSURL}`; exports.handler = async (opts) => { if (opts.gitlabUploads) { - winston.warn( + logger.warn( '--gitlab-uploads will be deprecated soon, use --native instead' ); opts.native = true; diff --git a/bin/cml/comment/create.e2e.test.js b/bin/cml/comment/create.e2e.test.js index 1951bc4c3..c37073898 100644 --- a/bin/cml/comment/create.e2e.test.js +++ b/bin/cml/comment/create.e2e.test.js @@ -33,18 +33,4 @@ describe('Comment integration tests', () => { path ); }); - - test('cml send-comment to current repo', async () => { - const report = `## Test Comment`; - - await fs.writeFile(path, report); - await exec('node', './bin/cml.js', 'send-comment', path); - }); - - test('cml send-comment --publish to current repo', async () => { - const report = `## Test Comment\n![](assets/logo.png)`; - - await fs.writeFile(path, report); - await exec('node', './bin/cml.js', 'send-comment', '--publish', path); - }); }); diff --git a/bin/cml/runner/launch.js b/bin/cml/runner/launch.js index b548680fb..9dd501291 100755 --- a/bin/cml/runner/launch.js +++ b/bin/cml/runner/launch.js @@ -4,7 +4,7 @@ const fs = require('fs').promises; const net = require('net'); const kebabcaseKeys = require('kebabcase-keys'); const timestring = require('timestring'); -const winston = require('winston'); +const { logger } = require('../../../src/logger'); const { exec, randid, sleep } = require('../../../src/utils'); const tf = require('../../../src/terraform'); @@ -29,26 +29,26 @@ const shutdown = async (opts) => { if (!RUNNER) return true; try { - winston.info(`Unregistering runner ${name}...`); + logger.info(`Unregistering runner ${name}...`); await cml.unregisterRunner({ name }); } catch (err) { if (err.message.includes('is still running a job')) { - winston.warn(`\tCancelling shutdown: ${err.message}`); + logger.warn(`\tCancelling shutdown: ${err.message}`); return false; } - winston.error(`\tFailed: ${err.message}`); + logger.error(`\tFailed: ${err.message}`); } RUNNER.kill('SIGINT'); - winston.info('\tSuccess'); + logger.info('\tSuccess'); return true; }; const retryWorkflows = async () => { try { if (!noRetry && RUNNER_JOBS_RUNNING.length > 0) { - winston.info(`Still pending jobs, retrying workflow...`); + logger.info(`Still pending jobs, retrying workflow...`); await Promise.all( RUNNER_JOBS_RUNNING.map( @@ -58,14 +58,14 @@ const shutdown = async (opts) => { ); } } catch (err) { - winston.error(err); + logger.error(err); } }; const destroyLeo = async () => { if (!tfResource) return; - winston.info(`Waiting ${destroyDelay} seconds to destroy`); + logger.info(`Waiting ${destroyDelay} seconds to destroy`); await sleep(destroyDelay); const { cloud, id, region } = JSON.parse( @@ -83,7 +83,7 @@ const shutdown = async (opts) => { id ); } catch (err) { - winston.error(`\tFailed destroying with LEO: ${err.message}`); + logger.error(`\tFailed destroying with LEO: ${err.message}`); } }; @@ -97,7 +97,7 @@ const shutdown = async (opts) => { clearInterval(watcher); await retryWorkflows(); } catch (err) { - winston.error(`Error connecting the SCM: ${err.message}`); + logger.error(`Error connecting the SCM: ${err.message}`); } } @@ -105,13 +105,13 @@ const shutdown = async (opts) => { if (error) throw error; - winston.info('runner status', { reason, status: 'terminated' }); + logger.info('runner status', { reason, status: 'terminated' }); process.exit(0); }; const runCloud = async (opts) => { const runTerraform = async (opts) => { - winston.info('Terraform apply...'); + logger.info('Terraform apply...'); const { token, repo, driver } = cml; const { @@ -143,7 +143,7 @@ const runCloud = async (opts) => { await tf.checkMinVersion(); if (gpu === 'tesla') - winston.warn( + logger.warn( 'GPU model "tesla" has been deprecated; please use "v100" instead.' ); @@ -189,7 +189,7 @@ const runCloud = async (opts) => { return tfstate; }; - winston.info('Deploying cloud runner plan...'); + logger.info('Deploying cloud runner plan...'); const tfstate = await runTerraform(opts); const { resources } = tfstate; for (const resource of resources) { @@ -221,14 +221,14 @@ const runCloud = async (opts) => { timeouts: attributes.timeouts, kubernetesNodeSelector: attributes.kubernetes_node_selector }; - winston.info(JSON.stringify(nonSensitiveValues)); + logger.info(JSON.stringify(nonSensitiveValues)); } } } }; const runLocal = async (opts) => { - winston.info(`Launching ${cml.driver} runner`); + logger.info(`Launching ${cml.driver} runner`); const { workdir, name, @@ -265,10 +265,10 @@ const runLocal = async (opts) => { if (process.platform === 'linux') { const acpiSock = net.connect('/var/run/acpid.socket'); acpiSock.on('connect', () => { - winston.info('Connected to acpid service.'); + logger.info('Connected to acpid service.'); }); acpiSock.on('error', (err) => { - winston.warn( + logger.warn( `Error connecting to ACPI socket: ${err.message}. The acpid.service helps with instance termination detection.` ); }); @@ -283,10 +283,10 @@ const runLocal = async (opts) => { const dataHandler = ({ cloudSpot }) => async (data) => { - winston.debug(data.toString()); + logger.debug(data.toString()); const logs = await cml.parseRunnerLog({ data, name, cloudSpot }); for (const log of logs) { - winston.info('runner status', log); + logger.info('runner status', log); if (log.status === 'job_started') { const { job: id, pipeline, date } = log; @@ -380,7 +380,7 @@ const run = async (opts) => { throw new Error( `Runner name ${name} is already in use. Please change the name or terminate the existing runner.` ); - winston.info(`Reusing existing runner named ${name}...`); + logger.info(`Reusing existing runner named ${name}...`); return; } @@ -390,9 +390,7 @@ const run = async (opts) => { (runner) => runner.online ) ) { - winston.info( - `Reusing existing online runners with the ${labels} labels...` - ); + logger.info(`Reusing existing online runners with the ${labels} labels...`); return; } @@ -402,7 +400,7 @@ const run = async (opts) => { 'cml runner flag --reuse-idle is unsupported by bitbucket' ); } - winston.info( + logger.info( `Checking for existing idle runner matching labels: ${labels}.` ); const currentRunners = await cml.runnersByLabels({ labels, runners }); @@ -410,25 +408,25 @@ const run = async (opts) => { (runner) => runner.online && !runner.busy ); if (availableRunner) { - winston.info('Found matching idle runner.', availableRunner); + logger.info('Found matching idle runner.', availableRunner); return; } } if (dockerVolumes.length && cml.driver !== 'gitlab') - winston.warn('Parameters --docker-volumes is only supported in gitlab'); + logger.warn('Parameters --docker-volumes is only supported in gitlab'); if (cml.driver === 'github') - winston.warn( + logger.warn( 'Github Actions timeout has been updated from 72h to 35 days. Update your workflow accordingly to be able to restart it automatically.' ); if (RUNNER_NAME) - winston.warn( + logger.warn( 'ignoring RUNNER_NAME environment variable, use CML_RUNNER_NAME or --name instead' ); - winston.info(`Preparing workdir ${workdir}...`); + logger.info(`Preparing workdir ${workdir}...`); await fs.mkdir(workdir, { recursive: true }); await fs.chmod(workdir, '766'); diff --git a/bin/cml/tensorboard.js b/bin/cml/tensorboard.js index 851141bfc..06dbbf38b 100644 --- a/bin/cml/tensorboard.js +++ b/bin/cml/tensorboard.js @@ -1,5 +1,5 @@ exports.command = 'tensorboard'; -exports.description = 'Manage tensorboard.dev connections'; +exports.description = false; exports.builder = (yargs) => yargs .options({ diff --git a/bin/cml/tensorboard/connect.e2e.test.js b/bin/cml/tensorboard/connect.e2e.test.js index 6ac4b96b8..21b457bfc 100644 --- a/bin/cml/tensorboard/connect.e2e.test.js +++ b/bin/cml/tensorboard/connect.e2e.test.js @@ -19,7 +19,7 @@ const rmTbDevExperiment = async (tbOutput) => { }; describe('tbLink', () => { - test('timeout without result throws exception', async () => { + test.skip('timeout without result throws exception', async () => { const stdout = tempy.file({ extension: 'log' }); const stderror = tempy.file({ extension: 'log' }); const message = 'there is an error'; @@ -37,7 +37,7 @@ describe('tbLink', () => { expect(error.message).toBe(`Tensorboard took too long`); }); - test('valid url is returned', async () => { + test.skip('valid url is returned', async () => { const stdout = tempy.file({ extension: 'log' }); const stderror = tempy.file({ extension: 'log' }); const message = 'https://iterative.ai'; @@ -51,7 +51,7 @@ describe('tbLink', () => { }); describe('CML e2e', () => { - test('cml tensorboard-dev --md returns md and after command TB is still up', async () => { + test.skip('cml tensorboard-dev --md returns md and after command TB is still up', async () => { const name = 'My experiment'; const desc = 'Test experiment'; const title = 'go to the experiment'; @@ -80,7 +80,7 @@ describe('CML e2e', () => { expect(output.includes('cml=tb')).toBe(true); }); - test('cml tensorboard-dev invalid creds', async () => { + test.skip('cml tensorboard-dev invalid creds', async () => { try { await exec( 'node', diff --git a/bin/cml/tensorboard/connect.js b/bin/cml/tensorboard/connect.js index 8d6aa787b..fea2beff1 100644 --- a/bin/cml/tensorboard/connect.js +++ b/bin/cml/tensorboard/connect.js @@ -3,7 +3,7 @@ const kebabcaseKeys = require('kebabcase-keys'); const { spawn } = require('child_process'); const { homedir } = require('os'); const tempy = require('tempy'); -const winston = require('winston'); +const { logger } = require('../../../src/logger'); const { exec, watermarkUri, sleep } = require('../../../src/utils'); @@ -29,7 +29,7 @@ const tbLink = async (opts = {}) => { chrono = chrono + chronoStep; } - winston.error(await fs.readFile(stderror, 'utf8')); + logger.error(await fs.readFile(stderror, 'utf8')); throw new Error(`Tensorboard took too long`); }; @@ -52,7 +52,7 @@ const launchAndWaitLink = async (opts = {}) => { proc.unref(); proc.on('exit', async (code, signal) => { if (code || signal) { - winston.error(await fs.readFile(stderrPath, 'utf8')); + logger.error(await fs.readFile(stderrPath, 'utf8')); throw new Error(`Tensorboard failed with error ${code || signal}`); } }); @@ -81,6 +81,11 @@ exports.command = 'connect'; exports.description = `${DESCRIPTION}\n${DOCSURL}`; exports.handler = async (opts) => { + if (new Date() > new Date('2024-01-01')) { + logger.error('TensorBoard.dev has been shut down as of January 1, 2024'); + return; + } + const { file, credentials, name, description } = opts; const path = `${homedir()}/.config/tensorboard/credentials`; diff --git a/bin/legacy/deprecation.js b/bin/legacy/deprecation.js index c13e44f43..93802b038 100644 --- a/bin/legacy/deprecation.js +++ b/bin/legacy/deprecation.js @@ -1,4 +1,4 @@ -const winston = require('winston'); +const { logger } = require('../../src/logger'); // addDeprecationNotice adds middleware to the yargs chain to display a deprecation notice. const addDeprecationNotice = (opts = {}) => { @@ -13,7 +13,7 @@ const deprecationNotice = (opts, notice) => { if (driver.warn) { driver.warn(notice); } else { - winston.warn(notice); + logger.warn(notice); } }; diff --git a/package.json b/package.json index 5d19b1802..53256d05f 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "test": "jest --forceExit", "test:unit": "jest --testPathIgnorePatterns='e2e.test.js$' --forceExit", "test:e2e": "jest --testNamePattern='e2e.test.js$' --forceExit", - "do_snapshots": "jest --updateSnapshot" + "snapshot": "jest --updateSnapshot" }, "husky": { "hooks": { diff --git a/src/analytics.js b/src/analytics.js index 6fd3a3711..1d06a3d1b 100644 --- a/src/analytics.js +++ b/src/analytics.js @@ -8,7 +8,7 @@ const { promisify } = require('util'); const { scrypt } = require('crypto'); const { v4: uuidv4, v5: uuidv5, parse } = require('uuid'); const { userConfigDir } = require('appdirs'); -const winston = require('winston'); +const { logger } = require('./logger'); const isDocker = require('is-docker'); const { version: VERSION } = require('../package.json'); @@ -18,7 +18,7 @@ const { ITERATIVE_ANALYTICS_ENDPOINT = 'https://telemetry.cml.dev/api/v1/s2s/event?ip_policy=strict', ITERATIVE_ANALYTICS_TOKEN = 's2s.jtyjusrpsww4k9b76rrjri.bl62fbzrb7nd9n6vn5bpqt', ITERATIVE_DO_NOT_TRACK, - + CODESPACES, GITHUB_SERVER_URL, GITHUB_REPOSITORY_OWNER, GITHUB_ACTOR, @@ -52,7 +52,7 @@ const deterministic = async (data) => { }; const guessCI = () => { - if (GITHUB_SERVER_URL) return 'github'; + if (GITHUB_SERVER_URL && !CODESPACES) return 'github'; if (CI_SERVER_URL) return 'gitlab'; if (BITBUCKET_WORKSPACE) return 'bitbucket'; if (TF_BUILD) return 'azure'; @@ -133,7 +133,7 @@ const userId = async ({ cml } = {}) => { return id; } catch (err) { - winston.debug(`userId failure: ${err.message}`); + logger.debug(`userId failure: ${err.message}`); } }; @@ -223,7 +223,7 @@ const send = async ({ }); clearInterval(id); } catch (err) { - winston.debug(`Send analytics failed: ${err.message}`); + logger.debug(`Send analytics failed: ${err.message}`); } }; diff --git a/src/cml.js b/src/cml.js index fe971896d..95df80442 100644 --- a/src/cml.js +++ b/src/cml.js @@ -6,7 +6,7 @@ const git = require('simple-git')('./'); const path = require('path'); const fs = require('fs').promises; const chokidar = require('chokidar'); -const winston = require('winston'); +const { logger } = require('./logger'); const remark = require('remark'); const visit = require('unist-util-visit'); @@ -138,7 +138,7 @@ class CML { try { return await exec('git', 'rev-parse', ref); } catch (err) { - winston.warn( + logger.warn( 'Failed to obtain SHA. Perhaps not in the correct git folder' ); } @@ -224,7 +224,7 @@ class CML { if (node.url && !isWatermark(node)) { // Check for embedded images from dvclive if (node.url.startsWith('data:image/')) { - winston.debug( + logger.debug( `found already embedded image, head: ${node.url.slice(0, 25)}` ); const encodedData = node.url.slice(node.url.indexOf(',') + 1); @@ -253,7 +253,7 @@ class CML { }); } catch (err) { if (err.code === 'ENOENT') - winston.debug(`file not found: ${node.url} (${absolutePath})`); + logger.debug(`file not found: ${node.url} (${absolutePath})`); else throw err; } } @@ -281,7 +281,7 @@ class CML { if (lock) return; lock = true; try { - winston.info(`watcher event: ${event} ${path}`); + logger.info(`watcher event: ${event} ${path}`); await this.commentCreate({ ...opts, update: update || !first, @@ -291,12 +291,12 @@ class CML { await fs.unlink(triggerFile); } } catch (err) { - winston.warn(err); + logger.warn(err); } first = false; lock = false; }); - winston.info('watching for file changes...'); + logger.info('watching for file changes...'); await waitForever(); } @@ -426,7 +426,7 @@ class CML { const env = {}; const sensitive = [ '_CML_RUNNER_SENSITIVE_ENV', - ...process.env._CML_RUNNER_SENSITIVE_ENV.split(':') + ...(process.env._CML_RUNNER_SENSITIVE_ENV || '').split(':') ]; for (const variable in process.env) if (!sensitive.includes(variable)) env[variable] = process.env[variable]; @@ -560,7 +560,7 @@ class CML { const { files } = await git.status(); if (!files.length && globs.length) { - winston.warn('No changed files matched by glob path. Nothing to do.'); + logger.warn('No changed files matched by glob path. Nothing to do.'); return; } @@ -575,7 +575,7 @@ class CML { ); if (!paths.length && globs.length) { - winston.warn('Input files are not affected. Nothing to do.'); + logger.warn('Input files are not affected. Nothing to do.'); return; } @@ -596,7 +596,7 @@ class CML { target = targetBranch; } catch (error) { - winston.error('The target branch does not exist.'); + logger.error('The target branch does not exist.'); process.exit(1); } } @@ -669,7 +669,7 @@ Automated commits for ${this.repo}/commit/${sha} created by CML. } logError(e) { - winston.error(e.message); + logger.error(e.message); } } diff --git a/src/commenttarget.js b/src/commenttarget.js index fd527216c..64484675e 100644 --- a/src/commenttarget.js +++ b/src/commenttarget.js @@ -1,4 +1,4 @@ -const winston = require('winston'); +const { logger } = require('./logger'); const SEPARATOR = '/'; @@ -22,14 +22,14 @@ async function parseCommentTarget(opts = {}) { let commitPr; switch (commentTarget.toLowerCase()) { case 'commit': - winston.debug(`Comment target "commit" mapped to "commit/${drv.sha}"`); + logger.debug(`Comment target "commit" mapped to "commit/${drv.sha}"`); return { target: 'commit', commitSha: drv.sha }; case 'pr': case 'auto': // Determine PR id from forge env vars (if we're in a PR context). prNumber = drv.pr; if (prNumber) { - winston.debug( + logger.debug( `Comment target "${commentTarget}" mapped to "pr/${prNumber}"` ); return { target: 'pr', prNumber: prNumber }; @@ -39,14 +39,14 @@ async function parseCommentTarget(opts = {}) { [commitPr = {}] = await drv.commitPrs({ commitSha: drv.sha }); if (commitPr.url) { [prNumber] = commitPr.url.split('/').slice(-1); - winston.debug( + logger.debug( `Comment target "${commentTarget}" mapped to "pr/${prNumber}" based on commit "${drv.sha}"` ); return { target: 'pr', prNumber }; } // If target is 'auto', fallback to issuing commit comments. if (commentTarget === 'auto') { - winston.debug( + logger.debug( `Comment target "${commentTarget}" mapped to "commit/${drv.sha}"` ); return { target: 'commit', commitSha: drv.sha }; diff --git a/src/drivers/bitbucket_cloud.e2e.test.js b/src/drivers/bitbucket_cloud.e2e.test.js index 7715b5913..60cd615b0 100644 --- a/src/drivers/bitbucket_cloud.e2e.test.js +++ b/src/drivers/bitbucket_cloud.e2e.test.js @@ -1,9 +1,9 @@ const BitbucketCloud = require('./bitbucket_cloud'); const { - TEST_BBCLOUD_TOKEN: TOKEN, - TEST_BBCLOUD_REPO: REPO, - TEST_BBCLOUD_SHA: SHA, - TEST_BBCLOUD_ISSUE: ISSUE = 1 + TEST_BITBUCKET_TOKEN: TOKEN, + TEST_BITBUCKET_REPO: REPO, + TEST_BITBUCKET_SHA: SHA, + TEST_BITBUCKET_ISSUE: ISSUE = 1 } = process.env; describe('Non Enviromental tests', () => { diff --git a/src/drivers/bitbucket_cloud.js b/src/drivers/bitbucket_cloud.js index 76680da79..c54e3f907 100644 --- a/src/drivers/bitbucket_cloud.js +++ b/src/drivers/bitbucket_cloud.js @@ -1,10 +1,10 @@ const crypto = require('crypto'); const fetch = require('node-fetch'); -const winston = require('winston'); const { URL } = require('url'); const { spawn } = require('child_process'); const FormData = require('form-data'); const ProxyAgent = require('proxy-agent'); +const { logger } = require('../logger'); const { fetchUploadData, exec, gpuPresent, sleep } = require('../utils'); @@ -168,7 +168,7 @@ class BitbucketCloud { const { projectPath } = this; const { workdir, name, labels, env } = opts; - winston.warn( + logger.warn( `Bitbucket runner is working under /tmp folder and not under ${workdir} as expected` ); @@ -320,7 +320,7 @@ class BitbucketCloud { } async prAutoMerge({ pullRequestId, mergeMode, mergeMessage }) { - winston.warn( + logger.warn( 'Auto-merge is unsupported by Bitbucket Cloud; see https://jira.atlassian.com/browse/BCLOUD-14286. Trying to merge immediately...' ); const { projectPath } = this; @@ -420,7 +420,7 @@ class BitbucketCloud { const { projectPath } = this; if (!id && jobId) - winston.warn('BitBucket Cloud does not support pipelineRerun by jobId!'); + logger.warn('BitBucket Cloud does not support pipelineRerun by jobId!'); const { target } = await this.request({ endpoint: `/repositories/${projectPath}/pipelines/${id}`, @@ -435,7 +435,7 @@ class BitbucketCloud { } async pipelineJobs(opts = {}) { - winston.warn('BitBucket Cloud does not support pipelineJobs yet!'); + logger.warn('BitBucket Cloud does not support pipelineJobs yet!'); return []; } @@ -527,7 +527,7 @@ class BitbucketCloud { headers['Content-Type'] = 'application/json'; const requestUrl = url || `${api}${endpoint}`; - winston.debug( + logger.debug( `Bitbucket API request, method: ${method}, url: "${requestUrl}"` ); const response = await fetch(requestUrl, { @@ -542,7 +542,7 @@ class BitbucketCloud { : await response.text(); if (!response.ok) { - winston.debug(`Response status is ${response.status}`); + logger.debug(`Response status is ${response.status}`); // Attempt to get additional context. We have observed two different error schemas // from BitBucket API responses: `{"error": {"message": "Error message"}}` and // `{"error": "Error message"}`, apart from plain text responses like `Bad Request`. @@ -558,7 +558,7 @@ class BitbucketCloud { } warn(message) { - winston.warn(message); + logger.warn(message); } } diff --git a/src/drivers/github.js b/src/drivers/github.js index ad5e88c13..d0ca8f727 100644 --- a/src/drivers/github.js +++ b/src/drivers/github.js @@ -12,7 +12,7 @@ const tar = require('tar'); const ProxyAgent = require('proxy-agent'); const { download, exec, sleep } = require('../utils'); -const winston = require('winston'); +const { logger } = require('../logger'); const CHECK_TITLE = 'CML Report'; process.env.RUNNER_ALLOW_RUNASROOT = 1; @@ -55,7 +55,7 @@ const octokit = (token, repo, log) => { const throttleHandler = (reason, offset) => async (retryAfter, options) => { if (options.request.retryCount <= 5) { - winston.info( + logger.info( `Retrying because of ${reason} in ${retryAfter + offset} seconds` ); await new Promise((resolve) => setTimeout(resolve, offset * 1000)); @@ -184,9 +184,9 @@ class Github { const warning = 'This command only works inside a Github runner or a Github app.'; - if (!CI || TPI_TASK) winston.warn(warning); + if (!CI || TPI_TASK) logger.warn(warning); if (GITHUB_TOKEN && GITHUB_TOKEN !== this.token) - winston.warn( + logger.warn( `Your token is different than the GITHUB_TOKEN, this command does not work with PAT. ${warning}` ); @@ -209,7 +209,7 @@ class Github { async runnerToken() { const { owner, repo } = ownerRepo({ uri: this.repo }); - const { actions } = octokit(this.token, this.repo, winston); + const { actions } = octokit(this.token, this.repo, logger); if (typeof repo !== 'undefined') { const { @@ -238,7 +238,7 @@ class Github { async unregisterRunner(opts) { const { runnerId } = opts; const { owner, repo } = ownerRepo({ uri: this.repo }); - const { actions } = octokit(this.token, this.repo, winston); + const { actions } = octokit(this.token, this.repo, logger); if (typeof repo !== 'undefined') { await actions.deleteSelfHostedRunnerFromRepo({ @@ -309,7 +309,7 @@ class Github { async runners(opts = {}) { const { owner, repo } = ownerRepo({ uri: this.repo }); - const { paginate, actions } = octokit(this.token, this.repo, winston); + const { paginate, actions } = octokit(this.token, this.repo, logger); let runners; if (typeof repo === 'undefined') { @@ -331,7 +331,7 @@ class Github { async runnerById(opts = {}) { const { id } = opts; const { owner, repo } = ownerRepo({ uri: this.repo }); - const { actions } = octokit(this.token, this.repo, winston); + const { actions } = octokit(this.token, this.repo, logger); if (typeof repo === 'undefined') { const { data: runner } = await actions.getSelfHostedRunnerForOrg({ @@ -353,7 +353,7 @@ class Github { async runnerJob({ runnerId, status = 'queued' } = {}) { const { owner, repo } = ownerRepo({ uri: this.repo }); - const octokitClient = octokit(this.token, this.repo, winston); + const octokitClient = octokit(this.token, this.repo, logger); if (status === 'running') status = 'in_progress'; @@ -532,18 +532,18 @@ class Github { try { if (await this.isProtected({ branch: base })) { - winston.warn( + logger.warn( `Failed to enable auto-merge: Enable the feature in your repository settings: ${settingsUrl}#merge_types_auto_merge. Trying to merge immediately...` ); } else { - winston.warn( + logger.warn( `Failed to enable auto-merge: Set up branch protection and add "required status checks" for branch '${base}': ${settingsUrl}/branches. Trying to merge immediately...` ); } } catch (err) { if (!err.message.includes('Resource not accessible by integration')) throw err; - winston.warn( + logger.warn( `Failed to enable auto-merge. Trying to merge immediately...` ); } @@ -687,7 +687,7 @@ class Github { async pipelineRerun({ id = GITHUB_RUN_ID, jobId } = {}) { const { owner, repo } = ownerRepo({ uri: this.repo }); - const { actions } = octokit(this.token, this.repo, winston); + const { actions } = octokit(this.token, this.repo, logger); if (!id && jobId) { ({ @@ -736,7 +736,7 @@ class Github { async pipelineJobs(opts = {}) { const { jobs: runnerJobs } = opts; const { owner, repo } = ownerRepo({ uri: this.repo }); - const { actions } = octokit(this.token, this.repo, winston); + const { actions } = octokit(this.token, this.repo, logger); const jobs = await Promise.all( runnerJobs.map(async (job) => { diff --git a/src/drivers/gitlab.js b/src/drivers/gitlab.js index 3360e39c7..f6f0e471f 100644 --- a/src/drivers/gitlab.js +++ b/src/drivers/gitlab.js @@ -7,7 +7,7 @@ const fse = require('fs-extra'); const { resolve } = require('path'); const ProxyAgent = require('proxy-agent'); const { backOff } = require('exponential-backoff'); -const winston = require('winston'); +const { logger } = require('../logger'); const { fetchUploadData, download, gpuPresent } = require('../utils'); @@ -144,11 +144,21 @@ class Gitlab { return { uri: `${repo}${url}`, mime, size }; } - async runnerToken() { + async runnerToken(body) { const projectPath = await this.projectPath(); - const endpoint = `/projects/${projectPath}`; + const legacyEndpoint = `/projects/${projectPath}`; + const endpoint = `/user/runners`; - const { runners_token: runnersToken } = await this.request({ endpoint }); + const { id, runners_token: runnersToken } = await this.request({ + endpoint: legacyEndpoint + }); + + if (runnersToken === null) { + if (!body) body = new URLSearchParams(); + body.append('project_id', id); + body.append('runner_type', 'project_type'); + return (await this.request({ endpoint, method: 'POST', body })).token; + } return runnersToken; } @@ -156,16 +166,18 @@ class Gitlab { async registerRunner(opts = {}) { const { tags, name } = opts; - const token = await this.runnerToken(); const endpoint = `/runners`; const body = new URLSearchParams(); body.append('description', name); body.append('tag_list', tags); - body.append('token', token); body.append('locked', 'true'); body.append('run_untagged', 'true'); body.append('access_level', 'not_protected'); + const token = await this.runnerToken(new URLSearchParams(body)); + if (token.startsWith('glrt-')) return { token }; + + body.append('token', token); return await this.request({ endpoint, method: 'POST', body }); } @@ -330,7 +342,7 @@ class Gitlab { }) ); } catch ({ message }) { - winston.warn( + logger.warn( `Failed to enable auto-merge: ${message}. Trying to merge immediately...` ); body.set('merge_when_pipeline_succeeds', false); @@ -560,7 +572,7 @@ class Gitlab { } if (!url) throw new Error('Gitlab API endpoint not found'); - winston.debug(`Gitlab API request, method: ${method}, url: "${url}"`); + logger.debug(`Gitlab API request, method: ${method}, url: "${url}"`); const headers = { 'PRIVATE-TOKEN': token, Accept: 'application/json' }; const response = await fetch(url, { @@ -570,7 +582,7 @@ class Gitlab { agent: new ProxyAgent() }); if (!response.ok) { - winston.debug(`Response status is ${response.status}`); + logger.debug(`Response status is ${response.status}`); throw new Error(response.statusText); } if (raw) return response; @@ -579,7 +591,7 @@ class Gitlab { } warn(message) { - winston.warn(message); + logger.warn(message); } } diff --git a/src/logger.js b/src/logger.js new file mode 100644 index 000000000..e1e7a5c7c --- /dev/null +++ b/src/logger.js @@ -0,0 +1,32 @@ +const logger = require('winston'); + +const setupLogger = (opts) => { + const { log: level, silent } = opts; + + logger.configure({ + format: process.stdout.isTTY + ? logger.format.combine( + logger.format.colorize({ all: true }), + logger.format.simple() + ) + : logger.format.combine( + logger.format.errors({ stack: true }), + logger.format.json() + ), + transports: [ + new logger.transports.Console({ + stderrLevels: Object.keys(logger.config.npm.levels), + handleExceptions: true, + handleRejections: true, + level, + silent + }) + ] + }); +}; + +if (typeof jest !== 'undefined') { + setupLogger({ log: 'debug', silent: true }); +} + +module.exports = { logger, setupLogger }; diff --git a/src/terraform.js b/src/terraform.js index 344438f4d..11776a5aa 100644 --- a/src/terraform.js +++ b/src/terraform.js @@ -1,6 +1,6 @@ const fs = require('fs').promises; const { ltr } = require('semver'); -const winston = require('winston'); +const { logger } = require('./logger'); const { exec, tfCapture } = require('./utils'); const MIN_TF_VER = '0.14.0'; @@ -123,7 +123,7 @@ const iterativeCmlRunnerTpl = (opts = {}) => { } } }; - winston.debug(`terraform data: ${JSON.stringify(tfObj)}`); + logger.debug(`terraform data: ${JSON.stringify(tfObj)}`); return tfObj; }; diff --git a/src/terraform.test.js b/src/terraform.test.js index c165cdd04..6240a3753 100644 --- a/src/terraform.test.js +++ b/src/terraform.test.js @@ -1,18 +1,6 @@ -const winston = require('winston'); const { iterativeCmlRunnerTpl } = require('./terraform'); describe('Terraform tests', () => { - beforeAll(() => { - winston.configure({ - transports: [ - new winston.transports.Console({ - level: 'error', - handleExceptions: true - }) - ] - }); - }); - test('default options', async () => { const output = iterativeCmlRunnerTpl({}); expect(JSON.stringify(output, null, 2)).toMatchInlineSnapshot(` diff --git a/src/utils.js b/src/utils.js index 7bf891412..2fbed55e4 100644 --- a/src/utils.js +++ b/src/utils.js @@ -5,7 +5,7 @@ const fetch = require('node-fetch'); const ProxyAgent = require('proxy-agent'); const NodeSSH = require('node-ssh').NodeSSH; const stripAnsi = require('strip-ansi'); -const winston = require('winston'); +const { logger } = require('./logger'); const uuid = require('uuid'); const getOS = require('getos'); @@ -212,12 +212,12 @@ const tfCapture = async (command, args = [], options = {}) => { try { const { '@level': level, '@message': message } = JSON.parse(line); if (level === 'error') { - winston.error(`terraform error: ${message}`); + logger.error(`terraform error: ${message}`); } else { - winston.info(message); + logger.info(message); } } catch (err) { - winston.info(line); + logger.info(line); } }; buf.toString('utf8').split('\n').forEach(parse);