├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── check.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── README.md ├── action.yml ├── assets └── vega-lite.json ├── dist ├── 37.index.js └── index.js ├── package-lock.json ├── package.json ├── prettier.config.js ├── src └── setup-cml.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | !src/ 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'eslint:recommended', 4 | 'plugin:@typescript-eslint/recommended', 5 | 'plugin:eslint-plugin-jest/recommended', 6 | 'eslint-config-prettier' 7 | ], 8 | parser: '@typescript-eslint/parser', 9 | plugins: ['@typescript-eslint', 'eslint-plugin-node', 'eslint-plugin-jest'], 10 | rules: { 11 | '@typescript-eslint/no-require-imports': 'error', 12 | '@typescript-eslint/no-non-null-assertion': 'off', 13 | '@typescript-eslint/no-explicit-any': 'off', 14 | '@typescript-eslint/no-empty-function': 'off', 15 | '@typescript-eslint/ban-ts-comment': [ 16 | 'error', 17 | { 18 | 'ts-ignore': 'allow-with-description' 19 | } 20 | ], 21 | 'no-console': 'error', 22 | 'yoda': 'error', 23 | 'prefer-const': [ 24 | 'error', 25 | { 26 | destructuring: 'all' 27 | } 28 | ], 29 | 'no-control-regex': 'off', 30 | 'no-constant-condition': ['error', {checkLoops: false}], 31 | 'node/no-extraneous-import': 'error' 32 | }, 33 | env: { 34 | node: true, 35 | es6: true, 36 | 'jest/globals': true 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check CML GitHub Action 2 | on: 3 | schedule: 4 | - cron: 0 0 * * * 5 | pull_request: 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | checks: write 11 | 12 | jobs: 13 | verify: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - run: | 18 | npm ci 19 | npm run format 20 | npm run lint 21 | npm run build 22 | git diff --exit-code 23 | test-latest: 24 | defaults: 25 | run: 26 | shell: bash 27 | strategy: 28 | max-parallel: 1 29 | matrix: 30 | system: 31 | - ubuntu-latest 32 | - macos-latest 33 | - windows-latest 34 | runs-on: ${{ matrix.system }} 35 | steps: 36 | - uses: actions/checkout@v3 37 | - name: run action with latest 38 | uses: ./ 39 | - name: test 40 | run: cml --version 41 | - name: test CML 42 | env: 43 | REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | run: | 45 | echo 'Hello CML from ${{ matrix.system }}!' > report.md 46 | cml comment create report.md 47 | cml check create report.md 48 | test-version: 49 | defaults: 50 | run: 51 | shell: bash 52 | strategy: 53 | max-parallel: 1 54 | matrix: 55 | system: 56 | - ubuntu-latest 57 | - macos-latest 58 | - windows-latest 59 | runs-on: ${{ matrix.system }} 60 | steps: 61 | - uses: actions/checkout@v3 62 | - name: run action with latest 63 | uses: ./ 64 | with: 65 | version: v0.15.1 66 | - name: test CML specific version 67 | run: | 68 | if [ "$(cml --version 2>&1)" != '0.15.1' ]; then 69 | exit 1 70 | fi 71 | echo OK 72 | test-version-prefix: 73 | defaults: 74 | run: 75 | shell: bash 76 | strategy: 77 | max-parallel: 1 78 | matrix: 79 | system: 80 | - ubuntu-latest 81 | - macos-latest 82 | - windows-latest 83 | runs-on: ${{ matrix.system }} 84 | steps: 85 | - uses: actions/checkout@v3 86 | - name: run action with latest 87 | uses: ./ 88 | with: 89 | version: 0.15.1 90 | - name: test CML specific version 91 | run: | 92 | if [ "$(cml --version 2>&1)" != '0.15.1' ]; then 93 | exit 1 94 | fi 95 | echo OK 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | !.github/ 4 | !src/ 5 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 80, 3 | tabWidth: 2, 4 | useTabs: false, 5 | semi: true, 6 | singleQuote: true, 7 | trailingComma: 'none', 8 | bracketSpacing: false, 9 | arrowParens: 'avoid' 10 | }; 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Setup CML Action 2 | 3 | ![CML](https://user-images.githubusercontent.com/414967/90448663-1ce39c00-e0e6-11ea-8083-710825d2e94e.png) 4 | 5 | Continuous Machine Learning ([CML](https://cml.dev)) is an open-source library 6 | for implementing continuous integration & delivery (CI/CD) in machine learning 7 | projects. Use it to automate parts of your development workflow, including 8 | machine provisioning; model training and evaluation; comparing ML experiments 9 | across your project history, and monitoring changing datasets. 10 | 11 | The [iterative/setup-cml](https://github.com/iterative/setup-cml) can be used as 12 | a GitHub Action to provide [CML](https://cml.dev) functions in your workflow. 13 | The action allows users to install CML without using the CML Docker container. 14 | 15 | This action gives you: 16 | 17 | - Access to all [CML functions](https://github.com/iterative/cml#cml-functions). 18 | For example: 19 | - `cml comment create` for publishing data visualization and metrics from your 20 | CI workflow as comments in a pull request. 21 | - `cml pr create` to open a pull request. 22 | - `cml runner launch`, a function that enables workflows to provision cloud 23 | and on-premise computing resources for training models. 24 | - The freedom 🦅 to mix and match CML with your favorite data science tools and 25 | environments. 26 | 27 | Note that CML does not include DVC and its dependencies (see the 28 | [Setup DVC Action](https://github.com/iterative/setup-dvc)). 29 | 30 | ## Note on v2 31 | 32 | `v1` of setup-cml was a wrapper around a set of npm installs. `v2` installs CML from its 33 | pre-packaged binaries. Then attempts to run `npm install --global canvas@2 vega@5 vega-cli@5 vega-lite@5` 34 | if you do not wish to install these tools pass `vega: false` to the action. 35 | 36 | link to [`v1`](https://github.com/iterative/setup-cml/tree/v1) 37 | ## Usage 38 | 39 | This action is tested on `ubuntu-latest`, `macos-latest` and `windows-latest`. 40 | 41 | Basic usage: 42 | 43 | ```yaml 44 | steps: 45 | - uses: actions/checkout@v3 46 | - uses: iterative/setup-cml@v2 47 | ``` 48 | 49 | A specific version can be pinned to your workflow. 50 | 51 | ```yaml 52 | steps: 53 | - uses: actions/checkout@v3 54 | - uses: iterative/setup-cml@v2 55 | with: 56 | version: 'v0.18.1' 57 | ``` 58 | 59 | Without vega tools 60 | ```yaml 61 | steps: 62 | - uses: actions/checkout@v3 63 | - uses: iterative/setup-cml@v2 64 | with: 65 | version: 'v0.20.0' 66 | vega: false 67 | ``` 68 | 69 | ## Inputs 70 | 71 | The following inputs are supported. 72 | 73 | - `version` - (optional) The version of CML to install (e.g. '0.18.1'). Defaults 74 | to `latest` for the 75 | [most recent CML release](https://github.com/iterative/cml/releases). 76 | - `vega` - (optional) Whether to install vega dependencies. Defaults to `true`. 77 | runs command `npm install --global canvas@2 vega@5 vega-cli@5 vega-lite@5` 78 | 79 | ## A complete example 80 | 81 | ![](https://static.iterative.ai/img/cml/first_report.png) _A sample CML report 82 | from a machine learning project displayed in a Pull Request._ 83 | 84 | Assume that we have a machine learning script, `train.py` which outputs an image 85 | `plot.png`: 86 | 87 | ```yaml 88 | steps: 89 | - uses: actions/checkout@v2 90 | - uses: iterative/setup-cml@v2 91 | - env: 92 | REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Can use the default token for most functions 93 | run: | 94 | python train.py --output plot.png 95 | 96 | echo 'My first CML report' > report.md 97 | echo '![](./plot.png)' >> report.md 98 | cml comment create --publish report.md 99 | ``` 100 | In general [GitHub's runner token can be given enough permissions](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token) to perform most functions. 101 | When using the `cml runner launch` command a [PAT is required](https://cml.dev/doc/self-hosted-runners?tab=GitHub#personal-access-token) 102 | 103 | ### CML functions 104 | 105 | CML provides several helper functions. See [the docs](https://cml.dev/doc). 106 | 107 | ## Contributing 108 | 109 | To get started after cloning the repo, run `npm ci` (clean-install). 110 | Before pushing changes or opening a PR run `npm run format && npm run lint` to 111 | ensure that the code is formatted and linted. 112 | 113 | run `npm run build` to compile the action. 114 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: Setup CML (Continuous Machine Learning) 2 | description: Sets up CML (Continuous Machine Learning) - https://cml.dev. 3 | author: Iterative, Inc. 4 | inputs: 5 | version: 6 | description: The version of CML to install (e.g. '3.0.0'). 7 | default: latest 8 | required: false 9 | vega: 10 | description: Whether to install Vega tools 11 | default: 'true' 12 | required: false 13 | runs: 14 | using: node16 15 | main: dist/index.js 16 | branding: 17 | icon: terminal 18 | color: purple 19 | -------------------------------------------------------------------------------- /assets/vega-lite.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 3 | "description": "A simple bar chart with embedded data.", 4 | "data": { 5 | "values": [ 6 | { "a": "A", "b": 28 }, 7 | { "a": "B", "b": 55 }, 8 | { "a": "C", "b": 43 }, 9 | { "a": "D", "b": 91 }, 10 | { "a": "E", "b": 81 }, 11 | { "a": "F", "b": 53 }, 12 | { "a": "G", "b": 19 }, 13 | { "a": "H", "b": 87 }, 14 | { "a": "I", "b": 52 } 15 | ] 16 | }, 17 | "mark": "bar", 18 | "encoding": { 19 | "x": { "field": "a", "type": "nominal", "axis": { "labelAngle": 0 } }, 20 | "y": { "field": "b", "type": "quantitative" } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /dist/37.index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.id = 37; 3 | exports.ids = [37]; 4 | exports.modules = { 5 | 6 | /***/ 4037: 7 | /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { 8 | 9 | __webpack_require__.r(__webpack_exports__); 10 | /* harmony export */ __webpack_require__.d(__webpack_exports__, { 11 | /* harmony export */ "toFormData": () => (/* binding */ toFormData) 12 | /* harmony export */ }); 13 | /* harmony import */ var fetch_blob_from_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2777); 14 | /* harmony import */ var formdata_polyfill_esm_min_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(8010); 15 | 16 | 17 | 18 | let s = 0; 19 | const S = { 20 | START_BOUNDARY: s++, 21 | HEADER_FIELD_START: s++, 22 | HEADER_FIELD: s++, 23 | HEADER_VALUE_START: s++, 24 | HEADER_VALUE: s++, 25 | HEADER_VALUE_ALMOST_DONE: s++, 26 | HEADERS_ALMOST_DONE: s++, 27 | PART_DATA_START: s++, 28 | PART_DATA: s++, 29 | END: s++ 30 | }; 31 | 32 | let f = 1; 33 | const F = { 34 | PART_BOUNDARY: f, 35 | LAST_BOUNDARY: f *= 2 36 | }; 37 | 38 | const LF = 10; 39 | const CR = 13; 40 | const SPACE = 32; 41 | const HYPHEN = 45; 42 | const COLON = 58; 43 | const A = 97; 44 | const Z = 122; 45 | 46 | const lower = c => c | 0x20; 47 | 48 | const noop = () => {}; 49 | 50 | class MultipartParser { 51 | /** 52 | * @param {string} boundary 53 | */ 54 | constructor(boundary) { 55 | this.index = 0; 56 | this.flags = 0; 57 | 58 | this.onHeaderEnd = noop; 59 | this.onHeaderField = noop; 60 | this.onHeadersEnd = noop; 61 | this.onHeaderValue = noop; 62 | this.onPartBegin = noop; 63 | this.onPartData = noop; 64 | this.onPartEnd = noop; 65 | 66 | this.boundaryChars = {}; 67 | 68 | boundary = '\r\n--' + boundary; 69 | const ui8a = new Uint8Array(boundary.length); 70 | for (let i = 0; i < boundary.length; i++) { 71 | ui8a[i] = boundary.charCodeAt(i); 72 | this.boundaryChars[ui8a[i]] = true; 73 | } 74 | 75 | this.boundary = ui8a; 76 | this.lookbehind = new Uint8Array(this.boundary.length + 8); 77 | this.state = S.START_BOUNDARY; 78 | } 79 | 80 | /** 81 | * @param {Uint8Array} data 82 | */ 83 | write(data) { 84 | let i = 0; 85 | const length_ = data.length; 86 | let previousIndex = this.index; 87 | let {lookbehind, boundary, boundaryChars, index, state, flags} = this; 88 | const boundaryLength = this.boundary.length; 89 | const boundaryEnd = boundaryLength - 1; 90 | const bufferLength = data.length; 91 | let c; 92 | let cl; 93 | 94 | const mark = name => { 95 | this[name + 'Mark'] = i; 96 | }; 97 | 98 | const clear = name => { 99 | delete this[name + 'Mark']; 100 | }; 101 | 102 | const callback = (callbackSymbol, start, end, ui8a) => { 103 | if (start === undefined || start !== end) { 104 | this[callbackSymbol](ui8a && ui8a.subarray(start, end)); 105 | } 106 | }; 107 | 108 | const dataCallback = (name, clear) => { 109 | const markSymbol = name + 'Mark'; 110 | if (!(markSymbol in this)) { 111 | return; 112 | } 113 | 114 | if (clear) { 115 | callback(name, this[markSymbol], i, data); 116 | delete this[markSymbol]; 117 | } else { 118 | callback(name, this[markSymbol], data.length, data); 119 | this[markSymbol] = 0; 120 | } 121 | }; 122 | 123 | for (i = 0; i < length_; i++) { 124 | c = data[i]; 125 | 126 | switch (state) { 127 | case S.START_BOUNDARY: 128 | if (index === boundary.length - 2) { 129 | if (c === HYPHEN) { 130 | flags |= F.LAST_BOUNDARY; 131 | } else if (c !== CR) { 132 | return; 133 | } 134 | 135 | index++; 136 | break; 137 | } else if (index - 1 === boundary.length - 2) { 138 | if (flags & F.LAST_BOUNDARY && c === HYPHEN) { 139 | state = S.END; 140 | flags = 0; 141 | } else if (!(flags & F.LAST_BOUNDARY) && c === LF) { 142 | index = 0; 143 | callback('onPartBegin'); 144 | state = S.HEADER_FIELD_START; 145 | } else { 146 | return; 147 | } 148 | 149 | break; 150 | } 151 | 152 | if (c !== boundary[index + 2]) { 153 | index = -2; 154 | } 155 | 156 | if (c === boundary[index + 2]) { 157 | index++; 158 | } 159 | 160 | break; 161 | case S.HEADER_FIELD_START: 162 | state = S.HEADER_FIELD; 163 | mark('onHeaderField'); 164 | index = 0; 165 | // falls through 166 | case S.HEADER_FIELD: 167 | if (c === CR) { 168 | clear('onHeaderField'); 169 | state = S.HEADERS_ALMOST_DONE; 170 | break; 171 | } 172 | 173 | index++; 174 | if (c === HYPHEN) { 175 | break; 176 | } 177 | 178 | if (c === COLON) { 179 | if (index === 1) { 180 | // empty header field 181 | return; 182 | } 183 | 184 | dataCallback('onHeaderField', true); 185 | state = S.HEADER_VALUE_START; 186 | break; 187 | } 188 | 189 | cl = lower(c); 190 | if (cl < A || cl > Z) { 191 | return; 192 | } 193 | 194 | break; 195 | case S.HEADER_VALUE_START: 196 | if (c === SPACE) { 197 | break; 198 | } 199 | 200 | mark('onHeaderValue'); 201 | state = S.HEADER_VALUE; 202 | // falls through 203 | case S.HEADER_VALUE: 204 | if (c === CR) { 205 | dataCallback('onHeaderValue', true); 206 | callback('onHeaderEnd'); 207 | state = S.HEADER_VALUE_ALMOST_DONE; 208 | } 209 | 210 | break; 211 | case S.HEADER_VALUE_ALMOST_DONE: 212 | if (c !== LF) { 213 | return; 214 | } 215 | 216 | state = S.HEADER_FIELD_START; 217 | break; 218 | case S.HEADERS_ALMOST_DONE: 219 | if (c !== LF) { 220 | return; 221 | } 222 | 223 | callback('onHeadersEnd'); 224 | state = S.PART_DATA_START; 225 | break; 226 | case S.PART_DATA_START: 227 | state = S.PART_DATA; 228 | mark('onPartData'); 229 | // falls through 230 | case S.PART_DATA: 231 | previousIndex = index; 232 | 233 | if (index === 0) { 234 | // boyer-moore derrived algorithm to safely skip non-boundary data 235 | i += boundaryEnd; 236 | while (i < bufferLength && !(data[i] in boundaryChars)) { 237 | i += boundaryLength; 238 | } 239 | 240 | i -= boundaryEnd; 241 | c = data[i]; 242 | } 243 | 244 | if (index < boundary.length) { 245 | if (boundary[index] === c) { 246 | if (index === 0) { 247 | dataCallback('onPartData', true); 248 | } 249 | 250 | index++; 251 | } else { 252 | index = 0; 253 | } 254 | } else if (index === boundary.length) { 255 | index++; 256 | if (c === CR) { 257 | // CR = part boundary 258 | flags |= F.PART_BOUNDARY; 259 | } else if (c === HYPHEN) { 260 | // HYPHEN = end boundary 261 | flags |= F.LAST_BOUNDARY; 262 | } else { 263 | index = 0; 264 | } 265 | } else if (index - 1 === boundary.length) { 266 | if (flags & F.PART_BOUNDARY) { 267 | index = 0; 268 | if (c === LF) { 269 | // unset the PART_BOUNDARY flag 270 | flags &= ~F.PART_BOUNDARY; 271 | callback('onPartEnd'); 272 | callback('onPartBegin'); 273 | state = S.HEADER_FIELD_START; 274 | break; 275 | } 276 | } else if (flags & F.LAST_BOUNDARY) { 277 | if (c === HYPHEN) { 278 | callback('onPartEnd'); 279 | state = S.END; 280 | flags = 0; 281 | } else { 282 | index = 0; 283 | } 284 | } else { 285 | index = 0; 286 | } 287 | } 288 | 289 | if (index > 0) { 290 | // when matching a possible boundary, keep a lookbehind reference 291 | // in case it turns out to be a false lead 292 | lookbehind[index - 1] = c; 293 | } else if (previousIndex > 0) { 294 | // if our boundary turned out to be rubbish, the captured lookbehind 295 | // belongs to partData 296 | const _lookbehind = new Uint8Array(lookbehind.buffer, lookbehind.byteOffset, lookbehind.byteLength); 297 | callback('onPartData', 0, previousIndex, _lookbehind); 298 | previousIndex = 0; 299 | mark('onPartData'); 300 | 301 | // reconsider the current character even so it interrupted the sequence 302 | // it could be the beginning of a new sequence 303 | i--; 304 | } 305 | 306 | break; 307 | case S.END: 308 | break; 309 | default: 310 | throw new Error(`Unexpected state entered: ${state}`); 311 | } 312 | } 313 | 314 | dataCallback('onHeaderField'); 315 | dataCallback('onHeaderValue'); 316 | dataCallback('onPartData'); 317 | 318 | // Update properties for the next call 319 | this.index = index; 320 | this.state = state; 321 | this.flags = flags; 322 | } 323 | 324 | end() { 325 | if ((this.state === S.HEADER_FIELD_START && this.index === 0) || 326 | (this.state === S.PART_DATA && this.index === this.boundary.length)) { 327 | this.onPartEnd(); 328 | } else if (this.state !== S.END) { 329 | throw new Error('MultipartParser.end(): stream ended unexpectedly'); 330 | } 331 | } 332 | } 333 | 334 | function _fileName(headerValue) { 335 | // matches either a quoted-string or a token (RFC 2616 section 19.5.1) 336 | const m = headerValue.match(/\bfilename=("(.*?)"|([^()<>@,;:\\"/[\]?={}\s\t]+))($|;\s)/i); 337 | if (!m) { 338 | return; 339 | } 340 | 341 | const match = m[2] || m[3] || ''; 342 | let filename = match.slice(match.lastIndexOf('\\') + 1); 343 | filename = filename.replace(/%22/g, '"'); 344 | filename = filename.replace(/&#(\d{4});/g, (m, code) => { 345 | return String.fromCharCode(code); 346 | }); 347 | return filename; 348 | } 349 | 350 | async function toFormData(Body, ct) { 351 | if (!/multipart/i.test(ct)) { 352 | throw new TypeError('Failed to fetch'); 353 | } 354 | 355 | const m = ct.match(/boundary=(?:"([^"]+)"|([^;]+))/i); 356 | 357 | if (!m) { 358 | throw new TypeError('no or bad content-type header, no multipart boundary'); 359 | } 360 | 361 | const parser = new MultipartParser(m[1] || m[2]); 362 | 363 | let headerField; 364 | let headerValue; 365 | let entryValue; 366 | let entryName; 367 | let contentType; 368 | let filename; 369 | const entryChunks = []; 370 | const formData = new formdata_polyfill_esm_min_js__WEBPACK_IMPORTED_MODULE_1__/* .FormData */ .Ct(); 371 | 372 | const onPartData = ui8a => { 373 | entryValue += decoder.decode(ui8a, {stream: true}); 374 | }; 375 | 376 | const appendToFile = ui8a => { 377 | entryChunks.push(ui8a); 378 | }; 379 | 380 | const appendFileToFormData = () => { 381 | const file = new fetch_blob_from_js__WEBPACK_IMPORTED_MODULE_0__/* .File */ .$B(entryChunks, filename, {type: contentType}); 382 | formData.append(entryName, file); 383 | }; 384 | 385 | const appendEntryToFormData = () => { 386 | formData.append(entryName, entryValue); 387 | }; 388 | 389 | const decoder = new TextDecoder('utf-8'); 390 | decoder.decode(); 391 | 392 | parser.onPartBegin = function () { 393 | parser.onPartData = onPartData; 394 | parser.onPartEnd = appendEntryToFormData; 395 | 396 | headerField = ''; 397 | headerValue = ''; 398 | entryValue = ''; 399 | entryName = ''; 400 | contentType = ''; 401 | filename = null; 402 | entryChunks.length = 0; 403 | }; 404 | 405 | parser.onHeaderField = function (ui8a) { 406 | headerField += decoder.decode(ui8a, {stream: true}); 407 | }; 408 | 409 | parser.onHeaderValue = function (ui8a) { 410 | headerValue += decoder.decode(ui8a, {stream: true}); 411 | }; 412 | 413 | parser.onHeaderEnd = function () { 414 | headerValue += decoder.decode(); 415 | headerField = headerField.toLowerCase(); 416 | 417 | if (headerField === 'content-disposition') { 418 | // matches either a quoted-string or a token (RFC 2616 section 19.5.1) 419 | const m = headerValue.match(/\bname=("([^"]*)"|([^()<>@,;:\\"/[\]?={}\s\t]+))/i); 420 | 421 | if (m) { 422 | entryName = m[2] || m[3] || ''; 423 | } 424 | 425 | filename = _fileName(headerValue); 426 | 427 | if (filename) { 428 | parser.onPartData = appendToFile; 429 | parser.onPartEnd = appendFileToFormData; 430 | } 431 | } else if (headerField === 'content-type') { 432 | contentType = headerValue; 433 | } 434 | 435 | headerValue = ''; 436 | headerField = ''; 437 | }; 438 | 439 | for await (const chunk of Body) { 440 | parser.write(chunk); 441 | } 442 | 443 | parser.end(); 444 | 445 | return formData; 446 | } 447 | 448 | 449 | /***/ }) 450 | 451 | }; 452 | ; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dvcorg/setup-cml", 3 | "version": "2.0.0", 4 | "author": { 5 | "name": "CML", 6 | "url": "https://cml.dev" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/iterative/setup-cml.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/iterative/setup-cml/issues" 14 | }, 15 | "keywords": [ 16 | "cml", 17 | "continuous machine learning", 18 | "github action", 19 | "github actions" 20 | ], 21 | "license": "Apache-2.0", 22 | "licenses": [ 23 | { 24 | "type": "Apache-2.0", 25 | "url": "http://www.apache.org/licenses/LICENSE-2.0" 26 | } 27 | ], 28 | "engines": { 29 | "node": ">=16.x" 30 | }, 31 | "main": "src/setup-cml.ts", 32 | "scripts": { 33 | "build": "ncc build -o dist/ src/setup-cml.ts", 34 | "format": "prettier --no-error-on-unmatched-pattern --config ./.prettierrc.js --write \"**/*.{ts,yml,yaml}\"", 35 | "format-check": "prettier --no-error-on-unmatched-pattern --config ./.prettierrc.js --check \"**/*.{ts,yml,yaml}\"", 36 | "lintfix": "eslint --config ./.eslintrc.js \"**/*.ts\" --fix", 37 | "lint": "eslint --config ./.eslintrc.js \"**/*.ts\"", 38 | "test": "jest --passWithNoTests" 39 | }, 40 | "dependencies": { 41 | "@actions/core": "^1.10.0", 42 | "@actions/exec": "^1.1.1", 43 | "@actions/tool-cache": "^2.0.1", 44 | "@octokit/rest": "^20.0.2", 45 | "node-fetch": "^3.3.2" 46 | }, 47 | "devDependencies": { 48 | "@types/jest": "^27.0.2", 49 | "@types/node": "^16.11.25", 50 | "@types/semver": "^6.0.0", 51 | "@typescript-eslint/eslint-plugin": "^5.54.0", 52 | "@typescript-eslint/parser": "^5.54.0", 53 | "@vercel/ncc": "^0.38.0", 54 | "eslint": "^8.35.0", 55 | "eslint-config-prettier": "^8.6.0", 56 | "eslint-plugin-jest": "^27.2.1", 57 | "eslint-plugin-node": "^11.1.0", 58 | "jest": "^27.2.5", 59 | "prettier": "^2.8.4", 60 | "ts-jest": "^27.0.5", 61 | "typescript": "^4.2.3" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'avoid', 3 | singleQuote: true, 4 | trailingComma: 'none', 5 | printWidth: 80, 6 | tabWidth: 2, 7 | useTabs: false, 8 | proseWrap: 'always' 9 | }; 10 | -------------------------------------------------------------------------------- /src/setup-cml.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import * as tc from '@actions/tool-cache'; 3 | import {exec} from '@actions/exec'; 4 | import fetch from 'node-fetch'; // can be remove is github actions runs node 18 > https://github.com/octokit/octokit.js/#fetch-missing 5 | import {Octokit} from '@octokit/rest'; 6 | import os from 'os'; 7 | import {chmodSync} from 'fs'; 8 | 9 | async function run() { 10 | try { 11 | const version = core.getInput('version'); 12 | const vega = core.getBooleanInput('vega'); 13 | const arch = os.arch(); 14 | const platform = os.platform(); 15 | 16 | let cmlPath = tc.find('cml', version); 17 | if (cmlPath) { 18 | core.addPath(cmlPath); 19 | } else { 20 | const filename = deriveCMLAsset(arch, platform); 21 | const {url, version: retrievedVersion} = await getCmlDownloadUrl( 22 | version, 23 | filename 24 | ); 25 | cmlPath = await tc.downloadTool(url); 26 | const cachedCML = await tc.cacheFile( 27 | cmlPath, 28 | 'cml', 29 | 'cml', 30 | retrievedVersion 31 | ); 32 | chmodSync(`${cachedCML}/cml`, '755'); 33 | core.addPath(cachedCML); 34 | } 35 | if (vega) { 36 | try { 37 | exec('npm install --global canvas@2 vega@5 vega-cli@5 vega-lite@5'); 38 | } catch (error: any) { 39 | core.warning('Failed to intall vega tools'); 40 | } 41 | } 42 | } catch (error: any) { 43 | core.setFailed(error); 44 | } 45 | } 46 | 47 | interface CMLDownloadResponse { 48 | url: string; 49 | version: string; 50 | } 51 | 52 | async function getCmlDownloadUrl( 53 | version: string, 54 | assetName: string 55 | ): Promise { 56 | const octokit = new Octokit({ 57 | token: process.env.GITHUB_TOKEN, 58 | request: { 59 | fetch: fetch 60 | } 61 | }); 62 | let release; 63 | if (version == 'latest') { 64 | const response = await octokit.repos.getLatestRelease({ 65 | owner: 'iterative', 66 | repo: 'cml' 67 | }); 68 | release = response.data; 69 | } else { 70 | const response = await octokit.repos.getReleaseByTag({ 71 | owner: 'iterative', 72 | repo: 'cml', 73 | tag: version.startsWith('v') ? version : `v${version}` 74 | }); 75 | release = response.data; 76 | } 77 | const {tag_name, assets} = release; 78 | const asset = assets.find(asset => asset.name == assetName); 79 | if (!asset) { 80 | throw new Error( 81 | `Could not find CML binary ${assetName} for version ${version}` 82 | ); 83 | } 84 | return {url: asset.browser_download_url, version: tag_name}; 85 | } 86 | 87 | function deriveCMLAsset(arch: string, platform: string): string { 88 | if (!(arch == 'x64' || arch == 'arm64')) { 89 | throw new Error(`CML Unsupported architecture ${arch}`); 90 | } 91 | switch (platform) { 92 | case 'darwin': 93 | return `cml-macos-${arch}`; 94 | case 'linux': 95 | return `cml-linux-${arch}`; 96 | case 'win32': 97 | return `cml-win-${arch}.exe`; 98 | default: 99 | throw new Error(`CML Unsupported platform ${platform}`); 100 | } 101 | } 102 | 103 | run(); 104 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 4 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 5 | "outDir": "./lib", /* Redirect output structure to the directory. */ 6 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 7 | "sourceMap": true, 8 | "strict": true, /* Enable all strict type-checking options. */ 9 | "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ 10 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 11 | "resolveJsonModule": true, /* Allows importing modules with a '.json' extension, which is a common practice in node projects. */ 12 | }, 13 | "exclude": ["__tests__", "lib", "node_modules"] 14 | } 15 | --------------------------------------------------------------------------------