diff --git a/.github/workflows/backport-artifacts.yml b/.github/workflows/backport-artifacts.yml new file mode 100644 index 000000000..9ac74ce57 --- /dev/null +++ b/.github/workflows/backport-artifacts.yml @@ -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 diff --git a/.github/workflows/deployment-tag.yml b/.github/workflows/deployment-tag.yml new file mode 100644 index 000000000..24d13ff6e --- /dev/null +++ b/.github/workflows/deployment-tag.yml @@ -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 diff --git a/.github/workflows/require-audit-label.yml b/.github/workflows/require-audit-label.yml new file mode 100644 index 000000000..6b93738b5 --- /dev/null +++ b/.github/workflows/require-audit-label.yml @@ -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" diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 000000000..483d0bfda --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,161 @@ +# 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 +``` + +### Merging Audited Code + +Audits are performed on specific commits in feature branches. To preserve commit SHAs so audit reports remain valid, use **fast-forward merges** when merging audited code to `main`: + +```bash +git checkout main +git pull origin main +git merge --ff-only feature/audited-branch +git push origin main +``` + +If `main` has diverged (FF not possible), you must rebase and re-audit since rebasing changes commit SHAs. + +**Note:** GitHub's "Squash and merge" changes SHAs and breaks audit traceability. Use "Rebase and merge" (if no divergence) or merge locally with `--ff-only`. + +### 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. diff --git a/README.md b/README.md index 1879b2895..b9863ce37 100644 --- a/README.md +++ b/README.md @@ -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