diff --git a/coveralls/api.py b/coveralls/api.py index da276851..265178ab 100644 --- a/coveralls/api.py +++ b/coveralls/api.py @@ -49,16 +49,12 @@ def __init__(self, token_required=True, service_name=None, **kwargs): self.load_config_from_environment() - name, job, pr = self.load_config_from_ci_environment() + name, job, number, pr = self.load_config_from_ci_environment() self.config['service_name'] = self.config.get('service_name', name) if job: - # N.B. Github Actions uses a different chunk of the Coveralls - # config when running parallel builds, ie. `service_number` instead - # of `service_job_id`. - if name.startswith('github'): - self.config['service_number'] = job - else: - self.config['service_job_id'] = job + self.config['service_job_id'] = job + if number: + self.config['service_number'] = number if pr: self.config['service_pull_request'] = pr @@ -76,67 +72,78 @@ def ensure_token(self): @staticmethod def load_config_from_appveyor(): pr = os.environ.get('APPVEYOR_PULL_REQUEST_NUMBER') - return 'appveyor', os.environ.get('APPVEYOR_BUILD_ID'), pr + return 'appveyor', os.environ.get('APPVEYOR_BUILD_ID'), None, pr @staticmethod def load_config_from_buildkite(): pr = os.environ.get('BUILDKITE_PULL_REQUEST') if pr == 'false': pr = None - return 'buildkite', os.environ.get('BUILDKITE_JOB_ID'), pr + return 'buildkite', os.environ.get('BUILDKITE_JOB_ID'), None, pr @staticmethod def load_config_from_circle(): pr = os.environ.get('CI_PULL_REQUEST', '').split('/')[-1] or None - return 'circle-ci', os.environ.get('CIRCLE_BUILD_NUM'), pr + return 'circle-ci', os.environ.get('CIRCLE_BUILD_NUM'), None, pr - @staticmethod - def load_config_from_github(): - service_number = os.environ.get('GITHUB_SHA') + def load_config_from_github(self): + service = 'github' + if self.config.get('repo_token'): + service = 'github-actions' + else: + gh_token = os.environ.get('GITHUB_TOKEN') + if not gh_token: + raise CoverallsException( + 'Running on Github Actions but GITHUB_TOKEN is not set. ' + 'Add "env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}" to ' + 'your step config.') + self.config['repo_token'] = gh_token + + number = os.environ.get('GITHUB_RUN_ID') pr = None if os.environ.get('GITHUB_REF', '').startswith('refs/pull/'): pr = os.environ.get('GITHUB_REF', '//').split('/')[2] - service_number += '-PR-{}'.format(pr) - return 'github-actions', service_number, pr + return service, None, number, pr @staticmethod def load_config_from_jenkins(): pr = os.environ.get('CI_PULL_REQUEST', '').split('/')[-1] or None - return 'jenkins', os.environ.get('BUILD_NUMBER'), pr + return 'jenkins', os.environ.get('BUILD_NUMBER'), None, pr @staticmethod def load_config_from_travis(): pr = os.environ.get('TRAVIS_PULL_REQUEST') - return 'travis-ci', os.environ.get('TRAVIS_JOB_ID'), pr + return 'travis-ci', os.environ.get('TRAVIS_JOB_ID'), None, pr @staticmethod def load_config_from_semaphore(): + job = os.environ.get('SEMAPHORE_BUILD_NUMBER') pr = os.environ.get('PULL_REQUEST_NUMBER') - return 'semaphore-ci', os.environ.get('SEMAPHORE_BUILD_NUMBER'), pr + return 'semaphore-ci', job, None, pr @staticmethod def load_config_from_unknown(): - return 'coveralls-python', None, None + return 'coveralls-python', None, None, None def load_config_from_ci_environment(self): if os.environ.get('APPVEYOR'): - name, job, pr = self.load_config_from_appveyor() + name, job, number, pr = self.load_config_from_appveyor() elif os.environ.get('BUILDKITE'): - name, job, pr = self.load_config_from_buildkite() + name, job, number, pr = self.load_config_from_buildkite() elif os.environ.get('CIRCLECI'): - name, job, pr = self.load_config_from_circle() + name, job, number, pr = self.load_config_from_circle() elif os.environ.get('GITHUB_ACTIONS'): - name, job, pr = self.load_config_from_github() + name, job, number, pr = self.load_config_from_github() elif os.environ.get('JENKINS_HOME'): - name, job, pr = self.load_config_from_jenkins() + name, job, number, pr = self.load_config_from_jenkins() elif os.environ.get('TRAVIS'): self._token_required = False - name, job, pr = self.load_config_from_travis() + name, job, number, pr = self.load_config_from_travis() elif os.environ.get('SEMAPHORE'): - name, job, pr = self.load_config_from_semaphore() + name, job, number, pr = self.load_config_from_semaphore() else: - name, job, pr = self.load_config_from_unknown() - return (name, job, pr) + name, job, number, pr = self.load_config_from_unknown() + return (name, job, number, pr) def load_config_from_environment(self): coveralls_host = os.environ.get('COVERALLS_HOST') @@ -159,6 +166,10 @@ def load_config_from_environment(self): if flag_name: self.config['flag_name'] = flag_name + number = os.environ.get('COVERALLS_SERVICE_JOB_NUMBER') + if number: + self.config['service_number'] = number + def load_config_from_file(self): try: with open(os.path.join(os.getcwd(), diff --git a/docs/usage/configuration.rst b/docs/usage/configuration.rst index bb35eb1c..836d1541 100644 --- a/docs/usage/configuration.rst +++ b/docs/usage/configuration.rst @@ -53,13 +53,41 @@ Sample ``.coveralls.yml`` file:: Github Actions Gotcha --------------------- -There's something weird with using Github Actions that we've not yet been able to entirely sort out -- if you find you're getting a 422 error on Github Actions which looks like this:: - - Could not submit coverage: 422 Client Error: Unprocessable Entity for url: https://coveralls.io/api/v1/jobs - -Then you may be able to solve it by ensuring your ``secret`` is named ``COVERALLS_REPO_TOKEN``; it seems like Github Actions may do Magic(tm) to some environment variables based on their name. The following config block seems to work properly:: +Coveralls natively supports jobs running on Github Actions. You can directly pass the default-provided secret GITHUB_TOKEN:: env: - COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | coveralls + +For parallel builds you have to specify a unique flag-name for every step/job. You can use the official coveralls action to finalize the build:: + + jobs: + test: + strategy: + matrix: + test-name: + - test1 + - test2 + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Test + run: | + ./run_tests.sh ${{ matrix.test-name }} + coveralls + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COVERALLS_PARALLEL: true + COVERALLS_FLAG_NAME: test-${{ matrix.test-env }} + coveralls: + name: Coveralls + needs: test + runs-on: ubuntu-latest + steps: + - name: Finished + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + parallel-finished: true diff --git a/docs/usage/tox.rst b/docs/usage/tox.rst index 82aed24c..0f96c073 100644 --- a/docs/usage/tox.rst +++ b/docs/usage/tox.rst @@ -68,6 +68,9 @@ All variables: - ``GITHUB_REF`` - ``GITHUB_SHA`` - ``GITHUB_HEAD_REF`` +- ``GITHUB_RUN_ID`` +- ``GITHUB_RUN_NUMBER`` +- ``GITHUB_TOKEN`` Jenkins ------- diff --git a/tests/api/configuration_test.py b/tests/api/configuration_test.py index c70c0ee1..6580bb8a 100644 --- a/tests/api/configuration_test.py +++ b/tests/api/configuration_test.py @@ -66,6 +66,14 @@ def test_misconfigured(self): 'Not on TravisCI. You have to provide either repo_token in ' '.coveralls.mock or set the COVERALLS_REPO_TOKEN env var.') + @mock.patch.dict(os.environ, {'GITHUB_ACTIONS': 'true'}, clear=True) + def test_misconfigured_github(self): + with pytest.raises(Exception) as excinfo: + Coveralls() + + assert str(excinfo.value).startswith( + 'Running on Github Actions but GITHUB_TOKEN is not set.') + @mock.patch.dict(os.environ, {'APPVEYOR': 'True', 'APPVEYOR_BUILD_ID': '1234567', 'APPVEYOR_PULL_REQUEST_NUMBER': '1234'}, @@ -113,26 +121,34 @@ def test_circleci_no_config(self): {'GITHUB_ACTIONS': 'true', 'GITHUB_REF': 'refs/pull/1234/merge', 'GITHUB_SHA': 'bb0e00166b28f49db04d6a8b8cb4bddb5afa529f', - 'GITHUB_HEAD_REF': 'fixup-branch'}, + 'GITHUB_RUN_ID': '123456789', + 'GITHUB_RUN_NUMBER': '12', + 'GITHUB_HEAD_REF': 'fixup-branch', + 'COVERALLS_REPO_TOKEN': 'xxx'}, clear=True) def test_github_no_config(self): - cover = Coveralls(repo_token='xxx') + cover = Coveralls() assert cover.config['service_name'] == 'github-actions' assert cover.config['service_pull_request'] == '1234' - assert 'service_job_id' not in cover.config + assert cover.config['service_job_id'] == '12' + assert cover.config['service_number'] == '123456789' @mock.patch.dict( os.environ, {'GITHUB_ACTIONS': 'true', + 'GITHUB_TOKEN': 'xxx', 'GITHUB_REF': 'refs/heads/master', 'GITHUB_SHA': 'bb0e00166b28f49db04d6a8b8cb4bddb5afa529f', + 'GITHUB_RUN_ID': '987654321', + 'GITHUB_RUN_NUMBER': '21', 'GITHUB_HEAD_REF': ''}, clear=True) def test_github_no_config_no_pr(self): - cover = Coveralls(repo_token='xxx') - assert cover.config['service_name'] == 'github-actions' + cover = Coveralls() + assert cover.config['service_name'] == 'github' + assert cover.config['service_job_id'] == '21' + assert cover.config['service_number'] == '987654321' assert 'service_pull_request' not in cover.config - assert 'service_job_id' not in cover.config @mock.patch.dict( os.environ,