From 428fc556d3a2ee96475a3f47f80e46c5a68d6602 Mon Sep 17 00:00:00 2001 From: jayteemoney Date: Thu, 5 Nov 2026 14:43:03 +0100 Subject: [PATCH 1/3] chore(ci): add staging promotion workflow and branch guidance --- .github/pull_request_template.md | 20 +++++++ .github/workflows/promotion-guard.yml | 23 ++++++++ .github/workflows/test.yml | 2 +- CONTRIBUTING.md | 81 +++++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/promotion-guard.yml create mode 100644 CONTRIBUTING.md 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..eac6c55 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,7 +2,7 @@ name: CI 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..d485b85 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,81 @@ +# 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 test + cd frontend && npx tsc --noEmit && npm run lint && npm run build + cd ../openclaw-service && npx tsc --noEmit + ``` + +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 CI green and one approving review, 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`. + +3. Get CI green and one approving review, then merge. `main` deploys to production. + +## 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 + +`.github/workflows/test.yml` runs 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 + +The `staging` and `main` branches require a pull request, green CI, one +approving review, and an up-to-date branch before merging. Force pushes and +branch deletion are blocked. From 94678d1a40f63a03e43fe72a620e049ea7caab70 Mon Sep 17 00:00:00 2001 From: jayteemoney Date: Thu, 5 Nov 2026 16:17:00 +0100 Subject: [PATCH 2/3] chore(staging): deploy testnet contracts and document staging environment --- CONTRIBUTING.md | 42 +++++++++++++++-- deployments/default.testnet-plan.yaml | 67 +++++++++++++-------------- 2 files changed, 70 insertions(+), 39 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d485b85..ab59b2e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,7 +41,7 @@ feature/fix branch -> staging -> main gh pr create --base staging --fill ``` -4. Get CI green and one approving review, then merge into `staging`. +4. Get the required checks green, then merge into `staging`. 5. Verify the change on the staging deployment (or locally from `staging`). @@ -58,7 +58,35 @@ Once `staging` is verified and fit for release: 2. The `Promotion guard` check rejects any PR into `main` whose source branch is not `staging`. -3. Get CI green and one approving review, then merge. `main` deploys to production. +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. To let testers without Vercel accounts +in, add `staging.stackstream.xyz` under **Deployment Protection Exceptions**, or +share a protected deployment link. ## Hotfixes @@ -76,6 +104,10 @@ protection, but the fix must still be merged back into `staging` afterwards. - Frontend typecheck, lint, and production build - OpenClaw service typecheck -The `staging` and `main` branches require a pull request, green CI, one -approving review, and an up-to-date branch before merging. Force pushes and -branch deletion are blocked. +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. + +If GitHub Actions is unavailable (for example, an account billing lock), the +required check is temporarily the Vercel build. Run the local checks above +before merging and restore the full required checks once Actions is healthy. 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' From 224e2ff058fd0812926c75033f9a7f074b89d810 Mon Sep 17 00:00:00 2001 From: jayteemoney Date: Fri, 6 Nov 2026 12:32:03 +0100 Subject: [PATCH 3/3] ci: run full checks through the Vercel build --- .github/workflows/test.yml | 3 +++ CONTRIBUTING.md | 38 ++++++++++++++++++++++---------------- frontend/vercel.json | 4 ++++ scripts/ci.sh | 37 +++++++++++++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 16 deletions(-) create mode 100644 frontend/vercel.json create mode 100755 scripts/ci.sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eac6c55..6cd77bb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,5 +1,8 @@ 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, staging] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ab59b2e..fb004d5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,9 +29,10 @@ feature/fix branch -> staging -> main 2. Make changes and run the checks locally: ```bash - npm test - cd frontend && npx tsc --noEmit && npm run lint && npm run build - cd ../openclaw-service && npx tsc --noEmit + 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`**: @@ -56,7 +57,7 @@ Once `staging` is verified and fit for release: ``` 2. The `Promotion guard` check rejects any PR into `main` whose source branch is - not `staging`. + not `staging` (it runs when GitHub Actions is available). 3. Get the required checks green, then merge. `main` deploys to production. @@ -84,9 +85,9 @@ 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. To let testers without Vercel accounts -in, add `staging.stackstream.xyz` under **Deployment Protection Exceptions**, or -share a protected deployment link. +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 @@ -97,17 +98,22 @@ protection, but the fix must still be merged back into `staging` afterwards. ## CI -`.github/workflows/test.yml` runs on every pull request and on pushes to -`staging` and `main`: +The checks live in `scripts/ci.sh` and run in two places: -- Contract and unit tests (`npm test`) -- Frontend typecheck, lint, and production build -- OpenClaw service typecheck +- `.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. - -If GitHub Actions is unavailable (for example, an account billing lock), the -required check is temporarily the Vercel build. Run the local checks above -before merging and restore the full required checks once Actions is healthy. 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