Goal
Every Triarch deploy must follow dev → prod. Two invariants the pipeline must enforce:
- Prod can never be a higher version than dev. If
dev_version >= attempted_prod_version is false, the prod deploy is blocked.
- The exact version going to prod must have been successfully deployed to dev first. No bypassing dev "just this once," regardless of who is asking — including Mike.
Current state
There is no enforcement. As of 2026-05-14:
projects.current_version in triarch.dev is a single field — no separation of dev vs prod
latest_dev_at / latest_prod_at timestamps exist on projects but nothing checks order
- Deploy workflows in shared-workflows fire on
push: [main, release/**, hotfix/**] and immediately deploy to whatever Firebase target the workflow targets
- A direct push to
main or hotfix/** deploys to prod without a prior dev step
Proposed enforcement
1. Schema: separate dev and prod version per project
```sql
ALTER TABLE projects
ADD COLUMN dev_version TEXT,
ADD COLUMN prod_version TEXT,
ADD COLUMN dev_version_deployed_at TIMESTAMPTZ,
ADD COLUMN prod_version_deployed_at TIMESTAMPTZ;
-- Drop current_version after migrating callers
```
Populated by shared-workflows' notify step after a successful deploy to each env.
2. Pre-prod-deploy gate (new reusable workflow)
Add .github/workflows/gate-prod-version.yml in shared-workflows:
```yaml
inputs:
project_key: string # e.g. "triarchsecurity-admin"
target_version: string # e.g. "v3.54.0"
jobs:
gate:
steps:
- Fetch project's dev_version + prod_version from triarch.dev API
- Assert target_version <= dev_version (semver compare)
- Assert target_version > prod_version
- Assert dev_version_deployed_at >= 5 minutes ago AND the dev release for this exact version is recorded
- Fail loudly with a Slack notification if any assertion fails
```
3. Wire into each project's CI/CD
Each project's .github/workflows/*.yml for prod deploys gets:
```yaml
- uses: triarchsecurity/shared-workflows/.github/workflows/gate-prod-version.yml@v8
with:
project_key: triarchsecurity-admin
target_version: ${{ needs.version.outputs.version }}
```
The gate runs before the deploy step. Fails the job if invariants are violated.
4. Branch policy
main always deploys to dev (not prod). The current "push to main deploys to prod" pattern flips.
release/** and hotfix/** branches deploy to prod, but only after passing the gate
- Direct prod deploys via
workflow_dispatch still go through the gate — no manual bypass
--no-verify and [skip ci] ignored by the gate workflow
5. Audit + alerting
- Every gate check writes an audit log entry to triarch.dev (
action: "deploy_gate_check" with target_version, dev_version, prod_version, result)
- A failed gate posts to the project's Slack channel + the central deploy-audit channel
- Daily brief surfaces any gate failure in the last 24h
Acceptance
- New schema columns populated on every successful deploy
- Gate workflow exists in shared-workflows
- All 6 deployable projects (admin, portal, dev-portal, tmi, darksouls, truthtreason) use the gate on prod deploys
- A test deploy attempting prod-without-dev fails the gate and surfaces the failure clearly
- A test deploy attempting prod with version ≤ current prod fails the gate
- Mike (or anyone) cannot bypass — no
--force or [skip gate] option
Context
Mike's explicit ask 2026-05-14:
"I need to be sure that both the production and development versions are clear for each project, and that the ci-cd pipeline enforces that process... that prod can never be a higher version than dev, and that deployments MUST go to dev before going to prod, regardless of how many corners I want to cut myself."
The "regardless of how many corners I want to cut myself" is intentional — no escape hatch.
Related
Goal
Every Triarch deploy must follow
dev → prod. Two invariants the pipeline must enforce:dev_version >= attempted_prod_versionis false, the prod deploy is blocked.Current state
There is no enforcement. As of 2026-05-14:
projects.current_versionin triarch.dev is a single field — no separation of dev vs prodlatest_dev_at/latest_prod_attimestamps exist onprojectsbut nothing checks orderpush: [main, release/**, hotfix/**]and immediately deploy to whatever Firebase target the workflow targetsmainorhotfix/**deploys to prod without a prior dev stepProposed enforcement
1. Schema: separate dev and prod version per project
```sql
ALTER TABLE projects
ADD COLUMN dev_version TEXT,
ADD COLUMN prod_version TEXT,
ADD COLUMN dev_version_deployed_at TIMESTAMPTZ,
ADD COLUMN prod_version_deployed_at TIMESTAMPTZ;
-- Drop current_version after migrating callers
```
Populated by shared-workflows' notify step after a successful deploy to each env.
2. Pre-prod-deploy gate (new reusable workflow)
Add
.github/workflows/gate-prod-version.ymlin shared-workflows:```yaml
inputs:
project_key: string # e.g. "triarchsecurity-admin"
target_version: string # e.g. "v3.54.0"
jobs:
gate:
steps:
- Fetch project's dev_version + prod_version from triarch.dev API
- Assert target_version <= dev_version (semver compare)
- Assert target_version > prod_version
- Assert dev_version_deployed_at >= 5 minutes ago AND the dev release for this exact version is recorded
- Fail loudly with a Slack notification if any assertion fails
```
3. Wire into each project's CI/CD
Each project's
.github/workflows/*.ymlfor prod deploys gets:```yaml
with:
project_key: triarchsecurity-admin
target_version: ${{ needs.version.outputs.version }}
```
The gate runs before the deploy step. Fails the job if invariants are violated.
4. Branch policy
mainalways deploys to dev (not prod). The current "push to main deploys to prod" pattern flips.release/**andhotfix/**branches deploy to prod, but only after passing the gateworkflow_dispatchstill go through the gate — no manual bypass--no-verifyand[skip ci]ignored by the gate workflow5. Audit + alerting
action: "deploy_gate_check"with target_version, dev_version, prod_version, result)Acceptance
--forceor[skip gate]optionContext
Mike's explicit ask 2026-05-14:
The "regardless of how many corners I want to cut myself" is intentional — no escape hatch.
Related