Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions .github/workflows/backport-artifacts.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: Backport Deployment Artifacts

on:
push:
branches:
- deployed/testnet
- deployed/mainnet

jobs:
backport:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Check for commits to backport
id: check
run: |
BRANCH="${GITHUB_REF#refs/heads/}"

# Count commits in deployment branch that aren't in main
COMMITS_AHEAD=$(git rev-list --count main..$BRANCH)

echo "branch=$BRANCH" >> $GITHUB_OUTPUT
echo "commits_ahead=$COMMITS_AHEAD" >> $GITHUB_OUTPUT

if [ "$COMMITS_AHEAD" -gt 0 ]; then
echo "has_commits=true" >> $GITHUB_OUTPUT
echo "Found $COMMITS_AHEAD commit(s) to backport from $BRANCH to main"
else
echo "has_commits=false" >> $GITHUB_OUTPUT
echo "No commits to backport"
fi

- name: Check for existing PR
if: steps.check.outputs.has_commits == 'true'
id: existing
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
BRANCH="${{ steps.check.outputs.branch }}"

# Check if a backport PR already exists
EXISTING_PR=$(gh pr list --base main --head "$BRANCH" --state open --json number --jq '.[0].number // empty')

if [ -n "$EXISTING_PR" ]; then
echo "exists=true" >> $GITHUB_OUTPUT
echo "PR #$EXISTING_PR already exists"
else
echo "exists=false" >> $GITHUB_OUTPUT
fi

- name: Create backport PR
if: steps.check.outputs.has_commits == 'true' && steps.existing.outputs.exists == 'false'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
BRANCH="${{ steps.check.outputs.branch }}"
NETWORK="${BRANCH#deployed/}"

