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 provisions the runner VM — if any gate fails, the deploy is refused with a structured reason. Org admins 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).Under New environment, enter a name and click Create.
Open the new environment’s detail page, scroll to Deploy policies, configure the policies you want, click Save policies.
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 your burn-rate watchdogs maintain. Example:
my-api-latency-sev1, my-api-error-budget-sev2. - How it works: fremforge tracks the latest state of each listed alarm. When any listed alarm is in state
firing, this gate refuses dispatch. - Fail-closed: if fremforge has no recent state for a listed alarm, the gate fails closed — investigation 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. Pause flag
A per-environment switch to halt all deploys.
- When to use: during an incident. Set a pause; investigate; clear.
- Configure: Per-environment (tenant admin):
<org>/_admin/deployments→ “Pause flags” section. fremforge can also apply a platform-wide pause during an incident; any active pause that covers your environment blocks the deploy. - 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 platform:
- Preflight — fremforge looks up the environment for the tenant. No environment configured = no gates apply (lazy onboarding; fresh customers’ workflows aren’t blocked before they configure envs).
- Create-or-find a deploy attempt keyed 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 and PR body are read from Forgejo. Best-effort; fallback to synthetic if Forgejo is degraded.
- Evaluate all 9 gates in parallel — each gate is independent and reads its own signal.
- Compose — if ALL configured gates pass, status flips to
dispatchedand the runner VM provisions. 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 breakdown is recorded and 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.
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 fails closed when fremforge has no recent state for a listed alarm. If you’ve LISTED an alarm name that has never reported state, the gate auto-refuses. Remove the alarm name from the gate config, or make sure the alarm is wired so its state is reported.
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. → There is no per-tenant bypass UI yet. Contact support@frem.sh to bypass a gate temporarily. 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 | Platform-wide pause flag toggled |