Document version v2026100501 (quote this version when discussing or citing) · 中文版. For engineers in every department and for SRE. The Release Contract & Branching Template says what; this document says how. Sections marked contract describe what SRE does with a delivery and apply to everyone; sections marked template describe a recommended app-repo pipeline that teams may adopt or replace.
The delivery is one tag and its full commit hash per repo. SRE checks that they match, then deploys exactly that.
The artifact is built from the delivered tag and identified by its commit. Gray and production run the same artifact.
At build time CI has no environment's settings, so the image must not contain domains, API addresses or toggle values.
SRE approves deployment requests in GitLab; K8s is then synced by GitOps and VMs by a deployment job, both invisible to the requester.
git rev-parse <tag>^{commit}). A mismatch is not deployed until the Release Manager confirms a corrected pair.The environment repo is the git repo that records which version each environment runs and with which settings (currently fleet-delivery). The GitOps tool (Argo CD) syncs each environment to what the repo says. It covers services on K8s today; VM services will be added later. A deployment request is an MR on it: SRE's approval is the deployment decision, and the merge is the deployment.
| Environment repo path | Content | Approved by | After approval |
|---|---|---|---|
workloads/<svc>/<self-verification env>/ | Rendered K8s manifests | Auto-merged | GitOps sync |
workloads/<svc>/gray/ | Same, version = delivered tag (gray, real traffic) | SRE (CODEOWNERS) | GitOps sync |
workloads/<svc>/production-<cluster>/ | Same, version = the tag that passed gray | SRE (CODEOWNERS) | GitOps sync |
workloads/<svc>/production-vm/ | version.yaml (tag) and config files | SRE | A deployment job on an on-prem runner runs git checkout <tag> on the VM and restarts it |
| Same path, settings only | Feature toggles, environment variables; artifact unchanged, marked config-only | SRE, on the Release Manager's request | Same; services that read env get a rollout restart |
| Rollback | SRE, in reverse release-plan order, deploys each repo's previous version. K8s services: set the production version back (the Revert button on the deployment MR, or a new MR back to the previous tag); VM services: for now the existing deployment scripts, to be moved into the environment repo later. Can become a manual job rollback-release that takes the release plan and rolls back repo by repo in reverse order | Announced and executed by SRE | Each repo back to its previous version, one by one |
Argo CD syncs automatically and has no concept of approval; most departments are not on K8s either. Approval in a GitLab MR puts K8s and VMs through the same gate.
SRE always sees "the production version of some service changes from A to B". Moving a service from a VM to K8s changes the content of the same directory; the process stays the same.
The environment repo's production directories are not yet connected to the production clusters; the ApplicationSet's target cluster must be derived from the directory; main needs CODEOWNERS and code owner approval; the tag ↔ hash check and rollback-release jobs are not built yet. SRE completes these.
A shared CI/CD component that fits the branching template; each repo only provides its test command, Dockerfile and service name. Three paths: ordinary MR / push, rc tag, and the release tag after the merge into master.
the stack's test command
deploy to a self-verification environment
content == rc in production
rc is on release/* or hotfix/*
build once, record the digest
open the deployment request: tag, hash, digest
== master tip, content == rc in production
add alias X.Y.Z to the same image
add digest and MR list to the Release
delete release and merged feat branches
| Job | When it runs | What it does |
|---|---|---|
test | MR, branch push | The stack's test command (section 5). |
deploy-dev | Manual | Builds an image of the current commit and deploys it to a self-verification environment chosen by the developer. |
check-merge | MR → master | Guard, see section 4. Pure git, no API calls. |
tag-verify | First step of the tag pipeline | Guard, see section 4. If it fails nothing after it runs. |
build | rc tag | Builds the image, registry tag = commit, digest passed on. The only build for gray / production. |
request-deploy | rc tag | Opens (or updates) the deployment request on the environment repo with tag, full commit hash and digest; the Release Manager adds the release plan and revert method. The rest is SRE's (section 2). |
retag | Release tag | crane tag / skopeo copy adds the alias X.Y.Z to the image of the rc in production; nothing is redeployed. |
release-notes | Release tag | Updates the GitLab Release: digest, the matching rc, the Milestone's MR list. |
cleanup | Release tag | Deletes release/X.Y.Z (or hotfix/*) and the merged feat/* branches. |
Two jobs that keep the template's promises without relying on memory: "production runs the tag that was delivered" and "master only takes verified code". They run in the team's repo and protect the team; SRE's own check is the tag ↔ hash comparison in section 2.
| Guard | Conditions | When it fails |
|---|---|---|
check-mergeMR → master | ① The source is release/* or hotfix/*; ② the source's latest commit is exactly an rc tag (the one in production, with no commits after it); ③ master is an ancestor of that commit. | ①: a branch that should not enter master. ②: something was pushed after production; deliver a new rc. ③: something else reached master meanwhile; merge master in first and create a new rc. |
tag-verifytag pipeline | rc tag: the commit is on release/* or hotfix/*. Release tag: the commit is the tip of master, its file tree is identical to some rc (git diff --quiet rc final), and that rc is its ancestor. | The tag was created in the wrong place. Delete and recreate it before delivery; nothing was deployed. |
Whatever the pipeline, four things differ per stack: how to test, how to build, what the artifact is, and how settings get in. "Settings injected at deploy time" is part of the contract (build once); the rest is guidance.
| Next.js | Java Spring Boot | Laravel | |
|---|---|---|---|
test | npm ci && npm test, npm run lint, tsc --noEmit | ./mvnw -B verify or ./gradlew test | composer install, vendor/bin/phpunit |
build | multi-stage: npm ci → next build (output: 'standalone') → runtime image carries only .next/standalone | multi-stage: mvnw package → JRE image with the jar; or buildpacks / jib | multi-stage: composer install --no-dev, config:cache → php-fpm + nginx image |
| Artifact | Container image; shared components published as npm packages | Container image; shared components published as jars | Container image; shared components published as composer packages |
| Settings injection | Server side reads env at runtime. Client side must not put environment values in NEXT_PUBLIC_* (baked into the bundle at build time): use same-origin relative paths, a server-provided /api/config, or an env.js generated at container start | Runtime env → application.yaml placeholders, or a ConfigMap mounted as application-prod.yaml | Runtime env → config/*.php (after config:cache only config is read) |
| Feature toggles | Server side reads env, or the client fetches /api/config; the OpenFeature JS SDK can unify both | Read via Environment at runtime, or Togglz / OpenFeature; @ConditionalOnProperty is evaluated only at startup | config('features.x') from env; with a package, point its driver at env, not the DB |
| Restart on setting change? | Reading env: yes; reading a mounted file: no | Yes (default); with Spring Cloud Kubernetes reload: no | Yes (config:cache) |
| Health | Custom /api/health | Actuator /actuator/health/readiness | Laravel 11 /up |
| DB migration | Usually none; Prisma / Drizzle as on the right | Flyway / Liquibase, expand / contract | artisan migrate in a Job / init container; migrations are not rolled back |
| Version observability | Image label org.opencontainers.image.revision=<commit>; the app exposes /version returning the commit, so SRE can confirm in gray and production that the delivered hash is running. | ||
NEXT_PUBLIC_*) are treated as code, not environment settings: they follow the commit and do not differ by environment.| Setting | Value |
|---|---|
| Protected branches | master, release/*: push = No one, merge = Maintainers. |
| Merge method | Merge commit. Move work between branches by merging whole branches through MRs, not by cherry-pick. |
| Squash | Project setting "Allow", unchecked by default (GitLab sets this per project, not per target branch). Use it for feat/*, hotfix/* → release/* only; release/* → master with squash is blocked by check-merge / tag-verify. A squashed feat branch is not recognised by git as merged, so cleanup relies on "delete source branch after merge". |
| Protected tags | v*: only Maintainers (the Release Manager) can create. |
| Push rules | Branch names ^(feat|hotfix|release)/; commit message contains a ticket number #\d+; author email @fmt.com.tw (existing). |
| MR | Delete source branch after merge; pipeline must succeed; master and release/* as target need approval. |
| Milestone / Release | One vX.Y.Z per version; MRs are attached to the Milestone; Releases are created by the Release Manager, with notes generated automatically. |
| Registry | Immutable tags; the retention policy only cleans commit tags, never version tags. |
| Runner | Jobs that call the GitLab API use the company's group runners (tags:); pure-git checks use GIT_FETCH_EXTRA_FLAGS. |
| Setting | Value |
|---|---|
| Protected branch | main: push = No one; merge = Maintainers + each department's bot; Code owner approval required. |
| CODEOWNERS | /workloads/*/production*/, /workloads/*/gray/ → the sre group. CODEOWNERS only; no extra approval rule over all of main. |
| Merge method | Merge commit (the Revert button rolls back a whole deployment). |
| Bot | One service account per department (Developer + allowed to merge main), token in the department's group CI variable; changes are submitted with git commit + push, committer <svc>-gitops-bot@fmt.com.tw. |
| Deploy freeze | GitLab deploy freeze; during a freeze requests can still be opened but are not approved. |
Only the delivery: one tag + full commit hash per repo by D-1, an artifact built from that commit with settings injected at deploy time, and the deployment request (section 2). How you produce it is up to you.
The top-level group has an IP allowlist; gitlab.com SaaS runners are not on it. Point the job at the company's group runners (tags:). Pure-git checks need no API; use GIT_FETCH_EXTRA_FLAGS so the runner fetches the refs itself.
The push rule requires an @fmt.com.tw author email and the bot account's email is not. Use git commit + push with committer <svc>-gitops-bot@fmt.com.tw.
New projects default to ci_pipeline_variables_minimum_override_role = no_one_allowed. Set it to maintainer when needed, or use pipeline inputs.
When GitLab auto-protects the default branch it gives push to Maintainers. Change it to No one after creating the repo.
It blocks git push --delete; a Maintainer can still delete it from the UI / API. "Tags are immutable" is enforced by process, not by this setting.
The commits /revert API does not accept merge commits. Use the Revert button on the MR page, or git revert -m 1.
An MR opened by a code owner counts as satisfying the code owner rule, which keeps rollbacks fast. If two people must be required, add a separate approval rule.