Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5fa2fd5709 | |||
| 67274cbdb6 | |||
| 9e4346efa9 | |||
| 07b6fc6bb3 | |||
| 366978ce0f | |||
| c0b49959d4 | |||
| 1c685e2285 | |||
| aee4a4d624 | |||
| 36479307e9 | |||
| 74b50465fe | |||
| dc087969c5 | |||
| f4c195a56d | |||
| 41b96022db | |||
| b9e4fb0146 | |||
| 30f4858a34 |
@@ -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: |
|
||||
|
||||
+50
-10
@@ -22,6 +22,33 @@ jobs:
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
cache-dependency-path: frontend/pnpm-lock.yaml
|
||||
|
||||
- name: Build frontend into Host static assets
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
VERSION_DATE="$(TZ=Asia/Seoul date +%Y.%m.%d)"
|
||||
RELEASE_COUNT="$(git ls-remote --tags origin "refs/tags/v${VERSION_DATE}.*" | wc -l | tr -d ' ')"
|
||||
VERSION_SEQUENCE="$((RELEASE_COUNT + 1))"
|
||||
APP_VERSION="${VERSION_DATE}.${VERSION_SEQUENCE}.${GITHUB_SHA::10}"
|
||||
echo "VITE_APP_VERSION=${APP_VERSION}" >> "$GITHUB_ENV"
|
||||
echo "release_version=${APP_VERSION}"
|
||||
VITE_APP_VERSION="${APP_VERSION}" pnpm build
|
||||
grep -R -q 'app-version' dist
|
||||
grep -R -q 'UI contract 4.0' dist
|
||||
grep -R -q "${APP_VERSION}" dist
|
||||
find ../src/KArtSell.Host/wwwroot -mindepth 1 -delete
|
||||
cp -R dist/. ../src/KArtSell.Host/wwwroot/
|
||||
working-directory: frontend
|
||||
|
||||
- run: dotnet restore KArtSell.sln
|
||||
|
||||
- run: dotnet build KArtSell.sln --no-restore -c Release
|
||||
@@ -30,6 +57,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 +83,26 @@ 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; \
|
||||
sudo -n -l | grep -Fq '/usr/bin/systemctl restart kartsell' || { \
|
||||
echo 'Deployment blocked: one-time sudoers delegation is missing for kartsell.' >&2; \
|
||||
echo 'Expected: kjh2064 ALL=(root) NOPASSWD: /usr/bin/systemctl restart kartsell' >&2; \
|
||||
exit 77; \
|
||||
}; \
|
||||
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,22 @@
|
||||
# 배포 frontend artifact 계약
|
||||
|
||||
## Source
|
||||
|
||||
- 운영 배포 Run 3357 로그: `dotnet publish` 전 frontend build 단계 없음
|
||||
- 운영 bundle에 `app-version` 및 `UI contract 4.0` marker 없음
|
||||
- `frontend`의 재현 가능한 `pnpm-lock.yaml` 및 기존 CI frontend job
|
||||
|
||||
## Decision
|
||||
|
||||
배포 workflow는 Host publish 전에 다음 규칙으로 버전을 계산하고 frontend를 재생성한다.
|
||||
|
||||
```text
|
||||
YYYY.MM.DD.<당일 release 순번>.<commit SHA 10자리>
|
||||
```
|
||||
|
||||
당일 순번은 `vYYYY.MM.DD.*` release tag 개수에 1을 더해 계산한다. 예: `2026.08.06.1.acaa731b3f`. 생성된 `frontend/dist`를 Host `wwwroot`에 복사하고, `app-version`, `UI contract 4.0`, 계산된 전체 버전 marker가 없으면 배포를 중단한다.
|
||||
|
||||
## Evidence / Unknown
|
||||
|
||||
- Source 변경과 운영 artifact를 분리하지 않고, 매 배포 시 동일 commit에서 재생성한다.
|
||||
- 실제 운영 반영 증거는 이 Slice의 CI 및 deploy run 완료 후 보존한다.
|
||||
@@ -0,0 +1,32 @@
|
||||
# KArtSell 배포 재기동 권한 계약
|
||||
|
||||
## Source
|
||||
|
||||
- 운영 호스트 `hz-prod-01`의 실제 sudo 정책 조회 결과
|
||||
- 기존 `quantengine` 및 `taxbaik` 서비스의 특정 `systemctl restart` `NOPASSWD` 위임 패턴
|
||||
- `.gitea/workflows/deploy.yml`
|
||||
|
||||
## Assumption
|
||||
|
||||
- 배포 SSH 계정은 `kjh2064`로 유지한다.
|
||||
- 운영 서비스는 `/etc/systemd/system/kartsell.service`로 유지한다.
|
||||
- DbMigrator와 artifact 복사는 현재처럼 `kjh2064` 권한으로 수행한다.
|
||||
|
||||
## Decision
|
||||
|
||||
`kjh2064`에 전체 sudo 권한을 부여하지 않고, 운영자가 한 번만 다음 단일 명령을 `/etc/sudoers.d/kartsell-deploy`에 등록한다.
|
||||
|
||||
```sudoers
|
||||
kjh2064 ALL=(root) NOPASSWD: /usr/bin/systemctl restart kartsell
|
||||
```
|
||||
|
||||
파일 권한은 `0440`이어야 하며 `visudo -cf /etc/sudoers.d/kartsell-deploy` 검증 후 적용한다. 이후 CI는 비대화형 `sudo -n systemctl restart kartsell`만 사용하므로 배포마다 비밀번호 입력이나 sudo 등록이 필요 없다.
|
||||
|
||||
## Deployment guard
|
||||
|
||||
워크플로우는 artifact 복사와 DbMigrator 실행 전에 `sudo -n -l`로 위임 존재 여부를 검사한다. 위임이 없으면 운영 DB를 변경하지 않고 exit 77로 종료한다.
|
||||
|
||||
## Unknown / Decision Required
|
||||
|
||||
- 이 파일을 운영 호스트에 설치할 권한은 root 운영자에게만 있다.
|
||||
- 설치 후 필요한 증거: `visudo -cf` 결과, `sudo -n -l` 결과, 다음 deploy run의 성공 로그, 서비스 active 상태.
|
||||
@@ -0,0 +1,168 @@
|
||||
import { ref } from 'vue';
|
||||
import { KsStatusTag } from '@/shared/ui/components';
|
||||
const selectedId = ref('UI-001');
|
||||
const items = [
|
||||
{ id: 'UI-001', title: '공유 컴포넌트 카탈로그와 상태 프리뷰', owner: 'FE Platform', state: 'IN_PROGRESS' },
|
||||
{ id: 'UI-002', title: 'WBS 실행 화면 및 요구사항 추적', owner: 'Delivery', state: 'IN_PROGRESS' },
|
||||
{ id: 'DATA-001', title: 'DB 스키마 Read Model/API 계약', owner: 'Data Platform', state: 'DECISION_REQUIRED' },
|
||||
{ id: 'OPS-001', title: 'Playwright 시각·상태행렬 검증', owner: 'QA', state: 'IN_PROGRESS' },
|
||||
];
|
||||
const __VLS_ctx = {
|
||||
...{},
|
||||
...{},
|
||||
};
|
||||
let __VLS_components;
|
||||
let __VLS_intrinsics;
|
||||
let __VLS_directives;
|
||||
/** @type {__VLS_StyleScopedClasses['page-header']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['summary-grid']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['summary-grid']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['summary-grid']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['list']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['detail']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['wbs-row']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['wbs-row']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['wbs-row']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['wbs-row']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['detail']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['detail']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['detail']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['workspace']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['summary-grid']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['page-header']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['wbs-row']} */ ;
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.section, __VLS_intrinsics.section)({
|
||||
...{ class: "page" },
|
||||
'aria-labelledby': "wbs-title",
|
||||
});
|
||||
/** @type {__VLS_StyleScopedClasses['page']} */ ;
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.header, __VLS_intrinsics.header)({
|
||||
...{ class: "page-header" },
|
||||
});
|
||||
/** @type {__VLS_StyleScopedClasses['page-header']} */ ;
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.div, __VLS_intrinsics.div)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.p, __VLS_intrinsics.p)({
|
||||
...{ class: "eyebrow" },
|
||||
});
|
||||
/** @type {__VLS_StyleScopedClasses['eyebrow']} */ ;
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.h1, __VLS_intrinsics.h1)({
|
||||
id: "wbs-title",
|
||||
});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.p, __VLS_intrinsics.p)({});
|
||||
let __VLS_0;
|
||||
/** @ts-ignore @type { | typeof __VLS_components.KsStatusTag} */
|
||||
KsStatusTag;
|
||||
// @ts-ignore
|
||||
const __VLS_1 = __VLS_asFunctionalComponent1(__VLS_0, new __VLS_0({
|
||||
value: "AUTOMATION OFF",
|
||||
severity: "warning",
|
||||
}));
|
||||
const __VLS_2 = __VLS_1({
|
||||
value: "AUTOMATION OFF",
|
||||
severity: "warning",
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_1));
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.div, __VLS_intrinsics.div)({
|
||||
...{ class: "summary-grid" },
|
||||
});
|
||||
/** @type {__VLS_StyleScopedClasses['summary-grid']} */ ;
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.div, __VLS_intrinsics.div)({
|
||||
...{ class: "ks-card" },
|
||||
});
|
||||
/** @type {__VLS_StyleScopedClasses['ks-card']} */ ;
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.strong, __VLS_intrinsics.strong)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.span, __VLS_intrinsics.span)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.div, __VLS_intrinsics.div)({
|
||||
...{ class: "ks-card" },
|
||||
});
|
||||
/** @type {__VLS_StyleScopedClasses['ks-card']} */ ;
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.strong, __VLS_intrinsics.strong)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.span, __VLS_intrinsics.span)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.div, __VLS_intrinsics.div)({
|
||||
...{ class: "ks-card" },
|
||||
});
|
||||
/** @type {__VLS_StyleScopedClasses['ks-card']} */ ;
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.strong, __VLS_intrinsics.strong)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.span, __VLS_intrinsics.span)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.div, __VLS_intrinsics.div)({
|
||||
...{ class: "workspace" },
|
||||
});
|
||||
/** @type {__VLS_StyleScopedClasses['workspace']} */ ;
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.section, __VLS_intrinsics.section)({
|
||||
...{ class: "ks-card list" },
|
||||
'aria-labelledby': "wbs-list-title",
|
||||
});
|
||||
/** @type {__VLS_StyleScopedClasses['ks-card']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['list']} */ ;
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.h2, __VLS_intrinsics.h2)({
|
||||
id: "wbs-list-title",
|
||||
});
|
||||
for (const [item] of __VLS_vFor((__VLS_ctx.items))) {
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.button, __VLS_intrinsics.button)({
|
||||
...{ onClick: (...[$event]) => {
|
||||
return (__VLS_ctx.selectedId = item.id);
|
||||
// @ts-ignore
|
||||
[items, selectedId,];
|
||||
} },
|
||||
key: (item.id),
|
||||
...{ class: "wbs-row" },
|
||||
...{ class: ({ selected: __VLS_ctx.selectedId === item.id }) },
|
||||
type: "button",
|
||||
});
|
||||
/** @type {__VLS_StyleScopedClasses['wbs-row']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['selected']} */ ;
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.span, __VLS_intrinsics.span)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.b, __VLS_intrinsics.b)({});
|
||||
(item.id);
|
||||
(item.title);
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.small, __VLS_intrinsics.small)({});
|
||||
(item.owner);
|
||||
let __VLS_5;
|
||||
/** @ts-ignore @type { | typeof __VLS_components.KsStatusTag} */
|
||||
KsStatusTag;
|
||||
// @ts-ignore
|
||||
const __VLS_6 = __VLS_asFunctionalComponent1(__VLS_5, new __VLS_5({
|
||||
value: (item.state),
|
||||
severity: (item.state === 'DONE' ? 'success' : item.state === 'DECISION_REQUIRED' ? 'danger' : 'info'),
|
||||
}));
|
||||
const __VLS_7 = __VLS_6({
|
||||
value: (item.state),
|
||||
severity: (item.state === 'DONE' ? 'success' : item.state === 'DECISION_REQUIRED' ? 'danger' : 'info'),
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_6));
|
||||
// @ts-ignore
|
||||
[selectedId,];
|
||||
}
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.section, __VLS_intrinsics.section)({
|
||||
...{ class: "ks-card detail" },
|
||||
'aria-labelledby': "wbs-detail-title",
|
||||
});
|
||||
/** @type {__VLS_StyleScopedClasses['ks-card']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['detail']} */ ;
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.h2, __VLS_intrinsics.h2)({
|
||||
id: "wbs-detail-title",
|
||||
});
|
||||
if (__VLS_ctx.selectedId === 'DATA-001') {
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.p, __VLS_intrinsics.p)({
|
||||
...{ class: "warning" },
|
||||
});
|
||||
/** @type {__VLS_StyleScopedClasses['warning']} */ ;
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dl, __VLS_intrinsics.dl)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dt, __VLS_intrinsics.dt)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dd, __VLS_intrinsics.dd)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dt, __VLS_intrinsics.dt)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dd, __VLS_intrinsics.dd)({});
|
||||
}
|
||||
else {
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.p, __VLS_intrinsics.p)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dl, __VLS_intrinsics.dl)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dt, __VLS_intrinsics.dt)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dd, __VLS_intrinsics.dd)({});
|
||||
(__VLS_ctx.selectedId);
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dt, __VLS_intrinsics.dt)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dd, __VLS_intrinsics.dd)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dt, __VLS_intrinsics.dt)({});
|
||||
__VLS_asFunctionalElement1(__VLS_intrinsics.dd, __VLS_intrinsics.dd)({});
|
||||
}
|
||||
// @ts-ignore
|
||||
[selectedId, selectedId,];
|
||||
const __VLS_export = (await import('vue')).defineComponent({});
|
||||
export default {};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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
@@ -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)}')
|
||||
|
||||
Reference in New Issue
Block a user