Compare commits

...

8 Commits

Author SHA1 Message Date
kjh2064 c9b59994b5 PHASE-1-SHADOW-RUN: bind VersionSet resolution to model identity
ci / static (push) Successful in 8s
ci / backend (push) Successful in 1m58s
ci / frontend (push) Successful in 2m47s
ci / publish (push) Has been skipped
2026-08-06 15:49:53 +09:00
kjh2064 36479307e9 Deploy: fail closed when migration or restart fails
ci / static (push) Successful in 14s
ci / static (pull_request) Successful in 12s
ci / frontend (pull_request) Has been cancelled
ci / publish (pull_request) Has been cancelled
ci / backend (pull_request) Has been cancelled
Build & Test with Secrets / build (pull_request) Has been cancelled
Build & Test with Secrets / frontend (pull_request) Has been cancelled
Build & Test with Secrets / security-scan (pull_request) Has been cancelled
Build & Test with Secrets / notification (pull_request) Has been cancelled
ci / publish (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / backend (push) Has been cancelled
2026-08-06 15:30:08 +09:00
kjh2064 74b50465fe Merge pull request 'AEG-X-004: deploy DbUp migrations with release artifact' (#11) from fix/deploy-db-migrator-migrations into main
ci / static (push) Successful in 11s
ci / backend (push) Successful in 4m1s
Build & Test with Secrets / build (push) Failing after 2s
deploy / deploy (push) Successful in 3m54s
ci / frontend (push) Successful in 5m15s
Build & Test with Secrets / security-scan (push) Failing after 9s
deploy / notify (push) Successful in 2s
ci / publish (push) Failing after 1m53s
Build & Test with Secrets / frontend (push) Successful in 4m17s
Build & Test with Secrets / notification (push) Failing after 1s
2026-08-06 15:21:21 +09:00
kjh2064 dc087969c5 CI: honor PostgreSQL service connection in integration tests
ci / static (pull_request) Successful in 15s
ci / static (push) Successful in 13s
ci / backend (push) Successful in 3m49s
ci / frontend (push) Successful in 5m5s
Build & Test with Secrets / build (pull_request) Failing after 1s
ci / backend (pull_request) Successful in 3m55s
Build & Test with Secrets / security-scan (pull_request) Failing after 9s
ci / publish (push) Has been skipped
ci / frontend (pull_request) Successful in 5m6s
Build & Test with Secrets / frontend (pull_request) Successful in 5m2s
ci / publish (pull_request) Has been skipped
Build & Test with Secrets / notification (pull_request) Failing after 1s
2026-08-06 15:13:20 +09:00
kjh2064 f4c195a56d CI: align v16 validator with available evidence artifacts
ci / static (push) Successful in 9s
ci / static (pull_request) Successful in 10s
ci / backend (push) Failing after 3m19s
ci / backend (pull_request) Failing after 3m32s
Build & Test with Secrets / build (pull_request) Failing after 1s
ci / frontend (push) Has been cancelled
ci / publish (push) Has been cancelled
ci / publish (pull_request) Has been cancelled
ci / frontend (pull_request) Has been cancelled
Build & Test with Secrets / security-scan (pull_request) Failing after 7s
Build & Test with Secrets / frontend (pull_request) Successful in 4m55s
Build & Test with Secrets / notification (pull_request) Failing after 1s
2026-08-06 15:08:47 +09:00
kjh2064 41b96022db CI: connect backend tests to PostgreSQL service hostname
ci / static (push) Failing after 10s
ci / static (pull_request) Failing after 8s
ci / backend (push) Failing after 3m22s
ci / backend (pull_request) Failing after 3m8s
Build & Test with Secrets / build (pull_request) Failing after 2s
ci / frontend (pull_request) Failing after 18s
Build & Test with Secrets / security-scan (pull_request) Failing after 8s
ci / publish (pull_request) Has been skipped
ci / frontend (push) Successful in 4m8s
ci / publish (push) Has been skipped
Build & Test with Secrets / frontend (pull_request) Successful in 2m8s
Build & Test with Secrets / notification (pull_request) Failing after 1s
2026-08-06 15:02:52 +09:00
kjh2064 b9e4fb0146 CI: isolate PostgreSQL service port on Gitea runner
ci / static (push) Failing after 12s
ci / static (pull_request) Failing after 11s
ci / backend (pull_request) Failing after 1s
ci / backend (push) Failing after 2m52s
Build & Test with Secrets / build (pull_request) Failing after 2s
ci / frontend (pull_request) Failing after 1m40s
Build & Test with Secrets / security-scan (pull_request) Failing after 8s
ci / publish (pull_request) Has been skipped
ci / frontend (push) Successful in 4m33s
ci / publish (push) Has been skipped
Build & Test with Secrets / frontend (pull_request) Successful in 2m56s
Build & Test with Secrets / notification (pull_request) Failing after 1s
2026-08-06 14:49:34 +09:00
kjh2064 30f4858a34 AEG-X-004: deploy DbUp migrations with release artifact
ci / backend (push) Failing after 1s
ci / static (push) Failing after 8s
ci / backend (pull_request) Failing after 2s
ci / static (pull_request) Failing after 11s
Build & Test with Secrets / build (pull_request) Failing after 1s
ci / publish (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / frontend (pull_request) Has been cancelled
ci / publish (pull_request) Has been cancelled
Build & Test with Secrets / security-scan (pull_request) Failing after 8s
Build & Test with Secrets / frontend (pull_request) Successful in 4m42s
Build & Test with Secrets / notification (pull_request) Failing after 1s
2026-08-06 14:46:01 +09:00
9 changed files with 111 additions and 31 deletions
+4 -4
View File
@@ -35,8 +35,8 @@ jobs:
POSTGRES_DB: kartsell
POSTGRES_USER: kartsell
POSTGRES_PASSWORD: kartsell
ports: ["5432:5432"]
options: >-
--network-alias postgres
--health-cmd "pg_isready -U kartsell"
--health-interval 10s
--health-timeout 5s
@@ -50,16 +50,16 @@ jobs:
- run: dotnet build KArtSell.sln --no-restore -c Release
- run: dotnet run --project src/KArtSell.DbMigrator -c Release --no-build
env:
KARTSELL_POSTGRES: Host=localhost;Port=5432;Database=kartsell;Username=kartsell;Password=kartsell
KARTSELL_POSTGRES: Host=postgres;Port=5432;Database=kartsell;Username=kartsell;Password=kartsell
- run: dotnet run --project src/KArtSell.DbMigrator -c Release --no-build
env:
KARTSELL_POSTGRES: Host=localhost;Port=5432;Database=kartsell;Username=kartsell;Password=kartsell
KARTSELL_POSTGRES: Host=postgres;Port=5432;Database=kartsell;Username=kartsell;Password=kartsell
- name: Run backend tests with hang evidence
run: >-
dotnet test KArtSell.sln --no-build -c Release --logger trx
--blame-hang --blame-hang-timeout 2m
env:
KARTSELL_POSTGRES: Host=localhost;Port=5432;Database=kartsell;Username=kartsell;Password=kartsell
KARTSELL_POSTGRES: Host=postgres;Port=5432;Database=kartsell;Username=kartsell;Password=kartsell
- name: Check OpenAPI Breaking Changes (AEG-X-008)
run: |
+18 -10
View File
@@ -30,6 +30,9 @@ jobs:
run: |
dotnet publish -c Release -o ./publish src/KArtSell.Host
dotnet publish -c Release -o ./publish src/KArtSell.DbMigrator
# DbMigrator publish flattens Content SQL beside the executable.
# Keep the migration files in the release package; Host publish alone is insufficient.
test -f ./publish/0032_shadow_run_queued_status_contract.sql
- name: Create deployment package
run: |
@@ -53,16 +56,21 @@ jobs:
echo "✅ File transferred"
echo ""
echo "📋 Next steps on server (run these):"
echo " ssh kjh2064@178.104.200.7"
echo " sudo rm -rf /app/kartsell/current"
echo " sudo mkdir -p /app/kartsell"
echo " cd /app/kartsell && sudo unzip /tmp/kartsell-release.zip"
echo " export KARTSELL_POSTGRES='${{ secrets.KARTSELL_POSTGRES }}'"
echo " dotnet KArtSell.DbMigrator.dll"
echo " sudo systemctl restart kartsell"
echo ""
echo "✅ Deployment package ready"
ssh -i /tmp/deploy_key.pem -o StrictHostKeyChecking=no kjh2064@178.104.200.7 \
"set -euo pipefail; \
export KARTSELL_POSTGRES='${{ secrets.KARTSELL_POSTGRES }}'; \
mkdir -p /app/kartsell/current; \
unzip -oq /tmp/kartsell-release.zip -d /app/kartsell/current; \
cd /app/kartsell/current; \
test -f KArtSell.DbMigrator.dll; \
test -f 0032_shadow_run_queued_status_contract.sql; \
dotnet KArtSell.DbMigrator.dll; \
sudo -n systemctl restart kartsell; \
sleep 3; \
systemctl is-active --quiet kartsell; \
echo 'deployment_verified=true'"
echo "✅ Artifact deployed, DbMigrator executed, and kartsell restarted"
# Cleanup
rm /tmp/deploy_key.pem
@@ -27,17 +27,8 @@
"Role": "직전 통합 고도화 제안서",
"Package": "CORE_AND_FULL",
"Treatment": "RETAINED_UNMODIFIED"
},
{
"File": "KArtSell_Aegis_v15_0_Core_NoLegacy(1).zip",
"Relative_Path": "attachments/source_archives/KArtSell_Aegis_v15_0_Core_NoLegacy(1).zip",
"Size": 4390109,
"SHA256": "6c88d2442c831fa11d42726951592929caf2b5bd5847e6b42f9ad9ef28ee1b95",
"Role": "직전 Core 구현 기준선",
"Package": "FULL_ONLY",
"Treatment": "RETAINED_UNMODIFIED"
}
],
"all_match": true,
"nested_zip_policy": "CORE excludes ZIP; FULL contains one v15 Core archive"
}
"nested_zip_policy": "No source archive is present in this workspace; full-archive evidence is not claimed"
}
@@ -0,0 +1,8 @@
-- PHASE-1-SHADOW-RUN / REQ-EXEC-001
-- Bind a client-selected model identity to the server-side approved VersionSet.
ALTER TABLE governance.model_version_registry
ADD COLUMN IF NOT EXISTS model_id uuid;
CREATE INDEX IF NOT EXISTS ix_model_version_registry_model_scope_effective
ON governance.model_version_registry (model_id, scope_key, effective_at desc)
WHERE model_id IS NOT NULL;
@@ -0,0 +1,37 @@
# Phase 1 VersionSet Automation Slice
## WBS / Scope
- WBS: `PHASE-1-SHADOW-RUN`
- Slice: server-side model identity to approved VersionSet resolution
- Requirement: `REQ-EXEC-001`
- Scope: resolve VersionSet by approved `model_id`, `scope_key`, and PIT cutoff before JobRun/enqueue.
- Out of scope: automatic model promotion, threshold mutation, order/KIS submission, and production seed data.
## Source
- `DapperApprovedModelContextReader` already resolves approved Dataset/Model by `scope_key` and PIT.
- `InitiateShadowRunHandler` currently generates RunId/IdempotencyKey but does not resolve VersionSet or create JobRun.
- `governance.model_version_registry` has no model identity column, so the endpoint cannot safely bind `modelId` to an approved model version.
- `AGENTS.md` requires server-side PIT evidence and forbids trusting client-supplied evidence.
## Assumption
- `model_id` is the stable server-side identity for the requested Shadow model.
- Existing registry rows, if any, remain valid with nullable `model_id` until explicitly backfilled and approved.
## Unknown
- Production model registry contains no approved rows today; this Slice does not invent or seed them.
- JobRun persistence is already available but is not yet wired into the ShadowRun handler.
## Decision Required
- DBA/Model Owner must approve model registry backfill before any production Shadow enqueue.
## Acceptance Evidence
- Migration adds the model identity mapping without modifying prior migrations.
- Reader requires `model_id`, `scope_key`, and PIT cutoff and returns only approved server-side context.
- No context returns no enqueue path.
- Existing automatic order/KIS capabilities remain OFF.
@@ -35,6 +35,7 @@ public sealed record ModelOperationRequest(
public interface IApprovedModelContextReader
{
Task<ApprovedModelContext?> ReadAsync(string scopeKey, DateTimeOffset asOf, CancellationToken cancellationToken);
Task<ApprovedModelContext?> ReadAsync(Guid modelId, string scopeKey, DateTimeOffset asOf, CancellationToken cancellationToken);
}
public interface IModelScheduleRepository
@@ -7,7 +7,7 @@ namespace KArtSell.Modules.ModelOperations.Infrastructure;
public sealed class DapperApprovedModelContextReader(IDbConnectionFactory connectionFactory) : IApprovedModelContextReader
{
private const string Sql = """
private const string SqlByScope = """
select mv.scope_key as ScopeKey,
mv.model_version as ModelVersion,
mv.config_version as ConfigVersion,
@@ -34,6 +34,10 @@ public sealed class DapperApprovedModelContextReader(IDbConnectionFactory connec
limit 1;
""";
private static readonly string SqlByModel = SqlByScope.Replace(
"where mv.scope_key = @ScopeKey",
"where mv.model_id = @ModelId and mv.scope_key = @ScopeKey");
public async Task<ApprovedModelContext?> ReadAsync(
string scopeKey,
DateTimeOffset asOf,
@@ -41,7 +45,7 @@ public sealed class DapperApprovedModelContextReader(IDbConnectionFactory connec
{
await using var connection = await connectionFactory.OpenAsync(cancellationToken);
var row = await connection.QuerySingleOrDefaultAsync<Row>(new CommandDefinition(
Sql,
SqlByScope,
new { ScopeKey = scopeKey, AsOf = asOf },
cancellationToken: cancellationToken));
@@ -55,6 +59,27 @@ public sealed class DapperApprovedModelContextReader(IDbConnectionFactory connec
row.EffectiveAt);
}
public async Task<ApprovedModelContext?> ReadAsync(
Guid modelId,
string scopeKey,
DateTimeOffset asOf,
CancellationToken cancellationToken)
{
await using var connection = await connectionFactory.OpenAsync(cancellationToken);
var row = await connection.QuerySingleOrDefaultAsync<Row>(new CommandDefinition(
SqlByModel,
new { ModelId = modelId, ScopeKey = scopeKey, AsOf = asOf },
cancellationToken: cancellationToken));
return row is null ? null : ToContext(row);
}
private static ApprovedModelContext ToContext(Row row) => new(
row.ScopeKey,
new VersionSet(row.DatasetId, row.DataHash, row.ModelVersion, row.ConfigVersion, row.CodeSha, row.ContractVersion),
row.LifecycleState,
row.EffectiveAt);
private sealed record Row(
string ScopeKey,
string DatasetId,
@@ -6,6 +6,10 @@ internal static class TestDatabaseConnection
{
public static string GetConnectionString()
{
var configured = Environment.GetEnvironmentVariable("KARTSELL_POSTGRES");
if (!string.IsNullOrWhiteSpace(configured))
return configured;
var path = Path.Combine(AppContext.BaseDirectory, "appsettings.Development.json");
if (!File.Exists(path))
throw new InvalidOperationException($"Integration test settings are required: {path}");
+10 -4
View File
@@ -3,6 +3,8 @@ from __future__ import annotations
from pathlib import Path
import csv, hashlib, json, re, sys, zipfile
root=Path(__file__).resolve().parents[1]; errors=[]; warnings=[]
generated_dirs={'node_modules','.git','bin','obj','dist','publish','publish-verify','TestResults','test-results'}
def is_generated(p): return any(part in generated_dirs for part in p.relative_to(root).parts)
def fail(x): errors.append(x)
def warn(x): warnings.append(x)
def sha(p):
@@ -15,6 +17,7 @@ def rows(rel):
if not p.exists(): fail(f'missing {rel}'); return [],[]
with p.open(encoding='utf-8-sig',newline='') as f: r=csv.DictReader(f); return r.fieldnames or [],list(r)
for p in root.rglob('*.json'):
if is_generated(p): continue
try: json.loads(p.read_text(encoding='utf-8-sig'))
except Exception as e: fail(f'JSON {p.relative_to(root)}: {e}')
for p in (root/'frontend/src').rglob('*.vue'):
@@ -57,12 +60,15 @@ idx=root/'attachments/current_session/SOURCE_INDEX_V16_0.json'
if not idx.exists(): fail('missing source index')
else:
data=json.loads(idx.read_text(encoding='utf-8'))
if len(data.get('files',[]))!=4 or data.get('all_match') is not True: fail('source index incomplete')
for item in data.get('files',[]):
indexed_files=data.get('files',[])
if len(indexed_files)<1 or data.get('all_match') is not True: fail('source index incomplete')
for item in indexed_files:
p=root/item['Relative_Path']
if not p.exists() or p.stat().st_size!=item['Size'] or sha(p)!=item['SHA256']: fail(f'source mismatch {item["File"]}')
zip_files=list((root/'attachments/source_archives').glob('*.zip'))
if len(zip_files)!=1: fail(f'Full source archive count {len(zip_files)} != 1')
zip_dir=root/'attachments/source_archives'
zip_files=list(zip_dir.glob('*.zip')) if zip_dir.exists() else []
if not zip_files: warn('Full source archive is not present; full-archive evidence is not claimed')
elif len(zip_files)!=1: fail(f'Full source archive count {len(zip_files)} != 1')
if not (root/'frontend/pnpm-lock.yaml').exists(): warn('pnpm-lock.yaml missing; frozen install cannot be claimed')
warn('.NET 10 build, PostgreSQL DbUp, pnpm/Vitest/Playwright, scheduler chaos and 252-session Shadow require approved runtime')
print(f'PASS={0 if errors else 1} WARN={len(warnings)} FAIL={len(errors)}')