diff --git a/.gitea/workflows/deploy-prod.yml b/.gitea/workflows/deploy-prod.yml new file mode 100644 index 0000000..669ca89 --- /dev/null +++ b/.gitea/workflows/deploy-prod.yml @@ -0,0 +1,45 @@ +name: Deploy Production + +# Production promotion. The APPROVAL GATE is the git tag: production only ever +# deploys a tagged release cut on main (see docs/WORKFLOW.md §5). Cutting the tag +# is the deliberate, auditable "approve to go live" action — and the tag doubles +# as the rollback target. workflow_dispatch adds a manual "Run workflow" button +# for re-deploys/rollbacks. +# +# STATUS: inactive until (a) the Linux production server exists and (b) a +# self-hosted Gitea runner is registered on it with labels [self-hosted, production]. +# Tag pushes before then will queue harmlessly. This file is the wiring, ready to +# switch on — review the deploy step for your server before first use. +on: + push: + tags: ['v*'] + workflow_dispatch: + inputs: + ref: + description: 'Tag or commit to deploy (e.g. v1.2.0)' + required: true + +jobs: + deploy: + runs-on: [self-hosted, production] + steps: + - uses: actions/checkout@v4 + with: + # Deploy the exact tag that triggered the run (immutable), or the + # ref given to a manual dispatch. + ref: ${{ github.event.inputs.ref || github.ref_name }} + fetch-depth: 0 + + - name: Deploy release to production + run: | + echo "Deploying ${{ github.event.inputs.ref || github.ref_name }} to production" + # deploy/.env lives on the server (never in git). up.sh validates it, + # builds the Linux images, applies EF migrations on boot, and starts + # the stack behind the nginx reverse proxy. + ./deploy/up.sh --proxy + + - name: Smoke check + run: | + sleep 5 + curl -fsS http://localhost/ >/dev/null && echo "Prod responding on :80" || \ + { echo "Smoke check failed"; exit 1; } diff --git a/.gitea/workflows/deploy-staging.yml b/.gitea/workflows/deploy-staging.yml new file mode 100644 index 0000000..d38cf36 --- /dev/null +++ b/.gitea/workflows/deploy-staging.yml @@ -0,0 +1,42 @@ +name: Deploy Staging + +# Continuous deployment to the LOCAL staging stack. Fires when develop advances +# (i.e. after a PR is merged into develop). It re-verifies the code, then rebuilds +# and restarts the isolated staging stack on this machine. +# +# CRITICAL: staging lives on your Windows box (ports 18080/18081). A cloud or +# container runner CANNOT reach it, so this job MUST run on a self-hosted Gitea +# Actions runner registered ON that Windows machine with Docker access +# (labels: self-hosted, windows). Until that runner exists this job just waits +# in the queue (harmless, cancelable) — deploy staging manually meanwhile with: +# ./deploy/up.ps1 -Staging +on: + push: + branches: [develop] + workflow_dispatch: {} # also allow a manual "Run workflow" from the Gitea UI + +jobs: + deploy: + runs-on: [self-hosted, windows] + steps: + - uses: actions/checkout@v4 + + # Re-run the gate on the exact merged code before it touches staging. + - name: Build + test (Release) + shell: powershell + run: | + dotnet build InboxIntel.sln -c Release --nologo + dotnet test InboxIntel.sln -c Release --no-build --nologo + + - name: Redeploy staging stack + shell: powershell + run: | + docker compose -p inboxintel-staging ` + --env-file deploy/.env.staging ` + -f docker-compose.yml -f docker-compose.staging.yml ` + up -d --build + docker compose -p inboxintel-staging ps + + - name: Staging endpoints + shell: powershell + run: Write-Host "Staging up — Frontend http://localhost:18081 API http://localhost:18080/swagger" diff --git a/.gitea/workflows/security.yml b/.gitea/workflows/security.yml new file mode 100644 index 0000000..ed97bf7 --- /dev/null +++ b/.gitea/workflows/security.yml @@ -0,0 +1,46 @@ +name: Security + +# Scans run alongside CI on every PR and on pushes to the long-lived branches. +# This is the DETECTIVE layer (backstop). The PREVENTIVE layer is the local +# pre-commit hook — this catches anything that slipped past it (e.g. --no-verify) +# and scans the full history, not just the staged diff. +on: + push: + branches: [main, develop] + pull_request: + +jobs: + secrets: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # full history so gitleaks scans every commit + - name: Secret scan (gitleaks) + run: | + docker run --rm -v "$PWD:/repo" zricethezav/gitleaks:latest \ + detect --source=/repo --redact --verbose --exit-code 1 + + dependencies: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + - name: Restore + run: dotnet restore InboxIntel.sln + - name: .NET vulnerable packages (fail on any) + run: | + dotnet list InboxIntel.sln package --vulnerable --include-transitive 2>&1 | tee vuln.txt + if grep -qiE 'Critical|High|Moderate|Low' vuln.txt; then + echo "::error::Vulnerable NuGet packages detected — see the table above." + exit 1 + fi + echo "No vulnerable NuGet packages." + - uses: actions/setup-node@v4 + with: + node-version: '20' + - name: npm audit (fail on high/critical) + working-directory: frontend + run: npm audit --audit-level=high diff --git a/CHANGELOG.md b/CHANGELOG.md index dd3317d..441e486 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ All notable changes to InboxIntel are documented here. Format follows ## [Unreleased] ### Added +- CI/CD pipeline (`.gitea/workflows/`): `security` (gitleaks secret scan + NuGet/npm + vulnerability gate), `deploy-staging` (auto-redeploy local staging on `develop`), + `deploy-prod` (tag-gated production promotion, inactive until the server exists). - Formal Git workflow & environment strategy (`docs/WORKFLOW.md`). - Staging environment overlay (`docker-compose.staging.yml`) — production-shaped Linux containers on Windows, isolated ports/volumes.