Continuous Integration / config (pull_request) Successful in 12s
Continuous Integration / changes (pull_request) Successful in 22s
Continuous Integration / backend-build (pull_request) Successful in 5m53s
Continuous Integration / vulnerability-scan (pull_request) Successful in 5m46s
Continuous Integration / frontend-prepare (pull_request) Successful in 1m54s
Continuous Integration / backend-test (pull_request) Successful in 7m37s
Continuous Integration / frontend-build (pull_request) Successful in 2m14s
Continuous Integration / frontend-test (pull_request) Successful in 4m59s
Continuous Integration / frontend-lint (pull_request) Successful in 2m2s
Continuous Integration / publish-production (pull_request) Skipped
Continuous Integration / deploy-production (pull_request) Skipped
Continuous Integration / publish-test (pull_request) Successful in 7m34s
Continuous Integration / deploy-test (pull_request) Skipped
Implements Unit 2 "Offerings" (backend module, admin CRUD UI with drag-and-drop reordering, public GET /api/v1/offerings endpoint) and executes the feature's D-15 CI/CD cutover, switching the deploy pipeline's build/publish target from SlpModularCms.Api to SlpModularCms.Api.SlpSoftware. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWyStNL2ZsjrS7FLd7xvvN
508 lines
24 KiB
YAML
508 lines
24 KiB
YAML
name: Continuous Integration
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
deploy_test:
|
|
description: 'Deploy to Test'
|
|
type: boolean
|
|
default: false
|
|
deploy_production:
|
|
description: 'Deploy to Production'
|
|
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:
|
|
# Gitea has no reliable way to see a past run's workflow_dispatch input values from the UI, so
|
|
# this logs them explicitly — the only durable record of what was actually checked for a run.
|
|
- name: Log dispatch inputs
|
|
if: github.event_name == 'workflow_dispatch'
|
|
run: |
|
|
echo "Deploy to Test: ${{ github.event.inputs.deploy_test }}"
|
|
echo "Deploy to Production: ${{ github.event.inputs.deploy_production }}"
|
|
- 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"
|
|
|
|
# Path-based skip: keeps the pipeline short when a change only touches one side of the repo.
|
|
# fetch-depth: 0 gives paths-filter full local git history to diff against, rather than relying
|
|
# on its GitHub-API fallback for missing history, which targets GitHub's REST API and cannot be
|
|
# assumed to work against a Gitea remote. Changes under the workflow files themselves count as
|
|
# both backend and frontend, so a workflow edit is never left partially untested. A manual
|
|
# workflow_dispatch run has no `before` commit to diff against and is always an explicit request
|
|
# to run everything, so it force-reports both sides as changed rather than trusting the filter.
|
|
changes:
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
backend: ${{ github.event_name == 'workflow_dispatch' || steps.filter.outputs.backend == 'true' }}
|
|
frontend: ${{ github.event_name == 'workflow_dispatch' || steps.filter.outputs.frontend == 'true' }}
|
|
steps:
|
|
# Skipped on workflow_dispatch: there's no `before` commit to diff against, and a manual run
|
|
# always force-reports both sides changed anyway (see the outputs above), so the diff would be
|
|
# wasted work.
|
|
- uses: actions/checkout@v4
|
|
if: github.event_name != 'workflow_dispatch'
|
|
with:
|
|
fetch-depth: 0
|
|
- uses: dorny/paths-filter@v3
|
|
if: github.event_name != 'workflow_dispatch'
|
|
id: filter
|
|
with:
|
|
filters: |
|
|
workflow: &workflow
|
|
- '.gitea/workflows/**'
|
|
backend:
|
|
- *workflow
|
|
- 'src/**'
|
|
- 'tests/**'
|
|
- '*.sln'
|
|
- '**/*.csproj'
|
|
frontend:
|
|
- *workflow
|
|
- 'frontend/**'
|
|
|
|
# --- Gate 1: backend build ---
|
|
backend-build:
|
|
needs: changes
|
|
if: needs.changes.outputs.backend == 'true'
|
|
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: [changes, backend-build]
|
|
if: needs.changes.outputs.backend == 'true'
|
|
runs-on: ubuntu-latest
|
|
# Added for SlpModularCms.Api.Tests (slpsoftware-api feature, NFR-CS-01): its pipeline
|
|
# regression tests boot the real Api host via WebApplicationFactory, which unconditionally
|
|
# runs CmsHost.ConfigurePipeline's startup migration (MigrateCoreDatabase) -- unlike every
|
|
# other test project here, which mocks/uses EF Core InMemory and never touches a real
|
|
# database. Without a real, reachable MariaDB, those tests fail on the connection itself
|
|
# before any assertion runs.
|
|
#
|
|
# A `services:` block was tried first and rejected: this runner uses host networking for
|
|
# job AND service containers ("--network and --net in the options will be ignored" in the
|
|
# job log), so the `ports:` mapping was silently ignored, and the mariadb:11 service ended
|
|
# up sharing the host's network namespace directly on port 3306 -- which something else on
|
|
# this runner already answers on (root auth was rejected by a server that was clearly NOT
|
|
# the freshly-initialized container: "Access denied ... using password: YES" against a
|
|
# password nothing but this job ever set). An explicitly `docker run` container below,
|
|
# published on host port 3307 instead of 3306, avoids that collision entirely.
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: actions/setup-dotnet@v4
|
|
with:
|
|
dotnet-version: ${{ env.DOTNET_VERSION }}
|
|
- name: Start MariaDB for SlpModularCms.Api.Tests
|
|
run: |
|
|
docker run -d --name ci-mariadb \
|
|
-e MARIADB_ROOT_PASSWORD=ci_test_password \
|
|
-e MARIADB_DATABASE=SlpModularCms \
|
|
-p 3307:3306 \
|
|
mariadb:11
|
|
for i in $(seq 1 30); do
|
|
# mariadb:11 does not provide a `mysqladmin` alias -- verified locally against this
|
|
# exact image; the correct binary here is `mariadb-admin`.
|
|
if docker exec ci-mariadb mariadb-admin ping -uroot -pci_test_password --silent 2>/dev/null; then
|
|
echo "MariaDB is ready."
|
|
break
|
|
fi
|
|
echo "Waiting for MariaDB... ($i/30)"
|
|
sleep 2
|
|
done
|
|
- name: Test (Release)
|
|
env:
|
|
# Overrides the placeholder in SlpModularCms.Api/appsettings.Development.json for this
|
|
# CI run only -- ASP.NET Core's configuration layering applies environment variables
|
|
# after appsettings.*.json, so this reaches SlpModularCms.Api.Tests' WebApplicationFactory
|
|
# without touching any committed appsettings file. Port 3307, not 3306 -- see above.
|
|
ConnectionStrings__DefaultConnection: "Server=127.0.0.1;Port=3307;Database=SlpModularCms;Uid=root;Pwd=ci_test_password"
|
|
run: dotnet test SlpModularCms.sln -c Release
|
|
- name: Stop MariaDB for SlpModularCms.Api.Tests
|
|
if: always()
|
|
run: docker rm -f ci-mariadb || true
|
|
|
|
# --- 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:
|
|
needs: changes
|
|
if: needs.changes.outputs.backend == 'true'
|
|
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:
|
|
needs: changes
|
|
if: needs.changes.outputs.frontend == 'true'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: actions/setup-node@v3
|
|
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 each publish job's own "Build admin
|
|
# frontend" step (publish-test / publish-production, below), which builds frontend/ again with
|
|
# that environment's Vite variables set and copies the result into wwwroot/admin before
|
|
# `dotnet publish` runs — see that step's comment for why this isn't an MSBuild target instead.
|
|
frontend-build:
|
|
needs: [changes, frontend-prepare]
|
|
if: needs.changes.outputs.frontend == 'true'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: actions/setup-node@v3
|
|
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: [changes, frontend-prepare]
|
|
if: needs.changes.outputs.frontend == 'true'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: actions/setup-node@v3
|
|
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: [changes, frontend-prepare]
|
|
if: needs.changes.outputs.frontend == 'true'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: actions/setup-node@v3
|
|
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 by this job's own
|
|
# "Build admin frontend" step below, ahead of `dotnet publish` — see that step's comment for why.
|
|
#
|
|
# 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: [changes, backend-build, backend-test, vulnerability-scan, frontend-build, frontend-test, frontend-lint]
|
|
# always() bypasses the automatic skip-cascade from a gate job that was itself skipped (because
|
|
# its side of the repo didn't change). Gitea Actions only implements always() from the GitHub
|
|
# Actions status-check functions — success()/failure()/cancelled() are not supported — so each
|
|
# gate's outcome is checked explicitly via needs.<job>.result instead: a skipped gate is fine,
|
|
# but a gate that actually ran and failed or was cancelled still blocks the publish. The
|
|
# changes.outputs check makes sure there's something to publish at all: a run that touched
|
|
# neither backend nor frontend has nothing new to deploy. The final line skips this build on a
|
|
# manual workflow_dispatch run that didn't check deploy_test — building and uploading a test
|
|
# artifact nobody is going to deploy is wasted work; push/PR runs are unaffected.
|
|
if: |
|
|
always() &&
|
|
needs.changes.result == 'success' &&
|
|
needs.backend-build.result != 'failure' && needs.backend-build.result != 'cancelled' &&
|
|
needs.backend-test.result != 'failure' && needs.backend-test.result != 'cancelled' &&
|
|
needs.vulnerability-scan.result != 'failure' && needs.vulnerability-scan.result != 'cancelled' &&
|
|
needs.frontend-build.result != 'failure' && needs.frontend-build.result != 'cancelled' &&
|
|
needs.frontend-test.result != 'failure' && needs.frontend-test.result != 'cancelled' &&
|
|
needs.frontend-lint.result != 'failure' && needs.frontend-lint.result != 'cancelled' &&
|
|
(needs.changes.outputs.backend == 'true' || needs.changes.outputs.frontend == 'true') &&
|
|
(github.event_name != 'workflow_dispatch' || github.event.inputs.deploy_test == '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@v3
|
|
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
|
|
|
|
# Builds the admin SPA and lands it in wwwroot/admin *before* dotnet publish runs, as its own
|
|
# step rather than an MSBuild target hooked to Build/Publish. That was tried and reliably
|
|
# failed two different ways (see SlpModularCms.Api.csproj's comment on the topic): the SDK's
|
|
# wwwroot static-web-asset item set is fixed at project evaluation time, before any target
|
|
# runs, so files a target creates afterward never make it into the publish output; and forcing
|
|
# them in as an explicit Content item collided with the SDK's own static-web-asset resolution.
|
|
# Physically existing on disk before dotnet publish/build ever runs is the only thing that
|
|
# worked, verified locally against a clean obj/bin and a fresh node_modules.
|
|
- name: Build admin frontend
|
|
working-directory: frontend
|
|
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: |
|
|
pnpm install --frozen-lockfile
|
|
pnpm build
|
|
mkdir -p ../src/SlpModularCms.Api.SlpSoftware/wwwroot/admin
|
|
cp -r dist/. ../src/SlpModularCms.Api.SlpSoftware/wwwroot/admin/
|
|
|
|
- name: Publish (test)
|
|
working-directory: src/SlpModularCms.Api.SlpSoftware
|
|
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: [changes, backend-build, backend-test, vulnerability-scan, frontend-build, frontend-test, frontend-lint]
|
|
# See publish-test's comment above on why this checks needs.<job>.result explicitly instead of
|
|
# failure()/cancelled(), which Gitea Actions doesn't support.
|
|
if: |
|
|
always() &&
|
|
needs.changes.result == 'success' &&
|
|
needs.backend-build.result != 'failure' && needs.backend-build.result != 'cancelled' &&
|
|
needs.backend-test.result != 'failure' && needs.backend-test.result != 'cancelled' &&
|
|
needs.vulnerability-scan.result != 'failure' && needs.vulnerability-scan.result != 'cancelled' &&
|
|
needs.frontend-build.result != 'failure' && needs.frontend-build.result != 'cancelled' &&
|
|
needs.frontend-test.result != 'failure' && needs.frontend-test.result != 'cancelled' &&
|
|
needs.frontend-lint.result != 'failure' && needs.frontend-lint.result != 'cancelled' &&
|
|
github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_production == 'true' &&
|
|
(needs.changes.outputs.backend == 'true' || needs.changes.outputs.frontend == '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@v3
|
|
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
|
|
|
|
# See publish-test's "Build admin frontend" step for why this runs as its own step ahead of
|
|
# dotnet publish rather than an MSBuild target.
|
|
- name: Build admin frontend
|
|
working-directory: frontend
|
|
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: |
|
|
pnpm install --frozen-lockfile
|
|
pnpm build
|
|
mkdir -p ../src/SlpModularCms.Api.SlpSoftware/wwwroot/admin
|
|
cp -r dist/. ../src/SlpModularCms.Api.SlpSoftware/wwwroot/admin/
|
|
|
|
- name: Publish (production)
|
|
working-directory: src/SlpModularCms.Api.SlpSoftware
|
|
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 every push to master (unchanged — a merge should reach test without
|
|
# extra steps). A manual workflow_dispatch run only deploys to test when deploy_test is checked,
|
|
# since a dispatch run is often just re-running CI/production, not a new test build (FR-03, D-01,
|
|
# D-09).
|
|
deploy-test:
|
|
needs: [publish-test, config]
|
|
if: (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_test == 'true') || (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
|