Skip to main content
Private preview. fremforge is in private preview — invited customers only. Content is still subject to change. Request access →
Environments

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

  1. Go to fremforge admin → Environments (https://frem.sh/<org>/_admin/environments).

  2. Click Create environment, enter a name, save.

  3. Open the new environment’s detail page, scroll to Deploy gates, configure the gates you want, click Save deploy gates.

  4. 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 main deploys to production”; “only release/* 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-hour HH:MM in tz.
    • blackouts: array of absolute UTC ranges where deploys are forbidden regardless of allowed.
  • 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 state row updated by the SMN-fanout transition handler (and a periodic heartbeat keeps it fresh). When any listed alarm is in state firing, 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 state success. 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, and dispatched states. A completed or failed attempt 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.
  • 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:

  1. 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).
  2. 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.
  3. 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.
  4. Evaluate all 9 gates in parallel — each gate is independent and reads its own DB / Forgejo signal.
  5. Compose — if ALL configured gates pass, status flips to dispatched and the runner pod creates. If ONLY Gate A (approvers) is failing, status flips to pending_approval — admins can click Approve, the next retry re-evaluates. If any other gate fails, status flips to gates_failed.
  6. Audit — the full gate-result JSON lands in deploy_attempts.gates_result 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.

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:

PatternGates configuredRationale
Solo dev, single envnoneNo gates needed at solo-team scale; fremforge defaults to “all branches allowed”.
Small team, prod envB (refs=main) + I (pause-flag available)Prevents accidental feature-branch deploys to prod; oncall has the kill-switch.
Mid-size team, regulatedA (2 reviewers) + B (refs=main, release/*) + H (linked-ticket) + IApproval + audit-trail + cross-team kill-switch.
Enterprise, multi-stageA (3 reviewers) + B + C (business hours) + D (SLO healthy) + E (dep-scan + sast) + F (concurrency=1) + G (60-min cooldown) + H + IFull 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:

EventWhen
environment.gates.updateTenant admin changes gate config
tenant.deploy.approval_grantedApprover clicks Approve
tenant.deploy.approval_rejectedApprover clicks Reject (with reason)
tenant.deploy.approval_revokedApprover revokes their own approval
tenant.deploy.pause_set / _clearedPer-env pause flag toggled (tenant admin)
platform.deploy.pause_set / _clearedCluster 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.