Update repo_health.yml

This commit is contained in:
2025-12-30 16:31:42 -06:00
parent f3c18e3b1a
commit 47f26ed3ff

View File

@@ -59,15 +59,20 @@ permissions:
env: env:
# Global policy variables baked into workflow # Global policy variables baked into workflow
ALLOWED_SFTP_PROTOCOLS: sftp ALLOWED_SFTP_PROTOCOLS: sftp
# Release policy
RELEASE_REQUIRED_VARS: FTP_HOST,FTP_USER,FTP_PATH RELEASE_REQUIRED_VARS: FTP_HOST,FTP_USER,FTP_PATH
RELEASE_OPTIONAL_VARS: FTP_KEY,FTP_PASSWORD,FTP_PROTOCOL,FTP_PORT,FTP_PATH_SUFFIX RELEASE_OPTIONAL_VARS: FTP_KEY,FTP_PASSWORD,FTP_PROTOCOL,FTP_PORT,FTP_PATH_SUFFIX
SCRIPTS_RECOMMENDED_DIRS: scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate # Scripts governance policy
# Note: directories listed without a trailing slash.
SCRIPTS_REQUIRED_DIRS: scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
REPO_REQUIRED_FILES: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,docs/docs-index.md # Repo health policy
# Files are listed as-is; directories must end with a trailing slash.
REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,docs/docs-index.md,.github/workflows/,scripts/,docs/,dev/
REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore
REPO_REQUIRED_PATHS: .github/workflows,scripts,docs,dev
REPO_DISALLOWED_DIRS: src REPO_DISALLOWED_DIRS: src
REPO_DISALLOWED_FILES: TODO.md,todo.md REPO_DISALLOWED_FILES: TODO.md,todo.md
@@ -96,25 +101,25 @@ jobs:
const res = await github.rest.repos.getCollaboratorPermissionLevel({ const res = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
username: context.actor username: context.actor,
}); });
const permission = (res?.data?.permission || 'unknown').toLowerCase(); const permission = (res?.data?.permission || "unknown").toLowerCase();
const allowed = permission === 'admin'; const allowed = permission === "admin";
core.setOutput('permission', permission); core.setOutput("permission", permission);
core.setOutput('allowed', allowed ? 'true' : 'false'); core.setOutput("allowed", allowed ? "true" : "false");
const lines = []; const lines = [];
lines.push('### Access control'); lines.push("### Access control");
lines.push(''); lines.push("");
lines.push(`Actor: ${context.actor}`); lines.push(`Actor: ${context.actor}`);
lines.push(`Permission: ${permission}`); lines.push(`Permission: ${permission}`);
lines.push(`Allowed: ${allowed}`); lines.push(`Allowed: ${allowed}`);
lines.push(''); lines.push("");
lines.push('Policy: This workflow runs only for users with admin permission on the repository.'); lines.push("Policy: This workflow runs only for users with admin permission on the repository.");
await core.summary.addRaw(lines.join('\n')).write(); await core.summary.addRaw(lines.join("\n")).write();
- name: Deny execution when not permitted - name: Deny execution when not permitted
if: ${{ steps.perm.outputs.allowed != 'true' }} if: ${{ steps.perm.outputs.allowed != 'true' }}
@@ -138,7 +143,7 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Guardrails release secrets and vars - name: Guardrails release vars
env: env:
PROFILE_RAW: ${{ github.event.inputs.profile }} PROFILE_RAW: ${{ github.event.inputs.profile }}
FTP_HOST: ${{ secrets.FTP_HOST }} FTP_HOST: ${{ secrets.FTP_HOST }}
@@ -184,7 +189,6 @@ jobs:
done done
proto="${FTP_PROTOCOL:-sftp}" proto="${FTP_PROTOCOL:-sftp}"
if [ -n "${FTP_PROTOCOL:-}" ]; then if [ -n "${FTP_PROTOCOL:-}" ]; then
ok=false ok=false
for ap in "${allowed_proto[@]}"; do for ap in "${allowed_proto[@]}"; do
@@ -224,6 +228,8 @@ jobs:
FTP_KEY: ${{ secrets.FTP_KEY }} FTP_KEY: ${{ secrets.FTP_KEY }}
FTP_PASSWORD: ${{ secrets.FTP_PASSWORD }} FTP_PASSWORD: ${{ secrets.FTP_PASSWORD }}
FTP_PORT: ${{ secrets.FTP_PORT }} FTP_PORT: ${{ secrets.FTP_PORT }}
FTP_PATH: ${{ secrets.FTP_PATH }}
FTP_PATH_SUFFIX: ${{ vars.FTP_PATH_SUFFIX }}
run: | run: |
set -euo pipefail set -euo pipefail
@@ -241,15 +247,32 @@ jobs:
exit 0 exit 0
fi fi
mkdir -p "$HOME/.ssh" port="${FTP_PORT:-22}"
key_file="$HOME/.ssh/ci_sftp_key" target_path="${FTP_PATH}"
use_key=false if [ -n "${FTP_PATH_SUFFIX:-}" ]; then
target_path="${target_path%/}/${FTP_PATH_SUFFIX#/}"
fi
sftp_verbose="${SFTP_VERBOSE:-false}"
sftp_v_opt=()
[ "${sftp_verbose}" = 'true' ] && sftp_v_opt=(-vv)
{
printf '%s\n' '### SFTP connectivity test'
printf '%s\n' "Target path: ${target_path}"
printf '%s\n' 'Attempting non-destructive SFTP session'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
sftp_cmds="$(printf 'cd %s\npwd\nbye\n' "${target_path}")"
set +e
if [ -n "${FTP_KEY:-}" ]; then if [ -n "${FTP_KEY:-}" ]; then
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}" chmod 600 "${key_file}"
use_key=true
if [ -n "${FTP_PASSWORD:-}" ]; then if [ -n "${FTP_PASSWORD:-}" ]; then
first_line="$(head -n 1 "${key_file}" || true)" first_line="$(head -n 1 "${key_file}" || true)"
@@ -259,33 +282,17 @@ jobs:
fi fi
ssh-keygen -p -P "${FTP_PASSWORD}" -N '' -f "${key_file}" >/dev/null ssh-keygen -p -P "${FTP_PASSWORD}" -N '' -f "${key_file}" >/dev/null
fi fi
fi
port="${FTP_PORT:-22}" printf '%s' "${sftp_cmds}" | sftp "${sftp_v_opt[@]}" -oBatchMode=yes -oStrictHostKeyChecking=no -P "${port}" -i "${key_file}" "${FTP_USER}@${FTP_HOST}" >/tmp/sftp_check.log 2>&1
sftp_rc=$?
sftp_verbose="${SFTP_VERBOSE:-false}"
sftp_v_opt=()
if [ "${sftp_verbose}" = 'true' ]; then
sftp_v_opt=(-vv)
fi
{
printf '%s\n' '### SFTP connectivity test'
printf '%s\n' 'Attempting non-destructive SFTP session'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
set +e
if [ "${use_key}" = true ]; then
printf 'pwd\nbye\n' | sftp "${sftp_v_opt[@]}" -oBatchMode=yes -oStrictHostKeyChecking=no -P "${port}" -i "${key_file}" "${FTP_USER}@${FTP_HOST}" >/tmp/sftp_check.log 2>&1
elif [ -n "${FTP_PASSWORD:-}" ]; then elif [ -n "${FTP_PASSWORD:-}" ]; then
command -v sshpass >/dev/null 2>&1 || (sudo apt-get update -qq && sudo apt-get install -y sshpass >/dev/null) command -v sshpass >/dev/null 2>&1 || (sudo apt-get update -qq && sudo apt-get install -y sshpass >/dev/null)
printf 'pwd\nbye\n' | sshpass -p "${FTP_PASSWORD}" sftp "${sftp_v_opt[@]}" -oBatchMode=no -oStrictHostKeyChecking=no -P "${port}" "${FTP_USER}@${FTP_HOST}" >/tmp/sftp_check.log 2>&1 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 else
printf '%s\n' 'ERROR: No FTP_KEY or FTP_PASSWORD provided for SFTP authentication.' >> "${GITHUB_STEP_SUMMARY}" printf '%s\n' 'ERROR: No FTP_KEY or FTP_PASSWORD provided for SFTP authentication.' >> "${GITHUB_STEP_SUMMARY}"
exit 1 exit 1
fi fi
sftp_rc=$?
set -e set -e
printf '%s\n' '### SFTP connectivity result' >> "${GITHUB_STEP_SUMMARY}" printf '%s\n' '### SFTP connectivity result' >> "${GITHUB_STEP_SUMMARY}"
@@ -298,7 +305,7 @@ jobs:
printf '%s\n' "Status: FAILED (exit code ${sftp_rc})" printf '%s\n' "Status: FAILED (exit code ${sftp_rc})"
printf '\n' printf '\n'
printf '%s\n' 'Last SFTP output' printf '%s\n' 'Last SFTP output'
tail -n 40 /tmp/sftp_check.log || true tail -n 60 /tmp/sftp_check.log || true
} >> "${GITHUB_STEP_SUMMARY}" } >> "${GITHUB_STEP_SUMMARY}"
exit 1 exit 1
@@ -346,28 +353,31 @@ jobs:
exit 0 exit 0
fi fi
IFS=',' read -r -a recommended_dirs <<< "${SCRIPTS_RECOMMENDED_DIRS}" IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"
IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}" IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}"
missing_dirs=() missing_dirs=()
unapproved_dirs=() unapproved_dirs=()
for d in "${recommended_dirs[@]}"; do for d in "${required_dirs[@]}"; do
[ ! -d "${d}" ] && missing_dirs+=("${d}/") req="${d%/}"
[ ! -d "${req}" ] && missing_dirs+=("${req}/")
done done
while IFS= read -r d; do while IFS= read -r d; do
allowed=false allowed=false
for a in "${allowed_dirs[@]}"; do for a in "${allowed_dirs[@]}"; do
[ "${d}" = "${a}" ] && allowed=true a_norm="${a%/}"
[ "${d%/}" = "${a_norm}" ] && allowed=true
done done
[ "${allowed}" = false ] && unapproved_dirs+=("${d}/") [ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/")
done < <(find scripts -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##') done < <(find scripts -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##')
{ {
printf '%s\n' '### Scripts governance' printf '%s\n' '### Scripts governance'
if [ "${#missing_dirs[@]}" -gt 0 ]; then if [ "${#missing_dirs[@]}" -gt 0 ]; then
printf '%s\n' 'Missing recommended script directories:' printf '%s\n' 'Missing required script directories:'
for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done
printf '\n' printf '\n'
fi fi
@@ -381,9 +391,9 @@ jobs:
printf '%s\n' '| Area | Status | Notes |' printf '%s\n' '| Area | Status | Notes |'
printf '%s\n' '|------|--------|-------|' printf '%s\n' '|------|--------|-------|'
if [ "${#missing_dirs[@]}" -gt 0 ]; then if [ "${#missing_dirs[@]}" -gt 0 ]; then
printf '%s\n' '| Recommended directories | Warning | Missing recommended subfolders |' printf '%s\n' '| Required directories | Warning | Missing required subfolders |'
else else
printf '%s\n' '| Recommended directories | OK | All recommended subfolders present |' printf '%s\n' '| Required directories | OK | All required subfolders present |'
fi fi
if [ "${#unapproved_dirs[@]}" -gt 0 ]; then if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |' printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |'
@@ -430,37 +440,34 @@ jobs:
exit 0 exit 0
fi fi
IFS=',' read -r -a required_files <<< "${REPO_REQUIRED_FILES}" IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}"
IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}" IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}"
IFS=',' read -r -a required_paths <<< "${REPO_REQUIRED_PATHS}"
IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}" IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"
IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES}" IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES}"
missing_required=() missing_required=()
missing_optional=() missing_optional=()
for f in "${required_files[@]}"; do for item in "${required_artifacts[@]}"; do
[ ! -f "${f}" ] && missing_required+=("${f}") if printf '%s' "${item}" | grep -q '/$'; then
d="${item%/}"
[ ! -d "${d}" ] && missing_required+=("${item}")
else
[ ! -f "${item}" ] && missing_required+=("${item}")
fi
done done
for f in "${optional_files[@]}"; do for f in "${optional_files[@]}"; do
[ ! -f "${f}" ] && missing_optional+=("${f}") [ ! -f "${f}" ] && missing_optional+=("${f}")
done done
for p in "${required_paths[@]}"; do
[ ! -d "${p}" ] && missing_required+=("${p}/")
done
for d in "${disallowed_dirs[@]}"; do for d in "${disallowed_dirs[@]}"; do
if [ -d "${d}" ]; then d_norm="${d%/}"
missing_required+=("${d}/ (disallowed)") [ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)")
fi
done done
for f in "${disallowed_files[@]}"; do for f in "${disallowed_files[@]}"; do
if [ -f "${f}" ]; then [ -f "${f}" ] && missing_required+=("${f} (disallowed)")
missing_required+=("${f} (disallowed)")
fi
done done
git fetch origin --prune git fetch origin --prune