github-actions-ingester 0.2.0__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/CHANGELOG.md +27 -0
- {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/PKG-INFO +6 -2
- {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/README.md +5 -1
- {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/pyproject.toml +1 -1
- {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/src/github_actions_ingester/__init__.py +1 -1
- {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/src/github_actions_ingester/__main__.py +12 -1
- {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/src/github_actions_ingester/collector.py +22 -7
- {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/src/github_actions_ingester/github.py +4 -0
- {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/src/github_actions_ingester/store.py +37 -0
- {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/tests/test_collector.py +31 -0
- {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/tests/test_github.py +18 -0
- {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/tests/test_store.py +6 -1
- {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/.gitignore +0 -0
- {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/LICENSE +0 -0
- {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/src/github_actions_ingester/app_manifest.py +0 -0
- {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/src/github_actions_ingester/config.py +0 -0
- {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/src/github_actions_ingester/metrics.py +0 -0
- {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/src/github_actions_ingester/migrations/0001_initial.sql +0 -0
- {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/src/github_actions_ingester/ratelimit.py +0 -0
- {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/src/github_actions_ingester/server.py +0 -0
- {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/src/github_actions_ingester/workflow_schedule.py +0 -0
- {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/tests/__init__.py +0 -0
- {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/tests/conftest.py +0 -0
- {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/tests/helpers.py +0 -0
- {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/tests/test_app_manifest.py +0 -0
- {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/tests/test_cli.py +0 -0
- {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/tests/test_config.py +0 -0
- {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/tests/test_dashboard.py +0 -0
- {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/tests/test_metrics.py +0 -0
- {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/tests/test_ratelimit.py +0 -0
- {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/tests/test_server.py +0 -0
- {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/tests/test_workflow_schedule.py +0 -0
|
@@ -4,6 +4,33 @@ All notable changes to this project are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project
|
|
5
5
|
adheres to [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [0.2.2] — 2026-09-05
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- The GitHub rate-limit gauges were refreshed once per repository, so during
|
|
11
|
+
a long first backfill of a busy repository they could lag by twenty
|
|
12
|
+
minutes while the real budget ran out. They now follow every response,
|
|
13
|
+
as the README already said they did.
|
|
14
|
+
- `runs_upserted_total` and `jobs_upserted_total` move as rows are written
|
|
15
|
+
instead of once when the repository finishes.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- `jobs.progress` log line every 250 runs during the jobs phase, which was
|
|
19
|
+
silent for the whole duration of a large backfill.
|
|
20
|
+
|
|
21
|
+
## [0.2.1] — 2026-09-05
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- Chart: `deploymentAnnotations`, for a Reloader annotation on the
|
|
25
|
+
Deployment.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- A read role with a name different from the schema could not resolve the
|
|
29
|
+
views by bare name, and the Grafana PostgreSQL datasource has no
|
|
30
|
+
search_path setting. After granting read access the ingester now sets
|
|
31
|
+
the database's default `search_path` to the schema (needs the ingester
|
|
32
|
+
role to own the database; skipped with a warning otherwise).
|
|
33
|
+
|
|
7
34
|
## [0.2.0] — 2026-09-05
|
|
8
35
|
|
|
9
36
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: github-actions-ingester
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Standalone GitHub Actions ingester — pulls workflows, runs and jobs from the GitHub API into PostgreSQL for Grafana dashboards, FinOps and alerting. Prometheus /metrics for ingester health and scheduled-workflow liveness.
|
|
5
5
|
Project-URL: Homepage, https://github.com/danielgines/github-actions-ingester
|
|
6
6
|
Project-URL: Repository, https://github.com/danielgines/github-actions-ingester
|
|
@@ -162,7 +162,11 @@ table and view after each migration run, plus a default privilege so
|
|
|
162
162
|
tables added by later migrations are readable too. Point Grafana at the
|
|
163
163
|
database with one of those roles and it never needs the ingester's
|
|
164
164
|
credentials. The roles themselves are created by whoever manages the
|
|
165
|
-
database (the ingester never runs `CREATE ROLE`).
|
|
165
|
+
database (the ingester never runs `CREATE ROLE`). In the same step the
|
|
166
|
+
ingester sets the database's default `search_path` to the schema, so a
|
|
167
|
+
Grafana datasource (which cannot send one) resolves the views by bare
|
|
168
|
+
name; that statement needs the ingester's role to own the database, and
|
|
169
|
+
is skipped with a warning otherwise.
|
|
166
170
|
|
|
167
171
|
### Tables and views
|
|
168
172
|
|
|
@@ -99,7 +99,11 @@ table and view after each migration run, plus a default privilege so
|
|
|
99
99
|
tables added by later migrations are readable too. Point Grafana at the
|
|
100
100
|
database with one of those roles and it never needs the ingester's
|
|
101
101
|
credentials. The roles themselves are created by whoever manages the
|
|
102
|
-
database (the ingester never runs `CREATE ROLE`).
|
|
102
|
+
database (the ingester never runs `CREATE ROLE`). In the same step the
|
|
103
|
+
ingester sets the database's default `search_path` to the schema, so a
|
|
104
|
+
Grafana datasource (which cannot send one) resolves the views by bare
|
|
105
|
+
name; that statement needs the ingester's role to own the database, and
|
|
106
|
+
is skipped with a warning otherwise.
|
|
103
107
|
|
|
104
108
|
### Tables and views
|
|
105
109
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "github-actions-ingester"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.2"
|
|
4
4
|
description = "Standalone GitHub Actions ingester — pulls workflows, runs and jobs from the GitHub API into PostgreSQL for Grafana dashboards, FinOps and alerting. Prometheus /metrics for ingester health and scheduled-workflow liveness."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -26,7 +26,7 @@ from . import __version__
|
|
|
26
26
|
from .app_manifest import convert_code, manifest_form_url, manifest_json, write_private_key
|
|
27
27
|
from .collector import Collector
|
|
28
28
|
from .config import Settings, load_settings
|
|
29
|
-
from .github import AppAuth, GitHubAPIError, GitHubClient, TokenAuth
|
|
29
|
+
from .github import AppAuth, GitHubAPIError, GitHubClient, RateLimitState, TokenAuth
|
|
30
30
|
from .metrics import Metrics
|
|
31
31
|
from .ratelimit import RateLimiter
|
|
32
32
|
from .server import MetricsServer
|
|
@@ -77,6 +77,16 @@ def _build_client(settings: Settings, metrics: Metrics | None = None) -> GitHubC
|
|
|
77
77
|
if metrics is not None:
|
|
78
78
|
metrics.github_requests_total.labels(status=str(status)).inc()
|
|
79
79
|
|
|
80
|
+
# Refresh the budget gauges on every response, not once per repository:
|
|
81
|
+
# a busy repository keeps the collector inside one ingest_repository()
|
|
82
|
+
# call for many minutes, and an alert on the remaining budget must not
|
|
83
|
+
# wait for it to finish.
|
|
84
|
+
def on_rate_limit(rl: RateLimitState) -> None:
|
|
85
|
+
if metrics is not None:
|
|
86
|
+
metrics.github_rate_limit_remaining.set(rl.remaining)
|
|
87
|
+
metrics.github_rate_limit_limit.set(rl.limit)
|
|
88
|
+
metrics.github_rate_limit_reset_timestamp_seconds.set(rl.reset_at)
|
|
89
|
+
|
|
80
90
|
return GitHubClient(
|
|
81
91
|
auth=auth,
|
|
82
92
|
base_url=settings.github_api_base,
|
|
@@ -85,6 +95,7 @@ def _build_client(settings: Settings, metrics: Metrics | None = None) -> GitHubC
|
|
|
85
95
|
min_remaining=settings.api_min_remaining,
|
|
86
96
|
max_retries=settings.api_max_retries,
|
|
87
97
|
on_request=on_request,
|
|
98
|
+
on_rate_limit=on_rate_limit,
|
|
88
99
|
)
|
|
89
100
|
|
|
90
101
|
|
|
@@ -56,6 +56,7 @@ CONCLUSIONS = (
|
|
|
56
56
|
"stale",
|
|
57
57
|
"startup_failure",
|
|
58
58
|
)
|
|
59
|
+
_JOBS_PROGRESS_EVERY = 250
|
|
59
60
|
_RUN_BATCH = 500
|
|
60
61
|
|
|
61
62
|
|
|
@@ -198,9 +199,10 @@ class Collector:
|
|
|
198
199
|
|
|
199
200
|
written = 0
|
|
200
201
|
for batch in _batched(self._client.list_runs(repo, since, now), _RUN_BATCH):
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
202
|
+
n = self._store.upsert_runs(batch)
|
|
203
|
+
written += n
|
|
204
|
+
if n:
|
|
205
|
+
self._metrics.runs_upserted_total.labels(repository=repo.full_name).inc(n)
|
|
204
206
|
|
|
205
207
|
# Open runs that fell out of the window: refresh them individually.
|
|
206
208
|
stale = self._store.open_runs_before(repo.id, since, s.max_open_run_refresh)
|
|
@@ -213,8 +215,12 @@ class Collector:
|
|
|
213
215
|
raise
|
|
214
216
|
self._store.upsert_runs([run])
|
|
215
217
|
|
|
218
|
+
# One request per run: on a first backfill of a busy repository this
|
|
219
|
+
# loop runs for many minutes, so it reports progress on the way and
|
|
220
|
+
# moves the counters as it goes instead of once at the end.
|
|
216
221
|
jobs_written = 0
|
|
217
|
-
|
|
222
|
+
pending = self._store.runs_needing_jobs(repo.id)
|
|
223
|
+
for done, run_id in enumerate(pending, start=1):
|
|
218
224
|
try:
|
|
219
225
|
jobs = list(self._client.list_jobs(repo, run_id, s.jobs_filter))
|
|
220
226
|
except GitHubAPIError as exc:
|
|
@@ -223,9 +229,18 @@ class Collector:
|
|
|
223
229
|
self._store.upsert_jobs(run_id, [])
|
|
224
230
|
continue
|
|
225
231
|
raise
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
232
|
+
n = self._store.upsert_jobs(run_id, jobs)
|
|
233
|
+
jobs_written += n
|
|
234
|
+
if n:
|
|
235
|
+
self._metrics.jobs_upserted_total.labels(repository=repo.full_name).inc(n)
|
|
236
|
+
if done % _JOBS_PROGRESS_EVERY == 0 and done < len(pending):
|
|
237
|
+
logger.info(
|
|
238
|
+
"jobs.progress",
|
|
239
|
+
repo=repo.full_name,
|
|
240
|
+
runs_done=done,
|
|
241
|
+
runs_total=len(pending),
|
|
242
|
+
jobs=jobs_written,
|
|
243
|
+
)
|
|
229
244
|
|
|
230
245
|
self._store.set_cursor(repo.id, now, written)
|
|
231
246
|
logger.info(
|
|
@@ -338,6 +338,7 @@ class GitHubClient:
|
|
|
338
338
|
min_remaining: int = 200,
|
|
339
339
|
max_retries: int = 4,
|
|
340
340
|
on_request: Callable[[int], None] | None = None,
|
|
341
|
+
on_rate_limit: Callable[[RateLimitState], None] | None = None,
|
|
341
342
|
sleep: Callable[[float], None] = time.sleep,
|
|
342
343
|
clock: Callable[[], float] = time.time,
|
|
343
344
|
) -> None:
|
|
@@ -355,6 +356,7 @@ class GitHubClient:
|
|
|
355
356
|
self._min_remaining = min_remaining
|
|
356
357
|
self._max_retries = max_retries
|
|
357
358
|
self._on_request = on_request
|
|
359
|
+
self._on_rate_limit = on_rate_limit
|
|
358
360
|
self._sleep = sleep
|
|
359
361
|
self._clock = clock
|
|
360
362
|
self.rate_limit = RateLimitState()
|
|
@@ -404,6 +406,8 @@ class GitHubClient:
|
|
|
404
406
|
self._sleep(min(2**attempt, 30))
|
|
405
407
|
continue
|
|
406
408
|
self.rate_limit.update(resp.headers)
|
|
409
|
+
if self._on_rate_limit is not None:
|
|
410
|
+
self._on_rate_limit(self.rate_limit)
|
|
407
411
|
if self._on_request is not None:
|
|
408
412
|
self._on_request(resp.status_code)
|
|
409
413
|
if resp.status_code < 400:
|
{github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/src/github_actions_ingester/store.py
RENAMED
|
@@ -200,8 +200,45 @@ class Store:
|
|
|
200
200
|
)
|
|
201
201
|
granted.append(role)
|
|
202
202
|
logger.info("store.read_access_granted", roles=granted, schema=self._schema)
|
|
203
|
+
self._set_database_search_path()
|
|
203
204
|
return granted
|
|
204
205
|
|
|
206
|
+
def _set_database_search_path(self) -> None:
|
|
207
|
+
"""Make the schema resolve by bare name for every session on the database.
|
|
208
|
+
|
|
209
|
+
The ingester sets ``search_path`` on its own connections, and a role
|
|
210
|
+
named like the schema gets it for free (``"$user"`` is first in the
|
|
211
|
+
default path), which is why the Compose example works without this.
|
|
212
|
+
A read role with a different name (``grafana`` reading schema
|
|
213
|
+
``gha``) resolves ``minion_workflow_runs`` to nothing, and the
|
|
214
|
+
Grafana PostgreSQL datasource has no search_path setting.
|
|
215
|
+
|
|
216
|
+
``ALTER DATABASE ... SET`` needs the database owner. When the ingester
|
|
217
|
+
is not the owner this logs and moves on: the grants above still hold,
|
|
218
|
+
and the operator can run the statement once by hand.
|
|
219
|
+
"""
|
|
220
|
+
conn = self.connect()
|
|
221
|
+
try:
|
|
222
|
+
with conn.transaction(), conn.cursor() as cur:
|
|
223
|
+
cur.execute("SELECT current_database() AS db")
|
|
224
|
+
row = cur.fetchone()
|
|
225
|
+
database = row["db"] if row else ""
|
|
226
|
+
cur.execute(
|
|
227
|
+
sql.SQL("ALTER DATABASE {} SET search_path TO {}, public").format(
|
|
228
|
+
sql.Identifier(database), sql.Identifier(self._schema)
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
except psycopg.errors.InsufficientPrivilege:
|
|
232
|
+
conn.rollback()
|
|
233
|
+
logger.warning(
|
|
234
|
+
"store.search_path_not_set",
|
|
235
|
+
schema=self._schema,
|
|
236
|
+
hint="ingester role does not own the database; run "
|
|
237
|
+
"ALTER DATABASE <db> SET search_path TO <schema>, public as the owner",
|
|
238
|
+
)
|
|
239
|
+
return
|
|
240
|
+
logger.info("store.search_path_set", schema=self._schema)
|
|
241
|
+
|
|
205
242
|
def schema_version(self) -> int | None:
|
|
206
243
|
"""Highest applied migration, or None when the schema was never bootstrapped."""
|
|
207
244
|
conn = self.connect()
|
|
@@ -195,6 +195,37 @@ def test_first_cycle_backfills_everything(migrated_store: Store, clock: list[dat
|
|
|
195
195
|
assert _gauge(m, "gha_scheduled_workflow_last_conclusion", **labels, conclusion="failure") == 0
|
|
196
196
|
|
|
197
197
|
|
|
198
|
+
def test_jobs_phase_reports_progress_and_moves_counters(
|
|
199
|
+
migrated_store: Store, clock: list[datetime], monkeypatch: pytest.MonkeyPatch
|
|
200
|
+
) -> None:
|
|
201
|
+
"""A long jobs phase logs every N runs and increments the counter per run,
|
|
202
|
+
so a stalled backfill is visible before ingest_repository() returns."""
|
|
203
|
+
import github_actions_ingester.collector as mod
|
|
204
|
+
|
|
205
|
+
gh = FakeGitHub()
|
|
206
|
+
gh.runs[1] = [run(300 + i, 1, 10) for i in range(5)]
|
|
207
|
+
for i in range(5):
|
|
208
|
+
gh.jobs[300 + i] = [job(3000 + i, 300 + i, 1)]
|
|
209
|
+
monkeypatch.setattr(mod, "_JOBS_PROGRESS_EVERY", 2)
|
|
210
|
+
events: list[dict[str, Any]] = []
|
|
211
|
+
seen_counter: list[float | None] = []
|
|
212
|
+
c, m = make(migrated_store, gh, clock)
|
|
213
|
+
|
|
214
|
+
def spy(event: str, **kw: Any) -> None:
|
|
215
|
+
if event == "jobs.progress":
|
|
216
|
+
events.append(kw)
|
|
217
|
+
seen_counter.append(
|
|
218
|
+
_gauge(m, "gha_ingester_jobs_upserted_total", repository="acme/web")
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
monkeypatch.setattr(mod.logger, "info", spy)
|
|
222
|
+
c.run_cycle()
|
|
223
|
+
assert [(e["runs_done"], e["runs_total"]) for e in events] == [(2, 5), (4, 5)]
|
|
224
|
+
# The counter had already moved when the progress line was written.
|
|
225
|
+
assert seen_counter == [2.0, 4.0]
|
|
226
|
+
assert _gauge(m, "gha_ingester_jobs_upserted_total", repository="acme/web") == 5.0
|
|
227
|
+
|
|
228
|
+
|
|
198
229
|
def test_second_cycle_is_incremental(migrated_store: Store, clock: list[datetime]) -> None:
|
|
199
230
|
gh = FakeGitHub()
|
|
200
231
|
c, _ = make(migrated_store, gh, clock)
|
|
@@ -330,6 +330,24 @@ def test_on_request_hook_receives_status(httpx_mock: HTTPXMock) -> None:
|
|
|
330
330
|
assert seen == [404]
|
|
331
331
|
|
|
332
332
|
|
|
333
|
+
def test_on_rate_limit_hook_fires_on_every_response(httpx_mock: HTTPXMock) -> None:
|
|
334
|
+
seen: list[int] = []
|
|
335
|
+
httpx_mock.add_response(
|
|
336
|
+
url=f"{API}/a",
|
|
337
|
+
json={},
|
|
338
|
+
headers={"X-RateLimit-Limit": "5000", "X-RateLimit-Remaining": "4000"},
|
|
339
|
+
)
|
|
340
|
+
httpx_mock.add_response(
|
|
341
|
+
url=f"{API}/b",
|
|
342
|
+
json={},
|
|
343
|
+
headers={"X-RateLimit-Limit": "5000", "X-RateLimit-Remaining": "3999"},
|
|
344
|
+
)
|
|
345
|
+
c = _client(on_rate_limit=lambda rl: seen.append(rl.remaining))
|
|
346
|
+
c.get("/a")
|
|
347
|
+
c.get("/b")
|
|
348
|
+
assert seen == [4000, 3999]
|
|
349
|
+
|
|
350
|
+
|
|
333
351
|
# -- pagination / listings ---------------------------------------------------------
|
|
334
352
|
|
|
335
353
|
|
|
@@ -250,7 +250,10 @@ def test_grant_read_access_lets_a_role_read_views_and_future_tables(
|
|
|
250
250
|
schema = migrated_store.schema
|
|
251
251
|
reader_url = re.sub(r"//[^@]*@", f"//{role}:reader@", _with_userinfo(database_url))
|
|
252
252
|
with psycopg.connect(reader_url, autocommit=True) as reader:
|
|
253
|
-
|
|
253
|
+
# No SET search_path on purpose: Grafana cannot send one, so the
|
|
254
|
+
# database-level default the ingester sets must resolve the views.
|
|
255
|
+
path = reader.execute("SHOW search_path").fetchone()[0]
|
|
256
|
+
assert path.split(",")[0].strip().strip('"') == schema
|
|
254
257
|
assert reader.execute("SELECT count(*) FROM minion_workflow_runs").fetchone()[0] == 0
|
|
255
258
|
assert reader.execute("SELECT count(*) FROM workflow_jobs").fetchone()[0] == 0
|
|
256
259
|
assert reader.execute("SELECT count(*) FROM later_migration").fetchone()[0] == 0
|
|
@@ -262,6 +265,8 @@ def test_grant_read_access_lets_a_role_read_views_and_future_tables(
|
|
|
262
265
|
with psycopg.connect(database_url, autocommit=True) as admin:
|
|
263
266
|
admin.execute(f"DROP OWNED BY {role}")
|
|
264
267
|
admin.execute(f"DROP ROLE {role}")
|
|
268
|
+
db = admin.execute("SELECT current_database()").fetchone()[0]
|
|
269
|
+
admin.execute(f'ALTER DATABASE "{db}" RESET search_path')
|
|
265
270
|
|
|
266
271
|
|
|
267
272
|
def test_grant_read_access_requires_an_existing_role(migrated_store: Store) -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{github_actions_ingester-0.2.0 → github_actions_ingester-0.2.2}/tests/test_workflow_schedule.py
RENAMED
|
File without changes
|