Skip to content

Commit 66caaa7

Browse files
committed
feat(config): reorder configuration precedence
We've been getting a slew of cases where users are reporting requiring different configurations -- especially Github Actions, where we seem to need to set a different service name on a case-by-case basis. Reordering our configuration precedence such that user-specified values overwrite the CI defaults should allow users to fix these problems without requiring a one-size-fits-all code change. This is massively backwards incompatible for anyone with the same key set to two different values in their configs.
1 parent db523ff commit 66caaa7

File tree

3 files changed

+71
-42
lines changed

3 files changed

+71
-42
lines changed

coveralls/api.py

Lines changed: 51 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919

2020
class Coveralls:
21+
# pylint: disable=too-many-public-methods
2122
config_filename = '.coveralls.yml'
2223

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

44-
self.config = self.load_config_from_file()
45-
self.config.update(kwargs)
46-
if service_name:
47-
self.config['service_name'] = service_name
48-
if self.config.get('coveralls_host'):
49-
self._coveralls_host = self.config['coveralls_host']
50-
del self.config['coveralls_host']
51-
52-
self.load_config_from_environment()
53-
54-
name, job, number, pr = self.load_config_from_ci_environment()
55-
self.config['service_name'] = self.config.get('service_name', name)
56-
if job or os.environ.get('GITHUB_ACTIONS'):
57-
# N.B. Github Actions fails if this is not set even when null.
58-
# Other services fail if this is set to null. Sigh.
59-
self.config['service_job_id'] = job
60-
if number:
61-
self.config['service_number'] = number
62-
if pr:
63-
self.config['service_pull_request'] = pr
64-
46+
self.load_config(kwargs, service_name)
6547
self.ensure_token()
6648

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

53+
if os.environ.get('GITHUB_ACTIONS'):
54+
raise CoverallsException(
55+
'Running on Github Actions but GITHUB_TOKEN is not set. '
56+
'Add "env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}" to '
57+
'your step config.')
58+
7159
raise CoverallsException(
7260
'Not on TravisCI. You have to provide either repo_token in {} or '
7361
'set the COVERALLS_REPO_TOKEN env var.'.format(
7462
self.config_filename))
7563

64+
def load_config(self, kwargs, service_name):
65+
"""
66+
Loads all coveralls configuration in the following precedence order.
67+
68+
1. automatic CI configuration
69+
2. COVERALLS_* env vars
70+
3. .coveralls.yml config file
71+
4. CLI flags
72+
"""
73+
self.load_config_from_ci_environment()
74+
self.load_config_from_environment()
75+
self.load_config_from_file()
76+
self.config.update(kwargs)
77+
if self.config.get('coveralls_host'):
78+
# N.B. users can set --coveralls-host via CLI, but we don't keep
79+
# that in the config
80+
self._coveralls_host = self.config.pop('coveralls_host')
81+
if service_name:
82+
self.config['service_name'] = service_name
83+
7684
@staticmethod
7785
def load_config_from_appveyor():
7886
pr = os.environ.get('APPVEYOR_PULL_REQUEST_NUMBER')
@@ -92,23 +100,19 @@ def load_config_from_circle():
92100
return 'circle-ci', os.environ.get('CIRCLE_BUILD_NUM'), number, pr
93101

94102
def load_config_from_github(self):
95-
service = 'github'
96-
if self.config.get('repo_token'):
97-
service = 'github-actions'
98-
else:
99-
gh_token = os.environ.get('GITHUB_TOKEN')
100-
if not gh_token:
101-
raise CoverallsException(
102-
'Running on Github Actions but GITHUB_TOKEN is not set. '
103-
'Add "env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}" to '
104-
'your step config.')
105-
self.config['repo_token'] = gh_token
106-
107-
number = os.environ.get('GITHUB_RUN_ID')
103+
# Github tokens and standard Coveralls tokens are almost but not quite
104+
# the same -- forceibly using Github's flow seems to be more stable
105+
self.config['repo_token'] = os.environ.get('GITHUB_TOKEN')
106+
108107
pr = None
109108
if os.environ.get('GITHUB_REF', '').startswith('refs/pull/'):
110109
pr = os.environ.get('GITHUB_REF', '//').split('/')[2]
111-
return service, None, number, pr
110+
111+
# N.B. some users require this to be 'github' and some require it to
112+
# be 'github-actions'. Defaulting to 'github-actions' as it seems more
113+
# common -- users can specify the service name manually to override
114+
# this.
115+
return 'github-actions', None, os.environ.get('GITHUB_RUN_ID'), pr
112116

