github-actions-ingester 0.2.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.
Files changed (32) hide show
  1. {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.1}/CHANGELOG.md +13 -0
  2. {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.1}/PKG-INFO +6 -2
  3. {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.1}/README.md +5 -1
  4. {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.1}/pyproject.toml +1 -1
  5. {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.1}/src/github_actions_ingester/__init__.py +1 -1
  6. {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.1}/src/github_actions_ingester/store.py +37 -0
  7. {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.1}/tests/test_store.py +6 -1
  8. {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.1}/.gitignore +0 -0
  9. {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.1}/LICENSE +0 -0
  10. {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.1}/src/github_actions_ingester/__main__.py +0 -0
  11. {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.1}/src/github_actions_ingester/app_manifest.py +0 -0
  12. {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.1}/src/github_actions_ingester/collector.py +0 -0
  13. {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.1}/src/github_actions_ingester/config.py +0 -0
  14. {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.1}/src/github_actions_ingester/github.py +0 -0
  15. {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.1}/src/github_actions_ingester/metrics.py +0 -0
  16. {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.1}/src/github_actions_ingester/migrations/0001_initial.sql +0 -0
  17. {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.1}/src/github_actions_ingester/ratelimit.py +0 -0
  18. {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.1}/src/github_actions_ingester/server.py +0 -0
  19. {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.1}/src/github_actions_ingester/workflow_schedule.py +0 -0
  20. {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.1}/tests/__init__.py +0 -0
  21. {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.1}/tests/conftest.py +0 -0
  22. {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.1}/tests/helpers.py +0 -0
  23. {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.1}/tests/test_app_manifest.py +0 -0
  24. {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.1}/tests/test_cli.py +0 -0
  25. {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.1}/tests/test_collector.py +0 -0
  26. {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.1}/tests/test_config.py +0 -0
  27. {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.1}/tests/test_dashboard.py +0 -0
  28. {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.1}/tests/test_github.py +0 -0
  29. {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.1}/tests/test_metrics.py +0 -0
  30. {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.1}/tests/test_ratelimit.py +0 -0
  31. {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.1}/tests/test_server.py +0 -0
  32. {github_actions_ingester-0.2.0 → github_actions_ingester-0.2.1}/tests/test_workflow_schedule.py +0 -0
@@ -4,6 +4,19 @@ 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
+
7
20
  ## [0.2.0] — 2026-09-05
8
21
 
9
22
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: github-actions-ingester
3
- Version: 0.2.0
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
@@ -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.0"
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"
@@ -6,4 +6,4 @@ its own, and exposes Prometheus metrics about the ingestion itself and
6
6
  about the liveness of scheduled workflows.
7
7
  """
8
8
 
9
- __version__ = "0.2.0"
9
+ __version__ = "0.2.1"
@@ -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()
@@ -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
- reader.execute(f"SET search_path TO {schema}")
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: