diff --git a/.dependancron b/.dependancron index a6e7bcb30..c945ef144 100644 --- a/.dependancron +++ b/.dependancron @@ -1 +1 @@ -2.0.17 +2.0.18 diff --git a/README.md b/README.md index 8894e1ae2..c3e71c1a2 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@

+[![GHA](https://img.shields.io/github/v/tag/iterative/setup-cml?label=GitHub%20Actions&logo=GitHub)](https://github.com/iterative/setup-cml) +[![npm](https://img.shields.io/npm/v/@dvcorg/cml?logo=npm)](https://www.npmjs.com/package/@dvcorg/cml) + **What is CML?** Continuous Machine Learning (CML) is an open-source library for implementing continuous integration & delivery (CI/CD) in machine learning projects. Use it to automate parts of your development workflow, including model @@ -23,40 +26,39 @@ We built CML with these principles in mind: plots in each Git Pull Request. Rigorous engineering practices help your team make informed, data-driven decisions. - **No additional services.** Build your own ML platform using just GitHub or - GitLab and your favorite cloud services: AWS, Azure, GCP. No databases, + GitLab and your favourite cloud services: AWS, Azure, GCP. No databases, services or complex setup needed. -_⁉️ Need help? Just want to chat about continuous integration for ML? -[Visit our Discord channel!](https://discord.gg/bzA6uY7)_ +:question: Need help? Just want to chat about continuous integration for ML? +[Visit our Discord channel!](https://discord.gg/bzA6uY7) -🌟🌟🌟 Check out our +:play_or_pause_button: Check out our [YouTube video series](https://www.youtube.com/playlist?list=PL7WG7YrwYcnDBDuCkFbcyjnZQrdskFsBz) -for hands-on MLOps tutorials using CML! 🌟🌟🌟 +for hands-on MLOps tutorials using CML! ## Table of contents 1. [Usage](#usage) -2. [Getting started](#getting-started) +2. [Getting started (tutorial)](#getting-started) 3. [Using CML with DVC](#using-cml-with-dvc) 4. [Using self-hosted runners](#using-self-hosted-runners) 5. [Install CML as a package](#install-cml-as-a-package) -6. [Examples](#a-library-of-cml-projects) +6. [Example Projects](#see-also) ## Usage You'll need a GitHub or GitLab account to begin. Users may wish to familiarize themselves with [Github Actions](https://help.github.com/en/actions) or -[GitLab CI/CD](https://about.gitlab.com/stages-devops-lifecycle/continuous-integration/). +[GitLab CI/CD](https://about.gitlab.com/stages-devops-lifecycle/continuous-integration). Here, will discuss the GitHub use case. -⚠️ **GitLab users!** Please see our -[docs about configuring CML with GitLab](https://github.com/iterative/cml/wiki/CML-with-GitLab). - -🪣 **Bitbucket Cloud users** We support you, too- -[see our docs here](https://github.com/iterative/cml/wiki/CML-with-Bitbucket-Cloud).🪣 -_Bitbucket Server support estimated to arrive by January 2021._ - -The key file in any CML project is `.github/workflows/cml.yaml`. +- **GitLab users**: Please see our + [docs about configuring CML with GitLab](https://github.com/iterative/cml/wiki/CML-with-GitLab). +- **Bitbucket Cloud users**: Please see our + [docs on CML with Bitbucket Cloud](https://github.com/iterative/cml/wiki/CML-with-Bitbucket-Cloud). + _Bitbucket Server support estimated to arrive by May 2021._ +- **GitHub Actions users**: The key file in any CML project is + `.github/workflows/cml.yaml`: ```yaml name: your-workflow-name @@ -64,34 +66,47 @@ on: [push] jobs: run: runs-on: [ubuntu-latest] - container: docker://dvcorg/cml-py3:latest + # optionally use a convenient Ubuntu LTS + CUDA + DVC + CML image + # container: docker://dvcorg/cml-py3:latest steps: - uses: actions/checkout@v2 - - name: 'Train my model' - env: - repo_token: ${{ secrets.GITHUB_TOKEN }} + # may need to setup NodeJS & Python3 on e.g. self-hosted + # - uses: actions/setup-node@v2 + # with: + # node-version: '12' + # - uses: actions/setup-python@v2 + # with: + # python-version: '3.x' + - uses: iterative/setup-cml@v1 + - name: Train model run: | - # Your ML workflow goes here pip install -r requirements.txt python train.py - - # Write your CML report + - name: Write CML report + env: + repo_token: ${{ secrets.GITHUB_TOKEN }} + run: | + # Post reports as comments in GitHub PRs cat results.txt >> report.md cml-send-comment report.md ``` +We helpfully provide CML and other useful libraries pre-installed on our +[custom Docker images](https://github.com/iterative/cml/blob/master/docker/Dockerfile). +In the above example, uncommenting the field +`container: docker://dvcorg/cml-py3:latest` will make the GitHub Actions runner +pull the CML Docker image. The image already has NodeJS, Python 3, DVC and CML +set up on an Ubuntu LTS base with CUDA libraries and +[Terraform](https://www.terraform.io) installed for convenience. + ### CML Functions -CML provides a number of helper functions to help package outputs from ML -workflows, such as numeric data and data vizualizations about model performance, -into a CML report. The library comes pre-installed on our -[custom Docker images](https://github.com/iterative/cml/blob/master/docker/Dockerfile). -In the above example, note the field `container: docker://dvcorg/cml-py3:latest` -specifies the CML Docker image with Python 3 will be pulled by the GitHub -Actions runner. +CML provides a number of helper functions to help package the outputs of ML +workflows (including numeric data and visualizations about model performance) +into a CML report. -Below is a list of CML functions for writing markdown reports and delivering +Below is a table of CML functions for writing markdown reports and delivering those reports to your CI system (GitHub Actions or GitLab CI). | Function | Description | Inputs | @@ -105,38 +120,40 @@ those reports to your CI system (GitHub Actions or GitLab CI). CML reports are written in [GitHub Flavored Markdown](https://github.github.com/gfm/). That means they can -contain images, tables, formatted text, HTML blocks, code snippets and more - +contain images, tables, formatted text, HTML blocks, code snippets and more — really, what you put in a CML report is up to you. Some examples: -📝 **Text**. Write to your report using whatever method you prefer. For example, -copy the contents of a text file containing the results of ML model training: +:spiral_notepad: **Text** Write to your report using whatever method you prefer. +For example, copy the contents of a text file containing the results of ML model +training: ```bash cat results.txt >> report.md ``` -🖼️ **Images** Display images using the markdown or HTML. Note that if an image -is an output of your ML workflow (i.e., it is produced by your workflow), you -will need to use the `cml-publish` function to include it a CML report. For -example, if `graph.png` is the output of my workflow `python train.py`, run: +:framed_picture: **Images** Display images using the markdown or HTML. Note that +if an image is an output of your ML workflow (i.e., it is produced by your +workflow), you will need to use the `cml-publish` function to include it a CML +report. For example, if `graph.png` is output by `python train.py`, run: ```bash cml-publish graph.png --md >> report.md ``` -## Getting started +## Getting Started 1. Fork our - [example project repository](https://github.com/iterative/example_cml). ⚠️ - Note that if you are using GitLab, - [you will need to create a Personal Access Token](https://github.com/iterative/cml/wiki/CML-with-GitLab#variables) - for this example to work. + [example project repository](https://github.com/iterative/example_cml). + +> :warning: Note that if you are using GitLab, +> [you will need to create a Personal Access Token](https://github.com/iterative/cml/wiki/CML-with-GitLab#variables) +> for this example to work. ![](imgs/fork_project.png) -The following steps can all be done in the GitHub browser interface. However, to -follow along the commands, we recommend cloning your fork to your local -workstation: +> :warning: The following steps can all be done in the GitHub browser interface. +> However, to follow along with the commands, we recommend cloning your fork to +> your local workstation: ```bash git clone https://github.com//example_cml @@ -151,10 +168,11 @@ on: [push] jobs: run: runs-on: [ubuntu-latest] - container: docker://dvcorg/cml-py3:latest steps: - uses: actions/checkout@v2 - - name: 'Train my model' + - uses: actions/setup-python@v2 + - uses: iterative/setup-cml@v1 + - name: Train model env: repo_token: ${{ secrets.GITHUB_TOKEN }} run: | @@ -166,9 +184,9 @@ jobs: cml-send-comment report.md ``` -4. In your text editor of choice, edit line 16 of `train.py` to `depth = 5`. +3. In your text editor of choice, edit line 16 of `train.py` to `depth = 5`. -5. Commit and push the changes: +4. Commit and push the changes: ```bash git checkout -b experiment @@ -176,34 +194,38 @@ git add . && git commit -m "modify forest depth" git push origin experiment ``` -6. In GitHub, open up a Pull Request to compare the `experiment` branch to +5. In GitHub, open up a Pull Request to compare the `experiment` branch to `master`. ![](imgs/make_pr.png) Shortly, you should see a comment from `github-actions` appear in the Pull -Request with your CML report. This is a result of the function -`cml-send-comment` in your workflow. +Request with your CML report. This is a result of the `cml-send-comment` +function in your workflow. ![](imgs/cml_first_report.png) -This is the gist of the CML workflow: when you push changes to your GitHub -repository, the workflow in your `.github/workflows/cml.yaml` file gets run and -a report generated. CML functions let you display relevant results from the -workflow, like model performance metrics and vizualizations, in GitHub checks -and comments. What kind of workflow you want to run, and want to put in your CML -report, is up to you. +This is the outline of the CML workflow: + +- you push changes to your GitHub repository, +- the workflow in your `.github/workflows/cml.yaml` file gets run, and +- a report is generated and posted to GitHub. + +CML functions let you display relevant results from the workflow — such as model +performance metrics and visualizations — in GitHub checks and comments. What +kind of workflow you want to run, and want to put in your CML report, is up to +you. ## Using CML with DVC -In many ML projects, data isn't stored in a Git repository and needs to be +In many ML projects, data isn't stored in a Git repository, but needs to be downloaded from external sources. [DVC](https://dvc.org) is a common way to bring data to your CML runner. DVC also lets you visualize how metrics differ between commits to make reports like this: ![](imgs/dvc_cml_long_report.png) -The `.github/workflows/cml.yaml` file to create this report is: +The `.github/workflows/cml.yaml` file used to create this report is: ```yaml name: model-training @@ -214,8 +236,7 @@ jobs: container: docker://dvcorg/cml-py3:latest steps: - uses: actions/checkout@v2 - - name: 'Train my model' - shell: bash + - name: Train model env: repo_token: ${{ secrets.GITHUB_TOKEN }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -234,26 +255,27 @@ jobs: dvc metrics diff master --show-md >> report.md # Publish confusion matrix diff - echo -e "## Plots\n### Class confusions" >> report.md + 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 vl2png vega.json -s 1.5 | cml-publish --md >> report.md # Publish regularization function diff - echo "### Effects of regularization\n" >> report.md + echo "### Effects of regularization" >> report.md dvc plots diff --target estimators.csv -x Regularization --show-vega master > vega.json vl2png vega.json -s 1.5 | cml-publish --md >> report.md cml-send-comment report.md ``` -If you're using DVC with cloud storage, take note of environmental variables for -your storage format. +> :warning: If you're using DVC with cloud storage, take note of environment +> variables for your storage format. -### Environmental variables for supported cloud providers +### Environment variables for supported cloud providers
- S3 and S3 compatible storage (Minio, DigitalOcean Spaces, IBM Cloud Object Storage...) + S3 and S3-compatible storage (Minio, DigitalOcean Spaces, IBM Cloud Object Storage...) ```yaml @@ -264,7 +286,7 @@ env: AWS_SESSION_TOKEN: ${{ secrets.AWS_SESSION_TOKEN }} ``` -> :point_right: AWS_SESSION_TOKEN is optional. +> :point_right: `AWS_SESSION_TOKEN` is optional.
@@ -302,9 +324,10 @@ env: Google Storage -> :warning: Normally, GOOGLE_APPLICATION_CREDENTIALS points to the path of the -> json file that contains the credentials. However in the action this variable -> CONTAINS the content of the file. Copy that json and add it as a secret. +> :warning: Normally, `GOOGLE_APPLICATION_CREDENTIALS` is the **path** of the +> `json` file containing the credentials. However in the action this secret +> variable is the **contents** of the file. Copy the `json` contents and add it +> as a secret. ```yaml env: @@ -320,9 +343,9 @@ env: > :warning: After configuring your > [Google Drive credentials](https://dvc.org/doc/command-reference/remote/add) -> you will find a json file at -> `your_project_path/.dvc/tmp/gdrive-user-credentials.json`. Copy that json and -> add it as a secret. +> you will find a `json` file at +> `your_project_path/.dvc/tmp/gdrive-user-credentials.json`. Copy its contents +> and add it as a secret variable. ```yaml env: @@ -335,81 +358,82 @@ env: GitHub Actions are run on GitHub-hosted runners by default. However, there are many great reasons to use your own runners: to take advantage of GPUs; to -orchestrate your team's shared computing resources, or to train in the cloud. +orchestrate your team's shared computing resources, or to access on-premise +data. -☝️ **Tip!** Check out the -[official GitHub documentation](https://help.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners) -to get started setting up your self-hosted runner. +> :point_up: **Tip!** Check out the +> [official GitHub documentation](https://help.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners) +> to get started setting up your own self-hosted runner. ### Allocating cloud resources with CML -When a workflow requires computational resources (such as GPUs) CML can +When a workflow requires computational resources (such as GPUs), CML can automatically allocate cloud instances using `cml-runner`. You can spin up instances on your AWS or Azure account (GCP support is forthcoming!). For example, the following workflow deploys a `t2.micro` instance on AWS EC2 and trains a model on the instance. After the job runs, the instance automatically -shuts down. You might notice that this workflow is quite similar to the -[basic use case](#usage) highlighted in the beginning of the docs- that's -because it is! What's new is that we've added `cml-runner`, plus a few -environmental variables for passing your cloud service credentials to the +shuts down. + +You might notice that this workflow is quite similar to the +[basic use case](#usage) above. The only addition is `cml-runner` and a few +environment variables for passing your cloud service credentials to the workflow. ```yaml -name: "Train-in-the-cloud" +name: Train-in-the-cloud on: [push] - jobs: deploy-runner: runs-on: [ubuntu-latest] steps: - uses: iterative/setup-cml@v1 - uses: actions/checkout@v2 - - name: "Deploy runner on EC2" - shell: bash + - name: Deploy runner on EC2 env: repo_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} run: | cml-runner \ - --cloud aws \ - --cloud-region us-west \ - --cloud-type=t2.micro \ - --labels=cml-runner - name: model-training - needs: deploy-runner - runs-on: [self-hosted,cml-runner] + --cloud aws \ + --cloud-region us-west \ + --cloud-type=t2.micro \ + --labels=cml-runner + model-training: + needs: [deploy-runner] + runs-on: [self-hosted, cml-runner] container: docker://dvcorg/cml-py3:latest steps: - - uses: actions/checkout@v2 - - name: "Train my model" - env: - repo_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} - run: | - pip install -r requirements.txt - python train.py - # Publish report with CML - cat metrics.txt > report.md - cml-send-comment report.md + - uses: actions/checkout@v2 + - name: Train model + env: + repo_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + run: | + pip install -r requirements.txt + python train.py + + cat metrics.txt > report.md + cml-send-comment report.md ``` -In the above workflow, the step `deploy-runner` launches an EC2 `t2-micro` -instance in the `us-west` region. The next step, `model-training`, runs on the -newly launched instance. +In the workflow above, the `deploy-runner` step launches an EC2 `t2-micro` +instance in the `us-west` region. The `model-training` step then runs on the +newly-launched instance. -**Note that you can use any container with this workflow!** While you must have -CML and its dependencies setup to use CML functions like `cml-send-comment` from -your instance, you can create your favorite training environment in the cloud by -pulling the Docker container of your choice. +> :tada: **Note that you can use any container with this workflow!** While you +> must [have CML and its dependencies set up](#install-cml-as-a-package) to use +> functions such `cml-send-comment` from your instance, you can create your +> favourite training environment in the cloud by pulling the Docker container of +> your choice. We like the CML container (`docker://dvcorg/cml-py3`) because it comes loaded with Python, CUDA, `git`, `node` and other essentials for full-stack data -science. But we don't mind if you do it your way :) +science. ### Arguments -The function `cml-runner` accepts the following arguments: +The `cml-runner` function accepts the following arguments: ``` Usage: cml-runner.js @@ -462,30 +486,31 @@ Options: -h Show help [boolean] ``` -### Environmental variables +### Environment variables -You will need to -[create a personal access token](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line) -with repository read/write access and workflow privileges. In the example -workflow, this token is stored as `PERSONAL_ACCESS_TOKEN`. +> :warning: You will need to +> [create a personal access token](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line) +> with repository read/write access and workflow privileges. In the example +> workflow, this token is stored as `PERSONAL_ACCESS_TOKEN`. Note that you will also need to provide access credentials for your cloud compute resources as secrets. In the above example, `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` are required to deploy EC2 instances. Please see our docs about -[environmental variables needed to authenticate with supported cloud services](#environmental-variables-for-supported-cloud-providers). +[environment variables needed to authenticate with supported cloud services](#environment-variables-for-supported-cloud-providers). -### Using on-premise machines as self-hosted runners +### On-premise (local) runners -You can also use the new `cml-runner` function to set up a local self-hosted -runner. On your local machine or on-premise GPU cluster, you'll install CML as a -package and then run: +This means using on-premise machines as self-hosted runners. The `cml-runner` +function is used to set up a local self-hosted runner. On your local machine or +on-premise GPU cluster, [install CML as a package](#install-cml-as-a-package) +and then run: -```yaml +```bash cml-runner \ --repo $your_project_repository_url \ - --token=$personal_access_token \ + --token=$PERSONAL_ACCESS_TOKEN \ --labels tf \ --idle-timeout 180 ``` @@ -494,8 +519,9 @@ Now your machine will be listening for workflows from your project repository. ## Install CML as a package -In the above examples, CML is pre-installed in a custom Docker image, which is -pulled by a CI runner. You can also install CML as a package: +In the examples above, CML is installed by the `setup-cml` action, or comes +pre-installed in a custom Docker image pulled by a CI runner. You can also +install CML as a package: ```bash npm i -g @dvcorg/cml @@ -506,26 +532,28 @@ CLI commands: ```bash sudo apt-get install -y libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev \ - librsvg2-dev libfontconfig-dev + librsvg2-dev libfontconfig-dev npm install -g vega-cli vega-lite ``` -CML and Vega-Lite package installation require `npm` command from Node package. -Below you can find how to install Node. +CML and Vega-Lite package installation require the NodeJS package manager +(`npm`) which ships with NodeJS. Installation instructions are below. -### Install Node in GitHub +### Install NodeJS in GitHub -In GitHub there is a special action for NPM installation: +This is probably not necessary when using GitHub's default containers or one of +CML's Docker containers. Self-hosted runners may need to use a set up action to +install NodeJS: ```bash -uses: actions/setup-node@v1 +uses: actions/setup-node@v2 with: node-version: '12' ``` -### Install Node in GitLab +### Install NodeJS in GitLab -GitLab requires direct installation of the NMP package: +GitLab requires direct installation of NodeJS: ```bash curl -sL https://deb.nodesource.com/setup_12.x | bash @@ -533,9 +561,9 @@ apt-get update apt-get install -y nodejs ``` -## A library of CML projects +## See Also -Here are some example projects using CML. +These are some example projects using CML. - [Basic CML project](https://github.com/iterative/cml_base_case) - [CML with DVC to pull data](https://github.com/iterative/cml_dvc_case) diff --git a/bin/cml-pr.js b/bin/cml-pr.js new file mode 100644 index 000000000..04c4121f9 --- /dev/null +++ b/bin/cml-pr.js @@ -0,0 +1,41 @@ +#!/usr/bin/env node + +const print = console.log; +console.log = console.error; + +const yargs = require('yargs'); +const decamelize = require('decamelize-keys'); + +const CML = require('../src/cml'); + +const run = async (opts) => { + const globs = opts._.length ? opts._ : undefined; + const cml = new CML(opts); + print(await cml.pr_create({ ...opts, globs })); +}; + +const opts = decamelize( + yargs + .usage('Usage: $0 ') + .describe('md', 'Output in markdown format [](url).') + .boolean('md') + .default('repo') + .describe( + 'repo', + 'Specifies the repo to be used. If not specified is extracted from the CI ENV.' + ) + .default('token') + .describe( + 'token', + 'Personal access token to be used. If not specified in extracted from ENV repo_token.' + ) + .default('driver') + .choices('driver', ['github', 'gitlab']) + .describe('driver', 'If not specify it infers it from the ENV.') + .help('h').argv +); + +run(opts).catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/bin/cml-publish.js b/bin/cml-publish.js index a86cc864e..59f1c738b 100644 --- a/bin/cml-publish.js +++ b/bin/cml-publish.js @@ -71,7 +71,7 @@ const argv = yargs .default('token') .describe( 'token', - 'Personal access token to be used. If not specified in extracted from ENV repo_token or GITLAB_TOKEN.' + 'Personal access token to be used. If not specified, extracted from ENV REPO_TOKEN, GITLAB_TOKEN, GITHUB_TOKEN, or BITBUCKET_TOKEN.' ) .default('driver') .choices('driver', ['github', 'gitlab']) diff --git a/bin/cml-publish.test.js b/bin/cml-publish.test.js index e050e1802..c5c023fc3 100644 --- a/bin/cml-publish.test.js +++ b/bin/cml-publish.test.js @@ -25,8 +25,9 @@ describe('CML e2e', () => { exist. --repo Specifies the repo to be used. If not specified is extracted from the CI ENV. - --token Personal access token to be used. If not specified in - extracted from ENV repo_token or GITLAB_TOKEN. + --token Personal access token to be used. If not specified, + extracted from ENV REPO_TOKEN, GITLAB_TOKEN, GITHUB_TOKEN, + or BITBUCKET_TOKEN. --driver If not specify it infers it from the ENV. [choices: \\"github\\", \\"gitlab\\"] -h Show help [boolean]" diff --git a/bin/cml-runner.js b/bin/cml-runner.js index ea901e7ad..1a86ffce4 100755 --- a/bin/cml-runner.js +++ b/bin/cml-runner.js @@ -25,7 +25,7 @@ const { RUNNER_REUSE = false, RUNNER_DRIVER, RUNNER_REPO, - repo_token + REPO_TOKEN } = process.env; let cml; @@ -350,7 +350,7 @@ const opts = decamelize( 'repo', 'Repository to be used for registering the runner. If not specified, it will be inferred from the environment' ) - .default('token', repo_token) + .default('token', REPO_TOKEN) .describe( 'token', 'Personal access token to register a self-hosted runner on the repository. If not specified, it will be inferred from the environment' diff --git a/bin/cml-send-comment.js b/bin/cml-send-comment.js index 60aaf0874..179ce6842 100644 --- a/bin/cml-send-comment.js +++ b/bin/cml-send-comment.js @@ -47,7 +47,7 @@ const argv = yargs .default('token') .describe( 'token', - 'Personal access token to be used. If not specified in extracted from ENV repo_token.' + 'Personal access token to be used. If not specified in extracted from ENV REPO_TOKEN.' ) .default('driver') .choices('driver', ['github', 'gitlab']) diff --git a/bin/cml-send-comment.test.js b/bin/cml-send-comment.test.js index 91599218f..b596b1ad8 100644 --- a/bin/cml-send-comment.test.js +++ b/bin/cml-send-comment.test.js @@ -29,7 +29,7 @@ describe('Comment integration tests', () => { --repo Specifies the repo to be used. If not specified is extracted from the CI ENV. --token Personal access token to be used. If not specified in - extracted from ENV repo_token. + extracted from ENV REPO_TOKEN. --driver If not specify it infers it from the ENV. [choices: \\"github\\", \\"gitlab\\"] -h Show help [boolean]" diff --git a/bin/cml-send-github-check.js b/bin/cml-send-github-check.js index 50e187b8c..f35b0ecf7 100644 --- a/bin/cml-send-github-check.js +++ b/bin/cml-send-github-check.js @@ -43,7 +43,7 @@ const argv = yargs .default('token') .describe( 'token', - 'Personal access token to be used. If not specified in extracted from ENV repo_token.' + 'Personal access token to be used. If not specified in extracted from ENV REPO_TOKEN.' ) .help('h') .demand(1).argv; diff --git a/bin/cml-send-github-check.test.js b/bin/cml-send-github-check.test.js index bbbcc0ccd..ef33b3c89 100644 --- a/bin/cml-send-github-check.test.js +++ b/bin/cml-send-github-check.test.js @@ -45,7 +45,7 @@ describe('CML e2e', () => { --repo Specifies the repo to be used. If not specified is extracted from the CI ENV. --token Personal access token to be used. If not specified in extracted - from ENV repo_token. + from ENV REPO_TOKEN. -h Show help [boolean] --conclusion[choices: \\"success\\", \\"failure\\", \\"neutral\\", \\"cancelled\\", \\"skipped\\", \\"timed_out\\"] [default: Sets the conclusion status of the check.]" diff --git a/package-lock.json b/package-lock.json index 0ea68cec9..588a38765 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@dvcorg/cml", - "version": "0.3.6", + "version": "0.3.7", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -585,6 +585,42 @@ "@types/yargs": "^13.0.0" } }, + "@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "requires": { + "debug": "^4.1.1" + } + }, + "@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==" + }, + "@nodelib/fs.scandir": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz", + "integrity": "sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA==", + "requires": { + "@nodelib/fs.stat": "2.0.4", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz", + "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==" + }, + "@nodelib/fs.walk": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz", + "integrity": "sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==", + "requires": { + "@nodelib/fs.scandir": "2.1.4", + "fastq": "^1.6.0" + } + }, "@octokit/auth-token": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.2.tgz", @@ -954,6 +990,11 @@ "is-string": "^1.0.5" } }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==" + }, "array-unique": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", @@ -1714,6 +1755,14 @@ } } }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", @@ -1841,6 +1890,21 @@ "integrity": "sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==", "dev": true }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "requires": { + "path-type": "^4.0.0" + }, + "dependencies": { + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" + } + } + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -2539,6 +2603,66 @@ "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", "dev": true }, + "fast-glob": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.5.tgz", + "integrity": "sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg==", + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.0", + "merge2": "^1.3.0", + "micromatch": "^4.0.2", + "picomatch": "^2.2.1" + }, + "dependencies": { + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + }, + "dependencies": { + "picomatch": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz", + "integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==" + } + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } + } + } + }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2551,6 +2675,14 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fastq": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz", + "integrity": "sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==", + "requires": { + "reusify": "^1.0.4" + } + }, "fb-watchman": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", @@ -3357,7 +3489,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", - "dev": true, "requires": { "is-glob": "^4.0.1" } @@ -3371,6 +3502,26 @@ "type-fest": "^0.8.1" } }, + "globby": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.3.tgz", + "integrity": "sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg==", + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + }, + "dependencies": { + "ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==" + } + } + }, "graceful-fs": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", @@ -3921,8 +4072,7 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" }, "is-generator-fn": { "version": "2.1.0", @@ -3934,7 +4084,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, "requires": { "is-extglob": "^2.1.1" } @@ -5462,6 +5611,11 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" + }, "micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", @@ -6056,8 +6210,7 @@ "picomatch": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" }, "pify": { "version": "2.3.0", @@ -6239,6 +6392,11 @@ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", "dev": true }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -6498,6 +6656,11 @@ "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", "dev": true }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" + }, "rsvp": { "version": "4.8.5", "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", @@ -6510,6 +6673,14 @@ "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", "dev": true }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "requires": { + "queue-microtask": "^1.2.2" + } + }, "rxjs": { "version": "6.5.5", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz", @@ -6711,6 +6882,16 @@ "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", "dev": true }, + "simple-git": { + "version": "2.38.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.38.0.tgz", + "integrity": "sha512-CORjrfirWMEGbJAxaXDH/PjZVOeATeG2bkafM9DsLVcFkbF9sXQGIIpEI6FeyXpvUsFK69T/pa4+4FKY9TUJMQ==", + "requires": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.3.1" + } + }, "sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -6720,8 +6901,7 @@ "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" }, "slice-ansi": { "version": "2.1.0", diff --git a/package.json b/package.json index 3def2629e..2664457a7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@dvcorg/cml", - "version": "0.3.6", + "version": "0.3.7", "author": { "name": "DVC", "url": "http://cml.dev" @@ -35,7 +35,8 @@ "cml-publish": "bin/cml-publish.js", "cml-tensorboard-dev": "bin/cml-tensorboard-dev.js", "cml-runner": "bin/cml-runner.js", - "cml-cloud-runner-entrypoint": "bin/cml-runner.js" + "cml-cloud-runner-entrypoint": "bin/cml-runner.js", + "test-cml-pr": "bin/cml-pr.js" }, "scripts": { "lintfix": "eslint --fix ./", @@ -64,12 +65,14 @@ "form-data": "^3.0.0", "fs-extra": "^9.0.1", "git-url-parse": "^11.4.0", + "globby": "^11.0.3", "is-svg": "^4.2.2", "js-base64": "^3.5.2", "node-fetch": "^2.6.0", "node-forge": "^0.10.0", "node-ssh": "^11.0.0", "semver": "^7.3.4", + "simple-git": "^2.38.0", "strip-url-auth": "^1.0.1", "tar": "^6.1.0", "tempy": "^0.6.0", diff --git a/src/cml.js b/src/cml.js index 9fe359f65..8412158c9 100644 --- a/src/cml.js +++ b/src/cml.js @@ -1,6 +1,8 @@ const { execSync } = require('child_process'); const git_url_parse = require('git-url-parse'); const strip_auth = require('strip-url-auth'); +const git = require('simple-git/promise')('./'); +const globby = require('globby'); const Gitlab = require('./drivers/gitlab'); const Github = require('./drivers/github'); @@ -48,12 +50,15 @@ const get_driver = (opts) => { const infer_token = () => { const { + REPO_TOKEN, repo_token, GITHUB_TOKEN, GITLAB_TOKEN, BITBUCKET_TOKEN } = process.env; - return repo_token || GITHUB_TOKEN || GITLAB_TOKEN || BITBUCKET_TOKEN; + return ( + REPO_TOKEN || repo_token || GITHUB_TOKEN || GITLAB_TOKEN || BITBUCKET_TOKEN + ); }; class CML { @@ -229,11 +234,94 @@ class CML { await this.runner_token(); } catch (err) { throw new Error( - 'repo_token does not have enough permissions to access workflow API' + 'REPO_TOKEN does not have enough permissions to access workflow API' ); } } + async pr_create(opts = {}) { + const { globs = ['dvc.lock', '.gitignore'], md } = opts; + + const { files } = await git.status(); + if (!files.length) { + console.log('No files changed. Nothing to do.'); + return; + } + + const driver = get_driver(this); + const paths = (await globby(globs)).filter((path) => + files.map((item) => item.path).includes(path) + ); + + const render_pr = (url) => { + if (md) + return `[CML's ${ + this.driver === 'gitlab' ? 'Merge' : 'Pull' + } Request](${url})`; + return url; + }; + + const sha = await exec(`git rev-parse HEAD`); + const sha_short = sha.substr(0, 7); + let target = await exec(`git branch --show-current`); + if (!target) { + if (this.driver === 'gitlab') { + target = await exec('echo $CI_BUILD_REF_NAME'); + } + } + const source = `${target}-cmlpr-${sha_short}`; + + await exec(`git fetch origin`); + + const branch_exists = (await exec(`git branch -r`)).includes(source); + if (branch_exists) { + const prs = await driver.prs(); + const { url } = + prs.find((pr) => pr.source === source && pr.target === target) || {}; + + if (url) return render_pr(url); + } else { + try { + await exec(`git config --local user.email "david@iterative.ai"`); + await exec(`git config --local user.name "cml-bot"`); + await exec('git config advice.addIgnoredFile false'); + + if (this.driver !== 'github') { + const repo = new URL(this.repo); + repo.password = this.token; + repo.username = driver.user_name; + + await exec(`git remote rm origin`); + await exec(`git remote add origin "${repo.toString()}.git"`); + } + + await exec(`git checkout -B ${target} ${sha}`); + await exec(`git checkout -b ${source}`); + await exec(`git add ${paths.join(' ')}`); + await exec(`git commit -m "CML [skip ci]"`); + await exec(`git push --set-upstream origin ${source}`); + await exec(`git checkout -B ${target} ${sha}`); + } catch (err) { + await exec(`git checkout -B ${target} ${sha}`); + throw err; + } + } + + const title = `CML commits ${target} ${sha_short}`; + const description = ` + Automated commits for ${this.repo}/commit/${sha} created by CML. + `; + + const url = await driver.pr_create({ + source, + target, + title, + description + }); + + return render_pr(url); + } + log_error(e) { console.error(e.message); } diff --git a/src/cml.test.js b/src/cml.test.js index dc24de45f..c44f74ea0 100644 --- a/src/cml.test.js +++ b/src/cml.test.js @@ -13,7 +13,7 @@ describe('Github tests', () => { jest.resetModules(); process.env = {}; - process.env.repo_token = TOKEN; + process.env.REPO_TOKEN = TOKEN; }); afterAll(() => { @@ -91,7 +91,7 @@ describe('Gitlab tests', () => { jest.resetModules(); process.env = {}; - process.env.repo_token = TOKEN; + process.env.REPO_TOKEN = TOKEN; }); afterAll(() => { diff --git a/src/drivers/github.js b/src/drivers/github.js index a0be2f233..9330dbca0 100644 --- a/src/drivers/github.js +++ b/src/drivers/github.js @@ -231,6 +231,58 @@ class Github { ) .map((runner) => ({ id: runner.id, name: runner.name })); } + + async pr_create(opts = {}) { + const { source: head, target: base, title, description: body } = opts; + const { owner, repo } = owner_repo({ uri: this.repo }); + const { pulls } = octokit(this.token, this.repo); + + const { + data: { url } + } = await pulls.create({ + owner, + repo, + head, + base, + title, + body + }); + + return url; + } + + async prs(opts = {}) { + const { state = 'open' } = opts; + const { owner, repo } = owner_repo({ uri: this.repo }); + const { pulls } = octokit(this.token, this.repo); + + const { data: prs } = await pulls.list({ + owner, + repo, + state + }); + + return prs.map((pr) => { + const { + url, + head: { ref: source }, + base: { ref: target } + } = pr; + return { + url, + source, + target + }; + }); + } + + get user_email() { + return 'action@github.com'; + } + + get user_name() { + return 'GitHub Action'; + } } module.exports = Github; diff --git a/src/drivers/gitlab.js b/src/drivers/gitlab.js index c7bbf1188..68d59fe08 100644 --- a/src/drivers/gitlab.js +++ b/src/drivers/gitlab.js @@ -8,7 +8,7 @@ const { resolve } = require('path'); const { fetch_upload_data, download, exec } = require('../utils'); -const { IN_DOCKER } = process.env; +const { IN_DOCKER, GITLAB_USER_EMAIL, GITLAB_USER_NAME } = process.env; const API_VER = 'v4'; class Gitlab { constructor(opts = {}) { @@ -180,6 +180,39 @@ class Gitlab { return runners.map((runner) => ({ id: runner.id, name: runner.name })); } + async pr_create(opts = {}) { + const { project_path } = this; + const { source, target, title, description } = opts; + + const endpoint = `/projects/${project_path}/merge_requests`; + const body = new URLSearchParams(); + body.append('source_branch', source); + body.append('target_branch', target); + body.append('title', title); + body.append('description', description); + + const { web_url } = await this.request({ endpoint, method: 'POST', body }); + + return web_url; + } + + async prs(opts = {}) { + const { project_path } = this; + const { state = 'opened' } = opts; + + const endpoint = `/projects/${project_path}/merge_requests?state=${state}`; + const prs = await this.request({ endpoint, method: 'GET' }); + + return prs.map((pr) => { + const { web_url: url, source_branch: source, target_branch: target } = pr; + return { + url, + source, + target + }; + }); + } + async request(opts = {}) { const { token } = this; const { endpoint, method = 'GET', body, raw } = opts; @@ -198,6 +231,14 @@ class Gitlab { return await response.json(); } + + get user_email() { + return GITLAB_USER_EMAIL; + } + + get user_name() { + return GITLAB_USER_NAME; + } } module.exports = Gitlab;