Deployment environments
Environments are named deployment targets (e.g. staging, production, eu-west) with composable safety gates. A workflow job declaring environment: production is evaluated against ALL configured gates before fremforge dispatches the runner pod — if any gate fails, the deploy is refused with a structured reason. Operators triage via the admin UI; the workflow can be re-run once the underlying condition clears.
Quick start
Go to fremforge admin → Environments (
https://frem.sh/<org>/_admin/environments).Click Create environment, enter a name, save.
Open the new environment’s detail page, scroll to Deploy gates, configure the gates you want, click Save deploy gates.
Reference the environment in any workflow job:
jobs: deploy: runs-on: fremforge environment: production steps: - run: ./deploy.sh
The next deploy that targets this environment will be evaluated against your gates.
The 9 gates
All gates are independent and composable. ALL configured gates must PASS for a deploy to proceed. A gate with no configuration is skipped (treated as pass).
A. Required approvers
Block dispatch until N humans click Approve on the pending deploy.
- When to use: production deploys, regulated environments, anything where a second pair of eyes is the safety net.
- Configure: Required reviewers on the env detail page. 0 = no approval; 1–6 = N approvers required.
- Where to approve: fremforge admin → Deployments (
/<org>/_admin/deployments), “Pending approvals” section. - Notes: the workflow’s triggering user (the person who pushed the commit / opened the PR) cannot approve their own deploy. The platform rejects self-approval at both the route layer and the evaluator.
- Revoke: approvers can revoke their own approval before the deploy dispatches (e.g. you spotted a problem after clicking Approve). Once the deploy has been dispatched, the approval is immutable.
B. Allowed refs (branch / tag patterns)
Restrict which git refs can deploy to this environment.
- When to use: “only
maindeploys to production”; “onlyrelease/*tags deploy to canary”. - Configure: Deployment branches and tags on the env detail page. GitHub-style globs:
main,release/*,**/hotfix-*,refs/tags/v[0-9]+.*. Empty = any ref allowed.
C. Time window
Restrict deploys to specific business hours or block during freezes.
When to use: “deploys only Mon–Thu 09:00–17:00 CET”; “block all deploys 22 Dec – 2 Jan (holiday freeze)”.
Configure: Time-window JSON on the env detail page. Shape:
{ "tz": "Europe/Copenhagen", "allowed": [ { "days": ["Mon", "Tue", "Wed", "Thu"], "from": "09:00", "to": "17:00" } ], "blackouts": [ { "from": "2026-12-22T00:00Z", "to": "2027-01-02T00:00Z", "reason": "holiday freeze" } ] }tz: IANA timezone. Defaults to UTC.allowed: array of allowed windows. Days are 3-letter (Mon..Sun). Times are 24-hourHH:MMintz.blackouts: array of absolute UTC ranges where deploys are forbidden regardless ofallowed.
Notes: blackouts take precedence over
allowed. Empty = no time restriction.
D. SLO budget healthy
Refuse deploys while the SLO error-budget is being burned.
- When to use: “don’t deploy while customer-facing latency or error rate is in red — fix the regression first.”
- Configure: Block when these alarms are firing — comma-separated CES/LTS alarm names that fremforge’s burn-rate watchdogs maintain Example:
fremforge-prd-api-slo-burn-1h-sev1, fremforge-prd-api-slo-burn-6h-sev2. - How it works: fremforge maintains a per-alarm
staterow updated by the SMN-fanout transition handler (and a periodic heartbeat keeps it fresh). When any listed alarm is in statefiring, this gate refuses dispatch. - Fail-closed: a missing row OR a stale (>5 min) row also fails — operator inspection is preferred to silent bypass.
E. Required status checks
Require named Forgejo commit-status contexts to be success on the deploying commit.
- When to use: “deploys must wait until dep-scan, SAST, and image-scan all pass on this commit.”
- Configure: Required check contexts — one per line. Examples:
dep-scan,sast,sha-pin-scan,image-scan-trivy. - How it works: fremforge reads
GET /api/v1/repos/<o>/<r>/statuses/<sha>at preflight time. Each required context must report statesuccess. Missing OR non-success states fail the gate.
F. Concurrency lock
Cap the number of in-flight deploys to this environment.
- When to use: “only one prod deploy at a time” — prevents race conditions where two deploys clobber each other.
- Configure: Max in-flight deploys — integer 0–100. 0 = no cap.
- Counts:
pending_gates,pending_approval, anddispatchedstates. Acompletedorfailedattempt no longer counts.
G. Recent-rollback cooldown
After a rollback, block deploys for N minutes so the team can investigate.
- When to use: “after a rollback, force a 60-min pause before re-deploying” — prevents accidentally re-triggering the same regression.
- Configure: Cooldown after rollback (minutes) — integer 0–1440 (24h). 0 = no cooldown.
H. Linked-ticket required
Require the deploying PR’s body to reference an issue/incident ticket.
- When to use: compliance / audit-trail requirement — every prod deploy must have a “why” attached.
- Configure: check Require PR body to reference an issue/incident ticket. Optional Ticket pattern regex override.
- Default pattern:
(?i)(JIRA|FF|INC|FIX)-[0-9]+|fixes? #[0-9]+|closes? #[0-9]+|resolves? #[0-9]+— matches JIRA-1234, FF-99,fixes #42,Closes #99, etc. The(?i)prefix makes it case-insensitive. - Per-tenant override: any valid JavaScript regex.
(?i)prefix is honoured.
I. Operator pause flag
Cluster-wide / tenant-wide / per-env switch to halt all deploys.
- When to use: during an incident. Oncall sets a cluster-wide pause; investigates; clears.
- Configure scopes:
- Per-environment (tenant admin):
<org>/_admin/deployments→ “Pause flags” section. - Tenant-wide (operator):
_app/_admin/deploy-pause. Blocks all envs for one tenant. - Cluster-wide (operator):
_app/_admin/deploy-pause. Blocks every deploy everywhere.
- Per-environment (tenant admin):
- Resolution: the most-specific scope wins for the reason field, but ANY active scope blocks the deploy.
How an evaluation runs
When a workflow with environment: <name> is picked up by the runner-controller:
- Preflight — the controller looks up the environment row in the tenant. No row = no gates apply (lazy onboarding; fresh customers’ workflows aren’t blocked before they configure envs).
- Create-or-find a
deploy_attemptkeyed on(env, ref, commit_sha). Re-dispatches of the same workflow against the same commit share the same attempt — so an approval flow lasts across retries. - Resolve plumbing — actor email via Forgejo
/users/<name>; PR body via/repos/<o>/<r>/commits/<sha>/pulls. Best-effort; fallback to synthetic if Forgejo is degraded. - Evaluate all 9 gates in parallel — each gate is independent and reads its own DB / Forgejo signal.
- Compose — if ALL configured gates pass, status flips to
dispatchedand the runner pod creates. If ONLY Gate A (approvers) is failing, status flips topending_approval— admins can click Approve, the next retry re-evaluates. If any other gate fails, status flips togates_failed. - Audit — the full gate-result JSON lands in
deploy_attempts.gates_resultand an audit event fires.
What a refused deploy looks like
The customer sees: workflow run cancelled, with a commit-status check showing the refusal reason (e.g. deploy_gate_C_time_window — outside allowed windows).
The org admin sees: a row in Pending deployments with the full evaluation breakdown — which gates passed, which failed, with the specific reason string per gate.
The operator sees: LTS log lines runner_dispatch_refused_deploy_gate keyed on the gate name; a CES alarm fires when refusal counts exceed normal (configurable per-tenant).
Combining gates
The ladder is intentionally composable — most customers configure 2–3 gates per env, not all 9. Suggested starting points:
| Pattern | Gates configured | Rationale |
|---|---|---|
| Solo dev, single env | none | No gates needed at solo-team scale; fremforge defaults to “all branches allowed”. |
| Small team, prod env | B (refs=main) + I (pause-flag available) | Prevents accidental feature-branch deploys to prod; oncall has the kill-switch. |
| Mid-size team, regulated | A (2 reviewers) + B (refs=main, release/*) + H (linked-ticket) + I | Approval + audit-trail + cross-team kill-switch. |
| Enterprise, multi-stage | A (3 reviewers) + B + C (business hours) + D (SLO healthy) + E (dep-scan + sast) + F (concurrency=1) + G (60-min cooldown) + H + I | Full ladder; every gate makes sense at this scale. |
Start small — add gates as you grow.
Per-environment secrets and variables
Independently of the gates, environments carry their own secrets and variables that are only injected into jobs declaring environment: <name>. See Secrets for the secret-handling pattern.
OIDC environment claim
Workflows that declare environment: <name> get an additional OIDC claim — sub becomes repo:<owner>/<repo>:environment:<name>. This is what cloud trust policies match on for short-lived federation (AWS STS AssumeRoleWithWebIdentity, Azure WIF, GCP Workload Identity Federation). See OIDC federation for the trust-policy shapes.
Troubleshooting
The workflow is stuck on “Review pending” and no notification went out. → Check the env’s Required reviewers count. Approvals happen on the Pending deployments admin page; fremforge doesn’t email reviewers (yet). Add the page to your team’s morning-standup loop.
A deploy was refused with deploy_gate_D_slo_burn but I don’t have Gate D configured.
→ Gate D refuses on a missing OR stale slo_burn_state row. If you’ve LISTED an alarm name but never wrote a row for it, the gate auto-refuses. Either remove the alarm name from the gate config OR seed the row via POST /jobs/slo-burn-state-write (operator-only).
The PR body matches a ticket pattern but Gate H still fails. → The deploy_attempt was created BEFORE the PR was opened. fremforge captures the PR body at first-evaluation time and doesn’t re-fetch. Push an empty commit to trigger a fresh evaluation.
I want to bypass the gates temporarily.
→ Operator-only: set signing_check_bypass for the tenant via SQL (the deploy-gate ladder doesn’t yet have a per-tenant bypass UI; that’s a Phase 2 polish item). The pause-flag scopes are clear/active toggles only.
Audit events
Every gate decision emits structured audit events:
| Event | When |
|---|---|
environment.gates.update | Tenant admin changes gate config |
tenant.deploy.approval_granted | Approver clicks Approve |
tenant.deploy.approval_rejected | Approver clicks Reject (with reason) |
tenant.deploy.approval_revoked | Approver revokes their own approval |
tenant.deploy.pause_set / _cleared | Per-env pause flag toggled (tenant admin) |
platform.deploy.pause_set / _cleared | Cluster or tenant pause flag toggled (operator) |
Plus the runner-controller’s structured log lines: runner_dispatch_refused_deploy_gate_<gate>, deploy_gate_eval_failed, deploy_gate_persist_failed.