From 2bd35fa1c2d728c6389f757bfbb1d0dd58d74c14 Mon Sep 17 00:00:00 2001 From: njord Date: Sat, 18 Jul 2026 13:02:06 -0500 Subject: [PATCH] ci: fix dev-publish versioning, add pre-merge semver-label check, dedupe CodeQL, cut dev-image refs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four release-pipeline fixes: - publish-test.yml: the "Generate dev version" step still grepped/sed'd a static `version = "..."` in pyproject.toml, but the project moved to `dynamic = ["version"]` reading src/synth_panel/__version__.py. The sed silently no-oped, every push rebuilt the identical 1.5.7 wheel, and TestPyPI rejected it (400 File already exists) — 23 consecutive red runs. Now reads/writes the version file directly, asserts the base version is non-empty, and adds a post-build tripwire that fails loud if dist/ contains no .dev-suffixed artifact. - semver-label.yml (new): pre-merge "Semver Label" check that fails unless the PR carries exactly one of semver:patch|minor|major|skip. The post-merge Auto Semver Tag workflow fails loud on a missing label, but only after merge — every merge since v1.5.7 shipped unlabeled and no release was cut. This surfaces the requirement while it is still a one-click fix. (Wired as a required check separately.) - codeql.yml: drop the push-to-main trigger; every change reaches main via PR so each change was scanned twice. PR trigger + weekly cron stay. - Cut retiring dev-image refs: delete .devcontainer/ (based on ghcr.io/dataviking-tech/ai-dev-base, which the org is retiring), drop the stale devcontainer mentions from Dockerfile comments, .gitignore, .dockerignore, and docs/agent-integration-landscape.md. The published runtime image already builds FROM python:3.12-slim — no rebase needed. Co-Authored-By: Claude Fable 5 --- .devcontainer/devcontainer.json | 12 ------ .devcontainer/postCreateCommand.sh | 45 --------------------- .devcontainer/postStartCommand.sh | 42 -------------------- .dockerignore | 1 - .github/workflows/codeql.yml | 5 ++- .github/workflows/publish-test.yml | 23 +++++++++-- .github/workflows/semver-label.yml | 61 +++++++++++++++++++++++++++++ .gitignore | 4 -- Dockerfile | 4 +- docs/agent-integration-landscape.md | 13 ++---- 10 files changed, 89 insertions(+), 121 deletions(-) delete mode 100644 .devcontainer/devcontainer.json delete mode 100755 .devcontainer/postCreateCommand.sh delete mode 100755 .devcontainer/postStartCommand.sh create mode 100644 .github/workflows/semver-label.yml diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 035011a0..00000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "synthpanel Dev", - "image": "ghcr.io/dataviking-tech/ai-dev-base:edge", - "updateRemoteUserUID": false, - "containerEnv": { - "PYTHONPATH": "/workspaces/synthpanel/src", - "VIRTUAL_ENV": "/workspaces/synthpanel/.venv" - }, - "initializeCommand": "docker pull ghcr.io/dataviking-tech/ai-dev-base:edge || podman pull ghcr.io/dataviking-tech/ai-dev-base:edge || echo No container runtime found, skipping pre-pull", - "postCreateCommand": "bash .devcontainer/postCreateCommand.sh", - "postStartCommand": "bash .devcontainer/postStartCommand.sh" -} diff --git a/.devcontainer/postCreateCommand.sh b/.devcontainer/postCreateCommand.sh deleted file mode 100755 index a8586cc8..00000000 --- a/.devcontainer/postCreateCommand.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bash -# postCreateCommand: runs ONCE on container creation. -# Sets up synthpanel for development and testing. -set -euo pipefail - -echo "Setting up synthpanel development environment..." - -cd /workspaces/synthpanel - -# Initialize uv project (creates .venv if needed, generates uv.lock) -echo "Initializing uv project..." -uv lock 2>&1 || echo "uv lock failed, continuing..." - -# Install the package in editable mode with all extras -echo "Installing synthpanel (editable)..." -uv pip install -e ".[dev,mcp]" 2>&1 || uv pip install -e . 2>&1 || { - echo "uv pip install failed, falling back to PYTHONPATH mode" -} - -# Verify the synthpanel entry point is available -if command -v synthpanel >/dev/null 2>&1; then - echo "synthpanel CLI installed and on PATH" -elif [ -x ".venv/bin/synthpanel" ]; then - echo "synthpanel installed in .venv (activate with: source .venv/bin/activate)" -else - echo "Warning: synthpanel entry point not found" - echo " Use: PYTHONPATH=src python3 -m synth_panel" -fi - -# Verify CLI works -if synthpanel --help >/dev/null 2>&1 || .venv/bin/synthpanel --help >/dev/null 2>&1; then - echo "synthpanel CLI is working" -else - echo "Warning: synthpanel CLI failed to load" -fi - -echo "synthpanel dev environment ready" -echo "" -echo "Quick start:" -echo " export ANTHROPIC_API_KEY='sk-...'" -echo " synthpanel prompt 'Say hello'" -echo " synthpanel panel run --personas examples/personas.yaml --instrument examples/survey.yaml" -echo "" -echo "MCP server:" -echo " synthpanel mcp-serve" diff --git a/.devcontainer/postStartCommand.sh b/.devcontainer/postStartCommand.sh deleted file mode 100755 index 3b68c9e3..00000000 --- a/.devcontainer/postStartCommand.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env bash -# postStartCommand: runs on EVERY container start. -# Ensures synthpanel is ready and prints status. -set -euo pipefail - -# Ensure every new terminal activates the venv -BASHRC_SNIPPET='# synthpanel venv activation -if [ -d "/workspaces/synthpanel/.venv" ]; then - export VIRTUAL_ENV="/workspaces/synthpanel/.venv" - export PATH="$VIRTUAL_ENV/bin:$PATH" -fi -export PYTHONPATH="${PYTHONPATH:-/workspaces/synthpanel/src}"' - -if ! grep -q "synthpanel venv activation" ~/.bashrc 2>/dev/null; then - echo "$BASHRC_SNIPPET" >> ~/.bashrc -fi - -# Activate for this script too -if [ -d "/workspaces/synthpanel/.venv" ]; then - export VIRTUAL_ENV="/workspaces/synthpanel/.venv" - export PATH="$VIRTUAL_ENV/bin:$PATH" -fi - -export PYTHONPATH="${PYTHONPATH:-/workspaces/synthpanel/src}" - -# Quick health check -if python3 -m synth_panel --help >/dev/null 2>&1; then - echo "synthpanel is ready" -else - echo "Warning: synthpanel not working, try: uv sync" -fi - -# Check for API keys -if [ -n "${ANTHROPIC_API_KEY:-}" ]; then - echo "Anthropic API key is set" -elif [ -n "${OPENAI_API_KEY:-}" ]; then - echo "OpenAI API key is set" -elif [ -n "${GOOGLE_API_KEY:-}${GEMINI_API_KEY:-}" ]; then - echo "Google/Gemini API key is set" -else - echo "No API key set. Export one of: ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY" -fi diff --git a/.dockerignore b/.dockerignore index 786904f0..8a534d1d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,7 +6,6 @@ .github .gitignore .pre-commit-config.yaml -.devcontainer .claude .claude-plugin .runtime diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index c5d5e7a7..730c19d5 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,8 +1,9 @@ name: CodeQL +# No `push: branches: [main]` trigger: every change reaches main through a +# PR, so a push trigger would scan each change twice. The weekly cron covers +# newly-published CodeQL queries against an unchanged main. on: - push: - branches: [main] pull_request: branches: [main] schedule: diff --git a/.github/workflows/publish-test.yml b/.github/workflows/publish-test.yml index 576a28b9..8c3d07bc 100644 --- a/.github/workflows/publish-test.yml +++ b/.github/workflows/publish-test.yml @@ -1,7 +1,7 @@ name: Publish Dev to TestPyPI # Every merge to main publishes a dev build to test.pypi.org. -# Version is auto-generated: {pyproject.toml version}.dev{run_number} +# Version is auto-generated: {src/synth_panel/__version__.py version}.dev{run_number} # # ONE-TIME MANUAL SETUP (required before the first successful publish): # @@ -47,9 +47,19 @@ jobs: id: version run: | set -euo pipefail - BASE=$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/') + # pyproject.toml declares `dynamic = ["version"]` and reads the + # version from src/synth_panel/__version__.py, so that file — not + # pyproject.toml — is what must be rewritten here. The previous + # pyproject-based sed silently no-oped after the dynamic-version + # migration and every push re-uploaded the same wheel, which + # TestPyPI rejected with "400 File already exists". + BASE=$(sed -n 's/^__version__ = "\(.*\)"$/\1/p' src/synth_panel/__version__.py) + if [ -z "$BASE" ]; then + echo "::error::Could not read base version from src/synth_panel/__version__.py" + exit 1 + fi DEV_VERSION="${BASE}.dev${GITHUB_RUN_NUMBER}" - sed -i "s/^version = \".*\"/version = \"${DEV_VERSION}\"/" pyproject.toml + printf '__version__ = "%s"\n' "$DEV_VERSION" > src/synth_panel/__version__.py echo "version=$DEV_VERSION" >> "$GITHUB_OUTPUT" echo "Dev version: $DEV_VERSION" @@ -59,6 +69,13 @@ jobs: - name: Build package run: python -m build + - name: Assert dev suffix applied + run: | + # Tripwire: if the version rewrite above ever silently no-ops again + # (e.g. the version source moves), fail loud here instead of letting + # TestPyPI reject a duplicate upload with an opaque 400. + ls dist/*.dev* >/dev/null || { echo "::error::dev suffix not applied"; exit 1; } + - name: Show built artifacts run: ls -lh dist/ diff --git a/.github/workflows/semver-label.yml b/.github/workflows/semver-label.yml new file mode 100644 index 00000000..84f0fd18 --- /dev/null +++ b/.github/workflows/semver-label.yml @@ -0,0 +1,61 @@ +name: Semver Label + +# Pre-merge guard: every PR must carry exactly one semver:* label +# (semver:patch, semver:minor, semver:major, or semver:skip) BEFORE merge. +# +# The post-merge Auto Semver Tag workflow (auto-tag.yml) fails loud when a +# merged PR lacks a label — but by then the omission has already shipped and +# needs a manual label + rerun to recover. This check surfaces the same +# requirement on the PR itself, while it can still be fixed with one click. +# It is wired as a required status check on main separately (branch ruleset, +# managed in dataviking-infra) — this file only defines the check. +# +# `labeled` / `unlabeled` are in the trigger list so the check re-evaluates +# immediately when labels change; the event payload's label set reflects the +# state after the change. + +on: + pull_request: + types: [opened, reopened, synchronize, labeled, unlabeled] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + semver-label: + name: Semver Label + runs-on: ubuntu-latest + steps: + - name: Require exactly one semver label + env: + LABELS_JSON: ${{ toJSON(github.event.pull_request.labels.*.name) }} + run: | + set -euo pipefail + echo "PR labels: ${LABELS_JSON}" + + SEMVER_LABELS=$(echo "$LABELS_JSON" | jq -r '.[] | select(startswith("semver:"))') + COUNT=$(echo "$SEMVER_LABELS" | grep -c . || true) + + if [ "$COUNT" -eq 0 ]; then + echo "::error::No semver label on this PR. Add exactly one of: semver:patch, semver:minor, semver:major, semver:skip." + exit 1 + fi + + if [ "$COUNT" -gt 1 ]; then + echo "::error::Multiple semver labels found: $(echo "$SEMVER_LABELS" | tr '\n' ' '). Use exactly one." + exit 1 + fi + + case "$SEMVER_LABELS" in + semver:patch|semver:minor|semver:major|semver:skip) + echo "Semver label OK: $SEMVER_LABELS" + ;; + *) + echo "::error::Unrecognized semver label '$SEMVER_LABELS'. Valid labels: semver:patch, semver:minor, semver:major, semver:skip." + exit 1 + ;; + esac diff --git a/.gitignore b/.gitignore index 5ee1d533..618e627d 100644 --- a/.gitignore +++ b/.gitignore @@ -32,10 +32,6 @@ state.json gastown-*.json CLAUDE.local.md -# DevContainer temp files -.devcontainer/.env -.devcontainer/tmp/ - # synthpanel data .synthpanel/ sessions/ diff --git a/Dockerfile b/Dockerfile index 93198498..8f6554e8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,9 +3,7 @@ # Production runtime image for synthpanel. # # This image is sized for ephemeral / serverless invocation (Lambda, Cloud Run, -# GitHub Actions, n8n, etc.) — small footprint, no editor tooling. The -# devcontainer at .devcontainer/devcontainer.json remains the source of truth -# for *development*; this file is a slimmed-down production sibling. +# GitHub Actions, n8n, etc.) — small footprint, no editor tooling. # # Default CMD launches the MCP server on stdio. Override with any synthpanel # subcommand: diff --git a/docs/agent-integration-landscape.md b/docs/agent-integration-landscape.md index 96e83610..1f9367bf 100644 --- a/docs/agent-integration-landscape.md +++ b/docs/agent-integration-landscape.md @@ -45,7 +45,7 @@ import. ## A. Current Integration Surfaces (Inventory) -SynthPanel ships six integration surfaces today: +SynthPanel ships five integration surfaces today: | Surface | State | Agent-Reachable? | |---|---|---| @@ -54,7 +54,6 @@ SynthPanel ships six integration surfaces today: | **MCP server** (12 tools, 4 resources, 3 prompts) | FastMCP, stdio transport, defaults to haiku | Yes — primary agent interface | | **Claude Code plugin** (`.claude-plugin/plugin.json`) | Wraps MCP server + ships `/focus-group` skill | Yes — Claude Code only | | **Claude Code skill** (`skills/focus-group/SKILL.md`) | Structured focus-group workflow | Yes — Claude Code only | -| **Devcontainer** (`.devcontainer/devcontainer.json`) | GitHub Codespaces / dev environments | Partial — dev-only, not agent-facing | **Key observations:** - The MCP server is the only general-purpose agent interface. @@ -270,14 +269,11 @@ docker run -e ANTHROPIC_API_KEY=sk-... synthpanel/synthpanel mcp-serve **Why it matters:** - Agents in serverless environments (Lambda, Cloud Run, GitHub Actions) can spin up a SynthPanel container as a tool-call target. -- Devcontainer already exists (`.devcontainer/devcontainer.json`) with a - base image (`ghcr.io/dataviking-tech/ai-dev-base:edge`). A production - image is a subset. - n8n, Zapier, and custom orchestrators that support Docker-based tools get a zero-install path. -**Effort:** 1-2 days. Dockerfile based on existing devcontainer. Publish to -GHCR and Docker Hub. +**Effort:** 1-2 days. Dockerfile based on an official `python:*-slim` image. +Publish to GHCR and Docker Hub. **Impact:** MEDIUM. Enables specific use cases (serverless, ephemeral) but most developers will pip-install. Worth doing after the top 3. @@ -374,7 +370,7 @@ in examples that prove MCP works. | REST API | No | Yes | Yes | | Custom GPT | No | Unknown | Unknown | | LangChain/CrewAI wrapper | No (MCP bridge works) | No | No | -| Docker image | No (devcontainer exists) | Unknown | Unknown | +| Docker image | No | Unknown | Unknown | | Composio listing | No | No | No | | Open source | Yes (MIT) | Yes (MIT) | No (SaaS) | | Pricing | Free (BYOK) | SaaS | SaaS | @@ -468,7 +464,6 @@ they form a complete strategy: - `src/synth_panel/mcp/server.py`: 12 tool handlers wrapping orchestrator functions - `.claude-plugin/plugin.json`: MCP server config + skill reference - `skills/focus-group/SKILL.md`: 5-step workflow skill -- `.devcontainer/devcontainer.json`: `ghcr.io/dataviking-tech/ai-dev-base:edge` ---