U5 — the gate everything else has to pass
continuous_integration.yaml: six blocking checks, then a separate publish per environment so a Vite build never gets tagged for the wrong one, then a call into last commit's deploy workflow. Along the way: the lint list had drifted (two problems not in the requirement, one already fixed), and the Umami-origin gate needed a variable pair of its own since the backend's side of that comparison lives on the host, not in CI. Pinned the two vulnerable packages while at it.
This commit is contained in:
@@ -0,0 +1,349 @@
|
|||||||
|
name: Continuous Integration
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
deploy_production:
|
||||||
|
description: 'Also deploy to production after a successful build/test (in addition to the automatic test deploy)'
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened]
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
|
||||||
|
# Reusable settings for this workflow. Change these in one place if the .NET/Node/pnpm version,
|
||||||
|
# artifact names, or deploy destinations change. The env context is NOT available inside a called
|
||||||
|
# reusable workflow's `with:` inputs (see the `config` job below, which works around this by passing
|
||||||
|
# these values through as job outputs to the `deploy-test` / `deploy-production` jobs).
|
||||||
|
env:
|
||||||
|
DOTNET_VERSION: '10.0.x'
|
||||||
|
NODE_VERSION: '20'
|
||||||
|
PNPM_VERSION: '9'
|
||||||
|
PUBLISH_RID: linux-arm64
|
||||||
|
ARTIFACT_NAME_TEST: app-test
|
||||||
|
ARTIFACT_NAME_PRODUCTION: app-production
|
||||||
|
DEPLOY_ENVIRONMENT_TEST: test
|
||||||
|
DEPLOY_ENVIRONMENT_PRODUCTION: production
|
||||||
|
DEPLOY_PATH_TEST: ${{ vars.DEPLOY_PATH_TEST }}
|
||||||
|
DEPLOY_PATH_PRODUCTION: ${{ vars.DEPLOY_PATH_PRODUCTION }}
|
||||||
|
SERVICE_NAME_TEST: ${{ vars.SERVICE_NAME_TEST }}
|
||||||
|
SERVICE_NAME_PRODUCTION: ${{ vars.SERVICE_NAME_PRODUCTION }}
|
||||||
|
HEALTH_CHECK_URL_TEST: ${{ vars.HEALTH_CHECK_URL_TEST }}
|
||||||
|
HEALTH_CHECK_URL_PRODUCTION: ${{ vars.HEALTH_CHECK_URL_PRODUCTION }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Passes the env: values above through as job outputs, since the env context is unavailable in
|
||||||
|
# the `with:` block of a job that calls a reusable workflow (see deploy-test / deploy-production).
|
||||||
|
config:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
artifact_name_test: ${{ steps.set.outputs.artifact_name_test }}
|
||||||
|
artifact_name_production: ${{ steps.set.outputs.artifact_name_production }}
|
||||||
|
deploy_environment_test: ${{ steps.set.outputs.deploy_environment_test }}
|
||||||
|
deploy_environment_production: ${{ steps.set.outputs.deploy_environment_production }}
|
||||||
|
deploy_path_test: ${{ steps.set.outputs.deploy_path_test }}
|
||||||
|
deploy_path_production: ${{ steps.set.outputs.deploy_path_production }}
|
||||||
|
service_name_test: ${{ steps.set.outputs.service_name_test }}
|
||||||
|
service_name_production: ${{ steps.set.outputs.service_name_production }}
|
||||||
|
health_check_url_test: ${{ steps.set.outputs.health_check_url_test }}
|
||||||
|
health_check_url_production: ${{ steps.set.outputs.health_check_url_production }}
|
||||||
|
steps:
|
||||||
|
- id: set
|
||||||
|
run: |
|
||||||
|
echo "artifact_name_test=${{ env.ARTIFACT_NAME_TEST }}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "artifact_name_production=${{ env.ARTIFACT_NAME_PRODUCTION }}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "deploy_environment_test=${{ env.DEPLOY_ENVIRONMENT_TEST }}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "deploy_environment_production=${{ env.DEPLOY_ENVIRONMENT_PRODUCTION }}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "deploy_path_test=${{ env.DEPLOY_PATH_TEST }}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "deploy_path_production=${{ env.DEPLOY_PATH_PRODUCTION }}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "service_name_test=${{ env.SERVICE_NAME_TEST }}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "service_name_production=${{ env.SERVICE_NAME_PRODUCTION }}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "health_check_url_test=${{ env.HEALTH_CHECK_URL_TEST }}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "health_check_url_production=${{ env.HEALTH_CHECK_URL_PRODUCTION }}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# --- Gate 1: backend build ---
|
||||||
|
backend-build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: ${{ env.DOTNET_VERSION }}
|
||||||
|
- name: Restore and build (Release)
|
||||||
|
run: dotnet build SlpModularCms.sln -c Release
|
||||||
|
|
||||||
|
# --- Gate 2: backend tests ---
|
||||||
|
backend-test:
|
||||||
|
needs: backend-build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: ${{ env.DOTNET_VERSION }}
|
||||||
|
- name: Test (Release)
|
||||||
|
run: dotnet test SlpModularCms.sln -c Release
|
||||||
|
|
||||||
|
# --- Gate 3: vulnerability scan ---
|
||||||
|
# `dotnet list package --vulnerable` always exits 0, even when it reports vulnerabilities, so the
|
||||||
|
# step greps its own output and fails deliberately (FR-22, D-12, OPEN-03 — closed by pinning
|
||||||
|
# Microsoft.OpenApi and System.Security.Cryptography.Xml in the relevant .csproj files).
|
||||||
|
vulnerability-scan:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: ${{ env.DOTNET_VERSION }}
|
||||||
|
- name: Check for vulnerable packages
|
||||||
|
run: |
|
||||||
|
OUTPUT=$(dotnet list SlpModularCms.sln package --vulnerable --include-transitive 2>&1)
|
||||||
|
echo "$OUTPUT"
|
||||||
|
if echo "$OUTPUT" | grep -q "has the following vulnerable packages"; then
|
||||||
|
echo "Vulnerable packages detected — see above."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
frontend-prepare:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: ${{ env.PNPM_VERSION }}
|
||||||
|
- name: Get pnpm store directory
|
||||||
|
id: pnpm-store
|
||||||
|
working-directory: frontend
|
||||||
|
run: echo "path=$(pnpm store path)" >> "$GITHUB_OUTPUT"
|
||||||
|
- name: Cache pnpm store
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ steps.pnpm-store.outputs.path }}
|
||||||
|
key: pnpm-${{ hashFiles('frontend/pnpm-lock.yaml') }}
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: frontend
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
# --- Gate 4: frontend build ---
|
||||||
|
# Validates the frontend compiles on its own, independent of any environment. The actual
|
||||||
|
# per-environment deployable artifact is produced later by `dotnet publish` (publish-test /
|
||||||
|
# publish-production jobs below), which triggers the same frontend build internally via
|
||||||
|
# SlpModularCms.Api.csproj's BuildAndCopyAdminFrontend MSBuild target, with the environment's
|
||||||
|
# Vite variables set on the process so that target picks them up.
|
||||||
|
frontend-build:
|
||||||
|
needs: frontend-prepare
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: ${{ env.PNPM_VERSION }}
|
||||||
|
- name: Get pnpm store directory
|
||||||
|
id: pnpm-store
|
||||||
|
working-directory: frontend
|
||||||
|
run: echo "path=$(pnpm store path)" >> "$GITHUB_OUTPUT"
|
||||||
|
- name: Restore pnpm store
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ steps.pnpm-store.outputs.path }}
|
||||||
|
key: pnpm-${{ hashFiles('frontend/pnpm-lock.yaml') }}
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: frontend
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
- name: Build
|
||||||
|
working-directory: frontend
|
||||||
|
run: pnpm run build
|
||||||
|
|
||||||
|
# --- Gate 5: frontend tests ---
|
||||||
|
frontend-test:
|
||||||
|
needs: frontend-prepare
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: ${{ env.PNPM_VERSION }}
|
||||||
|
- name: Get pnpm store directory
|
||||||
|
id: pnpm-store
|
||||||
|
working-directory: frontend
|
||||||
|
run: echo "path=$(pnpm store path)" >> "$GITHUB_OUTPUT"
|
||||||
|
- name: Restore pnpm store
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ steps.pnpm-store.outputs.path }}
|
||||||
|
key: pnpm-${{ hashFiles('frontend/pnpm-lock.yaml') }}
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: frontend
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
- name: Unit tests
|
||||||
|
working-directory: frontend
|
||||||
|
run: pnpm run test
|
||||||
|
|
||||||
|
# --- Gate 6: frontend lint / format-check ---
|
||||||
|
frontend-lint:
|
||||||
|
needs: frontend-prepare
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: ${{ env.PNPM_VERSION }}
|
||||||
|
- name: Get pnpm store directory
|
||||||
|
id: pnpm-store
|
||||||
|
working-directory: frontend
|
||||||
|
run: echo "path=$(pnpm store path)" >> "$GITHUB_OUTPUT"
|
||||||
|
- name: Restore pnpm store
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ steps.pnpm-store.outputs.path }}
|
||||||
|
key: pnpm-${{ hashFiles('frontend/pnpm-lock.yaml') }}
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: frontend
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
- name: Lint
|
||||||
|
working-directory: frontend
|
||||||
|
run: pnpm run lint
|
||||||
|
|
||||||
|
# Publishes the Api project for the test environment. The admin SPA is built as part of this
|
||||||
|
# publish (BuildAndCopyAdminFrontend, BeforeTargets="Publish"), so the VITE_* variables below are
|
||||||
|
# set on this step's environment, not passed as CLI arguments — pnpm build (invoked by MSBuild)
|
||||||
|
# reads them the same way `vite build` always does.
|
||||||
|
#
|
||||||
|
# REF-U5-01: the Umami-origin drift check this gate performs exists because U3's own startup check
|
||||||
|
# (BR-U3-22) can't work — the backend can never see VITE_UMAMI_WEBSITE_ID at runtime. It compares
|
||||||
|
# two CI-time values instead: the frontend's Umami script origin, and a Gitea variable that MUST be
|
||||||
|
# kept in sync with the host's real SecurityHeaders__AllowedScriptOrigins__0 env var (D-16). This
|
||||||
|
# gate cannot see the actual runtime CSP configuration; it only catches drift between the frontend
|
||||||
|
# build and what this variable claims the CSP allows.
|
||||||
|
publish-test:
|
||||||
|
needs: [backend-build, backend-test, vulnerability-scan, frontend-build, frontend-test, frontend-lint]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: ${{ env.DOTNET_VERSION }}
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: ${{ env.PNPM_VERSION }}
|
||||||
|
|
||||||
|
- name: Verify Umami origin is permitted (REF-U5-01)
|
||||||
|
if: ${{ vars.VITE_UMAMI_SCRIPT_URL != '' }}
|
||||||
|
run: |
|
||||||
|
ORIGIN=$(echo "${{ vars.VITE_UMAMI_SCRIPT_URL }}" | sed -E 's#^(https?://[^/]+).*#\1#')
|
||||||
|
if ! echo ",${{ vars.SECURITY_ALLOWED_SCRIPT_ORIGINS_TEST }}," | grep -qF ",$ORIGIN,"; then
|
||||||
|
echo "Umami origin '$ORIGIN' is not present in the SECURITY_ALLOWED_SCRIPT_ORIGINS_TEST Gitea variable."
|
||||||
|
echo "Fix the variable, or the test environment's SecurityHeaders CSP config it must mirror (D-16)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Publish (test)
|
||||||
|
working-directory: src/SlpModularCms.Api
|
||||||
|
env:
|
||||||
|
VITE_APP_ENV: test
|
||||||
|
VITE_UMAMI_SCRIPT_URL: ${{ vars.VITE_UMAMI_SCRIPT_URL }}
|
||||||
|
VITE_UMAMI_WEBSITE_ID: ${{ vars.VITE_UMAMI_WEBSITE_ID_TEST }}
|
||||||
|
VITE_SENTRY_DSN: ${{ vars.VITE_SENTRY_DSN }}
|
||||||
|
run: >
|
||||||
|
dotnet publish -c Release -r ${{ env.PUBLISH_RID }} --self-contained false
|
||||||
|
-o ${{ github.workspace }}/${{ env.ARTIFACT_NAME_TEST }}
|
||||||
|
|
||||||
|
- name: Upload publish artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: ${{ env.ARTIFACT_NAME_TEST }}
|
||||||
|
path: ${{ env.ARTIFACT_NAME_TEST }}
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
# Same as publish-test, but only for a manual workflow_dispatch run with deploy_production set —
|
||||||
|
# a distinct job rather than a parameterized reuse of publish-test, because VITE_APP_ENV is a
|
||||||
|
# Vite build-time value: one dist/ bundle cannot be tagged as both 'test' and 'production' (FR-05).
|
||||||
|
publish-production:
|
||||||
|
needs: [backend-build, backend-test, vulnerability-scan, frontend-build, frontend-test, frontend-lint]
|
||||||
|
if: github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_production == 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: ${{ env.DOTNET_VERSION }}
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: ${{ env.PNPM_VERSION }}
|
||||||
|
|
||||||
|
- name: Verify Umami origin is permitted (REF-U5-01)
|
||||||
|
if: ${{ vars.VITE_UMAMI_SCRIPT_URL != '' }}
|
||||||
|
run: |
|
||||||
|
ORIGIN=$(echo "${{ vars.VITE_UMAMI_SCRIPT_URL }}" | sed -E 's#^(https?://[^/]+).*#\1#')
|
||||||
|
if ! echo ",${{ vars.SECURITY_ALLOWED_SCRIPT_ORIGINS_PRODUCTION }}," | grep -qF ",$ORIGIN,"; then
|
||||||
|
echo "Umami origin '$ORIGIN' is not present in the SECURITY_ALLOWED_SCRIPT_ORIGINS_PRODUCTION Gitea variable."
|
||||||
|
echo "Fix the variable, or the production environment's SecurityHeaders CSP config it must mirror (D-16)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Publish (production)
|
||||||
|
working-directory: src/SlpModularCms.Api
|
||||||
|
env:
|
||||||
|
VITE_APP_ENV: production
|
||||||
|
VITE_UMAMI_SCRIPT_URL: ${{ vars.VITE_UMAMI_SCRIPT_URL }}
|
||||||
|
VITE_UMAMI_WEBSITE_ID: ${{ vars.VITE_UMAMI_WEBSITE_ID_PRODUCTION }}
|
||||||
|
VITE_SENTRY_DSN: ${{ vars.VITE_SENTRY_DSN }}
|
||||||
|
run: >
|
||||||
|
dotnet publish -c Release -r ${{ env.PUBLISH_RID }} --self-contained false
|
||||||
|
-o ${{ github.workspace }}/${{ env.ARTIFACT_NAME_PRODUCTION }}
|
||||||
|
|
||||||
|
- name: Upload publish artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: ${{ env.ARTIFACT_NAME_PRODUCTION }}
|
||||||
|
path: ${{ env.ARTIFACT_NAME_PRODUCTION }}
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
# Automatic test deploy on push to master, or on any workflow_dispatch run (FR-03, D-01, D-09).
|
||||||
|
deploy-test:
|
||||||
|
needs: [publish-test, config]
|
||||||
|
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/master')
|
||||||
|
uses: ./.gitea/workflows/deploy-scp.yaml
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
artifact_name: ${{ needs.config.outputs.artifact_name_test }}
|
||||||
|
environment: ${{ needs.config.outputs.deploy_environment_test }}
|
||||||
|
deploy_path: ${{ needs.config.outputs.deploy_path_test }}
|
||||||
|
service_name: ${{ needs.config.outputs.service_name_test }}
|
||||||
|
health_check_url: ${{ needs.config.outputs.health_check_url_test }}
|
||||||
|
run_db_backup: false
|
||||||
|
|
||||||
|
# Production deploy ONLY on an explicit workflow_dispatch run with deploy_production checked
|
||||||
|
# (FR-04, D-09) — never reachable from a plain push to master, so pushing cannot deploy production
|
||||||
|
# under any circumstance.
|
||||||
|
deploy-production:
|
||||||
|
needs: [publish-production, config]
|
||||||
|
if: github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_production == 'true'
|
||||||
|
uses: ./.gitea/workflows/deploy-scp.yaml
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
artifact_name: ${{ needs.config.outputs.artifact_name_production }}
|
||||||
|
environment: ${{ needs.config.outputs.deploy_environment_production }}
|
||||||
|
deploy_path: ${{ needs.config.outputs.deploy_path_production }}
|
||||||
|
service_name: ${{ needs.config.outputs.service_name_production }}
|
||||||
|
health_check_url: ${{ needs.config.outputs.health_check_url_production }}
|
||||||
|
run_db_backup: true
|
||||||
@@ -1104,3 +1104,47 @@ by Infrastructure Design. U5 carries REF-U3-01's Umami origin gate plus FR-21 an
|
|||||||
**Context**: CONSTRUCTION — Round 3 — U6 Code Generation Part 2 complete, presenting completion message
|
**Context**: CONSTRUCTION — Round 3 — U6 Code Generation Part 2 complete, presenting completion message
|
||||||
|
|
||||||
---
|
---
|
||||||
|
## Code Generation — U6 Deploy Workflow (Approved)
|
||||||
|
|
||||||
|
**Timestamp**: 2026-07-28T00:45:00Z
|
||||||
|
|
||||||
|
**User Input**: "Continue to Next Stage"
|
||||||
|
|
||||||
|
**AI Response**: "U6 Code Generation approved. Committed as two commits: 9a77eec (Infrastructure Design) and bd2a963 (Code Generation — deploy-scp.yaml). Updated aidlc-state.md. Proceeding to Code Generation planning for U5 CI Workflow & Quality Gates."
|
||||||
|
|
||||||
|
**Context**: CONSTRUCTION — Round 3 — U6 fully complete; entering U5 Code Generation
|
||||||
|
|
||||||
|
---
|
||||||
|
## Code Generation Plan — U5 CI Workflow & Quality Gates (Planning + partial execution)
|
||||||
|
|
||||||
|
**Timestamp**: 2026-07-28T01:10:00Z
|
||||||
|
|
||||||
|
**User Input**: "(implicit — continuing after U6 approval)"
|
||||||
|
|
||||||
|
**AI Response**: "Investigated FR-21 (lint) and FR-22 (vulnerable packages) directly, since resolving them required live toolchain output rather than design work: fixed the current pnpm lint state (5 errors + 1 warning from FR-21's list, plus 2 additional problems found by drift — badge.tsx and a previously-hidden SetStatusDialog effect the React Compiler plugin could not reach until the watch() incompatibility was fixed), and pinned Microsoft.OpenApi (2.11.0) and System.Security.Cryptography.Xml (10.0.10) via live NuGet advisory lookups, closing OPEN-03. Verified: 372 backend tests, 237 frontend tests, tsc clean, frontend build clean, 0 vulnerable packages solution-wide. Created u5-ci-workflow-code-generation-plan.md documenting both completed steps plus the pending step (the CI workflow YAML itself), including a new mechanism decision (REF-U5-01): the REF-U3-01 gate cannot read the live host env var per D-16, so it is implemented against a parallel pair of Gitea variables that must be kept in sync with the host manually. Presenting for approval before writing continuous_integration.yaml."
|
||||||
|
|
||||||
|
**Context**: CONSTRUCTION — Round 3 — U5 Code Generation Part 1, prerequisite fixes done and verified; awaiting approval for the CI workflow YAML itself
|
||||||
|
|
||||||
|
---
|
||||||
|
## Code Generation Plan — U5 (Approved)
|
||||||
|
|
||||||
|
**Timestamp**: 2026-07-28T01:15:00Z
|
||||||
|
|
||||||
|
**User Input**: "Approve Plan"
|
||||||
|
|
||||||
|
**AI Response**: "Proceeding to generate continuous_integration.yaml."
|
||||||
|
|
||||||
|
**Context**: CONSTRUCTION — Round 3 — U5 Code Generation Part 2 starting
|
||||||
|
|
||||||
|
---
|
||||||
|
## Code Generation — U5 CI Workflow & Quality Gates (Complete)
|
||||||
|
|
||||||
|
**Timestamp**: 2026-07-28T01:40:00Z
|
||||||
|
|
||||||
|
**User Input**: "(implicit — plan approved, generation executed)"
|
||||||
|
|
||||||
|
**AI Response**: "Generated .gitea/workflows/continuous_integration.yaml (12 jobs). Discovered and resolved REF-U5-01 during generation: the REF-U3-01 gate's design assumed the CI job could read the backend's live SecurityHeaders:AllowedScriptOrigins, but per D-16 that's a host env var invisible to the runner — resolved with a parallel Gitea variable pair that must be kept in sync with the host manually, carried to Operations. Clarified the artifact model: frontend-build is a standalone PR gate; the real per-environment artifacts come from dotnet publish, which triggers the embedded admin SPA build via the existing MSBuild target. Validated YAML (12 jobs, deploy-production confirmed unreachable without workflow_dispatch+flag). Re-confirmed full verification: 372 backend tests, 237 frontend tests, tsc clean, frontend build clean, lint clean, 0 vulnerable packages. Documented in construction/u5-ci-workflow/code/generation-summary.md."
|
||||||
|
|
||||||
|
**Context**: CONSTRUCTION — Round 3 — U5 Code Generation complete, presenting completion message
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
+135
@@ -0,0 +1,135 @@
|
|||||||
|
# Code Generation Plan — U5 CI Workflow & Quality Gates
|
||||||
|
|
||||||
|
## Unit Context
|
||||||
|
|
||||||
|
- **Unit**: U5 CI Workflow & Quality Gates (Pipeline-type — YAML + prerequisite frontend/package
|
||||||
|
fixes, no new C# business logic)
|
||||||
|
- **Component**: C-12 CI workflow (`.gitea/workflows/continuous_integration.yaml`)
|
||||||
|
- **Requirements**: FR-01, FR-04, FR-05, FR-21, FR-22
|
||||||
|
- **Carried-in item**: REF-U3-01 — a blocking gate comparing `VITE_UMAMI_SCRIPT_URL` against that
|
||||||
|
environment's `SecurityHeaders:AllowedScriptOrigins`, since the backend cannot see the frontend's
|
||||||
|
build-time Umami variable itself
|
||||||
|
- **Depends on**: U3, U4 (both committed — gates must run against finished application code, per
|
||||||
|
`unit-of-work-dependency.md` Ordering Constraint 4)
|
||||||
|
- **Depended on by**: U6 (invoked by this workflow's `deploy-test` / `deploy-production` jobs)
|
||||||
|
- **No test project applies**: same category as U6 — `.gitea/workflows/` is pipeline
|
||||||
|
configuration. The lint fixes and package pins ARE ordinary application/frontend code changes and
|
||||||
|
are covered by the existing backend test suite and frontend lint/type-check, not a new test
|
||||||
|
project.
|
||||||
|
|
||||||
|
## Note on sequencing (transparency)
|
||||||
|
|
||||||
|
Two of this plan's steps — the lint fixes (FR-21) and the package pins (FR-22) — were investigated
|
||||||
|
and completed during initial analysis, before this plan document existed, because they required
|
||||||
|
running the actual toolchain (`pnpm run lint`, `dotnet list package --vulnerable`, live NuGet
|
||||||
|
advisory lookups) to discover the *current* facts (exact files/lines, exact patched versions) rather
|
||||||
|
than anything design-level. They are recorded here as completed steps for full traceability, and are
|
||||||
|
already build/test-verified (see Step 13.5 record below). **Step 3 — the CI workflow YAML itself —
|
||||||
|
is the step awaiting your review before being written.**
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
- [x] Step 1: Fix the 5 lint errors + 1 warning (FR-21)
|
||||||
|
- `AddCmsInstanceDialog.tsx:55` and `InviteUserDialog.tsx:50` (`react-hooks/set-state-in-effect`):
|
||||||
|
replaced the close-triggered `useEffect` with a `handleOpenChange` wrapper around the `Dialog`'s
|
||||||
|
`onOpenChange`, since resetting state on close is an event response, not a render synchronization
|
||||||
|
- `InviteUserDialog.tsx:54` (`react-hooks/immutability`, `reset` used before its declaration):
|
||||||
|
resolved as part of the same change — `useForm()` now runs before the effect it used to feed
|
||||||
|
- `SettingsPage.tsx:40` (`react-hooks/set-state-in-effect`): replaced with the "adjust state
|
||||||
|
during render" pattern (compare fetched `availability` against a tracked previous value, call
|
||||||
|
`setState` directly in the render body) — the React-documented alternative when state is derived
|
||||||
|
from changing data rather than a user action
|
||||||
|
- `SetStatusDialog.tsx:72` (warning, `react-hooks/incompatible-library` on `watch()`): replaced
|
||||||
|
`watch('status')` with `useWatch({ control, name: 'status' })`, react-hook-form's own
|
||||||
|
compiler-compatible alternative
|
||||||
|
- **Drift found during analysis, fixed as part of the same requirement, not filed**: current
|
||||||
|
`pnpm run lint` no longer matched FR-21's original 6-item list exactly —
|
||||||
|
`SetStatusDialog.tsx:32` from the requirement was already clean, but two problems FR-21 did not
|
||||||
|
list were present: `badge.tsx:32` (`react-refresh/only-export-components`, fixed identically to
|
||||||
|
the existing precedent in `button.tsx` — an inline eslint-disable, not a file split, to match
|
||||||
|
established convention) and a second, previously-hidden effect in
|
||||||
|
`SetStatusDialog.tsx` (lines 74–86) that the React Compiler's lint plugin had not been able to
|
||||||
|
analyze until the `watch()` incompatibility above was resolved. Fixed with the same
|
||||||
|
render-time-sync pattern as `SettingsPage.tsx`, split so the close-triggered reset lives in
|
||||||
|
`handleOpenChange` and the instance-driven reset lives in the render-time sync
|
||||||
|
- Result: `pnpm run lint` — 0 problems
|
||||||
|
|
||||||
|
- [x] Step 2: Pin vulnerable packages (FR-22, closing OPEN-03)
|
||||||
|
- `Microsoft.OpenApi` 2.0.0 (transitive via `Microsoft.AspNetCore.OpenApi` 10.0.9, high severity,
|
||||||
|
GHSA-v5pm-xwqc-g5wc) → pinned to `2.11.0` in `SlpModularCms.Core.csproj` (latest 2.x; patched at
|
||||||
|
2.7.5+, deliberately not jumping to the 3.x major that `Microsoft.AspNetCore.OpenApi` 10.0.9
|
||||||
|
does not target)
|
||||||
|
- `System.Security.Cryptography.Xml` 10.0.9 (transitive via `Microsoft.AspNetCore.DataProtection`,
|
||||||
|
five high-severity advisories, all patched at 10.0.10) → pinned to `10.0.10` in
|
||||||
|
`SlpModularCms.Core.csproj`, plus independently in `SlpModularCms.Modules.Master.Tests.csproj`
|
||||||
|
and `SlpModularCms.Modules.Availability.Tests.csproj`, which each carry their own direct
|
||||||
|
`Microsoft.AspNetCore.DataProtection` reference for testing and therefore have their own
|
||||||
|
unpinned path to the vulnerable transitive version that Core's pin alone does not reach
|
||||||
|
- Result: `dotnet list SlpModularCms.sln package --vulnerable --include-transitive` — clean across
|
||||||
|
all 10 projects
|
||||||
|
|
||||||
|
- [x] Step 3: Generate `.gitea/workflows/continuous_integration.yaml`
|
||||||
|
- Triggers: `pull_request` (opened/synchronize/reopened, validation only), `push` to `master`
|
||||||
|
(validation + automatic test deploy), `workflow_dispatch` with `deploy_production` boolean
|
||||||
|
(default `false`)
|
||||||
|
- `config` job: turns environment-specific `env:` values into job outputs (same reason as the
|
||||||
|
reference project — the `env` context is unavailable inside a called reusable workflow's `with:`
|
||||||
|
block)
|
||||||
|
- Six blocking gates (FR-01, D-10): backend build (`dotnet build -c Release`), backend tests
|
||||||
|
(`dotnet test -c Release`), vulnerability scan (`dotnet list package --vulnerable`, now clean per
|
||||||
|
Step 2), frontend build, frontend tests, frontend lint/format-check (now clean per Step 1)
|
||||||
|
- **REF-U3-01 gate — a mechanism decision this unit must make, not just implement**: the pattern
|
||||||
|
doc says the CI job compares "both values" (frontend's `VITE_UMAMI_SCRIPT_URL` vs. backend's
|
||||||
|
`SecurityHeaders:AllowedScriptOrigins`), but per D-16 the live backend value is a **host**
|
||||||
|
environment variable the Gitea runner cannot read — no `appsettings.{Environment}.json` exists;
|
||||||
|
there's only the single, environment-agnostic `appsettings.json`. Reading the real runtime value
|
||||||
|
is therefore not possible from CI, the exact problem this gate exists to solve, one level up.
|
||||||
|
**Resolution (new, flagged as REF-U5-01)**: add a second pair of Gitea Actions variables,
|
||||||
|
`SECURITY_ALLOWED_SCRIPT_ORIGINS_TEST` / `_PRODUCTION`, that mirror the intended host env var
|
||||||
|
value and are compared against `VITE_UMAMI_SCRIPT_URL_TEST` / `_PRODUCTION` at CI time. This
|
||||||
|
makes the gate implementable today, at the cost of a second place that must be kept in sync with
|
||||||
|
the host's actual `SecurityHeaders__AllowedScriptOrigins__0` env var — carried to Operations
|
||||||
|
host-setup documentation as an explicit "these two must match" note, not silently assumed
|
||||||
|
- **Refinement during generation**: `VITE_UMAMI_SCRIPT_URL` is one shared Gitea variable for both
|
||||||
|
environments (D-24 — one Umami instance), not a `_TEST`/`_PRODUCTION` pair; only
|
||||||
|
`VITE_UMAMI_WEBSITE_ID` differs per environment, matching the reference project's exact pattern.
|
||||||
|
The REF-U5-01 gate therefore compares the same origin against each environment's own
|
||||||
|
`SECURITY_ALLOWED_SCRIPT_ORIGINS_*` variable — two gate runs, one shared input
|
||||||
|
- **Artifact clarification during generation**: the "frontend build" gate (`pnpm run build`) is a
|
||||||
|
standalone PR-time validation, independent of environment. The actual per-environment deployable
|
||||||
|
artifact is a full `dotnet publish` of `SlpModularCms.Api` (`publish-test` / `publish-production`
|
||||||
|
jobs), which triggers the admin SPA's build a second time internally via the existing
|
||||||
|
`BuildAndCopyAdminFrontend` MSBuild target — the environment's `VITE_*` variables are set on that
|
||||||
|
step's process environment, not passed as build arguments, since that target invokes `pnpm build`
|
||||||
|
directly
|
||||||
|
- Two environment-specific publish artifacts (FR-05): `publish-test`
|
||||||
|
(`VITE_APP_ENV=test`) and `publish-production` (gated on `workflow_dispatch` with
|
||||||
|
`deploy_production: true`), each with its own `VITE_UMAMI_WEBSITE_ID`, shared `VITE_SENTRY_DSN`
|
||||||
|
and `VITE_UMAMI_SCRIPT_URL`
|
||||||
|
- Toolchain pinned explicitly (`actions/setup-dotnet`, `pnpm/action-setup`), no `latest` tags (D-04)
|
||||||
|
- `deploy-test` / `deploy-production` jobs call `./.gitea/workflows/deploy-scp.yaml` (U6) with
|
||||||
|
`secrets: inherit`, supplying `artifact_name`, `environment`, `deploy_path`, `service_name`,
|
||||||
|
`health_check_url` from Gitea variables per `infrastructure-design.md` § 6, plus
|
||||||
|
`run_db_backup: true` only for `deploy-production`
|
||||||
|
- Production reachable only via `workflow_dispatch` with the flag set (FR-04) — `deploy-test` never
|
||||||
|
triggers a production deploy under any push condition
|
||||||
|
|
||||||
|
- [x] Step 4: Validate YAML
|
||||||
|
- Parsed successfully: 12 jobs, `deploy-production`'s condition requires
|
||||||
|
`workflow_dispatch` AND `deploy_production == 'true'` — confirmed unreachable from a bare `push`
|
||||||
|
event (FR-04). Job graph: `config`/`backend-build`/`vulnerability-scan`/`frontend-prepare` run
|
||||||
|
independently → `backend-test` needs `backend-build` → `frontend-build`/`frontend-test`/
|
||||||
|
`frontend-lint` need `frontend-prepare` → `publish-test`/`publish-production` need all six gates
|
||||||
|
→ `deploy-test`/`deploy-production` need their publish job plus `config`
|
||||||
|
|
||||||
|
- [x] Step 5: Documentation — `construction/u5-ci-workflow/code/generation-summary.md`
|
||||||
|
|
||||||
|
## Story / Requirement Traceability
|
||||||
|
|
||||||
|
| Step | Covers |
|
||||||
|
|---|---|
|
||||||
|
| 1 | FR-21 |
|
||||||
|
| 2 | FR-22, OPEN-03 |
|
||||||
|
| 3 | FR-01, FR-04, FR-05, REF-U3-01 |
|
||||||
|
| 4 | Definition of Done: "workflow YAML is syntactically valid" |
|
||||||
|
| 5 | Definition of Done: "all six gates pass locally against the current tree" — recorded from Steps 1–2's verification plus this unit's own build/test check (Step 13.5) |
|
||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
# Code Generation Summary — U5 CI Workflow & Quality Gates
|
||||||
|
|
||||||
|
**Date**: 2026-07-28
|
||||||
|
|
||||||
|
## Files Created
|
||||||
|
|
||||||
|
- `.gitea/workflows/continuous_integration.yaml` — 12-job CI workflow (C-12)
|
||||||
|
|
||||||
|
## Files Modified (FR-21 — lint fixes)
|
||||||
|
|
||||||
|
- `frontend/src/components/cms/AddCmsInstanceDialog.tsx` — close-triggered reset moved from a
|
||||||
|
`useEffect` into a `handleOpenChange` wrapper (event response, not a render synchronization)
|
||||||
|
- `frontend/src/components/users/InviteUserDialog.tsx` — same pattern, plus reordered `useForm()`
|
||||||
|
before its use
|
||||||
|
- `frontend/src/pages/SettingsPage.tsx` — replaced the prop-sync effect with the "adjust state
|
||||||
|
during render" pattern (comparing fetched `availability` against a tracked previous value)
|
||||||
|
- `frontend/src/components/cms/SetStatusDialog.tsx` — `watch('status')` replaced with
|
||||||
|
`useWatch({ control, name: 'status' })`; the instance-driven reset effect (which the React
|
||||||
|
Compiler plugin could not previously analyze past the `watch()` incompatibility) split into a
|
||||||
|
render-time sync (on `instance` change) and a `handleOpenChange` wrapper (on close)
|
||||||
|
- `frontend/src/components/ui/badge.tsx` — added an inline `eslint-disable-next-line
|
||||||
|
react-refresh/only-export-components`, matching the existing precedent in `button.tsx` rather than
|
||||||
|
introducing a new file-splitting convention
|
||||||
|
|
||||||
|
## Files Modified (FR-22 — package pins, closing OPEN-03)
|
||||||
|
|
||||||
|
- `src/SlpModularCms.Core/SlpModularCms.Core.csproj` — added `Microsoft.OpenApi` `2.11.0` and
|
||||||
|
`System.Security.Cryptography.Xml` `10.0.10`
|
||||||
|
- `src/SlpModularCms.Modules.Master.Tests/SlpModularCms.Modules.Master.Tests.csproj` and
|
||||||
|
`src/SlpModularCms.Modules.Availability.Tests/SlpModularCms.Modules.Availability.Tests.csproj` —
|
||||||
|
added `System.Security.Cryptography.Xml` `10.0.10` (both projects carry their own direct
|
||||||
|
`Microsoft.AspNetCore.DataProtection` reference for testing, an independent path to the vulnerable
|
||||||
|
transitive version that Core's pin alone does not reach)
|
||||||
|
|
||||||
|
## What the CI Workflow Implements
|
||||||
|
|
||||||
|
Twelve jobs: `config` (env→output passthrough), the six blocking gates (`backend-build`,
|
||||||
|
`backend-test`, `vulnerability-scan`, `frontend-build`, `frontend-test`, `frontend-lint`),
|
||||||
|
`publish-test`/`publish-production` (environment-specific `dotnet publish`, each running the
|
||||||
|
REF-U5-01 Umami-origin drift check first), and `deploy-test`/`deploy-production` (call
|
||||||
|
`deploy-scp.yaml` from U6 with `secrets: inherit`).
|
||||||
|
|
||||||
|
Triggers: `pull_request` (validation only — the six gates, no publish/deploy jobs run since none of
|
||||||
|
them are reachable without `push`-to-`master` or `workflow_dispatch`), `push` to `master`
|
||||||
|
(validation + automatic test deploy), `workflow_dispatch` with `deploy_production` (default `false`).
|
||||||
|
|
||||||
|
## New Decisions Raised During This Unit
|
||||||
|
|
||||||
|
**REF-U5-01** — the design record said the REF-U3-01 gate compares "both values" without saying
|
||||||
|
how the CI job would read the backend side. It can't: per D-16, the live
|
||||||
|
`SecurityHeaders:AllowedScriptOrigins` is a **host** environment variable, invisible to the Gitea
|
||||||
|
runner, exactly the class of problem REF-U3-01 already exists to solve for the frontend's Umami ID.
|
||||||
|
Resolved by adding `SECURITY_ALLOWED_SCRIPT_ORIGINS_TEST` / `_PRODUCTION` Gitea variables that must
|
||||||
|
be **manually kept in sync** with the corresponding host env var — carried to Operations host-setup
|
||||||
|
documentation as an explicit two-places-must-match note, not silently assumed to always agree.
|
||||||
|
|
||||||
|
**Artifact model clarified**: unlike the reference project (where the frontend `dist/` folder *is*
|
||||||
|
the deployable artifact), this repository's admin SPA is embedded into the .NET publish output by
|
||||||
|
the existing `BuildAndCopyAdminFrontend` MSBuild target. The CI workflow's `frontend-build` gate
|
||||||
|
therefore validates the frontend independently (PR-time signal only); the actual per-environment
|
||||||
|
deployable artifacts are produced by `publish-test` / `publish-production`, which set the
|
||||||
|
environment's `VITE_*` variables on the `dotnet publish` process so the embedded frontend build
|
||||||
|
picks them up exactly as it would during a local `dotnet publish`.
|
||||||
|
|
||||||
|
## Gitea Variables and Secrets Introduced by This Unit
|
||||||
|
|
||||||
|
Beyond what `infrastructure-design.md` § 6 already defined for U6:
|
||||||
|
|
||||||
|
| Variable | Scope | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `VITE_SENTRY_DSN` | shared | Frontend Sentry DSN, same value both environments (D-19 — one Sentry project) |
|
||||||
|
| `VITE_UMAMI_SCRIPT_URL` | shared | Umami script host, same value both environments (D-24 — one Umami instance) |
|
||||||
|
| `VITE_UMAMI_WEBSITE_ID_TEST` / `_PRODUCTION` | per-environment | Umami website entry ID |
|
||||||
|
| `SECURITY_ALLOWED_SCRIPT_ORIGINS_TEST` / `_PRODUCTION` | per-environment | REF-U5-01 gate input — must mirror the host's `SecurityHeaders__AllowedScriptOrigins__0` |
|
||||||
|
|
||||||
|
## Verification (Step 13.5 — this unit's own check)
|
||||||
|
|
||||||
|
- **Lint (FR-21)**: `pnpm run lint` — 0 problems (was 6; 2 more found by drift, all fixed)
|
||||||
|
- **Vulnerability scan (FR-22)**: `dotnet list SlpModularCms.sln package --vulnerable
|
||||||
|
--include-transitive` — clean across all 10 projects (was 2 packages, multiple advisories each)
|
||||||
|
- **Backend build**: `dotnet build SlpModularCms.sln -c Release` — 0 errors
|
||||||
|
- **Backend tests**: 372 passed, 0 failed (196 Core, 82 Availability, 57 Master, 37 Identity) —
|
||||||
|
unchanged from the Round 2 baseline, confirming the package pins introduced no regressions
|
||||||
|
- **Frontend type-check**: `npx tsc -b` — clean
|
||||||
|
- **Frontend tests**: 237 passed, 0 failed — unchanged from the Round 2 baseline, confirming the
|
||||||
|
dialog refactors preserved behaviour
|
||||||
|
- **Frontend build**: `pnpm run build` — succeeds
|
||||||
|
- **CI workflow YAML**: parsed successfully — 12 jobs, `deploy-production` confirmed unreachable
|
||||||
|
without `workflow_dispatch` + the flag
|
||||||
|
|
||||||
|
No C# test project applies to `.gitea/workflows/` itself (pipeline configuration, same exception as
|
||||||
|
U6 and the Slave API). Real execution — an actual Gitea instance running the workflow against the
|
||||||
|
Pi — is out of scope until Operations, matching U6's boundary.
|
||||||
|
|
||||||
|
## Deferred to Operations (not built here, by design)
|
||||||
|
|
||||||
|
- Actual values for all Gitea variables and secrets listed above and in
|
||||||
|
`infrastructure-design.md` § 6
|
||||||
|
- Keeping `SECURITY_ALLOWED_SCRIPT_ORIGINS_*` in sync with the host's real CSP configuration
|
||||||
|
(REF-U5-01)
|
||||||
|
- Real pipeline run against the actual Gitea instance and self-hosted runner
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -50,14 +50,14 @@ export function AddCmsInstanceDialog({ open, onOpenChange }: AddCmsInstanceDialo
|
|||||||
mode: 'onTouched',
|
mode: 'onTouched',
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
const handleOpenChange = (nextOpen: boolean) => {
|
||||||
if (!open) {
|
if (!nextOpen) {
|
||||||
setServerError(null);
|
setServerError(null);
|
||||||
addInstance.reset();
|
addInstance.reset();
|
||||||
reset();
|
reset();
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
onOpenChange(nextOpen);
|
||||||
}, [open]);
|
};
|
||||||
|
|
||||||
const onSubmit = handleSubmit(async (values) => {
|
const onSubmit = handleSubmit(async (values) => {
|
||||||
setServerError(null);
|
setServerError(null);
|
||||||
@@ -71,7 +71,7 @@ export function AddCmsInstanceDialog({ open, onOpenChange }: AddCmsInstanceDialo
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
<DialogContent className="sm:max-w-md">
|
<DialogContent className="sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t('cms.add.title')}</DialogTitle>
|
<DialogTitle>{t('cms.add.title')}</DialogTitle>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useForm, Controller } from 'react-hook-form';
|
import { useForm, useWatch, Controller } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -60,7 +60,6 @@ export function SetStatusDialog({ instance, open, onOpenChange }: SetStatusDialo
|
|||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
control,
|
control,
|
||||||
watch,
|
|
||||||
reset,
|
reset,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
} = useForm<SetStatusFormData>({
|
} = useForm<SetStatusFormData>({
|
||||||
@@ -69,21 +68,28 @@ export function SetStatusDialog({ instance, open, onOpenChange }: SetStatusDialo
|
|||||||
defaultValues: { status: 'Available', disableMessage: '' },
|
defaultValues: { status: 'Available', disableMessage: '' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectedStatus = watch('status');
|
const selectedStatus = useWatch({ control, name: 'status' });
|
||||||
|
|
||||||
useEffect(() => {
|
// Adjusts form values when the target instance changes, without an Effect
|
||||||
if (!open || !instance) {
|
// (https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes).
|
||||||
|
const [syncedInstance, setSyncedInstance] = useState(instance);
|
||||||
|
if (instance !== syncedInstance) {
|
||||||
|
setSyncedInstance(instance);
|
||||||
|
if (instance) {
|
||||||
|
reset({ status: instance.status, disableMessage: instance.disableMessage ?? '' });
|
||||||
|
} else {
|
||||||
|
reset({ status: 'Available', disableMessage: '' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOpenChange = (nextOpen: boolean) => {
|
||||||
|
if (!nextOpen) {
|
||||||
setServerError(null);
|
setServerError(null);
|
||||||
updateStatus.reset();
|
updateStatus.reset();
|
||||||
reset({ status: 'Available', disableMessage: '' });
|
reset({ status: 'Available', disableMessage: '' });
|
||||||
} else {
|
|
||||||
reset({
|
|
||||||
status: instance.status,
|
|
||||||
disableMessage: instance.disableMessage ?? '',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
onOpenChange(nextOpen);
|
||||||
}, [open, instance]);
|
};
|
||||||
|
|
||||||
const onSubmit = handleSubmit(async (values) => {
|
const onSubmit = handleSubmit(async (values) => {
|
||||||
if (!instance) return;
|
if (!instance) return;
|
||||||
@@ -100,7 +106,7 @@ export function SetStatusDialog({ instance, open, onOpenChange }: SetStatusDialo
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
<DialogContent className="sm:max-w-md">
|
<DialogContent className="sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t('cms.setStatus.title')}</DialogTitle>
|
<DialogTitle>{t('cms.setStatus.title')}</DialogTitle>
|
||||||
|
|||||||
@@ -29,4 +29,5 @@ function Badge({ className, variant, ...props }: BadgeProps) {
|
|||||||
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
|
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
export { Badge, badgeVariants };
|
export { Badge, badgeVariants };
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -45,17 +45,6 @@ export function InviteUserDialog({ open, onOpenChange }: InviteUserDialogProps)
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) {
|
|
||||||
setStep(1);
|
|
||||||
setInviteLink(null);
|
|
||||||
setServerError(null);
|
|
||||||
inviteUser.reset();
|
|
||||||
reset();
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@@ -66,6 +55,17 @@ export function InviteUserDialog({ open, onOpenChange }: InviteUserDialogProps)
|
|||||||
resolver: zodResolver(inviteUserSchema),
|
resolver: zodResolver(inviteUserSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleOpenChange = (nextOpen: boolean) => {
|
||||||
|
if (!nextOpen) {
|
||||||
|
setStep(1);
|
||||||
|
setInviteLink(null);
|
||||||
|
setServerError(null);
|
||||||
|
inviteUser.reset();
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
onOpenChange(nextOpen);
|
||||||
|
};
|
||||||
|
|
||||||
const onSubmit = handleSubmit(async (values) => {
|
const onSubmit = handleSubmit(async (values) => {
|
||||||
setServerError(null);
|
setServerError(null);
|
||||||
try {
|
try {
|
||||||
@@ -84,7 +84,7 @@ export function InviteUserDialog({ open, onOpenChange }: InviteUserDialogProps)
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
<DialogContent className="sm:max-w-md">
|
<DialogContent className="sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Lock } from 'lucide-react';
|
import { Lock } from 'lucide-react';
|
||||||
@@ -35,12 +35,16 @@ export function SettingsPage() {
|
|||||||
const [selectedMode, setSelectedMode] = useState<AvailabilityStatus>('Available');
|
const [selectedMode, setSelectedMode] = useState<AvailabilityStatus>('Available');
|
||||||
const [reason, setReason] = useState('');
|
const [reason, setReason] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
// Adjusts local edit state when the fetched availability changes, without an Effect
|
||||||
|
// (https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes).
|
||||||
|
const [syncedAvailability, setSyncedAvailability] = useState(availability);
|
||||||
|
if (availability !== syncedAvailability) {
|
||||||
|
setSyncedAvailability(availability);
|
||||||
if (availability) {
|
if (availability) {
|
||||||
setSelectedMode(availability.status);
|
setSelectedMode(availability.status);
|
||||||
setReason(availability.message ?? '');
|
setReason(availability.message ?? '');
|
||||||
}
|
}
|
||||||
}, [availability]);
|
}
|
||||||
|
|
||||||
const isMasterControlled = availability?.isMasterControlled ?? false;
|
const isMasterControlled = availability?.isMasterControlled ?? false;
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,17 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.9" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.9" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
|
||||||
<PackageReference Include="Sentry.AspNetCore" Version="6.8.0" />
|
<PackageReference Include="Sentry.AspNetCore" Version="6.8.0" />
|
||||||
|
<!--
|
||||||
|
Pinned above the versions Microsoft.AspNetCore.OpenApi / Microsoft.AspNetCore.DataProtection
|
||||||
|
would otherwise pull in transitively (FR-22, OPEN-03): Microsoft.OpenApi 2.0.0 has a high
|
||||||
|
severity advisory (GHSA-v5pm-xwqc-g5wc, patched at 2.7.5+ on the 2.x line — deliberately not
|
||||||
|
jumping to the 3.x major, which AspNetCore.OpenApi 10.0.9 does not target); DataProtection's
|
||||||
|
System.Security.Cryptography.Xml 10.0.9 carries five high severity advisories, all patched at
|
||||||
|
10.0.10. Both are direct overrides here because this is the common project both vulnerable
|
||||||
|
chains pass through.
|
||||||
|
-->
|
||||||
|
<PackageReference Include="Microsoft.OpenApi" Version="2.11.0" />
|
||||||
|
<PackageReference Include="System.Security.Cryptography.Xml" Version="10.0.10" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
+2
@@ -11,6 +11,8 @@
|
|||||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
<PackageReference Include="FluentAssertions" Version="8.10.0" />
|
<PackageReference Include="FluentAssertions" Version="8.10.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="10.0.9" />
|
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="10.0.9" />
|
||||||
|
<!-- Pinned above the version DataProtection pulls transitively (FR-22, OPEN-03) — see Core.csproj -->
|
||||||
|
<PackageReference Include="System.Security.Cryptography.Xml" Version="10.0.10" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.11" />
|
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.11" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="10.0.9" />
|
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="10.0.9" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.9" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.9" />
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
<PackageReference Include="FluentAssertions" Version="8.10.0" />
|
<PackageReference Include="FluentAssertions" Version="8.10.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="10.0.9" />
|
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="10.0.9" />
|
||||||
|
<!-- Pinned above the version DataProtection pulls transitively (FR-22, OPEN-03) — see Core.csproj -->
|
||||||
|
<PackageReference Include="System.Security.Cryptography.Xml" Version="10.0.10" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.9" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.9" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
|
|||||||
Reference in New Issue
Block a user