Skip to content
Open
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
11 changes: 10 additions & 1 deletion .github/workflows/build-images.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -223,10 +223,19 @@ jobs:
needs: [generate-tag, build-backend, build-frontend, build-worker, build-mcp-server]
if: needs.generate-tag.outputs.should_push == 'true'
steps:
- name: Get GitHub App token
id: app-token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.CI_GITHUB_APP_ID }}
private-key: ${{ secrets.CI_GITHUB_APP_PRIVATE_KEY }}
owner: runwhen
repositories: infra-flux-nonprod-shared

- name: Trigger registry-test image update
uses: actions/github-script@v7
with:
github-token: ${{ secrets.PAT }}
github-token: ${{ steps.app-token.outputs.token }}
script: |
const tag = '${{ needs.generate-tag.outputs.tag }}';
await github.rest.actions.createWorkflowDispatch({
Expand Down
11 changes: 10 additions & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -234,10 +234,19 @@ jobs:
runs-on: ubuntu-latest
needs: [create-release, build-backend, build-frontend, build-worker, build-mcp-server]
steps:
- name: Get GitHub App token
id: app-token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.CI_GITHUB_APP_ID }}
private-key: ${{ secrets.CI_GITHUB_APP_PRIVATE_KEY }}
owner: runwhen
repositories: infra-flux-nonprod-shared
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Prod deploy scoped to nonprod infrastructure repository

Medium Severity

The deploy-to-prod job creates a GitHub App token scoped to infra-flux-nonprod-shared and dispatches update-registry-prod-images.yaml against that same nonprod repo. If production infrastructure lives in a separate infra-flux-prod repo, this token won't have access. The old PAT likely had broader permissions, so switching to the narrowly-scoped App token may have inadvertently locked prod deploys to the nonprod repo.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit cd5d9d8. Configure here.


