Update repo_health.yml

This commit is contained in:
2025-12-27 02:33:50 -06:00
parent 5cdb6a7f5c
commit 21a3021ad5

View File

@@ -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"