From 21a3021ad5d0a318a57a2df5adc9536779084e60 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com> Date: Sat, 27 Dec 2025 02:33:50 -0600 Subject: [PATCH] Update repo_health.yml --- .github/workflows/repo_health.yml | 275 ++++++++++++++++++++++-------- 1 file changed, 205 insertions(+), 70 deletions(-) diff --git a/.github/workflows/repo_health.yml b/.github/workflows/repo_health.yml index 78df145..1ea607a 100644 --- a/.github/workflows/repo_health.yml +++ b/.github/workflows/repo_health.yml @@ -29,6 +29,14 @@ name: Joomla Repo Health +concurrency: + group: repo-health-${{ github.repository }}-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + on: workflow_dispatch: inputs: @@ -54,6 +62,7 @@ permissions: jobs: access_check: + timeout-minutes: 10 name: Access control runs-on: ubuntu-latest permissions: @@ -104,6 +113,7 @@ jobs: exit 1 release_config: + timeout-minutes: 20 name: Release configuration runs-on: ubuntu-latest needs: [access_check] @@ -206,7 +216,7 @@ jobs: mkdir -p "$HOME/.ssh" key_file="$HOME/.ssh/ci_sftp_key" - printf '%s\\n' "${FTP_KEY}" > "${key_file}" + printf '%s\n' "${FTP_KEY}" > "${key_file}" chmod 600 "${key_file}" # If FTP_PASSWORD is present, treat it as the private key passphrase and decrypt the key in place. @@ -226,13 +236,25 @@ jobs: echo "### SFTP connectivity test" >> "${GITHUB_STEP_SUMMARY}" echo "Attempting non-destructive SFTP session (pwd only)." >> "${GITHUB_STEP_SUMMARY}" - printf 'pwd\\nbye\\n' | sftp -oBatchMode=yes -oStrictHostKeyChecking=no -P "${port}" -i "${key_file}" "${FTP_USER}@${FTP_HOST}" + printf 'pwd\nbye\n' | sftp -oBatchMode=yes -oStrictHostKeyChecking=no -P "${port}" -i "${key_file}" "${FTP_USER}@${FTP_HOST}" >/tmp/sftp_check.log 2>&1 + sftp_rc=$? - echo "SFTP connectivity check passed." >> "${GITHUB_STEP_SUMMARY}" + if [ "${sftp_rc}" -eq 0 ]; then + echo "### SFTP connectivity result" >> "${GITHUB_STEP_SUMMARY}" + echo "Status: SUCCESS" >> "${GITHUB_STEP_SUMMARY}" + else + echo "### SFTP connectivity result" >> "${GITHUB_STEP_SUMMARY}" + echo "Status: FAILED (exit code ${sftp_rc})" >> "${GITHUB_STEP_SUMMARY}" + echo "" >> "${GITHUB_STEP_SUMMARY}" + echo "Last SFTP output:" >> "${GITHUB_STEP_SUMMARY}" + tail -n 10 /tmp/sftp_check.log >> "${GITHUB_STEP_SUMMARY}" + exit 1 + fi scripts_config: name: Scripts and tooling runs-on: ubuntu-latest + timeout-minutes: 15 needs: [access_check] if: ${{ needs.access_check.outputs.allowed == 'true' }} permissions: @@ -325,49 +347,124 @@ jobs: command -v xmllint >/dev/null 2>&1 && tool_status+=("xmllint") || true command -v shellcheck >/dev/null 2>&1 && tool_status+=("shellcheck") || true - export MISSING_DIRS="$(printf '%s\n' "${missing_dirs[@]:-}")" - export MISSING_FILES="$(printf '%s\n' "${missing_files[@]:-}")" + export MISSING_DIRS="$(printf '%s +' "${missing_dirs[@]:-}")" + export MISSING_FILES="$(printf '%s +' "${missing_files[@]:-}")" export TOOLS="${tool_status[*]:-}" report_json="$(python3 - <<'PY' - import json - import os - profile = os.environ.get('PROFILE_RAW') or 'all' - required_script_dirs = [ - "scripts/fix", - "scripts/lib", - "scripts/release", - "scripts/run", - "scripts/validate", - ] - required_script_files = [ - "scripts/validate/manifest.sh", - "scripts/validate/xml_wellformed.sh", - "scripts/validate/changelog.sh", - "scripts/validate/tabs.sh", - "scripts/validate/paths.sh", - "scripts/validate/version_alignment.sh", - "scripts/validate/language_structure.sh", - "scripts/validate/php_syntax.sh", - "scripts/validate/no_secrets.sh", - "scripts/validate/license_headers.sh", - ] - missing_dirs = os.environ.get('MISSING_DIRS','').split('\n') if os.environ.get('MISSING_DIRS') else [] - missing_files = os.environ.get('MISSING_FILES','').split('\n') if os.environ.get('MISSING_FILES') else [] - tools = os.environ.get('TOOLS','').split() if os.environ.get('TOOLS') else [] - out = { - "profile": profile, - "checked": { - "required_script_dirs": required_script_dirs, - "required_script_files": required_script_files, - }, - "missing_dirs": [x for x in missing_dirs if x], - "missing_files": [x for x in missing_files if x], - "tools_available": tools, - } - print(json.dumps(out, indent=2)) - PY - )" +import json +import os + +profile = os.environ.get('PROFILE_RAW') or 'all' +required_script_dirs = [ + "scripts/fix", + "scripts/lib", + "scripts/release", + "scripts/run", + "scripts/validate", +] +required_script_files = [ + "scripts/validate/manifest.sh", + "scripts/validate/xml_wellformed.sh", + "scripts/validate/changelog.sh", + "scripts/validate/tabs.sh", + "scripts/validate/paths.sh", + "scripts/validate/version_alignment.sh", + "scripts/validate/language_structure.sh", + "scripts/validate/php_syntax.sh", + "scripts/validate/no_secrets.sh", + "scripts/validate/license_headers.sh", +] + +missing_dirs = os.environ.get('MISSING_DIRS','').split(' +') if os.environ.get('MISSING_DIRS') else [] +missing_files = os.environ.get('MISSING_FILES','').split(' +') if os.environ.get('MISSING_FILES') else [] +tools = os.environ.get('TOOLS','').split() if os.environ.get('TOOLS') else [] + +out = { + "profile": profile, + "checked": { + "required_script_dirs": required_script_dirs, + "required_script_files": required_script_files, + }, + "missing_dirs": [x for x in missing_dirs if x], + "missing_files": [x for x in missing_files if x], + "tools_available": tools, +} + +print(json.dumps(out, indent=2)) +PY +)" + + { + echo "### Guardrails: scripts and tooling" + echo "Tools available: ${tool_status[*]:-none}" + echo "" + echo "### Guardrails report (JSON)" + echo "```json" + echo "${report_json}" + echo "```" + } >> "${GITHUB_STEP_SUMMARY}" + + if [ "${#missing_dirs[@]}" -gt 0 ]; then + echo "### Missing required script directories" >> "${GITHUB_STEP_SUMMARY}" + for m in "${missing_dirs[@]}"; do echo "- ${m}" >> "${GITHUB_STEP_SUMMARY}"; done + echo "ERROR: Guardrails failed. Missing required script directories." >> "${GITHUB_STEP_SUMMARY}" + exit 1 + fi + + if [ "${#unapproved_dirs[@]}" -gt 0 ]; then + echo "### Unapproved script directories detected" >> "${GITHUB_STEP_SUMMARY}" + for m in "${unapproved_dirs[@]}"; do echo "- ${m}" >> "${GITHUB_STEP_SUMMARY}"; done + echo "ERROR: Guardrails failed. Only fix, lib, release, run, validate directories are allowed under scripts/." >> "${GITHUB_STEP_SUMMARY}" + exit 1 + fi + + if [ "${#missing_files[@]}" -gt 0 ]; then + echo "### Missing script files" >> "${GITHUB_STEP_SUMMARY}" + for m in "${missing_files[@]}"; do echo "- ${m}" >> "${GITHUB_STEP_SUMMARY}"; done + echo "ERROR: Guardrails failed. Missing required script files." >> "${GITHUB_STEP_SUMMARY}" + exit 1 + fi + + if [ "${#legacy_glob_found[@]}" -gt 0 ]; then + echo "### Legacy validate_* scripts detected at scripts/ root (disallowed)" >> "${GITHUB_STEP_SUMMARY}" + for m in "${legacy_glob_found[@]}"; do echo "- ${m}" >> "${GITHUB_STEP_SUMMARY}"; done + echo "ERROR: Guardrails failed. Move scripts into scripts/validate/ with approved filenames." >> "${GITHUB_STEP_SUMMARY}" + exit 1 + fi + + non_exec=() + while IFS= read -r f; do + [ -n "${f}" ] && non_exec+=("${f}") + done < <(find scripts -type f -name '*.sh' ! -perm -u=x 2>/dev/null || true) + + if [ "${#non_exec[@]}" -gt 0 ]; then + echo "### Non-executable shell scripts detected" >> "${GITHUB_STEP_SUMMARY}" + for m in "${non_exec[@]}"; do echo "- ${m}" >> "${GITHUB_STEP_SUMMARY}"; done + echo "ERROR: Guardrails failed. All scripts/**/*.sh must be executable." >> "${GITHUB_STEP_SUMMARY}" + exit 1 + fi + + sh_files="$(find scripts -type f -name '*.sh' 2>/dev/null || true)" + + if [ -z "${sh_files}" ]; then + echo "No shell scripts found under scripts/." >> "${GITHUB_STEP_SUMMARY}" + echo "Shell quality gate skipped." >> "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + + while IFS= read -r f; do + [ -z "${f}" ] && continue + bash -n "${f}" + done <<< "${sh_files}" + + shellcheck -x ${sh_files} + + echo "Shell quality gate passed." >> "${GITHUB_STEP_SUMMARY}" { echo "### Guardrails: scripts and tooling" @@ -439,6 +536,7 @@ jobs: repo_health: name: Repository health runs-on: ubuntu-latest + timeout-minutes: 15 needs: [access_check] if: ${{ needs.access_check.outputs.allowed == 'true' }} permissions: @@ -471,6 +569,8 @@ jobs: "CHANGELOG.md" "CONTRIBUTING.md" "CODE_OF_CONDUCT.md" + "TODO.md" + "docs/docs-index.md" ) optional_files=( @@ -503,7 +603,6 @@ jobs: [ ! -d "${p}" ] && missing_required+=("${p}/") done - # dev/ is the only source root. src/ must not exist. if [ -d "src" ]; then missing_required+=("src/ (disallowed, use dev/ only)") fi @@ -544,34 +643,70 @@ jobs: content_warnings+=("README.md missing expected brand keyword") fi - export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")" - export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")" - export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")" + export MISSING_REQUIRED="$(printf '%s +' "${missing_required[@]:-}")" + export MISSING_OPTIONAL="$(printf '%s +' "${missing_optional[@]:-}")" + export CONTENT_WARNINGS="$(printf '%s +' "${content_warnings[@]:-}")" report_json="$(python3 - <<'PY' - import json - import os - profile = os.environ.get('PROFILE_RAW') or 'all' - required_files = ["README.md","LICENSE","CHANGELOG.md","CONTRIBUTING.md","CODE_OF_CONDUCT.md"] - optional_files = ["SECURITY.md","GOVERNANCE.md",".editorconfig",".gitattributes",".gitignore"] - required_paths = [".github/workflows","scripts","docs","dev"] - missing_required = os.environ.get('MISSING_REQUIRED','').split('\n') if os.environ.get('MISSING_REQUIRED') else [] - missing_optional = os.environ.get('MISSING_OPTIONAL','').split('\n') if os.environ.get('MISSING_OPTIONAL') else [] - content_warnings = os.environ.get('CONTENT_WARNINGS','').split('\n') if os.environ.get('CONTENT_WARNINGS') else [] - out = { - "profile": profile, - "checked": { - "required_files": required_files, - "optional_files": optional_files, - "required_paths": required_paths, - }, - "missing_required": [x for x in missing_required if x], - "missing_optional": [x for x in missing_optional if x], - "content_warnings": [x for x in content_warnings if x], - } - print(json.dumps(out, indent=2)) - PY - )" +import json +import os + +profile = os.environ.get('PROFILE_RAW') or 'all' +required_files = ["README.md","LICENSE","CHANGELOG.md","CONTRIBUTING.md","CODE_OF_CONDUCT.md","TODO.md","docs/docs-index.md"] +optional_files = ["SECURITY.md","GOVERNANCE.md",".editorconfig",".gitattributes",".gitignore"] +required_paths = [".github/workflows","scripts","docs","dev"] + +missing_required = os.environ.get('MISSING_REQUIRED','').split(' +') if os.environ.get('MISSING_REQUIRED') else [] +missing_optional = os.environ.get('MISSING_OPTIONAL','').split(' +') if os.environ.get('MISSING_OPTIONAL') else [] +content_warnings = os.environ.get('CONTENT_WARNINGS','').split(' +') if os.environ.get('CONTENT_WARNINGS') else [] + +out = { + "profile": profile, + "checked": { + "required_files": required_files, + "optional_files": optional_files, + "required_paths": required_paths, + }, + "missing_required": [x for x in missing_required if x], + "missing_optional": [x for x in missing_optional if x], + "content_warnings": [x for x in content_warnings if x], +} + +print(json.dumps(out, indent=2)) +PY +)" + + { + echo "### Guardrails: repository health" + echo "" + echo "### Guardrails report (JSON)" + echo "```json" + echo "${report_json}" + echo "```" + } >> "${GITHUB_STEP_SUMMARY}" + + if [ "${#missing_required[@]}" -gt 0 ]; then + echo "### Missing required repo artifacts" >> "${GITHUB_STEP_SUMMARY}" + for m in "${missing_required[@]}"; do echo "- ${m}" >> "${GITHUB_STEP_SUMMARY}"; done + echo "ERROR: Guardrails failed. Missing required repository artifacts." >> "${GITHUB_STEP_SUMMARY}" + exit 1 + fi + + if [ "${#missing_optional[@]}" -gt 0 ]; then + echo "### Missing optional repo artifacts" >> "${GITHUB_STEP_SUMMARY}" + for m in "${missing_optional[@]}"; do echo "- ${m}" >> "${GITHUB_STEP_SUMMARY}"; done + fi + + if [ "${#content_warnings[@]}" -gt 0 ]; then + echo "### Repo content warnings" >> "${GITHUB_STEP_SUMMARY}" + for m in "${content_warnings[@]}"; do echo "- ${m}" >> "${GITHUB_STEP_SUMMARY}"; done + fi { echo "### Guardrails: repository health"