diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..0e5b4e5 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,20 @@ +## What changed + + + +## Target branch + +- [ ] This PR targets `staging` (normal feature, fix, docs, or chore work) +- [ ] This PR targets `main` and comes from `staging` (production promotion) + +PRs into `main` are rejected unless the source branch is `staging`. + +## Verification + +- [ ] CI is green +- [ ] Change was exercised on the staging deployment +- [ ] No secrets or credentials are included + +## Notes for reviewers + + diff --git a/.github/workflows/promotion-guard.yml b/.github/workflows/promotion-guard.yml new file mode 100644 index 0000000..f4bd454 --- /dev/null +++ b/.github/workflows/promotion-guard.yml @@ -0,0 +1,23 @@ +name: Promotion guard + +on: + pull_request: + branches: [main] + +permissions: + contents: read + pull-requests: read + +jobs: + source-branch: + name: PRs to main must come from staging + runs-on: ubuntu-latest + steps: + - name: Verify source branch + if: github.head_ref != 'staging' + run: | + echo "::error::Changes reach main only by promoting the staging branch. Open this PR against staging instead." + exit 1 + + - name: Source branch allowed + run: echo "Source branch is staging." diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 41a4a02..6cd77bb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,8 +1,11 @@ name: CI +# Keep these commands in sync with scripts/ci.sh, which Vercel runs as part of +# the required Vercel check while GitHub Actions is unavailable. + on: push: - branches: [main] + branches: [main, staging] pull_request: # A newer push to the same branch makes the older run pointless. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..fb004d5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,119 @@ +# Contributing + +## Branch model + +We use three layers of branches: + +``` +feature/fix branch -> staging -> main + (local work) (QA/verify) (production) +``` + +| Branch | Purpose | Direct pushes | Merges into it | +| --- | --- | --- | --- | +| Work branches (`feat/...`, `fix/...`, `chore/...`, `docs/...`) | Day-to-day development | Allowed by the author | — | +| `staging` | Integration and testing ground | Blocked | Work branches via PR | +| `main` | Production | Blocked | `staging` only, via PR | + +`main` is production. Nothing is merged into `main` until it has run on `staging`. + +## Day-to-day flow + +1. Create a work branch off the latest `staging` (not `main`): + + ```bash + git fetch origin + git switch -c feat/my-change origin/staging + ``` + +2. Make changes and run the checks locally: + + ```bash + npm ci + npm ci --prefix frontend + npm ci --prefix openclaw-service + ./scripts/ci.sh all + ``` + +3. Push the branch and open a pull request **targeting `staging`**: + + ```bash + git push -u origin feat/my-change + gh pr create --base staging --fill + ``` + +4. Get the required checks green, then merge into `staging`. + +5. Verify the change on the staging deployment (or locally from `staging`). + +## Promoting to production + +Once `staging` is verified and fit for release: + +1. Open a promotion pull request from `staging` into `main`: + + ```bash + gh pr create --base main --head staging --title "Promote staging to production" --fill + ``` + +2. The `Promotion guard` check rejects any PR into `main` whose source branch is + not `staging` (it runs when GitHub Actions is available). + +3. Get the required checks green, then merge. `main` deploys to production. + +## Staging environment + +`staging` is deployed to and runs against +Stacks **testnet**, not mainnet. Production (`main`) runs against mainnet. + +The testnet configuration is stored as branch-scoped Vercel environment +variables for the `staging` branch: + +- `NEXT_PUBLIC_NETWORK=testnet` +- `NEXT_PUBLIC_CONTRACT_DEPLOYER=ST36MRYTKD8X92Q6386RA9QV62A14NJMM9D9DYM3V` +- `NEXT_PUBLIC_CHAIN_ID=2147483648` +- `NEXT_PUBLIC_RPC_URL=https://api.testnet.hiro.so` + +The testnet contracts are deployed with: + +```bash +clarinet deployments generate --testnet --medium-cost +clarinet deployments apply --testnet -d --no-dashboard +``` + +The deployer mnemonic lives in the gitignored `settings/Testnet.toml`; never +commit it. Testnet STX comes from the +[Hiro faucet](https://explorer.hiro.so/sandbox/faucet?chain=testnet). + +Staging is behind Vercel Authentication and is intentionally SSO-only while the +project is maintained by a single person. External testers can be added later +through **Deployment Protection Exceptions** when the team grows. + +## Hotfixes + +Hotfixes follow the same path: branch off `staging`, open a PR into `staging`, +verify, then promote `staging` to `main`. If production is broken and an +expedited release is unavoidable, an administrator may temporarily relax branch +protection, but the fix must still be merged back into `staging` afterwards. + +## CI + +The checks live in `scripts/ci.sh` and run in two places: + +- `.github/workflows/test.yml` runs them as three jobs on every pull request and + on pushes to `staging` and `main`: + - Contract and unit tests (`npm test`) + - Frontend typecheck, lint, and production build + - OpenClaw service typecheck +- Vercel runs `scripts/ci.sh all` as part of every deployment through + `frontend/vercel.json`, so the required `Vercel` check covers the same three + groups. + +While GitHub Actions is unavailable (for example, an account billing lock), +branch protection requires only the `Vercel` check. Once Actions is healthy, +restore the three job checks on both branches and the `Promotion guard` check on +`main`. + +The `staging` and `main` branches require a pull request, the required status +checks, and an up-to-date branch before merging. Force pushes and branch +deletion are blocked. diff --git a/deployments/default.testnet-plan.yaml b/deployments/default.testnet-plan.yaml index b58e58d..5d14ca3 100644 --- a/deployments/default.testnet-plan.yaml +++ b/deployments/default.testnet-plan.yaml @@ -1,39 +1,38 @@ ---- id: 0 name: Testnet deployment network: testnet -stacks-node: "https://api.testnet.hiro.so" -bitcoin-node: "http://blockstack:blockstacksystem@bitcoind.testnet.stacks.co:18332" +stacks-node: https://api.testnet.hiro.so +bitcoin-node: http://blockstack:blockstacksystem@bitcoind.testnet.stacks.co:18332 plan: batches: - - id: 0 - transactions: - - contract-publish: - contract-name: sip-010-trait - expected-sender: ST2343MJFA65A4FD5N5ZDP00ETDG2GYJ4D2FBNXE0 - cost: 10260 - path: contracts/traits/sip-010-trait.clar - anchor-block-only: true - clarity-version: 3 - - contract-publish: - contract-name: mock-sip010-token - expected-sender: ST2343MJFA65A4FD5N5ZDP00ETDG2GYJ4D2FBNXE0 - cost: 26500 - path: contracts/mocks/mock-sip010-token.clar - anchor-block-only: true - clarity-version: 3 - - contract-publish: - contract-name: stream-manager - expected-sender: ST2343MJFA65A4FD5N5ZDP00ETDG2GYJ4D2FBNXE0 - cost: 311340 - path: contracts/stream-manager.clar - anchor-block-only: true - clarity-version: 3 - - contract-publish: - contract-name: stream-factory - expected-sender: ST2343MJFA65A4FD5N5ZDP00ETDG2GYJ4D2FBNXE0 - cost: 60810 - path: contracts/stream-factory.clar - anchor-block-only: true - clarity-version: 3 - epoch: "3.0" + - id: 0 + transactions: + - transaction-type: contract-publish + contract-name: sip-010-trait + expected-sender: ST36MRYTKD8X92Q6386RA9QV62A14NJMM9D9DYM3V + cost: 140750 + path: contracts/traits/sip-010-trait.clar + anchor-block-only: true + clarity-version: 3 + - transaction-type: contract-publish + contract-name: mock-sip010-token + expected-sender: ST36MRYTKD8X92Q6386RA9QV62A14NJMM9D9DYM3V + cost: 141101 + path: contracts/mocks/mock-sip010-token.clar + anchor-block-only: true + clarity-version: 3 + - transaction-type: contract-publish + contract-name: stream-manager + expected-sender: ST36MRYTKD8X92Q6386RA9QV62A14NJMM9D9DYM3V + cost: 147156 + path: contracts/stream-manager.clar + anchor-block-only: true + clarity-version: 3 + - transaction-type: contract-publish + contract-name: stream-factory + expected-sender: ST36MRYTKD8X92Q6386RA9QV62A14NJMM9D9DYM3V + cost: 142067 + path: contracts/stream-factory.clar + anchor-block-only: true + clarity-version: 3 + epoch: '3.0' diff --git a/frontend/vercel.json b/frontend/vercel.json new file mode 100644 index 0000000..d614c46 --- /dev/null +++ b/frontend/vercel.json @@ -0,0 +1,4 @@ +{ + "installCommand": "cd .. && npm ci --include=dev --prefer-offline --no-audit --no-fund && npm ci --prefix frontend --include=dev --prefer-offline --no-audit --no-fund && npm ci --prefix openclaw-service --include=dev --prefer-offline --no-audit --no-fund", + "buildCommand": "cd .. && ./scripts/ci.sh all" +} diff --git a/scripts/ci.sh b/scripts/ci.sh new file mode 100755 index 0000000..785d6e1 --- /dev/null +++ b/scripts/ci.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +run_test() { + cd "$ROOT" + NODE_ENV=test npm test +} + +run_frontend() { + cd "$ROOT/frontend" + npx tsc --noEmit + npm run lint + npm run build +} + +run_openclaw() { + cd "$ROOT/openclaw-service" + npx tsc --noEmit +} + +case "${1:-all}" in + all) + run_test + run_frontend + run_openclaw + cd "$ROOT/frontend" + ;; + test) run_test ;; + frontend) run_frontend ;; + openclaw) run_openclaw ;; + *) + echo "usage: $0 [all|test|frontend|openclaw]" >&2 + exit 1 + ;; +esac