| name: Stale | |
| on: | |
| schedule: | |
| - cron: "17 3 * * *" | |
| workflow_dispatch: | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" | |
| permissions: {} | |
| jobs: | |
| stale: | |
| permissions: | |
| issues: write | |
| pull-requests: write | |
| runs-on: blacksmith-16vcpu-ubuntu-2404 | |
| steps: | |
| - uses: actions/create-github-app-token@v2 | |
| id: app-token | |
| continue-on-error: true | |
| with: | |
| app-id: "2729701" | |
| private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} | |
| - uses: actions/create-github-app-token@v2 | |
| id: app-token-fallback | |
| continue-on-error: true | |
| with: | |
| app-id: "2971289" | |
| private-key: ${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }} | |
| - name: Mark stale issues and pull requests (primary) | |
| id: stale-primary | |
| continue-on-error: true | |
| uses: actions/stale@v10 | |
| with: | |
| repo-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }} | |
| days-before-issue-stale: 7 | |
| days-before-issue-close: 5 | |
| days-before-pr-stale: 5 | |
| days-before-pr-close: 3 | |
| stale-issue-label: stale | |
| stale-pr-label: stale | |
| exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale | |
| exempt-pr-labels: maintainer,no-stale | |
| operations-per-run: 2000 | |
| ascending: true | |
| exempt-all-assignees: true | |
| remove-stale-when-updated: true | |
| stale-issue-message: | | |
| This issue has been automatically marked as stale due to inactivity. | |
| Please add updates or it will be closed. | |
| stale-pr-message: | | |
| This pull request has been automatically marked as stale due to inactivity. | |
| Please add updates or it will be closed. | |
| close-issue-message: | | |
| Closing due to inactivity. | |
| If this is still an issue, please retry on the latest OpenClaw release and share updated details. | |
| If you are absolutely sure it still happens on the latest release, open a new issue with fresh repro steps. | |
| close-issue-reason: not_planned | |
| close-pr-message: | | |
| Closing due to inactivity. | |
| If you believe this PR should be revived, post in #pr-thunderdome-dangerzone on Discord to talk to a maintainer. | |
| That channel is the escape hatch for high-quality PRs that get auto-closed. | |
| - name: Check stale state cache | |
| id: stale-state | |
| if: always() | |
| uses: actions/github-script@v8 | |
| with: | |
| github-token: ${{ steps.app-token-fallback.outputs.token || steps.app-token.outputs.token }} | |
| script: | | |
| const cacheKey = "_state"; | |
| const { owner, repo } = context.repo; | |
| try { | |
| const { data } = await github.rest.actions.getActionsCacheList({ | |
| owner, | |
| repo, | |
| key: cacheKey, | |
| }); | |
| const caches = data.actions_caches ?? []; | |
| const hasState = caches.some(cache => cache.key === cacheKey); | |
| core.setOutput("has_state", hasState ? "true" : "false"); | |
| } catch (error) { | |
| const message = error instanceof Error ? error.message : String(error); | |
| core.warning(`Failed to check stale state cache: ${message}`); | |
| core.setOutput("has_state", "false"); | |
| } | |
| - name: Mark stale issues and pull requests (fallback) | |
| if: (steps.stale-primary.outcome == 'failure' || steps.stale-state.outputs.has_state == 'true') && steps.app-token-fallback.outputs.token != '' | |
| uses: actions/stale@v10 | |
| with: | |
| repo-token: ${{ steps.app-token-fallback.outputs.token }} | |
| days-before-issue-stale: 7 | |
| days-before-issue-close: 5 | |
| days-before-pr-stale: 5 | |
| days-before-pr-close: 3 | |
| stale-issue-label: stale | |
| stale-pr-label: stale | |
| exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale | |
| exempt-pr-labels: maintainer,no-stale | |
| operations-per-run: 2000 | |
| ascending: true | |
| exempt-all-assignees: true | |
| remove-stale-when-updated: true | |
| stale-issue-message: | | |
| This issue has been automatically marked as stale due to inactivity. | |
| Please add updates or it will be closed. | |
| stale-pr-message: | | |
| This pull request has been automatically marked as stale due to inactivity. | |
| Please add updates or it will be closed. | |
| close-issue-message: | | |
| Closing due to inactivity. | |
| If this is still an issue, please retry on the latest OpenClaw release and share updated details. | |
| If you are absolutely sure it still happens on the latest release, open a new issue with fresh repro steps. | |
| close-issue-reason: not_planned | |
| close-pr-message: | | |
| Closing due to inactivity. | |
| If you believe this PR should be revived, post in #pr-thunderdome-dangerzone on Discord to talk to a maintainer. | |
| That channel is the escape hatch for high-quality PRs that get auto-closed. | |
| lock-closed-issues: | |
| permissions: | |
| issues: write | |
| runs-on: blacksmith-16vcpu-ubuntu-2404 | |
| steps: | |
| - uses: actions/create-github-app-token@v2 | |
| id: app-token | |
| with: | |
| app-id: "2729701" | |
| private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} | |
| - name: Lock closed issues after 48h of no comments | |
| uses: actions/github-script@v8 | |
| with: | |
| github-token: ${{ steps.app-token.outputs.token }} | |
| script: | | |
| const lockAfterHours = 48; | |
| const lockAfterMs = lockAfterHours * 60 * 60 * 1000; | |
| const perPage = 100; | |
| const cutoffMs = Date.now() - lockAfterMs; | |
| const { owner, repo } = context.repo; | |
| let locked = 0; | |
| let inspected = 0; | |
| let page = 1; | |
| while (true) { | |
| const { data: issues } = await github.rest.issues.listForRepo({ | |
| owner, | |
| repo, | |
| state: "closed", | |
| sort: "updated", | |
| direction: "desc", | |
| per_page: perPage, | |
| page, | |
| }); | |
| if (issues.length === 0) { | |
| break; | |
| } | |
| for (const issue of issues) { | |
| if (issue.pull_request) { | |
| continue; | |
| } | |
| if (issue.locked) { | |
| continue; | |
| } | |
| if (!issue.closed_at) { | |
| continue; | |
| } | |
| inspected += 1; | |
| const closedAtMs = Date.parse(issue.closed_at); | |
| if (!Number.isFinite(closedAtMs)) { | |
| continue; | |
| } | |
| if (closedAtMs > cutoffMs) { | |
| continue; | |
| } | |
| let lastCommentMs = 0; | |
| if (issue.comments > 0) { | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner, | |
| repo, | |
| issue_number: issue.number, | |
| per_page: 1, | |
| page: 1, | |
| sort: "created", | |
| direction: "desc", | |
| }); | |
| if (comments.length > 0) { | |
| lastCommentMs = Date.parse(comments[0].created_at); | |
| } | |
| } | |
| const lastActivityMs = Math.max(closedAtMs, lastCommentMs || 0); | |
| if (lastActivityMs > cutoffMs) { | |
| continue; | |
| } | |
| await github.rest.issues.lock({ | |
| owner, | |
| repo, | |
| issue_number: issue.number, | |
| lock_reason: "resolved", | |
| }); | |
| locked += 1; | |
| } | |
| page += 1; | |
| } | |
| core.info(`Inspected ${inspected} closed issues; locked ${locked}.`); | |