- name: Trigger registry-prod image update
uses: actions/github-script@v7
with:
github-token: ${{ secrets.PAT }}
github-token: ${{ steps.app-token.outputs.token }}
script: |
const tag = '${{ needs.create-release.outputs.release_tag }}';
await github.rest.actions.createWorkflowDispatch({
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ See [mcp-server/README.md](mcp-server/README.md) for details.
| Topic | Location |
|---|---|
| Architecture | [cc-registry-v2/docs/ARCHITECTURE.md](cc-registry-v2/docs/ARCHITECTURE.md) |
| CCV catalog (image tracking + PAPI API) | [cc-registry-v2/docs/CCV.md](cc-registry-v2/docs/CCV.md) |
| Configuration | [cc-registry-v2/docs/CONFIGURATION.md](cc-registry-v2/docs/CONFIGURATION.md) |
| Indexing pipeline | [cc-registry-v2/docs/MCP_WORKFLOW.md](cc-registry-v2/docs/MCP_WORKFLOW.md) |
| Deployment | [cc-registry-v2/docs/DEPLOYMENT_GUIDE.md](cc-registry-v2/docs/DEPLOYMENT_GUIDE.md) |
Expand Down
7 changes: 5 additions & 2 deletions cc-registry-v2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ For full architecture details, see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
3. **Access the application**
- Frontend: http://localhost:3000
- Backend API: http://localhost:8001
- API Documentation: http://localhost:8001/docs
- API Documentation: http://localhost:8001/api/docs
- Task Monitor (Flower): http://localhost:5555
- MCP Server: http://localhost:8000 (optional - see [MCP Server Integration](MCP_SERVER_INTEGRATION.md))

Expand Down Expand Up @@ -213,7 +213,10 @@ codecollection-registry/

## API Documentation

Visit http://localhost:8001/docs for interactive API documentation.
Visit http://localhost:8001/api/docs for interactive API documentation. In
production the same path is exposed through the ingress at
`https://<host>/api/docs` (the frontend SPA owns `/`, so the backend's
Swagger UI is intentionally mounted under `/api/`).

## Configuration

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""add image metadata to codecollection_versions and visibility to codecollections

Adds the columns needed to track versioned OCI image artifacts per ref so the
RunWhen platform (PAPI) can consume a built-image catalog directly from the
codecollection-registry instead of running its own corestate-operator.

Also adds a `visibility` column on `codecollections` so a CC can be tracked
for image consumption but kept out of the public registry website / MCP /
AI search (e.g. customer-private, internal, deprecated CCs).

Revision ID: 004
Revises: 003
Create Date: 2026-05-11
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = "004"
down_revision = "003"
branch_labels = None
depends_on = None


def upgrade() -> None:
# --- image metadata on codecollection_versions ---
op.execute(
"""
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'codecollection_versions'
AND column_name = 'image_registry'
) THEN
ALTER TABLE codecollection_versions
ADD COLUMN image_registry VARCHAR(500),
ADD COLUMN image_tag VARCHAR(200),
ADD COLUMN image_digest VARCHAR(80),
ADD COLUMN commit_hash VARCHAR(40),
ADD COLUMN rt_revision VARCHAR(40),
ADD COLUMN image_built_at TIMESTAMP;
END IF;
END $$;
"""
)

# Index for PAPI's "latest ref for this CC" lookups.
op.execute(
"""
CREATE INDEX IF NOT EXISTS ix_ccv_collection_image_tag
ON codecollection_versions (codecollection_id, image_tag);
"""
)

# --- visibility on codecollections ---
op.execute(
"""
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'codecollections'
AND column_name = 'visibility'
) THEN
ALTER TABLE codecollections
ADD COLUMN visibility VARCHAR(20) NOT NULL DEFAULT 'public';
END IF;
END $$;
"""
)
op.execute(
"""
CREATE INDEX IF NOT EXISTS ix_cc_visibility
ON codecollections (visibility);
"""
)


def downgrade() -> None:
op.execute("DROP INDEX IF EXISTS ix_cc_visibility")
op.execute("ALTER TABLE codecollections DROP COLUMN IF EXISTS visibility")

op.execute("DROP INDEX IF EXISTS ix_ccv_collection_image_tag")
op.execute(
"""
ALTER TABLE codecollection_versions
DROP COLUMN IF EXISTS image_built_at,
DROP COLUMN IF EXISTS rt_revision,
DROP COLUMN IF EXISTS commit_hash,
DROP COLUMN IF EXISTS image_digest,
DROP COLUMN IF EXISTS image_tag,
DROP COLUMN IF EXISTS image_registry;
"""
)
70 changes: 55 additions & 15 deletions cc-registry-v2/backend/app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,35 @@ class Settings(BaseSettings):
# API Settings
API_V1_STR: str = "/api/v1"
PROJECT_NAME: str = "CodeCollection Registry"


# Config-file paths (codecollections.yaml, schedules.yaml, sources.yaml).
#
# Historically these were hardcoded to /app/<file>.yaml and mounted from
# ConfigMaps using `subPath`, which has a critical limitation: subPath
# mounts do NOT receive ConfigMap updates from the kubelet, so any
# change to the ConfigMap requires a pod restart to take effect. This
# is the bug that made the stewartshea typo "stick" in registry-test
# even after the ConfigMap was fixed.
#
# By exposing these as env-driven settings, k8s deployments can mount
# each ConfigMap as a *directory* (no subPath) and point the env vars
# at the resulting paths (e.g. /etc/cc-registry/codecollections/codecollections.yaml).
# Directory mounts auto-update; subPath mounts don't.
#
# Defaults preserve the legacy /app/<file>.yaml behavior so local dev
# (docker-compose bind-mounts) and existing deployments continue to
# work unchanged.
#
# CAVEAT: `schedules.yaml` is only loaded at Celery beat startup
# (see app/tasks/celery_app.py::load_schedules_from_yaml). Hot-reload
# via directory mount avoids the *deploy* step but you still need to
# restart the scheduler pod for new schedules to take effect.
# `codecollections.yaml` and `sources.yaml` are re-read on every task
# invocation and pick up changes within ~60s of a ConfigMap edit.
CODECOLLECTIONS_FILE: str = "/app/codecollections.yaml"
SCHEDULES_FILE: str = "/app/schedules.yaml"
SOURCES_FILE: str = "/app/sources.yaml"

@model_validator(mode='after')
def construct_urls(self):
"""Construct DATABASE_URL and REDIS_URL from components if not provided"""
Expand All @@ -84,20 +112,32 @@ def construct_urls(self):
# Fallback to default for development
self.DATABASE_URL = "postgresql://user:password@database:5432/codecollection_registry"

# Build REDIS_URL from Sentinel config or components if not provided
if not self.REDIS_URL:
if self.REDIS_SENTINEL_HOSTS:
# For Redis Sentinel, we'll use a sentinel:// URL format
# Format: sentinel://[:password@]host1:port1,host2:port2/service_name/db_number
auth = f":{self.REDIS_PASSWORD}@" if self.REDIS_PASSWORD else ""
self.REDIS_URL = f"sentinel://{auth}{self.REDIS_SENTINEL_HOSTS}/{self.REDIS_SENTINEL_MASTER}/{self.REDIS_DB}"

# Important: Don't let REDIS_URL override our explicit REDIS_DB setting
# When using Sentinel, REDIS_DB must remain as the integer/string we set explicitly
logger.info(f"Constructed Sentinel URL. REDIS_DB remains: {self.REDIS_DB} (type: {type(self.REDIS_DB)})")
else:
# Fallback to default for development
self.REDIS_URL = "redis://redis:6379/0"
# Build REDIS_URL. Sentinel ALWAYS wins when REDIS_SENTINEL_HOSTS
# is set, even if a REDIS_URL was also provided via env. Helm
# charts commonly set both, and pointing a Redis client at a
# Sentinel data-plane port (26379) results in "Only HELLO
# messages are accepted" errors on every command. See
# tasks/celery_app.py::_configure_broker_url for the matching
# Celery-side precedence rule.
if self.REDIS_SENTINEL_HOSTS:
if self.REDIS_URL:
logger.info(
"Both REDIS_SENTINEL_HOSTS and REDIS_URL are set; "
"preferring Sentinel. Remove REDIS_URL from the deployment "
"to silence this notice."
)
# Format: sentinel://[:password@]host1:port1,host2:port2/service_name/db_number
auth = f":{self.REDIS_PASSWORD}@" if self.REDIS_PASSWORD else ""
self.REDIS_URL = (
f"sentinel://{auth}{self.REDIS_SENTINEL_HOSTS}/"
f"{self.REDIS_SENTINEL_MASTER}/{self.REDIS_DB}"
)
logger.info(
f"Constructed Sentinel URL. REDIS_DB remains: {self.REDIS_DB} "
f"(type: {type(self.REDIS_DB)})"
)
elif not self.REDIS_URL:
self.REDIS_URL = "redis://redis:6379/0"

# Validate REDIS_DB is correct type
if self.REDIS_SENTINEL_HOSTS:
Expand Down
38 changes: 38 additions & 0 deletions cc-registry-v2/backend/app/core/visibility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""
Visibility filter helpers.

A CodeCollection's `visibility` flag controls whether it appears on
public-audience surfaces:

- 'public' – default. Shown on the registry website, MCP, AI search, etc.
- 'hidden' – tracked for PAPI consumption but excluded from public lists.

This is a UX/discovery toggle, NOT a security boundary. Image-level
access control still lives in the OCI registry.

Centralizing the filter here keeps the rule consistent across endpoints —
if we ever add a third visibility tier (e.g. 'archived'), we change one
place rather than auditing every router.
"""
from __future__ import annotations

from sqlalchemy.orm import Query

from app.models import CodeCollection

PUBLIC_VISIBILITY = "public"
HIDDEN_VISIBILITY = "hidden"


def public_only(query: Query) -> Query:
"""
Apply `visibility = 'public'` to a SQLAlchemy query that selects from
or joins to `codecollections`. Use this on every public-audience
endpoint (anything PAPI / corestate would NOT call).
"""
return query.filter(CodeCollection.visibility == PUBLIC_VISIBILITY)


def is_public(cc: CodeCollection) -> bool:
"""Predicate version for code paths that already have a loaded row."""
return (cc.visibility or PUBLIC_VISIBILITY) == PUBLIC_VISIBILITY
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unused exports in new visibility module

Low Severity

HIDDEN_VISIBILITY and is_public are defined in visibility.py but never imported or referenced anywhere else in the codebase. They are dead code that adds confusion about how visibility filtering is meant to be used.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2fd8dfe. Configure here.

Loading
Loading