Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
79a8e62
Initial plan
Copilot May 6, 2026
47ead0a
feat: implement external case-result endpoints with auto-upsert and t…
Copilot May 6, 2026
4dccab4
chore: align migration uniqueness constraint with model naming
Copilot May 6, 2026
d27f09b
Initial plan
Copilot May 6, 2026
922ee20
Fix flaky e2e CRUD target selection to avoid seeded row pollution
Copilot May 6, 2026
2847f5a
Merge pull request #309 from bg-playground/copilot/fix-flaky-e2e-trac…
bg-playground May 7, 2026
da61b43
Initial plan
Copilot May 7, 2026
52bd6e4
fix(e2e): harden seed loading and add pre-test seed smoke check
Copilot May 7, 2026
b6af208
chore(e2e): simplify seeded external_id extraction in global setup
Copilot May 7, 2026
3826c32
chore(seed): use current_schema for auto_registered detection
Copilot May 7, 2026
c9ba7d6
Merge pull request #312 from bg-playground/copilot/bgstm-311-fix-e2e-…
bg-playground May 7, 2026
5d6923b
Initial plan
Copilot May 7, 2026
9d263c2
feat: add actor-aware audit logging for external results sessions
Copilot May 7, 2026
4a570eb
Merge pull request #313 from bg-playground/copilot/297-wire-audit-log…
bg-playground May 7, 2026
b16c0f4
Initial plan
Copilot May 7, 2026
028005b
feat: add external results smoke workflow and assertions
Copilot May 7, 2026
1f3fe6d
chore: refine smoke workflow reliability and bootstrap errors
Copilot May 7, 2026
3e87ac0
chore: align smoke scripts with typing conventions
Copilot May 7, 2026
3257dee
chore: polish smoke script diagnostics and style
Copilot May 7, 2026
447c8d2
fix: unblock smoke bootstrap runner-token issuance
Copilot May 7, 2026
f4a5451
chore: add smoke diagnostics and bootstrap fallback note
Copilot May 7, 2026
fe6ad1b
fix: make smoke assertions audit-only for case data
Copilot May 7, 2026
ac367f9
chore: annotate audit-only case reconstruction follow-up
Copilot May 7, 2026
dfc6be6
fix: accept missing runner in external session create
Copilot May 8, 2026
09dae73
chore: tighten runner fallback semantics and test naming
Copilot May 8, 2026
53c0631
fix: stabilize smoke audit-log diagnostics and details parsing
Copilot May 8, 2026
506e0c4
chore: use pytest monkeypatch fixture in audit-log test
Copilot May 8, 2026
46721ef
chore: use timezone-aware timestamp in audit-log test fixture
Copilot May 8, 2026
b37fbe4
chore: document audit-log invalid-details fallback behavior
Copilot May 8, 2026
b8fbd43
chore: bump frameworks smoke pin and relax v0.1 requirement assertion
Copilot May 8, 2026
5ddf90a
chore: clarify spec wording for fixed reporter SHA pin
Copilot May 8, 2026
b5b2f92
Initial plan
Copilot May 8, 2026
2f7c14d
feat: add external case-result endpoints with CRUD, model, migration,…
Copilot May 8, 2026
d9c3ff5
fix: use generic auth.invalid code for dual-auth dependency
Copilot May 8, 2026
c8beda2
chore: refine dual-auth invalid credential error code
Copilot May 8, 2026
df18dcc
fix: preserve unresolved requirement ids on case idempotent collisions
Copilot May 8, 2026
d6fcee4
refactor: document dual-auth behavior and centralize requirement-id r…
Copilot May 8, 2026
612c762
chore: use runner token id when auto-registering external test cases
Copilot May 8, 2026
eefe4b4
fix: prevent case_outcome enum double-create in migration and align m…
Copilot May 8, 2026
d02d554
Merge pull request #317 from bg-playground/copilot/303-case-result-en…
bg-playground May 8, 2026
a242e66
Initial plan
Copilot May 8, 2026
218c612
feat(#298): artifact upload endpoint + pluggable storage backend
Copilot May 8, 2026
90cc027
fix: remove unnecessary type: ignore[arg-type] comment
Copilot May 8, 2026
a2adc37
fix: path traversal, streaming scope doc, index name alignment (revie…
Copilot May 8, 2026
3a8991f
fix: tighten filename regex, add dot-filename tests, add index comment
Copilot May 8, 2026
d2501a2
Merge pull request #319 from bg-playground/copilot/add-artifacts-uplo…
bg-playground May 8, 2026
4a5f9e4
Merge remote-tracking branch 'origin/main' into copilot/bgstm-295-add…
Copilot May 8, 2026
2ef55a7
test: assert artifact metadata fields in smoke snapshot validation
Copilot May 8, 2026
e893b5e
fix: add alembic merge revision for external-results heads
Copilot May 8, 2026
6fb03ec
chore: align merge migration typing with Python 3.11 style
Copilot May 8, 2026
e2c0385
Merge pull request #314 from bg-playground/copilot/bgstm-295-add-smok…
bg-playground May 8, 2026
a7daae8
Initial plan
Copilot May 8, 2026
3711f37
Initial plan
Copilot May 8, 2026
e337c5a
docs(spec): remove unimplemented artifact dedup references
Copilot May 8, 2026
f56af4f
feat: add projects API and enforce external session project FK valida…
Copilot May 8, 2026
01a22fc
Merge pull request #323 from bg-playground/copilot/remove-sha-256-ded…
bg-playground May 8, 2026
ac5dd0b
Merge pull request #322 from bg-playground/copilot/add-projects-api
bg-playground May 8, 2026
477318c
Initial plan
Copilot May 8, 2026
07d6cdd
feat: resolve requirement external ids on external case results
Copilot May 8, 2026
7d4c3c4
refactor: streamline requirement external id deduplication
Copilot May 8, 2026
1182626
fix: return linked requirement ids from case result link helper
Copilot May 8, 2026
32c3b98
Merge pull request #324 from bg-playground/copilot/add-requirement-ex…
bg-playground May 8, 2026
eedb7c6
Initial plan
Copilot May 8, 2026
7c3b947
Wire smoke workflow end-to-end for requirement external IDs (#316 PR …
Copilot May 8, 2026
2ba90dd
Merge pull request #326 from bg-playground/copilot/add-smoke-workflow…
bg-playground May 8, 2026
4880575
Initial plan
Copilot May 8, 2026
5454aa9
fix(frontend): stabilize RBAC forbidden checks with admin-route testid
Copilot May 8, 2026
ef03b25
Merge pull request #327 from bg-playground/copilot/fix-e2e-test-admin…
bg-playground May 8, 2026
355f93c
Initial plan
Copilot May 8, 2026
69edf19
fix(e2e): improve globalSetup error messages with container-log debug…
Copilot May 8, 2026
c37e0ed
Merge pull request #328 from bg-playground/copilot/fix-e2e-silent-see…
bg-playground May 9, 2026
06d8778
Initial plan
Copilot May 9, 2026
f3b1ae8
test: add audit-emission tests and regression-proof enforcement test;…
Copilot May 9, 2026
0998550
fix: rename _fake_storage_result to fake_storage_result in enforcemen…
Copilot May 9, 2026
daa54ad
Merge pull request #330 from bg-playground/copilot/add-audit-emission…
bg-playground May 9, 2026
c507e89
Initial plan
Copilot May 9, 2026
3174d7e
feat(#320): convert upload_artifact to streaming-form-data with mid-s…
Copilot May 9, 2026
6373f59
Merge pull request #331 from bg-playground/copilot/streaming-multipar…
bg-playground May 9, 2026
0d6c349
Initial plan
Copilot May 9, 2026
25b1dcf
chore(frontend): upgrade vite 8 with plugin-react 6
Copilot May 9, 2026
b153c23
Merge pull request #332 from bg-playground/copilot/upgrade-vite-8-and…
bg-playground May 9, 2026
06dcb04
Initial plan
Copilot May 9, 2026
93f8889
chore(frontend): upgrade typescript to 6.0.x
Copilot May 9, 2026
44cb8c9
Merge pull request #333 from bg-playground/copilot/upgrade-typescript…
bg-playground May 9, 2026
0d1ce8e
chore(deps): add dependabot groups to batch patch/minor bumps per eco…
bg-playground May 9, 2026
faf6125
Initial plan
Copilot May 6, 2026
0029ad0
feat: implement external case-result endpoints with auto-upsert and t…
Copilot May 6, 2026
631abbd
chore: align migration uniqueness constraint with model naming
Copilot May 6, 2026
e1dcebd
fix: rebase on main and drop stale duplicate case migration artifacts
Copilot May 9, 2026
1cac3ce
Merge remote branch state into rebased branch
Copilot May 9, 2026
f0b34d8
fix: remove stale duplicate case migration and model changes after re…
Copilot May 9, 2026
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
28 changes: 28 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ updates:
commit-message:
prefix: "chore(deps)"
open-pull-requests-limit: 10
groups:
python-patch-minor:
patterns:
- "*"
update-types:
- "minor"
- "patch"

# NPM dependencies (frontend)
- package-ecosystem: "npm"
Expand All @@ -25,6 +32,13 @@ updates:
commit-message:
prefix: "chore(deps)"
open-pull-requests-limit: 10
groups:
npm-patch-minor:
patterns:
- "*"
update-types:
- "minor"
- "patch"

# Docker dependencies
- package-ecosystem: "docker"
Expand All @@ -38,6 +52,13 @@ updates:
commit-message:
prefix: "chore(deps)"
open-pull-requests-limit: 10
groups:
docker-patch-minor:
patterns:
- "*"
update-types:
- "minor"
- "patch"

# GitHub Actions
- package-ecosystem: "github-actions"
Expand All @@ -51,3 +72,10 @@ updates:
commit-message:
prefix: "chore(ci)"
open-pull-requests-limit: 10
groups:
actions-patch-minor:
patterns:
- "*"
update-types:
- "minor"
- "patch"
147 changes: 147 additions & 0 deletions .github/workflows/external-results-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
name: External Results Smoke

on:
pull_request:
paths:
- 'backend/app/api/external_results.py'
- 'backend/app/schemas/external_*.py'
- 'backend/app/models/external_*.py'
- 'backend/app/crud/external_*.py'
- 'backend/alembic/versions/**'
- 'docs/specs/external_results_v1.md'
- '.github/workflows/external-results-smoke.yml'
- 'scripts/smoke/**'
push:
branches:
- main
workflow_dispatch:

permissions:
contents: read

concurrency:
group: external-results-smoke-${{ github.ref }}
cancel-in-progress: true

jobs:
smoke:
name: External Results contract smoke
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- name: Checkout BGSTM
uses: actions/checkout@v4
with:
path: bgstm

- name: Checkout bgstm-playwright-frameworks (pinned)
uses: actions/checkout@v4
with:
repository: bg-playground/bgstm-playwright-frameworks
ref: 942416538dc5d9a23895c9f601e4e4f2d152e7e5
path: frameworks

- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Set up Node.js 20
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Set up pnpm 9
uses: pnpm/action-setup@v4
with:
version: 9

- name: Install smoke script dependencies
run: python -m pip install --quiet httpx

- name: Start BGSTM backend + Postgres
run: docker compose -f bgstm/docker-compose.test.yml up -d db backend

- name: Wait for BGSTM health
run: |
echo "Waiting for backend health check..."
for i in $(seq 1 30); do
if curl -sf http://localhost:8001/health > /dev/null; then
echo "Backend is healthy"
exit 0
fi
echo "Attempt $i/30 -- backend not ready yet, waiting 5s..."
sleep 5
done
echo "Backend failed to become healthy"
docker compose -f bgstm/docker-compose.test.yml logs backend || true
exit 1

- name: Normalize runner token scopes column type for smoke
run: |
docker compose -f bgstm/docker-compose.test.yml exec -T db psql \
-U bgstm_test \
-d bgstm_test \
-v ON_ERROR_STOP=1 \
-c "DO \$\$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'runner_tokens'
AND column_name = 'scopes'
AND udt_name IN ('json', 'jsonb')
) THEN
CREATE OR REPLACE FUNCTION _bgstm_json_to_text_array(j json)
RETURNS text[]
LANGUAGE sql
IMMUTABLE
AS \$fn\$
SELECT COALESCE(array_agg(value), ARRAY[]::text[])
FROM json_array_elements_text(j) AS t(value);
\$fn\$;

ALTER TABLE runner_tokens
ALTER COLUMN scopes TYPE text[]
USING _bgstm_json_to_text_array(scopes);

DROP FUNCTION _bgstm_json_to_text_array(json);
END IF;
END
\$\$;"

- name: Verify schema matches migrations
run: |
docker compose -f bgstm/docker-compose.test.yml exec -T backend alembic current
docker compose -f bgstm/docker-compose.test.yml exec -T db psql -U bgstm_test -d bgstm_test -c "\d audit_log"

- name: Bootstrap project and runner token
id: bootstrap
run: python bgstm/scripts/smoke/bootstrap.py

- name: Install frameworks dependencies
run: pnpm -C frameworks install --frozen-lockfile

- name: Build frameworks workspace
run: pnpm -C frameworks build

- name: Install Chromium for crm-example
run: pnpm -C frameworks/examples/crm-example exec playwright install --with-deps chromium

- name: Run crm-example smoke fixture
run: |
# Playwright fails-by-design (smoke fixture has 1 intentional failure). The real pass/fail signal is the assertion step below.
pnpm --filter crm-example -C frameworks smoke || true

- name: Assert BGSTM persisted expected smoke results
id: assert_results
run: python bgstm/scripts/smoke/assert.py

- name: Dump backend logs (always)
if: always()
run: docker compose -f bgstm/docker-compose.test.yml logs backend

- name: Tear down BGSTM stack
if: always()
run: docker compose -f bgstm/docker-compose.test.yml down -v
58 changes: 58 additions & 0 deletions backend/alembic/versions/i8j9k0l1m2n3_audit_log_actor_kind.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""add actor_kind and actor_token_id to audit_log

Revision ID: i8j9k0l1m2n3
Revises: h7i8j9k0l1m2
Create Date: 2026-05-07 14:30:00.000000

"""

from typing import Sequence, Union

import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "i8j9k0l1m2n3"
down_revision: Union[str, None] = "h7i8j9k0l1m2"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.add_column("audit_log", sa.Column("actor_kind", sa.String(length=20), nullable=False, server_default="user"))
op.add_column("audit_log", sa.Column("actor_token_id", postgresql.UUID(as_uuid=True), nullable=True))
op.alter_column("audit_log", "user_id", existing_type=postgresql.UUID(as_uuid=True), nullable=True)

op.execute("UPDATE audit_log SET actor_kind = 'user' WHERE actor_kind IS NULL")

op.create_foreign_key(
"fk_audit_log_actor_token_id_runner_tokens",
"audit_log",
"runner_tokens",
["actor_token_id"],
["id"],
ondelete="SET NULL",
)
op.create_check_constraint(
"ck_audit_log_actor_identity",
"audit_log",
"(actor_kind = 'user' AND user_id IS NOT NULL AND actor_token_id IS NULL) "
"OR (actor_kind = 'runner_token' AND actor_token_id IS NOT NULL AND user_id IS NULL)",
)
op.create_index("idx_audit_log_actor_token_id", "audit_log", ["actor_token_id"])
op.create_index("idx_audit_log_actor_kind_created_at", "audit_log", ["actor_kind", "created_at"])


def downgrade() -> None:
op.drop_index("idx_audit_log_actor_kind_created_at", table_name="audit_log")
op.drop_index("idx_audit_log_actor_token_id", table_name="audit_log")
op.drop_constraint("ck_audit_log_actor_identity", "audit_log", type_="check")
op.drop_constraint("fk_audit_log_actor_token_id_runner_tokens", "audit_log", type_="foreignkey")

# Restore non-null user_id invariant for pre-actor rows.
op.execute("DELETE FROM audit_log WHERE user_id IS NULL")
op.alter_column("audit_log", "user_id", existing_type=postgresql.UUID(as_uuid=True), nullable=False)
op.drop_column("audit_log", "actor_token_id")
op.drop_column("audit_log", "actor_kind")
55 changes: 55 additions & 0 deletions backend/alembic/versions/j9k0l1m2n3o4_audit_log_details_json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""store audit_log.details as JSONB on PostgreSQL

Revision ID: j9k0l1m2n3o4
Revises: i8j9k0l1m2n3
Create Date: 2026-05-08 00:00:00.000000

"""

from typing import Sequence, Union

import sqlalchemy as sa

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "j9k0l1m2n3o4"
down_revision: Union[str, None] = "i8j9k0l1m2n3"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
bind = op.get_bind()
if bind.dialect.name == "postgresql":
op.execute(
"""
ALTER TABLE audit_log
ALTER COLUMN details TYPE JSONB
USING CASE
WHEN details IS NULL OR btrim(details) = '' THEN NULL
ELSE details::jsonb
END
"""
)
return

op.alter_column("audit_log", "details", existing_type=sa.Text(), type_=sa.JSON(), existing_nullable=True)


def downgrade() -> None:
bind = op.get_bind()
if bind.dialect.name == "postgresql":
op.execute(
"""
ALTER TABLE audit_log
ALTER COLUMN details TYPE TEXT
USING CASE
WHEN details IS NULL THEN NULL
ELSE details::text
END
"""
)
return

op.alter_column("audit_log", "details", existing_type=sa.JSON(), type_=sa.Text(), existing_nullable=True)
Loading
Loading