Skip to content
This repository was archived by the owner on Mar 13, 2026. It is now read-only.

Commit abb9057

Browse files
arponpesduhow
andauthored
Add tests and GitHub Actions (#9)
Co-authored-by: David Girón <[email protected]>
1 parent 7ba7d9f commit abb9057

10 files changed

Lines changed: 248 additions & 7 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
paths:
9+
- "src/**"
10+
workflow_dispatch:
11+
12+
jobs:
13+
build:
14+
runs-on: ubuntu-latest
15+
name: Build
16+
steps:
17+
- uses: actions/checkout@v3
18+
- name: Login GHCR
19+
uses: docker/login-action@v2
20+
with:
21+
username: ${{ github.repository_owner }}
22+
password: ${{ secrets.GITHUB_TOKEN }}
23+
registry: ghcr.io
24+
- name: Set metadata
25+
id: meta
26+
uses: docker/metadata-action@v4
27+
with:
28+
images: ghcr.io/${{ github.repository_owner }}/github-workflows-monitoring
29+
flavor: |
30+
latest=true
31+
tags: |
32+
type=ref,event=branch
33+
type=ref,event=pr
34+
type=semver,pattern={{version}}
35+
type=semver,pattern={{major}}.{{minor}}
36+
- name: Docker build and push
37+
uses: docker/build-push-action@v4
38+
with:
39+
push: ${{ github.event_name != 'pull_request' }}
40+
tags: ${{ steps.meta.outputs.tags }}
41+
labels: ${{ steps.meta.outputs.labels }}
42+
context: .

.github/workflows/tests.yaml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
8+
jobs:
9+
build:
10+
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
python-version: ["3.10", "3.11"]
15+
16+
steps:
17+
- uses: actions/checkout@v3
18+
- name: Set up Python ${{ matrix.python-version }}
19+
uses: actions/setup-python@v4
20+
with:
21+
python-version: ${{ matrix.python-version }}
22+
- name: Install dependencies
23+
run: |
24+
python -m pip install --upgrade pip
25+
pip install -r tests/requirements.txt
26+
- name: Lint with flake8
27+
run: |
28+
flake8
29+
- name: Test with pytest
30+
run: |
31+
pytest --cov=src

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,18 @@
1-
# github-workflows-monitoring
1+
# github-workflows-monitoring
2+
3+
[![Tests](https://github.com/midokura/github-workflows-monitoring/actions/workflows/tests.yaml/badge.svg)](https://github.com/midokura/github-workflows-monitoring/actions/workflows/tests.yaml)
4+
5+
## About
6+
7+
Github Workflow Monitoring is a small Flask-based web server that connects to Github using websockets to monitor Github Actions workflows. It tracks each workflow's state (queued, in_progress, completed) and calculates the time spent in each state. The metrics are logged in logfmt format for easy consumption by Grafana.
8+
9+
## Testing
10+
11+
Into a virtual environment, install the requirements:
12+
13+
pip install -r tests/requirements.txt
14+
15+
16+
To run the tests:
17+
18+
pytest --cov=src

pytest.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[pytest]
2+
testpaths = tests/tests.py
3+
pythonpath = src

setup.cfg

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,8 @@ python_requires = >=3.8
88
packages = find:
99
install_requires =
1010
Flask>=2.2,<3
11+
12+
[flake8]
13+
max-line-length = 120
14+
exit-zero = True
15+
max-complexity = 10

src/app.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from flask import Flask, abort, request
77

8+
89
from const import GithubHeaders, LOGGING_CONFIG
910
from utils import parse_datetime
1011

@@ -21,6 +22,7 @@
2122

2223
jobs = dict()
2324

25+
2426
# check all calls are valid
2527
@app.before_request
2628
def validate_origin_github():
@@ -90,8 +92,10 @@ def process_workflow_job():
9092
app.logger.warning(f"Unknown action {action}, removing from memory")
9193
if job_id in jobs:
9294
del jobs[job_id]
95+
msg = None
9396

94-
app.logger.info(msg)
97+
if msg:
98+
app.logger.info(msg)
9599
return True
96100

97101

@@ -102,11 +106,7 @@ def github_webhook_process():
102106

103107
if command == "process_workflow_job":
104108
app.logger.debug(f"Calling function {command}")
105-
response = process_workflow_job()
106-
107-
if not response:
108-
app.logger.error(f"Error calling {event} function")
109-
return abort(500)
109+
process_workflow_job()
110110
return "OK"
111111

112112
app.logger.error(f"Unknown event type {event}, can't handle")

tests/__init__.py

Whitespace-only changes.

tests/conftest.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import pytest
2+
from src.app import app
3+
4+
5+
@pytest.fixture()
6+
def app_instance():
7+
yield app
8+
9+
10+
@pytest.fixture()
11+
def client(app_instance):
12+
return app_instance.test_client()

tests/requirements.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Flask
2+
pytest
3+
pytest-cov
4+
flake8

tests/tests.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
from src.const import GithubHeaders
2+
3+
HEADERS = {
4+
GithubHeaders.EVENT.value: "workflow_job",
5+
"Content-Type": "application/json",
6+
"User-Agent": "GitHub-Hookshot/123",
7+
}
8+
BODY = {
9+
"action": "",
10+
"workflow_job": {
11+
"id": 0,
12+
"workflow_name": "CI",
13+
"started_at": "2023-01-27T14:00:00Z",
14+
},
15+
"repository": {
16+
"full_name": "foo/foo",
17+
},
18+
}
19+
20+
21+
def test_method_not_allowed(client):
22+
assert client.get("/github-webhook").status_code == 401
23+
24+
25+
def test_headers_not_correct(client, caplog):
26+
response = client.post("/github-webhook")
27+
assert response.status_code == 401
28+
assert caplog.messages == [
29+
"User-Agent is werkzeug/2.2.2",
30+
"Content is not JSON",
31+
"No GitHub Event received!",
32+
]
33+
34+
35+
def test_no_body_bad_request(client):
36+
response = client.post("/github-webhook", headers=HEADERS)
37+
assert response.status_code == 400
38+
39+
40+
def test_unknown_event(client, caplog):
41+
headers = HEADERS.copy()
42+
headers[GithubHeaders.EVENT.value] = "foo"
43+
response = client.post("/github-webhook", headers=headers, json={})
44+
assert response.status_code == 405
45+
assert caplog.messages == ["Unknown event type foo, can't handle"]
46+
47+
48+
def test_started_job_not_stored(client, caplog):
49+
body_started = BODY.copy()
50+
body_started["action"] = "in_progress"
51+
body_started["workflow_job"]["id"] = 2
52+
response = client.post("/github-webhook", headers=HEADERS, json=body_started)
53+
assert response.status_code == 200
54+
assert caplog.messages == [
55+
"Job 2 is in_progress but not stored!",
56+
'action=in_progress repository=foo/foo job_id=2 workflow="CI" time_to_start=0',
57+
]
58+
59+
60+
def test_finished_job_not_stored(client, caplog):
61+
body_finished = BODY.copy()
62+
body_finished["action"] = "completed"
63+
body_finished["workflow_job"]["id"] = 3
64+
response = client.post("/github-webhook", headers=HEADERS, json=body_finished)
65+
assert response.status_code == 200
66+
assert caplog.messages == [
67+
"Job 3 is completed but not stored!",
68+
'action=completed repository=foo/foo job_id=3 workflow="CI" time_to_finish=0',
69+
]
70+
71+
72+
def test_unknown_action(client, caplog):
73+
body_started = BODY.copy()
74+
body_started["action"] = "queued"
75+
body_started["workflow_job"]["id"] = 4
76+
response = client.post("/github-webhook", headers=HEADERS, json=body_started)
77+
body_failed = body_started.copy()
78+
body_failed["action"] = "failed"
79+
response = client.post("/github-webhook", headers=HEADERS, json=body_failed)
80+
assert response.status_code == 200
81+
assert caplog.messages == [
82+
'action=queued repository=foo/foo job_id=4 workflow="CI"',
83+
"Unknown action failed, removing from memory",
84+
]
85+
86+
87+
def test_queued_job(client, caplog):
88+
body_queued = BODY.copy()
89+
body_queued["action"] = "queued"
90+
body_queued["workflow_job"]["id"] = 1
91+
response = client.post("/github-webhook", headers=HEADERS, json=body_queued)
92+
assert response.status_code == 200
93+
assert caplog.messages == [
94+
'action=queued repository=foo/foo job_id=1 workflow="CI"'
95+
]
96+
97+
98+
def test_logging_flow(client, caplog):
99+
body_queued = BODY.copy()
100+
body_queued["action"] = "queued"
101+
body_queued["workflow_job"]["id"] = 5
102+
103+
response = client.post("/github-webhook", headers=HEADERS, json=body_queued)
104+
assert response.status_code == 200
105+
assert (
106+
caplog.messages[0] == 'action=queued repository=foo/foo job_id=5 workflow="CI"'
107+
)
108+
109+
body_started = BODY.copy()
110+
body_started["action"] = "in_progress"
111+
body_started["workflow_job"]["started_at"] = "2023-01-27T14:00:05Z"
112+
response = client.post("/github-webhook", headers=HEADERS, json=body_started)
113+
assert response.status_code == 200
114+
assert (
115+
caplog.messages[1]
116+
== 'action=in_progress repository=foo/foo job_id=5 workflow="CI" time_to_start=5'
117+
)
118+
119+
body_completed = BODY.copy()
120+
body_completed["action"] = "completed"
121+
body_completed["workflow_job"]["completed_at"] = "2023-01-27T14:05:00Z"
122+
response = client.post("/github-webhook", headers=HEADERS, json=body_completed)
123+
assert response.status_code == 200
124+
assert (
125+
caplog.messages[2]
126+
== 'action=completed repository=foo/foo job_id=5 workflow="CI" time_to_finish=295'
127+
)

0 commit comments

Comments
 (0)