Skip to content

Commit 5050d26

Browse files
authored
Merge pull request #29 from typelicious/codex/feature/v0.5-onboarding-publish-2026-03-12
feat: add v0.5 onboarding and publishing baseline
2 parents 4e106c7 + 16b4fb4 commit 5050d26

14 files changed

Lines changed: 333 additions & 8 deletions

.github/workflows/ci.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,9 @@ jobs:
4040
python-version: "3.12"
4141
- name: Build Python package
4242
run: |
43-
python -m pip install --upgrade pip build
43+
python -m pip install --upgrade pip build twine
4444
python -m build
45+
python -m twine check dist/*
4546
4647
lint:
4748
runs-on: ubuntu-latest
@@ -53,3 +54,4 @@ jobs:
5354
- run: pip install ruff
5455
- run: ruff check .
5556
- run: ruff format --check .
57+
- run: bash -n scripts/*
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: Publish Dry Run
2+
3+
on:
4+
workflow_dispatch:
5+
pull_request:
6+
branches: [main]
7+
paths:
8+
- "Dockerfile"
9+
- ".dockerignore"
10+
- "pyproject.toml"
11+
- "README.md"
12+
- "RELEASES.md"
13+
- ".github/workflows/publish-dry-run.yml"
14+
- ".github/workflows/release-artifacts.yml"
15+
16+
jobs:
17+
python-publish-dry-run:
18+
runs-on: ubuntu-latest
19+
steps:
20+
- uses: actions/checkout@v4
21+
- uses: actions/setup-python@v5
22+
with:
23+
python-version: "3.12"
24+
- name: Build distributions
25+
run: |
26+
python -m pip install --upgrade pip build twine
27+
python -m build
28+
- name: Validate Python distributions
29+
run: python -m twine check dist/*
30+
- name: Upload dry-run Python artifacts
31+
uses: actions/upload-artifact@v4
32+
with:
33+
name: python-publish-dry-run
34+
path: dist/*
35+
36+
ghcr-publish-dry-run:
37+
runs-on: ubuntu-latest
38+
steps:
39+
- uses: actions/checkout@v4
40+
- uses: docker/setup-buildx-action@v3
41+
- uses: docker/metadata-action@v5
42+
id: meta
43+
with:
44+
images: ghcr.io/${{ github.repository }}
45+
tags: |
46+
type=raw,value=dry-run
47+
type=sha
48+
- name: Build container image without pushing
49+
uses: docker/build-push-action@v6
50+
with:
51+
context: .
52+
file: ./Dockerfile
53+
push: false
54+
tags: ${{ steps.meta.outputs.tags }}
55+
labels: ${{ steps.meta.outputs.labels }}

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ The format is intentionally lightweight and human-readable. Group entries by rel
1111
- Added `contract: image-provider` plus an OpenAI-compatible `POST /v1/images/generations` path for image-capable providers
1212
- Added a shipped Dockerfile and tag-driven release-artifacts workflow for Python distributions, GHCR images, and optional PyPI publishing
1313
- Added public community-health and security baseline files: Code of Conduct, Security Policy, issue templates, PR template, Dependabot, and CodeQL
14+
- Added generic onboarding helpers (`foundrygate-bootstrap`, `foundrygate-doctor`) and a publish-dry-run workflow for GHCR and Python package validation
1415

1516
## v0.4.0 - 2026-03-12
1617

README.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
[![OpenClaw-friendly](https://img.shields.io/badge/OpenClaw-friendly-111827.svg)](https://openclaw.ai/)
1010
[![Docker](https://img.shields.io/badge/docker-ready-2496ED?logo=docker&logoColor=white)](./Dockerfile)
1111
[![PyPI](https://img.shields.io/badge/pypi-workflow%20ready-3775A9?logo=pypi&logoColor=white)](./RELEASES.md)
12+
[![Publish Dry Run](https://github.com/typelicious/FoundryGate/actions/workflows/publish-dry-run.yml/badge.svg)](https://github.com/typelicious/FoundryGate/actions/workflows/publish-dry-run.yml)
1213
[![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](./pyproject.toml)
1314

1415
## Quick Navigation
@@ -24,6 +25,7 @@
2425
- [Configuration](#configuration)
2526
- [Deployment](#deployment)
2627
- [Helper Scripts](#helper-scripts)
28+
- [Publishing](#publishing)
2729
- [Community And Security](#community-and-security)
2830
- [Repo Safety And CI](#repo-safety-and-ci)
2931
- [Workflow](#workflow)
@@ -83,6 +85,14 @@ curl -fsS http://127.0.0.1:8090/health
8385
curl -fsS http://127.0.0.1:8090/v1/models
8486
```
8587

88+
If you want the fastest local bootstrap, use the generic helpers first:
89+
90+
```bash
91+
./scripts/foundrygate-bootstrap
92+
$EDITOR .env
93+
./scripts/foundrygate-doctor
94+
```
95+
8696
If you prefer the Linux service path instead of a manual Python run, jump to [Helper Scripts](#helper-scripts) and use `./scripts/foundrygate-install`.
8797

8898
If you install the project as a package, the `foundrygate` and `foundrygate-stats` console scripts are available.
@@ -94,6 +104,7 @@ If every configured provider API key is empty, FoundryGate still starts, but it
94104
- [Architecture](./docs/ARCHITECTURE.md)
95105
- [Integrations](./docs/INTEGRATIONS.md)
96106
- [Onboarding](./docs/ONBOARDING.md)
107+
- [Publishing](./docs/PUBLISHING.md)
97108
- [Troubleshooting](./docs/TROUBLESHOOTING.md)
98109
- [Roadmap](./docs/FOUNDRYGATE-ROADMAP.md)
99110

@@ -641,14 +652,36 @@ python -m build
641652

642653
Tagged releases always build Python artifacts. PyPI publishing is wired behind the repository variable `PYPI_PUBLISH=true` plus GitHub trusted publishing for the `pypi` environment.
643654

655+
## Publishing
656+
657+
FoundryGate now has a real publish dry-run path for both Python artifacts and the container image.
658+
659+
GitHub workflow:
660+
661+
- [publish-dry-run](./.github/workflows/publish-dry-run.yml) builds the wheel and sdist, runs `twine check`, and builds the GHCR image without pushing it
662+
- [release-artifacts](./.github/workflows/release-artifacts.yml) is still the tag-driven publish path for real releases
663+
664+
Local dry-run commands:
665+
666+
```bash
667+
python -m pip install --upgrade build twine
668+
python -m build
669+
python -m twine check dist/*
670+
docker build -t foundrygate:dry-run .
671+
```
672+
673+
Use the dry run before cutting a release when Docker, packaging metadata, or release automation changed.
674+
644675
## Helper Scripts
645676

646-
The scripts in [scripts](./scripts) are optional wrappers around `systemd`, `journalctl`, and `curl`. They are most useful on Linux hosts that already use the included `systemd` unit.
677+
The scripts in [scripts](./scripts) are optional wrappers around onboarding, `systemd`, `journalctl`, and `curl`.
647678

648679
Running `./scripts/foundrygate-install` also creates symlinks in `/usr/local/bin`.
649680

650681
| Script | What it does |
651682
| --- | --- |
683+
| `foundrygate-bootstrap` | Creates `.env` from `.env.example` if needed, creates a local state dir, and appends a safe local `FOUNDRYGATE_DB_PATH` if none is set |
684+
| `foundrygate-doctor` | Checks for config/env presence, writable DB path, at least one configured provider key, and optional local health endpoints |
652685
| `foundrygate-install` | Installs the unit file, creates `/var/lib/foundrygate`, creates helper symlinks, reloads `systemd`, and starts the service |
653686
| `foundrygate-start` | Runs `systemctl start foundrygate.service` |
654687
| `foundrygate-stop` | Runs `systemctl stop foundrygate.service` |
@@ -683,6 +716,7 @@ Security automation and review baseline:
683716
FoundryGate includes two GitHub Actions workflows:
684717

685718
- [CI](./.github/workflows/ci.yml): runs Ruff plus the test matrix on Python 3.10 through 3.13
719+
- [publish-dry-run](./.github/workflows/publish-dry-run.yml): validates Python distributions and the container build without publishing them
686720
- [release-artifacts](./.github/workflows/release-artifacts.yml): builds Python distributions on tags, pushes container images to GHCR, and can publish to PyPI when trusted publishing is configured
687721
- [repo-safety](./.github/workflows/repo-safety.yml): rejects accidental artifacts and secrets-like files
688722
- [CodeQL](./.github/workflows/codeql.yml): performs repository code scanning for Python

RELEASES.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ This repo does not require a heavy release process. Use lightweight tags plus Gi
1414
6. Create a GitHub Release from that tag.
1515
7. Use the changelog entry as the release notes, then add any short upgrade notes if needed.
1616
8. Confirm that README plus the relevant docs pages still match the shipped runtime behavior.
17+
9. If packaging or Docker changed shortly before the release, run the publish dry run first.
1718

1819
## Example
1920

@@ -34,6 +35,12 @@ Tagged releases now trigger [release-artifacts](./.github/workflows/release-arti
3435
- push the container image to GHCR
3536
- publish to PyPI only when `PYPI_PUBLISH=true` is set and GitHub trusted publishing is configured for the `pypi` environment
3637

38+
The repo also includes [publish-dry-run](./.github/workflows/publish-dry-run.yml):
39+
40+
- build Python distributions without publishing them
41+
- run `twine check`
42+
- build the GHCR image without pushing it
43+
3744
## Versioning Guidance
3845

3946
- Use `x.y.z` version numbers and matching `vx.y.z` Git tags.

docs/FOUNDRYGATE-ROADMAP.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ Primary goals:
182182
- publish an official Docker release path
183183
- publish FoundryGate to PyPI
184184
- add provider and client onboarding helpers for many-provider and many-client deployments
185+
- add a publish dry-run path for Python package and GHCR validation before real release tags
185186
- add validation workflows so operators can catch config mistakes before rollout
186187
- complete the public community-health baseline and security-overview baseline for the repo
187188

docs/ONBOARDING.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,23 @@ The safest onboarding order is:
88

99
1. one provider
1010
2. one client
11-
3. observability
11+
3. bootstrap + diagnostics
1212
4. second provider
1313
5. client-specific defaults
1414
6. policy constraints
1515

1616
## Provider onboarding sequence
1717

18+
### 0. Bootstrap the local checkout
19+
20+
Run the generic helpers before changing config:
21+
22+
```bash
23+
./scripts/foundrygate-bootstrap
24+
$EDITOR .env
25+
./scripts/foundrygate-doctor
26+
```
27+
1828
### 1. Add one provider
1929

2030
- define the provider in `config.yaml`
@@ -93,6 +103,7 @@ Current state:
93103

94104
- manual updates via Git or `foundrygate-update`
95105
- tag-driven release artifacts for Python distributions and container images
106+
- publish dry-run workflow for Python packaging and GHCR container builds
96107

97108
Planned state:
98109

docs/PUBLISHING.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# FoundryGate Publishing
2+
3+
## Goal
4+
5+
Keep release publishing boring and repeatable.
6+
7+
FoundryGate currently ships through:
8+
9+
- Git tags and GitHub Releases
10+
- Python distributions (`sdist` and `wheel`)
11+
- a GHCR container image
12+
13+
PyPI remains opt-in and only publishes when trusted publishing is configured and `PYPI_PUBLISH=true` is set at the repository level.
14+
15+
## Dry-Run Path
16+
17+
Use the dry-run path whenever packaging, Docker, or release automation changes.
18+
19+
### GitHub
20+
21+
The repo includes [publish-dry-run](../.github/workflows/publish-dry-run.yml):
22+
23+
- builds the Python package
24+
- runs `twine check dist/*`
25+
- builds the container image through `docker/build-push-action`
26+
- does not push to GHCR
27+
- does not publish to PyPI
28+
29+
### Local
30+
31+
```bash
32+
python -m pip install --upgrade build twine
33+
python -m build
34+
python -m twine check dist/*
35+
docker build -t foundrygate:dry-run .
36+
```
37+
38+
## Real Release Path
39+
40+
The real publish flow stays tag-driven through [release-artifacts](../.github/workflows/release-artifacts.yml):
41+
42+
1. cut the release PR and merge it to `main`
43+
2. tag the release from `main`
44+
3. push the tag
45+
4. let `release-artifacts` build Python distributions and the GHCR image
46+
5. publish the GitHub Release
47+
6. optionally allow PyPI publication through trusted publishing
48+
49+
## Trust Boundaries
50+
51+
- Dry-run workflows should never require production credentials.
52+
- Real release publication should use GitHub environments and trusted publishing instead of long-lived secrets where possible.
53+
- PyPI publication should remain opt-in until the package workflow is stable across several releases.

foundrygate/main.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,28 @@
3131
_metrics: MetricsStore
3232

3333

34+
def _client_error_response(message: str, *, error_type: str, status_code: int) -> JSONResponse:
35+
"""Return a client-facing JSON error without exposing internal exception details."""
36+
return JSONResponse({"error": message, "type": error_type}, status_code=status_code)
37+
38+
39+
def _request_hook_error_response(exc: Exception) -> JSONResponse:
40+
"""Return a sanitized request-hook failure response."""
41+
logger.warning("Request hook processing failed: %s", exc)
42+
return _client_error_response(
43+
"Request hook processing failed",
44+
error_type="request_hook_error",
45+
status_code=500,
46+
)
47+
48+
49+
def _invalid_request_response(message: str, *, exc: Exception | None = None) -> JSONResponse:
50+
"""Return a sanitized invalid-request response."""
51+
if exc is not None:
52+
logger.info("Invalid request rejected: %s", exc)
53+
return _client_error_response(message, error_type="invalid_request_error", status_code=400)
54+
55+
3456
async def _refresh_local_worker_probes(force: bool = False) -> None:
3557
"""Refresh local-worker health state when probes are due."""
3658
timeout_seconds = float(_config.health.get("timeout_seconds", 10))
@@ -537,7 +559,7 @@ async def preview_route(request: Request):
537559
effective_body,
538560
) = await _resolve_route_preview(body, headers)
539561
except HookExecutionError as exc:
540-
return JSONResponse({"error": str(exc), "type": "request_hook_error"}, status_code=500)
562+
return _request_hook_error_response(exc)
541563

542564
return {
543565
"requested_model": model_requested,
@@ -578,9 +600,9 @@ async def image_generations(request: Request):
578600
effective_body,
579601
) = await _resolve_image_route_preview(body, headers)
580602
except HookExecutionError as exc:
581-
return JSONResponse({"error": str(exc), "type": "request_hook_error"}, status_code=500)
603+
return _request_hook_error_response(exc)
582604
except ValueError as exc:
583-
return JSONResponse({"error": str(exc), "type": "invalid_request_error"}, status_code=400)
605+
return _invalid_request_response("Invalid image generation request", exc=exc)
584606

585607
prompt = effective_body["prompt"].strip()
586608
image_fields = _collect_image_request_fields(effective_body)
@@ -690,7 +712,7 @@ async def chat_completions(request: Request):
690712
effective_body,
691713
) = await _resolve_route_preview(body, headers)
692714
except HookExecutionError as exc:
693-
return JSONResponse({"error": str(exc), "type": "request_hook_error"}, status_code=500)
715+
return _request_hook_error_response(exc)
694716
messages = effective_body.get("messages", [])
695717
stream = effective_body.get("stream", False)
696718
temperature = effective_body.get("temperature")

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ dev = [
3636
"pytest-asyncio>=0.24",
3737
"httpx", # for TestClient
3838
"ruff>=0.8",
39+
"twine>=6.1",
3940
]
4041

4142
[project.scripts]

0 commit comments

Comments
 (0)