diff --git a/.agents/skills/release.md b/.agents/skills/release.md index 951d7f2409..1235c41460 100644 --- a/.agents/skills/release.md +++ b/.agents/skills/release.md @@ -1,6 +1,6 @@ --- name: release -description: Guide the release process for @openhands/agent-canvas — version bump PR, E2E validation, merge with automatic tagging, and downstream npm/Docker publishing. +description: Guide the release process for @openhands/agent-canvas — version bump on the release branch, QA, then tag to publish to npm and Docker. triggers: - release - new release @@ -11,191 +11,143 @@ triggers: # Release Process for @openhands/agent-canvas -You are guiding a release of the `@openhands/agent-canvas` package. Follow these steps **in order**. Do NOT skip ahead — each step has a checkpoint where you must wait for the user. +## Overview -## Step 1: Check Current Version and Ask the User +Releases use a **long-lived release branch** model: -**IMPORTANT: You MUST complete this step and get explicit user confirmation before doing anything else.** +1. A `rel-X.Y.Z` branch is created from `main` at the start of a release cycle. +2. QA and fixes land on the branch (cherry-picked from main or landed directly). +3. When ready, a tag (`vX.Y.Z-rc.1`, `vX.Y.Z`, etc.) is pushed to the branch. +4. The tag push triggers all downstream workflows automatically. +5. **The release branch is never merged back to main.** -First, read the current version from `package.json`: +npm dist-tags by version tier: + +| Version | Example | npm dist-tag | `npm install` resolves? | +|---|---|---|---| +| Alpha | `1.0.0-alpha.1` | `alpha` | `@alpha` only | +| Beta | `1.0.0-beta.1` | `beta` | `@beta` only | +| RC | `1.0.0-rc.1` | `rc` | `@rc` only | +| Stable | `1.0.0` | `latest` | ✅ default | + +--- + +## Step 1: Confirm the Release Branch Exists + +The branch must be named `rel-X.Y.Z` (e.g. `rel-1.0.0`). Check: + +```bash +git branch -r | grep rel- +``` + +If it doesn't exist yet, create it from main: + +```bash +git checkout main && git pull origin main +git checkout -b rel- +git push -u origin rel- +``` + +**STOP HERE if the branch doesn't exist.** Ask the user to confirm the release series (e.g. `1.0.0`) before creating it. + +--- + +## Step 2: Ensure `package.json` Version Is Set + +The version in `package.json` must match the tag you're about to push. + +Check the current version: ```bash node -p "require('./package.json').version" ``` -Then present the result to the user and suggest the next logical version. Use these rules to form your suggestion: -- If the current version is a pre-release like `1.0.0-alpha.7`, suggest `1.0.0-alpha.8` (bump the last numeric segment). -- If the current version is stable like `1.2.3`, suggest `1.2.4` (patch bump) but mention they can also do `1.3.0` (minor) or `2.0.0` (major). - -**Version format**: This project uses semver with optional pre-release suffixes. -- Pre-release examples: `1.0.0-alpha.8`, `1.0.0-beta.1`, `1.0.0-rc.1` -- Stable examples: `1.0.0`, `1.1.0`, `2.0.0` - -**STOP HERE.** Tell the user the current version, your suggested next version, and ask: - -> The current version is ``. I'd suggest bumping to ``. What version would you like to release? - -**Do not proceed to Step 2 until the user confirms a version.** - -## Step 2: Create the Release PR - -### 2a. Create the release branch - -The branch **must** be named `rel-` (e.g., `rel-1.0.0-alpha.8`). This naming convention is required — the `create-release.yml` workflow detects merged release PRs by matching the `rel-` branch prefix. +If it needs updating (e.g. bumping from `1.0.0-alpha.8` to `1.0.0-rc.1`), update both `package.json` and `package-lock.json`: ```bash -git checkout main -git pull origin main -git checkout -b rel- +git checkout rel- +git pull origin rel- +npm version --no-git-tag-version +git add package.json package-lock.json +git commit -m "chore: bump version to " +git push ``` -### 2b. Bump the version - -Update the version in **both** `package.json` and `package-lock.json`: +**Also update version references in `README.md`** if the Docker image tag examples reference a specific version: ```bash -npm version --no-git-tag-version +sed -i 's/ghcr.io\/openhands\/agent-canvas:[0-9]*\.[0-9]*\.[0-9]*[^ ]*/ghcr.io\/openhands\/agent-canvas:/g' README.md +git add README.md && git commit -m "docs: update README version to " && git push ``` -This updates both files without creating a git tag (the tag is created automatically on merge). +--- -### 2c. Update version references in README.md +## Step 3: Push the Tag -The `README.md` contains Docker image tags that reference a specific version (e.g., `ghcr.io/openhands/agent-canvas:`). Update **all** version references in `README.md` to match the new release version: +Confirm the branch is in the right state (CI green, QA done), then push the tag: ```bash -# Find and replace the old version tag with the new one -sed -i 's/ghcr.io\/openhands\/agent-canvas:[0-9]*\.[0-9]*\.[0-9]*[^ ]*/ghcr.io\/openhands\/agent-canvas:/g' README.md +git checkout rel- +git pull origin rel- +git tag v +git push origin v ``` -Verify the change: +Examples: +- First release candidate: `git tag v1.0.0-rc.1 && git push origin v1.0.0-rc.1` +- Subsequent RC: `git tag v1.0.0-rc.2 && git push origin v1.0.0-rc.2` +- Full release: `git tag v1.0.0 && git push origin v1.0.0` -```bash -git diff --stat -# Should show: package.json, package-lock.json, and README.md changed -``` - -### 2d. Commit and push - -```bash -git add package.json package-lock.json README.md -git commit -m "chore: bump version to " -git push -u origin rel- -``` - -### 2e. Create the PR - -Create the PR targeting `main` with the `e2e-tests` label: - -```bash -gh pr create \ - --title "chore: bump version to " \ - --body "## Release v - -This PR bumps the version to **** for release. - -### Release Checklist -- [x] Version bumped in package.json and package-lock.json -- [x] Version references updated in README.md -- [ ] CI passes (lint, test, build) -- [ ] Visual snapshot tests pass -- [ ] Mock-LLM E2E tests pass (triggered by \`e2e-tests\` label) -- [ ] Review and approve - -### What happens on merge -When this PR is merged, the \`create-release.yml\` workflow will automatically: -1. Create a GitHub release with tag \`v\` and auto-generated notes -2. The tag push triggers \`npm-publish.yml\` to publish to npm -3. The tag push triggers \`docker.yml\` to build and push Docker images to GHCR" \ - --base main \ - --head "rel-" \ - --label "e2e-tests" -``` - -## Step 3: Wait for CI and E2E Tests - -The following checks must pass before merging: - -| Workflow | Trigger | What it checks | -|---|---|---| -| **CI** (`ci.yml`) | Every PR | Lint, unit tests, app build, library build | -| **Snapshot Tests** (`snapshot-tests.yml`) | Every PR | Visual regression screenshots | -| **Mock-LLM E2E Tests** (`mock-llm-e2e.yml`) | `e2e-tests` label | End-to-end tests with a mock LLM against a real agent-server | - -Monitor the PR checks: - -```bash -gh pr checks --watch -``` - -If the mock-LLM E2E tests fail, investigate the failure in the workflow artifacts. The `e2e-tests` label can be removed and re-added to re-trigger the workflow. - -If snapshot tests show intentional changes (e.g., version string in the UI changed), add the `update-snapshots` label to acknowledge the changes. - -## Step 4: Merge the PR - -Once all checks pass and the PR is approved, merge it: - -```bash -gh pr merge --squash --delete-branch -``` - -### Automatic tagging on merge - -The `create-release.yml` workflow automatically runs when a PR from a `rel-*` branch is merged into `main`. It will: - -1. **Extract the version** from the branch name (e.g., `rel-1.0.0-alpha.8` → `1.0.0-alpha.8`) -2. **Create a GitHub release** with tag `v` targeting the merge commit -3. **Auto-generate release notes** from the commits since the previous release -4. **Mark pre-release versions** (those containing a hyphen) as pre-releases - -You do **not** need to manually create a tag or GitHub release. - -### Downstream workflows triggered by the tag - -The tag push (`v*`) automatically triggers: +**The tag push is the release trigger.** Three workflows fire in parallel: | Workflow | What it does | |---|---| -| **npm-publish.yml** | Builds and publishes `@openhands/agent-canvas` to npm with provenance | -| **docker.yml** | Builds and pushes multi-arch Docker images to `ghcr.io/openhands/agent-canvas` | +| `create-release.yml` | Creates the GitHub Release object with auto-generated notes | +| `npm-publish.yml` | Builds and publishes to npm with the correct dist-tag | +| `docker.yml` | Builds and pushes multi-arch Docker images to GHCR | -### Verify the release +--- -After merging, verify the downstream workflows complete successfully: +## Step 4: Verify the Release ```bash -# Check the GitHub release was created +# GitHub release gh release view v -# Watch downstream workflow runs -gh run list --workflow=npm-publish.yml --limit=1 -gh run list --workflow=docker.yml --limit=1 +# npm (allow ~2 min for publish to propagate) +npm view @openhands/agent-canvas@ +npm view @openhands/agent-canvas dist-tags # confirm correct dist-tag + +# Docker +docker pull ghcr.io/openhands/agent-canvas: ``` -Confirm the package is available: -- **npm**: `npm view @openhands/agent-canvas@` -- **Docker**: `docker pull ghcr.io/openhands/agent-canvas:` +Monitor workflow runs: + +```bash +gh run list --workflow=npm-publish.yml --limit=3 +gh run list --workflow=docker.yml --limit=3 +``` + +--- ## Troubleshooting -### E2E tests not triggering -The `e2e-tests` label must be present on the PR. If you added it but tests didn't run, remove and re-add the label, or manually trigger the workflow from the Actions tab. - -### Tag already exists -If a tag `v` already exists (e.g., from a previous failed attempt), the `create-release.yml` workflow will skip creation. Delete the existing release and tag first: +### package.json version doesn't match the tag +`npm-publish.yml` validates that `package.json` version equals the tag version and fails if they differ. Fix the version on the branch, push, then delete and re-push the tag: ```bash -gh release delete v --yes -git push origin :refs/tags/v +git push origin :refs/tags/v # delete remote tag +git tag -d v # delete local tag +# fix package.json, commit, push +git tag v && git push origin v ``` -### npm publish failed -The `npm-publish.yml` workflow validates that `package.json` version matches the tag version. If they don't match, the publish will fail. Ensure the version bump in Step 2b matches the branch name exactly. +### GitHub release already exists +`create-release.yml` skips silently if the release already exists. To recreate it: +```bash +gh release delete v --yes +``` +Then the workflow will re-create it on the next tag push (or run it manually from the Actions tab). -## Reference - -This release process is modeled after the [OpenHands/software-agent-sdk release workflow](https://github.com/OpenHands/software-agent-sdk/blob/main/.github/workflows/README-RELEASE.md) for consistency across OpenHands projects. Key differences: -- agent-canvas uses npm (not PyPI) for package publishing -- agent-canvas uses `npm version` (not `make set-package-version`) for version bumping -- agent-canvas release branches use `rel-` naming (same as SDK) -- Both repos use `create-release.yml` to auto-create GitHub releases on merge of `rel-*` PRs +### npm publish failed mid-way +Check the `npm-publish.yml` run logs. The dist-tag is resolved from the `package.json` version — if the version string contains `alpha`, `beta`, or `rc`, it uses the matching dist-tag; otherwise `latest`. diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index c8707e5d89..45656a1166 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -1,54 +1,46 @@ --- name: Create GitHub Release -# Automatically create a GitHub release when a release PR is merged into main. -# This bridges the gap between merging the release PR and the downstream -# workflows (npm-publish, docker) which trigger on tag push (v*). -# -# Modeled after OpenHands/software-agent-sdk's create-release.yml for consistency. +# Automatically create a GitHub release object when a v* tag is pushed. +# The tag is pushed manually to a rel-X.Y.Z release branch (never merged to main). +# This workflow creates the GitHub Release; the same tag push also triggers +# npm-publish.yml and docker.yml in parallel. on: - pull_request: - types: [closed] - branches: [main] + push: + tags: + - 'v*' jobs: create-release: - # Only run when a release PR is merged (not just closed) - # and the branch follows the rel-X.Y.Z naming convention. - if: > - github.event.pull_request.merged == true && - startsWith(github.event.pull_request.head.ref, 'rel-') runs-on: ubuntu-24.04 permissions: contents: write - pull-requests: read steps: - - name: Extract version from branch name + - name: Extract version from tag id: version - env: - PR_HEAD_REF: ${{ github.event.pull_request.head.ref }} run: | - BRANCH="$PR_HEAD_REF" - VERSION="${BRANCH#rel-}" + TAG="${GITHUB_REF#refs/tags/}" + VERSION="${TAG#v}" - # Accept semver with optional pre-release suffix (e.g. 1.0.0, 1.0.0-alpha.8) + # Accept semver with optional pre-release suffix (e.g. 1.0.0, 1.0.0-rc.1) if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then - echo "❌ Could not extract valid version from branch: $BRANCH" + echo "❌ Could not extract valid version from tag: $TAG" exit 1 fi echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "tag=$TAG" >> "$GITHUB_OUTPUT" echo "📦 Version: $VERSION" - name: Check release does not already exist id: check env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VERSION: ${{ steps.version.outputs.version }} + TAG: ${{ steps.version.outputs.tag }} run: | - if gh release view "v${VERSION}" --repo "${{ github.repository }}" > /dev/null 2>&1; then - echo "⚠️ Release v${VERSION} already exists, skipping" + if gh release view "$TAG" --repo "${{ github.repository }}" > /dev/null 2>&1; then + echo "⚠️ Release $TAG already exists, skipping" echo "exists=true" >> "$GITHUB_OUTPUT" else echo "exists=false" >> "$GITHUB_OUTPUT" @@ -69,12 +61,9 @@ jobs: - name: Create GitHub Release if: steps.check.outputs.exists == 'false' env: - # Use a PAT (not GITHUB_TOKEN) so the resulting tag-push event - # propagates to downstream workflows (npm-publish.yml, docker.yml). - # Tags created via GITHUB_TOKEN are silently suppressed by GitHub - # and will not trigger other workflow runs. - GH_TOKEN: ${{ secrets.OPENHANDS_BOT_GITHUB_PAT_PUBLIC || secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} VERSION: ${{ steps.version.outputs.version }} + TAG: ${{ steps.version.outputs.tag }} PREV_TAG: ${{ steps.prev_tag.outputs.prev_tag }} run: | NOTES_START_FLAG=() @@ -82,34 +71,32 @@ jobs: NOTES_START_FLAG=(--notes-start-tag "$PREV_TAG") fi - # Detect pre-release versions (anything with a hyphen, e.g. 1.0.0-alpha.8) + # Detect pre-release versions (anything with a hyphen, e.g. 1.0.0-rc.1) PRERELEASE_FLAG=() if [[ "$VERSION" == *-* ]]; then PRERELEASE_FLAG=(--prerelease) fi - gh release create "v${VERSION}" \ + gh release create "$TAG" \ --repo "${{ github.repository }}" \ - --target "${{ github.event.pull_request.merge_commit_sha }}" \ - --title "v${VERSION}" \ + --title "$TAG" \ --generate-notes \ "${NOTES_START_FLAG[@]}" \ "${PRERELEASE_FLAG[@]}" - echo "✅ Release v${VERSION} created!" - echo "🔗 https://github.com/${{ github.repository }}/releases/tag/v${VERSION}" + echo "✅ Release $TAG created!" + echo "🔗 https://github.com/${{ github.repository }}/releases/tag/$TAG" - name: Summary if: steps.check.outputs.exists == 'false' env: - VERSION: ${{ steps.version.outputs.version }} + TAG: ${{ steps.version.outputs.tag }} run: | - echo "## ✅ Release v${VERSION} Created" >> "$GITHUB_STEP_SUMMARY" + echo "## ✅ Release $TAG Created" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" - echo "- **Tag**: v${VERSION}" >> "$GITHUB_STEP_SUMMARY" - echo "- **Release**: https://github.com/${{ github.repository }}/releases/tag/v${VERSION}" >> "$GITHUB_STEP_SUMMARY" + echo "- **Tag**: $TAG" >> "$GITHUB_STEP_SUMMARY" + echo "- **Release**: https://github.com/${{ github.repository }}/releases/tag/$TAG" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" - echo "### Triggered Downstream Workflows" >> "$GITHUB_STEP_SUMMARY" - echo "The tag push \`v${VERSION}\` will automatically trigger:" >> "$GITHUB_STEP_SUMMARY" + echo "Downstream workflows triggered by the same tag push:" >> "$GITHUB_STEP_SUMMARY" echo "- **npm-publish.yml** — publish \`@openhands/agent-canvas\` to npm" >> "$GITHUB_STEP_SUMMARY" echo "- **docker.yml** — build and push \`ghcr.io/openhands/agent-canvas\` Docker images" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 134fa51b86..bae15a48a7 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -90,18 +90,29 @@ jobs: fi echo "✓ Version $PACKAGE_VERSION matches release tag" - # Temporary: publish all versions (including prerelease) as "latest" - # until the first stable release (see cleanup issue #395). - # Note: OIDC trusted-publishing tokens cover only the `npm publish` - # call itself; a separate `npm dist-tag add` after publish fails - # with E401, so we publish directly with --tag latest in one step. - # - # Named prerelease dist-tags (alpha/beta/rc) are intentionally NOT - # published under this policy — only "latest" is used. This means - # `npm install @openhands/agent-canvas@alpha` will not resolve to - # any published version. When prerelease dist-tags need to be - # re-introduced (issue #395 cleanup), update this step to publish - # with a versioned dist-tag (e.g. --tag alpha) and only tag stable - # releases as "latest". + # Resolve the npm dist-tag from the version's pre-release identifier: + # alpha → --tag alpha (e.g. 1.0.0-alpha.1) + # beta → --tag beta (e.g. 1.0.0-beta.1) + # rc → --tag rc (e.g. 1.0.0-rc.1) + # stable → --tag latest (e.g. 1.0.0) + # Note: OIDC trusted-publishing tokens cover only the `npm publish` call + # itself; a separate `npm dist-tag add` would fail with E401, so the tag + # is resolved and passed directly in one step. + - name: Resolve npm dist-tag + id: dist_tag + run: | + VERSION=$(node -p "require('./package.json').version") + if [[ "$VERSION" == *-alpha* ]]; then + DIST_TAG="alpha" + elif [[ "$VERSION" == *-beta* ]]; then + DIST_TAG="beta" + elif [[ "$VERSION" == *-rc* ]]; then + DIST_TAG="rc" + else + DIST_TAG="latest" + fi + echo "dist_tag=$DIST_TAG" >> "$GITHUB_OUTPUT" + echo "📦 Version $VERSION → dist-tag: $DIST_TAG" + - name: Publish to npm with provenance - run: npm publish --access public --provenance --tag latest + run: npm publish --access public --provenance --tag ${{ steps.dist_tag.outputs.dist_tag }} diff --git a/AGENTS.md b/AGENTS.md index 52100f1daa..fcded59e28 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -552,6 +552,6 @@ When adding code that needs a new string, decide up front which rule it falls un - Spec files live under `specs/`. Spec IDs are stable — never renumber. Mark deprecated specs with ~~strikethrough~~. Tag implementation code and tests with `// @spec BM-002 — Short title` comments so specs are grep-able across the codebase (`grep -rn '@spec BM-' src/ __tests__/`). Place the comment on the line immediately above the relevant code block or test. When multiple tests cover the same spec, use `it.each` if the test structure is identical. -- Release automation: `.github/workflows/create-release.yml` automatically creates a GitHub release (with tag `v`) when a PR from a `rel-*` branch is merged into `main`. This is modeled after the SDK's `create-release.yml`. The tag push then triggers `npm-publish.yml` and `docker.yml` downstream. The release skill (`.agents/skills/release.md`, keyword trigger: `release`) guides agents through the full process: version bump PR, `e2e-tests` label for mock-LLM E2E validation, merge with auto-tagging. Pre-release versions (containing a hyphen) are marked as pre-releases on the GitHub release. +- Release automation: releases use a **long-lived release branch** model — a `rel-X.Y.Z` branch is created from `main`, QA/fixes land there, and publishing is triggered by pushing a `v*` tag directly to that branch (the branch is never merged back to main). Tag format determines the npm dist-tag: `alpha` → `alpha`, `beta` → `beta`, `rc` → `rc`, no suffix → `latest`. Three workflows fire in parallel on every `v*` tag push: `create-release.yml` (creates the GitHub Release object with auto-generated notes and marks pre-release for hyphenated versions), `npm-publish.yml` (builds and publishes to npm with the correct dist-tag), and `docker.yml` (builds multi-arch Docker images). The release skill (`.agents/skills/release.md`, keyword trigger: `release`) guides agents through the full process. - Cloud conversation resume gating: when a cloud conversation is closed from the UI (`pauseCloudSandbox` is called), the conversation's `conversation_url` is NOT cleared -- it still points to the old sandbox host. `WebSocketProviderWrapper` must suppress the URL (pass `null` to `ConversationWebSocketProvider`) while `sandbox_status === "PAUSED"`, otherwise the WebSocket immediately tries the stale URL before the sandbox wakes. Symmetrically, `useActiveConversation`'s refetch interval must fast-poll (3 s) on both `!conversation_url` AND `sandbox_status === "PAUSED"` -- checking only the missing URL would leave the hook on the 30 s interval while the sandbox is resuming. The resume sequence: navigate -> sandbox PAUSED detected -> `resumeCloudSandbox` called (in `conversation.tsx`) -> fast-poll detects RUNNING -> `conversationUrl` unblocked -> WebSocket connects.