├── .envrc
├── .flaskenv
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .vscode
├── extensions.json
└── settings.json
├── CODEOWNERS
├── Dockerfile
├── GithubAppPostInstall.md
├── LICENSE
├── README.md
├── cli.py
├── pyproject.toml
├── requirements-dev.txt
├── requirements.frozen.txt
├── requirements.txt
├── setup.md
├── src
├── __init__.py
├── github_app.py
├── github_sdk.py
├── main.py
├── sentry_config.py
└── web_app_handler.py
└── tests
├── __init__.py
├── conftest.py
├── fixtures
├── jobA
│ ├── job.json
│ ├── runs.json
│ ├── trace.json
│ └── workflow.json
├── skipped_workflow.json
└── webhook_event.json
├── test_github_sdk.py
├── test_sentry_config_file.py
└── test_web_app_handler.py
/.envrc:
--------------------------------------------------------------------------------
1 | # shellcheck shell=bash
2 | missing() {
3 | ! command -v "$1" >/dev/null 2>&1
4 | }
5 |
6 | set -e
7 |
8 | if missing pre-commit; then
9 | echo >&2 "You're missing pre-commit. Install it with brew install pre-commit"
10 | return 1
11 | fi
12 | # If installed, it executes very fast
13 | pre-commit install >/dev/null
14 | # Convenient if you want secrets to load (e.g. personal token, Github webhook secret) or override values from env.development
15 | dotenv_if_exists .env
16 |
17 | if [ ! -d .venv ]; then
18 | python3 -m venv .venv
19 | .venv/bin/pip install -U pip
20 | .venv/bin/pip install wheel
21 | .venv/bin/pip install -r requirements.txt -r requirements-dev.txt
22 | fi
23 |
24 | source .venv/bin/activate
25 |
26 | # https://github.com/direnv/direnv/wiki/PS1
27 | unset PS1
28 |
--------------------------------------------------------------------------------
/.flaskenv:
--------------------------------------------------------------------------------
1 | FLASK_APP="src.main"
2 | FLASK_ENV="development"
3 | FLASK_HOST="0.0.0.0"
4 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | paths-ignore:
6 | - "**.md"
7 | push:
8 | branches:
9 | - "main"
10 | paths-ignore:
11 | - "**.md"
12 |
13 | jobs:
14 | # In reality, this will not be completely accurate as only when a Docker image is deployed to GCP
15 | # we will truly have a deployed release. Nevertheless, this will be useful once it is deployed
16 | # since it will help add commits to releases
17 | release:
18 | if: ${{ github.ref == 'refs/heads/main' }}
19 | name: Create a Sentry release
20 | runs-on: ubuntu-latest
21 | timeout-minutes: 10
22 | steps:
23 | - name: Checkout
24 | uses: actions/checkout@v3
25 | with:
26 | # This is necessary for the a release to include commits
27 | fetch-depth: 0
28 |
29 | - name: Create Sentry release
30 | uses: getsentry/action-release@v1
31 | env:
32 | SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
33 | SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
34 | with:
35 | projects: "sentry-github-actions-app"
36 | environment: production
37 | # We have created releases from the PR, thus, the CLI gets confused finding commits
38 | ignore_missing: true
39 |
40 | docker-build:
41 | name: Docker build
42 | runs-on: ubuntu-latest
43 | timeout-minutes: 10
44 | steps:
45 | - name: Checkout
46 | uses: actions/checkout@v3
47 |
48 | - name: Build
49 | run: |
50 | docker build -t docker-app .
51 |
52 | - name: Test starting
53 | run: |
54 | docker run -d --rm docker-app
55 | docker stop $(docker ps -aq)
56 |
57 | tests:
58 | name: Unit tests
59 | runs-on: ubuntu-latest
60 | timeout-minutes: 10
61 | steps:
62 | - name: Checkout
63 | uses: actions/checkout@v3
64 |
65 | - uses: actions/setup-python@v4
66 | with:
67 | python-version: "3.10"
68 |
69 | - name: Set up
70 | run: |
71 | pip install tox
72 |
73 | - name: Run tests
74 | run: |
75 | tox
76 |
77 | - uses: codecov/codecov-action@v2
78 | with:
79 | verbose: true # optional (default = false)
80 |
81 | pre-commit-checks:
82 | name: Pre-commit checks
83 | runs-on: ubuntu-latest
84 | timeout-minutes: 10
85 | steps:
86 | - name: Checkout
87 | uses: actions/checkout@v3
88 |
89 | - uses: actions/setup-python@v4
90 | with:
91 | python-version: "3.10"
92 |
93 | - name: Set up
94 | run: |
95 | pip install wheel
96 | pip install pre-commit
97 |
98 | - name: Install hooks
99 | run: |
100 | pre-commit install
101 |
102 | - name: Run hooks
103 | run: |
104 | pre-commit run --all-files
105 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
131 | # Ignore generated credentials from google-github-actions/auth
132 | gha-creds-*.json
133 |
134 | # Ignore PEM
135 | *.pem
136 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/psf/black
3 | rev: 22.6.0 # Replace by any tag/version: https://github.com/psf/black/tags
4 | hooks:
5 | - id: black
6 | language_version: python3
7 | - repo: https://github.com/python-jsonschema/check-jsonschema
8 | rev: 0.17.1
9 | hooks:
10 | - id: check-github-workflows
11 | - repo: https://github.com/pre-commit/pre-commit-hooks
12 | rev: v4.2.0
13 | hooks:
14 | - id: trailing-whitespace
15 | - id: end-of-file-fixer
16 | - id: check-yaml
17 | - id: debug-statements
18 | # - id: name-tests-test
19 | - id: requirements-txt-fixer
20 | - repo: https://github.com/asottile/reorder_python_imports
21 | rev: v3.1.0
22 | hooks:
23 | - id: reorder-python-imports
24 | args: [--py38-plus, --add-import, "from __future__ import annotations"]
25 | - repo: https://github.com/asottile/pyupgrade
26 | rev: v2.32.1
27 | hooks:
28 | - id: pyupgrade
29 | args: [--py38-plus]
30 | # - repo: https://github.com/PyCQA/flake8
31 | # rev: 4.0.1
32 | # hooks:
33 | # - id: flake8
34 | # - repo: https://github.com/pre-commit/mirrors-mypy
35 | # rev: v0.960
36 | # hooks:
37 | # - id: mypy
38 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=827846
3 | // for the documentation about the extensions.json format
4 | "recommendations": ["esbenp.prettier-vscode"]
5 | }
6 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "python.autoComplete.extraPaths": ["__pypackages__/3.8/lib"],
3 | "python.analysis.extraPaths": ["__pypackages__/3.8/lib"],
4 | "editor.formatOnSave": true,
5 | "editor.formatOnSaveMode": "file",
6 | "editor.defaultFormatter": "esbenp.prettier-vscode",
7 | "[javascript]": {
8 | "editor.defaultFormatter": "esbenp.prettier-vscode"
9 | },
10 | "[ignore]": {
11 | "editor.defaultFormatter": "foxundermoon.shell-format"
12 | },
13 | "[python]": {
14 | "editor.defaultFormatter": "ms-python.python"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @getsentry/dev-infra
2 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Build stage
2 | FROM python:3.11 AS builder
3 | RUN pip install -U pip setuptools wheel
4 | # Copy files
5 | COPY requirements.frozen.txt /project/
6 | WORKDIR /project
7 | RUN pip install --no-cache-dir -r requirements.frozen.txt
8 |
9 | # Execution stage
10 | FROM python:3.11
11 | WORKDIR /app
12 | COPY src/ /project/src
13 | # Retrieve packages from build stage
14 | COPY --from=builder /usr/local/lib/python3.11/site-packages/ /usr/local/lib/python3.11/site-packages/
15 | RUN pip install gunicorn==20.1.0
16 | # Source code
17 | COPY src/ /app/src/
18 | # 1 worker, 4 worker threads should be more than enough.
19 | # --worker-class gthread is automatically set if --threads > 1.
20 | # In my experience this configuration hovers around 100 MB
21 | # baseline (noop app code) memory usage in Cloud Run.
22 | # --timeout 0 disables gunicorn's automatic worker restarting.
23 | # "Workers silent for more than this many seconds are killed and restarted."
24 | # If things get bad you might want to --max-requests, --max-requests-jitter, --workers 2.
25 | CMD ["gunicorn", "--bind", ":8080", "--workers", "1", "--threads", "4", "--timeout", "0", "src.main:app"]
26 |
--------------------------------------------------------------------------------
/GithubAppPostInstall.md:
--------------------------------------------------------------------------------
1 | # Github App Post Installation page
2 |
3 | Currently the Github App is only meant to be used for internal use.
4 |
5 | If the app was to be made public and needed post-installation steps they would be listed here.
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Armen Zambrano G.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Sentry GitHub Actions App
2 |
3 | [](https://codecov.io/gh/getsentry/sentry-github-actions-app)
4 |
5 | **NOTE**: If this is a project you would like us to invest in, please let us know in [this issue](https://github.com/getsentry/sentry-github-actions-app/issues/46). You can try this app by following the steps in the "Try it out" section below.
6 |
7 | [This app](https://github.com/apps/sentry-github-app-alpha) allows your organization to instrument GitHub Actions with Sentry. You can use this to get insights of what parts of your CI are slow or failing often.
8 |
9 | It works by listening to GitHub workflow events via a webhook configured in a GitHub App. These events are then stored in Sentry as performance transactions. The best value for this project is when you have [Sentry's Discover](https://docs.sentry.io/product/discover-queries/) and [Dashboards](https://docs.sentry.io/product/dashboards/) in order to slice the data and answer the questions you care about. In a sense, this project turns GitHub's CI into an app which Sentry monitors, thus, you can use many of the features you're already familiar with.
10 |
11 | **NOTE**: The Discover feature is only available on either a Business or Trial Plan.
12 |
13 | **NOTE**: The Dashboards feature is only available on the Team Plan or higher.
14 |
15 | There's likely products out there that can give you insights into GitHub's CI, however, this may be cheaper, it saves you having to add another vendor and your developers are already familiar with using Sentry.
16 |
17 | ## Examples
18 |
19 | This screenshot shows that you can group GitHub Actions through various tags in Sentry's Discover:
20 |
21 |
22 |
23 | This screenshot shows the transaction view for an individual GitHub Action showing each step of the job:
24 |
25 |
26 |
27 | ## Features
28 |
29 | Here's a list of benefits you will get if you use this project:
30 |
31 | - Visualize a GitHub job and the breakdown per step
32 | - For instance, it makes it easy to see what percentage of the jobs are dedicated to set up versus running tests
33 | - Create CI alerts
34 | - For instance, if your main branch has a failure rate above a certain threshold
35 | - Notice when `on: schedule` workflows fail which currently GitHub does not make it easy to notice
36 | - Create widgets and dashboards showing your CI data
37 | - For instance you can show: slowest jobs, most failing jobs, jobs requirying re-runs, repos consuming most CI
38 | - Some of the main tags by which you can slice the data: workflow file, repo, branch, commit, job_status, pull_request, run_attempt
39 |
40 | Watch this video showing the features described above:
41 |
42 | https://user-images.githubusercontent.com/44410/187217254-ad7c2eba-f4d4-4a08-9733-54cf92f466ec.mp4
43 |
44 | ## Try it out
45 |
46 | Steps to follow:
47 |
48 | 1. Create a Sentry project to gather your GitHub CI data
49 | 1. You don't need to select a platform in the wizard or any alerts
50 | 2. Find the DSN key under the settings of the project you created
51 | 2. Create a private repo named `.sentry`
52 | 3. Create a file in that repo called `sentry_config.ini` with these contents (adjust your DSN value)
53 |
54 | ```ini
55 | [sentry-github-actions-app]
56 | ; DSN value of the Sentry project you created in step (1) above.
57 | dsn = https://foo@bar.ingest.sentry.io/foobar
58 | ```
59 |
60 | 4. Install [this GitHub App](https://github.com/apps/sentry-github-app-alpha)
61 | 1. **"All repositories"** - this is the easiest option; all repositories will have telemetry from GitHub Actions sent.
62 | 2. **"Only select repositories"**
63 | 1. Ensure that the `.sentry` private repository (created in step 2 above) is selected
64 | 2. Select any projects that are desired to have telemetry from all containing GitHub Actions sent.
65 | - **Note:** Regardless of which is selected, we only request **workflow jobs** and a **single file** permissions.
66 |
67 | **NOTE**: In other words, we won't be able to access any of your code.
68 |
69 | Give us feedback in [this issue](https://github.com/getsentry/sentry-github-actions-app/issues/46).
70 |
71 | ## Local development
72 |
73 | Prerequisites:
74 |
75 | - ngrok (for local dev)
76 |
77 | Set up:
78 |
79 | ```bash
80 | python3 -m venv .venv
81 | source .venv/bin/activate
82 | pip install wheel
83 | pip install -r requirements.txt -r requirements-dev.txt
84 | ```
85 |
86 | You can ingest a single job without webhooks by using the cli. For example:
87 |
88 | ```shell
89 | # This is a normal URL of a job on GitHub
90 | python3 cli.py https://github.com/getsentry/sentry/runs/5759197422?check_suite_focus=true
91 | # From test fixture
92 | python3 cli.py tests/fixtures/jobA/job.json
93 | ```
94 |
95 | Steps to ingest events from a repository:
96 |
97 | - Install ngrok, authenticate and start it up (`ngrok http 5001`)
98 | - Take note of the URL
99 | - Create a GitHub webhook for the repo you want to analyze
100 | - Choose `Workflow jobs` events & make sure to choose `application/json`
101 | - Put Ngrok's URL there
102 |
103 | Table of commands:
104 |
105 | | Command | Description |
106 | | ---------------------------------- | ---------------------------------------------- |
107 | | flask run -p 5001 | Start the Flask app on |
108 | | pre-commit install | Install pre-commit hooks |
109 | | tox | Run tests in isolated environment |
110 | | pytest | Run Python tests |
111 | | pytest --cov=src --cov-report=html | Generate code coverage. |
112 |
113 | ## Sentry staff info
114 |
115 | Google Cloud Build will automatically build a Docker image when the code merges on `main`. Log-in to Google Cloud Run and deploy the latest image.
116 |
--------------------------------------------------------------------------------
/cli.py:
--------------------------------------------------------------------------------
1 | # This script only works if you have installed the GH app on an org OR you have created an app with the same permissions
2 | #
3 | # This script can be used to ingest a GH job. To ingest a job point it to the URL showing you the log of a job
4 | # NOTE: Make sure "?check_suite_focus=true" is not included; zsh does not like it
5 | # For instance https://github.com/getsentry/sentry/runs/5759197422?check_suite_focus=true
6 | from __future__ import annotations
7 |
8 | import argparse
9 | import logging
10 | import os
11 |
12 | import requests
13 |
14 | from src.github_app import GithubAppToken
15 | from src.github_sdk import GithubClient
16 | from src.sentry_config import fetch_dsn_for_github_org
17 | from src.web_app_handler import WebAppHandler
18 |
19 | logging.getLogger().setLevel(os.environ.get("LOGGING_LEVEL", "INFO"))
20 | logging.basicConfig()
21 |
22 |
23 | def _fetch_job(url: str) -> tuple(str, dict):
24 | _, _, _, org, repo, _, run_id = url.split("?")[0].split("/")
25 | req = requests.get(
26 | f"https://api.github.com/repos/{org}/{repo}/actions/jobs/{run_id}",
27 | )
28 | req.raise_for_status()
29 | job = req.json()
30 | return org, job
31 |
32 |
33 | def main() -> int:
34 | parser = argparse.ArgumentParser()
35 | parser.add_argument("url")
36 | parser.add_argument("--installation-id")
37 | args = parser.parse_args()
38 |
39 | org, job = _fetch_job(args.url)
40 | if org != "getsentry":
41 | assert (
42 | args.installation_id is not None
43 | ), "If you try to use a non-default org, you also need to specify the installation ID."
44 |
45 | # You can have a default installation ID by using an env variable
46 | installation_id = args.installation_id or os.environ["INSTALLATION_ID"]
47 |
48 | web_app = WebAppHandler()
49 | with GithubAppToken(**web_app.config.gh_app._asdict()).get_token(
50 | installation_id
51 | ) as token:
52 | dsn = fetch_dsn_for_github_org(org, token)
53 | client = GithubClient(token=token, dsn=dsn)
54 | client.send_trace(job)
55 |
56 |
57 | if __name__ == "__main__":
58 | raise SystemExit(main())
59 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "Github Actions to Sentry app"
3 | version = "0.0.1"
4 | description = "It listens to workflow events and inserts them into Sentry."
5 | authors = [
6 | {name = "Armen Zambrano G.", email = "armenzg@sentry.io"},
7 | ]
8 | license-expression = "MIT"
9 | requires-python = ">=3.8"
10 |
11 | [project.urls]
12 | Homepage = "https://github.com/getsentry/github-actions-app"
13 |
14 | [tool.pytest.ini_options]
15 | # -ra: Show extra test summary info: (a)ll except passed
16 | # -q: Decrease verbosity
17 | # --lf: Rerun only the tests that failed at the last run (or all if none failed)
18 | addopts = "-ra -q"
19 | testpaths = [ "tests" ]
20 |
21 | [tool.tox]
22 | legacy_tox_ini = """
23 | [tox]
24 | envlist = python3.10
25 | skipsdist = true
26 |
27 | [testenv]
28 | deps =
29 | -rrequirements.txt
30 | -rrequirements-dev.txt
31 | commands =
32 | pytest
33 | """
34 |
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | black>=22.3.0
2 | freezegun>=1.2.1
3 | pre-commit>=2.18.1
4 | pytest>=7.1.1
5 | pytest-cov>=3.0.0
6 | python-dotenv>=0.20.0
7 | responses>=0.20.0
8 |
--------------------------------------------------------------------------------
/requirements.frozen.txt:
--------------------------------------------------------------------------------
1 | blinker==1.7.0
2 | cachetools==5.3.2
3 | certifi==2023.11.17
4 | cffi==1.16.0
5 | charset-normalizer==2.0.12
6 | click==8.1.7
7 | cryptography==41.0.7
8 | Flask==2.3.3
9 | google-api-core==2.15.0
10 | google-auth==2.25.2
11 | google-cloud-secret-manager==2.11.0
12 | googleapis-common-protos==1.62.0
13 | grpc-google-iam-v1==0.13.0
14 | grpcio==1.60.0
15 | grpcio-status==1.60.0
16 | idna==3.6
17 | itsdangerous==2.1.2
18 | Jinja2==3.1.2
19 | MarkupSafe==2.1.3
20 | proto-plus==1.23.0
21 | protobuf==4.25.1
22 | pyasn1==0.5.1
23 | pyasn1-modules==0.3.0
24 | pycparser==2.21
25 | PyJWT==2.4.0
26 | requests==2.27.1
27 | rsa==4.9
28 | sentry-sdk==1.5.8
29 | urllib3==1.26.18
30 | Werkzeug==3.0.1
31 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | flask==2.3.3
2 | google-cloud-secret-manager==2.11.0
3 | PyJWT[crypto]==2.4.0
4 | requests==2.27.1
5 | sentry-sdk[flask]==1.5.8
6 |
--------------------------------------------------------------------------------
/setup.md:
--------------------------------------------------------------------------------
1 | # Set-up
2 |
3 | This page includes information about our deployment. No need to be read by customers installing the Github App.
4 |
5 | ## Backend deployment
6 |
7 | In Sentry.io, we created a project to track errors of the backend itself (`APP_DSN` in section below).
8 |
9 | The app is deployed via GC. The app's deployment is in the webhook url for the Github app. Currently the deployment requires manual intervention.
10 |
11 | Common environment variables:
12 |
13 | - `APP_DSN`: Report your deployment errors to Sentry
14 | - `GH_WEBHOOK_SECRET`: Secret shared between Github's webhook and your app
15 | - Create a secret with `python3 -c 'import secrets; print(secrets.token_urlsafe(20))'` on your command line
16 | - `LOGGING_LEVEL` (optional): To set the verbosity of Python's logging (defaults to INFO)
17 |
18 | Github App specific variables:
19 |
20 | - `GH_APP_ID` - Github App ID
21 | - When you create the Github App, you will see the value listed in the page
22 | - `GH_APP_PRIVATE_KEY` - Private key
23 | - When you load the Github App page, at the bottom of the page under "Private Keys" select "Generate a private key"
24 | - A .pem file will be downloaded locally.
25 | - Convert it into a single line value by using base64 (`base64 -i path_to_pem_file`) and delete it
26 |
27 | For local development, you need to make the App's webhook point to your ngrok set up. You should create a new private key (a .pem file that gets automatically downloaded when generated) for your local development and do not forget to delete the private key when you are done.
28 |
29 | ## The Github App
30 |
31 | **NOTE**: Only Sentry Github Admins can make changes to the app
32 |
33 | Configuration taken after creating the app:
34 |
35 | - Configure the Webhook URL to point to the backend deployment
36 | - Set the Webhook Secret to be the same as the backend deployment
37 | - Set the following permissions for the app:
38 | - Actions: Workflows, workflow runs and artifacts -> Read-only
39 | - Singe File: sentry_config.ini
40 | - Subscribe to events:
41 | - [Workflow job](https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#workflow_job)
42 |
43 | After completing the creation of the app you will need to make few more changes:
44 |
45 | - Click on `Generate a private key`
46 | - Run `base64 -i `
47 | - Add to the deployment the variable `GH_APP_PRIVATE_KEY`
48 | - Delete the file just downloaded
49 | - Add to the deployment the variable `GH_APP_ID`
50 | - Select "Install App" and install the app on your Github org
51 | - Grant access to "All Repositories"
52 |
--------------------------------------------------------------------------------
/src/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/getsentry/sentry-github-actions-app/1d039c3628f33bbbb19b63643dae32721e7c105d/src/__init__.py
--------------------------------------------------------------------------------
/src/github_app.py:
--------------------------------------------------------------------------------
1 | """
2 | This module contains the logic to support running the app as a Github App
3 | """
4 | from __future__ import annotations
5 |
6 | import contextlib
7 | import time
8 | from typing import Generator
9 |
10 | import jwt
11 | import requests
12 |
13 |
14 | class GithubAppToken:
15 | def __init__(self, private_key, app_id) -> None:
16 | self.headers = self.get_authentication_header(private_key, app_id)
17 |
18 | # From docs: Installation access tokens have the permissions
19 | # configured by the GitHub App and expire after one hour.
20 | @contextlib.contextmanager
21 | def get_token(self, installation_id: int) -> Generator[str, None, None]:
22 | req = requests.post(
23 | url=f"https://api.github.com/app/installations/{installation_id}/access_tokens",
24 | headers=self.headers,
25 | )
26 | req.raise_for_status()
27 | resp = req.json()
28 | try:
29 | # This token expires in an hour
30 | yield resp["token"]
31 | finally:
32 | requests.delete(
33 | "https://api.github.com/installation/token",
34 | headers={"Authorization": f"token {resp['token']}"},
35 | )
36 |
37 | def get_jwt_token(self, private_key, app_id):
38 | payload = {
39 | # issued at time, 60 seconds in the past to allow for clock drift
40 | "iat": int(time.time()) - 60,
41 | # JWT expiration time (5 minutes maximum)
42 | "exp": int(time.time()) + 5 * 60,
43 | # GitHub App's identifier
44 | "iss": app_id,
45 | }
46 | return jwt.encode(payload, private_key, algorithm="RS256")
47 |
48 | def get_authentication_header(self, private_key, app_id):
49 | jwt_token = self.get_jwt_token(private_key, app_id)
50 | return {
51 | "Accept": "application/vnd.github.v3+json",
52 | "Authorization": f"Bearer {jwt_token}",
53 | }
54 |
--------------------------------------------------------------------------------
/src/github_sdk.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import gzip
4 | import hashlib
5 | import io
6 | import logging
7 | import uuid
8 | from datetime import datetime
9 |
10 | import requests
11 | from sentry_sdk.envelope import Envelope
12 | from sentry_sdk.utils import format_timestamp
13 |
14 |
15 | class GithubSentryError(Exception):
16 | pass
17 |
18 |
19 | def get_uuid():
20 | return uuid.uuid4().hex
21 |
22 |
23 | def get_uuid_from_string(input_string):
24 | hash_object = hashlib.sha256(input_string.encode())
25 | hash_value = hash_object.hexdigest()
26 | return uuid.UUID(hash_value[:32]).hex
27 |
28 |
29 | class GithubClient:
30 | # This transform GH jobs conclusion keywords to Sentry performance status
31 | github_status_trace_status = {"success": "ok", "failure": "internal_error"}
32 |
33 | def __init__(self, token, dsn, dry_run=False) -> None:
34 | self.token = token
35 | self.dry_run = dry_run
36 | if dsn:
37 | base_uri, project_id = dsn.rsplit("/", 1)
38 | self.sentry_key = base_uri.rsplit("@")[0].rsplit("https://")[1]
39 | # '{BASE_URI}/api/{PROJECT_ID}/{ENDPOINT}/'
40 | self.sentry_project_url = f"{base_uri}/api/{project_id}/envelope/"
41 |
42 | def _fetch_github(self, url):
43 | headers = {"Authorization": f"token {self.token}"}
44 |
45 | req = requests.get(url, headers=headers)
46 | req.raise_for_status()
47 | return req
48 |
49 | def _get_extra_metadata(self, job):
50 | # XXX: This is the slowest call
51 | runs = self._fetch_github(job["run_url"]).json()
52 | workflow = self._fetch_github(runs["workflow_url"]).json()
53 | repo = runs["repository"]["full_name"]
54 | meta = {
55 | # "workflow_name": workflow["name"],
56 | "author": runs["head_commit"]["author"],
57 | # https://getsentry.atlassian.net/browse/TET-22
58 | # Tags are not linkified externally, plain text data can be selected in browsers and opened
59 | "data": {
60 | "job": job["html_url"],
61 | },
62 | "tags": {
63 | # e.g. success, failure, skipped
64 | "job_status": job["conclusion"],
65 | "branch": runs["head_branch"],
66 | "commit": runs["head_sha"],
67 | "repo": repo,
68 | "run_attempt": runs["run_attempt"], # Rerunning a job
69 | # It allows querying jobs within the same workflow (e.g. foo.yml)
70 | "workflow": workflow["path"].rsplit("/")[-1],
71 | },
72 | }
73 | if runs.get("pull_requests"):
74 | pr_number = runs["pull_requests"][0]["number"]
75 | meta["data"]["pr"] = f"https://github.com/{repo}/pull/{pr_number}"
76 | meta["tags"]["pull_request"] = pr_number
77 |
78 | return meta
79 |
80 | # Documentation about traces, transactions and spans
81 | # https://docs.sentry.io/product/sentry-basics/tracing/distributed-tracing/#traces
82 | # https://develop.sentry.dev/sdk/performance/
83 | def _generate_trace(self, job):
84 | meta = self._get_extra_metadata(job)
85 | transaction = _base_transaction(job)
86 | transaction["user"] = meta["author"]
87 | transaction["tags"] = meta["tags"]
88 | transaction["contexts"]["trace"]["data"] = meta["data"]
89 |
90 | # Transactions have name, spans don't.
91 | transaction["contexts"]["trace"]["op"] = job["name"]
92 | transaction["contexts"]["trace"]["description"] = job["name"]
93 | transaction["contexts"]["trace"][
94 | "status"
95 | ] = self.github_status_trace_status.get(job["conclusion"], "unimplemented")
96 | transaction["spans"] = _generate_spans(
97 | job["steps"],
98 | transaction["contexts"]["trace"]["span_id"],
99 | transaction["contexts"]["trace"]["trace_id"],
100 | )
101 | return transaction
102 |
103 | def _send_envelope(self, trace):
104 | if self.dry_run:
105 | return
106 | envelope = Envelope()
107 | envelope.add_transaction(trace)
108 | now = datetime.utcnow()
109 |
110 | headers = {
111 | "event_id": get_uuid(), # Does this have to match anything?
112 | "sent_at": format_timestamp(now),
113 | "Content-Type": "application/x-sentry-envelope",
114 | "Content-Encoding": "gzip",
115 | "X-Sentry-Auth": f"Sentry sentry_key={self.sentry_key},"
116 | + f"sentry_client=gha-sentry/0.0.1,sentry_timestamp={now},"
117 | + "sentry_version=7",
118 | }
119 |
120 | body = io.BytesIO()
121 | with gzip.GzipFile(fileobj=body, mode="w") as f:
122 | envelope.serialize_into(f)
123 |
124 | req = requests.post(
125 | self.sentry_project_url,
126 | data=body.getvalue(),
127 | headers=headers,
128 | )
129 | req.raise_for_status()
130 | return req
131 |
132 | def send_trace(self, job):
133 | # This can happen when the workflow is skipped and there are no steps
134 | if job["conclusion"] == "skipped":
135 | logging.info(
136 | f"We are ignoring '{job['name']}' because it was skipped -> {job['html_url']}",
137 | )
138 | return
139 | trace = self._generate_trace(job)
140 | if trace:
141 | return self._send_envelope(trace)
142 |
143 |
144 | def _base_transaction(job):
145 | return {
146 | "event_id": get_uuid(),
147 | # The distinctive feature of a Transaction is type: "transaction".
148 | "type": "transaction",
149 | "transaction": job["name"],
150 | "contexts": {
151 | "trace": {
152 | "span_id": get_uuid()[:16],
153 | # This trace id is basically just a hash from run_id and run_attempt turned into a uuid. We use it to trace jobs within a workflow.
154 | # `run_id` identifiess a particular workflow run and does not increment when rerunning jobs. This is why we also use `run_attempt` in the job.
155 | # https://docs.github.com/en/actions/learn-github-actions/contexts
156 | # https://docs.github.com/en/webhooks/webhook-events-and-payloads#workflow_job
157 | "trace_id": get_uuid_from_string(
158 | "run_id:"
159 | + str(job["run_id"])
160 | + "run_attempt:"
161 | + str(job["run_attempt"])
162 | ),
163 | "type": "trace",
164 | },
165 | },
166 | "user": {},
167 | # When ingesting old data during development (e.g. using fixtures), Sentry's UI will
168 | # show an error for transactions with "Clock drift detected in SDK"; It is harmeless.
169 | "start_timestamp": job["started_at"],
170 | "timestamp": job["completed_at"],
171 | }
172 |
173 |
174 | # https://develop.sentry.dev/sdk/event-payloads/span/
175 | def _generate_spans(steps, parent_span_id, trace_id):
176 | spans = []
177 | for step in steps:
178 | try:
179 | spans.append(
180 | {
181 | "op": step["name"],
182 | "name": step["name"],
183 | "parent_span_id": parent_span_id,
184 | "span_id": get_uuid()[:16],
185 | "start_timestamp": step["started_at"],
186 | "timestamp": step["completed_at"],
187 | "trace_id": trace_id,
188 | },
189 | )
190 | except Exception as e:
191 | logging.exception(e)
192 | return spans
193 |
--------------------------------------------------------------------------------
/src/main.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import logging
4 | import os
5 |
6 | import sentry_sdk
7 | from flask import abort
8 | from flask import Flask
9 | from flask import jsonify
10 | from flask import request
11 | from sentry_sdk import capture_exception
12 | from sentry_sdk.integrations.flask import FlaskIntegration
13 |
14 | from .web_app_handler import WebAppHandler
15 |
16 | APP_DSN = os.environ.get("APP_DSN")
17 | if APP_DSN:
18 | # This tracks errors and performance of the app itself rather than GH workflows
19 | sentry_sdk.init(
20 | dsn=APP_DSN,
21 | integrations=[FlaskIntegration()],
22 | # Set traces_sample_rate to 1.0 to capture 100%
23 | # of transactions for performance monitoring.
24 | # We recommend adjusting this value in production.
25 | traces_sample_rate=1.0,
26 | environment=os.environ.get("FLASK_ENV", "production"),
27 | )
28 |
29 | LOGGING_LEVEL = os.environ.get("LOGGING_LEVEL", "INFO")
30 | # Set the logging level for all loggers (e.g. requests)
31 | logging.getLogger().setLevel(LOGGING_LEVEL)
32 | logging.basicConfig()
33 |
34 | # Logger for this module
35 | logger = logging.getLogger(__name__)
36 | logger.setLevel(LOGGING_LEVEL)
37 | logger.info("App logging is working.")
38 |
39 | handler = WebAppHandler()
40 |
41 | app = Flask(__name__)
42 |
43 |
44 | @app.route("/", methods=["POST"])
45 | def main():
46 | if not handler.valid_signature(request.data, request.headers):
47 | abort(
48 | 400,
49 | "The secret you are using on your Github webhook does not match this app's secret.",
50 | )
51 |
52 | # Top-level crash preventing try block
53 | try:
54 | reason, http_code = handler.handle_event(request.json, request.headers)
55 | return jsonify({"reason": reason}), http_code
56 | except Exception as e:
57 | logger.exception(e)
58 | capture_exception(e)
59 | return jsonify({"reason": "There was an error."}), 500
60 |
61 |
62 | if os.environ.get("FLASK_ENV") == "development":
63 |
64 | @app.route("/debug-sentry")
65 | def trigger_error():
66 | try:
67 | 1 / 0
68 | except Exception as e:
69 | # Report it to Sentry
70 | capture_exception(e)
71 | # Let Flask handle the rest
72 | raise e
73 |
--------------------------------------------------------------------------------
/src/sentry_config.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import base64
4 | from configparser import ConfigParser
5 | from functools import lru_cache
6 |
7 | import requests
8 |
9 | SENTRY_CONFIG_API_URL = (
10 | "https://api.github.com/repos/{owner}/.sentry/contents/sentry_config.ini"
11 | )
12 |
13 |
14 | def fetch_dsn_for_github_org(org: str, token: str) -> str:
15 | # Using the GH app token allows fetching the file in a private repo
16 | headers = {
17 | "Accept": "application/vnd.github+json",
18 | "Authorization": f"token {token}",
19 | }
20 | api_url = SENTRY_CONFIG_API_URL.replace("{owner}", org)
21 | # - Get meta about sentry_config.ini file
22 | resp = requests.get(api_url, headers=headers)
23 | resp.raise_for_status()
24 | meta = resp.json()
25 |
26 | if meta["type"] != "file":
27 | # XXX: custom error
28 | raise Exception(meta["type"])
29 |
30 | assert meta["encoding"] == "base64", meta["encoding"]
31 | file_contents = base64.b64decode(meta["content"]).decode()
32 |
33 | # - Read ini file and assertions
34 | cp = ConfigParser()
35 | cp.read_string(file_contents)
36 | return cp.get("sentry-github-actions-app", "dsn")
37 |
--------------------------------------------------------------------------------
/src/web_app_handler.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import base64
4 | import hmac
5 | import logging
6 | import os
7 | from typing import NamedTuple
8 |
9 | from .github_app import GithubAppToken
10 | from .github_sdk import GithubClient
11 | from src.sentry_config import fetch_dsn_for_github_org
12 |
13 | LOGGING_LEVEL = os.environ.get("LOGGING_LEVEL", logging.INFO)
14 | logger = logging.getLogger(__name__)
15 | logger.setLevel(LOGGING_LEVEL)
16 |
17 |
18 | class WebAppHandler:
19 | def __init__(self, dry_run=False):
20 | self.config = init_config()
21 | self.dry_run = dry_run
22 |
23 | def handle_event(self, data, headers):
24 | # We return 200 to make webhook not turn red since everything got processed well
25 | http_code = 200
26 | reason = "OK"
27 |
28 | if headers["X-GitHub-Event"] != "workflow_job":
29 | reason = "Event not supported."
30 | elif data["action"] != "completed":
31 | reason = "We cannot do anything with this workflow state."
32 | else:
33 | # For now, this simplifies testing
34 | if self.dry_run:
35 | return reason, http_code
36 |
37 | installation_id = data["installation"]["id"]
38 | org = data["repository"]["owner"]["login"]
39 |
40 | # We are executing in Github App mode
41 | if self.config.gh_app:
42 | with GithubAppToken(**self.config.gh_app._asdict()).get_token(
43 | installation_id
44 | ) as token:
45 | # Once the Sentry org has a .sentry repo we can remove the DSN from the deployment
46 | dsn = fetch_dsn_for_github_org(org, token)
47 | client = GithubClient(
48 | token=token,
49 | dsn=dsn,
50 | dry_run=self.dry_run,
51 | )
52 | client.send_trace(data["workflow_job"])
53 | else:
54 | # Once the Sentry org has a .sentry repo we can remove the DSN from the deployment
55 | dsn = fetch_dsn_for_github_org(org, token)
56 | client = GithubClient(
57 | token=self.config.gh.token,
58 | dsn=dsn,
59 | dry_run=self.dry_run,
60 | )
61 | client.send_trace(data["workflow_job"])
62 |
63 | return reason, http_code
64 |
65 | def valid_signature(self, body, headers):
66 | if not self.config.gh.webhook_secret:
67 | return True
68 | else:
69 | signature = headers["X-Hub-Signature-256"].replace("sha256=", "")
70 | body_signature = hmac.new(
71 | self.config.gh.webhook_secret.encode(),
72 | msg=body,
73 | digestmod="sha256",
74 | ).hexdigest()
75 | return hmac.compare_digest(body_signature, signature)
76 |
77 |
78 | class GithubAppConfig(NamedTuple):
79 | app_id: int
80 | private_key: str
81 |
82 |
83 | class GitHubConfig(NamedTuple):
84 | webhook_secret: str | None
85 | token: str | None
86 |
87 |
88 | class Config(NamedTuple):
89 | gh_app: GithubAppConfig | None
90 | gh: GitHubConfig
91 |
92 |
93 | def get_gh_app_private_key():
94 | private_key = None
95 | # K_SERVICE is a reserved variable for Google Cloud services
96 | if os.environ.get("K_SERVICE") and not os.environ.get("GH_APP_PRIVATE_KEY"):
97 | # XXX: Put in here since it currently affects test execution
98 | # ImportError: dlopen(/Users/armenzg/code/github-actions-app/.venv/lib/python3.10/site-packages/grpc/_cython/cygrpc.cpython-310-darwin.so, 0x0002): tried: '/Users/armenzg/code/github-actions-app/.venv/lib/python3.10/site-packages/grpc/_cython/cygrpc.cpython-310-darwin.so'
99 | # (mach-o file, but is an incompatible architecture (have 'x86_64', need 'arm64e'))
100 | from google.cloud import secretmanager
101 |
102 | gcp_client = secretmanager.SecretManagerServiceClient()
103 | uri = (
104 | f"projects/sentry-dev-tooling/secrets/SentryGithubAppPrivateKey/versions/1"
105 | )
106 |
107 | logger.info(f"Grabbing secret from {uri}")
108 | private_key = base64.b64decode(
109 | gcp_client.access_secret_version(
110 | name=uri,
111 | ).payload.data.decode("UTF-8"),
112 | )
113 | else:
114 | # This block only applies for development since we are not executing on GCP
115 | private_key = base64.b64decode(os.environ["GH_APP_PRIVATE_KEY"])
116 | return private_key
117 |
118 |
119 | def init_config():
120 | gh_app = None
121 | try:
122 | # This variable is the key to enabling Github App mode or not
123 | if os.environ.get("GH_APP_ID"):
124 | private_key = get_gh_app_private_key()
125 | gh_app = GithubAppConfig(
126 | app_id=os.environ["GH_APP_ID"],
127 | private_key=private_key,
128 | )
129 | except Exception as e:
130 | logger.exception(e)
131 | logger.warning(
132 | "We have failed to load the private key, however, we will fallback to the PAT method.",
133 | )
134 |
135 | return Config(
136 | gh_app,
137 | GitHubConfig(
138 | # This token is a PAT
139 | token=os.environ.get("GH_TOKEN"),
140 | webhook_secret=os.environ.get("GH_WEBHOOK_SECRET"),
141 | ),
142 | )
143 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/getsentry/sentry-github-actions-app/1d039c3628f33bbbb19b63643dae32721e7c105d/tests/__init__.py
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import json
4 |
5 | import pytest
6 |
7 |
8 | @pytest.fixture
9 | def jobA_job():
10 | with open("tests/fixtures/jobA/job.json") as f:
11 | return json.load(f)
12 |
13 |
14 | @pytest.fixture
15 | def jobA_runs():
16 | with open("tests/fixtures/jobA/runs.json") as f:
17 | return json.load(f)
18 |
19 |
20 | @pytest.fixture
21 | def jobA_workflow():
22 | with open("tests/fixtures/jobA/workflow.json") as f:
23 | return json.load(f)
24 |
25 |
26 | @pytest.fixture
27 | def skipped_workflow():
28 | with open("tests/fixtures/skipped_workflow.json") as f:
29 | return json.load(f)
30 |
31 |
32 | @pytest.fixture
33 | def jobA_trace():
34 | with open("tests/fixtures/jobA/trace.json") as f:
35 | return json.load(f)
36 |
37 |
38 | @pytest.fixture
39 | def uuid_list():
40 | return [
41 | "5ae279acd9824cbfa85042013cfbf8b7",
42 | "a401d83c7ec0495f82a8da8d9a389f5b",
43 | "4d4d3477c624836b1b3a3729a7de688a",
44 | "0726fb4a2341477cbfa36ebd830bd8e3",
45 | "a7776ed12daa449bb0642e9ea1bb4152",
46 | "6e147ff372f9498abb5749a1210d8e0a",
47 | "a401d83c7ec0495f82a8da8d9a389f5b",
48 | "a401d83c7ec0495f82a8da8d9a389f5b",
49 | ]
50 |
51 |
52 | @pytest.fixture
53 | def webhook_event():
54 | with open("tests/fixtures/webhook_event.json") as f:
55 | return json.load(f)
56 |
--------------------------------------------------------------------------------
/tests/fixtures/jobA/job.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 5857527450,
3 | "run_id": 2104746951,
4 | "run_url": "https://api.github.com/repos/getsentry/sentry/actions/runs/2104746951",
5 | "run_attempt": 1,
6 | "node_id": "CR_kwDOAA1TcM8AAAABXSLGmg",
7 | "head_sha": "fd976218a94d8a0cd203c711ea7dbe68c573ef4d",
8 | "url": "https://api.github.com/repos/getsentry/sentry/actions/jobs/5857527450",
9 | "html_url": "https://github.com/getsentry/sentry/runs/5857527450?check_suite_focus=true",
10 | "status": "completed",
11 | "conclusion": "success",
12 | "started_at": "2022-04-06T19:52:17Z",
13 | "completed_at": "2022-04-06T20:05:37Z",
14 | "name": "frontend tests (0)",
15 | "steps": [
16 | {
17 | "name": "Set up job",
18 | "status": "completed",
19 | "conclusion": "success",
20 | "number": 1,
21 | "started_at": "2022-04-06T19:52:16.000Z",
22 | "completed_at": "2022-04-06T19:52:20.000Z"
23 | },
24 | {
25 | "name": "Pull ghcr.io/getsentry/action-html-to-image:latest",
26 | "status": "completed",
27 | "conclusion": "success",
28 | "number": 2,
29 | "started_at": "2022-04-06T19:52:20.000Z",
30 | "completed_at": "2022-04-06T19:52:58.000Z"
31 | },
32 | {
33 | "name": "Post Checkout sentry",
34 | "status": "completed",
35 | "conclusion": "success",
36 | "number": 26,
37 | "started_at": "2022-04-06T20:05:37.000Z",
38 | "completed_at": "2022-04-06T20:05:35.000Z"
39 | },
40 | {
41 | "name": "Complete job",
42 | "status": "completed",
43 | "conclusion": "success",
44 | "number": 28,
45 | "started_at": "2022-04-06T20:05:35.000Z",
46 | "completed_at": "2022-04-06T20:05:35.000Z"
47 | }
48 | ],
49 | "check_run_url": "https://api.github.com/repos/getsentry/sentry/check-runs/5857527450",
50 | "labels": ["ubuntu-20.04"],
51 | "runner_id": 82,
52 | "runner_name": "GitHub Actions 42",
53 | "runner_group_id": 2,
54 | "runner_group_name": "GitHub Actions"
55 | }
56 |
--------------------------------------------------------------------------------
/tests/fixtures/jobA/runs.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 2104746951,
3 | "name": "acceptance",
4 | "node_id": "WFR_kwLOAA1TcM59c-PH",
5 | "head_branch": "ahmed/add-naming-layer",
6 | "head_sha": "fd976218a94d8a0cd203c711ea7dbe68c573ef4d",
7 | "run_number": 53142,
8 | "event": "pull_request",
9 | "status": "completed",
10 | "conclusion": "success",
11 | "workflow_id": 1174556,
12 | "check_suite_id": 5961050594,
13 | "check_suite_node_id": "CS_kwDOAA1TcM8AAAABY05p4g",
14 | "url": "https://api.github.com/repos/getsentry/sentry/actions/runs/2104746951",
15 | "html_url": "https://github.com/getsentry/sentry/actions/runs/2104746951",
16 | "pull_requests": [
17 | {
18 | "url": "https://api.github.com/repos/getsentry/sentry/pulls/33347",
19 | "id": 901742401,
20 | "number": 33347,
21 | "head": {
22 | "ref": "ahmed/add-naming-layer",
23 | "sha": "fd976218a94d8a0cd203c711ea7dbe68c573ef4d",
24 | "repo": {
25 | "id": 873328,
26 | "url": "https://api.github.com/repos/getsentry/sentry",
27 | "name": "sentry"
28 | }
29 | },
30 | "base": {
31 | "ref": "master",
32 | "sha": "4cf18bee34fcb9a0e38b6f1b7dd52323dd6ab45f",
33 | "repo": {
34 | "id": 873328,
35 | "url": "https://api.github.com/repos/getsentry/sentry",
36 | "name": "sentry"
37 | }
38 | }
39 | }
40 | ],
41 | "created_at": "2022-04-06T19:52:00Z",
42 | "updated_at": "2022-04-06T20:14:17Z",
43 | "actor": {
44 | "login": "ahmedetefy",
45 | "id": 10855986,
46 | "node_id": "MDQ6VXNlcjEwODU1OTg2",
47 | "avatar_url": "https://avatars.githubusercontent.com/u/10855986?v=4",
48 | "gravatar_id": "",
49 | "url": "https://api.github.com/users/ahmedetefy",
50 | "html_url": "https://github.com/ahmedetefy",
51 | "followers_url": "https://api.github.com/users/ahmedetefy/followers",
52 | "following_url": "https://api.github.com/users/ahmedetefy/following{/other_user}",
53 | "gists_url": "https://api.github.com/users/ahmedetefy/gists{/gist_id}",
54 | "starred_url": "https://api.github.com/users/ahmedetefy/starred{/owner}{/repo}",
55 | "subscriptions_url": "https://api.github.com/users/ahmedetefy/subscriptions",
56 | "organizations_url": "https://api.github.com/users/ahmedetefy/orgs",
57 | "repos_url": "https://api.github.com/users/ahmedetefy/repos",
58 | "events_url": "https://api.github.com/users/ahmedetefy/events{/privacy}",
59 | "received_events_url": "https://api.github.com/users/ahmedetefy/received_events",
60 | "type": "User",
61 | "site_admin": false
62 | },
63 | "run_attempt": 1,
64 | "run_started_at": "2022-04-06T19:52:00Z",
65 | "triggering_actor": {
66 | "login": "ahmedetefy",
67 | "id": 10855986,
68 | "node_id": "MDQ6VXNlcjEwODU1OTg2",
69 | "avatar_url": "https://avatars.githubusercontent.com/u/10855986?v=4",
70 | "gravatar_id": "",
71 | "url": "https://api.github.com/users/ahmedetefy",
72 | "html_url": "https://github.com/ahmedetefy",
73 | "followers_url": "https://api.github.com/users/ahmedetefy/followers",
74 | "following_url": "https://api.github.com/users/ahmedetefy/following{/other_user}",
75 | "gists_url": "https://api.github.com/users/ahmedetefy/gists{/gist_id}",
76 | "starred_url": "https://api.github.com/users/ahmedetefy/starred{/owner}{/repo}",
77 | "subscriptions_url": "https://api.github.com/users/ahmedetefy/subscriptions",
78 | "organizations_url": "https://api.github.com/users/ahmedetefy/orgs",
79 | "repos_url": "https://api.github.com/users/ahmedetefy/repos",
80 | "events_url": "https://api.github.com/users/ahmedetefy/events{/privacy}",
81 | "received_events_url": "https://api.github.com/users/ahmedetefy/received_events",
82 | "type": "User",
83 | "site_admin": false
84 | },
85 | "jobs_url": "https://api.github.com/repos/getsentry/sentry/actions/runs/2104746951/jobs",
86 | "logs_url": "https://api.github.com/repos/getsentry/sentry/actions/runs/2104746951/logs",
87 | "check_suite_url": "https://api.github.com/repos/getsentry/sentry/check-suites/5961050594",
88 | "artifacts_url": "https://api.github.com/repos/getsentry/sentry/actions/runs/2104746951/artifacts",
89 | "cancel_url": "https://api.github.com/repos/getsentry/sentry/actions/runs/2104746951/cancel",
90 | "rerun_url": "https://api.github.com/repos/getsentry/sentry/actions/runs/2104746951/rerun",
91 | "previous_attempt_url": null,
92 | "workflow_url": "https://api.github.com/repos/getsentry/sentry/actions/workflows/1174556",
93 | "head_commit": {
94 | "id": "fd976218a94d8a0cd203c711ea7dbe68c573ef4d",
95 | "tree_id": "e12e240849bd390ab3fb7f54f178e11446d29ddd",
96 | "message": "Clean up tests",
97 | "timestamp": "2022-04-06T19:51:41Z",
98 | "author": {
99 | "name": "Ahmed Etefy",
100 | "email": "ahmed.etefy12@gmail.com"
101 | },
102 | "committer": {
103 | "name": "Ahmed Etefy",
104 | "email": "ahmed.etefy12@gmail.com"
105 | }
106 | },
107 | "repository": {
108 | "id": 873328,
109 | "node_id": "MDEwOlJlcG9zaXRvcnk4NzMzMjg=",
110 | "name": "sentry",
111 | "full_name": "getsentry/sentry",
112 | "private": false,
113 | "owner": {
114 | "login": "getsentry",
115 | "id": 1396951,
116 | "node_id": "MDEyOk9yZ2FuaXphdGlvbjEzOTY5NTE=",
117 | "avatar_url": "https://avatars.githubusercontent.com/u/1396951?v=4",
118 | "gravatar_id": "",
119 | "url": "https://api.github.com/users/getsentry",
120 | "html_url": "https://github.com/getsentry",
121 | "followers_url": "https://api.github.com/users/getsentry/followers",
122 | "following_url": "https://api.github.com/users/getsentry/following{/other_user}",
123 | "gists_url": "https://api.github.com/users/getsentry/gists{/gist_id}",
124 | "starred_url": "https://api.github.com/users/getsentry/starred{/owner}{/repo}",
125 | "subscriptions_url": "https://api.github.com/users/getsentry/subscriptions",
126 | "organizations_url": "https://api.github.com/users/getsentry/orgs",
127 | "repos_url": "https://api.github.com/users/getsentry/repos",
128 | "events_url": "https://api.github.com/users/getsentry/events{/privacy}",
129 | "received_events_url": "https://api.github.com/users/getsentry/received_events",
130 | "type": "Organization",
131 | "site_admin": false
132 | },
133 | "html_url": "https://github.com/getsentry/sentry",
134 | "description": "Sentry is cross-platform application monitoring, with a focus on error reporting.",
135 | "fork": false,
136 | "url": "https://api.github.com/repos/getsentry/sentry",
137 | "forks_url": "https://api.github.com/repos/getsentry/sentry/forks",
138 | "keys_url": "https://api.github.com/repos/getsentry/sentry/keys{/key_id}",
139 | "collaborators_url": "https://api.github.com/repos/getsentry/sentry/collaborators{/collaborator}",
140 | "teams_url": "https://api.github.com/repos/getsentry/sentry/teams",
141 | "hooks_url": "https://api.github.com/repos/getsentry/sentry/hooks",
142 | "issue_events_url": "https://api.github.com/repos/getsentry/sentry/issues/events{/number}",
143 | "events_url": "https://api.github.com/repos/getsentry/sentry/events",
144 | "assignees_url": "https://api.github.com/repos/getsentry/sentry/assignees{/user}",
145 | "branches_url": "https://api.github.com/repos/getsentry/sentry/branches{/branch}",
146 | "tags_url": "https://api.github.com/repos/getsentry/sentry/tags",
147 | "blobs_url": "https://api.github.com/repos/getsentry/sentry/git/blobs{/sha}",
148 | "git_tags_url": "https://api.github.com/repos/getsentry/sentry/git/tags{/sha}",
149 | "git_refs_url": "https://api.github.com/repos/getsentry/sentry/git/refs{/sha}",
150 | "trees_url": "https://api.github.com/repos/getsentry/sentry/git/trees{/sha}",
151 | "statuses_url": "https://api.github.com/repos/getsentry/sentry/statuses/{sha}",
152 | "languages_url": "https://api.github.com/repos/getsentry/sentry/languages",
153 | "stargazers_url": "https://api.github.com/repos/getsentry/sentry/stargazers",
154 | "contributors_url": "https://api.github.com/repos/getsentry/sentry/contributors",
155 | "subscribers_url": "https://api.github.com/repos/getsentry/sentry/subscribers",
156 | "subscription_url": "https://api.github.com/repos/getsentry/sentry/subscription",
157 | "commits_url": "https://api.github.com/repos/getsentry/sentry/commits{/sha}",
158 | "git_commits_url": "https://api.github.com/repos/getsentry/sentry/git/commits{/sha}",
159 | "comments_url": "https://api.github.com/repos/getsentry/sentry/comments{/number}",
160 | "issue_comment_url": "https://api.github.com/repos/getsentry/sentry/issues/comments{/number}",
161 | "contents_url": "https://api.github.com/repos/getsentry/sentry/contents/{+path}",
162 | "compare_url": "https://api.github.com/repos/getsentry/sentry/compare/{base}...{head}",
163 | "merges_url": "https://api.github.com/repos/getsentry/sentry/merges",
164 | "archive_url": "https://api.github.com/repos/getsentry/sentry/{archive_format}{/ref}",
165 | "downloads_url": "https://api.github.com/repos/getsentry/sentry/downloads",
166 | "issues_url": "https://api.github.com/repos/getsentry/sentry/issues{/number}",
167 | "pulls_url": "https://api.github.com/repos/getsentry/sentry/pulls{/number}",
168 | "milestones_url": "https://api.github.com/repos/getsentry/sentry/milestones{/number}",
169 | "notifications_url": "https://api.github.com/repos/getsentry/sentry/notifications{?since,all,participating}",
170 | "labels_url": "https://api.github.com/repos/getsentry/sentry/labels{/name}",
171 | "releases_url": "https://api.github.com/repos/getsentry/sentry/releases{/id}",
172 | "deployments_url": "https://api.github.com/repos/getsentry/sentry/deployments"
173 | },
174 | "head_repository": {
175 | "id": 873328,
176 | "node_id": "MDEwOlJlcG9zaXRvcnk4NzMzMjg=",
177 | "name": "sentry",
178 | "full_name": "getsentry/sentry",
179 | "private": false,
180 | "owner": {
181 | "login": "getsentry",
182 | "id": 1396951,
183 | "node_id": "MDEyOk9yZ2FuaXphdGlvbjEzOTY5NTE=",
184 | "avatar_url": "https://avatars.githubusercontent.com/u/1396951?v=4",
185 | "gravatar_id": "",
186 | "url": "https://api.github.com/users/getsentry",
187 | "html_url": "https://github.com/getsentry",
188 | "followers_url": "https://api.github.com/users/getsentry/followers",
189 | "following_url": "https://api.github.com/users/getsentry/following{/other_user}",
190 | "gists_url": "https://api.github.com/users/getsentry/gists{/gist_id}",
191 | "starred_url": "https://api.github.com/users/getsentry/starred{/owner}{/repo}",
192 | "subscriptions_url": "https://api.github.com/users/getsentry/subscriptions",
193 | "organizations_url": "https://api.github.com/users/getsentry/orgs",
194 | "repos_url": "https://api.github.com/users/getsentry/repos",
195 | "events_url": "https://api.github.com/users/getsentry/events{/privacy}",
196 | "received_events_url": "https://api.github.com/users/getsentry/received_events",
197 | "type": "Organization",
198 | "site_admin": false
199 | },
200 | "html_url": "https://github.com/getsentry/sentry",
201 | "description": "Sentry is cross-platform application monitoring, with a focus on error reporting.",
202 | "fork": false,
203 | "url": "https://api.github.com/repos/getsentry/sentry",
204 | "forks_url": "https://api.github.com/repos/getsentry/sentry/forks",
205 | "keys_url": "https://api.github.com/repos/getsentry/sentry/keys{/key_id}",
206 | "collaborators_url": "https://api.github.com/repos/getsentry/sentry/collaborators{/collaborator}",
207 | "teams_url": "https://api.github.com/repos/getsentry/sentry/teams",
208 | "hooks_url": "https://api.github.com/repos/getsentry/sentry/hooks",
209 | "issue_events_url": "https://api.github.com/repos/getsentry/sentry/issues/events{/number}",
210 | "events_url": "https://api.github.com/repos/getsentry/sentry/events",
211 | "assignees_url": "https://api.github.com/repos/getsentry/sentry/assignees{/user}",
212 | "branches_url": "https://api.github.com/repos/getsentry/sentry/branches{/branch}",
213 | "tags_url": "https://api.github.com/repos/getsentry/sentry/tags",
214 | "blobs_url": "https://api.github.com/repos/getsentry/sentry/git/blobs{/sha}",
215 | "git_tags_url": "https://api.github.com/repos/getsentry/sentry/git/tags{/sha}",
216 | "git_refs_url": "https://api.github.com/repos/getsentry/sentry/git/refs{/sha}",
217 | "trees_url": "https://api.github.com/repos/getsentry/sentry/git/trees{/sha}",
218 | "statuses_url": "https://api.github.com/repos/getsentry/sentry/statuses/{sha}",
219 | "languages_url": "https://api.github.com/repos/getsentry/sentry/languages",
220 | "stargazers_url": "https://api.github.com/repos/getsentry/sentry/stargazers",
221 | "contributors_url": "https://api.github.com/repos/getsentry/sentry/contributors",
222 | "subscribers_url": "https://api.github.com/repos/getsentry/sentry/subscribers",
223 | "subscription_url": "https://api.github.com/repos/getsentry/sentry/subscription",
224 | "commits_url": "https://api.github.com/repos/getsentry/sentry/commits{/sha}",
225 | "git_commits_url": "https://api.github.com/repos/getsentry/sentry/git/commits{/sha}",
226 | "comments_url": "https://api.github.com/repos/getsentry/sentry/comments{/number}",
227 | "issue_comment_url": "https://api.github.com/repos/getsentry/sentry/issues/comments{/number}",
228 | "contents_url": "https://api.github.com/repos/getsentry/sentry/contents/{+path}",
229 | "compare_url": "https://api.github.com/repos/getsentry/sentry/compare/{base}...{head}",
230 | "merges_url": "https://api.github.com/repos/getsentry/sentry/merges",
231 | "archive_url": "https://api.github.com/repos/getsentry/sentry/{archive_format}{/ref}",
232 | "downloads_url": "https://api.github.com/repos/getsentry/sentry/downloads",
233 | "issues_url": "https://api.github.com/repos/getsentry/sentry/issues{/number}",
234 | "pulls_url": "https://api.github.com/repos/getsentry/sentry/pulls{/number}",
235 | "milestones_url": "https://api.github.com/repos/getsentry/sentry/milestones{/number}",
236 | "notifications_url": "https://api.github.com/repos/getsentry/sentry/notifications{?since,all,participating}",
237 | "labels_url": "https://api.github.com/repos/getsentry/sentry/labels{/name}",
238 | "releases_url": "https://api.github.com/repos/getsentry/sentry/releases{/id}",
239 | "deployments_url": "https://api.github.com/repos/getsentry/sentry/deployments"
240 | }
241 | }
242 |
--------------------------------------------------------------------------------
/tests/fixtures/jobA/trace.json:
--------------------------------------------------------------------------------
1 | {
2 | "contexts": {
3 | "trace": {
4 | "data": {
5 | "job": "https://github.com/getsentry/sentry/runs/5857527450?check_suite_focus=true",
6 | "pr": "https://github.com/getsentry/sentry/pull/33347"
7 | },
8 | "description": "frontend tests (0)",
9 | "op": "frontend tests (0)",
10 | "span_id": "a401d83c7ec0495f",
11 | "status": "ok",
12 | "trace_id": "4d4d3477c624836b1b3a3729a7de688a",
13 | "type": "trace"
14 | }
15 | },
16 | "event_id": "5ae279acd9824cbfa85042013cfbf8b7",
17 | "spans": [
18 | {
19 | "name": "Set up job",
20 | "op": "Set up job",
21 | "parent_span_id": "a401d83c7ec0495f",
22 | "span_id": "4d4d3477c624836b",
23 | "start_timestamp": "2022-04-06T19:52:16.000Z",
24 | "timestamp": "2022-04-06T19:52:20.000Z",
25 | "trace_id": "4d4d3477c624836b1b3a3729a7de688a"
26 | },
27 | {
28 | "name": "Pull ghcr.io/getsentry/action-html-to-image:latest",
29 | "op": "Pull ghcr.io/getsentry/action-html-to-image:latest",
30 | "parent_span_id": "a401d83c7ec0495f",
31 | "span_id": "0726fb4a2341477c",
32 | "start_timestamp": "2022-04-06T19:52:20.000Z",
33 | "timestamp": "2022-04-06T19:52:58.000Z",
34 | "trace_id": "4d4d3477c624836b1b3a3729a7de688a"
35 | },
36 | {
37 | "name": "Post Checkout sentry",
38 | "op": "Post Checkout sentry",
39 | "parent_span_id": "a401d83c7ec0495f",
40 | "span_id": "a7776ed12daa449b",
41 | "start_timestamp": "2022-04-06T20:05:37.000Z",
42 | "timestamp": "2022-04-06T20:05:35.000Z",
43 | "trace_id": "4d4d3477c624836b1b3a3729a7de688a"
44 | },
45 | {
46 | "name": "Complete job",
47 | "op": "Complete job",
48 | "parent_span_id": "a401d83c7ec0495f",
49 | "span_id": "6e147ff372f9498a",
50 | "start_timestamp": "2022-04-06T20:05:35.000Z",
51 | "timestamp": "2022-04-06T20:05:35.000Z",
52 | "trace_id": "4d4d3477c624836b1b3a3729a7de688a"
53 | }
54 | ],
55 | "start_timestamp": "2022-04-06T19:52:17Z",
56 | "tags": {
57 | "branch": "ahmed/add-naming-layer",
58 | "commit": "fd976218a94d8a0cd203c711ea7dbe68c573ef4d",
59 | "job_status": "success",
60 | "pull_request": 33347,
61 | "repo": "getsentry/sentry",
62 | "run_attempt": 1,
63 | "workflow": "acceptance.yml"
64 | },
65 | "timestamp": "2022-04-06T20:05:37Z",
66 | "transaction": "frontend tests (0)",
67 | "type": "transaction",
68 | "user": {
69 | "email": "ahmed.etefy12@gmail.com",
70 | "name": "Ahmed Etefy"
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/tests/fixtures/jobA/workflow.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 1174556,
3 | "node_id": "MDg6V29ya2Zsb3cxMTc0NTU2",
4 | "name": "acceptance",
5 | "path": ".github/workflows/acceptance.yml",
6 | "state": "active",
7 | "created_at": "2020-05-01T23:00:30.000Z",
8 | "updated_at": "2022-03-17T18:24:06.000Z",
9 | "url": "https://api.github.com/repos/getsentry/sentry/actions/workflows/1174556",
10 | "html_url": "https://github.com/getsentry/sentry/blob/master/.github/workflows/acceptance.yml",
11 | "badge_url": "https://github.com/getsentry/sentry/workflows/acceptance/badge.svg"
12 | }
13 |
--------------------------------------------------------------------------------
/tests/fixtures/skipped_workflow.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 2098089400,
3 | "name": "sentry pull request bot",
4 | "node_id": "WFR_kwLOAA1TcM59Dk24",
5 | "head_branch": "master",
6 | "head_sha": "e6f148cf35ff4d7e5da932cd3b1ffe8f40b1af52",
7 | "run_number": 31031,
8 | "event": "issue_comment",
9 | "status": "completed",
10 | "conclusion": "skipped",
11 | "workflow_id": 3020796,
12 | "check_suite_id": 5943587195,
13 | "check_suite_node_id": "CS_kwDOAA1TcM8AAAABYkPxew",
14 | "url": "https://api.github.com/repos/getsentry/sentry/actions/runs/2098089400",
15 | "html_url": "https://github.com/getsentry/sentry/actions/runs/2098089400",
16 | "pull_requests": [
17 | {
18 | "url": "https://api.github.com/repos/heholek/sentry/pulls/86",
19 | "id": 894496956,
20 | "number": 86,
21 | "head": {
22 | "ref": "master",
23 | "sha": "e6f148cf35ff4d7e5da932cd3b1ffe8f40b1af52",
24 | "repo": {
25 | "id": 873328,
26 | "url": "https://api.github.com/repos/getsentry/sentry",
27 | "name": "sentry"
28 | }
29 | },
30 | "base": {
31 | "ref": "master",
32 | "sha": "f05d2d00449b044f1f81ad294c6d2988cf504a1e",
33 | "repo": {
34 | "id": 447747682,
35 | "url": "https://api.github.com/repos/heholek/sentry",
36 | "name": "sentry"
37 | }
38 | }
39 | },
40 | {
41 | "url": "https://api.github.com/repos/Nexuscompute/sentry/pulls/226",
42 | "id": 893903127,
43 | "number": 226,
44 | "head": {
45 | "ref": "master",
46 | "sha": "e6f148cf35ff4d7e5da932cd3b1ffe8f40b1af52",
47 | "repo": {
48 | "id": 873328,
49 | "url": "https://api.github.com/repos/getsentry/sentry",
50 | "name": "sentry"
51 | }
52 | },
53 | "base": {
54 | "ref": "master",
55 | "sha": "f05d2d00449b044f1f81ad294c6d2988cf504a1e",
56 | "repo": {
57 | "id": 435303402,
58 | "url": "https://api.github.com/repos/Nexuscompute/sentry",
59 | "name": "sentry"
60 | }
61 | }
62 | },
63 | {
64 | "url": "https://api.github.com/repos/sljeff/sentry/pulls/204",
65 | "id": 887021323,
66 | "number": 204,
67 | "head": {
68 | "ref": "master",
69 | "sha": "e6f148cf35ff4d7e5da932cd3b1ffe8f40b1af52",
70 | "repo": {
71 | "id": 873328,
72 | "url": "https://api.github.com/repos/getsentry/sentry",
73 | "name": "sentry"
74 | }
75 | },
76 | "base": {
77 | "ref": "master",
78 | "sha": "f05d2d00449b044f1f81ad294c6d2988cf504a1e",
79 | "repo": {
80 | "id": 428910254,
81 | "url": "https://api.github.com/repos/sljeff/sentry",
82 | "name": "sentry"
83 | }
84 | }
85 | },
86 | {
87 | "url": "https://api.github.com/repos/NeatNerdPrime/sentry/pulls/1423",
88 | "id": 884824402,
89 | "number": 1423,
90 | "head": {
91 | "ref": "master",
92 | "sha": "e6f148cf35ff4d7e5da932cd3b1ffe8f40b1af52",
93 | "repo": {
94 | "id": 873328,
95 | "url": "https://api.github.com/repos/getsentry/sentry",
96 | "name": "sentry"
97 | }
98 | },
99 | "base": {
100 | "ref": "master",
101 | "sha": "8429cf33623b759a3ff7bddcf13d251b0dab9b8e",
102 | "repo": {
103 | "id": 212712897,
104 | "url": "https://api.github.com/repos/NeatNerdPrime/sentry",
105 | "name": "sentry"
106 | }
107 | }
108 | },
109 | {
110 | "url": "https://api.github.com/repos/littlekign/sentry/pulls/100",
111 | "id": 841827111,
112 | "number": 100,
113 | "head": {
114 | "ref": "master",
115 | "sha": "e6f148cf35ff4d7e5da932cd3b1ffe8f40b1af52",
116 | "repo": {
117 | "id": 873328,
118 | "url": "https://api.github.com/repos/getsentry/sentry",
119 | "name": "sentry"
120 | }
121 | },
122 | "base": {
123 | "ref": "master",
124 | "sha": "b2aa926c73ae5bf70ac538a9794e6a5ad37fd8cd",
125 | "repo": {
126 | "id": 253251945,
127 | "url": "https://api.github.com/repos/littlekign/sentry",
128 | "name": "sentry"
129 | }
130 | }
131 | },
132 | {
133 | "url": "https://api.github.com/repos/httpsgithu/sentry/pulls/22",
134 | "id": 750344111,
135 | "number": 22,
136 | "head": {
137 | "ref": "master",
138 | "sha": "e6f148cf35ff4d7e5da932cd3b1ffe8f40b1af52",
139 | "repo": {
140 | "id": 873328,
141 | "url": "https://api.github.com/repos/getsentry/sentry",
142 | "name": "sentry"
143 | }
144 | },
145 | "base": {
146 | "ref": "master",
147 | "sha": "284fefe45ccc3d21de6e52ce8a480c10b5bef3c1",
148 | "repo": {
149 | "id": 250191602,
150 | "url": "https://api.github.com/repos/httpsgithu/sentry",
151 | "name": "sentry"
152 | }
153 | }
154 | },
155 | {
156 | "url": "https://api.github.com/repos/KingDEV95/sentry/pulls/313",
157 | "id": 750313238,
158 | "number": 313,
159 | "head": {
160 | "ref": "master",
161 | "sha": "e6f148cf35ff4d7e5da932cd3b1ffe8f40b1af52",
162 | "repo": {
163 | "id": 873328,
164 | "url": "https://api.github.com/repos/getsentry/sentry",
165 | "name": "sentry"
166 | }
167 | },
168 | "base": {
169 | "ref": "master",
170 | "sha": "576dbb92b9f5bec8bec4a1759610583e9237a49b",
171 | "repo": {
172 | "id": 256645481,
173 | "url": "https://api.github.com/repos/KingDEV95/sentry",
174 | "name": "sentry"
175 | }
176 | }
177 | },
178 | {
179 | "url": "https://api.github.com/repos/skyplaying/sentry/pulls/57",
180 | "id": 750273455,
181 | "number": 57,
182 | "head": {
183 | "ref": "master",
184 | "sha": "e6f148cf35ff4d7e5da932cd3b1ffe8f40b1af52",
185 | "repo": {
186 | "id": 873328,
187 | "url": "https://api.github.com/repos/getsentry/sentry",
188 | "name": "sentry"
189 | }
190 | },
191 | "base": {
192 | "ref": "master",
193 | "sha": "576dbb92b9f5bec8bec4a1759610583e9237a49b",
194 | "repo": {
195 | "id": 340541581,
196 | "url": "https://api.github.com/repos/skyplaying/sentry",
197 | "name": "sentry"
198 | }
199 | }
200 | },
201 | {
202 | "url": "https://api.github.com/repos/dk-dev/sentry/pulls/335",
203 | "id": 622223860,
204 | "number": 335,
205 | "head": {
206 | "ref": "master",
207 | "sha": "e6f148cf35ff4d7e5da932cd3b1ffe8f40b1af52",
208 | "repo": {
209 | "id": 873328,
210 | "url": "https://api.github.com/repos/getsentry/sentry",
211 | "name": "sentry"
212 | }
213 | },
214 | "base": {
215 | "ref": "master",
216 | "sha": "4645a05b5ad92c6eaf522f88063219d0d784be07",
217 | "repo": {
218 | "id": 13605132,
219 | "url": "https://api.github.com/repos/dk-dev/sentry",
220 | "name": "sentry"
221 | }
222 | }
223 | },
224 | {
225 | "url": "https://api.github.com/repos/waterdrops/sentry/pulls/17",
226 | "id": 620768325,
227 | "number": 17,
228 | "head": {
229 | "ref": "master",
230 | "sha": "e6f148cf35ff4d7e5da932cd3b1ffe8f40b1af52",
231 | "repo": {
232 | "id": 873328,
233 | "url": "https://api.github.com/repos/getsentry/sentry",
234 | "name": "sentry"
235 | }
236 | },
237 | "base": {
238 | "ref": "master",
239 | "sha": "12678a7dfc9f604485ea1847ab631a3d92670338",
240 | "repo": {
241 | "id": 82283669,
242 | "url": "https://api.github.com/repos/waterdrops/sentry",
243 | "name": "sentry"
244 | }
245 | }
246 | },
247 | {
248 | "url": "https://api.github.com/repos/symfu/sentry/pulls/1165",
249 | "id": 582026148,
250 | "number": 1165,
251 | "head": {
252 | "ref": "master",
253 | "sha": "e6f148cf35ff4d7e5da932cd3b1ffe8f40b1af52",
254 | "repo": {
255 | "id": 873328,
256 | "url": "https://api.github.com/repos/getsentry/sentry",
257 | "name": "sentry"
258 | }
259 | },
260 | "base": {
261 | "ref": "master",
262 | "sha": "0902abd3d3528df231fffd3929730495dcf65f79",
263 | "repo": {
264 | "id": 224076470,
265 | "url": "https://api.github.com/repos/symfu/sentry",
266 | "name": "sentry"
267 | }
268 | }
269 | },
270 | {
271 | "url": "https://api.github.com/repos/fakegit/sentry/pulls/1132",
272 | "id": 574585722,
273 | "number": 1132,
274 | "head": {
275 | "ref": "master",
276 | "sha": "e6f148cf35ff4d7e5da932cd3b1ffe8f40b1af52",
277 | "repo": {
278 | "id": 873328,
279 | "url": "https://api.github.com/repos/getsentry/sentry",
280 | "name": "sentry"
281 | }
282 | },
283 | "base": {
284 | "ref": "master",
285 | "sha": "d830eb47b4c60ae45994464e157587b5880cce53",
286 | "repo": {
287 | "id": 220568438,
288 | "url": "https://api.github.com/repos/fakegit/sentry",
289 | "name": "sentry"
290 | }
291 | }
292 | },
293 | {
294 | "url": "https://api.github.com/repos/dgkh1975/sentry/pulls/1",
295 | "id": 568785494,
296 | "number": 1,
297 | "head": {
298 | "ref": "master",
299 | "sha": "e6f148cf35ff4d7e5da932cd3b1ffe8f40b1af52",
300 | "repo": {
301 | "id": 873328,
302 | "url": "https://api.github.com/repos/getsentry/sentry",
303 | "name": "sentry"
304 | }
305 | },
306 | "base": {
307 | "ref": "master",
308 | "sha": "4f1d6dbdfd9ea5cdb785e009af2ef91d8f988dc8",
309 | "repo": {
310 | "id": 333858862,
311 | "url": "https://api.github.com/repos/dgkh1975/sentry",
312 | "name": "sentry"
313 | }
314 | }
315 | },
316 | {
317 | "url": "https://api.github.com/repos/Mu-L/sentry/pulls/48",
318 | "id": 563125460,
319 | "number": 48,
320 | "head": {
321 | "ref": "master",
322 | "sha": "e6f148cf35ff4d7e5da932cd3b1ffe8f40b1af52",
323 | "repo": {
324 | "id": 873328,
325 | "url": "https://api.github.com/repos/getsentry/sentry",
326 | "name": "sentry"
327 | }
328 | },
329 | "base": {
330 | "ref": "master",
331 | "sha": "ebcf4b671702fe9bf01c4921a766593c74e26608",
332 | "repo": {
333 | "id": 295943252,
334 | "url": "https://api.github.com/repos/Mu-L/sentry",
335 | "name": "sentry"
336 | }
337 | }
338 | },
339 | {
340 | "url": "https://api.github.com/repos/Jankyboy/sentry/pulls/1",
341 | "id": 495898142,
342 | "number": 1,
343 | "head": {
344 | "ref": "master",
345 | "sha": "e6f148cf35ff4d7e5da932cd3b1ffe8f40b1af52",
346 | "repo": {
347 | "id": 873328,
348 | "url": "https://api.github.com/repos/getsentry/sentry",
349 | "name": "sentry"
350 | }
351 | },
352 | "base": {
353 | "ref": "master",
354 | "sha": "31f606045ce6c1c96df8869f6ae0e890bec8cbcc",
355 | "repo": {
356 | "id": 300087432,
357 | "url": "https://api.github.com/repos/Jankyboy/sentry",
358 | "name": "sentry"
359 | }
360 | }
361 | },
362 | {
363 | "url": "https://api.github.com/repos/Mattlk13/sentry-1/pulls/243",
364 | "id": 302696163,
365 | "number": 243,
366 | "head": {
367 | "ref": "master",
368 | "sha": "e6f148cf35ff4d7e5da932cd3b1ffe8f40b1af52",
369 | "repo": {
370 | "id": 873328,
371 | "url": "https://api.github.com/repos/getsentry/sentry",
372 | "name": "sentry"
373 | }
374 | },
375 | "base": {
376 | "ref": "master",
377 | "sha": "19b0870916b80250f3cb69277641bfdd03320415",
378 | "repo": {
379 | "id": 81418058,
380 | "url": "https://api.github.com/repos/Mattlk13/sentry-1",
381 | "name": "sentry-1"
382 | }
383 | }
384 | },
385 | {
386 | "url": "https://api.github.com/repos/simmetria/sentry/pulls/1",
387 | "id": 26629667,
388 | "number": 1,
389 | "head": {
390 | "ref": "master",
391 | "sha": "e6f148cf35ff4d7e5da932cd3b1ffe8f40b1af52",
392 | "repo": {
393 | "id": 873328,
394 | "url": "https://api.github.com/repos/getsentry/sentry",
395 | "name": "sentry"
396 | }
397 | },
398 | "base": {
399 | "ref": "master",
400 | "sha": "9731f26adb44847d1c883cca108afc0755cf21cc",
401 | "repo": {
402 | "id": 6701590,
403 | "url": "https://api.github.com/repos/simmetria/sentry",
404 | "name": "sentry"
405 | }
406 | }
407 | }
408 | ],
409 | "created_at": "2022-04-05T18:33:44Z",
410 | "updated_at": "2022-04-05T18:33:47Z",
411 | "actor": {
412 | "login": "JoshFerge",
413 | "id": 1976777,
414 | "node_id": "MDQ6VXNlcjE5NzY3Nzc=",
415 | "avatar_url": "https://avatars.githubusercontent.com/u/1976777?v=4",
416 | "gravatar_id": "",
417 | "url": "https://api.github.com/users/JoshFerge",
418 | "html_url": "https://github.com/JoshFerge",
419 | "followers_url": "https://api.github.com/users/JoshFerge/followers",
420 | "following_url": "https://api.github.com/users/JoshFerge/following{/other_user}",
421 | "gists_url": "https://api.github.com/users/JoshFerge/gists{/gist_id}",
422 | "starred_url": "https://api.github.com/users/JoshFerge/starred{/owner}{/repo}",
423 | "subscriptions_url": "https://api.github.com/users/JoshFerge/subscriptions",
424 | "organizations_url": "https://api.github.com/users/JoshFerge/orgs",
425 | "repos_url": "https://api.github.com/users/JoshFerge/repos",
426 | "events_url": "https://api.github.com/users/JoshFerge/events{/privacy}",
427 | "received_events_url": "https://api.github.com/users/JoshFerge/received_events",
428 | "type": "User",
429 | "site_admin": false
430 | },
431 | "run_attempt": 1,
432 | "run_started_at": "2022-04-05T18:33:44Z",
433 | "triggering_actor": {
434 | "login": "JoshFerge",
435 | "id": 1976777,
436 | "node_id": "MDQ6VXNlcjE5NzY3Nzc=",
437 | "avatar_url": "https://avatars.githubusercontent.com/u/1976777?v=4",
438 | "gravatar_id": "",
439 | "url": "https://api.github.com/users/JoshFerge",
440 | "html_url": "https://github.com/JoshFerge",
441 | "followers_url": "https://api.github.com/users/JoshFerge/followers",
442 | "following_url": "https://api.github.com/users/JoshFerge/following{/other_user}",
443 | "gists_url": "https://api.github.com/users/JoshFerge/gists{/gist_id}",
444 | "starred_url": "https://api.github.com/users/JoshFerge/starred{/owner}{/repo}",
445 | "subscriptions_url": "https://api.github.com/users/JoshFerge/subscriptions",
446 | "organizations_url": "https://api.github.com/users/JoshFerge/orgs",
447 | "repos_url": "https://api.github.com/users/JoshFerge/repos",
448 | "events_url": "https://api.github.com/users/JoshFerge/events{/privacy}",
449 | "received_events_url": "https://api.github.com/users/JoshFerge/received_events",
450 | "type": "User",
451 | "site_admin": false
452 | },
453 | "jobs_url": "https://api.github.com/repos/getsentry/sentry/actions/runs/2098089400/jobs",
454 | "logs_url": "https://api.github.com/repos/getsentry/sentry/actions/runs/2098089400/logs",
455 | "check_suite_url": "https://api.github.com/repos/getsentry/sentry/check-suites/5943587195",
456 | "artifacts_url": "https://api.github.com/repos/getsentry/sentry/actions/runs/2098089400/artifacts",
457 | "cancel_url": "https://api.github.com/repos/getsentry/sentry/actions/runs/2098089400/cancel",
458 | "rerun_url": "https://api.github.com/repos/getsentry/sentry/actions/runs/2098089400/rerun",
459 | "previous_attempt_url": null,
460 | "workflow_url": "https://api.github.com/repos/getsentry/sentry/actions/workflows/3020796",
461 | "head_commit": {
462 | "id": "e6f148cf35ff4d7e5da932cd3b1ffe8f40b1af52",
463 | "tree_id": "d02c6f802c073da7d1f0605ddabe7980847a7798",
464 | "message": "chore(eslint): Don't specify the .eslintrc.* file in vscode workspace settings (#33308)\n\nI saw an error in the `vscode-eslint` plugin:\r\n```\r\nError: Invalid Options:\r\n- Unknown options: configFile\r\n- 'configFile' has been removed. Please use the 'overrideConfigFile' option instead.\r\n```\r\n\r\nThe plugin can find our eslintrc file without the explicit `configFile` field because the file sticks to the normal naming convention.",
465 | "timestamp": "2022-04-05T18:26:04Z",
466 | "author": {
467 | "name": "Ryan Albrecht",
468 | "email": "ryan.albrecht@sentry.io"
469 | },
470 | "committer": {
471 | "name": "GitHub",
472 | "email": "noreply@github.com"
473 | }
474 | },
475 | "repository": {
476 | "id": 873328,
477 | "node_id": "MDEwOlJlcG9zaXRvcnk4NzMzMjg=",
478 | "name": "sentry",
479 | "full_name": "getsentry/sentry",
480 | "private": false,
481 | "owner": {
482 | "login": "getsentry",
483 | "id": 1396951,
484 | "node_id": "MDEyOk9yZ2FuaXphdGlvbjEzOTY5NTE=",
485 | "avatar_url": "https://avatars.githubusercontent.com/u/1396951?v=4",
486 | "gravatar_id": "",
487 | "url": "https://api.github.com/users/getsentry",
488 | "html_url": "https://github.com/getsentry",
489 | "followers_url": "https://api.github.com/users/getsentry/followers",
490 | "following_url": "https://api.github.com/users/getsentry/following{/other_user}",
491 | "gists_url": "https://api.github.com/users/getsentry/gists{/gist_id}",
492 | "starred_url": "https://api.github.com/users/getsentry/starred{/owner}{/repo}",
493 | "subscriptions_url": "https://api.github.com/users/getsentry/subscriptions",
494 | "organizations_url": "https://api.github.com/users/getsentry/orgs",
495 | "repos_url": "https://api.github.com/users/getsentry/repos",
496 | "events_url": "https://api.github.com/users/getsentry/events{/privacy}",
497 | "received_events_url": "https://api.github.com/users/getsentry/received_events",
498 | "type": "Organization",
499 | "site_admin": false
500 | },
501 | "html_url": "https://github.com/getsentry/sentry",
502 | "description": "Sentry is cross-platform application monitoring, with a focus on error reporting.",
503 | "fork": false,
504 | "url": "https://api.github.com/repos/getsentry/sentry",
505 | "forks_url": "https://api.github.com/repos/getsentry/sentry/forks",
506 | "keys_url": "https://api.github.com/repos/getsentry/sentry/keys{/key_id}",
507 | "collaborators_url": "https://api.github.com/repos/getsentry/sentry/collaborators{/collaborator}",
508 | "teams_url": "https://api.github.com/repos/getsentry/sentry/teams",
509 | "hooks_url": "https://api.github.com/repos/getsentry/sentry/hooks",
510 | "issue_events_url": "https://api.github.com/repos/getsentry/sentry/issues/events{/number}",
511 | "events_url": "https://api.github.com/repos/getsentry/sentry/events",
512 | "assignees_url": "https://api.github.com/repos/getsentry/sentry/assignees{/user}",
513 | "branches_url": "https://api.github.com/repos/getsentry/sentry/branches{/branch}",
514 | "tags_url": "https://api.github.com/repos/getsentry/sentry/tags",
515 | "blobs_url": "https://api.github.com/repos/getsentry/sentry/git/blobs{/sha}",
516 | "git_tags_url": "https://api.github.com/repos/getsentry/sentry/git/tags{/sha}",
517 | "git_refs_url": "https://api.github.com/repos/getsentry/sentry/git/refs{/sha}",
518 | "trees_url": "https://api.github.com/repos/getsentry/sentry/git/trees{/sha}",
519 | "statuses_url": "https://api.github.com/repos/getsentry/sentry/statuses/{sha}",
520 | "languages_url": "https://api.github.com/repos/getsentry/sentry/languages",
521 | "stargazers_url": "https://api.github.com/repos/getsentry/sentry/stargazers",
522 | "contributors_url": "https://api.github.com/repos/getsentry/sentry/contributors",
523 | "subscribers_url": "https://api.github.com/repos/getsentry/sentry/subscribers",
524 | "subscription_url": "https://api.github.com/repos/getsentry/sentry/subscription",
525 | "commits_url": "https://api.github.com/repos/getsentry/sentry/commits{/sha}",
526 | "git_commits_url": "https://api.github.com/repos/getsentry/sentry/git/commits{/sha}",
527 | "comments_url": "https://api.github.com/repos/getsentry/sentry/comments{/number}",
528 | "issue_comment_url": "https://api.github.com/repos/getsentry/sentry/issues/comments{/number}",
529 | "contents_url": "https://api.github.com/repos/getsentry/sentry/contents/{+path}",
530 | "compare_url": "https://api.github.com/repos/getsentry/sentry/compare/{base}...{head}",
531 | "merges_url": "https://api.github.com/repos/getsentry/sentry/merges",
532 | "archive_url": "https://api.github.com/repos/getsentry/sentry/{archive_format}{/ref}",
533 | "downloads_url": "https://api.github.com/repos/getsentry/sentry/downloads",
534 | "issues_url": "https://api.github.com/repos/getsentry/sentry/issues{/number}",
535 | "pulls_url": "https://api.github.com/repos/getsentry/sentry/pulls{/number}",
536 | "milestones_url": "https://api.github.com/repos/getsentry/sentry/milestones{/number}",
537 | "notifications_url": "https://api.github.com/repos/getsentry/sentry/notifications{?since,all,participating}",
538 | "labels_url": "https://api.github.com/repos/getsentry/sentry/labels{/name}",
539 | "releases_url": "https://api.github.com/repos/getsentry/sentry/releases{/id}",
540 | "deployments_url": "https://api.github.com/repos/getsentry/sentry/deployments"
541 | },
542 | "head_repository": {
543 | "id": 873328,
544 | "node_id": "MDEwOlJlcG9zaXRvcnk4NzMzMjg=",
545 | "name": "sentry",
546 | "full_name": "getsentry/sentry",
547 | "private": false,
548 | "owner": {
549 | "login": "getsentry",
550 | "id": 1396951,
551 | "node_id": "MDEyOk9yZ2FuaXphdGlvbjEzOTY5NTE=",
552 | "avatar_url": "https://avatars.githubusercontent.com/u/1396951?v=4",
553 | "gravatar_id": "",
554 | "url": "https://api.github.com/users/getsentry",
555 | "html_url": "https://github.com/getsentry",
556 | "followers_url": "https://api.github.com/users/getsentry/followers",
557 | "following_url": "https://api.github.com/users/getsentry/following{/other_user}",
558 | "gists_url": "https://api.github.com/users/getsentry/gists{/gist_id}",
559 | "starred_url": "https://api.github.com/users/getsentry/starred{/owner}{/repo}",
560 | "subscriptions_url": "https://api.github.com/users/getsentry/subscriptions",
561 | "organizations_url": "https://api.github.com/users/getsentry/orgs",
562 | "repos_url": "https://api.github.com/users/getsentry/repos",
563 | "events_url": "https://api.github.com/users/getsentry/events{/privacy}",
564 | "received_events_url": "https://api.github.com/users/getsentry/received_events",
565 | "type": "Organization",
566 | "site_admin": false
567 | },
568 | "html_url": "https://github.com/getsentry/sentry",
569 | "description": "Sentry is cross-platform application monitoring, with a focus on error reporting.",
570 | "fork": false,
571 | "url": "https://api.github.com/repos/getsentry/sentry",
572 | "forks_url": "https://api.github.com/repos/getsentry/sentry/forks",
573 | "keys_url": "https://api.github.com/repos/getsentry/sentry/keys{/key_id}",
574 | "collaborators_url": "https://api.github.com/repos/getsentry/sentry/collaborators{/collaborator}",
575 | "teams_url": "https://api.github.com/repos/getsentry/sentry/teams",
576 | "hooks_url": "https://api.github.com/repos/getsentry/sentry/hooks",
577 | "issue_events_url": "https://api.github.com/repos/getsentry/sentry/issues/events{/number}",
578 | "events_url": "https://api.github.com/repos/getsentry/sentry/events",
579 | "assignees_url": "https://api.github.com/repos/getsentry/sentry/assignees{/user}",
580 | "branches_url": "https://api.github.com/repos/getsentry/sentry/branches{/branch}",
581 | "tags_url": "https://api.github.com/repos/getsentry/sentry/tags",
582 | "blobs_url": "https://api.github.com/repos/getsentry/sentry/git/blobs{/sha}",
583 | "git_tags_url": "https://api.github.com/repos/getsentry/sentry/git/tags{/sha}",
584 | "git_refs_url": "https://api.github.com/repos/getsentry/sentry/git/refs{/sha}",
585 | "trees_url": "https://api.github.com/repos/getsentry/sentry/git/trees{/sha}",
586 | "statuses_url": "https://api.github.com/repos/getsentry/sentry/statuses/{sha}",
587 | "languages_url": "https://api.github.com/repos/getsentry/sentry/languages",
588 | "stargazers_url": "https://api.github.com/repos/getsentry/sentry/stargazers",
589 | "contributors_url": "https://api.github.com/repos/getsentry/sentry/contributors",
590 | "subscribers_url": "https://api.github.com/repos/getsentry/sentry/subscribers",
591 | "subscription_url": "https://api.github.com/repos/getsentry/sentry/subscription",
592 | "commits_url": "https://api.github.com/repos/getsentry/sentry/commits{/sha}",
593 | "git_commits_url": "https://api.github.com/repos/getsentry/sentry/git/commits{/sha}",
594 | "comments_url": "https://api.github.com/repos/getsentry/sentry/comments{/number}",
595 | "issue_comment_url": "https://api.github.com/repos/getsentry/sentry/issues/comments{/number}",
596 | "contents_url": "https://api.github.com/repos/getsentry/sentry/contents/{+path}",
597 | "compare_url": "https://api.github.com/repos/getsentry/sentry/compare/{base}...{head}",
598 | "merges_url": "https://api.github.com/repos/getsentry/sentry/merges",
599 | "archive_url": "https://api.github.com/repos/getsentry/sentry/{archive_format}{/ref}",
600 | "downloads_url": "https://api.github.com/repos/getsentry/sentry/downloads",
601 | "issues_url": "https://api.github.com/repos/getsentry/sentry/issues{/number}",
602 | "pulls_url": "https://api.github.com/repos/getsentry/sentry/pulls{/number}",
603 | "milestones_url": "https://api.github.com/repos/getsentry/sentry/milestones{/number}",
604 | "notifications_url": "https://api.github.com/repos/getsentry/sentry/notifications{?since,all,participating}",
605 | "labels_url": "https://api.github.com/repos/getsentry/sentry/labels{/name}",
606 | "releases_url": "https://api.github.com/repos/getsentry/sentry/releases{/id}",
607 | "deployments_url": "https://api.github.com/repos/getsentry/sentry/deployments"
608 | }
609 | }
610 |
--------------------------------------------------------------------------------
/tests/fixtures/webhook_event.json:
--------------------------------------------------------------------------------
1 | {
2 | "headers": {
3 | "Request URL": "https://github-actions-app-dwunkkvj6a-uc.a.run.app/",
4 | "Request method": "POST",
5 | "Accept": "*/*",
6 | "content-type": "application/json",
7 | "User-Agent": "GitHub-Hookshot/d0ceae1",
8 | "X-GitHub-Delivery": "99ba54a0-d21e-11ec-8158-e3ba791db828",
9 | "X-GitHub-Event": "workflow_job",
10 | "X-GitHub-Hook-ID": 349900435,
11 | "X-GitHub-Hook-Installation-Target-ID": 873328,
12 | "X-GitHub-Hook-Installation-Target-Type": "repository",
13 | "X-Hub-Signature": "sha1=aaaeb75e9ef80af1a95ffdf4b4b8b2a69ff8ff69",
14 | "X-Hub-Signature-256": "sha256=d9259f51d3b64e7fe0cbe09d9b08b8ee763170d3521fecc35fd8b453be8cf6a5"
15 | },
16 | "payload": {
17 | "action": "completed",
18 | "workflow_job": {
19 | "id": 3283437728,
20 | "run_id": 1113865931,
21 | "run_attempt": 2,
22 | "run_url": "https://api.github.com/repos/armenzg/sentry-hackweek/actions/runs/1113865931",
23 | "node_id": "MDg6Q2hlY2tSdW4zMjgzNDM3NzI4",
24 | "head_sha": "2238b40c4b398874db76239c1d60325af9037563",
25 | "url": "https://api.github.com/repos/armenzg/sentry-hackweek/actions/jobs/3283437728",
26 | "html_url": "https://github.com/armenzg/sentry-hackweek/runs/3283437728",
27 | "status": "completed",
28 | "conclusion": "success",
29 | "started_at": "2021-08-09T18:12:37Z",
30 | "completed_at": "2021-08-09T18:12:41Z",
31 | "name": "salutation",
32 | "steps": [
33 | {
34 | "name": "Set up job",
35 | "status": "completed",
36 | "conclusion": "success",
37 | "number": 1,
38 | "started_at": "2021-08-09T18:12:37.000Z",
39 | "completed_at": "2021-08-09T18:12:40.000Z"
40 | },
41 | {
42 | "name": "Checkout",
43 | "status": "completed",
44 | "conclusion": "success",
45 | "number": 2,
46 | "started_at": "2021-08-09T18:12:40.000Z",
47 | "completed_at": "2021-08-09T18:12:40.000Z"
48 | },
49 | {
50 | "name": "Welcome",
51 | "status": "completed",
52 | "conclusion": "success",
53 | "number": 3,
54 | "started_at": "2021-08-09T18:12:40.000Z",
55 | "completed_at": "2021-08-09T18:12:40.000Z"
56 | },
57 | {
58 | "name": "Post Checkout",
59 | "status": "completed",
60 | "conclusion": "success",
61 | "number": 6,
62 | "started_at": "2021-08-09T18:12:40.000Z",
63 | "completed_at": "2021-08-09T18:12:41.000Z"
64 | },
65 | {
66 | "name": "Complete job",
67 | "status": "completed",
68 | "conclusion": "success",
69 | "number": 7,
70 | "started_at": "2021-08-09T18:12:41.000Z",
71 | "completed_at": "2021-08-09T18:12:41.000Z"
72 | }
73 | ],
74 | "check_run_url": "https://api.github.com/repos/armenzg/sentry-hackweek/check-runs/3283437728",
75 | "labels": ["ubuntu-latest"]
76 | },
77 | "repository": {
78 | "id": 394302061,
79 | "node_id": "MDEwOlJlcG9zaXRvcnkzOTQzMDIwNjE=",
80 | "name": "sentry-hackweek",
81 | "full_name": "armenzg/sentry-hackweek",
82 | "private": false,
83 | "owner": {
84 | "login": "armenzg",
85 | "id": 44410,
86 | "node_id": "MDQ6VXNlcjQ0NDEw",
87 | "avatar_url": "https://avatars.githubusercontent.com/u/44410?v=4",
88 | "gravatar_id": "",
89 | "url": "https://api.github.com/users/armenzg",
90 | "html_url": "https://github.com/armenzg",
91 | "followers_url": "https://api.github.com/users/armenzg/followers",
92 | "following_url": "https://api.github.com/users/armenzg/following{/other_user}",
93 | "gists_url": "https://api.github.com/users/armenzg/gists{/gist_id}",
94 | "starred_url": "https://api.github.com/users/armenzg/starred{/owner}{/repo}",
95 | "subscriptions_url": "https://api.github.com/users/armenzg/subscriptions",
96 | "organizations_url": "https://api.github.com/users/armenzg/orgs",
97 | "repos_url": "https://api.github.com/users/armenzg/repos",
98 | "events_url": "https://api.github.com/users/armenzg/events{/privacy}",
99 | "received_events_url": "https://api.github.com/users/armenzg/received_events",
100 | "type": "User",
101 | "site_admin": false
102 | },
103 | "html_url": "https://github.com/armenzg/sentry-hackweek",
104 | "description": null,
105 | "fork": false,
106 | "url": "https://api.github.com/repos/armenzg/sentry-hackweek",
107 | "forks_url": "https://api.github.com/repos/armenzg/sentry-hackweek/forks",
108 | "keys_url": "https://api.github.com/repos/armenzg/sentry-hackweek/keys{/key_id}",
109 | "collaborators_url": "https://api.github.com/repos/armenzg/sentry-hackweek/collaborators{/collaborator}",
110 | "teams_url": "https://api.github.com/repos/armenzg/sentry-hackweek/teams",
111 | "hooks_url": "https://api.github.com/repos/armenzg/sentry-hackweek/hooks",
112 | "issue_events_url": "https://api.github.com/repos/armenzg/sentry-hackweek/issues/events{/number}",
113 | "events_url": "https://api.github.com/repos/armenzg/sentry-hackweek/events",
114 | "assignees_url": "https://api.github.com/repos/armenzg/sentry-hackweek/assignees{/user}",
115 | "branches_url": "https://api.github.com/repos/armenzg/sentry-hackweek/branches{/branch}",
116 | "tags_url": "https://api.github.com/repos/armenzg/sentry-hackweek/tags",
117 | "blobs_url": "https://api.github.com/repos/armenzg/sentry-hackweek/git/blobs{/sha}",
118 | "git_tags_url": "https://api.github.com/repos/armenzg/sentry-hackweek/git/tags{/sha}",
119 | "git_refs_url": "https://api.github.com/repos/armenzg/sentry-hackweek/git/refs{/sha}",
120 | "trees_url": "https://api.github.com/repos/armenzg/sentry-hackweek/git/trees{/sha}",
121 | "statuses_url": "https://api.github.com/repos/armenzg/sentry-hackweek/statuses/{sha}",
122 | "languages_url": "https://api.github.com/repos/armenzg/sentry-hackweek/languages",
123 | "stargazers_url": "https://api.github.com/repos/armenzg/sentry-hackweek/stargazers",
124 | "contributors_url": "https://api.github.com/repos/armenzg/sentry-hackweek/contributors",
125 | "subscribers_url": "https://api.github.com/repos/armenzg/sentry-hackweek/subscribers",
126 | "subscription_url": "https://api.github.com/repos/armenzg/sentry-hackweek/subscription",
127 | "commits_url": "https://api.github.com/repos/armenzg/sentry-hackweek/commits{/sha}",
128 | "git_commits_url": "https://api.github.com/repos/armenzg/sentry-hackweek/git/commits{/sha}",
129 | "comments_url": "https://api.github.com/repos/armenzg/sentry-hackweek/comments{/number}",
130 | "issue_comment_url": "https://api.github.com/repos/armenzg/sentry-hackweek/issues/comments{/number}",
131 | "contents_url": "https://api.github.com/repos/armenzg/sentry-hackweek/contents/{+path}",
132 | "compare_url": "https://api.github.com/repos/armenzg/sentry-hackweek/compare/{base}...{head}",
133 | "merges_url": "https://api.github.com/repos/armenzg/sentry-hackweek/merges",
134 | "archive_url": "https://api.github.com/repos/armenzg/sentry-hackweek/{archive_format}{/ref}",
135 | "downloads_url": "https://api.github.com/repos/armenzg/sentry-hackweek/downloads",
136 | "issues_url": "https://api.github.com/repos/armenzg/sentry-hackweek/issues{/number}",
137 | "pulls_url": "https://api.github.com/repos/armenzg/sentry-hackweek/pulls{/number}",
138 | "milestones_url": "https://api.github.com/repos/armenzg/sentry-hackweek/milestones{/number}",
139 | "notifications_url": "https://api.github.com/repos/armenzg/sentry-hackweek/notifications{?since,all,participating}",
140 | "labels_url": "https://api.github.com/repos/armenzg/sentry-hackweek/labels{/name}",
141 | "releases_url": "https://api.github.com/repos/armenzg/sentry-hackweek/releases{/id}",
142 | "deployments_url": "https://api.github.com/repos/armenzg/sentry-hackweek/deployments",
143 | "created_at": "2021-08-09T13:28:32Z",
144 | "updated_at": "2021-08-09T13:28:35Z",
145 | "pushed_at": "2021-08-09T18:12:28Z",
146 | "git_url": "git://github.com/armenzg/sentry-hackweek.git",
147 | "ssh_url": "git@github.com:armenzg/sentry-hackweek.git",
148 | "clone_url": "https://github.com/armenzg/sentry-hackweek.git",
149 | "svn_url": "https://github.com/armenzg/sentry-hackweek",
150 | "homepage": null,
151 | "size": 2,
152 | "stargazers_count": 0,
153 | "watchers_count": 0,
154 | "language": null,
155 | "has_issues": true,
156 | "has_projects": true,
157 | "has_downloads": true,
158 | "has_wiki": true,
159 | "has_pages": false,
160 | "forks_count": 0,
161 | "mirror_url": null,
162 | "archived": false,
163 | "disabled": false,
164 | "open_issues_count": 2,
165 | "license": null,
166 | "forks": 0,
167 | "open_issues": 2,
168 | "watchers": 0,
169 | "default_branch": "main"
170 | },
171 | "sender": {
172 | "login": "armenzg",
173 | "id": 44410,
174 | "node_id": "MDQ6VXNlcjQ0NDEw",
175 | "avatar_url": "https://avatars.githubusercontent.com/u/44410?v=4",
176 | "gravatar_id": "",
177 | "url": "https://api.github.com/users/armenzg",
178 | "html_url": "https://github.com/armenzg",
179 | "followers_url": "https://api.github.com/users/armenzg/followers",
180 | "following_url": "https://api.github.com/users/armenzg/following{/other_user}",
181 | "gists_url": "https://api.github.com/users/armenzg/gists{/gist_id}",
182 | "starred_url": "https://api.github.com/users/armenzg/starred{/owner}{/repo}",
183 | "subscriptions_url": "https://api.github.com/users/armenzg/subscriptions",
184 | "organizations_url": "https://api.github.com/users/armenzg/orgs",
185 | "repos_url": "https://api.github.com/users/armenzg/repos",
186 | "events_url": "https://api.github.com/users/armenzg/events{/privacy}",
187 | "received_events_url": "https://api.github.com/users/armenzg/received_events",
188 | "type": "User",
189 | "site_admin": false
190 | }
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/tests/test_github_sdk.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import sys
4 | from datetime import datetime
5 | from unittest.mock import patch
6 |
7 | import pytest
8 | import requests
9 | import responses
10 | from freezegun import freeze_time
11 | from requests import HTTPError
12 | from sentry_sdk.utils import format_timestamp
13 |
14 | from src.github_sdk import GithubClient
15 |
16 | DSN = "https://foo@random.ingest.sentry.io/bar"
17 | TOKEN = "irrelevant"
18 |
19 | # Different versions of Python represent certain errors differently
20 | prepend = ""
21 | if sys.version_info[:2] >= (3, 10):
22 | prepend = "GithubClient."
23 |
24 |
25 | def test_job_without_steps(skipped_workflow):
26 | sdk = GithubClient(dsn=DSN, token=TOKEN)
27 | assert sdk.send_trace(skipped_workflow) == None
28 |
29 |
30 | def test_initialize_without_setting_dsn():
31 | with pytest.raises(TypeError) as excinfo:
32 | GithubClient()
33 | (msg,) = excinfo.value.args
34 | assert (
35 | msg
36 | == f"{prepend}__init__() missing 2 required positional arguments: 'token' and 'dsn'"
37 | )
38 |
39 |
40 | def test_initialize_without_setting_token():
41 | with pytest.raises(TypeError) as excinfo:
42 | GithubClient(dsn=DSN)
43 | (msg,) = excinfo.value.args
44 | assert msg == f"{prepend}__init__() missing 1 required positional argument: 'token'"
45 |
46 |
47 | @responses.activate
48 | def test_ensure_raise_error_on_github_api_failure():
49 | """We want to delegate to the app using the SDK to handle the error."""
50 | url = "https://api.github.com/repos/getsentry/sentry/actions/runs/2104746951"
51 | responses.get(url, json="{reason: Internal Server Error}", status=500)
52 | with pytest.raises(HTTPError) as excinfo:
53 | client = GithubClient(dsn=DSN, token=TOKEN)
54 | client._fetch_github(url)
55 | (msg,) = excinfo.value.args
56 | assert (
57 | msg
58 | == "500 Server Error: Internal Server Error for url: https://api.github.com/repos/getsentry/sentry/actions/runs/2104746951"
59 | )
60 |
61 |
62 | @freeze_time()
63 | @responses.activate
64 | @patch("src.github_sdk.get_uuid")
65 | def test_trace_generation(
66 | mock_get_uuid,
67 | jobA_job,
68 | jobA_runs,
69 | jobA_workflow,
70 | jobA_trace,
71 | uuid_list,
72 | ):
73 | mock_get_uuid.side_effect = uuid_list
74 | responses.get(
75 | "https://api.github.com/repos/getsentry/sentry/actions/runs/2104746951",
76 | json=jobA_runs,
77 | )
78 | responses.get(
79 | "https://api.github.com/repos/getsentry/sentry/actions/workflows/1174556",
80 | json=jobA_workflow,
81 | )
82 | client = GithubClient(dsn=DSN, token=TOKEN)
83 | assert client._generate_trace(jobA_job) == jobA_trace
84 |
85 |
86 | @freeze_time()
87 | @responses.activate
88 | @patch("src.github_sdk.get_uuid")
89 | def test_send_trace(
90 | mock_get_uuid,
91 | jobA_job,
92 | jobA_runs,
93 | jobA_workflow,
94 | uuid_list,
95 | ):
96 | mock_get_uuid.side_effect = uuid_list
97 | responses.get(
98 | "https://api.github.com/repos/getsentry/sentry/actions/runs/2104746951",
99 | json=jobA_runs,
100 | )
101 | responses.get(
102 | "https://api.github.com/repos/getsentry/sentry/actions/workflows/1174556",
103 | json=jobA_workflow,
104 | )
105 |
106 | responses.post("https://foo@random.ingest.sentry.io/api/bar/envelope/")
107 |
108 | client = GithubClient(dsn=DSN, token=TOKEN)
109 | resp = client.send_trace(jobA_job)
110 | # This cannot happen in a fixture, otherwise, there will be a tiny bit of a clock drift
111 | now = datetime.utcnow()
112 |
113 | envelope_headers = {
114 | "User-Agent": f"python-requests/{requests.__version__}",
115 | "Accept-Encoding": "gzip, deflate",
116 | "Accept": "*/*",
117 | "Connection": "keep-alive",
118 | "event_id": "a401d83c7ec0495f82a8da8d9a389f5b",
119 | "sent_at": format_timestamp(now),
120 | "Content-Type": "application/x-sentry-envelope",
121 | "Content-Encoding": "gzip",
122 | "X-Sentry-Auth": f"Sentry sentry_key=foo,sentry_client=gha-sentry/0.0.1,sentry_timestamp={now},sentry_version=7",
123 | "Content-Length": "693",
124 | }
125 |
126 | for k, v in resp.request.headers.items():
127 | assert envelope_headers[k] == v
128 |
129 | # XXX: We will deal with this another time
130 | # assert (
131 | # resp.request.body
132 | # == b"\x1f\x8b\x08\x00\xf1\x16}b\x02\xff\xb5TM\x8f\xd30\x10\xbd\xef\xaf\x88|\x02\xa9m\x1c\xc7\x89\x93H\x08\xd0\x8a;\x12\x9c@\xa8\x9a\xd8\xe3&\xbb\xf9\"vX\xaa\xaa\xff\x1d{\xdb\xee\x97\x96n\xcb\x8aS\xd3\x99\xf1\xf8\xbd7o\xbc\xd9^l\x88]\x0fH\nbG\xe8\x0cH[\xf7\x1d\x99\x11\xd9w\x16;\xbb\xdc'a\x18\x9aZ\x82O\x86W\xe6\xb6\xa2\xc1ne+RD\x19\xa7\xbe\r\xfe\xf2\xf5\xb5r\xd5\t \x139H\x95g\x8c\xcbRC\x96P\xceh\x14K]\xea\xac\x14\xee\xf4\xb3\x97>\xfcW\x10=\xdebP\x81EcM\xf0\x86\xbe=\xe0\xfam\r)6\xbe\\\xa2\xff0\x03t\xbb\x9b\x81\xd3He\xb1\x14()\xcf\x13\xbdk*q\x97\xcd\xa2\x92\x96\x08)\xa0\xd2<\x8b\x04\x08\xaeb\xa6\x04Ms\x9a)\xcd\x1e\xe1r\xadgD\x81\x05\x7f\xc3U_\xbahe\xed`\x8a0\\\xd5\xb6\x9a\xca\x85\xec\xdbp\x85\xd68\xde\xe3:\xdc\xff\x8cSg\xc2$KD\xc2\x04O\xe8{Y\xa1\xbc^\x9a\xa9\xb6\xb8\xd4\xbd\x9c\xcc;;N\xbe\xf50\x9e\xd8q\x98\x9a&\x8c\xe3\x98\x0b\xb2\x9d\x91~\xf8\x9b4\n\x8d\x1c\xeb\xe1\x98z\xc6\x82\x9d\x9cv\xa4\xbf&[\xd7l28zz\x1d\xb4\x9e\xf5\xc7\xaaE\x15|\xb2\xa8\xd7\xae\x18[\xa8\x1b\xaf\xa9\x8f.\xd0G#\xf6a\xe5\xa3\x1e\xa8\x07\xe3\xfa\x8d\xce#u\xeb\xee\x80\xd6#c\x94\xb19\xe5s\x9a~\x8d\xf2\"aE$\xbeyY\x9f/a\xb4\xa0I\x11\xefJ`e\xf6R/\xefp\x9aIJ4\xc6\xa5K\xe7\rY\x1d\xe0\x84\xa0\xd4\xdc\xa1\xae\xbb\xd5\xbc\x81\xb5c\xe1\xad\xd1\xb6\xb5\xf5\xd4U.R\x16e\x90s\x95\x01\x95\x8aQ\xe7\x88(B\x10\xaa\xc44\x93\x89\x88Qs\xe5\xce\x8c8\xf4\xee\xc4S\xcd}f\xea\x96`-\xb6\x83k\x19\xcd\xc8M?^\xeb\xa6\xbf\xf1\x08\x1c\xa6\xc1:8\xb8X\xb7\x8d\x1f\xa5\x9b\xd0r\xc4\x9f\x93\xe3H\x8a\xdbQyq\x9c+\x1d\x87\xef\x9b\xdd\xcc\xbe\xa0\r\xa6!\xf0N\x9a\x1d\x04\x7f\x14\x1b`\xf4\x1bt\xd4\xcc\xf7I*X\xaaK\x0e,\xe6\x11\x17B\x92\xd3\xa6\x91.(\xa5G&\xb2+c\xf4\xae\xec\x8c\xed\xd9\xce\xf6T?;=\x82U%\xc7E\xdd?\xf0\xf3n\xb3\xe7\x95m\x9b\xb9\xed\xe7u\x0b+,\x1a\xf0\x06\xbd\x97\xe4\x9f\xce\x9e'\x1d\x08!RT\x11S\x00\x9c\xe7\xe5i\xd2=\xd0\xe4XY\x92\xbdN\xba\xde\xd8\xe0\xd2\xbf\x19\xfdd\x83;7\x1e\xc4y>{\x1e\xfd\x14\x9da\xb4\x8e\x05\xd39\xcf3x\x89\xfeaI_\xa0\xbf/K^E\xff\xb2o\x87\xc6=5\x8f\xd7\xe4I\xf4<\xba\x8e\xa2\xdbWT\x98\xe8\x88\xbb\xa7\xe1D\xba\xc9\xff\xa4\xfbc{\xf1\x07Hk>,{\x07\x00\x00"
133 | # )
134 |
--------------------------------------------------------------------------------
/tests/test_sentry_config_file.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from unittest import TestCase
4 |
5 | import responses
6 |
7 | from src.sentry_config import fetch_dsn_for_github_org
8 | from src.sentry_config import SENTRY_CONFIG_API_URL as api_url
9 |
10 | expected_dsn = (
11 | "https://73805ee0a679438d909bb0e6e05fb97f@o510822.ingest.sentry.io/6627507"
12 | )
13 | sentry_config_file_meta = {
14 | "name": "sentry_config.ini",
15 | "path": "sentry_config.ini",
16 | "sha": "21a49641b349f82af39b3ef5f54613a556e60fcc",
17 | "size": 619,
18 | "url": "https://api.github.com/repos/armenzg/.sentry/contents/sentry_config.ini?ref=main",
19 | "html_url": "https://github.com/armenzg/.sentry/blob/main/sentry_config.ini",
20 | "git_url": "https://api.github.com/repos/armenzg/.sentry/git/blobs/21a49641b349f82af39b3ef5f54613a556e60fcc",
21 | "download_url": "https://raw.githubusercontent.com/armenzg/.sentry/main/sentry_config.ini?token=A2NWFUXUKJYCKJQXPAUSHULC6KTXLAVPNFXHG5DBNRWGC5DJN5XF62LEZYA2YZLLWFUW443UMFWGYYLUNFXW4X3UPFYGLN2JNZ2GKZ3SMF2GS33OJFXHG5DBNRWGC5DJN5XA",
22 | "type": "file",
23 | "content": "OyBUaGlzIGZpbGUgbmVlZHMgdG8gYmUgcGxhY2VkIHdpdGhpbiBhIHByaXZh\ndGUgcmVwb3NpdG9yeSBuYW1lZCAuc2VudHJ5IChpdCBjYW4gYWxzbyBiZSBt\nYWRlIHB1YmxpYykKOyBUaGlzIGZpbGUgZm9yIG5vdyB3aWxsIGNvbmZpZ3Vy\nZSB0aGUgc2VudHJ5LWdpdGh1Yi1hY3Rpb25zLWFwcAo7IGJ1dCBpdCBtYXli\nZSB1c2VkIGJ5IGZ1dHVyZSBTZW50cnkgc2VydmljZXMKCjsgVGhpcyBjb25m\naWd1cmVzIGh0dHBzOi8vZ2l0aHViLmNvbS9nZXRzZW50cnkvc2VudHJ5LWdp\ndGh1Yi1hY3Rpb25zLWFwcAo7IEZvciBub3cgaXQgaXMgb25seSB1c2VkIHRv\nIGNvbmZpZ3VyZSB0aGUgcHJvamVjdCB5b3Ugd2FudCB5b3VyIG9yZydzIENJ\nCjsgdG8gcG9zdCB0cmFuc2FjdGlvbnMgdG8KW3NlbnRyeS1naXRodWItYWN0\naW9ucy1hcHBdCjsgVGhpcyBwcm9qZWN0IGlzIHVuZGVyIHNlbnRyeS1lY29z\neXN0ZW0gb3JnCjsgaHR0cHM6Ly9zZW50cnkuaW8vb3JnYW5pemF0aW9ucy9z\nZW50cnktZWNvc3lzdGVtL3BlcmZvcm1hbmNlLz9wcm9qZWN0PTY2Mjc1MDcK\nZHNuID0gaHR0cHM6Ly83MzgwNWVlMGE2Nzk0MzhkOTA5YmIwZTZlMDVmYjk3\nZkBvNTEwODIyLmluZ2VzdC5zZW50cnkuaW8vNjYyNzUwNw==\n",
24 | "encoding": "base64",
25 | "_links": {
26 | "self": "https://api.github.com/repos/armenzg/.sentry/contents/sentry_config.ini?ref=main",
27 | "git": "https://api.github.com/repos/armenzg/.sentry/git/blobs/21a49641b349f82af39b3ef5f54613a556e60fcc",
28 | "html": "https://github.com/armenzg/.sentry/blob/main/sentry_config.ini",
29 | },
30 | }
31 | org = "armenzg"
32 | token = "foo_token"
33 |
34 |
35 | class TestSentryConfigCase(TestCase):
36 | def setUp(self) -> None:
37 | self.api_url = api_url.replace("{owner}", org)
38 | responses.add(
39 | method="GET",
40 | url=self.api_url,
41 | json=sentry_config_file_meta,
42 | status=200,
43 | )
44 | return super().setUp()
45 |
46 | @responses.activate
47 | def test_fetch_parse_sentry_config_file(self) -> None:
48 | assert fetch_dsn_for_github_org(org, token) == expected_dsn
49 |
50 | def test_fetch_private_repo(self) -> None:
51 | pass
52 |
53 | def test_file_missing(self) -> None:
54 | pass
55 |
56 | def test_bad_contents(self) -> None:
57 | pass
58 |
--------------------------------------------------------------------------------
/tests/test_web_app_handler.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import json
4 | from unittest import mock
5 |
6 | import pytest
7 |
8 | from src.web_app_handler import WebAppHandler
9 |
10 | valid_signature = "d9259f51d3b64e7fe0cbe09d9b08b8ee763170d3521fecc35fd8b453be8cf6a5"
11 |
12 |
13 | def test_invalid_header():
14 | handler = WebAppHandler()
15 | # This is missing X-GitHub-Event in the headers
16 | with pytest.raises(KeyError) as excinfo:
17 | handler.handle_event(data={}, headers={})
18 | (msg,) = excinfo.value.args
19 | assert msg == "X-GitHub-Event"
20 |
21 |
22 | # XXX: These tests could be covered with a JSON schema
23 | def test_invalid_github_event():
24 | handler = WebAppHandler()
25 | # This has an invalid X-GitHub-Event value
26 | reason, http_code = handler.handle_event(
27 | data={},
28 | headers={"X-GitHub-Event": "not_a_workflow_job"},
29 | )
30 | assert reason == "Event not supported."
31 | assert http_code == 200
32 |
33 |
34 | def test_missing_action_key():
35 | handler = WebAppHandler()
36 | # This payload is missing the action key
37 | with pytest.raises(KeyError) as excinfo:
38 | handler.handle_event(
39 | data={"bad_key": "irrelevant"},
40 | headers={"X-GitHub-Event": "workflow_job"},
41 | )
42 | (msg,) = excinfo.value.args
43 | assert msg == "action"
44 |
45 |
46 | def test_not_completed_workflow():
47 | handler = WebAppHandler()
48 | # This payload has an an action state we cannot process
49 | reason, http_code = handler.handle_event(
50 | data={"action": "not_completed"},
51 | headers={"X-GitHub-Event": "workflow_job"},
52 | )
53 | assert reason == "We cannot do anything with this workflow state."
54 | assert http_code == 200
55 |
56 |
57 | @pytest.mark.skip(reason="Not so important")
58 | def test_missing_workflow_job(monkeypatch):
59 | monkeypatch.delenv("GH_APP_ID", raising=False)
60 | handler = WebAppHandler()
61 | # This tries to send a trace but we're missing the workflow_job key
62 | with pytest.raises(KeyError) as excinfo:
63 | handler.handle_event(
64 | data={"action": "completed"},
65 | headers={"X-GitHub-Event": "workflow_job"},
66 | )
67 | (msg,) = excinfo.value.args
68 | assert msg == "workflow_job"
69 |
70 |
71 | def test_valid_signature_no_secret(monkeypatch):
72 | monkeypatch.delenv("GH_WEBHOOK_SECRET", raising=False)
73 | handler = WebAppHandler()
74 | assert handler.valid_signature(body={}, headers={}) == True
75 |
76 |
77 | def test_valid_signature(monkeypatch, webhook_event):
78 | monkeypatch.setenv("GH_WEBHOOK_SECRET", "fake_secret")
79 | handler = WebAppHandler()
80 | assert (
81 | handler.valid_signature(
82 | body=json.dumps(webhook_event["payload"]).encode(),
83 | headers={"X-Hub-Signature-256": f"sha256={valid_signature}"},
84 | )
85 | == True
86 | )
87 |
88 |
89 | def test_invalid_signature(monkeypatch, webhook_event):
90 | monkeypatch.setenv("GH_WEBHOOK_SECRET", "mistyped_secret")
91 | handler = WebAppHandler()
92 | # This is unit testing that the function works as expected
93 | assert (
94 | handler.valid_signature(
95 | body=json.dumps(webhook_event["payload"]).encode(),
96 | headers={"X-Hub-Signature-256": f"sha256={valid_signature}"},
97 | )
98 | == False
99 | )
100 |
101 |
102 | def test_handle_event_with_secret(monkeypatch, webhook_event):
103 | monkeypatch.setenv("GH_WEBHOOK_SECRET", "fake_secret")
104 | handler = WebAppHandler(dry_run=True)
105 | reason, http_code = handler.handle_event(
106 | data=webhook_event["payload"],
107 | headers=webhook_event["headers"],
108 | )
109 | assert reason == "OK"
110 | assert http_code == 200
111 |
--------------------------------------------------------------------------------