From 9ad3dafce5666c55e8cd5b16cc5ffba42da6cce1 Mon Sep 17 00:00:00 2001 From: jinliyl <6469360+jinliyl@users.noreply.github.com> Date: Fri, 11 Sep 2026 18:21:49 +0800 Subject: [PATCH] chore(ci): harden and split workflows (#539) * chore(ci): harden and split workflows * fix(ci): support token-based npm publishing * test: make disappearing resource check portable * fix(ci): make Studio releases recoverable * fix(ci): stop Studio publishing on cancellation --- .github/dependabot.yml | 13 ++ .github/workflows/_build-docs.yml | 7 +- .github/workflows/_build-python-packages.yml | 7 +- .github/workflows/_release-npm-plugin.yml | 155 ++++++++++++++++ .github/workflows/ci-docs.yml | 23 +-- .github/workflows/ci-dsh-plugin.yml | 52 ++++++ .github/workflows/ci-openclaw-plugin.yml | 54 ++++++ .github/workflows/ci-packages.yml | 4 +- .github/workflows/ci-python-quality.yml | 29 ++- .github/workflows/ci-python-tests.yml | 11 +- .github/workflows/ci-reme-studio.yml | 9 +- .github/workflows/ci-typescript.yml | 62 ------- .github/workflows/ci-windows.yml | 9 +- .github/workflows/deploy-docs.yml | 3 +- .github/workflows/policy-pr-title.yml | 5 +- .github/workflows/release-auto-fin.yml | 10 +- .github/workflows/release-daily-paper.yml | 10 +- .github/workflows/release-dsh-plugin.yml | 49 +++++ .github/workflows/release-openclaw-plugin.yml | 75 ++++++++ .github/workflows/release-python.yml | 16 +- .github/workflows/release-reme-studio.yml | 105 ++++++++++- .../workflows/release-typescript-plugin.yml | 172 ------------------ .github/workflows/security-codeql.yml | 5 +- tests/unit/test_background_steps.py | 17 +- 24 files changed, 587 insertions(+), 315 deletions(-) create mode 100644 .github/workflows/_release-npm-plugin.yml create mode 100644 .github/workflows/ci-dsh-plugin.yml create mode 100644 .github/workflows/ci-openclaw-plugin.yml delete mode 100644 .github/workflows/ci-typescript.yml create mode 100644 .github/workflows/release-dsh-plugin.yml create mode 100644 .github/workflows/release-openclaw-plugin.yml delete mode 100644 .github/workflows/release-typescript-plugin.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8ed3bfa0..43bd5345 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,5 +1,18 @@ version: 2 updates: + - package-ecosystem: "github-actions" + directory: "/" + target-branch: "main" + schedule: + interval: "weekly" + day: "monday" + time: "09:30" + timezone: "Asia/Shanghai" + open-pull-requests-limit: 5 + commit-message: + prefix: "chore" + include: "scope" + - package-ecosystem: "pip" directory: "/" target-branch: "main" diff --git a/.github/workflows/_build-docs.yml b/.github/workflows/_build-docs.yml index 338c0ec6..92aec7d2 100644 --- a/.github/workflows/_build-docs.yml +++ b/.github/workflows/_build-docs.yml @@ -21,18 +21,19 @@ jobs: build: name: Build documentation runs-on: ubuntu-latest + timeout-minutes: 20 defaults: run: working-directory: github-pages steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - name: Set up Node - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: '22.22.3' cache: npm @@ -54,6 +55,6 @@ jobs: - name: Upload Pages artifact if: inputs.upload_pages_artifact - uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4 + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 with: path: github-pages/dist diff --git a/.github/workflows/_build-python-packages.yml b/.github/workflows/_build-python-packages.yml index 2802bfcf..43446983 100644 --- a/.github/workflows/_build-python-packages.yml +++ b/.github/workflows/_build-python-packages.yml @@ -21,14 +21,15 @@ jobs: distributions: name: Build Python distributions runs-on: ubuntu-latest + timeout-minutes: 45 steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: '3.11' @@ -81,7 +82,7 @@ jobs: - name: Upload ReMe distributions if: inputs.upload_artifacts - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: reme-distributions path: dist/reme/ diff --git a/.github/workflows/_release-npm-plugin.yml b/.github/workflows/_release-npm-plugin.yml new file mode 100644 index 00000000..7d6c6657 --- /dev/null +++ b/.github/workflows/_release-npm-plugin.yml @@ -0,0 +1,155 @@ +name: _Release npm plugin + +on: + workflow_call: + inputs: + directory: + description: Repository-relative package directory + required: true + type: string + package_name: + description: Exact public npm package name + required: true + type: string + artifact_name: + description: Prefix for the packed package artifact + required: true + type: string + version: + description: Exact package.json version; an optional v prefix is accepted + required: true + type: string + npm_tag: + description: npm distribution tag + required: true + type: string + use_npm_token: + description: Use the npm environment NPM_TOKEN instead of Trusted Publishing + required: false + default: false + type: boolean + validate_clawhub: + description: Validate the package against the ClawHub contract + required: false + default: false + type: boolean + outputs: + version: + description: Normalized package version + value: ${{ jobs.build.outputs.version }} + secrets: + NPM_TOKEN: + description: Optional bootstrap or recovery token for npm publishing + required: false + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 30 + outputs: + version: ${{ steps.validate.outputs.version }} + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "24.16.0" + cache: npm + cache-dependency-path: ${{ inputs.directory }}/package-lock.json + + - name: Validate package identity and version + id: validate + working-directory: ${{ inputs.directory }} + env: + EXPECTED_NAME: ${{ inputs.package_name }} + RELEASE_VERSION: ${{ inputs.version }} + NPM_TAG: ${{ inputs.npm_tag }} + run: | + node --input-type=module <<'JS' + import { appendFileSync, readFileSync } from 'node:fs'; + const manifest = JSON.parse(readFileSync('package.json', 'utf8')); + const expected = process.env.RELEASE_VERSION.replace(/^v/, ''); + if (manifest.name !== process.env.EXPECTED_NAME) { + throw new Error(`Expected ${process.env.EXPECTED_NAME}, found ${manifest.name}`); + } + if (manifest.version !== expected) throw new Error(`package.json is ${manifest.version}, workflow input is ${expected}`); + if (manifest.version.includes('-') !== (process.env.NPM_TAG === 'next')) { + throw new Error('Prereleases must use next; stable releases must use latest'); + } + appendFileSync(process.env.GITHUB_OUTPUT, `version=${manifest.version}\n`); + JS + + - run: npm ci + working-directory: ${{ inputs.directory }} + - name: Validate package + working-directory: ${{ inputs.directory }} + run: | + npm run format:check + npm run lint + npm run typecheck + npm test + npm run test:package + - name: Validate ClawHub contract + if: inputs.validate_clawhub + working-directory: ${{ inputs.directory }} + run: npx --yes clawhub@0.23.3 package validate . --json + - name: Pack + working-directory: ${{ inputs.directory }} + run: | + mkdir -p "$RUNNER_TEMP/plugin-package" + npm pack --pack-destination "$RUNNER_TEMP/plugin-package" + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ inputs.artifact_name }}-${{ steps.validate.outputs.version }} + path: ${{ runner.temp }}/plugin-package/*.tgz + if-no-files-found: error + + publish: + if: github.ref == 'refs/heads/main' + needs: build + runs-on: ubuntu-latest + timeout-minutes: 10 + environment: npm + permissions: + contents: read + id-token: write + steps: + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "24" + registry-url: https://registry.npmjs.org + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ inputs.artifact_name }}-${{ needs.build.outputs.version }} + path: dist/plugin + - name: Reject an existing package version + env: + PACKAGE_NAME: ${{ inputs.package_name }} + PACKAGE_VERSION: ${{ needs.build.outputs.version }} + run: | + if npm view "${PACKAGE_NAME}@${PACKAGE_VERSION}" version >/dev/null 2>&1; then + echo "${PACKAGE_NAME}@${PACKAGE_VERSION} already exists" >&2 + exit 1 + fi + - name: Publish to npm with Trusted Publishing + if: ${{ !inputs.use_npm_token }} + env: + NPM_TAG: ${{ inputs.npm_tag }} + run: npm publish dist/plugin/*.tgz --access public --tag "$NPM_TAG" --provenance + - name: Publish to npm with NPM_TOKEN + if: inputs.use_npm_token + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_TAG: ${{ inputs.npm_tag }} + run: | + if [[ -z "${NODE_AUTH_TOKEN}" ]]; then + echo "NPM_TOKEN is required when use_npm_token is enabled" >&2 + exit 1 + fi + npm publish dist/plugin/*.tgz --access public --tag "$NPM_TAG" --provenance diff --git a/.github/workflows/ci-docs.yml b/.github/workflows/ci-docs.yml index 14eaf977..963b8e4e 100644 --- a/.github/workflows/ci-docs.yml +++ b/.github/workflows/ci-docs.yml @@ -1,29 +1,8 @@ name: CI / Documentation on: - push: - branches: [main, master, dev, develop] - paths: - - '.github/workflows/ci-docs.yml' - - '.github/workflows/_build-docs.yml' - - 'AGENTS.md' - - 'README.md' - - 'README_ZH.md' - - 'docs/**' - - 'github-pages/**' - - 'reme/config/default.yaml' - - 'integrations/claude_code/README.md' - - 'integrations/hermes_agent/README.md' - - 'reme_studio/README*.md' - - 'reme_studio/public/og.jpg' - - 'integrations/dsh/README*.md' - - 'integrations/dsh/figures/**' - - 'integrations/openclaw/README*.md' - - 'plugins/*/README*.md' - - 'benchmark/*/README*.md' - - 'benchmark/toolmemory/gitcha.png' pull_request: - branches: [main, master, dev, develop] + branches: [main] paths: - '.github/workflows/ci-docs.yml' - '.github/workflows/_build-docs.yml' diff --git a/.github/workflows/ci-dsh-plugin.yml b/.github/workflows/ci-dsh-plugin.yml new file mode 100644 index 00000000..7f90efe8 --- /dev/null +++ b/.github/workflows/ci-dsh-plugin.yml @@ -0,0 +1,52 @@ +name: CI / DSH plugin + +on: + push: + branches: [main] + paths: + - ".github/workflows/ci-dsh-plugin.yml" + - ".github/workflows/_release-npm-plugin.yml" + - ".github/workflows/release-dsh-plugin.yml" + - "integrations/dsh/**" + pull_request: + branches: [main] + paths: + - ".github/workflows/ci-dsh-plugin.yml" + - ".github/workflows/_release-npm-plugin.yml" + - ".github/workflows/release-dsh-plugin.yml" + - "integrations/dsh/**" + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + package: + name: Validate DSH plugin + runs-on: ubuntu-latest + timeout-minutes: 20 + defaults: + run: + working-directory: integrations/dsh + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "24.16.0" + cache: npm + cache-dependency-path: integrations/dsh/package-lock.json + + - run: npm ci + - run: npm run format:check + - run: npm run lint + - run: npm run typecheck + - run: npm test + - run: npm run test:package diff --git a/.github/workflows/ci-openclaw-plugin.yml b/.github/workflows/ci-openclaw-plugin.yml new file mode 100644 index 00000000..a2a968e4 --- /dev/null +++ b/.github/workflows/ci-openclaw-plugin.yml @@ -0,0 +1,54 @@ +name: CI / OpenClaw plugin + +on: + push: + branches: [main] + paths: + - ".github/workflows/ci-openclaw-plugin.yml" + - ".github/workflows/_release-npm-plugin.yml" + - ".github/workflows/release-openclaw-plugin.yml" + - "integrations/openclaw/**" + pull_request: + branches: [main] + paths: + - ".github/workflows/ci-openclaw-plugin.yml" + - ".github/workflows/_release-npm-plugin.yml" + - ".github/workflows/release-openclaw-plugin.yml" + - "integrations/openclaw/**" + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + package: + name: Validate OpenClaw plugin + runs-on: ubuntu-latest + timeout-minutes: 20 + defaults: + run: + working-directory: integrations/openclaw + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "24.16.0" + cache: npm + cache-dependency-path: integrations/openclaw/package-lock.json + + - run: npm ci + - run: npm run format:check + - run: npm run lint + - run: npm run typecheck + - run: npm test + - run: npm run test:package + - name: Validate ClawHub contract + run: npx --yes clawhub@0.23.3 package validate . --json diff --git a/.github/workflows/ci-packages.yml b/.github/workflows/ci-packages.yml index 228b4b80..b79aa3ae 100644 --- a/.github/workflows/ci-packages.yml +++ b/.github/workflows/ci-packages.yml @@ -2,7 +2,7 @@ name: CI / Python packages on: push: - branches: [main, master, dev, develop] + branches: [main] paths: - '.github/workflows/ci-packages.yml' - '.github/workflows/_build-python-packages.yml' @@ -14,7 +14,7 @@ on: - 'tests/unit/test_package_versions.py' - 'LICENSE' pull_request: - branches: [main, master, dev, develop] + branches: [main] paths: - '.github/workflows/ci-packages.yml' - '.github/workflows/_build-python-packages.yml' diff --git a/.github/workflows/ci-python-quality.yml b/.github/workflows/ci-python-quality.yml index d39302f6..eb1d962a 100644 --- a/.github/workflows/ci-python-quality.yml +++ b/.github/workflows/ci-python-quality.yml @@ -2,7 +2,9 @@ name: CI / Python quality on: push: + branches: [main] pull_request: + branches: [main] workflow_dispatch: permissions: @@ -13,16 +15,39 @@ concurrency: cancel-in-progress: true jobs: + actionlint: + name: GitHub Actions + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Validate workflows with actionlint + env: + ACTIONLINT_VERSION: 1.7.12 + ACTIONLINT_SHA256: 8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 + run: | + archive="actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" + curl --fail --location --proto '=https' --retry 3 --silent --show-error \ + --output "${RUNNER_TEMP}/${archive}" \ + "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/${archive}" + echo "${ACTIONLINT_SHA256} ${RUNNER_TEMP}/${archive}" | sha256sum --check + tar -xzf "${RUNNER_TEMP}/${archive}" -C "${RUNNER_TEMP}" actionlint + "${RUNNER_TEMP}/actionlint" .github/workflows/*.yml + pre-commit: name: Pre-commit runs-on: ubuntu-latest + timeout-minutes: 30 steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Setup Python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: '3.11' cache: pip diff --git a/.github/workflows/ci-python-tests.yml b/.github/workflows/ci-python-tests.yml index 9aab1e75..5eeb5d29 100644 --- a/.github/workflows/ci-python-tests.yml +++ b/.github/workflows/ci-python-tests.yml @@ -2,9 +2,9 @@ name: CI / Python tests on: push: - branches: [main, master, dev, develop] + branches: [main] pull_request: - branches: [main, master, dev, develop] + branches: [main] workflow_dispatch: concurrency: @@ -18,18 +18,19 @@ jobs: unit-tests: name: Unit Tests - py${{ matrix.python-version }} runs-on: ubuntu-latest + timeout-minutes: 90 strategy: fail-fast: false matrix: - python-version: ["3.11", "3.12", "3.13"] + python-version: ["3.11", "3.12", "3.13", "3.14"] steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ matrix.python-version }} cache: 'pip' diff --git a/.github/workflows/ci-reme-studio.yml b/.github/workflows/ci-reme-studio.yml index 66f5a416..2870a79b 100644 --- a/.github/workflows/ci-reme-studio.yml +++ b/.github/workflows/ci-reme-studio.yml @@ -2,6 +2,7 @@ name: CI / ReMe Studio on: push: + branches: [main] paths: - "reme_studio/**" - ".github/workflows/ci-reme-studio.yml" @@ -11,6 +12,7 @@ on: - "pyproject.toml" - "LICENSE" pull_request: + branches: [main] paths: - "reme_studio/**" - ".github/workflows/ci-reme-studio.yml" @@ -33,17 +35,18 @@ jobs: studio: name: Studio checks runs-on: ubuntu-latest + timeout-minutes: 30 defaults: run: working-directory: reme_studio steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Setup Node - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: "22.22.3" cache: npm @@ -67,7 +70,7 @@ jobs: tar -tzf "${RUNNER_TEMP}"/agentscope-ai-reme_studio-*.tgz | grep '^package/dist-static/index.html$' - name: Set up Python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" diff --git a/.github/workflows/ci-typescript.yml b/.github/workflows/ci-typescript.yml deleted file mode 100644 index 998de3cc..00000000 --- a/.github/workflows/ci-typescript.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: CI / TypeScript plugins - -on: - push: - branches: [main, master, dev, develop] - paths: - - ".github/workflows/ci-typescript.yml" - - ".github/workflows/release-typescript-plugin.yml" - - "integrations/dsh/**" - - "integrations/openclaw/**" - pull_request: - branches: [main, master, dev, develop] - paths: - - ".github/workflows/ci-typescript.yml" - - ".github/workflows/release-typescript-plugin.yml" - - "integrations/dsh/**" - - "integrations/openclaw/**" - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - package: - name: Validate ${{ matrix.name }} - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - include: - - name: DeepSeek Harness plugin - directory: integrations/dsh - - name: OpenClaw plugin - directory: integrations/openclaw - defaults: - run: - working-directory: ${{ matrix.directory }} - - steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - with: - persist-credentials: false - - - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 - with: - node-version: "24.16.0" - cache: npm - cache-dependency-path: ${{ matrix.directory }}/package-lock.json - - - run: npm ci - - run: npm run format:check - - run: npm run lint - - run: npm run typecheck - - run: npm test - - run: npm run test:package - - name: Validate OpenClaw package contract - if: matrix.directory == 'integrations/openclaw' - run: npx --yes clawhub@0.23.3 package validate . --json diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index 75473b56..dcb93e2a 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -2,9 +2,9 @@ name: CI / Windows on: push: - branches: [main, master, dev, develop] + branches: [main] pull_request: - branches: [main, master, dev, develop] + branches: [main] workflow_dispatch: concurrency: @@ -18,18 +18,19 @@ jobs: cli-smoke: name: CLI smoke - py${{ matrix.python-version }} runs-on: windows-latest + timeout-minutes: 30 strategy: fail-fast: false matrix: python-version: ["3.11"] steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ matrix.python-version }} cache: 'pip' diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 667d45ef..8f91e7b8 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -49,10 +49,11 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest needs: build + timeout-minutes: 10 permissions: pages: write id-token: write steps: - name: Deploy id: deployment - uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5 + uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1 diff --git a/.github/workflows/policy-pr-title.yml b/.github/workflows/policy-pr-title.yml index 9ab0957f..084d291a 100644 --- a/.github/workflows/policy-pr-title.yml +++ b/.github/workflows/policy-pr-title.yml @@ -2,8 +2,8 @@ name: Policy / PR title on: pull_request: - branches: [main, master, dev, develop] - types: [opened, edited, synchronize, reopened] + branches: [main] + types: [opened, edited, reopened] permissions: contents: read @@ -12,6 +12,7 @@ permissions: jobs: check-pr-title: runs-on: ubuntu-latest + timeout-minutes: 10 steps: - name: Check PR title format uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1 diff --git a/.github/workflows/release-auto-fin.yml b/.github/workflows/release-auto-fin.yml index d24e522d..0981f3c3 100644 --- a/.github/workflows/release-auto-fin.yml +++ b/.github/workflows/release-auto-fin.yml @@ -30,16 +30,17 @@ concurrency: jobs: build: runs-on: ubuntu-latest + timeout-minutes: 45 env: RELEASE_VERSION: ${{ inputs.version }} steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: '3.11' @@ -149,7 +150,7 @@ jobs: PY - name: Upload distributions - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: reme-auto-fin-${{ inputs.version }} path: dist/auto-fin/ @@ -158,6 +159,7 @@ jobs: publish: needs: build runs-on: ubuntu-latest + timeout-minutes: 10 environment: pypi permissions: contents: read @@ -165,7 +167,7 @@ jobs: steps: - name: Download distributions - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: reme-auto-fin-${{ inputs.version }} path: dist/auto-fin diff --git a/.github/workflows/release-daily-paper.yml b/.github/workflows/release-daily-paper.yml index 13d86ceb..f5651eb3 100644 --- a/.github/workflows/release-daily-paper.yml +++ b/.github/workflows/release-daily-paper.yml @@ -29,16 +29,17 @@ concurrency: jobs: build: runs-on: ubuntu-latest + timeout-minutes: 45 env: RELEASE_VERSION: ${{ inputs.version }} steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: '3.11' @@ -149,7 +150,7 @@ jobs: PY - name: Upload distributions - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: reme-daily-paper-${{ inputs.version }} path: dist/daily-paper/ @@ -158,6 +159,7 @@ jobs: publish: needs: build runs-on: ubuntu-latest + timeout-minutes: 10 environment: pypi permissions: contents: read @@ -165,7 +167,7 @@ jobs: steps: - name: Download distributions - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: reme-daily-paper-${{ inputs.version }} path: dist/daily-paper diff --git a/.github/workflows/release-dsh-plugin.yml b/.github/workflows/release-dsh-plugin.yml new file mode 100644 index 00000000..cb2d9cfc --- /dev/null +++ b/.github/workflows/release-dsh-plugin.yml @@ -0,0 +1,49 @@ +# Configure npm Trusted Publishing for this caller filename and the npm environment. +name: Release / DSH plugin + +run-name: Publish ReMe DSH plugin ${{ inputs.version }} (${{ inputs.npm_tag }}) + +on: + workflow_dispatch: + inputs: + version: + description: Exact package.json version; an optional v prefix is accepted + required: true + type: string + npm_tag: + description: npm distribution tag + required: true + default: latest + type: choice + options: + - next + - latest + use_npm_token: + description: Use the npm environment NPM_TOKEN instead of Trusted Publishing + required: true + default: false + type: boolean + +permissions: + contents: read + +concurrency: + group: publish-reme-dsh-plugin + cancel-in-progress: false + +jobs: + release: + if: github.ref == 'refs/heads/main' + permissions: + contents: read + id-token: write + uses: ./.github/workflows/_release-npm-plugin.yml + secrets: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + with: + directory: integrations/dsh + package_name: '@agentscope-ai/reme-dsh-plugin' + artifact_name: agentscope-ai-reme-dsh-plugin + version: ${{ inputs.version }} + npm_tag: ${{ inputs.npm_tag }} + use_npm_token: ${{ inputs.use_npm_token }} diff --git a/.github/workflows/release-openclaw-plugin.yml b/.github/workflows/release-openclaw-plugin.yml new file mode 100644 index 00000000..a54333e2 --- /dev/null +++ b/.github/workflows/release-openclaw-plugin.yml @@ -0,0 +1,75 @@ +# Configure npm Trusted Publishing for this caller filename and the npm environment. +name: Release / OpenClaw plugin + +run-name: Publish ReMe OpenClaw plugin ${{ inputs.version }} (${{ inputs.npm_tag }}) + +on: + workflow_dispatch: + inputs: + version: + description: Exact package.json version; an optional v prefix is accepted + required: true + type: string + npm_tag: + description: npm distribution tag + required: true + default: latest + type: choice + options: + - next + - latest + use_npm_token: + description: Use the npm environment NPM_TOKEN instead of Trusted Publishing + required: true + default: false + type: boolean + publish_clawhub: + description: Also publish the package to ClawHub + required: true + default: false + type: boolean + +permissions: + contents: read + +concurrency: + group: publish-reme-openclaw-plugin + cancel-in-progress: false + +jobs: + release: + if: github.ref == 'refs/heads/main' + permissions: + contents: read + id-token: write + uses: ./.github/workflows/_release-npm-plugin.yml + secrets: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + with: + directory: integrations/openclaw + package_name: '@agentscope-ai/reme-openclaw-plugin' + artifact_name: agentscope-ai-reme-openclaw-plugin + version: ${{ inputs.version }} + npm_tag: ${{ inputs.npm_tag }} + use_npm_token: ${{ inputs.use_npm_token }} + validate_clawhub: true + + publish-clawhub: + if: ${{ inputs.publish_clawhub && github.ref == 'refs/heads/main' }} + needs: release + permissions: + actions: read + contents: read + id-token: write + uses: openclaw/clawhub/.github/workflows/package-publish.yml@87ca030c30f3cfb78ab15c8e66b5ff1469c8f9c8 # v0.23.3 + with: + family: code-plugin + version: ${{ needs.release.outputs.version }} + tags: ${{ inputs.npm_tag }} + source_repo: ${{ github.repository }} + source_commit: ${{ github.sha }} + source_ref: ${{ github.sha }} + source_path: integrations/openclaw + package_artifact_name: agentscope-ai-reme-openclaw-plugin-${{ needs.release.outputs.version }} + dry_run: false + wait_for_publication: true diff --git a/.github/workflows/release-python.yml b/.github/workflows/release-python.yml index 484f3a91..54bbb65b 100644 --- a/.github/workflows/release-python.yml +++ b/.github/workflows/release-python.yml @@ -1,14 +1,15 @@ -name: Release / Python packages +name: Release / ReMe Python package -# GitHub Releases in this repository publish only the reme-ai Python package. -# Configure a PyPI Trusted Publisher for this repository, workflow, and its -# pypi environment before running an automatic or manual release. +# Publishing a GitHub Release is the primary release trigger for reme-ai; workflow_dispatch is the recovery path. +# Configure a PyPI Trusted Publisher for this repository, workflow, and its pypi environment first. + +run-name: Publish reme-ai ${{ github.event.release.tag_name || inputs.version }} on: workflow_dispatch: inputs: version: - description: Release version + description: Exact reme-ai version; an optional v prefix is accepted required: true type: string release: @@ -26,19 +27,20 @@ jobs: name: Build and verify distributions uses: ./.github/workflows/_build-python-packages.yml with: - expected_version: ${{ github.event_name == 'release' && github.event.release.tag_name || inputs.version }} + expected_version: ${{ github.event.release.tag_name || inputs.version }} upload_artifacts: true publish-reme: needs: build runs-on: ubuntu-latest + timeout-minutes: 10 environment: pypi permissions: contents: read id-token: write steps: - name: Download ReMe distributions - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: reme-distributions path: dist/reme diff --git a/.github/workflows/release-reme-studio.yml b/.github/workflows/release-reme-studio.yml index 14b44807..899dbd41 100644 --- a/.github/workflows/release-reme-studio.yml +++ b/.github/workflows/release-reme-studio.yml @@ -1,6 +1,6 @@ # Release checklist: # 1. Update reme_studio/pyproject.toml, package.json, and package-lock.json to the same Studio version. -# 2. Configure npm Trusted Publishing and PyPI Trusted Publishing with the pypi environment. +# 2. Configure npm Trusted Publishing with the npm environment and PyPI Trusted Publishing with the pypi environment. # 3. Run this workflow manually with the exact Studio version. name: Release / ReMe Studio @@ -22,6 +22,20 @@ on: options: - next - latest + use_npm_token: + description: Use the npm environment NPM_TOKEN instead of Trusted Publishing + required: true + default: false + type: boolean + publish_target: + description: Packages to publish; single-package modes are for release recovery + required: true + default: both + type: choice + options: + - both + - pypi + - npm permissions: contents: read @@ -32,23 +46,25 @@ concurrency: jobs: build: + if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest + timeout-minutes: 45 env: RELEASE_VERSION: ${{ inputs.version }} NPM_TAG: ${{ inputs.npm_tag }} steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: "22.22.3" cache: npm cache-dependency-path: reme_studio/package-lock.json - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" @@ -108,7 +124,7 @@ jobs: assert (static_dir() / "index.html").is_file() PY - - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: reme-studio-${{ inputs.version }} path: | @@ -117,14 +133,16 @@ jobs: if-no-files-found: error publish-python: + if: inputs.publish_target != 'npm' needs: build runs-on: ubuntu-latest + timeout-minutes: 10 environment: pypi permissions: contents: read id-token: write steps: - - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: reme-studio-${{ inputs.version }} path: dist @@ -136,23 +154,90 @@ jobs: skip-existing: true publish-npm: - needs: build + if: >- + !cancelled() && + needs.build.result == 'success' && + (inputs.publish_target == 'npm' || + (inputs.publish_target == 'both' && needs.publish-python.result == 'success')) + needs: [build, publish-python] runs-on: ubuntu-latest + timeout-minutes: 10 + environment: npm permissions: contents: read id-token: write steps: - - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: "24" registry-url: https://registry.npmjs.org - - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: reme-studio-${{ inputs.version }} path: dist - - name: Publish ReMe Studio to npm + - name: Verify the matching PyPI release for npm-only recovery + if: inputs.publish_target == 'npm' + env: + PACKAGE_VERSION: ${{ inputs.version }} + run: | + python - <<'PY' + import os + import urllib.error + import urllib.request + + version = os.environ["PACKAGE_VERSION"].removeprefix("v") + url = f"https://pypi.org/pypi/reme-studio/{version}/json" + try: + with urllib.request.urlopen(url, timeout=30) as response: + if response.status != 200: + raise SystemExit(f"Unexpected PyPI response for reme-studio {version}: {response.status}") + except urllib.error.HTTPError as exc: + raise SystemExit(f"reme-studio {version} must exist on PyPI before npm-only recovery") from exc + PY + + - name: Check for an identical existing npm package + id: npm-version + env: + PACKAGE_VERSION: ${{ inputs.version }} + run: | + package_file=$(find dist/studio-npm -maxdepth 1 -name '*.tgz' -print -quit) + if [[ -z "${package_file}" ]]; then + echo "Studio npm artifact is missing" >&2 + exit 1 + fi + local_integrity=$(node --input-type=module - "${package_file}" <<'JS' + import { createHash } from 'node:crypto'; + import { readFileSync } from 'node:fs'; + const digest = createHash('sha512').update(readFileSync(process.argv[2])).digest('base64'); + console.log(`sha512-${digest}`); + JS + ) + if remote_integrity=$(npm view "@agentscope-ai/reme_studio@${PACKAGE_VERSION#v}" dist.integrity 2>/dev/null); then + if [[ "${remote_integrity}" != "${local_integrity}" ]]; then + echo "Existing npm package has different contents" >&2 + exit 1 + fi + echo "exists=true" >> "${GITHUB_OUTPUT}" + echo "The identical npm package already exists; nothing to publish" + else + echo "exists=false" >> "${GITHUB_OUTPUT}" + fi + + - name: Publish ReMe Studio to npm with Trusted Publishing + if: ${{ steps.npm-version.outputs.exists != 'true' && !inputs.use_npm_token }} env: NPM_TAG: ${{ inputs.npm_tag }} run: npm publish dist/studio-npm/*.tgz --access public --tag "${NPM_TAG}" --provenance + - name: Publish ReMe Studio to npm with NPM_TOKEN + if: ${{ steps.npm-version.outputs.exists != 'true' && inputs.use_npm_token }} + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_TAG: ${{ inputs.npm_tag }} + run: | + if [[ -z "${NODE_AUTH_TOKEN}" ]]; then + echo "NPM_TOKEN is required when use_npm_token is enabled" >&2 + exit 1 + fi + npm publish dist/studio-npm/*.tgz --access public --tag "${NPM_TAG}" --provenance diff --git a/.github/workflows/release-typescript-plugin.yml b/.github/workflows/release-typescript-plugin.yml deleted file mode 100644 index 31874b34..00000000 --- a/.github/workflows/release-typescript-plugin.yml +++ /dev/null @@ -1,172 +0,0 @@ -# Publish one self-contained host plugin without coupling its version to the other host. -name: Release / TypeScript plugin - -run-name: Publish ReMe ${{ inputs.plugin }} plugin ${{ inputs.version }} (${{ inputs.npm_tag }}) - -on: - workflow_dispatch: - inputs: - plugin: - description: Host plugin to publish - required: true - type: choice - options: - - dsh - - openclaw - version: - description: Exact package.json version; an optional v prefix is accepted - required: true - type: string - npm_tag: - description: npm distribution tag - required: true - default: latest - type: choice - options: - - next - - latest - publish_clawhub: - description: Also publish the OpenClaw package to ClawHub - required: true - default: false - type: boolean - -permissions: - contents: read - -concurrency: - group: publish-reme-${{ inputs.plugin }}-plugin - cancel-in-progress: false - -jobs: - build: - runs-on: ubuntu-latest - outputs: - artifact: ${{ steps.package.outputs.artifact }} - directory: ${{ steps.package.outputs.directory }} - name: ${{ steps.package.outputs.name }} - version: ${{ steps.validate.outputs.version }} - - steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - with: - persist-credentials: false - - - name: Select package - id: package - env: - PLUGIN: ${{ inputs.plugin }} - run: | - case "$PLUGIN" in - dsh) - echo 'directory=integrations/dsh' >> "$GITHUB_OUTPUT" - echo 'name=@agentscope-ai/reme-dsh-plugin' >> "$GITHUB_OUTPUT" - echo 'artifact=agentscope-ai-reme-dsh-plugin' >> "$GITHUB_OUTPUT" - ;; - openclaw) - echo 'directory=integrations/openclaw' >> "$GITHUB_OUTPUT" - echo 'name=@agentscope-ai/reme-openclaw-plugin' >> "$GITHUB_OUTPUT" - echo 'artifact=agentscope-ai-reme-openclaw-plugin' >> "$GITHUB_OUTPUT" - ;; - *) exit 1 ;; - esac - - - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 - with: - node-version: "24.16.0" - cache: npm - cache-dependency-path: ${{ steps.package.outputs.directory }}/package-lock.json - - - name: Validate package identity and version - id: validate - working-directory: ${{ steps.package.outputs.directory }} - env: - EXPECTED_NAME: ${{ steps.package.outputs.name }} - RELEASE_VERSION: ${{ inputs.version }} - NPM_TAG: ${{ inputs.npm_tag }} - run: | - node --input-type=module <<'JS' - import { appendFileSync, readFileSync } from 'node:fs'; - const manifest = JSON.parse(readFileSync('package.json', 'utf8')); - const expected = process.env.RELEASE_VERSION.replace(/^v/, ''); - if (manifest.name !== process.env.EXPECTED_NAME) throw new Error(`Unexpected package name: ${manifest.name}`); - if (manifest.version !== expected) throw new Error(`package.json is ${manifest.version}, workflow input is ${expected}`); - if (manifest.version.includes('-') !== (process.env.NPM_TAG === 'next')) { - throw new Error('Prereleases must use next; stable releases must use latest'); - } - appendFileSync(process.env.GITHUB_OUTPUT, `version=${manifest.version}\n`); - JS - - - run: npm ci - working-directory: ${{ steps.package.outputs.directory }} - - name: Validate package - working-directory: ${{ steps.package.outputs.directory }} - run: | - npm run format:check - npm run lint - npm run typecheck - npm test - npm run test:package - - name: Validate ClawHub contract - if: inputs.plugin == 'openclaw' - working-directory: integrations/openclaw - run: npx --yes clawhub@0.23.3 package validate . --json - - name: Pack - working-directory: ${{ steps.package.outputs.directory }} - run: | - mkdir -p "$RUNNER_TEMP/plugin-package" - npm pack --pack-destination "$RUNNER_TEMP/plugin-package" - - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 - with: - name: ${{ steps.package.outputs.artifact }}-${{ steps.validate.outputs.version }} - path: ${{ runner.temp }}/plugin-package/*.tgz - if-no-files-found: error - - publish: - needs: build - runs-on: ubuntu-latest - permissions: - contents: read - id-token: write - steps: - - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 - with: - node-version: "24" - registry-url: https://registry.npmjs.org - - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 - with: - name: ${{ needs.build.outputs.artifact }}-${{ needs.build.outputs.version }} - path: dist/plugin - - name: Reject an existing package version - env: - PACKAGE_NAME: ${{ needs.build.outputs.name }} - PACKAGE_VERSION: ${{ needs.build.outputs.version }} - run: | - if npm view "${PACKAGE_NAME}@${PACKAGE_VERSION}" version >/dev/null 2>&1; then - echo "${PACKAGE_NAME}@${PACKAGE_VERSION} already exists" >&2 - exit 1 - fi - - name: Publish to npm - env: - NPM_TAG: ${{ inputs.npm_tag }} - run: npm publish dist/plugin/*.tgz --access public --tag "$NPM_TAG" --provenance - - publish-clawhub: - if: ${{ inputs.plugin == 'openclaw' && inputs.publish_clawhub }} - needs: [build, publish] - permissions: - actions: read - contents: read - id-token: write - uses: openclaw/clawhub/.github/workflows/package-publish.yml@87ca030c30f3cfb78ab15c8e66b5ff1469c8f9c8 # v0.23.3 - with: - family: code-plugin - version: ${{ needs.build.outputs.version }} - tags: ${{ inputs.npm_tag }} - source_repo: ${{ github.repository }} - source_commit: ${{ github.sha }} - source_ref: ${{ github.sha }} - source_path: integrations/openclaw - package_artifact_name: ${{ needs.build.outputs.artifact }}-${{ needs.build.outputs.version }} - dry_run: false - wait_for_publication: true diff --git a/.github/workflows/security-codeql.yml b/.github/workflows/security-codeql.yml index b582b0a0..0bde8f30 100644 --- a/.github/workflows/security-codeql.yml +++ b/.github/workflows/security-codeql.yml @@ -10,9 +10,7 @@ on: workflow_dispatch: permissions: - actions: read contents: read - packages: read security-events: write concurrency: @@ -23,6 +21,7 @@ jobs: analyze: name: Analyze ${{ matrix.language }} runs-on: ubuntu-latest + timeout-minutes: 60 strategy: fail-fast: false matrix: @@ -30,7 +29,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false diff --git a/tests/unit/test_background_steps.py b/tests/unit/test_background_steps.py index 5ab2677d..87867a5b 100644 --- a/tests/unit/test_background_steps.py +++ b/tests/unit/test_background_steps.py @@ -1345,19 +1345,24 @@ def test_auto_resource_handles_file_removed_before_stat(): _install_file_jobs(app_ctx, fs) try: source = write_file(cwd / "resource" / "2026-01-01" / "vanishing.txt", "content") + original_is_file = Path.is_file original_stat = Path.stat - source_stat_calls = 0 + + def existing_is_file(path, *args, **kwargs): + if path == source: + return True + return original_is_file(path, *args, **kwargs) def disappearing_stat(path, *args, **kwargs): - nonlocal source_stat_calls if path == source: - source_stat_calls += 1 - if source_stat_calls > 1: - raise FileNotFoundError("file disappeared") + raise FileNotFoundError("file disappeared") return original_stat(path, *args, **kwargs) step = AutoTextResourceStep(app_context=app_ctx, file_store=fs) - with patch.object(Path, "stat", disappearing_stat): + with ( + patch.object(Path, "is_file", existing_is_file), + patch.object(Path, "stat", disappearing_stat), + ): resp = await step( RuntimeContext(changes=[{"change": "added", "path": str(source)}]), )