gh pr create \
--base main \
--head "$BRANCH" \
--title "Backport: ${NETWORK} deployment artifacts" \
--body "$(cat <<'EOF'
Automated backport of deployment artifacts from \`${{ steps.check.outputs.branch }}\`.

This PR brings deployment artifacts (e.g., updated addresses.json) back to main.

---
*Created automatically by the backport-artifacts workflow.*
EOF
)"

echo "## Backport PR Created" >> $GITHUB_STEP_SUMMARY
echo "- **From:** $BRANCH" >> $GITHUB_STEP_SUMMARY
echo "- **To:** main" >> $GITHUB_STEP_SUMMARY
echo "- **Commits:** ${{ steps.check.outputs.commits_ahead }}" >> $GITHUB_STEP_SUMMARY
55 changes: 55 additions & 0 deletions .github/workflows/deployment-tag.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Tag Deployment

on:
push:
branches:
- deployed/testnet
- deployed/mainnet

jobs:
tag-deployment:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-tags: true

- name: Extract network from branch
id: network
run: |
BRANCH="${GITHUB_REF#refs/heads/}"
NETWORK="${BRANCH#deployed/}"
echo "network=$NETWORK" >> $GITHUB_OUTPUT

- name: Generate tag name
id: tag
run: |
NETWORK="${{ steps.network.outputs.network }}"
DATE=$(date -u +%Y-%m-%d)
BASE_TAG="deploy/${NETWORK}/${DATE}"

# Check if tag already exists, add suffix if needed
COUNT=1
TAG="$BASE_TAG"

while git rev-parse "$TAG" >/dev/null 2>&1; do
COUNT=$((COUNT + 1))
TAG="${BASE_TAG}-${COUNT}"
done

echo "tag=$TAG" >> $GITHUB_OUTPUT

- name: Create and push tag
run: |
TAG="${{ steps.tag.outputs.tag }}"
NETWORK="${{ steps.network.outputs.network }}"
git config user.name "github-actions"
git config user.email "github-actions@github.com"
git tag -a "$TAG" -m "Deployment to ${NETWORK} on $(date -u +%Y-%m-%d)"
git push origin "$TAG"
echo "## Deployment Tagged" >> $GITHUB_STEP_SUMMARY
echo "- **Network:** ${NETWORK}" >> $GITHUB_STEP_SUMMARY
echo "- **Tag:** $TAG" >> $GITHUB_STEP_SUMMARY
echo "- **Commit:** ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
56 changes: 56 additions & 0 deletions .github/workflows/require-audit-label.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Require Audit Label

on:
pull_request:
branches: [main]
types: [opened, labeled, unlabeled, synchronize]

jobs:
check-label:
runs-on: ubuntu-latest
steps:
- name: Get changed files
id: changed
uses: actions/github-script@v7
with:
script: |
const { data: files } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
per_page: 100
});

// Filter for .sol files, excluding tests
const solFiles = files
.map(f => f.filename)
.filter(f => f.endsWith('.sol'))
.filter(f => !f.includes('/test/'))
.filter(f => !f.includes('/tests/'))
.filter(f => !f.endsWith('.t.sol'));

console.log('Non-test Solidity files changed:', solFiles);
core.setOutput('has_sol_files', solFiles.length > 0);
core.setOutput('sol_files', solFiles.join('\n'));

- name: Check for required label
if: steps.changed.outputs.has_sol_files == 'true'
run: |
echo "Solidity files changed (excluding tests):"
echo "${{ steps.changed.outputs.sol_files }}"
echo ""

LABELS='${{ toJson(github.event.pull_request.labels.*.name) }}'
if echo "$LABELS" | grep -q '"audited"'; then
echo "✓ PR has 'audited' label"
else
echo "::error::This PR modifies Solidity contract files and must have the 'audited' label before merging to main."
echo ""
echo "If this code has been audited, add the 'audited' label to proceed."
exit 1
fi

- name: Skip check (no contract changes)
if: steps.changed.outputs.has_sol_files == 'false'
run: |
echo "✓ No non-test Solidity files changed, skipping audit label check"
146 changes: 146 additions & 0 deletions DEPLOYMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# Deployment Strategy

This document outlines the branching and deployment strategy for Solidity contracts in this repository.

## Overview

We use a **promotion-based deployment model** where code flows from development through testnet to mainnet via pull requests. This ensures clear traceability of what code is deployed where.

```
feature/* ────────────────┐
main (deployment-ready)
▼ PR (testnet deployment)
deployed/testnet ──► tag: deploy/testnet/YYYY-MM-DD
▼ PR (mainnet deployment)
deployed/mainnet ──► tag: deploy/mainnet/YYYY-MM-DD
```

## Key Principles

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

2. **`main` is always deployable.** If code isn't ready for deployment, it stays in a feature branch. This also means code in `main` must be audited.

3. **`deployed/*` branches are append-only.** They only move forward via PRs, merging everything accumulated. This keeps history clean and ensures testnet accurately previews what will go to mainnet. Exception: emergency hotfixes.

4. **Tag every deployment.** Each merge to a deployment branch creates a tag (e.g., `deploy/mainnet/2026-04-16`) as an immutable historical record.

5. **Backport hotfixes.** If you fix something directly on a deployment branch, merge that fix back to `main` to prevent regression.

## Branches

| Branch | Purpose | Contains |
| ------------------ | --------------------------- | ------------------------------------------- |
| `feature/*` | Active development | Work-in-progress, not yet deployment-ready |
| `main` | Development head | Latest **deployment-ready** code |
| `deployed/testnet` | Testnet deployment tracking | Exactly what's deployed on Arbitrum Sepolia |
| `deployed/mainnet` | Mainnet deployment tracking | Exactly what's deployed on Arbitrum One |

### Finding deployed code

To see exactly what code is running on a network:

```bash
# What's on mainnet?
git checkout deployed/mainnet

# What's on testnet?
git checkout deployed/testnet

# What's pending deployment (in main but not yet on mainnet)?
git diff deployed/mainnet..main
```

## Tags

Each deployment automatically creates a tag for historical reference:

- `deploy/testnet/YYYY-MM-DD` — Testnet deployment snapshots
- `deploy/mainnet/YYYY-MM-DD` — Mainnet deployment snapshots

List all deployment tags:

```bash
git tag -l "deploy/*"
```

## Workflows

### Feature Development

Features are developed in feature branches and merged to `main` when complete.

```
feature/new-stuff ──PR──► main
```

### Testnet Deployment

When ready to deploy to testnet:

1. Create a PR from `main` to `deployed/testnet`
2. Deploy the contracts to Arbitrum Sepolia
3. Commit any deployment artifacts (e.g., updated `addresses.json`) to the PR
4. Review and merge the PR
5. Tag is created automatically

```
main ──PR──► deploy ──► commit artifacts ──► merge ──► tag: deploy/testnet/YYYY-MM-DD
```

### Mainnet Deployment

When ready to deploy to mainnet (typically after testnet validation and audit):

1. Create a PR from `deployed/testnet` to `deployed/mainnet`
2. Deploy the contracts to Arbitrum One
3. Commit any deployment artifacts to the PR
4. Review and merge the PR
5. Tag is created automatically

```
deployed/testnet ──PR──► deploy ──► commit artifacts ──► merge ──► tag: deploy/mainnet/YYYY-MM-DD
```

### Emergency Hotfix

For critical mainnet issues that cannot wait for the normal flow:

1. Branch from `deployed/mainnet`
2. Apply the fix
3. PR directly to `deployed/mainnet`
4. Tag and deploy
5. **Backport the fix to `main`** to prevent regression

```
deployed/mainnet ◄── hotfix/critical-fix
├──► tag: deploy/mainnet/YYYY-MM-DD
└──► PR to main (backport)
```

## Automation

### Auto-tagging

A GitHub Action (`.github/workflows/deployment-tag.yml`) automatically creates deployment tags when PRs are merged to deployment branches. No manual tagging is required.

### Audit Label Requirement

PRs to `main` that modify Solidity contract files require an `audited` label before merging (`.github/workflows/require-audit-label.yml`).

- **Applies to:** `.sol` files outside of test directories
- **Excludes:** Files in `/test/`, `/tests/`, or ending in `.t.sol`
- **Label:** `audited`

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

### Artifact Backporting

When deployment artifacts are committed to a deployment branch, a PR is automatically created to backport them to `main` (`.github/workflows/backport-artifacts.yml`).

This ensures `main` stays in sync with deployed artifacts (e.g., updated `addresses.json`) without manual backporting.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,10 @@ See [docs/Linting.md](docs/Linting.md) for detailed configuration, inline suppre

## Documentation

> Coming soon
- [Deployment Strategy](DEPLOYMENT.md) — Branching model and deployment workflow for Solidity contracts
- [Linting](docs/Linting.md) — Linting configuration and troubleshooting

For now, each package has its own README with more specific documentation you can check out.
Each package also has its own README with package-specific documentation.

## Contributing

Expand Down
Loading