Skip to content

Commit 371bea8

Browse files
committed
docs: refine deployment model to per-release branches with iteration
Revise the deployment strategy to match the realistic pattern: a release is worked on over weeks with multiple testnet iterations, audit feedback, and rebases onto an advancing main — not a one-shot "cut, deploy, merge-back" cycle. Key shifts in the model: - Branches named by release (deployment/YYYY-MM-DD/<release-name>), not just by date. The date is the branch-start date; the release name disambiguates sibling efforts. - Testnet is the iteration environment; a branch may accumulate several testnet tags as audit feedback is worked in. The audit gate is at main entry (directly or via merge-back); deployment branches may carry interim, not-yet-audited contract edits during iteration. - Mainnet is the audit-complete commitment point. Mainnet deploys only happen from a deployment branch rebased onto audited main. - Only one active deployment branch at a time; abandoning and replacing with a fresh branch is normal across a long iteration. - Mermaid flowchart in the Overview shows the two-decision loop: "ready for mainnet?" (no: iterate) and "fast-forwards onto main?" (no: rebase or fresh branch). - Cut the redundant "Cycle merge-back" subsection; consolidated on "release" terminology throughout.
1 parent 62d959b commit 371bea8

1 file changed

Lines changed: 52 additions & 51 deletions

File tree

DEPLOYMENT.md

Lines changed: 52 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,56 +4,63 @@ This document outlines the branching and deployment strategy for Solidity contra
44

55
## Overview
66

