codex-workspace-bootstrap 0.7.0__tar.gz → 0.8.0__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 (50) hide show
  1. {codex_workspace_bootstrap-0.7.0/src/codex_workspace_bootstrap.egg-info → codex_workspace_bootstrap-0.8.0}/PKG-INFO +51 -4
  2. {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/README.md +50 -3
  3. {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/pyproject.toml +1 -1
  4. {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/src/codex_workspace_bootstrap/__init__.py +1 -1
  5. {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/src/codex_workspace_bootstrap/audit.py +8 -6
  6. {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/src/codex_workspace_bootstrap/cli.py +19 -9
  7. codex_workspace_bootstrap-0.8.0/src/codex_workspace_bootstrap/integrations/__init__.py +1 -0
  8. codex_workspace_bootstrap-0.8.0/src/codex_workspace_bootstrap/integrations/github.py +79 -0
  9. codex_workspace_bootstrap-0.8.0/src/codex_workspace_bootstrap/integrations/github_actions_worker.py +266 -0
  10. codex_workspace_bootstrap-0.8.0/src/codex_workspace_bootstrap/integrations/github_app.py +30 -0
  11. codex_workspace_bootstrap-0.8.0/src/codex_workspace_bootstrap/integrations/github_app_service.py +94 -0
  12. codex_workspace_bootstrap-0.8.0/src/codex_workspace_bootstrap/integrations/github_checkout.py +274 -0
  13. codex_workspace_bootstrap-0.8.0/src/codex_workspace_bootstrap/integrations/github_cloud_run.py +821 -0
  14. codex_workspace_bootstrap-0.8.0/src/codex_workspace_bootstrap/integrations/github_delivery.py +220 -0
  15. codex_workspace_bootstrap-0.8.0/src/codex_workspace_bootstrap/integrations/github_manifest.py +204 -0
  16. codex_workspace_bootstrap-0.8.0/src/codex_workspace_bootstrap/integrations/github_runtime.py +332 -0
  17. codex_workspace_bootstrap-0.8.0/src/codex_workspace_bootstrap/integrations/github_webhook.py +136 -0
  18. {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/src/codex_workspace_bootstrap/preflight.py +70 -4
  19. {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0/src/codex_workspace_bootstrap.egg-info}/PKG-INFO +51 -4
  20. codex_workspace_bootstrap-0.8.0/src/codex_workspace_bootstrap.egg-info/SOURCES.txt +47 -0
  21. {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/tests/test_audit.py +24 -0
  22. {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/tests/test_cli.py +32 -1
  23. codex_workspace_bootstrap-0.8.0/tests/test_github_actions_worker.py +208 -0
  24. codex_workspace_bootstrap-0.8.0/tests/test_github_app_registration.py +34 -0
  25. codex_workspace_bootstrap-0.8.0/tests/test_github_app_service.py +144 -0
  26. codex_workspace_bootstrap-0.8.0/tests/test_github_checkout.py +217 -0
  27. codex_workspace_bootstrap-0.8.0/tests/test_github_cloud_run.py +297 -0
  28. codex_workspace_bootstrap-0.8.0/tests/test_github_delivery.py +266 -0
  29. codex_workspace_bootstrap-0.8.0/tests/test_github_integration.py +94 -0
  30. codex_workspace_bootstrap-0.8.0/tests/test_github_manifest.py +161 -0
  31. codex_workspace_bootstrap-0.8.0/tests/test_github_runtime.py +570 -0
  32. codex_workspace_bootstrap-0.8.0/tests/test_github_webhook.py +160 -0
  33. {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/tests/test_preflight.py +132 -0
  34. {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/tests/test_public_repo_patterns.py +30 -0
  35. codex_workspace_bootstrap-0.7.0/src/codex_workspace_bootstrap.egg-info/SOURCES.txt +0 -26
  36. {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/LICENSE +0 -0
  37. {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/setup.cfg +0 -0
  38. {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/src/codex_workspace_bootstrap/__main__.py +0 -0
  39. {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/src/codex_workspace_bootstrap/agents.py +0 -0
  40. {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/src/codex_workspace_bootstrap/doctor.py +0 -0
  41. {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/src/codex_workspace_bootstrap/fixes.py +0 -0
  42. {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/src/codex_workspace_bootstrap/instructions.py +0 -0
  43. {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/src/codex_workspace_bootstrap/sarif.py +0 -0
  44. {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/src/codex_workspace_bootstrap.egg-info/dependency_links.txt +0 -0
  45. {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/src/codex_workspace_bootstrap.egg-info/entry_points.txt +0 -0
  46. {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/src/codex_workspace_bootstrap.egg-info/top_level.txt +0 -0
  47. {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/tests/test_doctor.py +0 -0
  48. {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/tests/test_instruction_integrity.py +0 -0
  49. {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/tests/test_release_consistency.py +0 -0
  50. {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/tests/test_sarif.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-workspace-bootstrap
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: Preflight AI coding repositories before Codex, Copilot, Cline, Claude, Gemini, Continue, or Cursor touches them.
5
5
  Author: kohli217
6
6
  License: MIT
@@ -40,10 +40,23 @@ Dynamic: license-file
40
40
 
41
41
  Windows-first. Local by default. CI-friendly. Designed for repositories used with Codex, Copilot, Cline, Claude Code, Gemini CLI, Continue, Cursor, and similar coding agents.
42
42
 
43
- [日本語ガイド](docs/README.ja.md) · [Examples](docs/EXAMPLES.md) · [GitHub Action](docs/GITHUB_ACTION.md) · [Roadmap](docs/ROADMAP.md) · [Releases](https://github.com/kohli217/codex-workspace-bootstrap/releases)
43
+ [日本語ガイド](docs/README.ja.md) · [Examples](docs/EXAMPLES.md) · [GitHub Action](docs/GITHUB_ACTION.md) · [GitHub App](docs/GITHUB_APP.md) · [Free GitHub App deployment](deploy/cloudflare/README.md) · [Cloud Run alternative](deploy/cloudrun/README.md) · [Integration contract](docs/INTEGRATIONS.md) · [Roadmap](docs/ROADMAP.md) · [Releases](https://github.com/kohli217/codex-workspace-bootstrap/releases)
44
44
 
45
45
  > Community-maintained project. Not an official OpenAI product and not affiliated with OpenAI.
46
46
 
47
+ ## Why add this to your repository?
48
+
49
+ AI coding agents can move fast, but they can also follow stale or conflicting repository instructions. `codex-workspace-bootstrap` adds one repeatable check before that happens.
50
+
51
+ | Common failure | What `cwb` does |
52
+ | --- | --- |
53
+ | One instruction says `npm`, another says `pnpm` | Flags the mismatch before an agent edits the repository. |
54
+ | An instruction tells the agent to run a test/lint script that does not exist | Reports the invalid command instead of letting the mistake reach the agent. |
55
+ | A risky file such as `.env.production` is Git-tracked | Surfaces it as a blocking risk signal without printing its contents. |
56
+ | The repository is still missing important readiness pieces | `cwb preflight . --require-ready` can fail CI until the repository reaches `READY`. |
57
+
58
+ It is local-first: the core preflight does not send repository contents to a remote AI service.
59
+
47
60
  ## The 20-second demo
48
61
 
49
62
  ```powershell
@@ -219,6 +232,14 @@ cwb preflight . --require-ready
219
232
 
220
233
  Use `--require-ready` when CI should reject both `NEEDS ATTENTION` and `BLOCKED`, including missing repository-wide AI instructions or essential repository markers.
221
234
 
235
+ For a remote service such as a GitHub App, skip host-machine tool availability checks while keeping repository evidence, Git tracking, risk filenames, and instruction integrity checks:
236
+
237
+ ```powershell
238
+ cwb preflight . --repository-only
239
+ ```
240
+
241
+ This prevents a remote scanner from reporting the scanner host's Node.js, package-manager, WSL, PowerShell, or Codex availability as if it belonged to the inspected repository.
242
+
222
243
  ### Detailed audit
223
244
 
224
245
  ```powershell
@@ -267,7 +288,7 @@ The reusable Action is published on GitHub Marketplace.
267
288
  - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
268
289
  with:
269
290
  python-version: "3.13"
270
- - uses: kohli217/codex-workspace-bootstrap@v0.7.0
291
+ - uses: kohli217/codex-workspace-bootstrap@v0.8.0
271
292
  with:
272
293
  path: .
273
294
  strict: "true"
@@ -281,6 +302,32 @@ See [docs/GITHUB_ACTION.md](docs/GITHUB_ACTION.md).
281
302
 
282
303
  Read-only evaluations against real public repositories are documented in [docs/PUBLIC_REPO_EVALUATIONS.md](docs/PUBLIC_REPO_EVALUATIONS.md). They are reproducible technical evaluations, not claims of third-party adoption.
283
304
 
305
+ ## GitHub App
306
+
307
+ For public OSS repositories, CWB can also run as a self-hosted GitHub App. A verified `push` or `pull_request` event triggers repository-only preflight against the exact event revision and publishes a **CWB Preflight** Check Run on that commit.
308
+
309
+ The preferred zero-cost development deployment uses:
310
+
311
+ ```text
312
+ GitHub App
313
+ -> Cloudflare Worker + Queue (Free)
314
+ -> GitHub Actions in this public repository
315
+ -> short-lived repository-scoped installation token
316
+ -> CWB Preflight Check
317
+ ```
318
+
319
+ This path does not require a Google Cloud billing account. It is intentionally **public-repository-only**: private-repository events are rejected before they are queued into the public Actions worker.
320
+
321
+ The App keeps the minimum repository permissions documented by the project: Checks read/write, Contents read-only, and Pull requests read-only.
322
+
323
+ See [docs/GITHUB_APP.md](docs/GITHUB_APP.md) for the integration contract and [deploy/cloudflare/README.md](deploy/cloudflare/README.md) for the completely free development deployment.
324
+
325
+ ## Integration boundary
326
+
327
+ The CLI, reusable GitHub Action, GitHub App, and future AI-agent skills share one preflight engine and one policy evaluator. Machine-readable reports declare a schema version so integrations can detect incompatible changes instead of silently drifting.
328
+
329
+ See [docs/INTEGRATIONS.md](docs/INTEGRATIONS.md). The minimum GitHub App permissions and webhook subscriptions are defined in [docs/GITHUB_APP.md](docs/GITHUB_APP.md) and mirrored by code-level regression tests.
330
+
284
331
  ## Where it fits
285
332
 
286
333
  This project is a **preflight layer**, not an AI coding agent and not a deep security scanner.
@@ -334,7 +381,7 @@ py -m pip install codex-workspace-bootstrap
334
381
  Pinned GitHub release artifact:
335
382
 
336
383
  ```powershell
337
- py -m pip install "https://github.com/kohli217/codex-workspace-bootstrap/releases/download/v0.7.0/codex_workspace_bootstrap-0.7.0-py3-none-any.whl"
384
+ py -m pip install "https://github.com/kohli217/codex-workspace-bootstrap/releases/download/v0.8.0/codex_workspace_bootstrap-0.8.0-py3-none-any.whl"
338
385
  ```
339
386
 
340
387
  ## Development
@@ -12,10 +12,23 @@
12
12
 
13
13
  Windows-first. Local by default. CI-friendly. Designed for repositories used with Codex, Copilot, Cline, Claude Code, Gemini CLI, Continue, Cursor, and similar coding agents.
14
14
 
15
- [日本語ガイド](docs/README.ja.md) · [Examples](docs/EXAMPLES.md) · [GitHub Action](docs/GITHUB_ACTION.md) · [Roadmap](docs/ROADMAP.md) · [Releases](https://github.com/kohli217/codex-workspace-bootstrap/releases)
15
+ [日本語ガイド](docs/README.ja.md) · [Examples](docs/EXAMPLES.md) · [GitHub Action](docs/GITHUB_ACTION.md) · [GitHub App](docs/GITHUB_APP.md) · [Free GitHub App deployment](deploy/cloudflare/README.md) · [Cloud Run alternative](deploy/cloudrun/README.md) · [Integration contract](docs/INTEGRATIONS.md) · [Roadmap](docs/ROADMAP.md) · [Releases](https://github.com/kohli217/codex-workspace-bootstrap/releases)
16
16
 
17
17
  > Community-maintained project. Not an official OpenAI product and not affiliated with OpenAI.
18
18
 
19
+ ## Why add this to your repository?
20
+
21
+ AI coding agents can move fast, but they can also follow stale or conflicting repository instructions. `codex-workspace-bootstrap` adds one repeatable check before that happens.
22
+
23
+ | Common failure | What `cwb` does |
24
+ | --- | --- |
25
+ | One instruction says `npm`, another says `pnpm` | Flags the mismatch before an agent edits the repository. |
26
+ | An instruction tells the agent to run a test/lint script that does not exist | Reports the invalid command instead of letting the mistake reach the agent. |
27
+ | A risky file such as `.env.production` is Git-tracked | Surfaces it as a blocking risk signal without printing its contents. |
28
+ | The repository is still missing important readiness pieces | `cwb preflight . --require-ready` can fail CI until the repository reaches `READY`. |
29
+
30
+ It is local-first: the core preflight does not send repository contents to a remote AI service.
31
+
19
32
  ## The 20-second demo
20
33
 
21
34
  ```powershell
@@ -191,6 +204,14 @@ cwb preflight . --require-ready
191
204
 
192
205
  Use `--require-ready` when CI should reject both `NEEDS ATTENTION` and `BLOCKED`, including missing repository-wide AI instructions or essential repository markers.
193
206
 
207
+ For a remote service such as a GitHub App, skip host-machine tool availability checks while keeping repository evidence, Git tracking, risk filenames, and instruction integrity checks:
208
+
209
+ ```powershell
210
+ cwb preflight . --repository-only
211
+ ```
212
+
213
+ This prevents a remote scanner from reporting the scanner host's Node.js, package-manager, WSL, PowerShell, or Codex availability as if it belonged to the inspected repository.
214
+
194
215
  ### Detailed audit
195
216
 
196
217
  ```powershell
@@ -239,7 +260,7 @@ The reusable Action is published on GitHub Marketplace.
239
260
  - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
240
261
  with:
241
262
  python-version: "3.13"
242
- - uses: kohli217/codex-workspace-bootstrap@v0.7.0
263
+ - uses: kohli217/codex-workspace-bootstrap@v0.8.0
243
264
  with:
244
265
  path: .
245
266
  strict: "true"
@@ -253,6 +274,32 @@ See [docs/GITHUB_ACTION.md](docs/GITHUB_ACTION.md).
253
274
 
254
275
  Read-only evaluations against real public repositories are documented in [docs/PUBLIC_REPO_EVALUATIONS.md](docs/PUBLIC_REPO_EVALUATIONS.md). They are reproducible technical evaluations, not claims of third-party adoption.
255
276
 
277
+ ## GitHub App
278
+
279
+ For public OSS repositories, CWB can also run as a self-hosted GitHub App. A verified `push` or `pull_request` event triggers repository-only preflight against the exact event revision and publishes a **CWB Preflight** Check Run on that commit.
280
+
281
+ The preferred zero-cost development deployment uses:
282
+
283
+ ```text
284
+ GitHub App
285
+ -> Cloudflare Worker + Queue (Free)
286
+ -> GitHub Actions in this public repository
287
+ -> short-lived repository-scoped installation token
288
+ -> CWB Preflight Check
289
+ ```
290
+
291
+ This path does not require a Google Cloud billing account. It is intentionally **public-repository-only**: private-repository events are rejected before they are queued into the public Actions worker.
292
+
293
+ The App keeps the minimum repository permissions documented by the project: Checks read/write, Contents read-only, and Pull requests read-only.
294
+
295
+ See [docs/GITHUB_APP.md](docs/GITHUB_APP.md) for the integration contract and [deploy/cloudflare/README.md](deploy/cloudflare/README.md) for the completely free development deployment.
296
+
297
+ ## Integration boundary
298
+
299
+ The CLI, reusable GitHub Action, GitHub App, and future AI-agent skills share one preflight engine and one policy evaluator. Machine-readable reports declare a schema version so integrations can detect incompatible changes instead of silently drifting.
300
+
301
+ See [docs/INTEGRATIONS.md](docs/INTEGRATIONS.md). The minimum GitHub App permissions and webhook subscriptions are defined in [docs/GITHUB_APP.md](docs/GITHUB_APP.md) and mirrored by code-level regression tests.
302
+
256
303
  ## Where it fits
257
304
 
258
305
  This project is a **preflight layer**, not an AI coding agent and not a deep security scanner.
@@ -306,7 +353,7 @@ py -m pip install codex-workspace-bootstrap
306
353
  Pinned GitHub release artifact:
307
354
 
308
355
  ```powershell
309
- py -m pip install "https://github.com/kohli217/codex-workspace-bootstrap/releases/download/v0.7.0/codex_workspace_bootstrap-0.7.0-py3-none-any.whl"
356
+ py -m pip install "https://github.com/kohli217/codex-workspace-bootstrap/releases/download/v0.8.0/codex_workspace_bootstrap-0.8.0-py3-none-any.whl"
310
357
  ```
311
358
 
312
359
  ## Development
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codex-workspace-bootstrap"
7
- version = "0.7.0"
7
+ version = "0.8.0"
8
8
  description = "Preflight AI coding repositories before Codex, Copilot, Cline, Claude, Gemini, Continue, or Cursor touches them."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,3 +1,3 @@
1
1
  """codex-workspace-bootstrap package."""
2
2
 
3
- __version__ = "0.7.0"
3
+ __version__ = "0.8.0"
@@ -194,7 +194,7 @@ def _preview(paths: list[str], limit: int = 8) -> str:
194
194
  return f"{visible}{suffix}"
195
195
 
196
196
 
197
- def audit_repository(root: Path) -> list[Check]:
197
+ def audit_repository(root: Path, *, include_local_toolchain: bool = True) -> list[Check]:
198
198
  root = root.resolve()
199
199
  checks: list[Check] = []
200
200
 
@@ -285,8 +285,9 @@ def audit_repository(root: Path) -> list[Check]:
285
285
  )
286
286
  )
287
287
 
288
- for label, command in COMMON_TOOLS:
289
- checks.append(_tool_check(label, command))
288
+ if include_local_toolchain:
289
+ for label, command in COMMON_TOOLS:
290
+ checks.append(_tool_check(label, command))
290
291
 
291
292
  if (root / "package.json").is_file() and not (root / "package.json").is_symlink():
292
293
  managers = detect_node_package_managers(root)
@@ -300,9 +301,10 @@ def audit_repository(root: Path) -> list[Check]:
300
301
  + ", ".join(sorted(managers)),
301
302
  )
302
303
  )
303
- for manager in sorted(managers):
304
- command = NODE_PACKAGE_MANAGER_COMMANDS[manager]
305
- checks.append(_tool_check(manager, command))
304
+ if include_local_toolchain:
305
+ for manager in sorted(managers):
306
+ command = NODE_PACKAGE_MANAGER_COMMANDS[manager]
307
+ checks.append(_tool_check(manager, command))
306
308
  else:
307
309
  checks.append(
308
310
  Check(
@@ -10,7 +10,7 @@ from .agents import generate_agents
10
10
  from .audit import audit_repository, summary
11
11
  from .doctor import doctor_findings
12
12
  from .fixes import apply_fix_plan, build_fix_plan
13
- from .preflight import build_preflight, render_markdown
13
+ from .preflight import build_preflight, evaluate_preflight_policy, render_markdown
14
14
  from .sarif import checks_to_sarif, preflight_report_to_sarif
15
15
 
16
16
 
@@ -46,6 +46,11 @@ def _parser() -> argparse.ArgumentParser:
46
46
  action="store_true",
47
47
  help="Return a non-zero exit code unless the preflight state is READY",
48
48
  )
49
+ preflight.add_argument(
50
+ "--repository-only",
51
+ action="store_true",
52
+ help="Skip local toolchain availability checks and evaluate repository evidence only",
53
+ )
49
54
 
50
55
  fix = sub.add_parser("fix", help="Preview safe repository-readiness fixes")
51
56
  fix.add_argument("path", nargs="?", default=".")
@@ -91,13 +96,17 @@ def _run_preflight(
91
96
  strict: bool,
92
97
  fail_on_integrity: bool,
93
98
  require_ready: bool,
99
+ repository_only: bool,
94
100
  ) -> int:
95
101
  root = Path(path).expanduser().resolve()
96
102
  if not root.exists() or not root.is_dir():
97
103
  print(f"error: repository path does not exist or is not a directory: {root}", file=sys.stderr)
98
104
  return 2
99
105
 
100
- report = build_preflight(root)
106
+ report = build_preflight(
107
+ root,
108
+ include_local_toolchain=not repository_only,
109
+ )
101
110
 
102
111
  print("AI Repository Preflight")
103
112
  print(f"Repository: {root}")
@@ -163,13 +172,13 @@ def _run_preflight(
163
172
  "Preflight SARIF report",
164
173
  )
165
174
 
166
- if strict and report["state"] == "BLOCKED":
167
- return 1
168
- if fail_on_integrity and report["instruction_summary"]["findings"]:
169
- return 1
170
- if require_ready and report["state"] != "READY":
171
- return 1
172
- return 0
175
+ decision = evaluate_preflight_policy(
176
+ report,
177
+ strict=strict,
178
+ fail_on_integrity=fail_on_integrity,
179
+ require_ready=require_ready,
180
+ )
181
+ return 0 if decision.passed else 1
173
182
 
174
183
 
175
184
  def _run_fix(path: str, apply: bool) -> int:
@@ -301,6 +310,7 @@ def main(argv: list[str] | None = None) -> int:
301
310
  args.strict,
302
311
  args.fail_on_integrity,
303
312
  args.require_ready,
313
+ args.repository_only,
304
314
  )
305
315
  if args.command == "fix":
306
316
  return _run_fix(args.path, args.apply)
@@ -0,0 +1 @@
1
+ """Delivery adapters built on the stable preflight integration contract."""
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Literal
5
+
6
+ from ..preflight import (
7
+ PREFLIGHT_REPORT_SCHEMA_VERSION,
8
+ PolicyDecision,
9
+ evaluate_preflight_policy,
10
+ render_markdown,
11
+ )
12
+
13
+
14
+ GitHubCheckConclusion = Literal["success", "neutral", "failure"]
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class GitHubCheckResult:
19
+ """GitHub Check Run fields derived from one preflight report."""
20
+
21
+ name: str
22
+ conclusion: GitHubCheckConclusion
23
+ title: str
24
+ summary: str
25
+ policy: PolicyDecision
26
+
27
+ def to_check_run_fields(self) -> dict[str, object]:
28
+ """Return Check Run fields that a caller can combine with head_sha."""
29
+
30
+ return {
31
+ "name": self.name,
32
+ "status": "completed",
33
+ "conclusion": self.conclusion,
34
+ "output": {
35
+ "title": self.title,
36
+ "summary": self.summary,
37
+ },
38
+ }
39
+
40
+
41
+ def build_github_check(
42
+ report: dict[str, object],
43
+ *,
44
+ strict: bool = True,
45
+ fail_on_integrity: bool = False,
46
+ require_ready: bool = False,
47
+ name: str = "CWB Preflight",
48
+ ) -> GitHubCheckResult:
49
+ """Map a preflight report to a GitHub Check without making network calls."""
50
+
51
+ schema_version = report.get("schema_version")
52
+ if schema_version != PREFLIGHT_REPORT_SCHEMA_VERSION:
53
+ raise ValueError(
54
+ "unsupported preflight schema version: "
55
+ f"{schema_version!r}; expected {PREFLIGHT_REPORT_SCHEMA_VERSION}"
56
+ )
57
+
58
+ decision = evaluate_preflight_policy(
59
+ report,
60
+ strict=strict,
61
+ fail_on_integrity=fail_on_integrity,
62
+ require_ready=require_ready,
63
+ )
64
+ state = str(report.get("state", "UNKNOWN"))
65
+
66
+ if not decision.passed:
67
+ conclusion: GitHubCheckConclusion = "failure"
68
+ elif state == "READY":
69
+ conclusion = "success"
70
+ else:
71
+ conclusion = "neutral"
72
+
73
+ return GitHubCheckResult(
74
+ name=name,
75
+ conclusion=conclusion,
76
+ title=f"CWB preflight: {state}",
77
+ summary=render_markdown(report),
78
+ policy=decision,
79
+ )
@@ -0,0 +1,266 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import base64
5
+ from dataclasses import asdict
6
+ import json
7
+ import os
8
+ from urllib.error import HTTPError, URLError
9
+ from urllib.parse import urlencode, urlparse
10
+ from urllib.request import Request, urlopen
11
+
12
+ from .github_runtime import (
13
+ GitHubAppRuntimeError,
14
+ GitHubScanResult,
15
+ execute_github_scan_with_token,
16
+ )
17
+ from .github_webhook import GitHubWebhookTarget
18
+
19
+
20
+ _MAX_RESPONSE_BYTES = 1024 * 1024
21
+
22
+
23
+ class GitHubActionsWorkerError(RuntimeError):
24
+ """Raised when the free GitHub Actions worker cannot authenticate or run."""
25
+
26
+
27
+ def _base64url_decode(value: str) -> bytes:
28
+ if not value.strip():
29
+ raise GitHubActionsWorkerError("scan payload must not be empty")
30
+ padding = "=" * (-len(value) % 4)
31
+ try:
32
+ return base64.urlsafe_b64decode((value + padding).encode("ascii"))
33
+ except (ValueError, UnicodeEncodeError) as exc:
34
+ raise GitHubActionsWorkerError("scan payload is not valid base64url") from exc
35
+
36
+
37
+ def decode_scan_payload(encoded: str) -> tuple[str, str, GitHubWebhookTarget]:
38
+ try:
39
+ payload = json.loads(_base64url_decode(encoded).decode("utf-8"))
40
+ except (UnicodeDecodeError, json.JSONDecodeError) as exc:
41
+ raise GitHubActionsWorkerError("scan payload is not valid UTF-8 JSON") from exc
42
+ if not isinstance(payload, dict):
43
+ raise GitHubActionsWorkerError("scan payload must be a JSON object")
44
+
45
+ delivery_id = payload.get("delivery_id")
46
+ broker_grant = payload.get("broker_grant")
47
+ target = payload.get("target")
48
+ if not isinstance(delivery_id, str) or not delivery_id.strip():
49
+ raise GitHubActionsWorkerError("scan payload is missing delivery_id")
50
+ if not isinstance(broker_grant, str) or not broker_grant.strip():
51
+ raise GitHubActionsWorkerError("scan payload is missing broker_grant")
52
+ if not isinstance(target, dict):
53
+ raise GitHubActionsWorkerError("scan payload is missing target")
54
+
55
+ event = target.get("event")
56
+ repository = target.get("repository")
57
+ head_sha = target.get("head_sha")
58
+ installation_id = target.get("installation_id")
59
+ if event not in {"push", "pull_request"}:
60
+ raise GitHubActionsWorkerError("scan payload has unsupported event")
61
+ if not isinstance(repository, str) or repository.count("/") != 1:
62
+ raise GitHubActionsWorkerError("scan payload has invalid repository")
63
+ if not isinstance(head_sha, str) or not head_sha.strip():
64
+ raise GitHubActionsWorkerError("scan payload is missing head_sha")
65
+ if (
66
+ isinstance(installation_id, bool)
67
+ or not isinstance(installation_id, int)
68
+ or installation_id <= 0
69
+ ):
70
+ raise GitHubActionsWorkerError("scan payload is missing installation_id")
71
+
72
+ number = target.get("pull_request_number")
73
+ if number is not None and (
74
+ isinstance(number, bool)
75
+ or not isinstance(number, int)
76
+ or number <= 0
77
+ ):
78
+ raise GitHubActionsWorkerError("scan payload has invalid pull_request_number")
79
+
80
+ def optional_text(name: str) -> str | None:
81
+ value = target.get(name)
82
+ if value is None:
83
+ return None
84
+ if not isinstance(value, str) or not value.strip():
85
+ raise GitHubActionsWorkerError(f"scan payload has invalid {name}")
86
+ return value
87
+
88
+ return delivery_id, broker_grant, GitHubWebhookTarget(
89
+ event=event,
90
+ repository=repository,
91
+ head_sha=head_sha,
92
+ installation_id=installation_id,
93
+ action=optional_text("action"),
94
+ pull_request_number=number,
95
+ merge_commit_sha=optional_text("merge_commit_sha"),
96
+ ref=optional_text("ref"),
97
+ )
98
+
99
+
100
+ def _read_json_response(response) -> dict[str, object]:
101
+ raw = response.read(_MAX_RESPONSE_BYTES + 1)
102
+ if len(raw) > _MAX_RESPONSE_BYTES:
103
+ raise GitHubActionsWorkerError("remote response exceeded size limit")
104
+ try:
105
+ value = json.loads(raw.decode("utf-8"))
106
+ except (UnicodeDecodeError, json.JSONDecodeError) as exc:
107
+ raise GitHubActionsWorkerError("remote service returned invalid JSON") from exc
108
+ if not isinstance(value, dict):
109
+ raise GitHubActionsWorkerError("remote service returned non-object JSON")
110
+ return value
111
+
112
+
113
+ def request_actions_oidc_token(
114
+ *,
115
+ audience: str,
116
+ request_url: str | None = None,
117
+ request_token: str | None = None,
118
+ ) -> str:
119
+ url = request_url or os.environ.get("ACTIONS_ID_TOKEN_REQUEST_URL", "")
120
+ bearer = request_token or os.environ.get("ACTIONS_ID_TOKEN_REQUEST_TOKEN", "")
121
+ if not url or not bearer:
122
+ raise GitHubActionsWorkerError(
123
+ "GitHub Actions OIDC environment is unavailable; id-token: write is required"
124
+ )
125
+
126
+ separator = "&" if "?" in url else "?"
127
+ request = Request(
128
+ url + separator + urlencode({"audience": audience}),
129
+ headers={"Authorization": f"Bearer {bearer}"},
130
+ method="GET",
131
+ )
132
+ try:
133
+ with urlopen(request, timeout=20) as response:
134
+ body = _read_json_response(response)
135
+ except HTTPError as exc:
136
+ raise GitHubActionsWorkerError(
137
+ f"GitHub Actions OIDC request failed with HTTP {exc.code}"
138
+ ) from exc
139
+ except URLError as exc:
140
+ raise GitHubActionsWorkerError("GitHub Actions OIDC request failed") from exc
141
+
142
+ value = body.get("value")
143
+ if not isinstance(value, str) or not value.strip():
144
+ raise GitHubActionsWorkerError("GitHub Actions OIDC response is missing value")
145
+ return value
146
+
147
+
148
+ def _validated_token_endpoint(endpoint: str) -> str:
149
+ parsed = urlparse(endpoint)
150
+ if (
151
+ parsed.scheme != "https"
152
+ or not parsed.netloc
153
+ or parsed.username is not None
154
+ or parsed.password is not None
155
+ or parsed.query
156
+ or parsed.fragment
157
+ ):
158
+ raise GitHubActionsWorkerError(
159
+ "token endpoint must be an absolute HTTPS URL without credentials, query, or fragment"
160
+ )
161
+ return endpoint.rstrip("/")
162
+
163
+
164
+ def request_installation_token(
165
+ *,
166
+ endpoint: str,
167
+ oidc_token: str,
168
+ delivery_id: str,
169
+ broker_grant: str,
170
+ target: GitHubWebhookTarget,
171
+ ) -> str:
172
+ if target.installation_id is None:
173
+ raise GitHubActionsWorkerError("scan target is missing installation_id")
174
+ url = _validated_token_endpoint(endpoint)
175
+ request = Request(
176
+ url,
177
+ data=json.dumps(
178
+ {
179
+ "delivery_id": delivery_id,
180
+ "broker_grant": broker_grant,
181
+ "target": asdict(target),
182
+ },
183
+ separators=(",", ":"),
184
+ ).encode("utf-8"),
185
+ headers={
186
+ "Authorization": f"Bearer {oidc_token}",
187
+ "Content-Type": "application/json",
188
+ "User-Agent": "codex-workspace-bootstrap",
189
+ },
190
+ method="POST",
191
+ )
192
+ try:
193
+ with urlopen(request, timeout=20) as response:
194
+ body = _read_json_response(response)
195
+ except HTTPError as exc:
196
+ raise GitHubActionsWorkerError(
197
+ f"installation-token broker returned HTTP {exc.code}"
198
+ ) from exc
199
+ except URLError as exc:
200
+ raise GitHubActionsWorkerError(
201
+ "installation-token broker request failed"
202
+ ) from exc
203
+
204
+ token = body.get("token")
205
+ if not isinstance(token, str) or not token.strip():
206
+ raise GitHubActionsWorkerError(
207
+ "installation-token broker response is missing token"
208
+ )
209
+ return token
210
+
211
+
212
+ def execute_actions_scan(
213
+ *,
214
+ encoded_payload: str,
215
+ token_endpoint: str,
216
+ ) -> GitHubScanResult:
217
+ delivery_id, broker_grant, target = decode_scan_payload(encoded_payload)
218
+ audience = _validated_token_endpoint(token_endpoint)
219
+ oidc_token = request_actions_oidc_token(audience=audience)
220
+ installation_token = request_installation_token(
221
+ endpoint=audience,
222
+ oidc_token=oidc_token,
223
+ delivery_id=delivery_id,
224
+ broker_grant=broker_grant,
225
+ target=target,
226
+ )
227
+ return execute_github_scan_with_token(
228
+ target,
229
+ installation_token=installation_token,
230
+ external_id=delivery_id,
231
+ )
232
+
233
+
234
+ def main(argv: list[str] | None = None) -> int:
235
+ parser = argparse.ArgumentParser()
236
+ parser.add_argument(
237
+ "--payload",
238
+ default=os.environ.get("CWB_SCAN_PAYLOAD", ""),
239
+ help="base64url-encoded normalized GitHub scan target",
240
+ )
241
+ parser.add_argument(
242
+ "--token-endpoint",
243
+ default=os.environ.get("CWB_TOKEN_ENDPOINT", ""),
244
+ help="Cloudflare HTTPS installation-token broker endpoint",
245
+ )
246
+ args = parser.parse_args(argv)
247
+
248
+ try:
249
+ result = execute_actions_scan(
250
+ encoded_payload=args.payload,
251
+ token_endpoint=args.token_endpoint,
252
+ )
253
+ except (GitHubActionsWorkerError, GitHubAppRuntimeError) as exc:
254
+ print(f"CWB GitHub App scan failed: {exc}")
255
+ return 1
256
+
257
+ suffix = " (deduplicated)" if result.deduplicated else ""
258
+ print(
259
+ f"CWB GitHub App scan: {result.repository}@{result.commit_sha} "
260
+ f"-> {result.conclusion}{suffix}"
261
+ )
262
+ return 0
263
+
264
+
265
+ if __name__ == "__main__":
266
+ raise SystemExit(main())