feat(labels): estate label tooling + auto-triage for new issues #1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # SPDX-License-Identifier: MPL-2.0 | |
| name: Labels | |
| # Applies the canonical estate label set from .github/labels.json. | |
| # | |
| # Additive and idempotent by design: it CREATES missing labels and UPDATES | |
| # colour/description drift. It never deletes, and it never touches a label in | |
| # the `frozen` list -- those are applied by Dependabot / PR automation, or are | |
| # wired into triage.yml's exempt-issue-labels, and renaming them breaks things. | |
| # | |
| # jq is preinstalled on GitHub runners; PyYAML is not, which is why the payload | |
| # is JSON rather than YAML. | |
| # | |
| # ⚠ NO `uses:` ANYWHERE, DELIBERATELY. The estate enforces | |
| # .github/workflows/actions.lock, which is keyed BY WORKFLOW PATH: a workflow | |
| # the lock does not list is rejected before any step runs (startup_failure, and | |
| # therefore no check run at all). A dispatched workflow lands in repos whose | |
| # lock has not been regenerated, so it must not depend on any action. | |
| on: | |
| workflow_dispatch: | |
| push: | |
| paths: | |
| - '.github/labels.json' | |
| schedule: | |
| - cron: "23 4 1 * *" # monthly drift repair | |
| permissions: | |
| issues: write | |
| contents: read | |
| jobs: | |
| sync: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Apply canonical labels | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -uo pipefail | |
| work=$(mktemp -d); PAYLOAD=$work/labels.json | |
| # fetch instead of checking out -- no action means no lock entry to drift | |
| gh api "repos/$GITHUB_REPOSITORY/contents/.github/labels.json?ref=$GITHUB_SHA" \ | |
| --jq '.content' 2>/dev/null | base64 -d > "$PAYLOAD" || true | |
| [ -s "$PAYLOAD" ] || { echo "no .github/labels.json - nothing to do"; exit 0; } | |
| mapfile -t FROZEN < <(jq -r '.frozen[]' "$PAYLOAD") | |
| created=0; updated=0; skipped=0 | |
| existing=$(gh api "repos/$GITHUB_REPOSITORY/labels" --paginate \ | |
| --jq '.[] | [.name, .color, (.description // "")] | @tsv') | |
| while IFS=$'\t' read -r name color desc; do | |
| [ -z "$name" ] && continue | |
| frozen=0 | |
| for f in "${FROZEN[@]}"; do [ "$f" = "$name" ] && frozen=1 && break; done | |
| if [ "$frozen" -eq 1 ]; then skipped=$((skipped+1)); continue; fi | |
| cur=$(printf '%s\n' "$existing" | awk -F'\t' -v n="$name" '$1==n{print;exit}') | |
| if [ -z "$cur" ]; then | |
| gh label create "$name" --color "$color" --description "$desc" >/dev/null 2>&1 \ | |
| && created=$((created+1)) | |
| else | |
| ccol=$(cut -f2 <<<"$cur"); cdesc=$(cut -f3- <<<"$cur") | |
| if [ "${ccol,,}" != "${color,,}" ] || [ "$cdesc" != "$desc" ]; then | |
| gh label edit "$name" --color "$color" --description "$desc" >/dev/null 2>&1 \ | |
| && updated=$((updated+1)) | |
| fi | |
| fi | |
| sleep 0.4 | |
| done < <(jq -r '.labels[] | [.name, .color, .description] | @tsv' "$PAYLOAD") | |
| echo "created=$created updated=$updated frozen-skipped=$skipped" |