From c5564c5f7f6b9d80dfe6832a0da858a1f062fce9 Mon Sep 17 00:00:00 2001 From: "E.Gen" Date: Fri, 9 Oct 2026 19:42:31 +0900 Subject: [PATCH 1/3] Stop upstream-only workflows in the fork --- .github/workflows/README-RELEASE.md | 257 +++++----- .github/workflows/cancel-eval.yml | 65 --- .github/workflows/create-release.yml | 209 -------- .github/workflows/deploy-docs.yml | 25 - .github/workflows/integration-runner.yml | 26 +- .github/workflows/issue-duplicate-checker.yml | 95 ---- .github/workflows/issue-readiness-check.yml | 135 ------ .github/workflows/pr-artifacts.yml | 247 ---------- .github/workflows/pr-description-check.yml | 21 +- .github/workflows/prepare-release.yml | 175 ------- .github/workflows/pypi-release.yml | 57 +-- .../remove-duplicate-candidate-label.yml | 31 -- .github/workflows/run-eval.yml | 69 +-- .github/workflows/stale.yml | 34 -- .github/workflows/todo-management.yml | 331 ------------- ...escript-client-github-packages-publish.yml | 31 +- .../typescript-client-npm-publish.yml | 29 +- .github/workflows/version-bump-prs.yml | 456 ------------------ AGENTS.md | 16 +- CONTRIBUTING.md | 5 +- clients/typescript/AGENTS.md | 21 +- tests/cross/test_fork_workflow_triggers.py | 87 ++++ 22 files changed, 359 insertions(+), 2063 deletions(-) delete mode 100644 .github/workflows/cancel-eval.yml delete mode 100644 .github/workflows/create-release.yml delete mode 100644 .github/workflows/deploy-docs.yml delete mode 100644 .github/workflows/issue-duplicate-checker.yml delete mode 100644 .github/workflows/issue-readiness-check.yml delete mode 100644 .github/workflows/pr-artifacts.yml delete mode 100644 .github/workflows/prepare-release.yml delete mode 100644 .github/workflows/remove-duplicate-candidate-label.yml delete mode 100644 .github/workflows/stale.yml delete mode 100644 .github/workflows/todo-management.yml delete mode 100644 .github/workflows/version-bump-prs.yml create mode 100644 tests/cross/test_fork_workflow_triggers.py diff --git a/.github/workflows/README-RELEASE.md b/.github/workflows/README-RELEASE.md index 359dcf6b4a..aac889b247 100644 --- a/.github/workflows/README-RELEASE.md +++ b/.github/workflows/README-RELEASE.md @@ -1,67 +1,148 @@ -# Release Automation Workflows +# Release and CI Workflows (geeen-jp fork) -This document describes the automated release workflows for the OpenHands Software Agent SDK. +This document describes which GitHub Actions workflows run in the independent +geeen-jp fork of the OpenHands Software Agent SDK, which are stopped, and how a +person starts the manual ones. sdk#84 removed or disabled every workflow that +operates the upstream `OpenHands/software-agent-sdk` project (Cloud automation, +upstream docs, upstream downstream-release bumps, registry publishing). -## Overview +## Fork workflow inventory (sdk#84) -The release process has been automated with three GitHub Actions workflows: +"Auto" means a `push`, `pull_request`, `issues`, `issue_comment`, `schedule` or +`release` event starts the workflow. No workflow in the "removed" or "manual" +groups starts automatically. -1. **prepare-release.yml** - Prepares a release PR with version updates -2. **pypi-release.yml** - Automatically publishes packages to PyPI when a release is created -3. **release-binaries.yml** - Builds and smoke-tests the linux x86_64 agent-server binary - on releases and main pushes; release runs also attach binaries to the release - (no Docker; see Step 4c) +### Removed (upstream-only; the file is deleted) -## How to Create a New Release - -### Step 1: Trigger the Prepare Release Workflow - -1. Go to the [Actions tab](https://github.com/OpenHands/software-agent-sdk/actions) -2. Select **"Prepare Release"** workflow from the left sidebar -3. Click **"Run workflow"** button -4. Enter the version number (e.g., `1.2.3`) - must be in format `X.Y.Z` -5. Click **"Run workflow"** - -The workflow will automatically: -- ✅ Create a new branch named `rel-X.Y.Z` -- ✅ Update all package versions using `make set-package-version` -- ✅ Commit the changes -- ✅ Push the branch -- ✅ Create a PR with labels `integration-tests` and `test-examples` - -### Step 2: Review the PR - -The created PR will include a checklist. Complete the following: - -- [ ] Fix any deprecation deadlines if they exist -- [ ] Verify integration tests pass (triggered by `integration-tests` label) -- [ ] Verify example checks pass (triggered by `test-examples` label) -- [ ] Confirm any merged `release-note-required` PRs are accurately called out in the final release notes -- [ ] Review and approve the PR - -### Step 3: Create the GitHub Release - -1. Go to [Releases](https://github.com/OpenHands/software-agent-sdk/releases/new) -2. Click **"Draft a new release"** -3. Configure the release: - - **Tag**: `vX.Y.Z` (must match the version) - - **Branch**: `rel-X.Y.Z` (the branch created by the workflow) - - **Previous tag**: Select the previous release version -4. Click **"Generate release notes"** to auto-generate the changelog -5. Review and edit the release notes as needed -6. Click **"Publish release"** +| Workflow | Former trigger | Why removed | +|---|---|---| +| `deploy-docs.yml` | push to main (agent-server paths), dispatch | deploys upstream docs | +| `cancel-eval.yml` | dispatch | cancels upstream OpenHands/evaluation runs | +| `prepare-release.yml` | dispatch | opens `rel-X.Y.Z` PRs for the upstream release train | +| `version-bump-prs.yml` | dispatch (from `pypi-release.yml`) | opens version-bump PRs in OpenHands-CLI, automation and this repository | +| `create-release.yml` | merged `rel-*` PR (`contents: write`, `actions: write`) | creates the upstream GitHub release and dispatches publish workflows | +| `stale.yml` | daily cron | closes upstream stale issues/PRs | +| `issue-duplicate-checker.yml` | `issues: opened`, daily cron, dispatch | OpenHands Cloud duplicate check, auto-closes issues | +| `remove-duplicate-candidate-label.yml` | `issue_comment` | upstream label hygiene | +| `todo-management.yml` | dispatch, PR label `automatic-todo` | OpenHands Cloud TODO automation | +| `issue-readiness-check.yml` | `issues` events (`issues: write`, `actions: write`) | auto-labels `ready-for-dev`; no label policy is established for this fork, so unknown automatic issue writes fail closed | +| `pr-artifacts.yml` | `pull_request_target`, `pull_request_review` (`pull-requests: write`, `contents: write`) | auto-commits removal of `.pr/` and comments on PRs; the `.pr/` directory is now removed by hand before merge | + +### Manual only (dispatch with a confirmation input) + +| Workflow | How to run | Guard | +|---|---|---| +| `pypi-release.yml` | Actions > "Publish all OpenHands packages (uv)" > Run workflow, set `confirm` to `publish-pypi` | job `if: inputs.confirm == 'publish-pypi'`; fails when secret `PYPI_TOKEN_OPENHANDS` is missing; no token fallback; `contents: read` only | +| `typescript-client-npm-publish.yml` | Run workflow with `version` and `confirm` = `publish-npm` | job `if: inputs.confirm == 'publish-npm'`; npm trusted publishing (OIDC) only | +| `typescript-client-github-packages-publish.yml` | Run workflow with `version` and `confirm` = `publish-github-packages` | job `if: inputs.confirm == 'publish-github-packages'`; uses `GITHUB_TOKEN` | +| `run-eval.yml` | Run workflow with `reason` and `eval_limit`; or add `run-eval-1/50/200/500` to a same-repository PR | same-repository PRs only; no `release` trigger; needs secret `OPENHANDS_BOT_GITHUB_PAT_EVAL_DISPATCH` (fails when missing); it dispatches `OpenHands/evaluation`, so only run it when that is intended; permissions `contents: read`, `pull-requests: write` | +| `integration-runner.yml` | Run workflow, or add `integration-test` / `behavior-test` to a same-repository PR | fork PRs never run; `id-token` removed | +| `run-examples.yml` | Run workflow, or add `test-examples` to a PR (unchanged) | label or dispatch only | +| `security-scan.yml` | Run workflow, or add `security-scan` to a PR, or any label on a `rel-*` PR (unchanged) | plain `pull_request` (read-only token on forks) | -### Step 4: PyPI Publication (Automated) +### Kept (automatic, local rules) -Once the release is published, the **pypi-release.yml** workflow will automatically: -- ✅ Build all packages (openhands-sdk, openhands-tools, openhands-workspace, openhands-agent-server) -- ✅ Publish them to PyPI +| Workflow | Trigger | Permissions | +|---|---|---| +| `validate.yml` (job `validate`) | `pull_request` | read | +| `tests.yml`, `precommit.yml`, `check-docstrings.yml`, `deprecation-check.yml`, `check-documented-examples.yml`, `check-duplicate-examples.yml` | push / pull_request | read | +| `api-breakage.yml`, `agent-server-rest-api-breakage.yml`, `persisted-settings-compat.yml` | push / pull_request | unchanged | +| `typescript-client-ci.yml`, `typescript-client-integration-tests.yml`, `typescript-client-endpoint-audit.yml` | push / pull_request | unchanged | +| `server.yml` | binary and OpenAPI jobs on push/PR; Docker jobs on dispatch only (sdk#83) | unchanged | +| `release-binaries.yml` | push to main, `release: published`, dispatch | unchanged (linux binaries and `openapi.json`, not a registry publish) | +| `version-bump-guard.yml` | `pull_request` to main | `contents: read` | +| `review-thread-gate.yml` | `pull_request` to main | `contents: read`, `pull-requests: read` | +| `pr-description-check.yml` | `pull_request` | `contents: read`, `pull-requests: read` (was `pull_request_target`; `issues: read` and the `GITHUB_TOKEN` env were dropped) | + +`oh-update-documentation.yml.back` is not a workflow (GitHub ignores the +`.back` suffix) and is left as is. + +### Acceptance decisions (sdk#84) + +- Removed upstream-only workflows leave no `push`, `pull_request`, `issues`, + `issue_comment`, `schedule` or `release` trigger behind; `release-binaries.yml` + keeps its `release` trigger because it only builds and attaches linux binaries + and is covered by sdk#83. `tests/cross/test_fork_workflow_triggers.py` fails if + a removed workflow file returns or if any other workflow gains a `release`, + `schedule`, `issues` or `issue_comment` trigger. +- Publishing is not adopted by default. The three publish workflows keep their + steps (a future publish is not forbidden) but only start from + `workflow_dispatch`, and the job is skipped unless the `confirm` input equals + the exact word listed above. There is no fallback publish path when a secret + is missing, and fork PRs have no way to start them. +- `pypi-release.yml` no longer dispatches `version-bump-prs.yml` after + publishing, and its `actions: write` permission was removed. +- The `pr-description-check.yml` linked-issue `ready-for-dev` label lookup is + not enforced: the workflow passes no `GITHUB_TOKEN`, so + `check_pr_description.py` still requires a linked issue reference and the PR + template sections but skips the label lookup. This is because + `issue-readiness-check.yml` was removed and nothing applies the label. + `.github/scripts/check_issue_readiness.py` and `post-readiness-comment.mjs` + are left in place unused (non-goal: no source changes). +- `pr-artifacts.yml` was removed instead of reduced to a read-only check: its + only valuable part was the automatic `.pr/` cleanup, which needs write + access. Delete `.pr/` yourself before merging (see AGENTS.md "PR_ARTIFACTS"). +- `run-eval.yml`, `integration-runner.yml`, `run-examples.yml` and + `security-scan.yml` stay as the evaluation path. They start only from + dispatch, a label on a same-repository PR, or (security-scan) a `rel-*` PR; + an unlabeled PR starts none of them. Unchanged and still correct: + `run-examples.yml` and `security-scan.yml`. +- Out of scope and kept as is: `validate`, Python/REST/OpenAPI compatibility + checks, and the TypeScript CI / integration / endpoint-audit workflows. + +### Required checks 移行 (sdk#84) + +Main branch protection is unconfirmed (403 with the available token, rulesets +API returns `[]`). The operator must check Settings > Branches in the GitHub UI +and remove any of the following that is required; nothing was changed in +repository settings. -You can monitor the progress in the [Actions tab](https://github.com/OpenHands/software-agent-sdk/actions/workflows/pypi-release.yml). +| Check / job | Workflow | Change | +|---|---|---| +| `dispatch` | `deploy-docs.yml` | deleted | +| `cancel-eval` | `cancel-eval.yml` | deleted | +| `prepare-release` | `prepare-release.yml` | deleted | +| `create-version-bump-prs`, `bump-typescript-client` | `version-bump-prs.yml` | deleted | +| `create-release` | `create-release.yml` | deleted | +| `stale` | `stale.yml` | deleted | +| `smoke-clone`, `issue-duplicate-check`, `auto-close-duplicates` | `issue-duplicate-checker.yml` | deleted | +| `remove-duplicate-candidate` | `remove-duplicate-candidate-label.yml` | deleted | +| `scan-todos`, `process-todos`, `summary` | `todo-management.yml` | deleted | +| `check`, `Refresh PR gates for linked issues` | `issue-readiness-check.yml` | deleted | +| `cleanup-on-approval`, `check-pr-artifacts`, `cleanup-after-merge` | `pr-artifacts.yml` | deleted | +| `publish` | `pypi-release.yml`, `typescript-client-*-publish.yml` | skipped unless dispatched with `confirm` | +| `print-parameters`, `build-and-evaluate` | `run-eval.yml` | skipped on fork PRs and on release | +| `Validate PR description` | `pr-description-check.yml` | kept, trigger is now `pull_request` | + +Unchanged and required to stay: `validate`, `build-binary-and-test (ubuntu-latest)`, +`check-openapi-schema`, `Check package versions`, the review-thread gate and the +TypeScript client checks. + +### Re-syncing with upstream and restoring a workflow + +When merging upstream `OpenHands/software-agent-sdk`, expect modify/delete +conflicts for every removed file and content conflicts in the manual workflows. +Do not take the upstream side blindly: upstream re-adds `release`, `schedule` +and `issues` triggers, the `OpenHands/software-agent-sdk` repository gate and +the write permissions removed here. After a merge run +`pytest tests/cross/test_fork_workflow_triggers.py`; it names any workflow that +came back. + +To restore a deleted workflow deliberately, take it from the last commit that +had it (`git show c62f7a493:.github/workflows/.yml > .github/workflows/.yml`), +replace the `OpenHands/software-agent-sdk` repository gate with this +repository, remove triggers and write permissions you do not need, update +`tests/cross/test_fork_workflow_triggers.py` and this document, then confirm +the required checks in the GitHub UI. + +## Release binaries and Agent Server images + +`release-binaries.yml` and `server.yml` are kept (sdk#82, sdk#83); the sections +below describe them. ### Step 4b: Release Binaries (Automated) -In parallel with the PyPI workflow, **release-binaries.yml** also fires on `release: published`. +**release-binaries.yml** fires on `release: published` (a release created by hand). It also runs on every push to `main` as ongoing smoke coverage. It: - ✅ Builds the agent-server PyInstaller binary on `ubuntu-24.04` (linux x86_64 @@ -132,10 +213,8 @@ that was published with `publish=true`. - Credentials: only `secrets.GITHUB_TOKEN` through `docker/login-action`; it is never echoed. Auth failures, tag conflicts, pull failures, an unreachable `/health` and cancellations show as failed or cancelled jobs. -- Not changed: `version-bump-prs.yml` `bump-typescript-client` still waits up to - 45 minutes for `ghcr.io/openhands/agent-server:-python`; it runs only - in `OpenHands/software-agent-sdk` (never in this fork), so it is out of scope. - A release there needs a `server.yml` dispatch with `publish=true` first. +- `version-bump-prs.yml` (which waited for `ghcr.io/openhands/agent-server:-python`) + was removed in sdk#84. #### Build time / runner expectations @@ -150,36 +229,18 @@ No QEMU is needed: every job runs on an `ubuntu-24.04` (x86_64) runner and runs `linux/amd64` images natively. The Docker jobs in `server.yml` set up Docker Buildx for `docker/build-push-action` and `docker buildx imagetools`. -### Step 5: Version Bump PRs (Automated) - -After successful PyPI publication, the workflow will automatically create PRs to update SDK versions in downstream repositories: - -- **[OpenHands-CLI](https://github.com/OpenHands/openhands-cli)** - Updates `openhands-sdk` and `openhands-tools` versions -- **[automation](https://github.com/OpenHands/automation)** - Updates `openhands-sdk` and `openhands-workspace` versions. Opened with a `fix:` title so the repo's release-please cuts a patch release, publishing an `openhands-automation` build pinned to this SDK (which the agent-canvas `sdk-version-sync` check requires). -- **TypeScript client (`clients/typescript`)** - Opens a PR in this repository after both the exact GHCR image and release `openapi.json` are available, updates `config.agentServerImage`, regenerates the checked-in transport types, and includes an API-change summary. - -These PRs will: -- Be created automatically with branch name `bump-sdk-X.Y.Z` (`bump-agent-server-X.Y.Z` for typescript-client) -- Include links back to the SDK release -- Include generated Agent Server contract changes for the exact released - version rather than only changing the image tag -- Need to be reviewed and merged by maintainers - -### Step 6: Post-Release Tasks - -- [ ] Merge the release PR to main -- [ ] Review and merge the auto-created version bump PRs in OpenHands-CLI, automation, and the TypeScript client (merging the automation PR triggers its release-please release PR; merge that too to publish the pinned `openhands-automation`) -- [ ] Announce the release - ## Manual PyPI Release (If Needed) -If you need to manually trigger the PyPI release workflow: +Publishing is not adopted by default (sdk#84). To publish deliberately: + +1. Go to the Actions tab of this repository. +2. Select **"Publish all OpenHands packages (uv)"** and click **"Run workflow"**. +3. Select the branch/tag to publish from and type `publish-pypi` in `confirm`. +4. Click **"Run workflow"**. Any other `confirm` value skips the job. -1. Go to the [Actions tab](https://github.com/OpenHands/software-agent-sdk/actions) -2. Select **"Publish all OpenHands packages (uv)"** workflow -3. Click **"Run workflow"** -4. Select the branch/tag you want to publish from -5. Click **"Run workflow"** +The secret `PYPI_TOKEN_OPENHANDS` must exist; the job fails when it is missing. +The npm and GitHub Packages workflows work the same way with `publish-npm` and +`publish-github-packages` plus a `version` input. ## CI scope and required checks @@ -211,8 +272,7 @@ checks. In `release-binaries.yml`, `resolve-tag`, `build-binary`, ## Workflow Files -- `.github/workflows/prepare-release.yml` - Automated release preparation -- `.github/workflows/pypi-release.yml` - PyPI package publication +- `.github/workflows/pypi-release.yml` - PyPI package publication (manual, confirmation required) - `.github/workflows/release-binaries.yml` - Linux x86_64 binary and OpenAPI publishing on releases and main pushes - `.github/workflows/server.yml` - binary/OpenAPI checks on push and PR; on @@ -220,17 +280,6 @@ checks. In `release-binaries.yml`, `resolve-tag`, `build-binary`, ## Troubleshooting -### Version Format Error - -If you get a version format error, ensure you're using the format `X.Y.Z` (e.g., `1.2.3`), not `vX.Y.Z`. - -### PR Creation Failed - -If the PR creation fails, check: -- The branch doesn't already exist -- You have proper permissions -- The `GITHUB_TOKEN` has sufficient permissions - ### PyPI Publication Failed If PyPI publication fails: @@ -257,17 +306,3 @@ If a `server.yml` dispatch fails: the dispatch. - **`/health` never responded**: open the failing job; the cleanup trap dumps the last 100 lines of `docker logs` for the container. - -## Previous Manual Process - -For reference, the previous manual release checklist was: - -- [ ] Checkout SDK repo, use `make set-package-version version=x.x.x` to set the version -- [ ] Push to a branch like `rel-x.x.x` and start a PR -- [ ] Fix any "deprecation deadlines" if they exist -- [ ] Tag "integration-tests" and make sure integration test all pass -- [ ] Tag "test-examples" and make sure example checks all pass -- [ ] Draft a new release -- [ ] Use workflow to publish to PyPI on tag `v1.X.X` - -Most of these steps are now automated! diff --git a/.github/workflows/cancel-eval.yml b/.github/workflows/cancel-eval.yml deleted file mode 100644 index d9ec0dc87e..0000000000 --- a/.github/workflows/cancel-eval.yml +++ /dev/null @@ -1,65 +0,0 @@ ---- -name: Cancel Eval - -run-name: Cancel Eval (${{ inputs.run_id }}) - -on: - workflow_dispatch: - inputs: - run_id: - description: Workflow run ID to cancel - required: true - type: string - reason: - description: Reason for cancellation - required: false - type: string - -env: - EVAL_REPO: OpenHands/evaluation - EVAL_WORKFLOW: kill-eval-job.yml - -permissions: - contents: read - -jobs: - cancel-eval: - # Only run in the main repository, not in forks - if: github.repository == 'OpenHands/software-agent-sdk' - runs-on: ubuntu-latest - steps: - - name: Cancel evaluation job - env: - DISPATCH_TOKEN: ${{ secrets.OPENHANDS_BOT_GITHUB_PAT_EVAL_DISPATCH }} - RUN_ID: ${{ github.event.inputs.run_id }} - REASON: ${{ github.event.inputs.reason }} - run: |- - set -euo pipefail - - if [ -z "$DISPATCH_TOKEN" ]; then - echo "Missing dispatch token" >&2 - exit 1 - fi - - echo "Canceling evaluation workflow run: $RUN_ID" - - # Dispatch kill workflow in evaluation repo - PAYLOAD=$(jq -n \ - --arg ref "main" \ - --arg run_id "$RUN_ID" \ - --arg reason "$REASON" \ - '{ref: $ref, inputs: {run_id: $run_id, reason: $reason}}') - - RESPONSE=$(curl -sS -o /tmp/dispatch.out -w "%{http_code}" -X POST \ - -H "Authorization: token $DISPATCH_TOKEN" \ - -H "Accept: application/vnd.github+json" \ - -d "$PAYLOAD" \ - "https://api.github.com/repos/${EVAL_REPO}/actions/workflows/${EVAL_WORKFLOW}/dispatches") - - if [ "$RESPONSE" != "204" ]; then - echo "Dispatch failed (status $RESPONSE):" >&2 - cat /tmp/dispatch.out >&2 - exit 1 - fi - - echo "Cancellation dispatched successfully for run: $RUN_ID" diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml deleted file mode 100644 index 72631fc344..0000000000 --- a/.github/workflows/create-release.yml +++ /dev/null @@ -1,209 +0,0 @@ ---- -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 pypi-release -# workflow (which triggers on release published). - -on: - pull_request: - types: [closed] - branches: [main] - -jobs: - create-release: - # Only run when a release PR is merged (not just closed) - if: > - github.event.pull_request.merged == true && - startsWith(github.event.pull_request.head.ref, 'rel-') - runs-on: ubuntu-24.04 - permissions: - actions: write - contents: write - pull-requests: read - steps: - - name: Extract version from branch name - id: version - env: - PR_HEAD_REF: ${{ github.event.pull_request.head.ref }} - run: | - BRANCH="$PR_HEAD_REF" - VERSION="${BRANCH#rel-}" - - if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "❌ Could not extract valid version from branch: $BRANCH" - exit 1 - fi - - echo "version=$VERSION" >> "$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 }} - run: | - if gh release view "v${VERSION}" --repo "${{ github.repository }}" > /dev/null 2>&1; then - echo "⚠️ Release v${VERSION} already exists, skipping" - echo "exists=true" >> "$GITHUB_OUTPUT" - else - echo "exists=false" >> "$GITHUB_OUTPUT" - fi - - - name: Find previous release tag - if: steps.check.outputs.exists == 'false' - id: prev_tag - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - PREV_TAG=$(gh release list --repo "${{ github.repository }}" \ - --exclude-drafts --exclude-pre-releases --limit 1 \ - --json tagName --jq '.[0].tagName') - echo "prev_tag=${PREV_TAG}" >> "$GITHUB_OUTPUT" - echo "📌 Previous release tag: ${PREV_TAG:-}" - - - name: Build release-note-required preamble - if: steps.check.outputs.exists == 'false' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PREAMBLE_PATH: release-note-preamble.md - PREV_TAG: ${{ steps.prev_tag.outputs.prev_tag }} - run: | - python3 <<'PY' - import json - import os - import subprocess - from pathlib import Path - - repo = os.environ['GITHUB_REPOSITORY'] - prev_tag = os.environ.get('PREV_TAG', '').strip() - preamble_path = Path(os.environ['PREAMBLE_PATH']) - - args = [ - 'gh', - 'search', - 'prs', - '--repo', - repo, - '--state', - 'closed', - '--merged', - '--label', - 'release-note-required', - '--limit', - '200', - '--json', - 'number,title,url,author', - ] - if prev_tag: - published_at = subprocess.check_output( - [ - 'gh', - 'release', - 'view', - prev_tag, - '--repo', - repo, - '--json', - 'publishedAt', - '--jq', - '.publishedAt', - ], - text=True, - ).strip() - if published_at: - args.extend(['--merged-at', f'>={published_at}']) - - prs = json.loads(subprocess.check_output(args, text=True) or '[]') - prs.sort(key=lambda pr: pr['number']) - - if not prs: - preamble_path.write_text('') - raise SystemExit(0) - - lines = [ - '## Behavioral compatibility notes', - '', - 'The following merged PRs were labeled `release-note-required` and are called out explicitly in this release:', - '', - ] - for pr in prs: - author = (pr.get('author') or {}).get('login') - attribution = f' (@{author})' if author else '' - lines.append( - f"- [#{pr['number']}]({pr['url']}) {pr['title']}{attribution}" - ) - lines.append('') - preamble_path.write_text('\n'.join(lines)) - PY - - - - name: Create GitHub Release - if: steps.check.outputs.exists == 'false' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PREAMBLE_PATH: release-note-preamble.md - VERSION: ${{ steps.version.outputs.version }} - PREV_TAG: ${{ steps.prev_tag.outputs.prev_tag }} - run: | - NOTES_START_FLAG=() - if [ -n "$PREV_TAG" ]; then - NOTES_START_FLAG=(--notes-start-tag "$PREV_TAG") - fi - - PREAMBLE_FLAG=() - if [ -s "$PREAMBLE_PATH" ]; then - PREAMBLE_FLAG=(--notes "$(cat "$PREAMBLE_PATH")") - fi - - gh release create "v${VERSION}" \ - --repo "${{ github.repository }}" \ - --target "${{ github.event.pull_request.merge_commit_sha }}" \ - --title "v${VERSION}" \ - --generate-notes \ - "${PREAMBLE_FLAG[@]}" \ - "${NOTES_START_FLAG[@]}" - - echo "✅ Release v${VERSION} created!" - echo "🔗 https://github.com/${{ github.repository }}/releases/tag/v${VERSION}" - - - name: Dispatch PyPI release workflow - if: steps.check.outputs.exists == 'false' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VERSION: ${{ steps.version.outputs.version }} - run: | - gh workflow run pypi-release.yml \ - --repo "${{ github.repository }}" \ - --ref "v${VERSION}" - - echo "🚀 Dispatched pypi-release.yml for v${VERSION}" - - - name: Dispatch release binaries workflow - # Same GITHUB_TOKEN limitation applies to release-binaries.yml - # which triggers on release:published events. - if: steps.check.outputs.exists == 'false' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VERSION: ${{ steps.version.outputs.version }} - run: | - gh workflow run release-binaries.yml \ - --repo "${{ github.repository }}" \ - --ref "v${VERSION}" \ - -f release_tag="v${VERSION}" - - echo "📦 Dispatched release-binaries.yml for v${VERSION}" - - - name: Summary - env: - VERSION: ${{ steps.version.outputs.version }} - run: | - echo "## ✅ Release v${VERSION} 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 "" >> "$GITHUB_STEP_SUMMARY" - echo "The \`pypi-release.yml\` workflow was dispatched to publish packages to PyPI." >> "$GITHUB_STEP_SUMMARY" - echo "No Docker image is built automatically; dispatch \`server.yml\` (publish=true) from the \`v${VERSION}\` tag to publish one." >> "$GITHUB_STEP_SUMMARY" - echo "The \`release-binaries.yml\` workflow was dispatched to build and attach release binaries." >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml deleted file mode 100644 index 574af5155b..0000000000 --- a/.github/workflows/deploy-docs.yml +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: Dispatch to docs repo - -on: - push: - branches: - - main - paths: - - openhands-agent-server/** - workflow_dispatch: -jobs: - dispatch: - # Only run in the main repository, not in forks - if: github.repository == 'OpenHands/software-agent-sdk' - runs-on: ubuntu-24.04 - permissions: - contents: write - steps: - - name: Trigger docs repo sync - uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4 - with: - token: ${{ secrets.OPENHANDS_BOT_GITHUB_PAT_PUBLIC }} - repository: OpenHands/docs - event-type: update - client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}"}' diff --git a/.github/workflows/integration-runner.yml b/.github/workflows/integration-runner.yml index 009aa55cbd..d21a0af439 100644 --- a/.github/workflows/integration-runner.yml +++ b/.github/workflows/integration-runner.yml @@ -4,8 +4,9 @@ run-name: >- Run Integration Tests ${{ inputs.reason || github.event.label.name || 'manual' }} on: - # Use pull_request_target to access secrets even on fork PRs. - # Labeled PR runs are gated per job to integration-test and behavior-test. + # Labeled PR runs are gated per job to integration-test and behavior-test + # and to same-repository PRs: fork PRs never reach the secrets or the + # write permissions below. Manual runs use workflow_dispatch. pull_request_target: types: - labeled @@ -54,8 +55,9 @@ jobs: setup-matrix: if: >- github.event_name != 'pull_request_target' || - github.event.label.name == 'integration-test' || - github.event.label.name == 'behavior-test' + (github.event.pull_request.head.repo.full_name == github.repository && + (github.event.label.name == 'integration-test' || + github.event.label.name == 'behavior-test')) runs-on: ubuntu-latest outputs: matrix: ${{ steps.resolve-models.outputs.matrix }} @@ -171,7 +173,8 @@ jobs: # Post initial comment for label triggers (no dependencies - runs immediately) post-label-comment: if: > - github.event_name == 'pull_request_target' && ( + github.event_name == 'pull_request_target' && + github.event.pull_request.head.repo.full_name == github.repository && ( github.event.label.name == 'integration-test' || github.event.label.name == 'behavior-test' ) @@ -228,13 +231,14 @@ jobs: printf '%s' "$COMMENT_BODY" | gh issue comment "$ISSUE_NUMBER" --body-file - run-integration-tests: - # Security: Only run when integration-related labels are present or via workflow_dispatch - # This prevents automatic execution on fork PRs without maintainer approval + # Security: Only run when integration-related labels are present on a + # same-repository PR, or via workflow_dispatch. Fork PRs never run. # Note: uses always() to run even when comment jobs are skipped if: | always() && ( ( - github.event_name == 'pull_request_target' && ( + github.event_name == 'pull_request_target' && + github.event.pull_request.head.repo.full_name == github.repository && ( github.event.label.name == 'integration-test' || github.event.label.name == 'behavior-test' ) @@ -246,7 +250,6 @@ jobs: timeout-minutes: 180 permissions: contents: read - id-token: write pull-requests: write issues: write strategy: @@ -258,7 +261,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v7 with: - # For pull_request_target: checkout fork PR code (requires explicit repository) + # For pull_request_target: checkout the same-repository PR head # For other events: fallback to current repository and ref repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} ref: ${{ github.event.pull_request.head.sha || github.ref }} @@ -395,7 +398,8 @@ jobs: if: | always() && ( ( - github.event_name == 'pull_request_target' && ( + github.event_name == 'pull_request_target' && + github.event.pull_request.head.repo.full_name == github.repository && ( github.event.label.name == 'integration-test' || github.event.label.name == 'behavior-test' ) diff --git a/.github/workflows/issue-duplicate-checker.yml b/.github/workflows/issue-duplicate-checker.yml deleted file mode 100644 index 87c1d75385..0000000000 --- a/.github/workflows/issue-duplicate-checker.yml +++ /dev/null @@ -1,95 +0,0 @@ ---- -name: Issue Duplicate Check via OpenHands Cloud - -on: - issues: - types: [opened] - schedule: - - cron: 0 9 * * * - workflow_dispatch: - inputs: - mode: - description: Which workflow path to run - required: true - type: choice - options: - - smoke-clone - - issue-check - - auto-close - default: smoke-clone - issue_number: - description: Existing issue number to analyze when mode is issue-check - required: false - type: number - close_after_days: - description: Days to wait before auto-closing duplicate candidates in auto-close mode - required: false - type: number - default: 3 - -permissions: - contents: read - issues: write - -jobs: - smoke-clone: - if: > - github.repository == 'OpenHands/software-agent-sdk' && - github.event_name == 'workflow_dispatch' && inputs.mode == 'smoke-clone' - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v7 - - - name: Clone software-agent-sdk - run: | - git clone --depth 1 "https://github.com/${{ github.repository }}.git" /tmp/software-agent-sdk - echo "software-agent-sdk HEAD: $(git -C /tmp/software-agent-sdk rev-parse --short HEAD)" - - - name: Summarize smoke test - run: | - { - echo "## Smoke clone completed" - echo - echo "- software-agent-sdk cloned to /tmp/software-agent-sdk" - } >> "$GITHUB_STEP_SUMMARY" - - issue-duplicate-check: - if: | - github.repository == 'OpenHands/software-agent-sdk' && - (github.event_name == 'issues' || - (github.event_name == 'workflow_dispatch' && inputs.mode == 'issue-check' && inputs.issue_number != '')) - runs-on: ubuntu-latest - timeout-minutes: 35 - concurrency: - group: issue-duplicate-check-${{ github.repository }}-${{ github.event.issue.number || inputs.issue_number }} - cancel-in-progress: false - steps: - - name: Run issue duplicate check - uses: OpenHands/extensions/plugins/issue-duplicate-checker@fb020c71eb8e7cd2678f0251ab24327d3738da7d - with: - mode: issue-check - repository: ${{ github.repository }} - issue-number: ${{ github.event.issue.number || inputs.issue_number }} - close-after-days: ${{ inputs.close_after_days || '3' }} - openhands-api-key: ${{ secrets.OPENHANDS_API_KEY }} - github-token: ${{ secrets.OPENHANDS_BOT_GITHUB_PAT_PUBLIC || github.token }} - - auto-close-duplicates: - if: | - github.repository == 'OpenHands/software-agent-sdk' && - (github.event_name == 'schedule' || - (github.event_name == 'workflow_dispatch' && inputs.mode == 'auto-close')) - runs-on: ubuntu-latest - timeout-minutes: 20 - concurrency: - group: auto-close-duplicates-${{ github.repository }} - cancel-in-progress: false - steps: - - name: Auto-close aged duplicate candidates - uses: OpenHands/extensions/plugins/issue-duplicate-checker@fb020c71eb8e7cd2678f0251ab24327d3738da7d - with: - mode: auto-close - repository: ${{ github.repository }} - close-after-days: ${{ inputs.close_after_days || '3' }} - github-token: ${{ secrets.OPENHANDS_BOT_GITHUB_PAT_PUBLIC || github.token }} diff --git a/.github/workflows/issue-readiness-check.yml b/.github/workflows/issue-readiness-check.yml deleted file mode 100644 index a7b742a478..0000000000 --- a/.github/workflows/issue-readiness-check.yml +++ /dev/null @@ -1,135 +0,0 @@ ---- -name: Issue Readiness Check - -# Manages the `ready-for-dev` label based on type-specific readiness criteria. -# -# Bug reports (label `bug`): the Actual Behavior section must reference a -# reproducible SDK command (`python`, `pytest`, `uv`, or `pip`), plus an -# Acceptance Criteria section with at least one checklist item. -# -# Enhancements (label `enhancement`): the body must contain Desired Behavior and -# Acceptance Criteria sections, the latter with at least one checklist item. -# -# When criteria are met the label is added (idempotently); when they are not the -# label is removed. A feedback comment is posted (or updated) in three cases: -# 1. The issue is first opened or reopened — always comment so the author -# knows whether their issue is ready or what is missing. -# 2. The label is being added (issue became ready) — comment celebrating it. -# 3. The label is being removed (issue was ready but is no longer) — comment -# explaining what changed. -# Edits that do not change the label state do not produce a new comment. All -# comments are upserted by a hidden marker so there is at most one per issue. - -on: - issues: - types: [opened, edited, reopened, labeled, unlabeled] - -permissions: - issues: write - pull-requests: read - contents: read - -concurrency: - group: issue-readiness-${{ github.event.issue.number }} - cancel-in-progress: false - -jobs: - check: - if: github.event.issue.state == 'open' && github.event.issue.pull_request == null - runs-on: ubuntu-24.04 - timeout-minutes: 5 - steps: - - name: Checkout trusted workflow scripts - uses: actions/checkout@v7 - - - name: Evaluate issue readiness - id: readiness - env: - GITHUB_EVENT_PATH: ${{ github.event_path }} - run: | - set -euo pipefail - python .github/scripts/check_issue_readiness.py --json > /tmp/result.json - echo "ready=$(jq -r '.ready' /tmp/result.json)" >> "$GITHUB_OUTPUT" - # Write reasons to a file so the comment step can read them - # without shell-escaping multiline text. - jq -r '.reasons[]?' /tmp/result.json > /tmp/reasons.txt || true - echo "reasons_file=/tmp/reasons.txt" >> "$GITHUB_OUTPUT" - - - name: Add ready-for-dev label - if: steps.readiness.outputs.ready == 'true' - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - # Capture stderr — tolerate "label already present" (exit 0 - # from gh) but surface unexpected errors to the step summary. - if ! gh issue edit "${{ github.event.issue.number }}" \ - --repo "${{ github.repository }}" \ - --add-label "ready-for-dev" 2>/tmp/gh-err.txt; then - echo "::warning::Failed to add ready-for-dev label:" >> "$GITHUB_STEP_SUMMARY" - cat /tmp/gh-err.txt >> "$GITHUB_STEP_SUMMARY" - fi - - - name: Remove ready-for-dev label - if: steps.readiness.outputs.ready != 'true' && contains(github.event.issue.labels.*.name, 'ready-for-dev') - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - # Capture stderr — tolerate "label already removed" but - # surface unexpected errors. - if ! gh issue edit "${{ github.event.issue.number }}" \ - --repo "${{ github.repository }}" \ - --remove-label "ready-for-dev" 2>/tmp/gh-err.txt; then - echo "::warning::Failed to remove ready-for-dev label:" >> "$GITHUB_STEP_SUMMARY" - cat /tmp/gh-err.txt >> "$GITHUB_STEP_SUMMARY" - fi - - - name: Post feedback comment - if: >- - github.event.action == 'opened' - || github.event.action == 'reopened' - || (steps.readiness.outputs.ready == 'true' && !contains(github.event.issue.labels.*.name, 'ready-for-dev')) - || (steps.readiness.outputs.ready != 'true' && contains(github.event.issue.labels.*.name, 'ready-for-dev')) - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - if [ "${{ steps.readiness.outputs.ready }}" = "true" ]; then - node .github/scripts/post-readiness-comment.mjs \ - --issue-number "${{ github.event.issue.number }}" \ - --repo "${{ github.repository }}" \ - --ready - else - node .github/scripts/post-readiness-comment.mjs \ - --issue-number "${{ github.event.issue.number }}" \ - --repo "${{ github.repository }}" \ - --reasons-file "${{ steps.readiness.outputs.reasons_file }}" - fi - - # When the ready-for-dev label is added or removed, re-run the PR - # description check for every open PR that links this issue, so the - # `ready-for-dev` gate does not go stale between PR events. Runs after - # `check` so the label reflects the settled readiness state. - refresh-linked-pr-gates: - name: Refresh PR gates for linked issues - needs: check - if: >- - github.event.issue.state == 'open' - && github.event.issue.pull_request == null - && (github.event.action == 'labeled' || github.event.action == 'unlabeled') - && github.event.label.name == 'ready-for-dev' - runs-on: ubuntu-24.04 - timeout-minutes: 10 - permissions: - contents: read - pull-requests: read - actions: write - steps: - - name: Checkout trusted workflow scripts - uses: actions/checkout@v7 - - - name: Re-run PR description checks for linked open PRs - env: - GH_TOKEN: ${{ github.token }} - run: python .github/scripts/refresh_linked_pr_checks.py diff --git a/.github/workflows/pr-artifacts.yml b/.github/workflows/pr-artifacts.yml deleted file mode 100644 index a34ddabfd2..0000000000 --- a/.github/workflows/pr-artifacts.yml +++ /dev/null @@ -1,247 +0,0 @@ ---- -name: PR Artifacts - -on: - workflow_dispatch: # Manual trigger for testing - pull_request_target: - types: [opened, synchronize, reopened, closed] - branches: [main] - pull_request_review: - types: [submitted] - -jobs: - # Auto-remove .pr/ directory from same-repository PRs when approved. - cleanup-on-approval: - concurrency: - group: cleanup-pr-artifacts-${{ github.event.pull_request.number }} - cancel-in-progress: false - if: >- - github.repository == 'OpenHands/software-agent-sdk' && - github.event_name == 'pull_request_review' && - github.event.review.state == 'approved' && - github.event.pull_request.head.repo.full_name == github.repository - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - steps: - # Use PAT so the push triggers CI workflows that will complete and - # satisfy branch protection. We can't use [skip ci] because the Vercel - # GitHub App creates stuck checks that block merging. - - uses: actions/checkout@v7 - with: - ref: ${{ github.event.pull_request.head.ref }} - token: ${{ secrets.OPENHANDS_BOT_GITHUB_PAT_PUBLIC }} - - - name: Remove .pr/ directory - id: remove - run: | - if [ -d ".pr" ]; then - git config user.name "allhands-bot" - git config user.email "allhands-bot@users.noreply.github.com" - git rm -rf .pr/ - git commit -m "chore: Remove PR-only artifacts [automated]" - git push || { - echo "::error::Failed to push cleanup commit. Check branch protection rules." - exit 1 - } - echo "removed=true" >> "$GITHUB_OUTPUT" - echo "::notice::Removed .pr/ directory" - else - echo "removed=false" >> "$GITHUB_OUTPUT" - echo "::notice::No .pr/ directory to remove" - fi - - - name: Update PR comment after cleanup - if: steps.remove.outputs.removed == 'true' - uses: actions/github-script@v9 - with: - script: | - const marker = ''; - const body = `${marker} - ✅ **PR Artifacts Cleaned Up** - - The \`.pr/\` directory has been automatically removed. - `; - - const comments = await github.paginate( - github.rest.issues.listComments, - { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - per_page: 100, - }, - ); - - const existing = comments.find(c => c.body.includes(marker)); - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body: body, - }); - } - - # Inspect the fork through the API; never check out untrusted code with a write token. - check-pr-artifacts: - if: >- - github.event_name == 'pull_request_target' && - github.event.action != 'closed' - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - steps: - - name: Post or update PR comment - uses: actions/github-script@v9 - with: - script: | - const marker = ''; - const pullRequest = context.payload.pull_request; - const headRepository = pullRequest.head.repo; - let exists = true; - - try { - await github.rest.repos.getContent({ - owner: headRepository.owner.login, - repo: headRepository.name, - path: '.pr', - ref: pullRequest.head.sha, - }); - } catch (error) { - if (error.status === 404) { - exists = false; - } else { - throw error; - } - } - - const comments = await github.paginate( - github.rest.issues.listComments, - { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - per_page: 100, - }, - ); - const existing = comments.find(c => c.body.includes(marker)); - - if (!exists) { - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body: `${marker} - ✅ **PR Artifacts Cleaned Up** - - The \`.pr/\` directory is no longer present. - `, - }); - } - return; - } - - const isFork = headRepository.full_name !== context.payload.repository.full_name; - const cleanup = isFork - ? 'Because this is a fork PR, the directory will be **automatically removed from `main` immediately after merge**.' - : 'The directory will be **automatically removed when the PR is approved**.'; - const body = `${marker} - 📁 **PR Artifacts Notice** - - This PR contains a \`.pr/\` directory with temporary PR-specific documents. ${cleanup} - `; - - core.warning('.pr/ directory contains temporary PR-only artifacts'); - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body: body, - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: body, - }); - } - - # Fork branches cannot be modified reliably, so clean the trusted base branch - # after merge. This also covers same-repository PRs if approval cleanup was missed. - cleanup-after-merge: - concurrency: - group: cleanup-pr-artifacts-${{ github.event.pull_request.base.ref }} - cancel-in-progress: false - if: >- - github.repository == 'OpenHands/software-agent-sdk' && - github.event_name == 'pull_request_target' && - github.event.action == 'closed' && - github.event.pull_request.merged == true - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - steps: - - uses: actions/checkout@v7 - with: - ref: ${{ github.event.pull_request.base.ref }} - token: ${{ secrets.OPENHANDS_BOT_GITHUB_PAT_PUBLIC }} - - - name: Remove .pr/ directory - id: remove - env: - BASE_REF: ${{ github.event.pull_request.base.ref }} - run: | - if [ -d ".pr" ]; then - git config user.name "allhands-bot" - git config user.email "allhands-bot@users.noreply.github.com" - git rm -rf .pr/ - git commit -m "chore: Remove merged PR artifacts [automated]" - git push origin "HEAD:$BASE_REF" || { - echo "::error::Failed to remove .pr/ from $BASE_REF. Check branch protection rules." - exit 1 - } - echo "removed=true" >> "$GITHUB_OUTPUT" - echo "::notice::Removed .pr/ directory from $BASE_REF" - else - echo "removed=false" >> "$GITHUB_OUTPUT" - echo "::notice::No .pr/ directory to remove from $BASE_REF" - fi - - - name: Update PR comment after cleanup - if: steps.remove.outputs.removed == 'true' - uses: actions/github-script@v9 - with: - script: | - const marker = ''; - const body = `${marker} - ✅ **PR Artifacts Cleaned Up** - - The \`.pr/\` directory has been automatically removed from \`${context.payload.pull_request.base.ref}\` after merge. - `; - - const comments = await github.paginate( - github.rest.issues.listComments, - { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - per_page: 100, - }, - ); - const existing = comments.find(c => c.body.includes(marker)); - - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body: body, - }); - } diff --git a/.github/workflows/pr-description-check.yml b/.github/workflows/pr-description-check.yml index 295159fcad..ab52d164c3 100644 --- a/.github/workflows/pr-description-check.yml +++ b/.github/workflows/pr-description-check.yml @@ -1,24 +1,25 @@ --- name: PR Description Check -# Use pull_request_target so fork PR descriptions can be checked, but only run -# trusted validation code from the base branch checkout below. +# Read-only check of the PR template sections and the linked-issue reference. +# It uses plain pull_request (no secrets, read-only token on fork PRs) and +# deliberately passes no GITHUB_TOKEN to the script, so the `ready-for-dev` +# label lookup is skipped: this fork has no issue-readiness workflow that +# applies that label. See README-RELEASE.md. on: - pull_request_target: + pull_request: types: [opened, edited, reopened, ready_for_review, synchronize] permissions: contents: read pull-requests: read - issues: read jobs: validate-pr-description: name: Validate PR description # Draft PRs may still have incomplete descriptions; validate when review starts. - # Release PRs are generated by the trusted prepare-release workflow without - # the standard PR template. + # Release PRs (`rel-*` branches) do not use the standard PR template. if: >- github.event.pull_request.draft == false && github.event.pull_request.user.login != 'dependabot[bot]' && @@ -26,12 +27,8 @@ jobs: startsWith(github.event.pull_request.head.ref, 'rel-')) runs-on: ubuntu-24.04 steps: - - name: Checkout trusted workflow scripts + - name: Checkout uses: actions/checkout@v7 - with: - ref: ${{ github.event.pull_request.base.sha }} - - name: Validate PR template and linked-issue readiness - env: - GITHUB_TOKEN: ${{ github.token }} + - name: Validate PR template and linked-issue reference run: python .github/scripts/check_pr_description.py diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml deleted file mode 100644 index d0da736072..0000000000 --- a/.github/workflows/prepare-release.yml +++ /dev/null @@ -1,175 +0,0 @@ ---- -name: Prepare Release - -on: - workflow_dispatch: - inputs: - version: - description: Release version (e.g., 1.2.3) - required: true - type: string - -jobs: - prepare-release: - # Only run in the main repository, not in forks - if: github.repository == 'OpenHands/software-agent-sdk' - runs-on: ubuntu-24.04 - steps: - - name: Validate version format - env: - INPUTS_VERSION: ${{ inputs.version }} - run: | - if ! [[ "$INPUTS_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "❌ Invalid version format. Expected: X.Y.Z (e.g., 1.2.3)" - exit 1 - fi - echo "✅ Version format is valid: $INPUTS_VERSION" - - - name: Checkout repository - uses: actions/checkout@v7 - with: - token: ${{ secrets.OPENHANDS_BOT_GITHUB_PAT_PUBLIC }} - - - name: Install uv - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 - with: - version: latest - python-version: '3.13' - - - name: Configure Git - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - - name: Create release branch - env: - INPUTS_VERSION: ${{ inputs.version }} - run: | - BRANCH_NAME="rel-$INPUTS_VERSION" - echo "Creating branch: $BRANCH_NAME" - git checkout -b "$BRANCH_NAME" - echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV - - - name: Set package version - env: - INPUTS_VERSION: ${{ inputs.version }} - run: | - echo "🔧 Setting version to $INPUTS_VERSION" - make set-package-version version="$INPUTS_VERSION" - - - name: Set TypeScript client version - env: - INPUTS_VERSION: ${{ inputs.version }} - run: | - npm --prefix clients/typescript version "$INPUTS_VERSION" \ - --no-git-tag-version --ignore-scripts - - - name: Commit version changes - env: - INPUTS_VERSION: ${{ inputs.version }} - run: | - git add . - if git diff --staged --quiet; then - echo "No changes to commit" - else - git commit -m "Release v$INPUTS_VERSION" -m "Co-authored-by: openhands " - echo "✅ Changes committed" - fi - - - name: Push release branch - run: | - git push -u origin "$BRANCH_NAME" - echo "✅ Branch pushed: $BRANCH_NAME" - - - name: Create Pull Request - id: create_pr - env: - GH_TOKEN: ${{ secrets.OPENHANDS_BOT_GITHUB_PAT_PUBLIC }} - GITHUB_ACTOR: ${{ github.actor }} - INPUTS_VERSION: ${{ inputs.version }} - run: | - python3 << 'PY' - import os - from pathlib import Path - - actor = os.environ["GITHUB_ACTOR"] - version = os.environ["INPUTS_VERSION"] - Path("pr_body.txt").write_text( - f"""## Release v{version} - - This PR prepares the release for version **{version}**. - - **Started by:** @{actor} - - ### Release Checklist - - [x] Version set to {version} - - [ ] Fix any deprecation deadlines if they exist - - [ ] Integration tests pass (tagged with `integration-test`) - - [ ] Behavior tests pass (tagged with `behavior-test`) - - [ ] Example tests pass (tagged with `test-examples`) - - [ ] Security scan passes (tagged with `security-scan`) - - [ ] Confirm any `release-note-required` PRs are accurately called out in the final release notes - - ### What happens on merge - When this PR is merged, the `create-release.yml` workflow will automatically: - 1. Create a GitHub release with tag `v{version}` and auto-generated notes, plus an explicit preamble for merged `release-note-required` PRs - 2. Trigger `pypi-release.yml` to publish all packages to PyPI - 3. Trigger `version-bump-prs.yml` to create downstream version bump PRs - """ - ) - PY - - gh pr create \ - --title "Release v$INPUTS_VERSION" \ - --body-file pr_body.txt \ - --base main \ - --head "$BRANCH_NAME" \ - --label "integration-test" \ - --label "behavior-test" \ - --label "test-examples" \ - --label "security-scan" - - rm pr_body.txt - echo "✅ Pull request created successfully!" - - # Get PR URL and display it - PR_URL=$(gh pr view "$BRANCH_NAME" --json url --jq '.url') - echo "🔗 PR URL: $PR_URL" - echo "PR_URL=$PR_URL" >> $GITHUB_ENV - echo "pr_url=$PR_URL" >> $GITHUB_OUTPUT - - - name: Check Slack token availability - id: slack_token - env: - SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} - run: | - if [ -n "$SLACK_BOT_TOKEN" ]; then - echo "available=true" >> "$GITHUB_OUTPUT" - else - echo "available=false" >> "$GITHUB_OUTPUT" - fi - - - name: Notify Slack - if: steps.slack_token.outputs.available == 'true' - uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 - with: - method: chat.postMessage - token: ${{ secrets.SLACK_BOT_TOKEN }} - payload: | - channel: "#proj-agent" - text: "🛠️ *SDK v${{ inputs.version }} release preparation started*\n\nRelease PR: <${{ steps.create_pr.outputs.pr_url }}|Release v${{ inputs.version }}>\nStarted by: <${{ github.server_url }}/${{ github.actor }}|@${{ github.actor }}>\nRepo: <${{ github.server_url }}/${{ github.repository }}|${{ github.repository }}>" - - - name: Summary - env: - INPUTS_VERSION: ${{ inputs.version }} - run: | - echo "## ✅ Release Preparation Complete!" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "- **Version**: $INPUTS_VERSION" >> $GITHUB_STEP_SUMMARY - echo "- **Branch**: ${{ env.BRANCH_NAME }}" >> $GITHUB_STEP_SUMMARY - echo "- **PR URL**: ${{ env.PR_URL }}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Next Steps:" >> $GITHUB_STEP_SUMMARY - echo "1. Review the PR and address any deprecation deadlines" >> $GITHUB_STEP_SUMMARY - echo "2. Wait for integration, behavior, example tests, and security scan to pass" >> $GITHUB_STEP_SUMMARY - echo "3. Merge the PR — a GitHub release and PyPI publish will happen automatically" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/pypi-release.yml b/.github/workflows/pypi-release.yml index 9a2b6fe6d2..053f663ee5 100644 --- a/.github/workflows/pypi-release.yml +++ b/.github/workflows/pypi-release.yml @@ -1,47 +1,35 @@ --- name: Publish all OpenHands packages (uv) +# Manual only. Publishing to PyPI is not adopted for this fork by default: the +# job runs only when a person dispatches the workflow and types the exact +# confirmation `publish-pypi`; any other value skips the job. There is no +# release/push/pull_request trigger. The job fails closed when the +# PYPI_TOKEN_OPENHANDS secret is missing. See README-RELEASE.md. + on: - # Run manually workflow_dispatch: - # Run automatically when a release is published - release: - types: [published] + inputs: + confirm: + description: Type publish-pypi to publish to PyPI (anything else does nothing) + required: true + default: '' + type: string jobs: publish: - # Skip PyPI publishing for pre-releases (e.g., release candidates). - # Pre-releases can still be created on GitHub for testing without - # pushing packages to PyPI. Manual workflow_dispatch still runs on - # OpenHands/software-agent-sdk only (forks are excluded). - if: > - github.repository == 'OpenHands/software-agent-sdk' && - (github.event_name == 'workflow_dispatch' || - (!github.event.release.prerelease && - startsWith(github.event.release.tag_name, 'v'))) + if: inputs.confirm == 'publish-pypi' runs-on: ubuntu-24.04 permissions: - actions: write contents: read - outputs: - version: ${{ steps.extract_version.outputs.version }} steps: - name: Checkout uses: actions/checkout@v7 - - name: Extract version from release tag + - name: Extract version from pyproject.toml id: extract_version - env: - GITHUB_EVENT_RELEASE_TAG_NAME: ${{ github.event.release.tag_name }} run: | - # Get version from release tag (e.g., v1.2.3 -> 1.2.3) - if [[ "${{ github.event_name }}" == "release" ]]; then - VERSION="$GITHUB_EVENT_RELEASE_TAG_NAME" - VERSION="${VERSION#v}" # Remove 'v' prefix if present - else - # For manual dispatch, extract from pyproject.toml - VERSION=$(grep -m1 '^version = ' openhands-sdk/pyproject.toml | cut -d'"' -f2) - fi + VERSION=$(grep -m1 '^version = ' openhands-sdk/pyproject.toml | cut -d'"' -f2) echo "version=$VERSION" >> $GITHUB_OUTPUT echo "📦 Version: $VERSION" @@ -54,6 +42,7 @@ jobs: - name: Build and publish all packages env: UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN_OPENHANDS }} + VERSION: ${{ steps.extract_version.outputs.version }} run: | set -euo pipefail @@ -87,17 +76,3 @@ jobs: # This allows re-running the workflow after partial failures uv publish --token "$UV_PUBLISH_TOKEN" --check-url https://pypi.org/simple/ echo "✅ All packages built and published successfully!" - echo "" - echo "📋 Note: Version bump PRs will be created by the 'Create Version Bump PRs' workflow" - echo " which is dispatched after this publish succeeds." - - - name: Dispatch version bump workflow - env: - GH_TOKEN: ${{ github.token }} - VERSION: ${{ steps.extract_version.outputs.version }} - run: | - gh workflow run version-bump-prs.yml \ - --repo "${{ github.repository }}" \ - -f "version=${VERSION}" - - echo "🚀 Dispatched version-bump-prs.yml for v${VERSION}" diff --git a/.github/workflows/remove-duplicate-candidate-label.yml b/.github/workflows/remove-duplicate-candidate-label.yml deleted file mode 100644 index 937c4926e1..0000000000 --- a/.github/workflows/remove-duplicate-candidate-label.yml +++ /dev/null @@ -1,31 +0,0 @@ ---- -name: Remove duplicate candidate label on activity - -on: - issue_comment: - types: [created] - -permissions: - issues: write - -concurrency: - group: remove-duplicate-${{ github.repository }}-${{ github.event.issue.number }} - cancel-in-progress: false - -jobs: - remove-duplicate-candidate: - if: | - github.repository == 'OpenHands/software-agent-sdk' && - github.event.issue.state == 'open' && - github.event.issue.pull_request == null && - contains(github.event.issue.labels.*.name, 'duplicate-candidate') && - github.event.comment.user.type != 'Bot' && - !startsWith(github.event.comment.body || '', '