diff --git a/.concourse.yml b/.concourse.yml index 7c77508..4ff405c 100644 --- a/.concourse.yml +++ b/.concourse.yml @@ -27,7 +27,7 @@ resources: source: access_token: {{github-access-token}} private_key: {{github-private-key}} - repo: jtarchie/pullrequest-resource + repo: jtarchie/github-pullrequest-resource base: master - name: merge-pull-requests type: pull-request-test @@ -35,14 +35,64 @@ resources: access_token: {{github-access-token}} private_key: {{github-private-key}} base: test-merge - repo: jtarchie/pullrequest-resource + repo: jtarchie/github-pullrequest-resource jobs: - - name: test-pr-merge + - name: create-pr plan: - get: git-pr - passed: [ 'tests' ] + passed: [ 'docker' ] trigger: true + - task: create + config: + platform: linux + params: + GITHUB_ACCESS_TOKEN: {{github-access-token}} + image_resource: + type: docker-image + source: + repository: jtarchie/pr + tag: test + run: + path: ruby + args: + - -e + - | + require 'octokit' + + Octokit.access_token = ENV['GITHUB_ACCESS_TOKEN'] + + repo_name = ENV['REPO_NAME'] || 'jtarchie/pullrequest-resource' + base_branch = ENV['BASE_BRANCH'] || 'test-merge' + test_branch = "test-merge-#{Time.now.to_i}" + + sha = Octokit.ref(repo_name, "heads/#{base_branch}").object.sha + Octokit.create_ref(repo_name, "heads/#{test_branch}", sha) + blob_sha = Octokit.contents( + repo_name, + path: 'README.md', + ref: test_branch + ).sha + Octokit.update_contents( + repo_name, + 'README.md', + 'Updating content', + blob_sha, + "File content #{Time.now.to_s}", + branch: test_branch + ) + Octokit.create_pull_request( + repo_name, + base_branch, + test_branch, + 'testing the latest build', + 'testing the latest build' + ) + + - name: test-pr-merge + plan: + - get: git-pr + passed: [ 'create-pr' ] - get: merge-pull-requests trigger: true - put: merge-pull-requests @@ -100,29 +150,10 @@ jobs: - put: docker-pr params: build: git-pr - dockerfile: git-pr/Dockerfile.test + dockerfile: git-pr/Dockerfile tag: tag/name get_params: skip_download: true - - name: tests - plan: - - get: git-pr - passed: [ docker ] - trigger: true - - task: rspec - privileged: true - config: - image_resource: - type: docker-image - source: - repository: jtarchie/pr - tag: test - inputs: - - name: git-pr - platform: linux - run: - path: sh - args: ['-c', 'cd git-pr && bundle install && bundle exec rspec'] - name: release plan: - get: git-pr diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..2a58f93 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,14 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Provide a pipeline YAML that reproduces or demonstrates the issue. + +**Expected behavior** +A clear and concise description of what you expected to happen. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..1b2c02c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,14 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + +** Is your feature request to extend the resource with a Github API call? ** +Please clarify why this is beneficial and necessary for the resource. Most Github API calls can be performed with [`hub`](https://github.com/github/hub) or `curl` command from a `task`. The resource should not be a complete wrapper of the Github API. + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..8cbe5fe --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,29 @@ +# How to contribute to the pullrequest resource + +## **Did you find a bug?** + +* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/jtarchie/pullrequest-resource/issues). + +* If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/jtarchie/pullrequest-resource/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring. + +## **Did you write a patch that fixes a bug?** + +* Open a new GitHub pull request with the patch. + +* Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. + +* Write a test for your feature. Run all the tests! This will give you and us a higher confidence that nothing broke. + +## **Do you intend to add a new feature or change an existing one?** + +* Please open a [open a new issue](https://github.com/jtarchie/pullrequest-resource/issues/new) describing the feature you'd like to add. + +## **Requirements for a Pull Request + +A pull request won't be reviewed without the following: + +* Updated docs +* Test coverage of the added feature +* Clear explaination of the intention of the pull request. + +Thanks! diff --git a/.gitignore b/.gitignore index 9fc544b..e8f7344 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ example/ .bundle bin .secrets.yml +test_pipeline.yml +.vscode/ \ No newline at end of file diff --git a/.rubocop.yml b/.rubocop.yml index a6eef75..59228f1 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,18 +1,9 @@ # This is the configuration used to check the rubocop source code. -Style/Encoding: - Enabled: true - -Metrics/LineLength: - Enabled: false - require: rubocop-rspec -RSpec/FilePath: - Enabled: false - -Performance/RedundantMatch: - Enabled: false +Style/FrozenStringLiteralComment: + Enabled: always AllCops: - TargetRubyVersion: 2.2 + TargetRubyVersion: 2.5 diff --git a/CHANGELOG.md b/CHANGELOG.md index f44d4e2..7db79a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,51 @@ +# v35 @ 3/14/2018 + +* filter PRs by the mergeable status (thanks @ndmckinley) +* filter PRs based on the author's association to the repo (thanks @ndmckinley) +* Get the SHA of the base branch (thanks @aconrad) + +# v34 + +* fix shell escaping in pull request meta information so it doesn't break with special characters + +# v33 + +* access the Pull Request message body (thanks @ndmckinley) + +# v32 @ 1/27/2018 + +* enable filtering out PRs that have `ci_skip` messages (thanks @aditya87) + +# v31 @ 11/7/2017 + +* apply `depth` to the `git fetch` of the PR (thanks @bhcleek) +* checkout the original branch the PR was made against (thanks @bhcleek) + +# v30 + +* Use correct user for PR author in the git meta data Thanks @victoru + +# v29 + +* populate file with latest commit hash of PR branch + +# v28 @ 9/5/2017 + +* Output user of the PR to the meta data Thanks @drnic + +# v27 @ 8/31/2017 + +* Output the `git config` meta values into files in the `.git/` directory. Thanks @mazubieta + +# v26 @ 8/21/2017 + +* Support evaluating the concourse BUILD environment variables in a context. + +# v25 @ 8/1/2017 + +* support caching of API requests to Github. This decreases hitting the +rate limit per hour. It does not reduce the number of requests, though. + # v24 @ 6/26/2017 * `README.md` updates from @cjcjameson and @richarddowner diff --git a/Dockerfile b/Dockerfile index 40e9e43..ed63929 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,44 @@ -FROM alpine - -RUN apk add --update ca-certificates -RUN apk add --update curl -RUN apk add --update git -RUN apk add --update jq -RUN apk add --update openssh-client -RUN apk add --update perl -RUN apk add --update ruby -RUN apk add --update ruby-json -RUN gem install octokit activesupport httpclient faraday-http-cache --no-rdoc --no-ri +# Stage: Base +FROM alpine as resource +RUN set -ex; \ + apk add --update \ + ca-certificates \ + curl \ + git \ + jq \ + openssh-client \ + perl \ + ruby \ + ruby-json \ + ruby-bundler \ + ; \ + rm -rf /var/cache/apk/*; + +ADD Gemfile Gemfile.lock /opt/resource/ +RUN cd /opt/resource && bundle install --without test development ADD assets/ /opt/resource/ RUN chmod +x /opt/resource/* ADD scripts/install_git_lfs.sh install_git_lfs.sh RUN ./install_git_lfs.sh + +# Stage: Testing +FROM resource as tests + +RUN apk add --update \ + ruby-bundler \ + ruby-io-console \ + ruby-dev \ + openssl-dev \ + alpine-sdk + +COPY Gemfile Gemfile.lock /resource/ + +RUN cd /resource && bundle install + +COPY . /resource + +RUN cd /resource && rspec + +# Stage: Final +FROM resource diff --git a/Dockerfile.test b/Dockerfile.test deleted file mode 100644 index f01bffb..0000000 --- a/Dockerfile.test +++ /dev/null @@ -1,23 +0,0 @@ -FROM alpine - -RUN apk add --update ca-certificates -RUN apk add --update curl -RUN apk add --update git -RUN apk add --update jq -RUN apk add --update openssh-client -RUN apk add --update perl -RUN apk add --update ruby -RUN apk add --update ruby-json -RUN gem install octokit activesupport httpclient faraday-http-cache --no-rdoc --no-ri - -ADD assets/ /opt/resource/ -RUN chmod +x /opt/resource/* -ADD scripts/install_git_lfs.sh install_git_lfs.sh -RUN ./install_git_lfs.sh - -RUN apk add --update \ - ruby-bundler \ - ruby-io-console \ - ruby-dev \ - openssl-dev \ - alpine-sdk diff --git a/Gemfile b/Gemfile index f7e0b4c..a6f7ee4 100644 --- a/Gemfile +++ b/Gemfile @@ -1,9 +1,13 @@ +# frozen_string_literal: true + source 'https://rubygems.org' -gem 'octokit' -gem 'httpclient' -gem 'faraday-http-cache' +ruby '~> 2.4' + gem 'activesupport' +gem 'faraday-http-cache' +gem 'httpclient' +gem 'octokit' group :development do gem 'pry' @@ -12,7 +16,7 @@ group :development do end group :test do - gem 'rspec' gem 'puffing-billy' + gem 'rspec' gem 'webmock' end diff --git a/Gemfile.lock b/Gemfile.lock index 8b70355..b46d128 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,95 +1,96 @@ GEM remote: https://rubygems.org/ specs: - activesupport (5.1.2) + activesupport (5.2.0) concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (~> 0.7) + i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) - addressable (2.5.0) - public_suffix (~> 2.0, >= 2.0.2) - ast (2.3.0) - coderay (1.1.1) + addressable (2.5.2) + public_suffix (>= 2.0.2, < 4.0) + ast (2.4.0) + coderay (1.1.2) concurrent-ruby (1.0.5) cookiejar (0.3.3) crack (0.4.3) safe_yaml (~> 1.0.0) - diff-lcs (1.2.5) + diff-lcs (1.3) em-http-request (1.1.5) addressable (>= 2.3.4) cookiejar (!= 0.3.1) em-socksify (>= 0.3) eventmachine (>= 1.0.3) http_parser.rb (>= 0.6.0) - em-socksify (0.3.1) + em-socksify (0.3.2) eventmachine (>= 1.0.0.beta.4) - em-synchrony (1.0.5) + em-synchrony (1.0.6) eventmachine (>= 1.0.0.beta.1) eventmachine (1.0.9.1) eventmachine_httpserver (0.2.1) - faraday (0.9.2) + faraday (0.14.0) multipart-post (>= 1.2, < 3) faraday-http-cache (2.0.0) faraday (~> 0.8) - hashdiff (0.3.0) + hashdiff (0.3.7) http_parser.rb (0.6.0) - httpclient (2.8.2.4) - i18n (0.8.6) - method_source (0.8.2) - minitest (5.10.2) - multi_json (1.12.1) + httpclient (2.8.3) + i18n (1.0.0) + concurrent-ruby (~> 1.0) + method_source (0.9.0) + minitest (5.11.3) + multi_json (1.13.1) multipart-post (2.0.0) - octokit (4.6.0) + octokit (4.8.0) sawyer (~> 0.8.0, >= 0.5.3) - parser (2.3.1.4) - ast (~> 2.2) + parallel (1.12.1) + parser (2.5.0.5) + ast (~> 2.4.0) powerpack (0.1.1) - pry (0.10.4) + pry (0.11.3) coderay (~> 1.1.0) - method_source (~> 0.8.1) - slop (~> 3.4) - public_suffix (2.0.4) - puffing-billy (0.9.1) - addressable - em-http-request (~> 1.1.0) + method_source (~> 0.9.0) + public_suffix (3.0.2) + puffing-billy (1.0.0) + addressable (~> 2.5) + em-http-request (~> 1.1, >= 1.1.0) em-synchrony eventmachine (~> 1.0.4) eventmachine_httpserver http_parser.rb (~> 0.6.0) multi_json - rainbow (2.1.0) - rspec (3.5.0) - rspec-core (~> 3.5.0) - rspec-expectations (~> 3.5.0) - rspec-mocks (~> 3.5.0) - rspec-core (3.5.4) - rspec-support (~> 3.5.0) - rspec-expectations (3.5.0) + rainbow (3.0.0) + rspec (3.7.0) + rspec-core (~> 3.7.0) + rspec-expectations (~> 3.7.0) + rspec-mocks (~> 3.7.0) + rspec-core (3.7.1) + rspec-support (~> 3.7.0) + rspec-expectations (3.7.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.5.0) - rspec-mocks (3.5.0) + rspec-support (~> 3.7.0) + rspec-mocks (3.7.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.5.0) - rspec-support (3.5.0) - rubocop (0.45.0) - parser (>= 2.3.1.1, < 3.0) + rspec-support (~> 3.7.0) + rspec-support (3.7.1) + rubocop (0.54.0) + parallel (~> 1.10) + parser (>= 2.5) powerpack (~> 0.1) - rainbow (>= 1.99.1, < 3.0) + rainbow (>= 2.2.2, < 4.0) ruby-progressbar (~> 1.7) unicode-display_width (~> 1.0, >= 1.0.1) - rubocop-rspec (1.8.0) - rubocop (>= 0.42.0) - ruby-progressbar (1.8.1) + rubocop-rspec (1.25.0) + rubocop (>= 0.53.0) + ruby-progressbar (1.9.0) safe_yaml (1.0.4) - sawyer (0.8.0) + sawyer (0.8.1) addressable (>= 2.3.5, < 2.6) - faraday (~> 0.8, < 0.10) - slop (3.6.0) + faraday (~> 0.8, < 1.0) thread_safe (0.3.6) - tzinfo (1.2.3) + tzinfo (1.2.5) thread_safe (~> 0.1) - unicode-display_width (1.1.1) - webmock (2.1.0) + unicode-display_width (1.3.0) + webmock (3.3.0) addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff @@ -109,5 +110,8 @@ DEPENDENCIES rubocop-rspec webmock +RUBY VERSION + ruby 2.5.0p0 + BUNDLED WITH - 1.15.1 + 1.16.1 diff --git a/README.md b/README.md index 4050e88..f45e30d 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,20 @@ +# DEPRECATED +We would like for you to start using the new [github-pr-resource](https://github.com/telia-oss/github-pr-resource) that is based on Github's GraphQL resources. Using GraphQL fixes a lot of issues this repo has. + +For history or context regarding this change, please see this [issue](https://github.com/telia-oss/github-pr-resource/issues/34). + # Github Pull Request Resource -Tracks pull requests made to a particular github repo. In the spirit of [Travis +Tracks Github pull requests made to a particular Github repo. In the spirit of [Travis CI](https://travis-ci.org/), a status of pending, success, or failure will be set on the pull request, which must be explicitly defined in your pipeline. +NOTE: Pull requests are implemented differently between the git repo providers. This +resource only support *GITHUB*. + ## Deploying to Concourse -You can use the docker image by defining the [resource type](http://concourse.ci/configuring-resource-types.html) in your pipeline YAML. +You can use the docker image by defining the [resource type](https://concourse-ci.org/resource-types.html) in your pipeline YAML. For example: @@ -38,6 +46,9 @@ resource_types: linking to builds. On newer versions of Concourse ( >= v0.71.0) , the resource will automatically sets the URL. + This supports the [build environment](https://concourse-ci.org/implementing-resources.html#resource-metadata) + variables provided by concourse. For example, `context: $BUILD_JOB_NAME` will set the context to the job name. + * `private_key`: *Optional.* Private key to use when pulling/pushing. Example: ``` @@ -55,6 +66,15 @@ resource_types: * `disable_forks`: *Optional*, default false. If set to `true`, it will filter out pull requests that were created via users that forked from your repo. +* `only_mergeable`: *Optional*, default false. If set to `true`, it will filter + out pull requests that are not mergeable. A pull request is mergeable if it has no merge conflicts. + +* `require_review_approval`: *Optional*, default false. If set to `true`, it will + filter out pull requests that do not have an Approved review. + +* `authorship_restriction`: *Optional*, default false. If set to `true`, will only + return PRs created by someone who is a collaborator, repo owner, or organization member. + * `label`: *Optional.* If set to a string it will only return pull requests that have been marked with that specific label. It is case insensitive. @@ -70,8 +90,11 @@ marked with that specific label. It is case insensitive. * `ignore_paths`: *Optional.* The inverse of `paths`; changes to the specified files are ignored. +* `ci_skip`: *Optional.* Filters out PRs that have `[ci skip]` message. Default + is `false`. + * `skip_ssl_verification`: *Optional.* Skips git ssl verification by exporting - `GIT_SSL_NO_VERIFY=true`. + `GIT_SSL_NO_VERIFY=true` and applying it to the Github API client. * `git_config`: *Optional*. If specified as (list of pairs `name` and `value`) it will configure git global options, setting each name with each value. @@ -86,19 +109,14 @@ marked with that specific label. It is case insensitive. ### `check`: Check for new pull requests Concourse resources always iterate over the latest version. This maps well to -semver and git, but not with pull requests. To find the latests pull -requests, `check` queries for all PRs, selects only PRs without `concourse-ci` -status messages, and then only returns the oldest one from list. - -To ensure that `check` can iterate over all PRs, you must explicitly define an -`out` for the PR. +semver and git, but not with pull requests. This filters all open PRs +sorted by most recently updated. ### `in`: Clone the repository, at the given pull request ref -Clones the repository to the destination, and locks it down to a given ref. It is important -to specify `version: every`, otherwise you will only ever get the latest PR. - -Submodules are initialized and updated recursively, there is no option to to disable that, currently. +Clones the repository to the destination, and locks it down to a given ref. It +is important to specify `version: every`, otherwise you will only ever get the +latest PR. There is `git config` information set on the repo about the PR, which can be consumed within your tasks. @@ -108,13 +126,35 @@ For example: git config --get pullrequest.url # returns the URL to the pull request git config --get pullrequest.branch # returns the branch name used for the pull request git config --get pullrequest.id # returns the ID number of the PR +git config --get pullrequest.body # returns the PR body git config --get pullrequest.basebranch # returns the base branch used for the pull request +git config --get pullrequest.basesha # returns the commit of the base branch used for the pull request +git config --get pullrequest.userlogin # returns the github user login for the pull request author ``` + +#### Additional files populated + + * `.git/id`: the pull request id + + * `.git/url`: the URL for the pull request + + * `.git/branch`: the branch associated with the pull request + + * `.git/base_branch`: the base branch of the pull request + + * `.git/base_sha`: the commit of the base branch of the pull request + + * `.git/userlogin`: the user login of the pull request author + + * `.git/head_sha`: the latest commit hash of the branch associated with the pull request + + * `.git/body`: the body of the pull request. + #### Parameters * `git.depth`: *Optional.* If a positive integer is given, *shallow* clone the - repository using the `--depth` option. + repository using the `--depth` option. * `git.submodules`: *Optional*, default `all`. If `none`, submodules will not be fetched. If specified as a list of paths, only the given paths will be @@ -135,23 +175,30 @@ Set the status message for `concourse-ci` context on specified pull request. * `path`: *Required.* The path of the repository to reference the pull request. * `status`: *Required.* The status of success, failure, error, or pending. - * [`on_success`](https://concourse.ci/on-success-step.html) and [`on_failure`](https://concourse.ci/on-failure-step.html) triggers may be useful for you when you wanted to reflect build result to the PR (see the example below). + * [`on_success`](https://concourse-ci.org/on-success-step-hook.html#on_success) and [`on_failure`](https://concourse-ci.org/on-failure-step-hook.html#on_failure) triggers may be useful for you when you wanted to reflect build result to the PR (see the example below). * `context`: *Optional.* The context on the specified pull request (defaults to `status`). Any context will be prepended with `concourse-ci`, so a context of `unit-tests` will appear as `concourse-ci/unit-tests` on Github. -* `comment`: *Optional.* The file path of the comment message. Comment owner is same with the owner of `access_token`. + This supports the [build environment](https://concourse-ci.org/implementing-resources.html#resource-metadata) + variables provided by concourse. For example, `context: $BUILD_JOB_NAME` will set the context to the job name. + +* `comment`: *Optional.* The file path of the comment message. Comment owner is same with the owner of `access_token`. * `merge.method`: *Optional.* Use this to merge the PR into the target branch of the PR. There are three available merge methods -- `merge`, `squash`, or `rebase`. Please this [doc](https://developer.github.com/changes/2016-09-26-pull-request-merge-api-update/) for more information. * `merge.commit_msg`: *Optional.* Used with `merge` to set the commit message for the merge. Specify a file path to the merge commit message. +* `label`: *Optional.* A label to add to the pull request. + ## Example pipeline Please see this repo's [pipeline](https://github.com/jtarchie/pullrequest-resource/blob/master/.concourse.yml) for a perfect example. -## Tests +There's also an [example](https://github.com/starkandwayne/concourse-pullrequest-playtime) by @starkandwayne. + +## Running the tests Requires `ruby` to be installed. @@ -161,3 +208,8 @@ Requires `ruby` to be installed. bundle exec rspec ``` +Or with the `Dockerfile`, which runs the tests to see if it can successfully build: + + ``` + docker build . + ``` diff --git a/assets/lib/commands/base.rb b/assets/lib/commands/base.rb index 3affe17..4af37ec 100644 --- a/assets/lib/commands/base.rb +++ b/assets/lib/commands/base.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'faraday' require 'octokit' require 'faraday-http-cache' @@ -29,7 +31,7 @@ def initialize(input: Input.instance) def setup_octokit Octokit.auto_paginate = true - Octokit.connection_options[:ssl] = { verify: false } if input.source.no_ssl_verify + Octokit.connection_options[:ssl] = { verify: false } if input.source.skip_ssl_verification Octokit.configure do |c| c.api_endpoint = input.source.api_endpoint if input.source.api_endpoint c.access_token = input.source.access_token diff --git a/assets/lib/commands/check.rb b/assets/lib/commands/check.rb index 17f1571..792e211 100755 --- a/assets/lib/commands/check.rb +++ b/assets/lib/commands/check.rb @@ -1,4 +1,5 @@ #!/usr/bin/env ruby +# frozen_string_literal: true require 'json' require_relative 'base' @@ -18,7 +19,7 @@ def repo end end -if __FILE__ == $PROGRAM_NAME +if $PROGRAM_NAME == __FILE__ command = Commands::Check.new puts JSON.generate(command.output) end diff --git a/assets/lib/commands/in.rb b/assets/lib/commands/in.rb index eb95c7b..3308c0d 100755 --- a/assets/lib/commands/in.rb +++ b/assets/lib/commands/in.rb @@ -1,7 +1,9 @@ #!/usr/bin/env ruby +# frozen_string_literal: true require 'English' require 'json' +require 'shellwords' require_relative 'base' module Commands @@ -20,19 +22,33 @@ def output raise 'PR has merge conflicts' if pr['mergeable'] == false && fetch_merge - system("git clone #{depth_flag} #{uri} #{destination} 1>&2") + system("git clone #{depth_flag} --branch #{pr['base']['ref']} #{uri} #{destination} 1>&2") raise 'git clone failed' unless $CHILD_STATUS.exitstatus.zero? + Dir.chdir(File.join(destination, '.git')) do + File.write('url', pr['html_url']) + File.write('id', pr['number']) + File.write('body', pr['body']) + File.write('branch', pr['head']['ref']) + File.write('base_branch', pr['base']['ref']) + File.write('base_sha', pr['base']['sha']) + File.write('userlogin', pr['user']['login']) + File.write('head_sha', pr['head']['sha']) + end + Dir.chdir(destination) do - raise 'git clone failed' unless system("git fetch -q origin pull/#{id}/#{remote_ref}:#{branch_ref} 1>&2") + raise 'git clone failed' unless system("git fetch #{depth_flag} -q origin pull/#{id}/#{remote_ref}:#{branch_ref} 1>&2") system <<-BASH git checkout #{branch_ref} 1>&2 - git config --add pullrequest.url #{pr['html_url']} 1>&2 - git config --add pullrequest.id #{pr['number']} 1>&2 - git config --add pullrequest.branch #{pr['head']['ref']} 1>&2 - git config --add pullrequest.basebranch #{pr['base']['ref']} 1>&2 + git config --add pullrequest.url #{pr['html_url'].to_s.shellescape} 1>&2 + git config --add pullrequest.id #{pr['number'].to_s.shellescape} 1>&2 + git config --add pullrequest.body #{pr['body'].to_s.shellescape} 1>&2 + git config --add pullrequest.branch #{pr['head']['ref'].to_s.shellescape} 1>&2 + git config --add pullrequest.basebranch #{pr['base']['ref'].to_s.shellescape} 1>&2 + git config --add pullrequest.basesha #{pr['base']['sha'].to_s.shellescape} 1>&2 + git config --add pullrequest.userlogin #{pr['user']['login'].to_s.shellescape} 1>&2 BASH case input.params.git.submodules @@ -88,7 +104,7 @@ def depth_flag end end -if __FILE__ == $PROGRAM_NAME +if $PROGRAM_NAME == __FILE__ destination = ARGV.shift command = Commands::In.new(destination: destination) puts JSON.generate(command.output) diff --git a/assets/lib/commands/out.rb b/assets/lib/commands/out.rb index 889ea5c..547a581 100755 --- a/assets/lib/commands/out.rb +++ b/assets/lib/commands/out.rb @@ -1,4 +1,5 @@ #!/usr/bin/env ruby +# frozen_string_literal: true require 'json' require_relative 'base' @@ -51,10 +52,10 @@ def output contextes.each do |context| Status.new( state: params.status, - atc_url: atc_url, + atc_url: whitelist(context: atc_url), sha: sha, repo: repo, - context: context + context: whitelist(context: context) ).create! end @@ -65,6 +66,11 @@ def output metadata << { 'name' => 'comment', 'value' => comment } end + if params.label + Octokit.add_labels_to_an_issue(input.source.repo, id, [params.label]) + metadata << { 'name' => 'label', 'value' => params.label } + end + if params.merge.method commit_msg = if params.merge.commit_msg commit_path = File.join(destination, params.merge.commit_msg) @@ -85,19 +91,27 @@ def output private + def whitelist(context:) + c = context.dup + %w[BUILD_ID BUILD_NAME BUILD_JOB_NAME BUILD_PIPELINE_NAME BUILD_TEAM_NAME ATC_EXTERNAL_URL].each do |name| + c.gsub!("$#{name}", ENV[name] || '') + end + c + end + def params input.params end def check_defaults! - raise %(`status` "#{params.status}" is not supported -- only success, failure, error, or pending) unless %w(success failure error pending).include?(params.status) - raise %(`merge.method` "#{params.merge.method}" is not supported -- only merge, squash, or rebase) if params.merge.method && !%w(merge squash rebase).include?(params.merge.method) + raise %(`status` "#{params.status}" is not supported -- only success, failure, error, or pending) unless %w[success failure error pending].include?(params.status) + raise %(`merge.method` "#{params.merge.method}" is not supported -- only merge, squash, or rebase) if params.merge.method && !%w[merge squash rebase].include?(params.merge.method) raise '`path` required in `params`' unless params.path end end end -if __FILE__ == $PROGRAM_NAME +if $PROGRAM_NAME == __FILE__ destination = ARGV.shift command = Commands::Out.new(destination: destination) puts JSON.generate(command.output) diff --git a/assets/lib/filters/all.rb b/assets/lib/filters/all.rb index 04606e5..91afce3 100644 --- a/assets/lib/filters/all.rb +++ b/assets/lib/filters/all.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'octokit' require_relative '../pull_request' @@ -9,7 +11,7 @@ def initialize(pull_requests: [], input: Input.instance) def pull_requests @pull_requests ||= Octokit.pulls(input.source.repo, pull_options).map do |pr| - PullRequest.new(pr: pr) + PullRequest.new(pr: pr) # keep this lazy, specific filters should pull data if they need to end end diff --git a/assets/lib/filters/approval.rb b/assets/lib/filters/approval.rb new file mode 100644 index 0000000..a1ce168 --- /dev/null +++ b/assets/lib/filters/approval.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Filters + class Approval + def initialize(pull_requests:, input: Input.instance) + @pull_requests = pull_requests + @input = input + end + + def pull_requests + if @input.source.require_review_approval + @pull_requests.delete_if { |x| !x.review_approved? } + end + if @input.source.authorship_restriction + @pull_requests.delete_if { |x| !x.author_associated? } + end + + @pull_requests + end + end +end diff --git a/assets/lib/filters/ci_skip.rb b/assets/lib/filters/ci_skip.rb new file mode 100644 index 0000000..65f45ab --- /dev/null +++ b/assets/lib/filters/ci_skip.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Filters + class CISkip + def initialize(pull_requests:, input: Input.instance) + @pull_requests = pull_requests + @input = input + end + + def pull_requests + if !@input.source.ci_skip + @pull_requests + else + @memoized ||= @pull_requests.delete_if do |pr| + latest_commit = Octokit.commit(@input.source.repo, pr.sha) + latest_commit['commit']['message'] =~ /\[(ci skip|skip ci)\]/ + end + end + end + end +end diff --git a/assets/lib/filters/fork.rb b/assets/lib/filters/fork.rb index 6bc139a..f06c923 100644 --- a/assets/lib/filters/fork.rb +++ b/assets/lib/filters/fork.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Filters class Fork def initialize(pull_requests:, input: Input.instance) diff --git a/assets/lib/filters/label.rb b/assets/lib/filters/label.rb index 4f2e2db..56255ef 100644 --- a/assets/lib/filters/label.rb +++ b/assets/lib/filters/label.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Filters class Label def initialize(pull_requests:, input: Input.instance) diff --git a/assets/lib/filters/mergeable.rb b/assets/lib/filters/mergeable.rb new file mode 100644 index 0000000..b3ceb5d --- /dev/null +++ b/assets/lib/filters/mergeable.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Filters + class Mergeable + def initialize(pull_requests:, input: Input.instance) + @pull_requests = pull_requests + @input = input + end + + def pull_requests + if @input.source.only_mergeable + + @memoized ||= @pull_requests.delete_if do |pr| + response = Octokit.pull_request(@input.source.repo, pr.id) + !response['mergeable'] + end + else + @pull_requests + end + end + end +end diff --git a/assets/lib/filters/path.rb b/assets/lib/filters/path.rb index 545dc13..2126c4e 100644 --- a/assets/lib/filters/path.rb +++ b/assets/lib/filters/path.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative '../input' module Filters @@ -13,7 +15,7 @@ def pull_requests return @pull_requests if paths.empty? && ignore_paths.empty? - @memoized ||= @pull_requests.select do |pr| + @memoized ||= @pull_requests.reject do |pr| files = Octokit.pull_request_files(@input.source.repo, pr.id) unless paths.empty? files.select! do |file| @@ -29,7 +31,7 @@ def pull_requests end end end - !files.empty? + files.empty? end end end diff --git a/assets/lib/input.rb b/assets/lib/input.rb index 70b8ae9..78d1d4c 100644 --- a/assets/lib/input.rb +++ b/assets/lib/input.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'json' require 'ostruct' diff --git a/assets/lib/pull_request.rb b/assets/lib/pull_request.rb index e79a3e1..6c329ea 100644 --- a/assets/lib/pull_request.rb +++ b/assets/lib/pull_request.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'octokit' class PullRequest @@ -14,6 +16,17 @@ def from_fork? base_repo != head_repo end + def review_approved? + Octokit.pull_request_reviews(base_repo, id).any? { |r| r['state'] == 'APPROVED' } + end + + def author_associated? + # Checks whether the author is associated with the repo that the PR is against: + # either the owner of that repo, someone invited to collaborate, or a member + # of the organization who owns that repository. + %w[OWNER COLLABORATOR MEMBER].include? @pr['author_association'] + end + def equals?(id:, sha:) [self.sha, self.id.to_s] == [sha, id.to_s] end diff --git a/assets/lib/repository.rb b/assets/lib/repository.rb index a8ad96f..1ae03ed 100644 --- a/assets/lib/repository.rb +++ b/assets/lib/repository.rb @@ -1,12 +1,17 @@ +# frozen_string_literal: true + require_relative 'filters/all' require_relative 'filters/fork' require_relative 'filters/label' require_relative 'filters/path' +require_relative 'filters/ci_skip' +require_relative 'filters/mergeable' +require_relative 'filters/approval' class Repository attr_reader :name - def initialize(name:, input: Input.instance, filters: [Filters::All, Filters::Path, Filters::Fork, Filters::Label]) + def initialize(name:, input: Input.instance, filters: [Filters::All, Filters::Path, Filters::Fork, Filters::Label, Filters::CISkip, Filters::Mergeable, Filters::Approval]) @filters = filters @name = name @input = input diff --git a/assets/lib/status.rb b/assets/lib/status.rb index 28d0773..6fdd982 100644 --- a/assets/lib/status.rb +++ b/assets/lib/status.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'octokit' class Status diff --git a/spec/commands/check_spec.rb b/spec/commands/check_spec.rb index 52104cd..d225402 100644 --- a/spec/commands/check_spec.rb +++ b/spec/commands/check_spec.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + require_relative '../../assets/lib/commands/check' require 'webmock/rspec' require 'json' describe Commands::Check do def check(payload) - payload['source']['no_ssl_verify'] = true + payload['source']['skip_ssl_verification'] = true Input.instance(payload: payload) Commands::Check.new.output.map &:as_json @@ -62,6 +64,17 @@ def stub_json(uri, body) ] end end + + context 'and the top commit has [ci skip] in its message' do + before do + stub_json('https://api.github.com:443/repos/jtarchie/test/pulls?direction=asc&per_page=100&sort=updated&state=open', [{ number: 1, head: { sha: 'abcdef' } }]) + stub_json('https://api.github.com:443/repos/jtarchie/test/commits/abcdef', sha: 'abcdef', commit: { message: 'foo [ci skip] bar' }) + end + + it 'returns no versions' do + expect(check('source' => { 'repo' => 'jtarchie/test', 'ci_skip' => true }, 'version' => {})).to eq [] + end + end end context 'when there is more than one open pull request' do @@ -87,12 +100,12 @@ def stub_json(uri, body) end context 'when paginating through many PRs' do - def stub_body_json(uri, body, headers={}) + def stub_body_json(uri, body, headers = {}) stub_request(:get, uri) .to_return(headers: { 'Content-Type' => 'application/json', - 'ETag' => Digest::MD5.hexdigest(body.to_json), - }.merge(headers), body: body.to_json) + 'ETag' => Digest::MD5.hexdigest(body.to_json) + }.merge(headers), body: body.to_json) end def stub_cache_json(uri) @@ -104,18 +117,17 @@ def stub_cache_json(uri) pull_requests = (1..100).map do |i| { number: i, head: { sha: "abcdef-#{i}", repo: { full_name: 'jtarchie/test' } }, base: { repo: { full_name: 'jtarchie/test' } } } end - stub_body_json('https://api.github.com/repos/jtarchie/test/pulls?direction=asc&per_page=100&sort=updated&state=open', pull_requests[0..49], { - 'Link' => "; rel=\"next\"" - }) + + stub_body_json('https://api.github.com/repos/jtarchie/test/pulls?direction=asc&per_page=100&sort=updated&state=open', pull_requests[0..49], 'Link' => '; rel="next"') stub_body_json('https://api.github.com/repos/jtarchie/test/pulls?direction=asc&per_page=100&sort=updated&state=open&page=2', pull_requests[50..99]) - first_prs = check('source' => { 'repo' => 'jtarchie/test' } ) + first_prs = check('source' => { 'repo' => 'jtarchie/test' }) expect(first_prs.length).to eq 100 Billy.proxy.reset stub_cache_json('https://api.github.com/repos/jtarchie/test/pulls?direction=asc&per_page=100&sort=updated&state=open') stub_cache_json('https://api.github.com/repos/jtarchie/test/pulls?direction=asc&per_page=100&sort=updated&state=open&page=2') - second_prs = check('source' => { 'repo' => 'jtarchie/test' } ) + second_prs = check('source' => { 'repo' => 'jtarchie/test' }) expect(first_prs).to eq second_prs # expect A == B diff --git a/spec/commands/in_spec.rb b/spec/commands/in_spec.rb index cd7c7b1..b40b084 100644 --- a/spec/commands/in_spec.rb +++ b/spec/commands/in_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'json' require 'tmpdir' require 'webmock/rspec' @@ -15,7 +17,7 @@ def git_uri let(:dest_dir) { Dir.mktmpdir } def get(payload) - payload['source']['no_ssl_verify'] = true + payload['source']['skip_ssl_verification'] = true Input.instance(payload: payload) command = Commands::In.new(destination: dest_dir) command.output @@ -52,7 +54,24 @@ def dest_dir end before(:all) do - stub_json('https://api.github.com:443/repos/jtarchie/test/pulls/1', html_url: 'http://example.com', number: 1, head: { ref: 'foo' }, base: { ref: 'master' }) + stub_json('https://api.github.com:443/repos/jtarchie/test/pulls/1', + html_url: 'http://example.com', + number: 1, + head: { + ref: 'foo', + sha: 'hash' + }, + base: { + ref: 'master', + sha: 'basehash', + user: { + login: 'jtarchie' + } + }, + body: %(A comment with shell stuff var='\'`rm -rf ./*`\''), + user: { + login: 'jtarchie-contributor' + }) @output = get('version' => { 'ref' => @ref, 'pr' => '1' }, 'source' => { 'uri' => git_uri, 'repo' => 'jtarchie/test' }) end @@ -78,6 +97,11 @@ def dest_dir expect(value).to eq 'pr-foo' end + it 'sets config of the PR body' do + value = git('config --get pullrequest.body', dest_dir) + expect(value).to eq "A comment with shell stuff var=''`rm -rf ./*`''" + end + it 'sets config variable to branch name' do value = git('config pullrequest.branch', dest_dir) expect(value).to eq 'foo' @@ -87,11 +111,56 @@ def dest_dir value = git('config pullrequest.basebranch', dest_dir) expect(value).to eq 'master' end + + it 'sets config variable to basesha name' do + value = git('config pullrequest.basesha', dest_dir) + expect(value).to eq 'basehash' + end + + it 'sets config variable to user_login name' do + value = git('config pullrequest.userlogin', dest_dir) + expect(value).to eq 'jtarchie-contributor' + end + + it 'creates a file that icludes the id in the .git folder' do + value = File.read(File.join(dest_dir, '.git', 'id')).strip + expect(value).to eq '1' + end + + it 'creates a file that icludes the url in the .git folder' do + value = File.read(File.join(dest_dir, '.git', 'url')).strip + expect(value).to eq 'http://example.com' + end + + it 'creates a file that icludes ahe branch in the .git folder' do + value = File.read(File.join(dest_dir, '.git', 'branch')).strip + expect(value).to eq 'foo' + end + + it 'creates a file that icludes the base_branch in the .git folder' do + value = File.read(File.join(dest_dir, '.git', 'base_branch')).strip + expect(value).to eq 'master' + end + + it 'creates a file that icludes the base_sha in the .git folder' do + value = File.read(File.join(dest_dir, '.git', 'base_sha')).strip + expect(value).to eq 'basehash' + end + + it 'creates a file that includes the hash of the branch in the .git folder' do + value = File.read(File.join(dest_dir, '.git', 'head_sha')).strip + expect(value).to eq 'hash' + end + + it 'creates a file that contains the PR body in the .git folder' do + value = File.read(File.join(dest_dir, '.git', 'body')).strip + expect(value).to eq "A comment with shell stuff var=''`rm -rf ./*`''" + end end context 'when the git clone fails' do it 'provides a helpful erorr message' do - stub_json('https://api.github.com:443/repos/jtarchie/test/pulls/1', html_url: 'http://example.com', number: 1, head: { ref: 'foo' }, base: { ref: 'master' }) + stub_json('https://api.github.com:443/repos/jtarchie/test/pulls/1', html_url: 'http://example.com', number: 1, head: { ref: 'foo' }, base: { ref: 'master', user: { login: 'jtarchie' } }, user: { login: 'jtarchie-contributor' }) expect do get('version' => { 'ref' => @ref, 'pr' => '1' }, 'source' => { 'uri' => 'invalid_git_uri', 'repo' => 'jtarchie/test' }) @@ -104,7 +173,7 @@ def dest_dir context 'and fetch_merge is false' do it 'checks out as a branch named in the PR' do stub_json('https://api.github.com:443/repos/jtarchie/test/pulls/1', - html_url: 'http://example.com', number: 1, head: { ref: 'foo' }, base: { ref: 'master' }, mergeable: true) + html_url: 'http://example.com', number: 1, head: { ref: 'foo' }, base: { ref: 'master', user: { login: 'jtarchie' } }, user: { login: 'jtarchie-contributor' }, mergeable: true) get('version' => { 'ref' => @ref, 'pr' => '1' }, 'source' => { 'uri' => git_uri, 'repo' => 'jtarchie/test' }, 'params' => { 'fetch_merge' => false }) @@ -114,7 +183,7 @@ def dest_dir it 'does not fail cloning' do stub_json('https://api.github.com:443/repos/jtarchie/test/pulls/1', - html_url: 'http://example.com', number: 1, head: { ref: 'foo' }, base: { ref: 'master' }, mergeable: true) + html_url: 'http://example.com', number: 1, head: { ref: 'foo' }, base: { ref: 'master', user: { login: 'jtarchie' } }, user: { login: 'jtarchie-contributor' }, mergeable: true) expect do get('version' => { 'ref' => @ref, 'pr' => '1' }, 'source' => { 'uri' => git_uri, 'repo' => 'jtarchie/test' }, 'params' => { 'fetch_merge' => false }) @@ -125,7 +194,7 @@ def dest_dir context 'and fetch_merge is true' do it 'checks out the branch the PR would be merged into' do stub_json('https://api.github.com:443/repos/jtarchie/test/pulls/1', - html_url: 'http://example.com', number: 1, head: { ref: 'foo' }, base: { ref: 'master' }, mergeable: true) + html_url: 'http://example.com', number: 1, head: { ref: 'foo' }, base: { ref: 'master', user: { login: 'jtarchie' } }, user: { login: 'jtarchie-contributor' }, mergeable: true) get('version' => { 'ref' => @ref, 'pr' => '1' }, 'source' => { 'uri' => git_uri, 'repo' => 'jtarchie/test' }, 'params:' => { 'fetch_merge' => true }) @@ -135,7 +204,7 @@ def dest_dir it 'does not fail cloning' do stub_json('https://api.github.com:443/repos/jtarchie/test/pulls/1', - html_url: 'http://example.com', number: 1, head: { ref: 'foo' }, base: { ref: 'master' }, mergeable: true) + html_url: 'http://example.com', number: 1, head: { ref: 'foo' }, base: { ref: 'master', user: { login: 'jtarchie' } }, user: { login: 'jtarchie-contributor' }, mergeable: true) expect do get('version' => { 'ref' => @ref, 'pr' => '1' }, 'source' => { 'uri' => git_uri, 'repo' => 'jtarchie/test' }, 'params' => { 'fetch_merge' => true }) @@ -148,7 +217,7 @@ def dest_dir context 'and fetch_merge is true' do it 'raises a helpful error message' do stub_json('https://api.github.com:443/repos/jtarchie/test/pulls/1', - html_url: 'http://example.com', number: 1, head: { ref: 'foo' }, base: { ref: 'master' }, mergeable: false) + html_url: 'http://example.com', number: 1, head: { ref: 'foo' }, base: { ref: 'master', user: { login: 'jtarchie' } }, user: { login: 'jtarchie-contributor' }, mergeable: false) expect do get('version' => { 'ref' => @ref, 'pr' => '1' }, 'source' => { 'uri' => git_uri, 'repo' => 'jtarchie/test' }, 'params' => { 'fetch_merge' => true }) @@ -162,7 +231,8 @@ def dest_dir stub_json('https://api.github.com:443/repos/jtarchie/test/pulls/1', html_url: 'http://example.com', number: 1, head: { ref: 'foo' }, - base: { ref: 'master' }) + base: { ref: 'master', user: { login: 'jtarchie' } }, + user: { login: 'jtarchie-contributor' }) end def expect_arg(*args) @@ -184,7 +254,7 @@ def dont_expect_arg(*args) it 'disables lfs' do dont_expect_arg /git lfs fetch/ dont_expect_arg /git lfs checkout/ - get('version' => { 'ref' => @ref, 'pr' => '1' }, 'source' => { 'uri' => git_uri, 'repo' => 'jtarchie/test' }, 'params' => {'git' => {'disable_lfs' => true}}) + get('version' => { 'ref' => @ref, 'pr' => '1' }, 'source' => { 'uri' => git_uri, 'repo' => 'jtarchie/test' }, 'params' => { 'git' => { 'disable_lfs' => true } }) end it 'gets all the submodules' do @@ -205,17 +275,18 @@ def dont_expect_arg(*args) it 'get submodules with paths' do expect_arg /git submodule update --init --recursive path1/ expect_arg /git submodule update --init --recursive path2/ - get('version' => { 'ref' => @ref, 'pr' => '1' }, 'source' => { 'uri' => git_uri, 'repo' => 'jtarchie/test' }, 'params' => { 'git' => { 'submodules' => %w(path1 path2) } }) + get('version' => { 'ref' => @ref, 'pr' => '1' }, 'source' => { 'uri' => git_uri, 'repo' => 'jtarchie/test' }, 'params' => { 'git' => { 'submodules' => %w[path1 path2] } }) end it 'checkouts everything by depth' do expect_arg /git submodule update --init --recursive --depth 100 path1/ - expect_arg /git clone --depth 100/ + expect_arg /git clone --depth 100 --branch master/ + expect_arg /git fetch --depth 100/ get('version' => { 'ref' => @ref, 'pr' => '1' }, 'source' => { 'uri' => git_uri, 'repo' => 'jtarchie/test' }, 'params' => { 'git' => { - 'submodules' => %w(path1 path2), + 'submodules' => %w[path1 path2], 'depth' => 100 } }) diff --git a/spec/commands/out_spec.rb b/spec/commands/out_spec.rb index 6584da5..a6524f0 100644 --- a/spec/commands/out_spec.rb +++ b/spec/commands/out_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'json' require 'tmpdir' require 'webmock/rspec' @@ -16,7 +18,7 @@ def commit(msg) end def put(payload) - payload['source']['no_ssl_verify'] = true + payload['source']['skip_ssl_verification'] = true Input.instance(payload: payload) resource_dir = Dir.mktmpdir @@ -37,6 +39,7 @@ def stub_status_post stub_json(:get, "https://api.github.com:443/repos/jtarchie/test/statuses/#{@sha}", []) ENV['BUILD_ID'] = '1234' + ENV['ATC_EXTERNAL_URL'] = 'default-test-atc-url.com' end def stub_json(method, uri, body) @@ -45,6 +48,26 @@ def stub_json(method, uri, body) end context 'when the git repo has no pull request meta information' do + it 'sets the status just on the SHA' do + stub_status_post + + output = put('params' => { 'status' => 'pending', 'path' => 'resource' }, 'source' => { 'repo' => 'jtarchie/test' }) + expect(output).to eq('version' => { 'ref' => @sha }, + 'metadata' => [ + { 'name' => 'status', 'value' => 'pending' } + ]) + end + end + + context 'when the git repo has the pull request meta information' do + before do + git('config --add pullrequest.id 1') + stub_json(:get, 'https://api.github.com:443/repos/jtarchie/test/pulls/1', + html_url: 'http://example.com', + number: 1, + head: { sha: 'abcdef' }) + end + context 'when the merge is set' do it 'retuns an error for unsupported merge types' do stub_status_post @@ -56,13 +79,14 @@ def stub_json(method, uri, body) it 'returns metadata on success' do stub_status_post - stub_request(:put, 'https://api.github.com/repos/jtarchie/test/pulls/merge') + stub_request(:put, 'https://api.github.com/repos/jtarchie/test/pulls/1/merge') .with(body: { merge_method: 'merge', commit_message: '' }.to_json) output = put('params' => { 'status' => 'success', 'merge' => { 'method' => 'merge' }, 'path' => 'resource' }, 'source' => { 'repo' => 'jtarchie/test' }) - expect(output).to eq('version' => { 'ref' => @sha }, + expect(output).to eq('version' => { 'pr' => '1', 'ref' => @sha }, 'metadata' => [ { 'name' => 'status', 'value' => 'success' }, + { 'name' => 'url', 'value' => 'http://example.com' }, { 'name' => 'merge', 'value' => 'merge' }, { 'name' => 'merge_commit_msg', 'value' => '' } ]) @@ -71,7 +95,7 @@ def stub_json(method, uri, body) context 'on merge failure' do it 'raises an error' do stub_status_post - stub_request(:put, 'https://api.github.com/repos/jtarchie/test/pulls/merge') + stub_request(:put, 'https://api.github.com/repos/jtarchie/test/pulls/1/merge') .to_return(status: 405) expect do @@ -85,13 +109,14 @@ def stub_json(method, uri, body) File.write(File.join(dest_dir, 'merge_commit_msg'), 'merge commit message') stub_status_post - stub_request(:put, 'https://api.github.com/repos/jtarchie/test/pulls/merge') + stub_request(:put, 'https://api.github.com/repos/jtarchie/test/pulls/1/merge') .with(body: { merge_method: 'merge', commit_message: 'merge commit message' }.to_json) output = put('params' => { 'status' => 'success', 'merge' => { 'method' => 'merge', 'commit_msg' => 'resource/merge_commit_msg' }, 'path' => 'resource' }, 'source' => { 'repo' => 'jtarchie/test' }) - expect(output).to eq('version' => { 'ref' => @sha }, + expect(output).to eq('version' => { 'pr' => '1', 'ref' => @sha }, 'metadata' => [ { 'name' => 'status', 'value' => 'success' }, + { 'name' => 'url', 'value' => 'http://example.com' }, { 'name' => 'merge', 'value' => 'merge' }, { 'name' => 'merge_commit_msg', 'value' => 'merge commit message' } ]) @@ -99,7 +124,7 @@ def stub_json(method, uri, body) it 'returns an error if the file does not exist' do stub_status_post - stub_request(:put, 'https://api.github.com/repos/jtarchie/test/pulls/merge') + stub_request(:put, 'https://api.github.com/repos/jtarchie/test/pulls/123/merge') .with(body: { merge_method: 'merge', commit_message: 'merge commit message' }.to_json) expect do @@ -109,26 +134,6 @@ def stub_json(method, uri, body) end end - it 'sets the status just on the SHA' do - stub_status_post - - output = put('params' => { 'status' => 'pending', 'path' => 'resource' }, 'source' => { 'repo' => 'jtarchie/test' }) - expect(output).to eq('version' => { 'ref' => @sha }, - 'metadata' => [ - { 'name' => 'status', 'value' => 'pending' } - ]) - end - end - - context 'when the git repo has the pull request meta information' do - before do - git('config --add pullrequest.id 1') - stub_json(:get, 'https://api.github.com:443/repos/jtarchie/test/pulls/1', - html_url: 'http://example.com', - number: 1, - head: { sha: 'abcdef' }) - end - context 'when setting a status with a comment' do before do File.write(File.join(dest_dir, 'comment'), 'comment message') @@ -191,6 +196,26 @@ def stub_json(method, uri, body) end end + context 'when setting a status with a label' do + before do + stub_request(:post, "https://api.github.com/repos/jtarchie/test/issues/1/labels").with( + body: "[\"test_label\"]").to_return( + status: 200, body: "", headers: {}) + end + it 'posts a comment to the PR\'s SHA' do + stub_status_post + stub_json(:post, 'https://api.github.com:443/repos/jtarchie/test/issues/1/comments', id: 1) + + output, = put('params' => { 'status' => 'success', 'path' => 'resource', 'label' => 'test_label' }, 'source' => { 'repo' => 'jtarchie/test' }) + expect(output).to eq('version' => { 'ref' => @sha, 'pr' => '1' }, + 'metadata' => [ + { 'name' => 'status', 'value' => 'success' }, + { 'name' => 'url', 'value' => 'http://example.com' }, + { 'name' => 'label', 'value' => 'test_label' }, + ]) + end + end + context 'when the pull request is being release' do context 'and the build passed' do it 'sets into success mode' do @@ -212,6 +237,16 @@ def stub_json(method, uri, body) end end + context 'with base_url defined on source containing environment variable' do + it 'sets the target_url for status' do + ENV['BUILD_TEAM_NAME'] = 'build-env-var' + stub_status_post.with(body: hash_including('target_url' => 'http://example.com/build-env-var/builds/1234')) + + put('params' => { 'status' => 'success', 'path' => 'resource' }, 'source' => { 'repo' => 'jtarchie/test', 'base_url' => 'http://example.com/$BUILD_TEAM_NAME' }) + ENV['BUILD_TEAM_NAME'] = nil + end + end + context 'with no base_url defined, but with ATC_EXTERNAL_URL defined' do it 'sets the target_url for status' do ENV['ATC_EXTERNAL_URL'] = 'http://atc-endpoint.com' @@ -233,6 +268,18 @@ def stub_json(method, uri, body) put('params' => { 'status' => 'success', 'path' => 'resource', 'context' => 'my-custom-context' }, 'source' => { 'repo' => 'jtarchie/test' }) end + + context 'with build specific environment variables' do + %w[BUILD_ID BUILD_NAME BUILD_JOB_NAME BUILD_PIPELINE_NAME BUILD_TEAM_NAME ATC_EXTERNAL_URL].each do |env_var| + it "evaluates #{env_var} for a context" do + ENV[env_var] = 'build-env-var' + stub_status_post.with(body: hash_including('context' => 'concourse-ci/build-env-var')) + + put('params' => { 'status' => 'success', 'path' => 'resource', 'context' => "$#{env_var}" }, 'source' => { 'repo' => 'jtarchie/test' }) + ENV[env_var] = nil + end + end + end end context 'with setting multiple contextes' do diff --git a/spec/filters/approval_spec.rb b/spec/filters/approval_spec.rb new file mode 100644 index 0000000..ff9caae --- /dev/null +++ b/spec/filters/approval_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require_relative '../../assets/lib/filters/approval' +require_relative '../../assets/lib/pull_request' +require_relative '../../assets/lib/input' +require 'webmock/rspec' + +describe Filters::Approval do + let(:ignore_pr) do + PullRequest.new(pr: { 'number' => 1, 'head' => { 'sha' => 'abc' }, 'author_association' => 'NONE', + 'base' => { 'repo' => { 'full_name' => 'user/repo', 'permissions' => { 'push' => true } } } }) + end + + let(:pr) do + PullRequest.new(pr: { 'number' => 2, 'head' => { 'sha' => 'def' }, 'author_association' => 'OWNER', + 'base' => { 'repo' => { 'full_name' => 'user/repo', 'permissions' => { 'push' => true } } } }) + end + + let(:pull_requests) { [ignore_pr, pr] } + + def stub_json(uri, body) + stub_request(:get, uri) + .to_return(headers: { 'Content-Type' => 'application/json' }, body: body.to_json) + end + + context 'when all approval requirements are disabled' do + it 'does not filter' do + payload = { 'source' => { 'repo' => 'user/repo' } } + filter = described_class.new(pull_requests: pull_requests, input: Input.instance(payload: payload)) + + expect(filter.pull_requests).to eq pull_requests + end + + it 'does not filter when explictly disabled' do + payload = { 'source' => { 'repo' => 'user/repo', 'require_manual_approval' => false, 'require_review_approval' => false, 'authorship_restriction' => false } } + filter = described_class.new(pull_requests: pull_requests, input: Input.instance(payload: payload)) + + expect(filter.pull_requests).to eq pull_requests + end + end + + context 'when owner filtering is enabled' do + it 'only returns PRs that are repo-owners' do + payload = { 'source' => { 'repo' => 'user/repo', 'require_manual_approval' => false, 'require_review_approval' => false, 'authorship_restriction' => true } } + filter = described_class.new(pull_requests: pull_requests, input: Input.instance(payload: payload)) + + expect(filter.pull_requests).to eq [pr] + end + end + + context 'when approval filtering is enabled' do + before do + stub_json(%r{https://api.github.com/repos/user/repo/pulls/1/reviews}, [{ 'state' => 'CHANGES_REQUESTED' }]) + stub_json(%r{https://api.github.com/repos/user/repo/pulls/2/reviews}, [{ 'state' => 'APPROVED' }]) + end + + it 'only returns PRs that are approved' do + payload = { 'source' => { 'repo' => 'user/repo', 'require_manual_approval' => false, 'require_review_approval' => true, 'authorship_restriction' => false } } + filter = described_class.new(pull_requests: pull_requests, input: Input.instance(payload: payload)) + + expect(filter.pull_requests).to eq [pr] + end + end +end diff --git a/spec/filters/ci_skip_spec.rb b/spec/filters/ci_skip_spec.rb new file mode 100644 index 0000000..a511d1b --- /dev/null +++ b/spec/filters/ci_skip_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require_relative '../../assets/lib/filters/ci_skip' +require_relative '../../assets/lib/pull_request' +require_relative '../../assets/lib/input' +require 'webmock/rspec' + +describe Filters::CISkip do + let(:ignore_pr) do + PullRequest.new(pr: { 'number' => 1, 'head' => { 'sha' => 'abc' } }) + end + + let(:pr) do + PullRequest.new(pr: { 'number' => 2, 'head' => { 'sha' => 'def' } }) + end + + let(:pull_requests) { [ignore_pr, pr] } + + def stub_json(uri, body) + stub_request(:get, uri) + .to_return(headers: { 'Content-Type' => 'application/json' }, body: body.to_json) + end + + context 'when ci skip is disabled' do + it 'does not filter' do + payload = { 'source' => { 'repo' => 'user/repo' } } + filter = described_class.new(pull_requests: pull_requests, input: Input.instance(payload: payload)) + + expect(filter.pull_requests).to eq pull_requests + end + + it 'does not filter when explictly disabled' do + payload = { 'source' => { 'repo' => 'user/repo', 'ci_skip' => false } } + filter = described_class.new(pull_requests: pull_requests, input: Input.instance(payload: payload)) + + expect(filter.pull_requests).to eq pull_requests + end + end + + context 'when the ci skip filterings is enabled' do + before do + stub_json(%r{https://api.github.com/repos/user/repo/commits/abc}, 'commit' => { 'message' => '[ci skip]' }) + stub_json(%r{https://api.github.com/repos/user/repo/commits/def}, 'commit' => { 'message' => 'do not skip' }) + end + + it 'only returns PRs with that label' do + payload = { 'source' => { 'repo' => 'user/repo', 'ci_skip' => true } } + filter = described_class.new(pull_requests: pull_requests, input: Input.instance(payload: payload)) + + expect(filter.pull_requests).to eq [pr] + end + end +end diff --git a/spec/filters/label_spec.rb b/spec/filters/label_spec.rb index 65dd0aa..a9fb33b 100644 --- a/spec/filters/label_spec.rb +++ b/spec/filters/label_spec.rb @@ -1,5 +1,8 @@ +# frozen_string_literal: true + require_relative '../../assets/lib/filters/label' require_relative '../../assets/lib/pull_request' +require_relative '../../assets/lib/input' require 'webmock/rspec' describe Filters::Label do diff --git a/spec/filters/mergeable_spec.rb b/spec/filters/mergeable_spec.rb new file mode 100644 index 0000000..ba8857f --- /dev/null +++ b/spec/filters/mergeable_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require_relative '../../assets/lib/filters/mergeable' +require_relative '../../assets/lib/pull_request' +require_relative '../../assets/lib/input' +require 'webmock/rspec' + +describe Filters::Mergeable do + let(:ignore_pr) do + PullRequest.new(pr: { 'number' => 1, 'head' => { 'sha' => 'abc' }, 'mergeable' => false }) + end + + let(:pr) do + PullRequest.new(pr: { 'number' => 2, 'head' => { 'sha' => 'def' }, 'mergeable' => true }) + end + + let(:pull_requests) { [ignore_pr, pr] } + + def stub_json(uri, body) + stub_request(:get, uri) + .to_return(headers: { 'Content-Type' => 'application/json' }, body: body.to_json) + end + + context 'when mergeable requirement is disabled' do + it 'does not filter' do + payload = { 'source' => { 'repo' => 'user/repo' } } + filter = described_class.new(pull_requests: pull_requests, input: Input.instance(payload: payload)) + + expect(filter.pull_requests).to eq pull_requests + end + + it 'does not filter when explictly disabled' do + payload = { 'source' => { 'repo' => 'user/repo', 'only_mergeable' => false } } + filter = described_class.new(pull_requests: pull_requests, input: Input.instance(payload: payload)) + + expect(filter.pull_requests).to eq pull_requests + end + end + + context 'when the mergeable filtering is enabled' do + before do + stub_json(%r{https://api.github.com/repos/user/repo/pulls/1}, 'mergeable' => false) + stub_json(%r{https://api.github.com/repos/user/repo/pulls/2}, 'mergeable' => true) + end + + it 'only returns PRs with that are mergeable' do + payload = { 'source' => { 'repo' => 'user/repo', 'only_mergeable' => true } } + filter = described_class.new(pull_requests: pull_requests, input: Input.instance(payload: payload)) + + expect(filter.pull_requests).to eq [pr] + end + end +end diff --git a/spec/filters/path_spec.rb b/spec/filters/path_spec.rb index 36d393b..f79205a 100644 --- a/spec/filters/path_spec.rb +++ b/spec/filters/path_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative '../../assets/lib/filters/path' require_relative '../../assets/lib/pull_request' require 'webmock/rspec' diff --git a/spec/integration/check_spec.rb b/spec/integration/check_spec.rb index 100e111..109a12b 100644 --- a/spec/integration/check_spec.rb +++ b/spec/integration/check_spec.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + require 'spec_helper' require 'json' describe 'check' do def check(payload) path = ['./assets/check', '/opt/resource/check'].find { |p| File.exist? p } - payload[:source][:no_ssl_verify] = true + payload[:source][:skip_ssl_verification] = true output = `echo '#{JSON.generate(payload)}' | env http_proxy=#{proxy.url} #{path}` JSON.parse(output) diff --git a/spec/integration/in_spec.rb b/spec/integration/in_spec.rb index f627d45..3d9c1e3 100644 --- a/spec/integration/in_spec.rb +++ b/spec/integration/in_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'json' require 'tmpdir' @@ -35,7 +37,7 @@ def commit(msg) context 'for every PR that is checked out' do before do proxy.stub('https://api.github.com:443/repos/jtarchie/test/pulls/1') - .and_return(json: { html_url: 'http://example.com', number: 1, head: { ref: 'foo' }, base: { ref: 'master' } }) + .and_return(json: { html_url: 'http://example.com', number: 1, head: { ref: 'foo' }, base: { ref: 'master', user: { login: 'jtarchie' } }, user: { login: 'jtarchie-contributor' } }) end it 'checks out the pull request to dest_dir' do diff --git a/spec/integration/out_spec.rb b/spec/integration/out_spec.rb index 5fbdcbe..a1b8992 100644 --- a/spec/integration/out_spec.rb +++ b/spec/integration/out_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'fileutils' diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index de1a667..c29f10b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative 'support/proxy' require_relative 'support/cli' diff --git a/spec/support/cli.rb b/spec/support/cli.rb index cda74c7..b71425a 100644 --- a/spec/support/cli.rb +++ b/spec/support/cli.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + require 'open3' module CliIntegration def check(payload) path = ['./assets/check', '/opt/resource/check'].find { |p| File.exist? p } - payload[:source][:no_ssl_verify] = true + payload[:source][:skip_ssl_verification] = true output = `echo '#{JSON.generate(payload)}' | env http_proxy=#{proxy.url} #{path}` JSON.parse(output) @@ -11,13 +13,13 @@ def check(payload) def get(payload = {}) path = ['./assets/in', '/opt/resource/in'].find { |p| File.exist? p } - payload[:source][:no_ssl_verify] = true + payload[:source][:skip_ssl_verification] = true output, error, = Open3.capture3("echo '#{JSON.generate(payload)}' | env http_proxy=#{proxy.url} #{path} #{dest_dir}") response = begin JSON.parse(output) - rescue + rescue StandardError nil end [response, error] @@ -25,7 +27,7 @@ def get(payload = {}) def put(payload = {}) path = ['./assets/out', '/opt/resource/out'].find { |p| File.exist? p } - payload[:source][:no_ssl_verify] = true + payload[:source][:skip_ssl_verification] = true resource_dir = Dir.mktmpdir FileUtils.cp_r(dest_dir, File.join(resource_dir, 'resource')) @@ -34,7 +36,7 @@ def put(payload = {}) response = begin JSON.parse(output) - rescue + rescue StandardError nil end [response, error] diff --git a/spec/support/proxy.rb b/spec/support/proxy.rb index 7b334c4..e5a875c 100644 --- a/spec/support/proxy.rb +++ b/spec/support/proxy.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'billy' Billy.configure do |c| @@ -6,14 +8,3 @@ c.non_successful_error_level = :error c.non_whitelisted_requests_disabled = true end - -module StubCacheHandler - def handle_request(method, url, headers, body) - if response = super - Billy::Cache.instance.store(method.downcase, url, headers, body, response[:headers], response[:status], response[:content]) - response - end - end -end - -Billy::StubHandler.prepend(StubCacheHandler)