From f2b3add52017b75ef68eb331afc7c60a423825c8 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com> Date: Tue, 30 Dec 2025 17:47:19 -0600 Subject: [PATCH] Update repo_health.yml --- .github/workflows/repo_health.yml | 332 ++++++++++++++++++++++++++---- 1 file changed, 293 insertions(+), 39 deletions(-) diff --git a/.github/workflows/repo_health.yml b/.github/workflows/repo_health.yml index 6495d51..11e318c 100644 --- a/.github/workflows/repo_health.yml +++ b/.github/workflows/repo_health.yml @@ -76,6 +76,9 @@ env: REPO_DISALLOWED_DIRS: src REPO_DISALLOWED_FILES: TODO.md,todo.md + # Extended checks toggles + EXTENDED_CHECKS: "true" + # Operational toggles SFTP_VERBOSE: "false" @@ -167,7 +170,13 @@ jobs: esac if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then - printf '%s\n' "Profile ${profile} selected. Skipping release configuration checks." >> "${GITHUB_STEP_SUMMARY}" + { + printf '%s\n' '### Release configuration' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' 'Status: SKIPPED' + printf '%s\n' 'Reason: profile excludes release validation' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" exit 0 fi @@ -197,12 +206,39 @@ jobs: [ "${ok}" = false ] && missing+=("FTP_PROTOCOL_INVALID") fi + target_path="${FTP_PATH:-}" + if [ -n "${FTP_PATH_SUFFIX:-}" ]; then + target_path="${target_path%/}/${FTP_PATH_SUFFIX#/}" + fi + + auth_method='none' + [ -n "${FTP_KEY:-}" ] && auth_method='key' + [ -z "${FTP_KEY:-}" ] && [ -n "${FTP_PASSWORD:-}" ] && auth_method='password' + + { + printf '%s\n' '### Release configuration' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' '| Control | Value |' + printf '%s\n' '|---|---|' + printf '%s\n' "| Protocol | ${proto} |" + printf '%s\n' "| Port | ${FTP_PORT:-22} |" + printf '%s\n' "| Auth | ${auth_method} |" + printf '%s\n' "| Path (resolved) | ${target_path} |" + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + if [ "${#missing_optional[@]}" -gt 0 ]; then { printf '%s\n' '### Missing optional release configuration' for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done printf '\n' } >> "${GITHUB_STEP_SUMMARY}" + else + { + printf '%s\n' '### Optional release configuration' + printf '%s\n' 'None missing.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" fi if [ "${#missing[@]}" -gt 0 ]; then @@ -215,7 +251,8 @@ jobs: fi { - printf '%s\n' '### Guardrails release configuration' + printf '%s\n' '### Release configuration result' + printf '%s\n' 'Status: OK' printf '%s\n' 'All required release variables present.' printf '\n' } >> "${GITHUB_STEP_SUMMARY}" @@ -243,7 +280,13 @@ jobs: esac if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then - printf '%s\n' "Profile ${profile} selected. Skipping SFTP connectivity check." >> "${GITHUB_STEP_SUMMARY}" + { + printf '%s\n' '### SFTP connectivity' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' 'Status: SKIPPED' + printf '%s\n' 'Reason: profile excludes release validation' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" exit 0 fi @@ -258,10 +301,22 @@ jobs: sftp_v_opt=() [ "${sftp_verbose}" = 'true' ] && sftp_v_opt=(-vv) + auth_method='none' + if [ -n "${FTP_KEY:-}" ]; then + auth_method='key' + elif [ -n "${FTP_PASSWORD:-}" ]; then + auth_method='password' + fi + { - printf '%s\n' '### SFTP connectivity test' - printf '%s\n' "Target path: ${target_path}" - printf '%s\n' 'Attempting non-destructive SFTP session' + printf '%s\n' '### SFTP connectivity' + printf '%s\n' '| Control | Value |' + printf '%s\n' '|---|---|' + printf '%s\n' "| Host | ${FTP_HOST} |" + printf '%s\n' "| User | ${FTP_USER} |" + printf '%s\n' "| Port | ${port} |" + printf '%s\n' "| Auth | ${auth_method} |" + printf '%s\n' "| Path (resolved) | ${target_path} |" printf '\n' } >> "${GITHUB_STEP_SUMMARY}" @@ -277,7 +332,12 @@ jobs: if [ -n "${FTP_PASSWORD:-}" ]; then first_line="$(head -n 1 "${key_file}" || true)" if printf '%s\n' "${first_line}" | grep -q '^PuTTY-User-Key-File-'; then - printf '%s\n' 'ERROR: FTP_KEY appears to be a PuTTY PPK. Provide an OpenSSH private key.' >> "${GITHUB_STEP_SUMMARY}" + { + printf '%s\n' '### SFTP connectivity result' + printf '%s\n' 'Status: FAILED' + printf '%s\n' 'Reason: FTP_KEY appears to be a PuTTY PPK. Provide an OpenSSH private key.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" exit 1 fi ssh-keygen -p -P "${FTP_PASSWORD}" -N '' -f "${key_file}" >/dev/null @@ -290,24 +350,31 @@ jobs: printf '%s' "${sftp_cmds}" | sshpass -p "${FTP_PASSWORD}" sftp "${sftp_v_opt[@]}" -oBatchMode=no -oStrictHostKeyChecking=no -P "${port}" "${FTP_USER}@${FTP_HOST}" >/tmp/sftp_check.log 2>&1 sftp_rc=$? else - printf '%s\n' 'ERROR: No FTP_KEY or FTP_PASSWORD provided for SFTP authentication.' >> "${GITHUB_STEP_SUMMARY}" + { + printf '%s\n' '### SFTP connectivity result' + printf '%s\n' 'Status: FAILED' + printf '%s\n' 'Reason: No FTP_KEY or FTP_PASSWORD provided for SFTP authentication.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" exit 1 fi set -e - printf '%s\n' '### SFTP connectivity result' >> "${GITHUB_STEP_SUMMARY}" - if [ "${sftp_rc}" -eq 0 ]; then - printf '%s\n' 'Status: SUCCESS' >> "${GITHUB_STEP_SUMMARY}" - exit 0 - fi - { - printf '%s\n' "Status: FAILED (exit code ${sftp_rc})" + printf '%s\n' '### SFTP connectivity result' + if [ "${sftp_rc}" -eq 0 ]; then + printf '%s\n' 'Status: SUCCESS' + printf '%s\n' 'Validated host connectivity and remote path access.' + else + printf '%s\n' "Status: FAILED (exit code ${sftp_rc})" + printf '\n' + printf '%s\n' 'Last SFTP output' + tail -n 60 /tmp/sftp_check.log || true + fi printf '\n' - printf '%s\n' 'Last SFTP output' - tail -n 60 /tmp/sftp_check.log || true } >> "${GITHUB_STEP_SUMMARY}" - exit 1 + + [ "${sftp_rc}" -eq 0 ] || exit 1 scripts_governance: name: Scripts governance @@ -340,14 +407,21 @@ jobs: esac if [ "${profile}" = 'release' ] || [ "${profile}" = 'repo' ]; then - printf '%s\n' "Profile ${profile} selected. Skipping scripts governance." >> "${GITHUB_STEP_SUMMARY}" + { + printf '%s\n' '### Scripts governance' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' 'Status: SKIPPED' + printf '%s\n' 'Reason: profile excludes scripts governance' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" exit 0 fi if [ ! -d scripts ]; then { printf '%s\n' '### Scripts governance' - printf '%s\n' 'Warning: scripts/ directory not present. No scripts governance enforced.' + printf '%s\n' 'Status: OK (advisory)' + printf '%s\n' 'scripts/ directory not present. No scripts governance enforced.' printf '\n' } >> "${GITHUB_STEP_SUMMARY}" exit 0 @@ -375,34 +449,45 @@ jobs: { printf '%s\n' '### Scripts governance' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' '| Area | Status | Notes |' + printf '%s\n' '|---|---|---|' + + if [ "${#missing_dirs[@]}" -gt 0 ]; then + printf '%s\n' '| Required directories | Warning | Missing required subfolders |' + else + printf '%s\n' '| Required directories | OK | All required subfolders present |' + fi + + if [ "${#unapproved_dirs[@]}" -gt 0 ]; then + printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |' + else + printf '%s\n' '| Directory policy | OK | No unapproved directories |' + fi + + printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |' + printf '\n' if [ "${#missing_dirs[@]}" -gt 0 ]; then printf '%s\n' 'Missing required script directories:' for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done printf '\n' + else + printf '%s\n' 'Missing required script directories: none.' + printf '\n' fi if [ "${#unapproved_dirs[@]}" -gt 0 ]; then printf '%s\n' 'Unapproved script directories detected:' for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done printf '\n' + else + printf '%s\n' 'Unapproved script directories detected: none.' + printf '\n' fi - printf '%s\n' '| Area | Status | Notes |' - printf '%s\n' '|------|--------|-------|' - if [ "${#missing_dirs[@]}" -gt 0 ]; then - printf '%s\n' '| Required directories | Warning | Missing required subfolders |' - else - printf '%s\n' '| Required directories | OK | All required subfolders present |' - fi - if [ "${#unapproved_dirs[@]}" -gt 0 ]; then - printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |' - else - printf '%s\n' '| Directory policy | OK | No unapproved directories |' - fi - printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |' - printf '\n' printf '%s\n' 'Scripts governance completed in advisory mode.' + printf '\n' } >> "${GITHUB_STEP_SUMMARY}" repo_health: @@ -410,7 +495,7 @@ jobs: needs: access_check if: ${{ needs.access_check.outputs.allowed == 'true' }} runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 20 permissions: contents: read @@ -436,7 +521,13 @@ jobs: esac if [ "${profile}" = 'release' ] || [ "${profile}" = 'scripts' ]; then - printf '%s\n' "Profile ${profile} selected. Skipping repository health checks." >> "${GITHUB_STEP_SUMMARY}" + { + printf '%s\n' '### Repository health' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' 'Status: SKIPPED' + printf '%s\n' 'Reason: profile excludes repository health' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" exit 0 fi @@ -498,6 +589,10 @@ jobs: content_warnings+=("CHANGELOG.md missing '# Changelog' header") fi + if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then + content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)") + fi + if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then content_warnings+=("LICENSE does not look like a GPL text") fi @@ -533,8 +628,15 @@ jobs: )" { - printf '%s\n' '### Guardrails repository health' + printf '%s\n' '### Repository health' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' '| Metric | Value |' + printf '%s\n' '|---|---|' + printf '%s\n' "| Missing required | ${#missing_required[@]} |" + printf '%s\n' "| Missing optional | ${#missing_optional[@]} |" + printf '%s\n' "| Content warnings | ${#content_warnings[@]} |" printf '\n' + printf '%s\n' '### Guardrails report (JSON)' printf '%s\n' '```json' printf '%s\n' "${report_json}" @@ -547,6 +649,7 @@ jobs: printf '%s\n' '### Missing required repo artifacts' for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.' + printf '\n' } >> "${GITHUB_STEP_SUMMARY}" exit 1 fi @@ -567,6 +670,157 @@ jobs: } >> "${GITHUB_STEP_SUMMARY}" fi - printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}" + extended_enabled="${EXTENDED_CHECKS:-true}" + extended_findings=() -# EOF + if [ "${extended_enabled}" = 'true' ]; then + # CODEOWNERS presence + if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then + : + else + extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)") + fi + + # Workflow pinning advisory: flag uses @main/@master + if ls .github/workflows/*.yml >/dev/null 2>&1; then + bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' .github/workflows 2>/dev/null || true)" + if [ -n "${bad_refs}" ]; then + extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt") + { + printf '%s\n' '### Workflow pinning advisory' + printf '%s\n' 'Found uses: entries pinned to main/master:' + printf '%s\n' '```' + printf '%s\n' "${bad_refs}" + printf '%s\n' '```' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + # Docs index link integrity (docs/docs-index.md) + if [ -f 'docs/docs-index.md' ]; then + missing_links="$(python3 - <<'PY' +import os +import re + +idx = 'docs/docs-index.md' +base = os.getcwd() + +bad = [] +pat = re.compile(r'\[[^\]]+\]\(([^)]+)\)') + +with open(idx, 'r', encoding='utf-8') as f: + for line in f: + for m in pat.findall(line): + link = m.strip() + if link.startswith('http://') or link.startswith('https://') or link.startswith('#') or link.startswith('mailto:'): + continue + if link.startswith('/'): + rel = link.lstrip('/') + else: + rel = os.path.normpath(os.path.join(os.path.dirname(idx), link)) + rel = rel.split('#', 1)[0] + rel = rel.split('?', 1)[0] + if not rel: + continue + p = os.path.join(base, rel) + if not os.path.exists(p): + bad.append(rel) + +print('\n'.join(sorted(set(bad)))) +PY +)" + if [ -n "${missing_links}" ]; then + extended_findings+=("docs/docs-index.md contains broken relative links") + { + printf '%s\n' '### Docs index link integrity' + printf '%s\n' 'Broken relative links:' + while IFS= read -r l; do [ -n "${l}" ] && printf '%s\n' "- ${l}"; done <<< "${missing_links}" + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + # ShellCheck advisory + if [ -d 'scripts' ]; then + if ! command -v shellcheck >/dev/null 2>&1; then + sudo apt-get update -qq + sudo apt-get install -y shellcheck >/dev/null + fi + sc_out="$(shellcheck -S warning -x scripts/**/*.sh 2>/dev/null || true)" + if [ -n "${sc_out}" ]; then + extended_findings+=("ShellCheck warnings detected (advisory)") + { + printf '%s\n' '### ShellCheck (advisory)' + printf '%s\n' '```' + printf '%s\n' "${sc_out}" | head -n 200 + printf '%s\n' '```' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + # SPDX header advisory for common source types + spdx_missing=() + while IFS= read -r f; do + [ -z "${f}" ] && continue + if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then + spdx_missing+=("${f}") + fi + done < <(git ls-files '*.sh' '*.php' '*.js' '*.ts' '*.css' '*.xml' '*.yml' '*.yaml' 2>/dev/null || true) + + if [ "${#spdx_missing[@]}" -gt 0 ]; then + extended_findings+=("SPDX header missing in some tracked files (advisory)") + { + printf '%s\n' '### SPDX header advisory' + printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):' + for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + # Git hygiene advisory: branches older than 180 days (remote) + stale_cutoff_days=180 + stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 > days*86400) print $1}' | sed 's#^origin/##' | grep -v '^HEAD$' | head -n 50 || true)" + if [ -n "${stale_branches}" ]; then + extended_findings+=("Stale remote branches detected (advisory)") + { + printf '%s\n' '### Git hygiene advisory' + printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):" + while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}" + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + { + printf '%s\n' '### Guardrails coverage matrix' + printf '%s\n' '| Domain | Status | Notes |' + printf '%s\n' '|---|---|---|' + printf '%s\n' '| Access control | OK | Admin-only execution gate |' + printf '%s\n' '| Release configuration | OK | Variable presence and protocol gate |' + printf '%s\n' '| SFTP connectivity | OK | Connectivity plus remote path resolution |' + printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |' + printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |' + printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |' + if [ "${extended_enabled}" = 'true' ]; then + if [ "${#extended_findings[@]}" -gt 0 ]; then + printf '%s\n' '| Extended checks | Warning | See extended findings below |' + else + printf '%s\n' '| Extended checks | OK | No findings |' + fi + else + printf '%s\n' '| Extended checks | SKIPPED | EXTENDED_CHECKS disabled |' + fi + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + if [ "${extended_enabled}" = 'true' ] && [ "${#extended_findings[@]}" -gt 0 ]; then + { + printf '%s\n' '### Extended findings (advisory)' + for f in "${extended_findings[@]}"; do printf '%s\n' "- ${f}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}"