Close stale fleetie-initiated issues. (#45530)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #45700 

Not a product change. This PR will allow us to run the workflow
manually. After ~2 weeks, if there are no issues, we'll make it
automatic.

## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added automated workflow to identify and close stale issues created by
Fleet team members, with dry-run capability and operation limits.
* Added system to build and maintain a deduplicated list of Fleet team
member handles from GitHub organization and repository history.

* **Tests**
* Added comprehensive test suites for stale issue management and handle
list generation with mock GitHub API interactions and boundary condition
coverage.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/fleetdm/fleet/pull/45530?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Victor Lyuboslavsky
2026-05-18 17:33:12 -05:00
committed by GitHub
parent 9dfe159d60
commit d051cf082b
6 changed files with 1545 additions and 0 deletions
@@ -0,0 +1,82 @@
name: Close stale Fleetie-initiated issues
# Marks open issues authored by current or former Fleeties as stale after 2 years of no activity, then
# closes them after 14 more days of inactivity once labeled stale. Activity (any comment or update) bumps
# `updated_at` and resets both clocks. Issues with the `bug`, `:product`, or any `customer-*` label are
# exempt.
#
# Why this isn't just `actions/stale`: that action filters by labels, not by author. We can't pre-tag
# issues with a "fleetie-initiated" label and hand off to actions/stale either, because adding a label
# bumps `updated_at` and resets the staleness clock for the entire backlog. The closest upstream PR is
# https://github.com/actions/stale/pull/1181 (`anyOfAuthors` allowlist input). It has been open with no
# review since October 2024. When it merges, we should be able to replace this whole flow with a small
# `actions/stale` config that passes the handle list from `build-fleetie-handles.js` as `anyOfAuthors`
# and delete `stale-fleetie-issues.js` and its tests.
on:
workflow_dispatch: # Manual
inputs:
dry_run:
description: 'If true, log candidates without writing labels, comments, or closing issues.'
type: boolean
default: true
max_operations:
description: 'Maximum GitHub API write operations per run. Each modified issue costs 2 writes (comment + label, or comment + close).'
type: number
default: 400
concurrency:
# Scope by event_name + ref so a pull_request trigger doesn't preempt an in-flight
# workflow_dispatch run, and vice versa. Same-event same-ref runs still cancel as expected.
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
shell: bash
permissions:
contents: read
jobs:
close-stale-fleetie-issues:
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Checkout repo
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
fetch-depth: 0
- name: Set up Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # 4.4.0
with:
node-version: '24'
- name: Build Fleetie handle list
env:
# Fine-scoped PAT with read:org on the fleetdm org. Provisioned by IT; without it the script
# falls back to handbook-only sources and emits a warning.
READ_ORG_TOKEN: ${{ secrets.FLEET_GITHUB_TOKEN_MEMBERS_READ }}
FLEETIE_HANDLES_OUT: ${{ runner.temp }}/fleeties.txt
run: node .github/scripts/build-fleetie-handles.js
- name: Stale and close Fleetie-authored issues
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
env:
FLEETIE_HANDLES_FILE: ${{ runner.temp }}/fleeties.txt
# Force dry-run for every event other than workflow_dispatch.
DRY_RUN: ${{ (github.event_name == 'workflow_dispatch' && inputs.dry_run == false) && 'false' || 'true' }}
# parseMaxOps in the script applies the 400 default when the value is empty.
MAX_OPERATIONS: ${{ inputs.max_operations }}
with:
script: |
const run = require('./.github/scripts/stale-fleetie-issues.js');
await run({ github, context, core });
@@ -0,0 +1,64 @@
name: Test stale Fleetie-issue scripts
# Runs the unit tests for the stale-Fleetie-issue scripts on PRs that change either script (or its
# tests), the closer workflow, or this workflow. Handbook edits are deliberately excluded: the
# scripts are resilient to any handle-shaped content the handbook can realistically contain
# (denylist, length cap, trailing-hyphen filter, format regex), and the handbook changes often
# enough that triggering on it would mostly burn CI minutes with no signal.
on:
push:
branches:
- main
paths:
- '.github/scripts/build-fleetie-handles.js'
- '.github/scripts/build-fleetie-handles.test.js'
- '.github/scripts/stale-fleetie-issues.js'
- '.github/scripts/stale-fleetie-issues.test.js'
- '.github/workflows/close-stale-fleetie-initiated-issues.yml'
- '.github/workflows/test-stale-fleetie-issue-scripts.yml'
pull_request:
paths:
- '.github/scripts/build-fleetie-handles.js'
- '.github/scripts/build-fleetie-handles.test.js'
- '.github/scripts/stale-fleetie-issues.js'
- '.github/scripts/stale-fleetie-issues.test.js'
- '.github/workflows/close-stale-fleetie-initiated-issues.yml'
- '.github/workflows/test-stale-fleetie-issue-scripts.yml'
workflow_dispatch: # Manual
# For PRs, github.head_ref groups by source branch. For pushes (e.g. main), head_ref is empty so
# we fall back to github.ref so successive pushes to the same branch share a group and
# cancellation actually applies.
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
permissions:
contents: read
defaults:
run:
shell: bash
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Checkout repo
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
fetch-depth: 0
- name: Set up Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # 4.4.0
with:
node-version: '24'
- name: Run tests
run: node --test .github/scripts/build-fleetie-handles.test.js .github/scripts/stale-fleetie-issues.test.js