Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 28 additions & 25 deletions .github/workflows/cd-staging-gate.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# =============================================================================
# CD Staging Gate — Automated staging deployment verification with a parked
# production-environment handoff. Implements Phase 1-2 of the staged deployment
# workflow (docs/ops/DEPLOYMENT_WORKFLOW.md). Environment protection is external
# repository state and must not be inferred from this workflow definition.
# CD Staging Gate — Automated staging verification with a non-deploying parked
# handoff. Implements Phase 1-2 of the staged deployment workflow
# (docs/ops/DEPLOYMENT_WORKFLOW.md) without binding a GitHub environment or
# creating a deployment.
#
# Triggers:
# - Manual workflow dispatch (for re-runs or pre-release validation)
Expand All @@ -17,11 +17,11 @@ on:
workflow_dispatch:
inputs:
image_tag:
description: "Container image tag to deploy (e.g., v0.2.0)"
description: "Container image tag to build and verify (e.g., v0.2.0)"
required: true
type: string
skip_smoke:
description: "Skip smoke tests (emergency only)"
description: "Skip the local Compose smoke phase (diagnostic reruns only)"
required: false
type: boolean
default: false
Expand Down Expand Up @@ -96,10 +96,12 @@ jobs:
docker build -f deploy/docker/backend.Dockerfile -t taskdeck-api:${{ steps.resolve-tag.outputs.tag }} .
docker build --build-arg VITE_API_BASE_URL=/api -f deploy/docker/frontend.Dockerfile -t taskdeck-web:${{ steps.resolve-tag.outputs.tag }} .

- name: Prepare ephemeral Compose inputs
id: compose-inputs
run: node scripts/ci/prepare-staging-compose-inputs.mjs

- name: Verify compose configuration
run: |
TASKDECK_JWT_SECRET=ci-staging-gate-secret \
docker compose -f deploy/docker-compose.yml --profile baseline config > /dev/null
run: docker compose -f deploy/docker-compose.yml --profile baseline config > /dev/null

- name: Write Phase 1 summary
run: |
Expand Down Expand Up @@ -131,10 +133,12 @@ jobs:
docker build -f deploy/docker/backend.Dockerfile -t taskdeck-api:local .
docker build --build-arg VITE_API_BASE_URL=/api -f deploy/docker/frontend.Dockerfile -t taskdeck-web:local .

- name: Prepare ephemeral Compose inputs
id: compose-inputs
run: node scripts/ci/prepare-staging-compose-inputs.mjs

- name: Start stack
run: |
TASKDECK_JWT_SECRET=ci-staging-smoke-secret \
docker compose -f deploy/docker-compose.yml --profile baseline up -d
run: docker compose -f deploy/docker-compose.yml --profile baseline up -d

- name: Wait for services to be ready
run: |
Expand All @@ -157,7 +161,7 @@ jobs:
bash scripts/deploy/smoke-test.sh http://localhost:8080

- name: Collect logs on failure
if: failure()
if: ${{ failure() && steps.compose-inputs.outcome == 'success' }}
run: |
echo "--- Container status ---"
docker compose -f deploy/docker-compose.yml --profile baseline ps
Expand All @@ -166,7 +170,7 @@ jobs:
docker compose -f deploy/docker-compose.yml --profile baseline logs --tail=100

- name: Stop stack
if: always()
if: ${{ always() && steps.compose-inputs.outcome == 'success' }}
run: |
docker compose -f deploy/docker-compose.yml --profile baseline down -v

Expand All @@ -181,26 +185,25 @@ jobs:
EOF

# -----------------------------------------------------------------------
# Production Environment Handoff (protection rules are external repository state)
# Parked Handoff (summary only; no environment or deployment)
# -----------------------------------------------------------------------
Comment thread
Chris0Jeky marked this conversation as resolved.
promotion-gate:
name: "Phase 3: Production Environment Handoff"
parked-handoff:
name: "Phase 3: Parked Handoff Summary"
Comment thread
Chris0Jeky marked this conversation as resolved.
needs: [build-verification, staging-smoke]
if: always() && needs.build-verification.result == 'success' && (needs.staging-smoke.result == 'success' || needs.staging-smoke.result == 'skipped')
runs-on: ubuntu-latest
environment: production
steps:
- name: Record production environment handoff
- name: Record parked non-deploying handoff
run: |
cat <<EOF >> "$GITHUB_STEP_SUMMARY"
## Phase 3: Production Environment Handoff -- REACHED
## Phase 3: Parked Handoff -- REACHED

