Onboard to Release Framework
This document walks you through joining Release Framework. The goal is to
get you from “I want to put my project on Release Framework” to a merged
MR that provisions everything you need, in one repository (infra-mgmt),
self-service.
If you are looking for the moving-parts story, what each piece does and why it’s wired the way it is, read Architecture and Internals instead.
What Release Framework gives you
Joining Release Framework provisions:
- A Canonical project on
gitlab.com(public by default) and a Security mirror (always private), with a push mirror from canonical to security. - A per-project service-account bot with its own subgroup. The bot is the credential for repository mirroring and the identity that the security mirror’s pipelines run as.
- Branch and tag protections following Release Framework’s locked-down defaults.
- Configurable approval rules, plus a mandatory PSIRT approval rule on the security mirror.
- common-ci-tasks integration (autolabels, danger, renovate, semantic-release on by default; goreleaser, terraform opt-in).
- Vault integration on both projects.
- A smart sync path from the security mirror back to the canonical project’s container and package registries (deploy token + CI/CD job-token allowlist), so builds and deploys originating from the security mirror can publish to Canonical’s registries.
- Optional sharing into a runway deployment project so that pipelines on the security mirror can trigger downstream deploys.
There is no separate Build mirror. Builds and deploys for Release Framework projects run from the Security mirror.
If you need a configuration option that doesn’t exist yet, whether a new YAML field, a new module variable, or a new escape hatch, open a discussion on the Delivery tracker. We’d rather extend the platform deliberately than have you patch around a missing knob.
Release Framework is opinionated
Some defaults are non-negotiable on purpose (canonical public, security private, PSIRT approval mandatory on the security mirror, Vault always on, per-project service-account bot, locked-down protected-branch pushes). For the full list and the reasoning behind each, see An opinionated framework in the architecture doc.
Variables exist where projects legitimately need to vary behavior, things like approval rules, MR settings, common-ci-tasks toggles, share groups, and a few migration-only escape hatches. The defaults are the defaults for a reason; if you find yourself fighting them, open a discussion before working around them.
Prerequisites
Before you start:
- You can open MRs against
gitlab-com/gl-infra/infra-mgmt. - You know the canonical group path your project lives under (e.g.,
gitlab-org/software-delivery). - You’re aware that Atlantis applies the change after merge, review the plan output before applying.
Decide your path
flowchart TD
A{Does the canonical<br/>project already exist?}
A -- No --> B[New project flow]
A -- Yes --> C[Importing existing projects]
- Greenfield (nothing exists yet): New project flow.
- Anything already exists (canonical, security mirror, or both): Importing existing projects. The staged-import guidance applies whether you’re importing one resource or many.
New project flow
1. Add a YAML entry
Edit
data/projects/release-platform/repos.yaml
in infra-mgmt. The header comment at the top of that file is the
authoritative schema, covering required and optional fields, defaults, and
inline documentation for every option. Read it.
A minimal entry looks like this:
my-project:
name: "My Project"
description: "What my project does"
canonical_group: "gitlab-org/your-group"
share_groups:
delivery:
path: "gitlab-org/delivery"
access: "developer"
common_ci_tasks:
enabled: true
danger: true2. Pick the shape that matches your project
A few common cases:
Project that deploys via runway. Add runway_deployment_path. This
field is the opt-in signal that infra-mgmt reads to grant the bot
subgroup developer access on the runway deployment project, which is
what lets the security mirror’s pipelines trigger the downstream deploy
bridge:
my-service:
name: "My Service"
description: "..."
canonical_group: "gitlab-org/your-group"
runway_deployment_path: "gitlab-com/gl-infra/platform/runway/deployments/my-service"
common_ci_tasks:
enabled: true
goreleaser: truePrivate canonical with a module-managed security namespace. If your
security namespace is a group that infra-mgmt itself manages
(e.g., gitlab-org/security/auth), set both canonical_visibility and
security_namespace:
my-private-project:
name: "My Private Project"
description: "..."
canonical_group: "gitlab-org/auth"
canonical_visibility: "private"
security_namespace: "gitlab-org/security/auth"If your security namespace doesn’t exist yet and isn’t already module-managed, read the Module-managed vs pre-existing security namespaces section in the architecture doc before proceeding.
Custom Security namespace set security_namespace.
3. Open the MR
Open a merge request in infra-mgmt. Atlantis will run a plan and post the
output back to the MR.
4. Review the Atlantis plan
Look for:
- All the resources you expect to be created (canonical project, security project, bot subgroup, service account, access token, group shares, deploy token, CI/CD variables, etc.).
- No
destroyactions on anything pre-existing. - The runway-deployment share resource if you set
runway_deployment_path.
If the plan looks wrong, fix the YAML and push again. Don’t merge a plan you don’t understand.
5. Apply
Atlantis applies the change when you comment atlantis apply. Watch the apply output for
errors. After a successful apply, Atlantis will Merge.
6. Sanity-check after the apply
- The bot subgroup exists at
gitlab-com/gl-infra/release-framework-bots/<your-project-name>, with the service account user as a developer. - The push mirror on the canonical project is configured and shows as successful (canonical project -> Settings -> Repository -> Mirroring).
- The security mirror is reachable and contains your default branch.
- The security mirror has the protected CI/CD variables
CANONICAL_REGISTRY_TOKENandCANONICAL_REGISTRY_TOKEN_USERNAME. - If you set
runway_deployment_path, the bot subgroup is shared with the runway deployment project at developer access.
7. Wire your CI
It is mandatory that you scaffold your project using
common-template-copier.
Answering release_platform: true during the copier prompts generates a
.gitlab-ci.yml with release_platform_enabled: true already set on the
relevant common-ci-tasks component includes — no manual wiring needed.
Importing existing projects
The module supports importing existing projects. For the common case where
only the canonical project already exists, set import: true in your
YAML entry and the module generates the canonical project’s import block for
you (see
Importing the canonical project with import: true
below). For any other pre-existing resource (approval rules, archived
projects from prior failed applies, security mirror, and so on) you still add
Terraform import {} blocks by hand alongside your YAML entry in
environments/release-platform/projects_release_platform.tf.
Importing existing projects is something we do today, but it is fragile. The WARNING below is not theoretical, it is the lived experience that shaped this section. To contain the blast radius, prefer staging the work across two MRs:
- MR 1 - state alignment. Add the YAML entry with values that match
the current state of the project as closely as possible (existing
approval rules, share groups, MR settings, common-ci-tasks toggles)
plus the
import {}blocks. The Atlantis plan after import should show zero or near-zero diffs. This MR is about getting Terraform’s view of the world to agree with reality. - MR 2 - configuration. Once the project is cleanly under module management, edit the YAML to apply the Release Framework configuration you actually want. If this MR’s apply fails, you fail forward on configuration, not on Terraform’s import/destroy decision tree.
A single-MR import is still allowed for trivial cases (e.g., a brand-new project with default settings and no existing approval rules), but when in doubt, split it.
The order of operations below is the same as the new-project flow, with one extra step. For a staged import, run through it once per MR; the YAML diff is the only thing that changes between them.
Add your YAML entry to
data/projects/release-platform/repos.yaml. If the canonical project already exists, setimport: truein that entry (see Importing the canonical project withimport: true).Add
import {}blocks for any other pre-existing resource to the bottom ofenvironments/release-platform/projects_release_platform.tf. Real-world examples already in that file (artifact-registry, knowledge-graph, iam) are the templates to copy. Each block looks like:import { to = module.release_platform_project["my-project"].module.canonical.gitlab_project.project[0] id = "<numeric-project-id>" }With
import: trueset, the canonical project’s block is generated for you, so you do not hand-write that one. You still need a block for every other pre-existing resource the module would otherwise try to create: any pre-existing approval rules, the security mirror, archived projects from prior failed applies, and so on. Walk the module’s resource graph and the existing real-world imports to make sure you cover everything.Open the MR.
Read the Atlantis plan carefully. Imports should appear as
importactions, notcreate. There should be nodestroyactions on anything that already exists.alantis apply
Warning
If your import fails for any reason, treat it as urgent.
Once an import is in flight, the project is in a fragile state. A failed apply, whether from a state mismatch, a missing import for a resource that already exists, or unexpected drift, leaves Atlantis in a position where the next reconcile may interpret the unmanaged-but-already-existing resources as drift to reconcile. Depending on the resource, that reconciliation can include archival or destruction of the project.
Do not leave a half-imported project sitting overnight. If you need help
unwinding the failure, reach out to Delivery in #g_delivery on Slack
or ping a Delivery engineer directly, no need to open an issue first
if it’s blocking.
Importing the canonical project with import: true
When the canonical project already exists on gitlab.com, a plain apply fails
with a 400 has already been taken from the GitLab API, because the module
tries to create a project that is already there. Setting import: true in
your repos.yaml entry tells the module to import that project into Terraform
state instead of creating it:
my-project:
name: "My Project"
description: "..."
canonical_group: "gitlab-org/your-group"
# The canonical project already exists; import it rather than creating it.
import: trueThe import ID is derived as <canonical_group>/<path>, where path defaults
to the map key if you don’t set it explicitly. The field defaults to false,
so leave it out for greenfield projects.
import: true covers the canonical project only. It does not import the
security mirror, approval rules, or any other pre-existing resource; those
still need hand-written import {} blocks as described above. Because the
canonical-project import is the most common one, the flag removes the step
that was most error-prone to write by hand, but it does not remove the need to
read the Atlantis plan carefully before merging.
Runway deployment opt-in
If your project deploys via runway, wire it up in the order below. The steps
span two repos, and the runway/provisioner change lands in two MRs
with the infra-mgmt opt-in applied in between. This ordering is not
optional.
Important
Do not add extra_main_push_groups in the same runway/provisioner MR
that first provisions the deployment project. It will fail with a partial
apply — the deployment project is created, but its main branch-protection
fails with 422 Push access levels group does not have access to the project. Follow the phased sequence below; the “Why this order” note at the
end explains the underlying circular dependency.
1. Phase 1 — add your workload entry in runway/provisioner (without extra_main_push_groups)
Open an MR against
config/runtimes/gke/workloads.yml
in runway/provisioner. This registers your service and provisions the
deployment project. Leave extra_main_push_groups out for now — it is added
in Phase 2, after the bot subgroup has been shared into the project:
- runway_service_id: <service-id>
project_id: <canonical-service-project-id>
regions:
- us-east1
groups:
- <deploy-repo-access-group>project_idis your canonical service project id ongitlab.com(the project you onboarded above), not the runway deployment project id. The provisioner creates the deployment project itself.groupsmaps toshare_groupsat maintainer access on the deployment repo. Use it for a human-facing owner group (for example your team’s group); it is optional.
This apply creates the deployment project cleanly, because its main
branch-protection does not yet reference any group it cannot resolve.
2. Set runway_deployment_path in infra-mgmt
In your repos.yaml entry:
runway_deployment_path: "gitlab-com/gl-infra/platform/runway/deployments/<service>"This shares your project’s bot subgroup with the now-existing runway
deployment project at developer access, which is the permission the security
mirror’s pipeline needs to trigger the downstream Create deployment Helm chart bridge job. This apply must run after Phase 1: its
data.gitlab_project lookup requires the deployment project to already exist.
For the full picture of why this share exists and where it lives, see The service account and Identity and permissions in the architecture doc.
3. Phase 2 — add extra_main_push_groups in runway/provisioner
Open a second MR against workloads.yml adding extra_main_push_groups to
the entry from Phase 1. The bot subgroup path is always
gitlab-com/gl-infra/release-framework-bots/<project-key> where
<project-key> matches the map key you used in repos.yaml:
- runway_service_id: <service-id>
project_id: <canonical-service-project-id>
regions:
- us-east1
groups:
- <deploy-repo-access-group>
extra_main_push_groups:
- gitlab-com/gl-infra/release-framework-bots/<project-key>extra_main_push_groups adds the named group to the deployment project’s
main branch-protection allowed_to_push list. The Release Framework bot
subgroup must go here (not under groups), because triggering the
downstream Create deployment Helm chart bridge requires the bot to be in
allowed_to_push; a maintainer share alone does not satisfy that check. This
apply now succeeds because Step 2 already shared the bot subgroup into the
project.
4. Configure your .runway/ service YAML
This step lives in your project’s own (canonical) repository and is
independent of the provisioning sequence above — you can do it at any point.
Add imagePullSecrets and vaultAccessTokens to your
.runway/<service>/gke-service.yaml. This allows the deployed pod to pull
images from the private security mirror’s container registry. Without this
step, the deployment project is wired correctly but the pod cannot pull its
image.
imagePullSecrets:
- name: security-registry-reader
vaultAccessTokens:
- name: security-registry-reader
mount: ci
path: access_tokens/gitlab-com/<security-mirror-path>/read-registryReplace <security-mirror-path> with the full path of your security
mirror project on gitlab.com, using / as the separator
(e.g., gitlab-org/security/ops/artifact-registry for a project whose
security mirror lives at gitlab.com/gitlab-org/security/ops/artifact-registry).
See the
release-platform-canary example
for a complete reference.
Why this order
The provisioner sets allowed_to_push on main, but GitLab rejects any group
in allowed_to_push unless that group is already shared into the project.
That share is owned by infra-mgmt (via runway_deployment_path) and its
data.gitlab_project lookup needs the deployment project to already exist. So
the steps chain: Phase 1 creates the project, infra-mgmt then creates the
share, and Phase 2 finally references the group. This mirrors the two-MR
staging idiom already used for
Importing existing projects.
Day-2 changes
Most ongoing changes are YAML edits in repos.yaml:
- Approval rules: edit the
approval_rulesmap. - Share groups: edit
share_groups. - common-ci-tasks toggles: edit the
common_ci_tasksmap. - MR setting overrides: edit the
merge_requestsmap. - Vault paths: edit the
vaultmap.
Open an MR, review the Atlantis plan, run atlantis apply to merge.
Where to get help
- Bug, broken mirroring, broken pipelines, broken token rotation: Delivery tracker.
- Failed import (urgent): ping
#g_deliveryon Slack or contact a Delivery engineer directly. - Need a new YAML field, module variable, or platform feature: open a discussion on the Delivery tracker describing what you need and why.
- Background reading: the Architecture and Internals doc covers what each component does and why.