Skip to content

Issue Milestone Hygiene #6699

Issue Milestone Hygiene

Issue Milestone Hygiene #6699

name: Issue Milestone Hygiene
on:
issues:
types:
- opened
- edited
- reopened
- labeled
- unlabeled
- milestoned
- demilestoned
schedule:
- cron: '25 * * * *'
workflow_dispatch:
inputs:
apply_default_milestone:
description: 'How to handle default milestone assignment'
required: false
default: inherit
type: choice
options:
- inherit
- apply
- disable
create_default_milestone:
description: 'How to handle default milestone creation'
required: false
default: inherit
type: choice
options:
- inherit
- apply
- disable
default_milestone:
description: 'Default milestone title override'
required: false
type: string
default_milestone_due_on:
description: 'ISO-8601 due date when creating default milestone'
required: false
type: string
warn_only:
description: 'Warn only (do not fail)'
required: false
default: false
type: boolean
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.issue.number || github.ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
issues: write
jobs:
issue-milestone-hygiene:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Evaluate milestone hygiene policy
shell: bash
env:
REPO_SLUG: ${{ github.repository }}
GITHUB_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN || github.token }}
GH_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN || github.token }}
APPLY_INPUT: ${{ inputs.apply_default_milestone }}
CREATE_INPUT: ${{ inputs.create_default_milestone }}
DEFAULT_INPUT: ${{ inputs.default_milestone }}
DUE_INPUT: ${{ inputs.default_milestone_due_on }}
WARN_INPUT: ${{ inputs.warn_only }}
APPLY_VAR: ${{ vars.MILESTONE_HYGIENE_APPLY_DEFAULT || '' }}
CREATE_VAR: ${{ vars.MILESTONE_HYGIENE_CREATE_DEFAULT || '' }}
DEFAULT_VAR: ${{ vars.MILESTONE_HYGIENE_DEFAULT_MILESTONE || '' }}
DUE_VAR: ${{ vars.MILESTONE_HYGIENE_DEFAULT_MILESTONE_DUE_ON || '' }}
WARN_VAR: ${{ vars.MILESTONE_HYGIENE_WARN_ONLY || '0' }}
run: |
set -euo pipefail
args=(
"tools/npm/run-script.mjs"
"priority:milestone:hygiene"
"--"
"--repo" "$REPO_SLUG"
"--report" "tests/results/_agent/issue/milestone-hygiene-report.json"
)
warn_effective="${WARN_INPUT:-}"
if [[ -z "$warn_effective" || "$warn_effective" == "false" ]]; then
warn_effective="${WARN_VAR:-0}"
fi
warn_effective="$(echo "$warn_effective" | tr '[:upper:]' '[:lower:]')"
if [[ "$warn_effective" == "1" || "$warn_effective" == "true" || "$warn_effective" == "yes" || "$warn_effective" == "on" ]]; then
args+=("--warn-only")
fi
default_effective="${DEFAULT_INPUT:-}"
if [[ -z "$default_effective" ]]; then
default_effective="${DEFAULT_VAR:-}"
fi
if [[ -n "$default_effective" ]]; then
args+=("--default-milestone" "$default_effective")
fi
due_effective="${DUE_INPUT:-}"
if [[ -z "$due_effective" ]]; then
due_effective="${DUE_VAR:-}"
fi
if [[ -n "$due_effective" ]]; then
args+=("--default-milestone-due-on" "$due_effective")
fi
apply_input_effective="$(echo "${APPLY_INPUT:-}" | tr '[:upper:]' '[:lower:]')"
apply_var_effective="$(echo "${APPLY_VAR:-}" | tr '[:upper:]' '[:lower:]')"
if [[ "$apply_input_effective" == "apply" || "$apply_input_effective" == "1" || "$apply_input_effective" == "true" || "$apply_input_effective" == "yes" || "$apply_input_effective" == "on" ]]; then
args+=("--apply-default-milestone")
elif [[ "$apply_input_effective" == "disable" || "$apply_input_effective" == "0" || "$apply_input_effective" == "false" || "$apply_input_effective" == "no" || "$apply_input_effective" == "off" ]]; then
args+=("--no-apply-default-milestone")
elif [[ "$apply_var_effective" == "1" || "$apply_var_effective" == "true" || "$apply_var_effective" == "yes" || "$apply_var_effective" == "on" ]]; then
args+=("--apply-default-milestone")
elif [[ "$apply_var_effective" == "0" || "$apply_var_effective" == "false" || "$apply_var_effective" == "no" || "$apply_var_effective" == "off" ]]; then
args+=("--no-apply-default-milestone")
fi
create_input_effective="$(echo "${CREATE_INPUT:-}" | tr '[:upper:]' '[:lower:]')"
create_var_effective="$(echo "${CREATE_VAR:-}" | tr '[:upper:]' '[:lower:]')"
if [[ "$create_input_effective" == "apply" || "$create_input_effective" == "1" || "$create_input_effective" == "true" || "$create_input_effective" == "yes" || "$create_input_effective" == "on" ]]; then
args+=("--create-default-milestone")
elif [[ "$create_input_effective" == "disable" || "$create_input_effective" == "0" || "$create_input_effective" == "false" || "$create_input_effective" == "no" || "$create_input_effective" == "off" ]]; then
args+=("--no-create-default-milestone")
elif [[ "$create_var_effective" == "1" || "$create_var_effective" == "true" || "$create_var_effective" == "yes" || "$create_var_effective" == "on" ]]; then
args+=("--create-default-milestone")
elif [[ "$create_var_effective" == "0" || "$create_var_effective" == "false" || "$create_var_effective" == "no" || "$create_var_effective" == "off" ]]; then
args+=("--no-create-default-milestone")
fi
node "${args[@]}"
- name: Append summary
if: always()
shell: bash
run: |
node <<'NODE'
const fs = require('fs');
const reportPath = 'tests/results/_agent/issue/milestone-hygiene-report.json';
if (!fs.existsSync(reportPath)) {
console.log('milestone hygiene report missing');
process.exit(0);
}
const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
const remaining = report.summary?.remainingViolationCount ?? 0;
const lines = [
'## Issue Milestone Hygiene',
`- execution status: \`${report.execution?.status ?? 'unknown'}\``,
`- remaining violations: \`${remaining}\``,
`- required issues evaluated: \`${report.summary?.requiredIssueCount ?? 0}\``,
`- auto-assigned this run: \`${report.summary?.assignedDefaultMilestoneCount ?? 0}\``,
`- failed assignments: \`${report.summary?.failedAssignmentsCount ?? 0}\``,
`- default milestone created this run: \`${report.milestones?.createdDefaultMilestone ?? false}\``
];
const errors = Array.isArray(report.execution?.errors) ? report.execution.errors : [];
if (errors.length > 0) {
lines.push(`- execution errors: \`${errors.length}\``);
}
if (process.env.GITHUB_STEP_SUMMARY) {
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${lines.join('\\n')}\\n`);
}
console.log(lines.join('\\n'));
NODE
- name: Upload milestone hygiene report
if: always()
uses: actions/upload-artifact@v7
with:
name: issue-milestone-hygiene-report
path: tests/results/_agent/issue/milestone-hygiene-report.json
if-no-files-found: error