- **Image tag**: \`${{ needs.build-verification.outputs.image_tag }}\`
- Build verification: passed
- Staging smoke: ${{ needs.staging-smoke.result }}
- Environment binding: \`production\`
- Approval protection: external repository setting; not asserted by this workflow
- **Next step**: Keep the parked deployment path inactive until #1504 is maintainer-resolved
- Environment binding: none
- Deployment created: no
- **Next step**: Keep deployment inactive until #1504 is maintainer-resolved
EOF
echo "Production environment handoff reached for tag: ${{ needs.build-verification.outputs.image_tag }}"
echo "This workflow does not assert that environment approval protection is configured."
echo "Parked non-deploying handoff recorded for tag: ${{ needs.build-verification.outputs.image_tag }}"
echo "This workflow does not bind an environment or create a deployment."
4 changes: 3 additions & 1 deletion docs/AUDIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,9 @@ Taskdeck is a **mature, well-engineered product** at the end of its core build p
| `mutation-testing.yml` | Weekly | Stryker.NET + Stryker JS (non-blocking) |
| `ci-release.yml` | Tag/release | SBOM/provenance, container artifacts |
| `release-security.yml` | Tag/release | Dependency inventory, vulnerability reports |
| `cd-staging-gate.yml` | Release | 4-phase blue/green with manual approval |
| `cd-staging-gate.yml` | Release _(2026-04-16 snapshot; superseded)_ | 4-phase blue/green with manual approval _(historical)_ |

> **Current-state correction (2026-07-27):** after `#1228`, this workflow is manual-dispatch-only. The stacked, unmerged `#1506` candidate has no GitHub environment or deployment job and stops at a parked summary. Run [30244212896](https://github.com/Chris0Jeky/Taskdeck/actions/runs/30244212896) passed build, real S1-S9 smoke/cleanup, environment `none`, and deployment `no` at exact workflow/helper head `3efb7bd4`; both values were masked and the exact-SHA deployments query returned `[]`. This follow-up changes docs only. Post-retarget CodeQL, `#1504` activation, topology, and maintainer merge remain pending.

### Operations Maturity

Expand Down
6 changes: 5 additions & 1 deletion docs/IMPLEMENTATION_MASTERPLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ Companion Active Docs:

## Delivery update (2026-07-27, release workflow truth)

- **Manual-only parked staging gate (`#1228`):** `.github/workflows/cd-staging-gate.yml` no longer subscribes to `release: published`; its required-input `workflow_dispatch` path, jobs, permissions, and `production` environment binding remain available for deliberate operator runs. The repository does not currently configure that environment or required reviewers, so the workflow no longer calls the binding an approval gate; maintainer-owned activation protection is tracked in `#1504`. Active beta release lanes (`release-desktop.yml`, `ci-release.yml`, and `release-security.yml`) are unchanged, so publishing a release cannot start this parked staging path.
- **Historical parent milestone — manual-only staging gate (`#1228`):** `.github/workflows/cd-staging-gate.yml` no longer subscribes to `release: published`, so active beta release lanes (`release-desktop.yml`, `ci-release.yml`, and `release-security.yml`) cannot start the parked staging path. At that parent milestone, the required-input `workflow_dispatch` path, jobs, permissions, and an unprotected `production` environment binding remained; the binding was not an approval gate, and activation protection remained maintainer-owned in `#1504`.

## Staged delivery update (2026-07-27, parked staging rehearsal)

