From 3feb18945b751e06b6b4e4ccde831c2da732b360 Mon Sep 17 00:00:00 2001 From: Craig Osterhout Date: Thu, 9 Apr 2026 13:45:44 -0700 Subject: [PATCH 1/4] add stale workflow Signed-off-by: Craig Osterhout --- .github/workflows/stale.yml | 43 +++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/stale.yml diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000000..81d696319b8 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,43 @@ +# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. +# For more information, see: https://github.com/actions/stale +name: Mark stale issues and pull requests + +on: + schedule: + - cron: '30 1 * * *' # Daily at 1:30 AM UTC + +jobs: + stale: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + + steps: + - uses: actions/stale@v10 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + ascending: false + operations-per-run: 30 + + # Exempt labels - issues/PRs with these labels will never be marked stale + exempt-issue-labels: 'kind/help wanted,status/need-more-info,status/needs-analysis' + exempt-pr-labels: 'kind/help wanted,status/need-more-info,status/needs-analysis' + + # Stale messages + stale-issue-message: "There hasn't been any activity on this issue for a long time. If the problem is still relevant, add a comment on this issue. If not, this issue will be closed in 14 days." + stale-pr-message: "Thanks for the PR. We'd like to make our product docs better, but haven't been able to review all the suggestions. As our docs change often and quickly diverge, we do not have the bandwidth to review and rebase old PRs. If the updates are still relevant, review our [contribution guidelines](https://docs.docker.com/contribute/overview/) and rebase your PR against the latest version of the docs. This helps our maintainers focus on the active issues. If there's no activity, this PR will be closed in 30 days." + + # Close messages + close-issue-message: "Closing this issue as there hasn't been any activity on this issue for a long time. If the problem is still relevant, open a new issue and complete the issue template so we can capture the details required to investigate the issue further. This also helps our maintainers focus on the active issues." + close-pr-message: "Closing this PR as there hasn't been any activity on this PR for a long time. If the updates are still relevant, review our [contribution guidelines](https://docs.docker.com/contribute/overview/) and create a new PR against the latest version of our docs." + + # Timing configuration + days-before-issue-stale: 180 # 6 months + days-before-pr-stale: 180 # 6 months + days-before-issue-close: 14 # 2 weeks after stale + days-before-pr-close: 30 # 1 month after stale + + # Debug mode - set to false when ready for production + # When true, no actual changes will be made (dry-run for testing) + debug-only: true From 07911fd0bead1593fa2bdd9980eb5971e40542ee Mon Sep 17 00:00:00 2001 From: Craig Osterhout Date: Thu, 9 Apr 2026 14:30:17 -0700 Subject: [PATCH 2/4] add lifecycle commands Signed-off-by: Craig Osterhout --- .github/workflows/stale.yml | 171 ++++++++++++++++++++++++++++++++++-- 1 file changed, 165 insertions(+), 6 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 81d696319b8..8769f025fdb 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -1,14 +1,153 @@ # This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. +# It also handles lifecycle slash commands for managing stale labels. # For more information, see: https://github.com/actions/stale +# +# Debug mode: +# - Lifecycle commands: Set DEBUG_ONLY to 'true' in the lifecycle-commands job env +# - Stale action: Set debug-only to true in the stale job configuration name: Mark stale issues and pull requests on: schedule: - cron: '30 1 * * *' # Daily at 1:30 AM UTC + issue_comment: + types: [created] jobs: + lifecycle-commands: + runs-on: ubuntu-latest + if: github.event_name == 'issue_comment' + permissions: + issues: write + pull-requests: write + + steps: + - name: Handle lifecycle commands + uses: actions/github-script@v7 + env: + # Set to 'true' to test without making actual changes + DEBUG_ONLY: 'true' + with: + script: | + const comment = context.payload.comment.body.toLowerCase().trim(); + const debugOnly = process.env.DEBUG_ONLY === 'true'; + + if (debugOnly) { + console.log('🔍 DEBUG MODE: No changes will be made'); + } + + // Define commands and their required permissions + const commands = { + '/lifecycle frozen': { label: 'lifecycle/frozen', requiresWrite: true }, + '/lifecycle stale': { label: 'lifecycle/stale', requiresWrite: true }, + '/lifecycle active': { action: 'remove-stale', requiresWrite: false }, + '/remove-lifecycle frozen': { action: 'remove-frozen', requiresWrite: true }, + '/remove-lifecycle stale': { action: 'remove-stale', requiresWrite: false } + }; + + // Check if comment contains a lifecycle command + const commandKey = Object.keys(commands).find(cmd => + comment === cmd || comment.startsWith(cmd + ' ') + ); + + if (!commandKey) { + console.log('No lifecycle command found in comment'); + return; + } + + const commandConfig = commands[commandKey]; + const issue_number = context.issue.number; + + // Check user permissions for restricted commands + if (commandConfig.requiresWrite) { + if (debugOnly) { + console.log(`Would check permissions for user ${context.payload.comment.user.login} for ${commandKey}`); + } else { + try { + const { data: userPermission } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: context.payload.comment.user.login + }); + + const hasWriteAccess = ['admin', 'write', 'maintain'].includes(userPermission.permission); + + if (!hasWriteAccess) { + console.log(`User ${context.payload.comment.user.login} does not have permission for ${commandKey}`); + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: '-1' + }); + return; + } + console.log(`User ${context.payload.comment.user.login} has ${userPermission.permission} access`); + } catch (error) { + console.log('Error checking permissions:', error.message); + return; + } + } + } + + // Handle remove commands + if (commandConfig.action && commandConfig.action.startsWith('remove-')) { + const labelToRemove = commandConfig.action === 'remove-stale' ? 'lifecycle/stale' : 'lifecycle/frozen'; + + if (debugOnly) { + console.log(`Would remove ${labelToRemove} label from issue #${issue_number}`); + console.log('Would react with 👍 to comment'); + } else { + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue_number, + name: labelToRemove + }); + console.log(`Removed ${labelToRemove} label`); + + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: '+1' + }); + } catch (error) { + console.log(`Label ${labelToRemove} not found or already removed`); + } + } + } + // Handle add label commands + else if (commandConfig.label) { + if (debugOnly) { + console.log(`Would add ${commandConfig.label} label to issue #${issue_number}`); + console.log('Would react with 👍 to comment'); + } else { + try { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue_number, + labels: [commandConfig.label] + }); + console.log(`Added ${commandConfig.label} label`); + + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: '+1' + }); + } catch (error) { + console.log(`Error adding label: ${error.message}`); + } + } + } + stale: runs-on: ubuntu-latest + if: github.event_name == 'schedule' permissions: issues: write pull-requests: write @@ -21,16 +160,36 @@ jobs: operations-per-run: 30 # Exempt labels - issues/PRs with these labels will never be marked stale - exempt-issue-labels: 'kind/help wanted,status/need-more-info,status/needs-analysis' - exempt-pr-labels: 'kind/help wanted,status/need-more-info,status/needs-analysis' + exempt-issue-labels: 'kind/help wanted,status/need-more-info,status/needs-analysis,lifecycle/frozen' + exempt-pr-labels: 'kind/help wanted,status/need-more-info,status/needs-analysis,lifecycle/frozen' + + # Use lifecycle/stale label to match existing convention + stale-issue-label: 'lifecycle/stale' + stale-pr-label: 'lifecycle/stale' # Stale messages - stale-issue-message: "There hasn't been any activity on this issue for a long time. If the problem is still relevant, add a comment on this issue. If not, this issue will be closed in 14 days." - stale-pr-message: "Thanks for the PR. We'd like to make our product docs better, but haven't been able to review all the suggestions. As our docs change often and quickly diverge, we do not have the bandwidth to review and rebase old PRs. If the updates are still relevant, review our [contribution guidelines](https://docs.docker.com/contribute/overview/) and rebase your PR against the latest version of the docs. This helps our maintainers focus on the active issues. If there's no activity, this PR will be closed in 30 days." + stale-issue-message: | + There hasn't been any activity on this issue for a long time. If the problem is still relevant, **add a comment** to keep it open. Otherwise, this issue will be automatically closed in 14 days. + + **To remove the stale label:** Comment `/lifecycle active` + **To freeze (requires write access):** Comment `/lifecycle frozen` + stale-pr-message: | + Thanks for the PR. We'd like to make our product docs better, but haven't been able to review all the suggestions. As our docs change often and quickly diverge, we do not have the bandwidth to review and rebase old PRs. + + If the updates are still relevant, please **add a comment** and review our [contribution guidelines](https://docs.docker.com/contribute/overview/) to rebase your PR against the latest version of the docs. This helps our maintainers focus on active contributions. If there's no activity, this PR will be closed in 30 days. + + **To remove the stale label:** Comment `/lifecycle active` + **To freeze (requires write access):** Comment `/lifecycle frozen` # Close messages - close-issue-message: "Closing this issue as there hasn't been any activity on this issue for a long time. If the problem is still relevant, open a new issue and complete the issue template so we can capture the details required to investigate the issue further. This also helps our maintainers focus on the active issues." - close-pr-message: "Closing this PR as there hasn't been any activity on this PR for a long time. If the updates are still relevant, review our [contribution guidelines](https://docs.docker.com/contribute/overview/) and create a new PR against the latest version of our docs." + close-issue-message: | + Closing this issue as there hasn't been any activity for a long time. + + If the problem is still relevant, please **open a new issue** and complete the issue template so we can capture the details required to investigate further. This helps our maintainers focus on active issues. + close-pr-message: | + Closing this PR as there hasn't been any activity for a long time. + + If the updates are still relevant, please review our [contribution guidelines](https://docs.docker.com/contribute/overview/) and **create a new PR** against the latest version of our docs. # Timing configuration days-before-issue-stale: 180 # 6 months From 450ea902bb1fdfccc46f255a09afc7f929408e43 Mon Sep 17 00:00:00 2001 From: Craig Osterhout Date: Fri, 10 Apr 2026 08:52:23 -0700 Subject: [PATCH 3/4] pin sha Signed-off-by: Craig Osterhout --- .github/workflows/stale.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 8769f025fdb..fb218e345f4 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -2,6 +2,9 @@ # It also handles lifecycle slash commands for managing stale labels. # For more information, see: https://github.com/actions/stale # +# Security: Actions are pinned to full commit SHA to prevent supply chain attacks. +# To update, check releases and update both the SHA and version comment. +# # Debug mode: # - Lifecycle commands: Set DEBUG_ONLY to 'true' in the lifecycle-commands job env # - Stale action: Set debug-only to true in the stale job configuration @@ -23,7 +26,7 @@ jobs: steps: - name: Handle lifecycle commands - uses: actions/github-script@v7 + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 env: # Set to 'true' to test without making actual changes DEBUG_ONLY: 'true' @@ -153,9 +156,8 @@ jobs: pull-requests: write steps: - - uses: actions/stale@v10 + - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 with: - repo-token: ${{ secrets.GITHUB_TOKEN }} ascending: false operations-per-run: 30 From fc2717a510d078861c7757e848b49f765bbfa243 Mon Sep 17 00:00:00 2001 From: Craig Osterhout Date: Fri, 10 Apr 2026 09:03:34 -0700 Subject: [PATCH 4/4] comment about hardcoded dates Signed-off-by: Craig Osterhout --- .github/workflows/stale.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index fb218e345f4..5aab820989f 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -193,7 +193,9 @@ jobs: If the updates are still relevant, please review our [contribution guidelines](https://docs.docker.com/contribute/overview/) and **create a new PR** against the latest version of our docs. - # Timing configuration + # Timing configuration NOTE: If you change days-before-issue-close or + # days-before-pr-close, also update the hardcoded values in the + # stale-issue-message and stale-pr-message above to match. days-before-issue-stale: 180 # 6 months days-before-pr-stale: 180 # 6 months days-before-issue-close: 14 # 2 weeks after stale