github-actions-ingester 0.1.0__tar.gz → 0.2.1__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.1.0 → github_actions_ingester-0.2.1}/CHANGELOG.md +32 -0
- {github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/PKG-INFO +15 -1
- {github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/README.md +14 -0
- {github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/pyproject.toml +1 -1
- {github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/src/github_actions_ingester/__init__.py +1 -1
- {github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/src/github_actions_ingester/__main__.py +4 -0
- {github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/src/github_actions_ingester/config.py +15 -0
- {github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/src/github_actions_ingester/store.py +67 -0
- {github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/tests/test_config.py +8 -0
- {github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/tests/test_store.py +49 -0
- {github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/.gitignore +0 -0
- {github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/LICENSE +0 -0
- {github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/src/github_actions_ingester/app_manifest.py +0 -0
- {github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/src/github_actions_ingester/collector.py +0 -0
- {github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/src/github_actions_ingester/github.py +0 -0
- {github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/src/github_actions_ingester/metrics.py +0 -0
- {github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/src/github_actions_ingester/migrations/0001_initial.sql +0 -0
- {github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/src/github_actions_ingester/ratelimit.py +0 -0
- {github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/src/github_actions_ingester/server.py +0 -0
- {github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/src/github_actions_ingester/workflow_schedule.py +0 -0
- {github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/tests/__init__.py +0 -0
- {github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/tests/conftest.py +0 -0
- {github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/tests/helpers.py +0 -0
- {github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/tests/test_app_manifest.py +0 -0
- {github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/tests/test_cli.py +0 -0
- {github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/tests/test_collector.py +0 -0
- {github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/tests/test_dashboard.py +0 -0
- {github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/tests/test_github.py +0 -0
- {github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/tests/test_metrics.py +0 -0
- {github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/tests/test_ratelimit.py +0 -0
- {github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/tests/test_server.py +0 -0
- {github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/tests/test_workflow_schedule.py +0 -0
|
@@ -4,6 +4,38 @@ 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.1] — 2026-09-05
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Chart: `deploymentAnnotations`, for a Reloader annotation on the
|
|
11
|
+
Deployment.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- A read role with a name different from the schema could not resolve the
|
|
15
|
+
views by bare name, and the Grafana PostgreSQL datasource has no
|
|
16
|
+
search_path setting. After granting read access the ingester now sets
|
|
17
|
+
the database's default `search_path` to the schema (needs the ingester
|
|
18
|
+
role to own the database; skipped with a warning otherwise).
|
|
19
|
+
|
|
20
|
+
## [0.2.0] — 2026-09-05
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- Chart: `image.digest` renders the image as `repo:tag@digest` for
|
|
24
|
+
clusters that require digest pins.
|
|
25
|
+
- Chart: `extraVolumes` and `extraVolumeMounts`, so a CA bundle can be
|
|
26
|
+
mounted for `sslmode=verify-full`.
|
|
27
|
+
- `GHA_DATABASE_READ_ROLES` (chart: `database.readRoles`): existing roles
|
|
28
|
+
that get read-only access to the schema after every migration run,
|
|
29
|
+
USAGE plus SELECT on every table and view and a default privilege for
|
|
30
|
+
tables added later. Lets Grafana read through its own role instead of
|
|
31
|
+
the ingester's credentials.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
- `examples/github-app/create-app.html` filled the `manifest` field only
|
|
35
|
+
on submit; a viewer that posts before the listener runs sent an empty
|
|
36
|
+
manifest and GitHub answered `"url" wasn't supplied`. The fields are
|
|
37
|
+
now filled on load and on every edit.
|
|
38
|
+
|
|
7
39
|
## [0.1.0] — 2026-09-05
|
|
8
40
|
|
|
9
41
|
First release.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: github-actions-ingester
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
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
|
|
@@ -155,6 +155,19 @@ nothing. `github-actions-ingester migrate` does the same and exits, for
|
|
|
155
155
|
init containers or pipelines that want the schema in place before the
|
|
156
156
|
service starts.
|
|
157
157
|
|
|
158
|
+
The ingester owns the schema and writes to it; dashboards should read
|
|
159
|
+
through a role of their own. `GHA_DATABASE_READ_ROLES` names existing
|
|
160
|
+
roles (comma-separated) that get USAGE on the schema and SELECT on every
|
|
161
|
+
table and view after each migration run, plus a default privilege so
|
|
162
|
+
tables added by later migrations are readable too. Point Grafana at the
|
|
163
|
+
database with one of those roles and it never needs the ingester's
|
|
164
|
+
credentials. The roles themselves are created by whoever manages the
|
|
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.
|
|
170
|
+
|
|
158
171
|
### Tables and views
|
|
159
172
|
|
|
160
173
|
| Object | Content |
|
|
@@ -280,6 +293,7 @@ directory is read too; see [`.env.example`](.env.example)).
|
|
|
280
293
|
| `GHA_DATABASE_URL` | _required_ | libpq URL, e.g. `postgresql://user:pass@host:5432/db?sslmode=require` |
|
|
281
294
|
| `GHA_DATABASE_SCHEMA` | `gha` | Schema holding every table; created on first start |
|
|
282
295
|
| `GHA_DATABASE_CONNECT_TIMEOUT_SECONDS` | `10` | Connection timeout |
|
|
296
|
+
| `GHA_DATABASE_READ_ROLES` | `""` | Comma-separated existing roles granted read-only access to the schema on every start |
|
|
283
297
|
| `GHA_POLL_INTERVAL_SECONDS` | `300` | Seconds between cycles (min 30) |
|
|
284
298
|
| `GHA_BACKFILL_DAYS` | `30` | How far back the first cycle of a repository goes (1..3660) |
|
|
285
299
|
| `GHA_LOOKBACK_MINUTES` | `180` | Window before the cursor re-listed every cycle to catch status changes |
|
|
@@ -92,6 +92,19 @@ nothing. `github-actions-ingester migrate` does the same and exits, for
|
|
|
92
92
|
init containers or pipelines that want the schema in place before the
|
|
93
93
|
service starts.
|
|
94
94
|
|
|
95
|
+
The ingester owns the schema and writes to it; dashboards should read
|
|
96
|
+
through a role of their own. `GHA_DATABASE_READ_ROLES` names existing
|
|
97
|
+
roles (comma-separated) that get USAGE on the schema and SELECT on every
|
|
98
|
+
table and view after each migration run, plus a default privilege so
|
|
99
|
+
tables added by later migrations are readable too. Point Grafana at the
|
|
100
|
+
database with one of those roles and it never needs the ingester's
|
|
101
|
+
credentials. The roles themselves are created by whoever manages the
|
|
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.
|
|
107
|
+
|
|
95
108
|
### Tables and views
|
|
96
109
|
|
|
97
110
|
| Object | Content |
|
|
@@ -217,6 +230,7 @@ directory is read too; see [`.env.example`](.env.example)).
|
|
|
217
230
|
| `GHA_DATABASE_URL` | _required_ | libpq URL, e.g. `postgresql://user:pass@host:5432/db?sslmode=require` |
|
|
218
231
|
| `GHA_DATABASE_SCHEMA` | `gha` | Schema holding every table; created on first start |
|
|
219
232
|
| `GHA_DATABASE_CONNECT_TIMEOUT_SECONDS` | `10` | Connection timeout |
|
|
233
|
+
| `GHA_DATABASE_READ_ROLES` | `""` | Comma-separated existing roles granted read-only access to the schema on every start |
|
|
220
234
|
| `GHA_POLL_INTERVAL_SECONDS` | `300` | Seconds between cycles (min 30) |
|
|
221
235
|
| `GHA_BACKFILL_DAYS` | `30` | How far back the first cycle of a repository goes (1..3660) |
|
|
222
236
|
| `GHA_LOOKBACK_MINUTES` | `180` | Window before the cursor re-listed every cycle to catch status changes |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "github-actions-ingester"
|
|
3
|
-
version = "0.1
|
|
3
|
+
version = "0.2.1"
|
|
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"
|
|
@@ -152,6 +152,7 @@ def cmd_run(once: bool) -> int:
|
|
|
152
152
|
while not stop.is_set():
|
|
153
153
|
try:
|
|
154
154
|
store.migrate()
|
|
155
|
+
store.grant_read_access(settings.read_role_list())
|
|
155
156
|
break
|
|
156
157
|
except Exception as exc:
|
|
157
158
|
log.error("store.bootstrap_failed", error=str(exc), retry_in=15)
|
|
@@ -200,12 +201,15 @@ def cmd_migrate() -> int:
|
|
|
200
201
|
)
|
|
201
202
|
try:
|
|
202
203
|
report = store.migrate()
|
|
204
|
+
granted = store.grant_read_access(settings.read_role_list())
|
|
203
205
|
finally:
|
|
204
206
|
store.close()
|
|
205
207
|
print(
|
|
206
208
|
f"schema {settings.database_schema}: version {report.current_version}, "
|
|
207
209
|
f"applied now: {report.applied or 'nothing'}"
|
|
208
210
|
)
|
|
211
|
+
if granted:
|
|
212
|
+
print(f"read access granted to: {', '.join(granted)}")
|
|
209
213
|
return 0
|
|
210
214
|
|
|
211
215
|
|
|
@@ -27,6 +27,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
27
27
|
|
|
28
28
|
_REPO_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$")
|
|
29
29
|
_OWNER_RE = re.compile(r"^[A-Za-z0-9_.-]+$")
|
|
30
|
+
_ROLE_RE = re.compile(r"^[a-z_][a-z0-9_]{0,62}$")
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
def _split_csv(value: str) -> list[str]:
|
|
@@ -112,6 +113,14 @@ class Settings(BaseSettings):
|
|
|
112
113
|
description="Schema that holds every ingester table. Created on first start if missing.",
|
|
113
114
|
)
|
|
114
115
|
database_connect_timeout_seconds: int = Field(default=10, ge=1)
|
|
116
|
+
database_read_roles: str = Field(
|
|
117
|
+
default="",
|
|
118
|
+
description="Comma-separated PostgreSQL roles that get read-only access "
|
|
119
|
+
"to the schema after every migration run: USAGE on the schema, "
|
|
120
|
+
"SELECT on every table and view, and the same SELECT on tables the "
|
|
121
|
+
"ingester creates later. Meant for the role Grafana connects with. "
|
|
122
|
+
"The roles must already exist; the ingester never creates roles.",
|
|
123
|
+
)
|
|
115
124
|
|
|
116
125
|
# --- Collection ---
|
|
117
126
|
poll_interval_seconds: int = Field(
|
|
@@ -250,6 +259,9 @@ class Settings(BaseSettings):
|
|
|
250
259
|
for repo in self.repo_list():
|
|
251
260
|
if not _REPO_RE.match(repo):
|
|
252
261
|
raise ValueError(f"repos entry {repo!r} must look like owner/name")
|
|
262
|
+
for role in self.read_role_list():
|
|
263
|
+
if not _ROLE_RE.match(role):
|
|
264
|
+
raise ValueError(f"database_read_roles entry {role!r} is not a plain identifier")
|
|
253
265
|
if not self.org_list() and not self.repo_list():
|
|
254
266
|
raise ValueError("nothing to ingest: set GHA_ORGS and/or GHA_REPOS")
|
|
255
267
|
return self
|
|
@@ -267,6 +279,9 @@ class Settings(BaseSettings):
|
|
|
267
279
|
def exclude_patterns(self) -> list[str]:
|
|
268
280
|
return _split_csv(self.exclude_repos)
|
|
269
281
|
|
|
282
|
+
def read_role_list(self) -> list[str]:
|
|
283
|
+
return _split_csv(self.database_read_roles)
|
|
284
|
+
|
|
270
285
|
def is_excluded(self, full_name: str) -> bool:
|
|
271
286
|
name = full_name.lower()
|
|
272
287
|
return any(fnmatch.fnmatchcase(name, pat.lower()) for pat in self.exclude_patterns())
|
{github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/src/github_actions_ingester/store.py
RENAMED
|
@@ -172,6 +172,73 @@ class Store:
|
|
|
172
172
|
logger.info("store.migrations_up_to_date", version=current)
|
|
173
173
|
return MigrationReport(applied_now, current, len(pending))
|
|
174
174
|
|
|
175
|
+
def grant_read_access(self, roles: Sequence[str]) -> list[str]:
|
|
176
|
+
"""Give ``roles`` read-only access to the schema.
|
|
177
|
+
|
|
178
|
+
USAGE on the schema, SELECT on every table and view in it, and a
|
|
179
|
+
default privilege so tables created by later migrations are
|
|
180
|
+
readable too. Idempotent; run after every ``migrate``. The roles
|
|
181
|
+
must exist already: creating roles is the operator's job, and a
|
|
182
|
+
missing one raises like any other bootstrap error.
|
|
183
|
+
"""
|
|
184
|
+
granted: list[str] = []
|
|
185
|
+
if not roles:
|
|
186
|
+
return granted
|
|
187
|
+
conn = self.connect()
|
|
188
|
+
schema = sql.Identifier(self._schema)
|
|
189
|
+
with conn.transaction(), conn.cursor() as cur:
|
|
190
|
+
for role in roles:
|
|
191
|
+
ident = sql.Identifier(role)
|
|
192
|
+
cur.execute(sql.SQL("GRANT USAGE ON SCHEMA {} TO {}").format(schema, ident))
|
|
193
|
+
cur.execute(
|
|
194
|
+
sql.SQL("GRANT SELECT ON ALL TABLES IN SCHEMA {} TO {}").format(schema, ident)
|
|
195
|
+
)
|
|
196
|
+
cur.execute(
|
|
197
|
+
sql.SQL(
|
|
198
|
+
"ALTER DEFAULT PRIVILEGES IN SCHEMA {} GRANT SELECT ON TABLES TO {}"
|
|
199
|
+
).format(schema, ident)
|
|
200
|
+
)
|
|
201
|
+
granted.append(role)
|
|
202
|
+
logger.info("store.read_access_granted", roles=granted, schema=self._schema)
|
|
203
|
+
self._set_database_search_path()
|
|
204
|
+
return granted
|
|
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
|
+
|
|
175
242
|
def schema_version(self) -> int | None:
|
|
176
243
|
"""Highest applied migration, or None when the schema was never bootstrapped."""
|
|
177
244
|
conn = self.connect()
|
|
@@ -104,3 +104,11 @@ def test_normalizations() -> None:
|
|
|
104
104
|
assert s.log_format == "console"
|
|
105
105
|
assert s.jobs_filter == "latest"
|
|
106
106
|
assert s.github_api_base == "https://ghe.example.com/api/v3"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_read_roles_parsed_and_validated() -> None:
|
|
110
|
+
s = load_settings(github_token="x", database_read_roles=" grafana, reporting_ro ", **BASE)
|
|
111
|
+
assert s.read_role_list() == ["grafana", "reporting_ro"]
|
|
112
|
+
assert load_settings(github_token="x", **BASE).read_role_list() == []
|
|
113
|
+
with pytest.raises(ValidationError, match="plain identifier"):
|
|
114
|
+
load_settings(github_token="x", database_read_roles="grafana; drop", **BASE)
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import re
|
|
3
4
|
from datetime import timedelta
|
|
4
5
|
|
|
6
|
+
import psycopg
|
|
5
7
|
import pytest
|
|
6
8
|
|
|
7
9
|
from github_actions_ingester.store import Migration, Store, load_migrations
|
|
@@ -229,3 +231,50 @@ def _run_row(store: Store, run_id: int) -> dict[str, object]:
|
|
|
229
231
|
return dict(cur.fetchone() or {})
|
|
230
232
|
finally:
|
|
231
233
|
conn.rollback()
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_grant_read_access_lets_a_role_read_views_and_future_tables(
|
|
237
|
+
database_url: str, migrated_store: Store
|
|
238
|
+
) -> None:
|
|
239
|
+
role = "r_" + migrated_store.schema
|
|
240
|
+
with psycopg.connect(database_url, autocommit=True) as admin:
|
|
241
|
+
admin.execute(f"CREATE ROLE {role} LOGIN PASSWORD 'reader'")
|
|
242
|
+
try:
|
|
243
|
+
assert migrated_store.grant_read_access([role]) == [role]
|
|
244
|
+
# Idempotent: a second start must not fail on grants already given.
|
|
245
|
+
assert migrated_store.grant_read_access([role]) == [role]
|
|
246
|
+
# A table created by the ingester AFTER the grant is covered too
|
|
247
|
+
# (default privileges), which is what a later migration looks like.
|
|
248
|
+
with migrated_store.connect().transaction(), migrated_store.connect().cursor() as cur:
|
|
249
|
+
cur.execute("CREATE TABLE later_migration (id INT)")
|
|
250
|
+
schema = migrated_store.schema
|
|
251
|
+
reader_url = re.sub(r"//[^@]*@", f"//{role}:reader@", _with_userinfo(database_url))
|
|
252
|
+
with psycopg.connect(reader_url, autocommit=True) as reader:
|
|
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
|
|
257
|
+
assert reader.execute("SELECT count(*) FROM minion_workflow_runs").fetchone()[0] == 0
|
|
258
|
+
assert reader.execute("SELECT count(*) FROM workflow_jobs").fetchone()[0] == 0
|
|
259
|
+
assert reader.execute("SELECT count(*) FROM later_migration").fetchone()[0] == 0
|
|
260
|
+
with pytest.raises(psycopg.errors.InsufficientPrivilege):
|
|
261
|
+
reader.execute(
|
|
262
|
+
"INSERT INTO repositories (id, owner, name, full_name) VALUES (1,'a','b','a/b')"
|
|
263
|
+
)
|
|
264
|
+
finally:
|
|
265
|
+
with psycopg.connect(database_url, autocommit=True) as admin:
|
|
266
|
+
admin.execute(f"DROP OWNED BY {role}")
|
|
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')
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_grant_read_access_requires_an_existing_role(migrated_store: Store) -> None:
|
|
273
|
+
with pytest.raises(psycopg.errors.UndefinedObject):
|
|
274
|
+
migrated_store.grant_read_access(["no_such_role_" + migrated_store.schema])
|
|
275
|
+
assert migrated_store.grant_read_access([]) == []
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _with_userinfo(url: str) -> str:
|
|
279
|
+
"""pgserver URIs carry no user:pass; give the substitution something to replace."""
|
|
280
|
+
return url if "@" in url else url.replace("://", "://x:y@", 1)
|
|
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
|
|
File without changes
|
|
File without changes
|
{github_actions_ingester-0.1.0 → github_actions_ingester-0.2.1}/tests/test_workflow_schedule.py
RENAMED
|
File without changes
|