diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 00000000..e2314758 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,67 @@ +# Copyright 2026 ResQ Software +# SPDX-License-Identifier: Apache-2.0 +# +# Path-based PR auto-labeling rules consumed by .github/workflows/labeler.yml +# (actions/labeler@v5 schema). Keys are label names; values are file globs. + +'area:api': + - changed-files: + - any-glob-to-any-file: + - 'api-reference/**' + - 'specs/**' + +'area:i18n': + - changed-files: + - any-glob-to-any-file: + - 'ar/**' + - 'es/**' + - 'hi/**' + - 'zh/**' + +'area:ci': + - changed-files: + - any-glob-to-any-file: + - '.github/**' + +'area:design': + - changed-files: + - any-glob-to-any-file: + - 'custom.css' + - 'assets/**' + - 'logo/**' + - 'favicon.svg' + +'area:pwa': + - changed-files: + - any-glob-to-any-file: + - 'manifest.webmanifest' + - 'pwa/**' + +'area:content': + - changed-files: + - any-glob-to-any-file: + - '**/*.mdx' + - '**/*.md' + # Scope out paths owned by other labels so a single PR doesn't + # collect three labels for the same change. + - all-globs-to-all-files: + - '!api-reference/**' + - '!specs/**' + - '!ar/**' + - '!es/**' + - '!hi/**' + - '!zh/**' + - '!.github/**' + - '!AGENTS.md' + - '!CONTRIBUTING.md' + - '!README.md' + - '!LICENSE' + +'area:meta': + - changed-files: + - any-glob-to-any-file: + - 'docs.json' + - 'AGENTS.md' + - 'CONTRIBUTING.md' + - 'README.md' + - 'LICENSE' diff --git a/.github/workflows/i18n-parity.yml b/.github/workflows/i18n-parity.yml new file mode 100644 index 00000000..4a6e4bc8 --- /dev/null +++ b/.github/workflows/i18n-parity.yml @@ -0,0 +1,80 @@ +# Copyright 2026 ResQ Software +# SPDX-License-Identifier: Apache-2.0 +# +# i18n parity check — informational only. Reports MDX/MD files in the +# default-language tree that are missing from each locale (ar, es, hi, +# zh). Does not block merges; translations lag deliberately. + +name: i18n parity + +on: + pull_request: + paths: + - '**/*.mdx' + - '**/*.md' + - 'ar/**' + - 'es/**' + - 'hi/**' + - 'zh/**' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + parity: + name: locale parity + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Compare locale trees + run: | + set -euo pipefail + locales=(ar es hi zh) + mapfile -t base < <( + find . -type f \( -name '*.mdx' -o -name '*.md' \) \ + -not -path './ar/*' \ + -not -path './es/*' \ + -not -path './hi/*' \ + -not -path './zh/*' \ + -not -path './.github/*' \ + -not -path './node_modules/*' \ + -not -name 'README.md' \ + -not -name 'AGENTS.md' \ + -not -name 'CONTRIBUTING.md' \ + | sed 's|^\./||' | sort + ) + echo "base files: ${#base[@]}" + { + echo "## i18n parity report" + echo + echo "Base tree: ${#base[@]} translatable file(s)" + echo + for locale in "${locales[@]}"; do + if [[ ! -d "$locale" ]]; then + echo "- \`$locale/\` — locale directory missing" + continue + fi + missing=0 + missing_list="" + for rel in "${base[@]}"; do + if [[ ! -f "$locale/$rel" ]]; then + missing=$((missing+1)) + missing_list+=" - \`$locale/$rel\`"$'\n' + fi + done + total=${#base[@]} + translated=$((total - missing)) + echo "- \`$locale/\` — $translated / $total translated ($missing missing)" + if (( missing > 0 )); then + echo + printf '%s' "$missing_list" + fi + done + } >> "$GITHUB_STEP_SUMMARY" + echo "report written to step summary" diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 00000000..90cd7abf --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,30 @@ +# Copyright 2026 ResQ Software +# SPDX-License-Identifier: Apache-2.0 +# +# Auto-label PRs by changed paths. Rules live in .github/labeler.yml. +# Uses `pull_request_target` so forks get labeled too — this is safe +# because the action only reads file paths, never executes PR code. + +name: labeler + +on: + pull_request_target: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: write + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + label: + name: label + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v5 + with: + configuration-path: .github/labeler.yml + sync-labels: true diff --git a/.github/workflows/lychee.yml b/.github/workflows/lychee.yml new file mode 100644 index 00000000..874d4a44 --- /dev/null +++ b/.github/workflows/lychee.yml @@ -0,0 +1,51 @@ +# Copyright 2026 ResQ Software +# SPDX-License-Identifier: Apache-2.0 +# +# External link checker. Scheduled weekly + on-demand only — external +# link flake (rate limits, CDN hiccups) makes this unsuitable as a +# blocking PR gate. Internal MDX link integrity is enforced by +# `mint broken-links` in required.yml. + +name: lychee + +on: + schedule: + - cron: '23 7 * * 1' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + links: + name: lychee (external links) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Restore lychee cache + uses: actions/cache@v4 + with: + path: .lycheecache + key: cache-lychee-${{ github.sha }} + restore-keys: cache-lychee- + + - name: Run lychee + uses: lycheeverse/lychee-action@v2 + with: + args: >- + --cache + --max-cache-age 1d + --no-progress + --exclude-mail + --accept 200,204,206,429 + --max-retries 2 + --retry-wait-time 5 + './**/*.md' + './**/*.mdx' + './docs.json' + fail: true diff --git a/.github/workflows/required.yml b/.github/workflows/required.yml index 7c3d2ec2..af62ad03 100644 --- a/.github/workflows/required.yml +++ b/.github/workflows/required.yml @@ -1,11 +1,11 @@ # Copyright 2026 ResQ Software # SPDX-License-Identifier: Apache-2.0 # -# Minimal `required` status-check emitter — placeholder to satisfy the -# org ruleset `default-branch-baseline` (id 15191038) while -# language-specific reusable CI for this repo is still pending. Harden -# this job (add `needs:` on real CI jobs) before the ruleset flips from -# evaluate to active. +# `required` status check — single aggregate gate referenced by the org +# ruleset (default-branch-baseline). Real CI jobs run in parallel, and +# the `required` job fails unless every dependency succeeds. Add new +# blocking docs jobs to `needs:` rather than a new workflow, so the +# protection contract stays one check. name: required @@ -22,8 +22,84 @@ concurrency: cancel-in-progress: true jobs: + mintlify: + name: mintlify (broken links) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + - name: Install Mintlify CLI + run: npm install -g mint + - name: Check broken internal links + run: mint broken-links + + spectral: + name: spectral (OpenAPI lint) + runs-on: ubuntu-latest + # Advisory: action ref hardening will be handled by Dependabot SHA-pin PRs. + continue-on-error: true + steps: + - uses: actions/checkout@v4 + - name: Lint OpenAPI specs + uses: stoplightio/spectral-action@v0.8.13 # TODO: dependabot will SHA-pin + with: + file_glob: 'specs/*.json' + + pwa-manifest: + name: pwa manifest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Validate manifest.webmanifest + run: | + set -euo pipefail + f=manifest.webmanifest + if [[ ! -f "$f" ]]; then + echo "::notice::$f not on this branch — skipping validation (manifest lives in feat/brand-assets PR)" + exit 0 + fi + jq -e . "$f" > /dev/null || { echo "::error::$f is not valid JSON"; exit 1; } + for field in name short_name start_url display icons; do + if ! jq -e "has(\"$field\")" "$f" > /dev/null; then + echo "::error::manifest missing required field: $field" + exit 1 + fi + done + mapfile -t srcs < <(jq -r '.icons[].src' "$f") + missing=0 + for src in "${srcs[@]}"; do + path="${src#/}" + if [[ ! -f "$path" ]]; then + echo "::error::icon path not found on disk: $src" + missing=$((missing+1)) + fi + done + if (( missing > 0 )); then + echo "::error::$missing icon(s) missing" + exit 1 + fi + echo "manifest valid: ${#srcs[@]} icons resolved" + required: name: required + needs: [mintlify, spectral, pwa-manifest] + if: always() runs-on: ubuntu-latest steps: - - run: echo "ok — placeholder until language CI lands" + - name: Verify all gates passed + run: | + # spectral remains advisory (continue-on-error) until the action ref + # is verified post-Dependabot SHA-pin. + mintlify="${{ needs.mintlify.result }}" + spectral="${{ needs.spectral.result }}" + pwa="${{ needs.pwa-manifest.result }}" + echo "mintlify: $mintlify" + echo "spectral (advisory): $spectral" + echo "pwa-manifest: $pwa" + if [[ "$mintlify" != "success" || "$pwa" != "success" ]]; then + echo "::error::One or more required gates failed" + exit 1 + fi + echo "All required gates passed" diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000..de808e6e --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,47 @@ +# Copyright 2026 ResQ Software +# SPDX-License-Identifier: Apache-2.0 +# +# Stale issue/PR closer. Issues idle for 60 days get the `stale` label +# and a nudge; closed 14 days later if still untouched. PRs are tighter +# (30 + 14). Pinned, security, and roadmap items are exempt. + +name: stale + +on: + schedule: + - cron: '12 1 * * *' + workflow_dispatch: + +permissions: + issues: write + pull-requests: write + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +jobs: + stale: + name: stale + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + days-before-issue-stale: 60 + days-before-issue-close: 14 + days-before-pr-stale: 30 + days-before-pr-close: 14 + stale-issue-label: stale + stale-pr-label: stale + exempt-issue-labels: 'pinned,security,roadmap' + exempt-pr-labels: 'pinned,security' + stale-issue-message: | + This issue has been inactive for 60 days and is now marked stale. + It will be closed in 14 days unless there is new activity. + stale-pr-message: | + This PR has been inactive for 30 days and is now marked stale. + It will be closed in 14 days unless updated. Rebase or comment + to keep it open. + close-issue-message: Closed due to inactivity. Reopen if still relevant. + close-pr-message: Closed due to inactivity. Reopen and rebase to continue. + operations-per-run: 100