diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8f8974f..090a0d9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,12 +17,12 @@ repos: - --honor-noqa - repo: https://github.com/Lucas-C/pre-commit-hooks.git - rev: v1.5.4 + rev: v1.5.5 hooks: - id: remove-tabs - repo: https://github.com/python-jsonschema/check-jsonschema.git - rev: 0.27.3 + rev: 0.28.1 hooks: - id: check-github-actions - id: check-github-workflows @@ -67,7 +67,7 @@ repos: - id: codespell - repo: https://github.com/adrienverge/yamllint.git - rev: v1.33.0 + rev: v1.35.1 hooks: - id: yamllint files: \.(yaml|yml)$ @@ -78,13 +78,13 @@ repos: - --strict - repo: https://github.com/PyCQA/flake8.git - rev: 6.1.0 + rev: 7.0.0 hooks: - id: flake8 - alias: flake8-no-wps - name: flake8 WPS-excluded args: - --ignore + # NOTE: WPS326: Found implicit string concatenation + # NOTE: WPS332: Found walrus operator - >- D100, D101, @@ -92,22 +92,6 @@ repos: D107, E402, E501, - additional_dependencies: - - flake8-2020 ~= 1.7.0 - - flake8-pytest-style ~= 1.6.0 - -- repo: https://github.com/PyCQA/flake8.git - # NOTE: This is kept at v4 for until WPS starts supporting flake v5. - rev: 4.0.1 # enforce-version: 4.0.1 - hooks: - - id: flake8 - alias: flake8-only-wps - name: flake8 WPS-only - args: - - --ignore - # NOTE: WPS326: Found implicit string concatenation - # NOTE: WPS332: Found walrus operator - - >- WPS102, WPS110, WPS111, @@ -124,14 +108,14 @@ repos: WPS440, WPS441, WPS453, - - --select - - WPS additional_dependencies: - - wemake-python-styleguide ~= 0.17.0 + - flake8-2020 ~= 1.7.0 + - flake8-pytest-style ~= 1.6.0 + - wemake-python-styleguide ~= 0.19.0 language_version: python3.11 # flake8-commas doesn't work w/ Python 3.12 - repo: https://github.com/PyCQA/pylint.git - rev: v3.0.3 + rev: v3.1.0 hooks: - id: pylint args: @@ -150,36 +134,4 @@ repos: - --output-format - colorized -- repo: local - hooks: - - id: enforced-flake8-version - name: Verify that enforced flake8 version stays unchanged - description: >- - This is a sanity check and fixer that makes sure that - the `flake8` version in this file remains matching the - corresponding request in the `# enforce-version` comment. - # Using Python here because using - # shell test does not always work in CIs: - entry: >- - python -c 'import pathlib, re, sys; - pre_commit_config = pathlib.Path(sys.argv[1]); - cfg_txt = pre_commit_config.read_text(); - new_cfg_txt = re.sub( - r"(?P\s+)rev:\s(?:\d+\.\d+\.\d+)\s{0,2}" - r"#\senforce-version:\s(?P\d+\.\d+\.\d+)" - r"[ \t\f\v]*", - r"\grev: \g " - r"# enforce-version: \g", - cfg_txt, - ); - cfg_txt != new_cfg_txt and - pre_commit_config.write_text(new_cfg_txt) - ' - pass_filenames: true - language: system - files: >- - ^\.pre-commit-config\.ya?ml$ - types: - - yaml - ... diff --git a/oidc-exchange.py b/oidc-exchange.py index fb53e4f..cea6cfd 100644 --- a/oidc-exchange.py +++ b/oidc-exchange.py @@ -10,7 +10,7 @@ from urllib.parse import urlparse import id # pylint: disable=redefined-builtin import requests -_GITHUB_STEP_SUMMARY = Path(os.getenv("GITHUB_STEP_SUMMARY")) +_GITHUB_STEP_SUMMARY = Path(os.getenv('GITHUB_STEP_SUMMARY')) # The top-level error message that gets rendered. # This message wraps one of the other templates/messages defined below. @@ -45,7 +45,7 @@ permissions: ``` Learn more at https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings. -""" +""" # noqa: S105; not a password # Specialization of the token retrieval failure case, when we know that # the failure cause is use within a third-party PR. @@ -59,7 +59,7 @@ even if `id-token: write` is explicitly configured. To fix this, change your publishing workflow to use an event that forks of your repository cannot trigger (such as tag or release creation, or a manually triggered workflow dispatch). -""" +""" # noqa: S105; not a password # Rendered if the package index refuses the given OIDC token. _SERVER_REFUSED_TOKEN_EXCHANGE_MESSAGE = """ @@ -71,7 +71,7 @@ This generally indicates a trusted publisher configuration error, but could also indicate an internal error on GitHub or PyPI's part. {rendered_claims} -""" +""" # noqa: S105; not a password _RENDERED_CLAIMS = """ The claims rendered below are **for debugging purposes only**. You should **not** @@ -97,7 +97,7 @@ Token request failed: the index produced an unexpected This strongly suggests a server configuration or downtime issue; wait a few minutes and try again. -""" +""" # noqa: S105; not a password # Rendered if the package index's token response isn't a valid API token payload. _SERVER_TOKEN_RESPONSE_MALFORMED_MESSAGE = """ @@ -105,30 +105,30 @@ Token response error: the index gave us an invalid response. This strongly suggests a server configuration or downtime issue; wait a few minutes and try again. -""" +""" # noqa: S105; not a password def die(msg: str) -> NoReturn: - with _GITHUB_STEP_SUMMARY.open("a", encoding="utf-8") as io: + with _GITHUB_STEP_SUMMARY.open('a', encoding='utf-8') as io: print(_ERROR_SUMMARY_MESSAGE.format(message=msg), file=io) # HACK: GitHub Actions' annotations don't work across multiple lines naively; # translating `\n` into `%0A` (i.e., HTML percent-encoding) is known to work. # See: https://github.com/actions/toolkit/issues/193 - msg = msg.replace("\n", "%0A") - print(f"::error::Trusted publishing exchange failure: {msg}", file=sys.stderr) + msg = msg.replace('\n', '%0A') + print(f'::error::Trusted publishing exchange failure: {msg}', file=sys.stderr) sys.exit(1) def debug(msg: str): - print(f"::debug::{msg.title()}", file=sys.stderr) + print(f'::debug::{msg.title()}', file=sys.stderr) def get_normalized_input(name: str) -> str | None: - name = f"INPUT_{name.upper()}" + name = f'INPUT_{name.upper()}' if val := os.getenv(name): return val - return os.getenv(name.replace("-", "_")) + return os.getenv(name.replace('-', '_')) def assert_successful_audience_call(resp: requests.Response, domain: str): @@ -140,13 +140,13 @@ def assert_successful_audience_call(resp: requests.Response, domain: str): # This index supports OIDC, but forbids the client from using # it (either because it's disabled, ratelimited, etc.) die( - f"audience retrieval failed: repository at {domain} has trusted publishing disabled", + f'audience retrieval failed: repository at {domain} has trusted publishing disabled', ) case HTTPStatus.NOT_FOUND: # This index does not support OIDC. die( - "audience retrieval failed: repository at " - f"{domain} does not indicate trusted publishing support", + 'audience retrieval failed: repository at ' + f'{domain} does not indicate trusted publishing support', ) case other: status = HTTPStatus(other) @@ -154,67 +154,67 @@ def assert_successful_audience_call(resp: requests.Response, domain: str): # something we expect. This can happen if the index is broken, in maintenance mode, # misconfigured, etc. die( - "audience retrieval failed: repository at " - f"{domain} responded with unexpected {other}: {status.phrase}", + 'audience retrieval failed: repository at ' + f'{domain} responded with unexpected {other}: {status.phrase}', ) def render_claims(token: str) -> str: - _, payload, _ = token.split(".", 2) + _, payload, _ = token.split('.', 2) # urlsafe_b64decode needs padding; JWT payloads don't contain any. - payload += "=" * (4 - (len(payload) % 4)) + payload += '=' * (4 - (len(payload) % 4)) claims = json.loads(base64.urlsafe_b64decode(payload)) def _get(name: str) -> str: # noqa: WPS430 - return claims.get(name, "MISSING") + return claims.get(name, 'MISSING') return _RENDERED_CLAIMS.format( - sub=_get("sub"), - repository=_get("repository"), - repository_owner=_get("repository_owner"), - repository_owner_id=_get("repository_owner_id"), - job_workflow_ref=_get("job_workflow_ref"), - ref=_get("ref"), + sub=_get('sub'), + repository=_get('repository'), + repository_owner=_get('repository_owner'), + repository_owner_id=_get('repository_owner_id'), + job_workflow_ref=_get('job_workflow_ref'), + ref=_get('ref'), ) def event_is_third_party_pr() -> bool: # Non-`pull_request` events cannot be from third-party PRs. - if os.getenv("GITHUB_EVENT_NAME") != "pull_request": + if os.getenv('GITHUB_EVENT_NAME') != 'pull_request': return False - event_path = os.getenv("GITHUB_EVENT_PATH") + event_path = os.getenv('GITHUB_EVENT_PATH') if not event_path: # No GITHUB_EVENT_PATH indicates a weird GitHub or runner bug. - debug("unexpected: no GITHUB_EVENT_PATH to check") + debug('unexpected: no GITHUB_EVENT_PATH to check') return False try: event = json.loads(Path(event_path).read_bytes()) except json.JSONDecodeError: - debug("unexpected: GITHUB_EVENT_PATH does not contain valid JSON") + debug('unexpected: GITHUB_EVENT_PATH does not contain valid JSON') return False try: - return event["pull_request"]["head"]["repo"]["fork"] + return event['pull_request']['head']['repo']['fork'] except KeyError: return False -repository_url = get_normalized_input("repository-url") +repository_url = get_normalized_input('repository-url') repository_domain = urlparse(repository_url).netloc -token_exchange_url = f"https://{repository_domain}/_/oidc/mint-token" +token_exchange_url = f'https://{repository_domain}/_/oidc/mint-token' # Indices are expected to support `https://{domain}/_/oidc/audience`, # which tells OIDC exchange clients which audience to use. -audience_url = f"https://{repository_domain}/_/oidc/audience" -audience_resp = requests.get(audience_url) +audience_url = f'https://{repository_domain}/_/oidc/audience' +audience_resp = requests.get(audience_url, timeout=5) # S113 wants a timeout assert_successful_audience_call(audience_resp, repository_domain) -oidc_audience = audience_resp.json()["audience"] +oidc_audience = audience_resp.json()['audience'] -debug(f"selected trusted publishing exchange endpoint: {token_exchange_url}") +debug(f'selected trusted publishing exchange endpoint: {token_exchange_url}') try: oidc_token = id.detect_credential(audience=oidc_audience) @@ -229,7 +229,8 @@ except id.IdentityError as identity_error: # Now we can do the actual token exchange. mint_token_resp = requests.post( token_exchange_url, - json={"token": oidc_token}, + json={'token': oidc_token}, + timeout=5, # S113 wants a timeout ) try: @@ -246,9 +247,9 @@ except requests.JSONDecodeError: # On failure, the JSON response includes the list of errors that # occurred during minting. if not mint_token_resp.ok: - reasons = "\n".join( - f"* `{error['code']}`: {error['description']}" - for error in mint_token_payload["errors"] + reasons = '\n'.join( + f'* `{error["code"]}`: {error["description"]}' + for error in mint_token_payload['errors'] ) rendered_claims = render_claims(oidc_token) @@ -260,12 +261,12 @@ if not mint_token_resp.ok: ), ) -pypi_token = mint_token_payload.get("token") +pypi_token = mint_token_payload.get('token') if pypi_token is None: die(_SERVER_TOKEN_RESPONSE_MALFORMED_MESSAGE) # Mask the newly minted PyPI token, so that we don't accidentally leak it in logs. -print(f"::add-mask::{pypi_token}", file=sys.stderr) +print(f'::add-mask::{pypi_token}', file=sys.stderr) # This final print will be captured by the subshell in `twine-upload.sh`. print(pypi_token) diff --git a/print-hash.py b/print-hash.py index 499ef63..a1c4c87 100644 --- a/print-hash.py +++ b/print-hash.py @@ -4,15 +4,15 @@ import sys packages_dir = pathlib.Path(sys.argv[1]).resolve().absolute() -print("Showing hash values of files to be uploaded:") +print('Showing hash values of files to be uploaded:') for file_object in packages_dir.iterdir(): sha256 = hashlib.sha256() - md5 = hashlib.md5() + md5 = hashlib.md5() # noqa: S324; only use for reference blake2_256 = hashlib.blake2b(digest_size=256 // 8) print(file_object) - print("") + print('') content = file_object.read_bytes() @@ -20,7 +20,7 @@ for file_object in packages_dir.iterdir(): md5.update(content) blake2_256.update(content) - print(f"SHA256: {sha256.hexdigest()}") - print(f"MD5: {md5.hexdigest()}") - print(f"BLAKE2-256: {blake2_256.hexdigest()}") - print("") + print(f'SHA256: {sha256.hexdigest()}') + print(f'MD5: {md5.hexdigest()}') + print(f'BLAKE2-256: {blake2_256.hexdigest()}') + print('')