diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e34dd24 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,90 @@ +name: Release + +on: + pull_request: + types: [closed] + branches: [main] + +permissions: + contents: write + +jobs: + # Only run when a release/* PR is merged (not closed without merge) + check: + if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/v') + runs-on: ubuntu-latest + outputs: + version: ${{ steps.extract.outputs.version }} + steps: + - name: Extract version from branch name + id: extract + run: | + BRANCH="${{ github.event.pull_request.head.ref }}" + VERSION="${BRANCH#release/v}" + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "Releasing v${VERSION}" + + # ── 1. Build + test gate ───────────────────────────────────────────────── + verify: + needs: check + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20, 22] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - run: npm ci + - run: npm run build + - run: node --test test/*.test.mjs + + # ── 2. Tag + GitHub Release → triggers publish.yml (npm) ──────────────── + github-release: + needs: [check, verify] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Create tag and GitHub Release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ needs.check.outputs.version }} + run: | + git tag "v${VERSION}" + git push origin "v${VERSION}" + gh release create "v${VERSION}" --generate-notes --title "v${VERSION}" + + # ── 3. Publish to ClawHub ────────────────────────────────────────────── + clawhub-publish: + needs: [check, verify] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - name: Publish skill to ClawHub + env: + CLAWHUB_TOKEN: ${{ secrets.CLAWHUB_TOKEN }} + VERSION: ${{ needs.check.outputs.version }} + run: | + npx clawhub@latest auth login --token "$CLAWHUB_TOKEN" --no-browser + npx clawhub@latest publish "$(pwd)/skills/declaw" --version "$VERSION" + + # ── 4. Backmerge main → develop ──────────────────────────────────────── + backmerge: + needs: github-release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + - name: Merge main into develop + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout develop + git merge main --no-edit + git push origin develop diff --git a/AGENTS.md b/AGENTS.md index 51c7aeb..6c53870 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -128,13 +128,16 @@ git flow release finish git push origin main develop --tags ``` -### Important: Features Must Use PRs +### Important: All Changes Via PR -**Never directly merge feature branches.** Always: -1. Push feature branch to origin -2. Create PR targeting `develop` -3. Get review and merge via GitHub -4. **Close the corresponding issue** when merging (use `Fixes #N` or `Closes #N` in the PR description) +**Both `main` and `develop` are branch-protected. No direct push allowed.** + +1. Push feature/fix branch to origin +2. Create PR targeting `develop` (features/fixes) or `main` (releases/hotfixes) +3. CI must pass (`test (20)` + `test (22)`) +4. Squash merge only — one commit per PR +5. **Close the corresponding issue** when merging (use `Fixes #N` or `Closes #N` in the PR description) +6. Merged branches are auto-deleted ### Commit Convention @@ -168,9 +171,9 @@ When creating new issues: ## Release Process -### Automated Release (`scripts/release.sh`) +### Release Pipeline (Local + CI) -One command handles the full release pipeline: +One command kicks off the release via PR — CI handles the rest on merge: ```bash bash scripts/release.sh patch # 0.2.2 → 0.2.3 @@ -178,18 +181,43 @@ bash scripts/release.sh minor # 0.2.2 → 0.3.0 bash scripts/release.sh major # 0.2.2 → 1.0.0 ``` -The script performs these steps automatically: +**Local (`scripts/release.sh`):** 1. **Preflight**: verifies on `main`, clean tree, synced with remote 2. **Build + test**: `npm run build` + `node --test test/*.test.mjs` -3. **Version bump**: updates all 3 version-bearing files: - - `package.json` + `package-lock.json` (via `npm version --no-git-tag-version`) - - `openclaw.plugin.json` (plugin manifest) - - `skills/declaw/SKILL.md` (ClawHub skill frontmatter) -4. **Changelog check**: warns if `CHANGELOG.md` is missing a section for the new version -5. **Commit + tag**: `chore: release vX.Y.Z` + git tag `vX.Y.Z` -6. **Push**: main branch + tags to origin -7. **GitHub Release**: `gh release create` with auto-generated notes → triggers npm publish via CI -8. **Backmerge**: main → develop → push +3. **Version bump**: syncs all 3 version-bearing files +4. **Changelog check**: warns if `CHANGELOG.md` is missing new version section +5. **Create Release PR**: pushes `release/vX.Y.Z` branch → creates PR targeting `main` + +**CI (`.github/workflows/release.yml`, triggered when `release/v*` PR merges into `main`):** +6. **Build + test gate**: Node 20 + 22 matrix +7. **Tag + GitHub Release**: creates `vX.Y.Z` tag + auto-generated release notes → triggers npm publish +8. **ClawHub publish**: `npx clawhub@latest publish` with `CLAWHUB_TOKEN` secret +9. **Backmerge**: main → develop (via github-actions bot) + +### CI Workflows + +| Workflow | Trigger | What it does | +|---|---|---| +| `release.yml` | Release PR merged into `main` | Verify → Tag → GH Release → ClawHub → Backmerge | +| `publish.yml` | GH Release published | npm publish with `NPM_TOKEN` | +| `test.yml` | Push/PR to main/develop | Build + test (Node 20+22) | +| `auto-close-issues.yml` | PR merged | Close linked issues | +| `bootstrap-health.yml` | Scheduled | Ping all 5 bootstrap nodes | + +### Branch Protection + +Both `main` and `develop` are protected: +- **No direct push** — all changes via PR (squash merge only) +- **Required CI**: `test (20)` + `test (22)` must pass +- **No force push** or branch deletion +- **Enforced for admins** — no bypass + +### Repo Security + +- **Secret scanning + push protection**: enabled (GitHub catches leaked tokens) +- **Squash merge only**: one commit per PR, clean history +- **Auto-delete branches**: merged PR branches are cleaned up automatically +- **Required secrets**: `NPM_TOKEN` (npm), `CLAWHUB_TOKEN` (ClawHub) ### Pre-release: Update CHANGELOG @@ -198,18 +226,6 @@ Before running the release script, update `CHANGELOG.md`: - Categorize entries: `Breaking Changes`, `Added`, `Changed`, `Fixed` - Reference issues and PRs (e.g., `PR #8`, `Closes #7`) -### Post-release Checklist - -The script prints these reminders after release: -1. Verify npm: `https://www.npmjs.com/package/@resciencelab/declaw` -2. Publish skill: `npx clawhub@latest publish skills/declaw` -3. Deploy bootstrap if `bootstrap/server.mjs` changed (see below) - -### ClawHub Skill Publish -- Verify login: `npx clawhub@latest whoami` -- Publish: `npx clawhub@latest publish skills/declaw` -- Version is already synced by the release script - ### Bootstrap Node Deployment - Only needed when `bootstrap/server.mjs` or `bootstrap/package.json` changes - Deploy via AWS SSM (no SSH): diff --git a/scripts/release.sh b/scripts/release.sh index 5b93cdc..f0b2faf 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -1,11 +1,15 @@ #!/usr/bin/env bash set -euo pipefail -# DeClaw — automated release script +# DeClaw — release script (creates Release PR) # Usage: # bash scripts/release.sh patch # 0.2.2 → 0.2.3 # bash scripts/release.sh minor # 0.2.2 → 0.3.0 # bash scripts/release.sh major # 0.2.2 → 1.0.0 +# +# Flow: +# 1. Local: preflight → build+test → version bump → create Release PR +# 2. CI: PR merge triggers release.yml → tag + GH Release + npm + ClawHub + backmerge LEVEL="${1:-patch}" @@ -16,27 +20,24 @@ fi echo "=== DeClaw Release (${LEVEL}) ===" -# ── 0. Preflight checks ────────────────────────────────────────────────────── +# ── 0. Preflight ───────────────────────────────────────────────────────────── -# Must be on main BRANCH=$(git branch --show-current) if [[ "$BRANCH" != "main" ]]; then echo "Error: must be on 'main' branch (currently on '${BRANCH}')" exit 1 fi -# Working tree must be clean if [[ -n "$(git status --porcelain)" ]]; then echo "Error: working tree is not clean. Commit or stash changes first." exit 1 fi -# Remote must be up to date git fetch origin main --quiet LOCAL=$(git rev-parse main) REMOTE=$(git rev-parse origin/main) if [[ "$LOCAL" != "$REMOTE" ]]; then - echo "Error: local main ($LOCAL) differs from origin/main ($REMOTE). Pull or push first." + echo "Error: local main differs from origin/main. Pull or push first." exit 1 fi @@ -50,69 +51,63 @@ node --test test/*.test.mjs # ── 2. Version bump ────────────────────────────────────────────────────────── -# Bump package.json + package-lock.json (no git tag yet) VERSION=$(npm version "$LEVEL" --no-git-tag-version | tr -d 'v') echo "New version: ${VERSION}" -# Sync version to all version-bearing files: -# - openclaw.plugin.json (plugin manifest) -# - skills/declaw/SKILL.md (ClawHub skill frontmatter) sed -i '' "s/\"version\": \"[^\"]*\"/\"version\": \"${VERSION}\"/" openclaw.plugin.json sed -i '' "s/^version: .*/version: ${VERSION}/" skills/declaw/SKILL.md -echo "Version synced to: package.json, openclaw.plugin.json, skills/declaw/SKILL.md" +echo "Version synced: package.json, openclaw.plugin.json, skills/declaw/SKILL.md" -# ── 3. Verify CHANGELOG ────────────────────────────────────────────────────── +# ── 3. Changelog check ─────────────────────────────────────────────────────── if ! grep -q "\[${VERSION}\]" CHANGELOG.md; then echo "" echo "Warning: CHANGELOG.md does not contain a [${VERSION}] section." - echo "Please update CHANGELOG.md before releasing." - echo "" read -p "Continue without changelog entry? (y/N) " -n 1 -r echo if [[ ! "$REPLY" =~ ^[Yy]$ ]]; then echo "Aborting. Update CHANGELOG.md and re-run." - # Revert version bumps git checkout -- package.json package-lock.json openclaw.plugin.json skills/declaw/SKILL.md exit 1 fi fi -# ── 4. Commit + tag ────────────────────────────────────────────────────────── +# ── 4. Create release branch + PR ──────────────────────────────────────────── + +RELEASE_BRANCH="release/v${VERSION}" +git checkout -b "$RELEASE_BRANCH" git add -A git commit -m "chore: release v${VERSION}" -git tag "v${VERSION}" - -echo "Committed and tagged v${VERSION}" - -# ── 5. Push main + tag ─────────────────────────────────────────────────────── - -git push origin main --tags -echo "Pushed main + v${VERSION} tag" - -# ── 6. GitHub Release (triggers npm publish via CI) ────────────────────────── - -gh release create "v${VERSION}" --generate-notes --title "v${VERSION}" -echo "GitHub release created → npm publish will trigger via CI" - -# ── 7. Backmerge to develop ────────────────────────────────────────────────── - -git checkout develop -git merge main --no-edit -git push origin develop -git checkout main - -echo "Backmerged main → develop" - -# ── 8. Done ────────────────────────────────────────────────────────────────── +git push -u origin "$RELEASE_BRANCH" + +PR_URL=$(gh pr create \ + --base main \ + --head "$RELEASE_BRANCH" \ + --title "chore: release v${VERSION}" \ + --body "## Release v${VERSION} + +### Version bump +- \`package.json\` → ${VERSION} +- \`openclaw.plugin.json\` → ${VERSION} +- \`skills/declaw/SKILL.md\` → ${VERSION} + +### What happens on merge +CI (\`.github/workflows/release.yml\`) will automatically: +1. Create git tag \`v${VERSION}\` +2. Create GitHub Release (triggers npm publish) +3. Publish skill to ClawHub +4. Backmerge main → develop") echo "" -echo "=== Released v${VERSION} ===" +echo "=== Release PR created ===" +echo " ${PR_URL}" echo "" -echo "Post-release checklist:" -echo " 1. Verify npm: https://www.npmjs.com/package/@resciencelab/declaw" -echo " 2. Publish skill: npx clawhub@latest publish skills/declaw" -echo " 3. Deploy bootstrap (if server.mjs changed):" -echo " bash -c 'B64=\$(base64 -i bootstrap/server.mjs); for pair in \"i-04670f4d1a72c7d5d:us-east-2\" \"i-096ba79b9ae854339:us-west-2\" \"i-084242224f1a49b13:eu-west-1\" \"i-0b909aacd92097e43:ap-northeast-1\" \"i-0141cd0f56a902978:ap-southeast-1\"; do IID=\${pair%%:*}; REGION=\${pair##*:}; aws ssm send-command --instance-ids \$IID --region \$REGION --document-name AWS-RunShellScript --parameters \"{\\\"commands\\\":[\\\"echo '\\'\\''\${B64}'\\'\\'\" | base64 -d > /opt/declaw-bootstrap/server.mjs\\\",\\\"systemctl restart declaw-bootstrap\\\"]}\" --query Command.CommandId --output text; done'" +echo "Next steps:" +echo " 1. Wait for CI checks to pass" +echo " 2. Merge the PR (squash)" +echo " 3. CI handles: tag → GH Release → npm → ClawHub → backmerge" +echo "" + +git checkout main