- **Current stacked `#1506` candidate supersedes the parent handoff shape:** Docker Compose's effective JSON variable model fails closed unless exactly the JWT and connector inputs are required, then generates and masks fresh values immediately before each job's first Compose use. It removes the unprotected `production` binding and ends at a summary-only handoff that creates no deployment. Manual run [30244212896](https://github.com/Chris0Jeky/Taskdeck/actions/runs/30244212896) passed build, real S1-S9 smoke and cleanup, and the parked summary at exact unmerged workflow/helper head `3efb7bd4`; logs masked both values, and an exact-SHA deployments query returned `[]`. This evidence refresh changes documentation only. Local Node/governance 26/26 and real-Compose probes also pass. Both parent and child remain unmerged; `#1504`, topology/merge, and activation are maintainer-owned, and CodeQL was not generated for the non-default base so must run after retargeting.

## Delivery update (2026-07-26, agentic governance)

Expand Down
8 changes: 6 additions & 2 deletions docs/STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

Last Updated: 2026-07-27

Parked staging-gate rehearsal candidate (2026-07-27, `#1506`, **stacked on unmerged `#1228` and not shipped**):
- **The candidate makes both manual Compose phases self-contained without creating a deployment boundary.** Immediately before each job's first Compose invocation, it asks Docker Compose's effective `config --variables --format json` parser for the variable model, rejects command/JSON/schema/name/`Required` drift, and requires exactly the JWT and connector inputs to be mandatory. Only then does it generate a 48-byte JWT secret and 32-byte connector encryption key with the runner's cryptographic RNG, mask both, and write them to that job's environment file. The former `production` environment job is replaced by a summary-only parked handoff that binds no GitHub environment and creates no deployment.
- **Evidence and boundary:** manual run [30244212896](https://github.com/Chris0Jeky/Taskdeck/actions/runs/30244212896) passed build verification, real S1-S9 Compose smoke plus cleanup, and the parked summary on exact unmerged workflow/helper head `3efb7bd4499c30d0f0b3c3683e43221e46f103f7`; its logs show both inputs masked, `Environment binding: none`, and `Deployment created: no`, and the exact-SHA deployments query returned `[]`. This evidence refresh changes documentation only, not the proven workflow/helper bytes. The combined Node/governance suite 26/26 and local real-Compose probes also pass. The child remains stacked on parked PR `#1502`; `#1504`, topology/merge, and environment activation remain maintainer-owned. CodeQL was not generated for the non-default stacked base and is re-owed after retargeting.

Dockerless Testcontainers gate repair (2026-07-27, `#1518`):
- **The PostgreSQL integration collection now reaches its documented graceful-skip path before Testcontainers validates Docker.** `PostgresContainerFixture` checks `DockerAvailableCheck` before constructing the container, so a Dockerless host runs seven host-native integration tests and reports all 28 container-dependent tests skipped instead of throwing from the fixture constructor. The real public-constructor and injected-factory regressions prove that the unavailable path does not construct or dispose a phantom container. A timed-out Docker CLI probe is killed with its process tree and given a bounded reap before the check returns; the local exact tree finished with 7 passed / 28 skipped / 0 failed and zero `docker info` processes before or after. At reviewed head `6650370b`, the full serialized backend passed 7,465 tests with 33 intentional skips and hosted Container Integration proved the positive startup/connection/teardown path with 35 passed / 0 skipped / 0 failed. Four DCO-signed-off commits merged as `ff1ee066`; exact-main Required CI run `30257236213` and CodeQL run `30257235538` are green.

Expand Down Expand Up @@ -589,7 +593,7 @@ Eight parallel worktree agents delivered new features, security infrastructure,
- **CLD-03 OAuth PKCE and account linking** (`#676`/`#812`): DB-backed auth code store replacing in-memory `ConcurrentDictionary` — `OAuthAuthCode` entity with EF migration, `IOAuthAuthCodeRepository` with atomic `TryConsumeAtomicAsync` (raw SQL `UPDATE WHERE IsConsumed = 0 AND ExpiresAt > now`); PKCE support via `UsePkce = true` in ASP.NET Core 8 OAuth middleware; account linking endpoints (`POST /api/auth/github/link`, `DELETE /api/auth/github/link`, `GET /api/auth/linked-accounts`) with conflict detection and session verification; frontend Linked Accounts section in `ProfileSettingsView` with Link/Unlink buttons and avatar display; 24+ backend tests; adversarial review fixed CSRF on account linking, TOCTOU in expiry check, JWT plaintext in DB, DoS via full-table load, and unbounded table growth

**Ops & Observability:**
- **OPS-09 Staged deployment workflow** (`#101`/`#806`): ADR-0028 documents blue/green + canary deployment strategy with rollback criteria; `docs/ops/DEPLOYMENT_WORKFLOW.md` canonical 4-phase workflow (build verification → staging → production canary → production promotion) with rollback procedures, database migration safety, emergency hotfix override, and ownership/escalation model; `docs/ops/RELEASE_CHECKLIST.md` versioned smoke verification (7 pre-deploy + 9 automated staging + 7 manual staging + 7 canary + 6 post-promotion + 5 post-release checks) with failure response matrix; `scripts/deploy/smoke-test.sh` portable smoke test (9 automated checks: health, API, auth, board auth gate, frontend, SignalR, static assets, security headers, container restart detection); `.github/workflows/cd-staging-gate.yml` retains a `production` environment binding but no live environment protection is configured. _(⚠️ Parked by the 2026-06-13 archive pivot — staged cloud rollout de-scoped, see the parked-cloud note below. The workflow is manual-dispatch-only after **#1228**, so publishing a beta release cannot invoke it; required-reviewer protection before any reactivation is maintainer-owned in **#1504**.)_
- **OPS-09 Staged deployment workflow** (`#101`/`#806`): ADR-0028 documents blue/green + canary deployment strategy with rollback criteria; `docs/ops/DEPLOYMENT_WORKFLOW.md` canonical 4-phase workflow (build verification → staging → production canary → production promotion) with rollback procedures, database migration safety, emergency hotfix override, and ownership/escalation model; `docs/ops/RELEASE_CHECKLIST.md` versioned smoke verification (7 pre-deploy + 9 automated staging + 7 manual staging + 7 canary + 6 post-promotion + 5 post-release checks) with failure response matrix; `scripts/deploy/smoke-test.sh` portable smoke test (9 automated checks: health, API, auth, board auth gate, frontend, SignalR, static assets, security headers, container restart detection). _(⚠️ Parked by the 2026-06-13 archive pivot — staged cloud rollout de-scoped, see the parked-cloud note below. The workflow is manual-dispatch-only after **#1228**, so publishing a beta release cannot invoke it. The stacked **#1506** candidate removes its unprotected `production` binding, supplies masked ephemeral Compose inputs, and ends at a non-deploying summary. Run [30244212896](https://github.com/Chris0Jeky/Taskdeck/actions/runs/30244212896) proved build, real smoke, cleanup, and that summary at unmerged workflow/helper head `3efb7bd4`; retarget-time CodeQL, maintainer topology/merge, and **#1504** activation protection remain pending.)_
- **OBS-02 Error tracking and product analytics** (`#549`/`#811`): config-gated Sentry SDK for backend (`Sentry.AspNetCore` with `BeforeSend` PII scrubbing for emails/JWTs, `ServerName` blanked) and frontend; opt-in product telemetry service (`TelemetryEventService`) aligned with `docs/product/TELEMETRY_TAXONOMY.md` — property key allowlist (15 safe keys), max 10 properties, 200-char value truncation; `TelemetryController` with anonymous config endpoint and authenticated events endpoint; Plausible/Umami analytics script injection (`useAnalyticsScript`) with HTTPS-only URL validation; Pinia `telemetryStore` with consent management, event buffering, and flush; DNT/GPC privacy signal detection prevents auto-restore of consent; telemetry consent toggle in `ProfileSettingsView`; `docs/ops/OBSERVABILITY_SETUP.md` configuration guide; all telemetry opt-in and disabled by default; 38 backend + 25 frontend tests; adversarial review fixed Sentry PII leak, arbitrary properties injection, XSS via script URL, and DNT non-compliance

## Post-Merge Housekeeping (2026-04-12)
Expand Down Expand Up @@ -1245,7 +1249,7 @@ Mutation testing workflow: `.github/workflows/mutation-testing.yml`
- Backend Stryker.NET (Domain) + Frontend Stryker JS (captureStore/boardStore)
- Non-blocking; HTML/JSON reports uploaded as 30-day artifacts

> _(Historical archive-period caveat, 2026-06-13–2026-07-10: release/tag-triggered distribution lanes were parked during the archive pivot. ADR-0044 supersedes that hold and reactivates only the public-beta release work admitted by `docs/REVIVAL_PLAN.md` (REVIVAL-07/14). Tag/release events still **auto-fire** these workflows: `release-desktop.yml` builds the cross-platform self-contained exe + publishes a GitHub Release on any `v*` tag push, and `ci-release.yml`/`release-security.yml` run on `v*` tags / `release: published`. `cd-staging-gate.yml` is manual-dispatch-only after **#1228**, so those release events no longer enter its parked `production` environment gate. SBOM/provenance (`reusable-sbom-provenance.yml`) remains a release-security artifact.)_
> _(Historical archive-period caveat, 2026-06-13–2026-07-10: release/tag-triggered distribution lanes were parked during the archive pivot. ADR-0044 supersedes that hold and reactivates only the public-beta release work admitted by `docs/REVIVAL_PLAN.md` (REVIVAL-07/14). Tag/release events still **auto-fire** these workflows: `release-desktop.yml` builds the cross-platform self-contained exe + publishes a GitHub Release on any `v*` tag push, and `ci-release.yml`/`release-security.yml` run on `v*` tags / `release: published`. `cd-staging-gate.yml` is manual-dispatch-only after **#1228**, so those release events no longer enter the parked staging path. The stacked **#1506** candidate removes the remaining `production` environment binding and reaches only a non-deploying summary; run [30244212896](https://github.com/Chris0Jeky/Taskdeck/actions/runs/30244212896) proved that parked path at exact unmerged workflow/helper head `3efb7bd4`, while retarget-time CodeQL remains unproved and shipment/topology/activation stay maintainer-held. SBOM/provenance (`reusable-sbom-provenance.yml`) remains a release-security artifact.)_

Release workflow: `.github/workflows/ci-release.yml` _(release-triggered; container-image build/export is active only through REVIVAL-07's bounded beta-release scope; SBOM/provenance remains required)_

Expand Down
Loading
Loading