Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ tests/
docs/
*.md

# Documentation build output (MkDocs)
_site/

# Astro / Node.js (landing page)
site/node_modules/
site/dist/
site/.astro/

# Virtual environments
.venv/
venv/
Expand Down
2 changes: 1 addition & 1 deletion .grype.yaml → .github/.grype.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Grype configuration — CVE ignore list
# Kept in sync with .trivyignore.yaml.
# Kept in sync with .github/.trivyignore.yaml.

ignore:
- vulnerability: CVE-2026-22184
Expand Down
File renamed without changes.
File renamed without changes.
1 change: 1 addition & 0 deletions .github/release-please-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"release-type": "python",
"package-name": "synthorg",
"include-component-in-tag": false,
"changelog-path": ".github/CHANGELOG.md",
"bump-minor-pre-major": true,
"bump-patch-for-minor-pre-major": true,
"changelog-sections": [
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/dependency-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,13 @@ jobs:
# MIT-0: cffi 2.0.0 — permissive (MIT variant, no attribution required)
# LicenseRef-scancode-free-unknown: aiosqlite 0.21.0, aiodocker 0.26.0,
# pycparser 3.0, sse-starlette 3.3.2 — MIT per classifiers, scancode misdetects
# LGPL-*: @img/sharp-libvips-* (Astro image optimization, build-time only)
# BlueOak-1.0.0: lru-cache, sax (Astro transitive deps, permissive)
allow-licenses: >-
MIT, MIT-0, Apache-2.0, BSD-2-Clause, BSD-3-Clause,
ISC, MPL-2.0, PSF-2.0, Unlicense, 0BSD,
CC0-1.0, Python-2.0, Python-2.0.1,
LicenseRef-scancode-free-unknown
LicenseRef-scancode-free-unknown,
LGPL-2.0-only, LGPL-2.1-only, LGPL-3.0-only, LGPL-3.0-or-later,
BlueOak-1.0.0
Comment on lines +30 to +38

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

For actions/dependency-review-actionv4, does theallow-licenses setting apply globally to all dependency changes in the pull request, or can license allowances be scoped to specific packages or manifests?

💡 Result:

In actions/dependency-review-action@v4, allow-licenses is a single, global allowlist: the action will fail the PR if it introduces any dependency whose detected license doesn’t match the allow-licenses list. There’s no built-in way to scope different license allowlists to specific packages or specific manifests. [1]

If you need exceptions for particular dependencies, the v4 action provides allow-dependencies-licenses, which lets you exclude specific packages (by purl) from license checks—but that’s package-based, not manifest-scoped. [1]

Sources
[1] actions/dependency-review-action README (Configuration options for allow-licenses and allow-dependencies-licenses) (github.com)

Citations:


Don't globally allow LGPL licenses in the dependency review.

The allow-licenses setting applies globally to every dependency change in pull requests—there's no way to scope exceptions to specific packages or only build-time dependencies. Once LGPL licenses are added to this list, any future LGPL dependency will pass the check silently, even if it's a runtime or production dependency outside the intended Astro scope.

Use allow-dependencies-licenses instead to exclude only the specific Astro packages by their package URL (purl), or move LGPL exceptions to a narrower configuration if the action supports alternative scoping mechanisms.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/dependency-review.yml around lines 30 - 38, The workflow
currently adds LGPL entries to the global allow-licenses list (the
allow-licenses key) which permits LGPL for every dependency; remove the
LGPL-2.0-only, LGPL-2.1-only, LGPL-3.0-only and LGPL-3.0-or-later entries from
allow-licenses and instead add an allow-dependencies-licenses mapping that lists
only the specific package URLs (purls) for the Astro/build-time packages (e.g.,
the `@img/sharp-libvips-`* purls) or otherwise scope the exception to build-time
packages; update the configuration so LGPL is not globally permitted while the
specific Astro package purls are explicitly allowed via
allow-dependencies-licenses.

comment-summary-in-pr: always
87 changes: 81 additions & 6 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ jobs:
contents: read
packages: write
id-token: write
outputs:
digest: ${{ steps.push.outputs.digest }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
Expand Down Expand Up @@ -103,7 +105,7 @@ jobs:
format: table
exit-code: "1"
severity: CRITICAL
trivyignores: .trivyignore.yaml
trivyignores: .github/.trivyignore.yaml

- name: Trivy scan (high — warn only)
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
Expand All @@ -113,15 +115,15 @@ jobs:
format: table
exit-code: "0"
severity: HIGH
trivyignores: .trivyignore.yaml
trivyignores: .github/.trivyignore.yaml

- name: Grype scan
uses: anchore/scan-action@7037fa011853d5a11690026fb85feee79f4c946c # v7.3.2
with:
image: ${{ steps.scan-ref.outputs.ref }}
fail-build: true
severity-cutoff: critical
config: .grype.yaml
config: .github/.grype.yaml

# Push only after scans pass — prevents publishing vulnerable images.
# NOTE: This is a separate build invocation from the scan step. GHA cache
Expand Down Expand Up @@ -162,6 +164,8 @@ jobs:
contents: read
packages: write
id-token: write
outputs:
digest: ${{ steps.push.outputs.digest }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
Expand Down Expand Up @@ -214,7 +218,7 @@ jobs:
format: table
exit-code: "1"
severity: CRITICAL
trivyignores: .trivyignore.yaml
trivyignores: .github/.trivyignore.yaml

- name: Trivy scan (high — warn only)
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
Expand All @@ -224,15 +228,15 @@ jobs:
format: table
exit-code: "0"
severity: HIGH
trivyignores: .trivyignore.yaml
trivyignores: .github/.trivyignore.yaml

- name: Grype scan
uses: anchore/scan-action@7037fa011853d5a11690026fb85feee79f4c946c # v7.3.2
with:
image: ${{ steps.scan-ref.outputs.ref }}
fail-build: true
severity-cutoff: critical
config: .grype.yaml
config: .github/.grype.yaml

# Push only after scans pass — prevents publishing vulnerable images.
# NOTE: Separate build invocation; GHA cache ensures deterministic layers,
Expand Down Expand Up @@ -263,3 +267,74 @@ jobs:
exit 1
fi
cosign sign --yes ghcr.io/aureliolo/synthorg-web@${DIGEST}

# Append container image references to the GitHub Release (version tags only)
update-release:
name: Update Release Notes
if: startsWith(github.ref, 'refs/tags/v')
needs: [version, build-backend, build-web]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Append container images to release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
VERSION: ${{ needs.version.outputs.app_version }}
BACKEND_DIGEST: ${{ needs.build-backend.outputs.digest }}
WEB_DIGEST: ${{ needs.build-web.outputs.digest }}
run: |
set -euo pipefail

# Validate required inputs
if [ -z "$VERSION" ] || [ -z "$BACKEND_DIGEST" ] || [ -z "$WEB_DIGEST" ]; then
echo "::error::Missing required values: VERSION='$VERSION' BACKEND_DIGEST='$BACKEND_DIGEST' WEB_DIGEST='$WEB_DIGEST'"
exit 1
fi

# Bounded retry — Release Please may still be creating the release
EXISTING=""
for i in $(seq 1 6); do
if EXISTING=$(gh release view "$TAG" --json body -q '.body // ""' 2>/dev/null); then
break
fi
echo "Release '$TAG' not available yet (attempt $i/6), retrying in 10s..."
sleep 10
done
Comment on lines +298 to +305

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No explicit failure path after retry exhaustion

If all 6 attempts to find the release fail, the loop exits with EXISTING="" (its initial value) and execution continues silently. The only failure point is the subsequent gh release edit "$TAG" call, which fails with a generic "could not find release" message rather than a clear "exhausted retries" diagnostic.

Consider adding an explicit check after the loop using a success flag:

EXISTING=""
RELEASE_FOUND=false
for i in $(seq 1 6); do
    if EXISTING=$(gh release view "$TAG" --json body -q '.body // ""' 2>/dev/null); then
        RELEASE_FOUND=true
        break
    fi
    echo "Release '$TAG' not available yet (attempt $i/6), retrying in 10s..."
    sleep 10
done
if [ "$RELEASE_FOUND" = "false" ]; then
    echo "::error::Release '$TAG' not found after 6 retries — cannot append container images"
    exit 1
fi

Without this, you cannot distinguish between "release found with empty body" and "release never found" from the value of EXISTING alone, since both cases leave it as "".

Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/docker.yml
Line: 298-305

Comment:
**No explicit failure path after retry exhaustion**

If all 6 attempts to find the release fail, the loop exits with `EXISTING=""` (its initial value) and execution continues silently. The only failure point is the subsequent `gh release edit "$TAG"` call, which fails with a generic "could not find release" message rather than a clear "exhausted retries" diagnostic.

Consider adding an explicit check after the loop using a success flag:

```
EXISTING=""
RELEASE_FOUND=false
for i in $(seq 1 6); do
    if EXISTING=$(gh release view "$TAG" --json body -q '.body // ""' 2>/dev/null); then
        RELEASE_FOUND=true
        break
    fi
    echo "Release '$TAG' not available yet (attempt $i/6), retrying in 10s..."
    sleep 10
done
if [ "$RELEASE_FOUND" = "false" ]; then
    echo "::error::Release '$TAG' not found after 6 retries — cannot append container images"
    exit 1
fi
```

Without this, you cannot distinguish between "release found with empty body" and "release never found" from the value of `EXISTING` alone, since both cases leave it as `""`.

How can I resolve this? If you propose a fix, please make it concise.


IMAGES=$(cat <<'BLOCK'

## Container Images

| Image | Pull |
|-------|------|
| Backend | `docker pull ghcr.io/aureliolo/synthorg-backend:VERSION_PH` |
| Web | `docker pull ghcr.io/aureliolo/synthorg-web:VERSION_PH` |

**Digests** (for pinning):
- Backend: `ghcr.io/aureliolo/synthorg-backend@BACKEND_DIGEST_PH`
- Web: `ghcr.io/aureliolo/synthorg-web@WEB_DIGEST_PH`

All images are signed with [cosign](https://github.com/sigstore/cosign). Verify with:
```bash
cosign verify ghcr.io/aureliolo/synthorg-backend@BACKEND_DIGEST_PH \
--certificate-identity-regexp='github\.com/Aureliolo/synthorg' \
--certificate-oidc-issuer='https://token.actions.githubusercontent.com'

cosign verify ghcr.io/aureliolo/synthorg-web@WEB_DIGEST_PH \
--certificate-identity-regexp='github\.com/Aureliolo/synthorg' \
--certificate-oidc-issuer='https://token.actions.githubusercontent.com'
Comment thread
coderabbitai[bot] marked this conversation as resolved.
```
Comment thread
greptile-apps[bot] marked this conversation as resolved.
BLOCK
)
# Dedent (remove leading 10-space YAML indentation) and substitute placeholders
IMAGES=$(echo "$IMAGES" | sed 's/^ //')
IMAGES=${IMAGES//VERSION_PH/$VERSION}
IMAGES=${IMAGES//BACKEND_DIGEST_PH/$BACKEND_DIGEST}
IMAGES=${IMAGES//WEB_DIGEST_PH/$WEB_DIGEST}

# Idempotent: strip existing Container Images section before appending
CLEANED=$(echo "$EXISTING" | sed '/^## Container Images$/,$d')
gh release edit "$TAG" --notes "${CLEANED}${IMAGES}"
93 changes: 93 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
name: GitHub Pages

on:
push:
branches: [main]
paths:
- "docs/**"
- "site/**"
- "mkdocs.yml"
- "src/ai_company/**"
- ".github/workflows/pages.yml"
workflow_dispatch:
Comment on lines +3 to +12

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider gating deployment on CI success.

The workflow triggers on src/ai_company/** changes to regenerate API docs from docstrings, but doesn't wait for the main CI workflow to pass. This could deploy documentation for code that fails tests or type-checking.

Consider adding a dependency on the CI workflow:

💡 Suggested workflow modification
 on:
   push:
     branches: [main]
     paths:
       - "docs/**"
       - "site/**"
       - "mkdocs.yml"
       - "src/ai_company/**"
       - ".github/workflows/pages.yml"
   workflow_dispatch:
+  workflow_run:
+    workflows: ["CI"]  # or your main CI workflow name
+    types: [completed]
+    branches: [main]

Then add a condition to skip if CI failed:

jobs:
  build:
    # Only run if triggered manually, by push, or if CI succeeded
    if: >
      github.event_name != 'workflow_run' ||
      github.event.workflow_run.conclusion == 'success'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
on:
push:
branches: [main]
paths:
- "docs/**"
- "site/**"
- "mkdocs.yml"
- "src/ai_company/**"
- ".github/workflows/pages.yml"
workflow_dispatch:
on:
push:
branches: [main]
paths:
- "docs/**"
- "site/**"
- "mkdocs.yml"
- "src/ai_company/**"
- ".github/workflows/pages.yml"
workflow_dispatch:
workflow_run:
workflows: ["CI"]
types: [completed]
branches: [main]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/pages.yml around lines 3 - 12, The workflow currently runs
on pushes to src/ai_company/** and may deploy docs even if the main CI failed;
add a workflow_run trigger for the main CI and guard the build job with an if
condition so it only runs when not invoked by a workflow_run or when the
triggering workflow_run concluded successfully: update the top-level on: to
include a workflow_run for the CI workflow name/id, and add an if: condition on
the build job (jobs.build.if) equivalent to "github.event_name != 'workflow_run'
|| github.event.workflow_run.conclusion == 'success'" so docs
regeneration/deploy is skipped when CI failed.


permissions: {}

concurrency:
group: "pages"
cancel-in-progress: false

jobs:
build:
if: github.ref == 'refs/heads/main'
name: Build Site
runs-on: ubuntu-latest
Comment thread
coderabbitai[bot] marked this conversation as resolved.
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false

# --- MkDocs (documentation at /docs) ---
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.14"
allow-prereleases: true

- name: Install uv
uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1

- name: Install docs dependencies
run: uv sync --group docs --no-dev

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI reproducibility: this workflow uses uv sync without --frozen, which can allow dependency resolution drift (and differs from the repo’s setup-python-uv action that runs uv sync --frozen). Consider adding --frozen here (and optionally reusing the composite action) so Pages builds fail fast if uv.lock is out of date.

Suggested change
run: uv sync --group docs --no-dev
run: uv sync --group docs --no-dev --frozen

Copilot uses AI. Check for mistakes.

- name: Build MkDocs
run: uv run mkdocs build --strict

# --- Astro (landing page at /) ---
- name: Set up Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "22"

- name: Install Astro dependencies
working-directory: site
run: npm ci

- name: Build Astro
working-directory: site
run: npm run build

# --- Merge outputs ---
- name: Merge Astro + MkDocs into final output
run: |
# Guard against Astro accidentally producing a docs/ route
if [ -d "site/dist/docs" ]; then
echo "::error::Astro output contains 'docs/' directory which would overwrite MkDocs output"
exit 1
fi
# Astro output goes to root (use /. to avoid glob edge cases)
cp -r site/dist/. _site/
# MkDocs output is already at _site/docs/ from the build step

- name: Upload Pages artifact
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
with:
path: _site

deploy:
if: github.ref == 'refs/heads/main'
name: Deploy
needs: build
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac553fd0d31 # v4
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,13 @@ Thumbs.db
web/node_modules/
web/dist/

# Documentation build output (MkDocs)
_site/

# Astro / Node.js (landing page)
site/node_modules/
site/dist/
site/.astro/
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# uv
.python-version
16 changes: 15 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

```bash
uv sync # install all deps (dev + test)
uv sync --group docs # install MkDocs docs toolchain
uv run ruff check src/ tests/ # lint
uv run ruff check src/ tests/ --fix # lint + auto-fix
uv run ruff format src/ tests/ # format
Expand All @@ -37,8 +38,20 @@ uv run pytest tests/ -m integration -n auto # integration tests only
uv run pytest tests/ -m e2e -n auto # e2e tests only
uv run pytest tests/ -n auto --cov=ai_company --cov-fail-under=80 # full suite + coverage
uv run pre-commit run --all-files # all pre-commit hooks
uv run mkdocs build --strict # build docs (output: _site/docs/)
uv run mkdocs serve # local docs preview (http://127.0.0.1:8000)
```

## Documentation

- **Docs source**: `docs/` (MkDocs markdown + mkdocstrings auto-generated API reference)
- **Landing page**: `site/` (Astro, Concept C hybrid design)
- **Config**: `mkdocs.yml` at repo root
- **API reference**: auto-generated from docstrings via mkdocstrings + Griffe (AST-based, no imports)
- **CI**: `.github/workflows/pages.yml` — builds Astro landing + MkDocs docs, merges, deploys to GitHub Pages
- **Architecture decision**: `docs/decisions/ADR-003-documentation-architecture.md`
- **Dependencies**: `docs` group in `pyproject.toml` (`mkdocs-material`, `mkdocstrings[python]`, `griffe-pydantic`)

## Docker

```bash
Expand Down Expand Up @@ -159,7 +172,8 @@ src/ai_company/
## CI

- **Jobs**: lint (ruff) + type-check (mypy src/ tests/) + test (pytest + coverage) run in parallel → ci-pass (gate)
- **Docker**: `.github/workflows/docker.yml` — builds backend + web images, pushes to GHCR, signs with cosign. Scans: Trivy (CRITICAL = hard fail, HIGH = warn-only) + Grype (critical cutoff). CVE triage via `.trivyignore.yaml` and `.grype.yaml`. Images only pushed after scans pass. Triggers on push to main and version tags (`v*`).
- **Pages**: `.github/workflows/pages.yml` — builds Astro landing + MkDocs docs, merges, deploys to GitHub Pages on push to main
- **Docker**: `.github/workflows/docker.yml` — builds backend + web images, pushes to GHCR, signs with cosign. Scans: Trivy (CRITICAL = hard fail, HIGH = warn-only) + Grype (critical cutoff). CVE triage via `.github/.trivyignore.yaml` and `.github/.grype.yaml`. Images only pushed after scans pass. Triggers on push to main and version tags (`v*`).
- **Matrix**: Python 3.14
- **Dependabot**: daily uv + github-actions + docker updates, grouped minor/patch, no auto-merge
- **Secret scanning**: gitleaks workflow on push/PR + weekly schedule
Expand Down
Loading
Loading