From 0ab8d8258e4cf23f1d47b897d17294cfa30b995e Mon Sep 17 00:00:00 2001 From: jmeridth Date: Wed, 18 Mar 2026 18:50:47 -0500 Subject: [PATCH 1/3] fix: cast gh_app_id to string for JWT encoding compatibility Relates to https://github.com/github-community-projects/evergreen/issues/508 ## What Cast gh_app_id to str() when passing it to login_as_app_installation, which internally calls jwt.encode expecting the iss claim to be a string. Updated tests to pass integer app IDs and assert the string conversion occurs. ## Why Since v2.0.0, GitHub App authentication fails with "TypeError: Issuer (iss) must be a string" because newer versions of PyJWT enforce that the iss claim is a string, but gh_app_id was being passed as an integer. ## Notes - Tests now use assert_called_once_with instead of assert_called_once to verify the exact arguments, preventing this class of regression - Test inputs changed from strings to integers to mirror real-world usage where env vars are parsed as ints Signed-off-by: jmeridth --- auth.py | 2 +- test_auth.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/auth.py b/auth.py index 840d4eb..abd4cf9 100644 --- a/auth.py +++ b/auth.py @@ -32,7 +32,7 @@ def auth_to_github( else: gh = github3.github.GitHub() gh.login_as_app_installation( - gh_app_private_key_bytes, gh_app_id, gh_app_installation_id + gh_app_private_key_bytes, str(gh_app_id), gh_app_installation_id ) github_connection = gh elif ghe and token: diff --git a/test_auth.py b/test_auth.py index 9337abc..dff2963 100644 --- a/test_auth.py +++ b/test_auth.py @@ -56,9 +56,9 @@ def test_auth_to_github_with_ghe_and_ghe_app(self, mock_ghe): mock = mock_ghe.return_value mock.login_as_app_installation = MagicMock(return_value=True) result = auth.auth_to_github( - "", "123", "123", b"123", "https://github.example.com", True + "", 123, 456, b"123", "https://github.example.com", True ) - mock.login_as_app_installation.assert_called_once() + mock.login_as_app_installation.assert_called_once_with(b"123", "123", 456) self.assertEqual(result, mock) @patch("github3.github.GitHub") @@ -69,9 +69,9 @@ def test_auth_to_github_with_app(self, mock_gh): mock = mock_gh.return_value mock.login_as_app_installation = MagicMock(return_value=True) result = auth.auth_to_github( - "", "123", "123", b"123", "https://github.example.com", False + "", 123, 456, b"123", "https://github.example.com", False ) - mock.login_as_app_installation.assert_called_once() + mock.login_as_app_installation.assert_called_once_with(b"123", "123", 456) self.assertEqual(result, mock) @patch("github3.apps.create_jwt_headers", MagicMock(return_value="gh_token")) From e71102193885a16759f76e032de1121f3c8853b9 Mon Sep 17 00:00:00 2001 From: jmeridth Date: Wed, 18 Mar 2026 20:02:57 -0500 Subject: [PATCH 2/3] fix: harden-runner action, bump to 2.16.0 Signed-off-by: jmeridth --- .github/workflows/codeql.yml | 2 +- .github/workflows/contributors_report.yaml | 2 +- .github/workflows/copilot-setup-steps.yml | 2 +- .github/workflows/dependency-review.yml | 2 +- .github/workflows/docker-ci.yml | 2 +- .github/workflows/mark-ready-when-ready.yml | 2 +- .github/workflows/python-ci.yml | 2 +- .github/workflows/scorecard.yml | 2 +- .github/workflows/stale.yaml | 2 +- .github/workflows/super-linter.yaml | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0380a03..0cf1a71 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit diff --git a/.github/workflows/contributors_report.yaml b/.github/workflows/contributors_report.yaml index d80fb8e..32ea4a2 100644 --- a/.github/workflows/contributors_report.yaml +++ b/.github/workflows/contributors_report.yaml @@ -16,7 +16,7 @@ jobs: issues: write steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index fcb8d67..8663a0e 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -26,7 +26,7 @@ jobs: # If you do not check out your code, Copilot will do this for you. steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index c40f824..4f200f0 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit diff --git a/.github/workflows/docker-ci.yml b/.github/workflows/docker-ci.yml index a022d16..313845e 100644 --- a/.github/workflows/docker-ci.yml +++ b/.github/workflows/docker-ci.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit diff --git a/.github/workflows/mark-ready-when-ready.yml b/.github/workflows/mark-ready-when-ready.yml index 5deb181..3bb7cdf 100644 --- a/.github/workflows/mark-ready-when-ready.yml +++ b/.github/workflows/mark-ready-when-ready.yml @@ -25,7 +25,7 @@ jobs: github.event.pull_request.draft == true steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 6ebb592..8a9c6c7 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -25,7 +25,7 @@ jobs: python-version: [3.11, 3.12, 3.13, 3.14] steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index c554a6b..5165b56 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -27,7 +27,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 17c96be..41257d7 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -14,7 +14,7 @@ jobs: pull-requests: read steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit diff --git a/.github/workflows/super-linter.yaml b/.github/workflows/super-linter.yaml index 47d25cd..23cdbe3 100644 --- a/.github/workflows/super-linter.yaml +++ b/.github/workflows/super-linter.yaml @@ -23,7 +23,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit From 6aafe59aa1d1c7f07c70ceddd7a4a37ec9765ac1 Mon Sep 17 00:00:00 2001 From: jmeridth Date: Thu, 19 Mar 2026 07:24:23 -0500 Subject: [PATCH 3/3] fix: cast gh_app_id to str in get_github_app_installation_token ## What Extended the str(gh_app_id) cast to get_github_app_installation_token, which passes gh_app_id directly to github3.apps.create_jwt_headers. Updated type hints to int | None to match what get_int_env_var returns. ## Why The existing fix only covered auth_to_github but missed the second code path through get_github_app_installation_token, which would still hit PyJWT's "Issuer (iss) must be a string" TypeError at runtime. ## Notes - The str() cast happens inside get_github_app_installation_token rather than at the call site in evergreen.py, keeping the function defensive regardless of caller. Signed-off-by: jmeridth --- auth.py | 12 +++++++----- test_auth.py | 26 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/auth.py b/auth.py index abd4cf9..b4584dc 100644 --- a/auth.py +++ b/auth.py @@ -51,9 +51,9 @@ def auth_to_github( def get_github_app_installation_token( ghe: str, - gh_app_id: str, + gh_app_id: int | None, gh_app_private_key_bytes: bytes, - gh_app_installation_id: str, + gh_app_installation_id: int | None, ) -> str | None: """ Get a GitHub App Installation token. @@ -61,14 +61,16 @@ def get_github_app_installation_token( Args: ghe (str): the GitHub Enterprise endpoint - gh_app_id (str): the GitHub App ID + gh_app_id (int | None): the GitHub App ID gh_app_private_key_bytes (bytes): the GitHub App Private Key - gh_app_installation_id (str): the GitHub App Installation ID + gh_app_installation_id (int | None): the GitHub App Installation ID Returns: str: the GitHub App token """ - jwt_headers = github3.apps.create_jwt_headers(gh_app_private_key_bytes, gh_app_id) + jwt_headers = github3.apps.create_jwt_headers( + gh_app_private_key_bytes, str(gh_app_id) + ) api_endpoint = f"{ghe}/api/v3" if ghe else "https://api.github.com" url = f"{api_endpoint}/app/installations/{gh_app_installation_id}/access_tokens" diff --git a/test_auth.py b/test_auth.py index dff2963..967fadc 100644 --- a/test_auth.py +++ b/test_auth.py @@ -92,6 +92,32 @@ def test_get_github_app_installation_token(self, mock_post): self.assertEqual(result, dummy_token) + @patch( + "github3.apps.create_jwt_headers", + return_value={"Authorization": "Bearer gh_token"}, + ) + @patch("requests.post") + def test_get_github_app_installation_token_casts_int_app_id_to_str( + self, mock_post, mock_create_jwt + ): + """ + Test that get_github_app_installation_token casts an int gh_app_id to str + before passing it to create_jwt_headers (PyJWT requires iss to be a string). + """ + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = {"token": "dummytoken"} + mock_post.return_value = mock_response + + auth.get_github_app_installation_token( + ghe="", + gh_app_id=12345, + gh_app_private_key_bytes=b"private_key", + gh_app_installation_id=678910, + ) + + mock_create_jwt.assert_called_once_with(b"private_key", "12345") + @patch("github3.apps.create_jwt_headers", MagicMock(return_value="gh_token")) @patch("auth.requests.post") def test_get_github_app_installation_token_request_failure(self, mock_post):