Skip to content

Commit 60835d3

Browse files
committed
Merge main to resolve conflicts
Resolved conflicts by accepting main's versions: - AutomationProposalService.cs: whitespace changes - UnitOfWork.cs: error message update - ConcurrencyRaceConditionStressTests.cs: relaxed assertions for SQLite CI - AutomationProposalServiceEdgeCaseTests.cs: aligned test names and error messages
2 parents 705e411 + 6f47d6c commit 60835d3

File tree

110 files changed

+12856
-118
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

110 files changed

+12856
-118
lines changed
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
# =============================================================================
2+
# CD Staging Gate — Automated staging deployment verification and production
3+
# promotion gate. Implements Phase 1-2 of the staged deployment workflow
4+
# (docs/ops/DEPLOYMENT_WORKFLOW.md) with a manual approval gate for Phase 3.
5+
#
6+
# Triggers:
7+
# - Release published (automatic)
8+
# - Manual workflow dispatch (for re-runs or pre-release validation)
9+
#
10+
# ADR: ADR-0028 (Staged Deployment — Blue/Green with Canary Verification)
11+
# Issue: #101 (OPS-09)
12+
# =============================================================================
13+
14+
name: CD Staging Gate
15+
16+
on:
17+
workflow_dispatch:
18+
inputs:
19+
image_tag:
20+
description: "Container image tag to deploy (e.g., v0.2.0)"
21+
required: true
22+
type: string
23+
skip_smoke:
24+
description: "Skip smoke tests (emergency only)"
25+
required: false
26+
type: boolean
27+
default: false
28+
release:
29+
types:
30+
- published
31+
32+
permissions:
33+
contents: read
34+
actions: read
35+
36+
concurrency:
37+
group: cd-staging-${{ github.ref }}
38+
cancel-in-progress: false
39+
40+
env:
41+
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
42+
43+
jobs:
44+
# -----------------------------------------------------------------------
45+
# Phase 1: Build Verification
46+
# -----------------------------------------------------------------------
47+
build-verification:
48+
name: "Phase 1: Build Verification"
49+
runs-on: ubuntu-latest
50+
timeout-minutes: 20
51+
outputs:
52+
image_tag: ${{ steps.resolve-tag.outputs.tag }}
53+
steps:
54+
- name: Checkout
55+
uses: actions/checkout@v6
56+
57+
- name: Resolve image tag
58+
id: resolve-tag
59+
env:
60+
INPUT_TAG: ${{ inputs.image_tag }}
61+
EVENT_NAME: ${{ github.event_name }}
62+
RELEASE_TAG: ${{ github.event.release.tag_name }}
63+
run: |
64+
if [[ -n "$INPUT_TAG" ]]; then
65+
TAG="$INPUT_TAG"
66+
elif [[ "$EVENT_NAME" == "release" ]]; then
67+
TAG="$RELEASE_TAG"
68+
else
69+
TAG="$(git describe --tags --always)"
70+
fi
71+
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
72+
echo "Resolved image tag: $TAG"
73+
74+
- name: Setup .NET
75+
uses: actions/setup-dotnet@v5
76+
with:
77+
dotnet-version: 8.0.x
78+
cache: true
79+
cache-dependency-path: |
80+
backend/Taskdeck.sln
81+
backend/**/*.csproj
82+
83+
- name: Setup Node
84+
uses: actions/setup-node@v6
85+
with:
86+
node-version: 24.13.1
87+
cache: npm
88+
cache-dependency-path: frontend/taskdeck-web/package-lock.json
89+
90+
- name: Restore and build backend
91+
run: |
92+
dotnet restore backend/Taskdeck.sln
93+
dotnet build backend/Taskdeck.sln -c Release --no-restore
94+
95+
- name: Install and build frontend
96+
working-directory: frontend/taskdeck-web
97+
run: |
98+
npm ci
99+
npx vite build
100+
101+
- name: Build container images
102+
run: |
103+
docker build -f deploy/docker/backend.Dockerfile -t taskdeck-api:${{ steps.resolve-tag.outputs.tag }} .
104+
docker build --build-arg VITE_API_BASE_URL=/api -f deploy/docker/frontend.Dockerfile -t taskdeck-web:${{ steps.resolve-tag.outputs.tag }} .
105+
106+
- name: Verify compose configuration
107+
run: |
108+
TASKDECK_JWT_SECRET=ci-staging-gate-secret \
109+
docker compose -f deploy/docker-compose.yml --profile baseline config > /dev/null
110+
111+
- name: Write Phase 1 summary
112+
run: |
113+
cat <<EOF >> "$GITHUB_STEP_SUMMARY"
114+
## Phase 1: Build Verification -- PASSED
115+
116+
- **Image tag**: \`${{ steps.resolve-tag.outputs.tag }}\`
117+
- Backend: restore + build (Release) passed
118+
- Frontend: npm ci + vite build passed
119+
- Container images: built successfully
120+
- Compose config: validated
121+
EOF
122+
123+
# -----------------------------------------------------------------------
124+
# Phase 2: Staging Smoke Tests
125+
# -----------------------------------------------------------------------
126+
staging-smoke:
127+
name: "Phase 2: Staging Smoke Tests"
128+
needs: build-verification
129+
if: ${{ !inputs.skip_smoke }}
130+
runs-on: ubuntu-latest
131+
timeout-minutes: 15
132+
steps:
133+
- name: Checkout
134+
uses: actions/checkout@v6
135+
136+
- name: Build container images
137+
run: |
138+
docker build -f deploy/docker/backend.Dockerfile -t taskdeck-api:local .
139+
docker build --build-arg VITE_API_BASE_URL=/api -f deploy/docker/frontend.Dockerfile -t taskdeck-web:local .
140+
141+
- name: Start stack
142+
run: |
143+
TASKDECK_JWT_SECRET=ci-staging-smoke-secret \
144+
docker compose -f deploy/docker-compose.yml --profile baseline up -d
145+
146+
- name: Wait for services to be ready
147+
run: |
148+
echo "Waiting for services to start..."
149+
for attempt in $(seq 1 30); do
150+
if curl -sf http://localhost:8080/health/ready > /dev/null 2>&1; then
151+
echo "Services ready after $((attempt * 5)) seconds"
152+
break
153+
fi
154+
if [[ "$attempt" -eq 30 ]]; then
155+
echo "Services failed to start within 150 seconds" >&2
156+
docker compose -f deploy/docker-compose.yml --profile baseline logs
157+
exit 1
158+
fi
159+
sleep 5
160+
done
161+
162+
- name: Run smoke tests
163+
run: |
164+
bash scripts/deploy/smoke-test.sh http://localhost:8080
165+
166+
- name: Collect logs on failure
167+
if: failure()
168+
run: |
169+
echo "--- Container status ---"
170+
docker compose -f deploy/docker-compose.yml --profile baseline ps
171+
echo ""
172+
echo "--- Container logs ---"
173+
docker compose -f deploy/docker-compose.yml --profile baseline logs --tail=100
174+
175+
- name: Stop stack
176+
if: always()
177+
run: |
178+
docker compose -f deploy/docker-compose.yml --profile baseline down -v
179+
180+
- name: Write Phase 2 summary
181+
run: |
182+
cat <<EOF >> "$GITHUB_STEP_SUMMARY"
183+
## Phase 2: Staging Smoke Tests -- PASSED
184+
185+
- **Image tag**: \`${{ needs.build-verification.outputs.image_tag }}\`
186+
- All S1-S9 smoke checks passed
187+
- Stack started, verified, and cleaned up
188+
EOF
189+
190+
# -----------------------------------------------------------------------
191+
# Production Promotion Gate (manual approval)
192+
# -----------------------------------------------------------------------
193+
promotion-gate:
194+
name: "Phase 3: Production Promotion Gate"
195+
needs: [build-verification, staging-smoke]
196+
if: always() && needs.build-verification.result == 'success' && (needs.staging-smoke.result == 'success' || needs.staging-smoke.result == 'skipped')
197+
runs-on: ubuntu-latest
198+
environment: production
199+
steps:
200+
- name: Promotion approved
201+
run: |
202+
cat <<EOF >> "$GITHUB_STEP_SUMMARY"
203+
## Phase 3: Production Promotion Gate -- APPROVED
204+
205+
- **Image tag**: \`${{ needs.build-verification.outputs.image_tag }}\`
206+
- Build verification: passed
207+
- Staging smoke: ${{ needs.staging-smoke.result }}
208+
- Manual approval: granted
209+
- **Next step**: Execute Phase 3 (canary deployment) and Phase 4 (promotion) per \`docs/ops/DEPLOYMENT_WORKFLOW.md\`
210+
EOF
211+
echo "Production promotion approved for tag: ${{ needs.build-verification.outputs.image_tag }}"
212+
echo "Follow the deployment workflow in docs/ops/DEPLOYMENT_WORKFLOW.md for Phases 3-4."

.github/workflows/ci-extended.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,16 @@ jobs:
107107
dotnet-version: 8.0.x
108108
node-version: 24.13.1
109109

110+
visual-regression:
111+
name: Visual Regression
112+
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && (contains(github.event.pull_request.labels.*.name, 'testing') || contains(github.event.pull_request.labels.*.name, 'visual')))
113+
needs:
114+
- backend-solution
115+
uses: ./.github/workflows/reusable-visual-regression.yml
116+
with:
117+
dotnet-version: 8.0.x
118+
node-version: 24.13.1
119+
110120
load-concurrency-harness:
111121
name: Load and Concurrency Harness
112122
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'testing'))
@@ -119,3 +129,10 @@ jobs:
119129
k6-vus: "20"
120130
k6-duration: "90s"
121131
k6-user-pool: "6"
132+
133+
container-integration:
134+
name: Container Integration (Testcontainers)
135+
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'testing'))
136+
uses: ./.github/workflows/reusable-container-integration.yml
137+
with:
138+
dotnet-version: 8.0.x

.github/workflows/ci-required.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
# ├── reusable-backend-solution.yml (label: testing)
2222
# ├── reusable-e2e-smoke.yml (label: testing)
2323
# ├── reusable-demo-director-smoke.yml (label: automation)
24-
# └── reusable-load-concurrency-harness.yml (label: testing)
24+
# ├── reusable-load-concurrency-harness.yml (label: testing)
25+
# └── reusable-container-integration.yml (label: testing) — Testcontainers PostgreSQL
2526
#
2627
# ci-nightly.yml scheduled regression (03:25 UTC daily)
2728
# ├── reusable-openapi-guardrail.yml
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# =============================================================================
2+
# Mutation Testing — manual/nightly lane for mutation score tracking.
3+
# NOT a PR gate. Runs on manual dispatch and weekly schedule.
4+
# Reports uploaded as artifacts for triage, not enforcement.
5+
# =============================================================================
6+
7+
name: Mutation Testing
8+
9+
on:
10+
schedule:
11+
# Weekly on Sunday at 04:00 UTC — avoids overlap with nightly CI at 03:25.
12+
- cron: '0 4 * * 0'
13+
workflow_dispatch:
14+
inputs:
15+
backend_only:
16+
description: 'Run backend mutation testing only'
17+
required: false
18+
type: boolean
19+
default: false
20+
frontend_only:
21+
description: 'Run frontend mutation testing only'
22+
required: false
23+
type: boolean
24+
default: false
25+
26+
permissions:
27+
contents: read
28+
29+
concurrency:
30+
group: ${{ github.workflow }}-${{ github.ref }}
31+
cancel-in-progress: true
32+
33+
jobs:
34+
backend-mutation:
35+
name: Backend Mutation (Stryker.NET)
36+
if: ${{ !inputs.frontend_only }}
37+
runs-on: ubuntu-latest
38+
timeout-minutes: 60
39+
steps:
40+
- name: Checkout
41+
uses: actions/checkout@v4
42+
43+
- name: Setup .NET
44+
uses: actions/setup-dotnet@v4
45+
with:
46+
dotnet-version: 8.0.x
47+
48+
- name: Install Stryker.NET
49+
run: dotnet tool install --global dotnet-stryker
50+
51+
- name: Restore solution
52+
run: dotnet restore backend/Taskdeck.sln
53+
54+
- name: Run Stryker.NET
55+
working-directory: backend
56+
run: dotnet stryker --config-file stryker-config.json
57+
58+
- name: Upload Stryker report
59+
if: always()
60+
uses: actions/upload-artifact@v4
61+
with:
62+
name: stryker-net-report
63+
path: backend/StrykerOutput/**/reports/
64+
retention-days: 30
65+
66+
frontend-mutation:
67+
name: Frontend Mutation (Stryker JS)
68+
if: ${{ !inputs.backend_only }}
69+
runs-on: ubuntu-latest
70+
timeout-minutes: 60
71+
steps:
72+
- name: Checkout
73+
uses: actions/checkout@v4
74+
75+
- name: Setup Node.js
76+
uses: actions/setup-node@v4
77+
with:
78+
node-version: 24.13.1
79+
cache: npm
80+
cache-dependency-path: frontend/taskdeck-web/package-lock.json
81+
82+
- name: Install dependencies
83+
working-directory: frontend/taskdeck-web
84+
run: npm ci
85+
86+
- name: Run Stryker
87+
working-directory: frontend/taskdeck-web
88+
run: npx stryker run
89+
90+
- name: Upload Stryker report
91+
if: always()
92+
uses: actions/upload-artifact@v4
93+
with:
94+
name: stryker-js-report
95+
path: frontend/taskdeck-web/reports/
96+
retention-days: 30
97+
98+
summary:
99+
name: Mutation Testing Summary
100+
if: always()
101+
needs: [backend-mutation, frontend-mutation]
102+
runs-on: ubuntu-latest
103+
steps:
104+
- name: Report status
105+
env:
106+
BACKEND_RESULT: ${{ needs.backend-mutation.result || 'skipped' }}
107+
FRONTEND_RESULT: ${{ needs.frontend-mutation.result || 'skipped' }}
108+
run: |
109+
{
110+
echo "## Mutation Testing Results"
111+
echo ""
112+
echo "| Lane | Status |"
113+
echo "|------|--------|"
114+
echo "| Backend (Stryker.NET) | ${BACKEND_RESULT} |"
115+
echo "| Frontend (Stryker JS) | ${FRONTEND_RESULT} |"
116+
echo ""
117+
echo "Download HTML/JSON reports from the **Artifacts** section above."
118+
echo ""
119+
echo "See [Mutation Testing Policy](docs/testing/MUTATION_TESTING_POLICY.md) for threshold strategy and triage guidance."
120+
} >> "$GITHUB_STEP_SUMMARY"

0 commit comments

Comments
 (0)