113117
@staticmethod
114118
def load_config_from_jenkins():
@@ -148,6 +152,9 @@ def load_config_from_ci_environment(self):
148152
elif os.environ.get('CIRCLECI'):
149153
name, job, number, pr = self.load_config_from_circle()
150154
elif os.environ.get('GITHUB_ACTIONS'):
155+
# N.B. Github Actions fails if this is not set even when null.
156+
# Other services fail if this is set to null. Sigh.
157+
self.config['service_job_id'] = None
151158
name, job, number, pr = self.load_config_from_github()
152159
elif os.environ.get('JENKINS_HOME'):
153160
name, job, number, pr = self.load_config_from_jenkins()
@@ -158,7 +165,14 @@ def load_config_from_ci_environment(self):
158165
name, job, number, pr = self.load_config_from_semaphore()
159166
else:
160167
name, job, number, pr = self.load_config_from_unknown()
161-
return (name, job, number, pr)
168+
169+
self.config['service_name'] = name
170+
if job:
171+
self.config['service_job_id'] = job
172+
if number:
173+
self.config['service_number'] = number
174+
if pr:
175+
self.config['service_pull_request'] = pr
162176

163177
def load_config_from_environment(self):
164178
coveralls_host = os.environ.get('COVERALLS_HOST')
@@ -191,16 +205,14 @@ def load_config_from_file(self):
191205
self.config_filename)) as config:
192206
try:
193207
import yaml # pylint: disable=import-outside-toplevel
194-
return yaml.safe_load(config)
208+
self.config.update(yaml.safe_load(config))
195209
except ImportError:
196210
log.warning('PyYAML is not installed, skipping %s.',
197211
self.config_filename)
198212
except OSError:
199213
log.debug('Missing %s file. Using only env variables.',
200214
self.config_filename)
201215

202-
return {}
203-
204216
def merge(self, path):
205217
reader = codecs.getreader('utf-8')
206218
with open(path, 'rb') as fh:

docs/usage/configuration.rst

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,22 @@
33
Configuration
44
=============
55

6-
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.
6+
coveralls-python often works without any outside configuration by examining the
7+
environment it is being run in. Special handling has been added for AppVeyor,
8+
BuildKite, CircleCI, Github Actions, Jenkins, and TravisCI to make
9+
coveralls-python as close to "plug and play" as possible.
710

8-
Most often, you will simply need to run coveralls-python with no additional options after you have run your coverage suite::
11+
In cases where you do need to modify the configuration, we obey a very strict
12+
precedence order where the **latest value is used**:
13+
14+
* first, the CI environment will be loaded
15+
* second, any environment variables will be loaded (eg. those which begin with
16+
``COVERALLS_``
17+
* third, the config file is loaded (eg. ``./..coveralls.yml``)
18+
* finally, any command line flags are evaluated
19+
20+
Most often, you will simply need to run coveralls-python with no additional
21+
options after you have run your coverage suite::
922

1023
coveralls
1124

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

84+
Sometimes Github Actions gets a little picky about the service name which needs to
85+
be used in various cases. If you run into issues, try setting the ``COVERALLS_SERVICE_NAME``
86+
explicitly to either ``github`` or ``github-actions``.
87+
7188
For parallel builds, you have to add a final step to let coveralls.io know the
7289
parallel build is finished::
7390

tests/api/configuration_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ def test_github_no_config(self):
145145
clear=True)
146146
def test_github_no_config_no_pr(self):
147147
cover = Coveralls()
148-
assert cover.config['service_name'] == 'github'
148+
assert cover.config['service_name'] == 'github-actions'
149149
assert cover.config['service_number'] == '987654321'
150150
assert 'service_job_id' in cover.config
151151
assert 'service_pull_request' not in cover.config

0 commit comments

Comments
 (0)