7-
We use a **per-cycle deployment branch** model: each deployment cycle creates a fresh, short-lived branch from `main`, runs testnet then mainnet from that branch, and merges back to `main` when the cycle closes. Tags on the branch capture each deploy as a self-contained snapshot — deployed code, deployment scripts, and artifacts together.
8-
9-
```
10-
feature/* ──PR──► main (audited, deployment-ready)
11-
12-
▼ branch at cycle start
13-
deployment/YYYY-MM-DD
14-
15-
├─► deploy to testnet ──► tag: deploy/testnet/YYYY-MM-DD
16-
17-
├─► deploy to mainnet ──► tag: deploy/mainnet/YYYY-MM-DD
18-
19-
└──PR──► merge back to main
7+
We use a **per-release deployment branch** model. Each release under preparation has a `deployment/YYYY-MM-DD/<release-name>` branch: testnet deploys iterate on it, it rebases onto `main` when ready, deploys to mainnet, and merges back. Tags capture each deploy as a self-contained snapshot; a branch may accumulate several testnet tags before reaching mainnet.
8+
9+
Only one deployment branch should be active at a time. During release preparation, earlier iterations may be abandoned and replaced by fresh branches.
10+
11+
```mermaid
12+
flowchart TD
13+
main["main<br/>(always audited)"]
14+
branch["deployment/YYYY-MM-DD/&lt;release-name&gt;<br/>branched from main"]
15+
testnet["deploy to testnet<br/>tag: deploy/testnet/YYYY-MM-DD/&lt;name&gt;"]
16+
ready{"code ready for<br/>mainnet?"}
17+
ff{"fast-forwards<br/>onto main?"}
18+
mainnet["deploy to mainnet<br/>tag: deploy/mainnet/YYYY-MM-DD/&lt;name&gt;"]
19+
merge["FF merge back to main<br/>delete branch"]
20+
21+
main -->|branch| branch
22+
branch --> testnet
23+
testnet --> ready
24+
ready -->|no: iterate| branch
25+
ready -->|yes| ff
26+
ff -->|no: rebase<br/>or fresh branch| branch
27+
ff -->|yes| mainnet
28+
mainnet --> merge
2029
```
2130

2231
For hotfixes, branch from the tag in production instead of from `main`:
2332

2433
```
25-
deploy/mainnet/YYYY-MM-DD (tag) ──branch──► deployment/YYYY-MM-DD-hotfix
26-
27-
├─► fix + audit
28-
├─► deploy ──► tag: deploy/mainnet/YYYY-MM-DD
29-
└──PR──► merge back to main
34+
deploy/mainnet/YYYY-MM-DD/<name> ──branch──► deployment/YYYY-MM-DD/<name>-hotfix
35+
36+
├─► fix + audit
37+
├─► deploy ──► tag: deploy/mainnet/YYYY-MM-DD/<name>-hotfix
38+
└──PR──► merge back to main
3039
```
3140

3241
## Key Principles
3342

3443
1. **Work in feature branches.** All development happens in `feature/*` branches. Merge to `main` only when the work is complete.
3544

36-
2. **`main` is always deployable and always audited.** If code isn't ready, it stays in a feature branch. PRs modifying production Solidity contracts require an `audited` label before merging.
37-
38-
3. **Deployment branches are short-lived and branched fresh.** Each cycle starts a new `deployment/YYYY-MM-DD` branch from `main`. The branch accumulates deployment script changes, artifacts, and any cycle-specific fixes, then merges back to `main` and is deleted when the cycle closes. No long-lived deployment branches exist; the presence or absence of a `deployment/*` branch is itself the signal for "is a cycle in progress?"
45+
2. **`main` is always audited; mainnet is the audit-complete gate.** PRs modifying production Solidity require the `audited` label to merge to `main`. A deployment branch may carry interim, not-yet-audited contract edits and non-contract changes (artifacts, deployment script tweaks) while testnet iteration is ongoing — that's expected. Mainnet deploys happen only from a deployment branch rebased onto audited `main`, which means every contract change reaching mainnet has passed through the `main` audit gate.
3946

40-
4. **At most one active deployment cycle at a time.** Avoid starting a new cycle while another is in flight. The exception is an emergency hotfix, which runs on its own parallel branch. Keeping to a single active cycle makes "what's being deployed next" unambiguous and avoids the merge-ordering hazards of two concurrent deployment branches diverging from the same `main`. If testnet validation of a cycle is pending, wait for it to conclude (or be abandoned) before starting the next cycle.
47+
3. **Deployment branches are dated and named by release.** Each release has a `deployment/YYYY-MM-DD/<release-name>` branch (e.g. `deployment/2026-04-19/reward-manager-and-subgraph-service`) branched from `main` at start. It may iterate for weeks, rebasing onto an advancing `main`, until it reaches mainnet (merged back, deleted) or is abandoned (deleted).
4148

42-
5. **Hotfix branches are branched from the tag they patch.** A hotfix branches from the `deploy/mainnet/YYYY-MM-DD` tag currently in production, not from `main`. This keeps the hotfix diff minimal (against running code only) and avoids shipping accumulated but undeployed work on `main`.
49+
4. **Only one active deployment branch at a time, plus any hotfix in parallel.** A superseded branch is deleted before (or as) its replacement starts; its testnet tags remain as historical record.
4350

44-
6. **Tag every deployment.** Each deploy creates an immutable `deploy/<env>/YYYY-MM-DD` tag. The tag points at the deployment branch tip at the moment of deploy, so `git checkout <tag>` reproduces the full state: source code, deployment scripts, and artifacts.
51+
5. **Hotfix branches are branched from the tag they patch.** A hotfix branches from the `deploy/mainnet/YYYY-MM-DD/<name>` tag currently in production, not from `main`. This keeps the hotfix diff minimal (against running code only) and avoids shipping accumulated but undeployed work on `main`.
4552

46-
7. **Merge back to `main` closes every cycle.** At the end of every cycle (regular or hotfix) the deployment branch is merged back to `main` via a PR. This backports artifacts, pins deployment script changes, and ensures `main` reflects the currently-deployed state.
53+
6. **Tag every deployment.** Each deploy (testnet or mainnet) creates an immutable `deploy/<env>/YYYY-MM-DD/<name>` tag, reproducing the full state at that moment: source code, deployment scripts, and artifacts. A branch typically accumulates several testnet tags across iterations; the mainnet tag is the release.
4754

48-
8. **Prefer fast-forward, especially for audited changes.** Each non-FF merge creates a tree state no reviewer read before it existed. For audited PRs this weakens the link between the audit's pinned commit hash and the bytes that end up on `main`, reducing the audit proof from trivial SHA equality to a diff-based check. Rebase audited feature branches onto current `main` before merging whenever feasible, and deploy often enough that cycle merge-backs stay small.
55+
7. **Prefer rebase and FF merge to keep deployment branches linear on `main`.** When `main` advances during a release's iteration (other merges, audit sign-offs, etc.), rebase the deployment branch onto current `main` rather than merging main into it. This preserves the "audited bytes flow in unchanged" property and keeps the eventual merge-back a fast-forward. Non-FF merges create tree states nobody read before they existed and weaken the link between audit-hash and what actually deploys.
4956

5057
## Branches
5158

52-
| Branch | Purpose | Lifetime |
53-
| ----------------------- | ------------------------------------- | ------------------------------------- |
54-
| `feature/*` | Active development | Until merged to `main` |
55-
| `main` | Audited, deployment-ready code | Permanent |
56-
| `deployment/YYYY-MM-DD` | A single deployment cycle's workspace | Short-lived; deleted after merge-back |
59+
| Branch | Purpose | Lifetime |
60+
| --------------------------------- | ---------------------------------------------------- | ---------------------------------------------------------------------------------------- |
61+
| `feature/*` | Active development | Until merged to `main` |
62+
| `main` | Audited, deployment-ready code | Permanent |
63+
| `deployment/YYYY-MM-DD/<release>` | Workspace for one release's iteration and deployment | Until mainnet (then merged back and deleted) or abandoned (deleted; testnet tags remain) |
5764

5865
## Tags
5966

@@ -92,7 +99,7 @@ Diff between last mainnet deploy and current main:
9299
git diff "$(git tag -l 'deploy/mainnet/*' | sort | tail -1)"..main
93100
```
94101

95-
Check whether a deployment cycle is in progress:
102+
Check whether a deployment branch is active:
96103

97104
```bash
98105
git branch -a --list 'deployment/*'
@@ -108,47 +115,45 @@ Features are developed in feature branches and merged to `main` when complete. P
108115
feature/new-stuff ──PR (audited)──► main
109116
```
110117

111-
### Deployment Cycle
118+
### Release Deployment
112119

113-
When ready to start a deployment:
120+
A release typically goes through several testnet iterations before reaching mainnet. The flow:
114121

115-
1. Branch `deployment/YYYY-MM-DD` from current `main` and push it. Check that there are no other deployment branches.
116-
2. Run the deployment scripts against testnet. Commit the updated artifacts (e.g. `addresses.json`) and any deployment script changes to the branch, and push. Open a PR from the branch back to `main` once this first commit lands — it stays open for the whole cycle as the review/tracking thread and becomes the merge-back PR at the end.
117-
3. Run `tag-deployment.sh --network arbitrumSepolia --name <name> ...` (see [Tagging](#tagging)) to create an annotated `deploy/testnet/YYYY-MM-DD/<name>` tag pointing at the artifact commit. Push the tag.
118-
4. After testnet validation, run the scripts against mainnet from the same branch tip. Commit updated artifacts and push.
119-
5. Run `tag-deployment.sh --network arbitrumOne --name <name> ...` to create the `deploy/mainnet/YYYY-MM-DD/<name>` tag. Push the tag.
120-
6. Review and merge the open PR back into `main`.
121-
7. Delete the deployment branch. The tags remain as the permanent record, and the absence of any `deployment/*` branch correctly signals "no cycle in progress."
122+
1. **Branch.** Branch `deployment/YYYY-MM-DD/<release-name>` from current `main` and push it.
123+
2. **Iterate on testnet.** Deploy to testnet from the branch, commit artifacts, push. Tag each deploy with `tag-deployment.sh --network arbitrumSepolia --name <name> ...` (see [Tagging](#tagging)). Open a tracking PR from the branch to `main` after the first commit lands. Take audit feedback, amend code, redeploy, tag again. There can be multiple testnet deploy tags on a release branch.
124+
3. **Keep up with `main`.** If `main` advances during iteration, rebase the deployment branch onto current `main` prior to mainnet deployment. This keeps the branch a linear extension of the audited base rather than accumulating divergent history.
125+
4. **Deploy to mainnet.** Once the code is ready and has been rebased onto `main`, deploy to mainnet. Commit artifacts, push, and tag with `tag-deployment.sh --network arbitrumOne --name <name> ...`.
126+
5. **Merge to `main`.** Fast-forward merge the PR back into `main`. Delete the branch. Tags remain as the permanent record.
122127

123-
Because both testnet and mainnet deploy from the same branch, testnet previews mainnet by construction.
128+
A release may be superseded or abandoned at any point before mainnet. Delete the branch; its testnet tags remain as historical record of what was tried. A fresh `deployment/YYYY-MM-DD/<new-release-name>` can then be started from current `main`.
124129

125130
### Emergency Hotfix
126131

127132
For critical mainnet issues:
128133

129-
1. Branch `deployment/YYYY-MM-DD-hotfix` from the current `deploy/mainnet/YYYY-MM-DD` tag and push it.
134+
1. Branch `deployment/YYYY-MM-DD/<name>-hotfix` from the current `deploy/mainnet/YYYY-MM-DD/<name>` tag and push it.
130135
2. Apply the fix. If it touches contract source, it must be audited before deploy. Commit and push; open a PR back to `main` at this point — it stays open for the duration of the hotfix as the review/tracking thread and becomes the merge-back PR.
131136
3. Run the deployment scripts against mainnet (ideally testnet first as a dry run). Commit artifacts and push.
132137
4. Run `tag-deployment.sh --network arbitrumOne --name <name> ...` to create the `deploy/mainnet/YYYY-MM-DD/<name>` tag. Push the tag.
133138
5. Review and merge the open PR back into `main`. The `audited` label applies to any contract changes in this PR.
134139
6. Delete the hotfix branch.
135-
7. If another deployment cycle is already in flight on a separate `deployment/*` branch, rebase or merge that branch onto the hotfix before its deploy — otherwise it will silently revert the fix.
140+
7. If another deployment branch is active at hotfix time, incorporate the hotfix into that branch (rebase or cherry-pick) before mainnet deployment.
136141

137142
## Audit Integrity
138143

139144
Audits certify that specific files have specific content. The operational question is always:
140145

141146
> For every file in the audit scope, do its current bytes match the audited version's bytes?
142147
143-
This scheme preserves that property by construction. Deployment branches are branched from `main` (or from a deploy tag for hotfixes) and only move forward; the audited bytes on `main` flow into the deployment branch unchanged unless a cycle-specific fix explicitly modifies them, in which case the fix is gated by the `audited` label on its merge-back PR.
148+
This scheme preserves that property by construction. Deployment branches are branched from `main` (or from a deploy tag for hotfixes) and only move forward; audited bytes on `main` flow into the deployment branch unchanged unless a release-specific fix modifies themin which case the fix passes through the `main` audit gate when it lands there (directly or via merge-back).
144149

145150
The audit scope is a transitive closure — a reviewed contract's imports are implicitly in scope even if the PR didn't touch them — and the audit reference is a pinned commit SHA, not a PR number or label. A CI check can be added to provide a mechanical floor under the cultural FF-preference: diff the audited paths between the last audit tag and `HEAD`, and either require the diff to be empty or require a fresh audit. See [Appendix A: Audit Integrity CI Check](#appendix-a-audit-integrity-ci-check) for the sketch and the design decisions it depends on.
146151

147152
## Automation
148153

149154
### Tagging
150155

151-
Tag creation is a **scripted operator step**, not a GitHub Action. Deployments are infrequent enough that full automation offers little benefit, and the tagging script can capture context a CI workflow cannot: which deploy script was actually invoked, with what flags, by whom, and which contracts changed in which address books — baked into an annotated tag body, optionally signed.
156+
Tag creation is a **scripted operator step**, run after the deploy. The script captures context a CI workflow couldn't — which deploy script ran, with what flags, by whom, which contracts changed — baked into an annotated tag body, optionally signed. Deployments are infrequent enough that full automation wouldn't pay off anyway.
152157

153158
Implementation: [`packages/deployment/scripts/tag-deployment.sh`](packages/deployment/scripts/tag-deployment.sh). It takes `--deployer`, `--network`, `--name` (recommended), and `--base`; diffs each address book (`packages/horizon/addresses.json`, `packages/subgraph-service/addresses.json`, `packages/issuance/addresses.json`) against the base ref to enumerate new / updated / removed contracts; and creates the annotated tag in the `deploy/<env>/YYYY-MM-DD/<name>` format defined above (or the bare-date fallback when no name is given). Network names map `arbitrumOne``mainnet` and `arbitrumSepolia``testnet`.
154159

@@ -169,7 +174,7 @@ Then push:
169174
git push origin <tag>
170175
```
171176

172-
The diff against `--base` is what populates the tag body's "contracts" section. The default of the previous deploy tag for the same environment should normally be correct. For an initial deploy on an environment (no prior tag exists), pass `--base` explicitly.
177+
The diff against `--base` is what populates the tag body's "contracts" section. The default of the previous deploy tag for the same environment is normally be correct. For an initial deploy on an environment (no prior tag exists), pass `--base` explicitly.
173178

174179
### Audit Label Requirement
175180

@@ -181,10 +186,6 @@ PRs to `main` modifying Solidity contract files require an `audited` label befor
181186

182187
This enforces principle #2: code in `main` must be audited.
183188

184-
### Cycle merge-back
185-
186-
Merging a deployment branch back to `main` is a standard PR. In the common case the branch only added artifacts and scripts, so the audit gate is a no-op. If the cycle included any contract changes (e.g. a hotfix), those require the `audited` label before merge-back lands.
187-
188189
## Appendix A: Audit Integrity CI Check
189190

190191
A future workflow to enforce the byte-equality property at CI level rather than relying on the cultural FF-preference. Sketched here; design decisions still to make before implementation.

0 commit comments

Comments
 (0)