Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(config): reorder configuration precedence #249

Merged
merged 1 commit into from
Jan 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 51 additions & 39 deletions coveralls/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@


class Coveralls:
# pylint: disable=too-many-public-methods
config_filename = '.coveralls.yml'

def __init__(self, token_required=True, service_name=None, **kwargs):
Expand All @@ -40,39 +41,46 @@ def __init__(self, token_required=True, service_name=None, **kwargs):
self._data = None
self._coveralls_host = 'https://coveralls.io/'
self._token_required = token_required
self.config = {}

self.config = self.load_config_from_file()
self.config.update(kwargs)
if service_name:
self.config['service_name'] = service_name
if self.config.get('coveralls_host'):
self._coveralls_host = self.config['coveralls_host']
del self.config['coveralls_host']

self.load_config_from_environment()

name, job, number, pr = self.load_config_from_ci_environment()
self.config['service_name'] = self.config.get('service_name', name)
if job or os.environ.get('GITHUB_ACTIONS'):
# N.B. Github Actions fails if this is not set even when null.
# Other services fail if this is set to null. Sigh.
self.config['service_job_id'] = job
if number:
self.config['service_number'] = number
if pr:
self.config['service_pull_request'] = pr

self.load_config(kwargs, service_name)
self.ensure_token()

def ensure_token(self):
if self.config.get('repo_token') or not self._token_required:
return

if os.environ.get('GITHUB_ACTIONS'):
raise CoverallsException(
'Running on Github Actions but GITHUB_TOKEN is not set. '
'Add "env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}" to '
'your step config.')

raise CoverallsException(
'Not on TravisCI. You have to provide either repo_token in {} or '
'set the COVERALLS_REPO_TOKEN env var.'.format(
self.config_filename))

def load_config(self, kwargs, service_name):
"""
Loads all coveralls configuration in the following precedence order.

1. automatic CI configuration
2. COVERALLS_* env vars
3. .coveralls.yml config file
4. CLI flags
"""
self.load_config_from_ci_environment()
self.load_config_from_environment()
self.load_config_from_file()
self.config.update(kwargs)
if self.config.get('coveralls_host'):
# N.B. users can set --coveralls-host via CLI, but we don't keep
# that in the config
self._coveralls_host = self.config.pop('coveralls_host')
if service_name:
self.config['service_name'] = service_name

@staticmethod
def load_config_from_appveyor():
pr = os.environ.get('APPVEYOR_PULL_REQUEST_NUMBER')
Expand All @@ -92,23 +100,19 @@ def load_config_from_circle():
return 'circle-ci', os.environ.get('CIRCLE_BUILD_NUM'), number, pr

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')
# Github tokens and standard Coveralls tokens are almost but not quite
# the same -- forceibly using Github's flow seems to be more stable
self.config['repo_token'] = os.environ.get('GITHUB_TOKEN')

pr = None
if os.environ.get('GITHUB_REF', '').startswith('refs/pull/'):
pr = os.environ.get('GITHUB_REF', '//').split('/')[2]
return service, None, number, pr

# N.B. some users require this to be 'github' and some require it to
# be 'github-actions'. Defaulting to 'github-actions' as it seems more
# common -- users can specify the service name manually to override
# this.
return 'github-actions', None, os.environ.get('GITHUB_RUN_ID'), pr

@staticmethod
def load_config_from_jenkins():
Expand Down Expand Up @@ -148,6 +152,9 @@ def load_config_from_ci_environment(self):
elif os.environ.get('CIRCLECI'):
name, job, number, pr = self.load_config_from_circle()
elif os.environ.get('GITHUB_ACTIONS'):
# N.B. Github Actions fails if this is not set even when null.
# Other services fail if this is set to null. Sigh.
self.config['service_job_id'] = None
name, job, number, pr = self.load_config_from_github()
elif os.environ.get('JENKINS_HOME'):
name, job, number, pr = self.load_config_from_jenkins()
Expand All @@ -158,7 +165,14 @@ def load_config_from_ci_environment(self):
name, job, number, pr = self.load_config_from_semaphore()
else:
name, job, number, pr = self.load_config_from_unknown()
return (name, job, number, pr)

self.config['service_name'] = name
if job:
self.config['service_job_id'] = job
if number:
self.config['service_number'] = number
if pr:
self.config['service_pull_request'] = pr

def load_config_from_environment(self):
coveralls_host = os.environ.get('COVERALLS_HOST')
Expand Down Expand Up @@ -191,16 +205,14 @@ def load_config_from_file(self):
self.config_filename)) as config:
try:
import yaml # pylint: disable=import-outside-toplevel
return yaml.safe_load(config)
self.config.update(yaml.safe_load(config))
except ImportError:
log.warning('PyYAML is not installed, skipping %s.',
self.config_filename)
except OSError:
log.debug('Missing %s file. Using only env variables.',
self.config_filename)

return {}

def merge(self, path):
reader = codecs.getreader('utf-8')
with open(path, 'rb') as fh:
Expand Down
21 changes: 19 additions & 2 deletions docs/usage/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,22 @@
Configuration
=============

coveralls-python often works without any outside configuration by examining the environment it is being run in. Special handling has been added for AppVeyor, BuildKite, CircleCI, Github Actions, Jenkins, and TravisCI to make coveralls-python as close to "plug and play" as possible.
coveralls-python often works without any outside configuration by examining the
environment it is being run in. Special handling has been added for AppVeyor,
BuildKite, CircleCI, Github Actions, Jenkins, and TravisCI to make
coveralls-python as close to "plug and play" as possible.

Most often, you will simply need to run coveralls-python with no additional options after you have run your coverage suite::
In cases where you do need to modify the configuration, we obey a very strict
precedence order where the **latest value is used**:

* first, the CI environment will be loaded
* second, any environment variables will be loaded (eg. those which begin with
``COVERALLS_``
* third, the config file is loaded (eg. ``./..coveralls.yml``)
* finally, any command line flags are evaluated

Most often, you will simply need to run coveralls-python with no additional
options after you have run your coverage suite::

coveralls

Expand Down Expand Up @@ -68,6 +81,10 @@ Passing a coveralls.io token via the ``COVERALLS_REPO_TOKEN`` environment variab
(or via the ``repo_token`` parameter in the config file) is not needed for
Github Actions.

Sometimes Github Actions gets a little picky about the service name which needs to
be used in various cases. If you run into issues, try setting the ``COVERALLS_SERVICE_NAME``
explicitly to either ``github`` or ``github-actions``.

For parallel builds, you have to add a final step to let coveralls.io know the
parallel build is finished::

Expand Down
2 changes: 1 addition & 1 deletion tests/api/configuration_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def test_github_no_config(self):
clear=True)
def test_github_no_config_no_pr(self):
cover = Coveralls()
assert cover.config['service_name'] == 'github'
assert cover.config['service_name'] == 'github-actions'
assert cover.config['service_number'] == '987654321'
assert 'service_job_id' in cover.config
assert 'service_pull_request' not in cover.config
Expand Down