Update repo_health.yml
This commit is contained in:
251
.github/workflows/repo_health.yml
vendored
251
.github/workflows/repo_health.yml
vendored
@@ -11,7 +11,7 @@
|
|||||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
||||||
# PATH: /.github/workflows/repo_health.yml
|
# PATH: /.github/workflows/repo_health.yml
|
||||||
# VERSION: 03.05.00
|
# VERSION: 03.05.00
|
||||||
# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts using MokoStandards definition files.
|
# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts.
|
||||||
# NOTE: Field is user-managed.
|
# NOTE: Field is user-managed.
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
@@ -44,12 +44,35 @@ on:
|
|||||||
- scripts/**
|
- scripts/**
|
||||||
- docs/**
|
- docs/**
|
||||||
- dev/**
|
- dev/**
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- .github/workflows/**
|
||||||
|
- scripts/**
|
||||||
|
- docs/**
|
||||||
|
- dev/**
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GUARDRAILS_DEFINITION_URL: ${{ vars.MOKOSTANDARDS_GUARDRAILS_URL || 'https://raw.githubusercontent.com/mokoconsulting-tech/MokoStandards/main/repo-guardrails.definition.json' }}
|
# Global policy variables baked into workflow
|
||||||
|
ALLOWED_SFTP_PROTOCOLS: sftp
|
||||||
|
RELEASE_REQUIRED_VARS: FTP_HOST,FTP_USER,FTP_PATH
|
||||||
|
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_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_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore
|
||||||
|
REPO_REQUIRED_PATHS: .github/workflows,scripts,docs,dev
|
||||||
|
REPO_DISALLOWED_DIRS: src
|
||||||
|
REPO_DISALLOWED_FILES: TODO.md,todo.md
|
||||||
|
|
||||||
|
# Operational toggles
|
||||||
|
SFTP_VERBOSE: "false"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
access_check:
|
access_check:
|
||||||
@@ -115,56 +138,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Load guardrails definition
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
url="${GUARDRAILS_DEFINITION_URL}"
|
|
||||||
{
|
|
||||||
printf '%s\n' '### Guardrails policy source'
|
|
||||||
printf '%s\n' "${url}"
|
|
||||||
printf '\n'
|
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
|
|
||||||
if ! curl -fsSL "${url}" -o /tmp/repo_guardrails.definition.json; then
|
|
||||||
printf '%s\n' 'Warning: Unable to fetch guardrails definition. Falling back to workflow defaults.' >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
printf '%s\n' 'GUARDRAILS_LOADED=false' >> "${GITHUB_ENV}"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
python3 - <<'PY'
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
path = "/tmp/repo_guardrails.definition.json"
|
|
||||||
with open(path, "r", encoding="utf-8") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
|
|
||||||
env_path = os.environ.get("GITHUB_ENV")
|
|
||||||
if not env_path:
|
|
||||||
raise SystemExit("GITHUB_ENV not set")
|
|
||||||
|
|
||||||
def put_multiline(key: str, values):
|
|
||||||
vals = [str(v) for v in (values or []) if str(v).strip()]
|
|
||||||
marker = f"EOF_{uuid.uuid4().hex}"
|
|
||||||
with open(env_path, "a", encoding="utf-8") as w:
|
|
||||||
w.write(f"{key}<<{marker}\n")
|
|
||||||
for v in vals:
|
|
||||||
w.write(v + "\n")
|
|
||||||
w.write(f"{marker}\n\n")
|
|
||||||
|
|
||||||
put_multiline("GUARDRAILS_RELEASE_REQUIRED_SECRETS", data.get("release", {}).get("required_secrets"))
|
|
||||||
put_multiline("GUARDRAILS_RELEASE_OPTIONAL_SECRETS", data.get("release", {}).get("optional_secrets"))
|
|
||||||
put_multiline("GUARDRAILS_RELEASE_OPTIONAL_VARS", data.get("release", {}).get("optional_vars"))
|
|
||||||
put_multiline("GUARDRAILS_RELEASE_PROTOCOL_ALLOWED", data.get("release", {}).get("protocol", {}).get("allowed"))
|
|
||||||
|
|
||||||
with open(env_path, "a", encoding="utf-8") as w:
|
|
||||||
w.write("GUARDRAILS_LOADED=true\n")
|
|
||||||
|
|
||||||
print("Guardrails definition loaded")
|
|
||||||
PY
|
|
||||||
|
|
||||||
- name: Guardrails release secrets and vars
|
- name: Guardrails release secrets and vars
|
||||||
env:
|
env:
|
||||||
PROFILE_RAW: ${{ github.event.inputs.profile }}
|
PROFILE_RAW: ${{ github.event.inputs.profile }}
|
||||||
@@ -193,25 +166,9 @@ jobs:
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
required=("FTP_HOST" "FTP_USER" "FTP_PATH")
|
IFS=',' read -r -a required <<< "${RELEASE_REQUIRED_VARS}"
|
||||||
optional=("FTP_KEY" "FTP_PASSWORD" "FTP_PROTOCOL" "FTP_PORT" "FTP_PATH_SUFFIX")
|
IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_VARS}"
|
||||||
|
IFS=',' read -r -a allowed_proto <<< "${ALLOWED_SFTP_PROTOCOLS}"
|
||||||
if [ "${GUARDRAILS_LOADED:-false}" = 'true' ]; then
|
|
||||||
if [ -n "${GUARDRAILS_RELEASE_REQUIRED_SECRETS:-}" ]; then
|
|
||||||
mapfile -t required < <(printf '%s\n' "${GUARDRAILS_RELEASE_REQUIRED_SECRETS}" | sed '/^$/d')
|
|
||||||
fi
|
|
||||||
|
|
||||||
opt=()
|
|
||||||
if [ -n "${GUARDRAILS_RELEASE_OPTIONAL_SECRETS:-}" ]; then
|
|
||||||
while IFS= read -r v; do [ -n "${v}" ] && opt+=("${v}"); done < <(printf '%s\n' "${GUARDRAILS_RELEASE_OPTIONAL_SECRETS}" | sed '/^$/d')
|
|
||||||
fi
|
|
||||||
if [ -n "${GUARDRAILS_RELEASE_OPTIONAL_VARS:-}" ]; then
|
|
||||||
while IFS= read -r v; do [ -n "${v}" ] && opt+=("${v}"); done < <(printf '%s\n' "${GUARDRAILS_RELEASE_OPTIONAL_VARS}" | sed '/^$/d')
|
|
||||||
fi
|
|
||||||
if [ "${#opt[@]}" -gt 0 ]; then
|
|
||||||
optional=("${opt[@]}")
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
missing=()
|
missing=()
|
||||||
missing_optional=()
|
missing_optional=()
|
||||||
@@ -228,11 +185,6 @@ jobs:
|
|||||||
|
|
||||||
proto="${FTP_PROTOCOL:-sftp}"
|
proto="${FTP_PROTOCOL:-sftp}"
|
||||||
|
|
||||||
allowed_proto=("sftp")
|
|
||||||
if [ "${GUARDRAILS_LOADED:-false}" = 'true' ] && [ -n "${GUARDRAILS_RELEASE_PROTOCOL_ALLOWED:-}" ]; then
|
|
||||||
mapfile -t allowed_proto < <(printf '%s\n' "${GUARDRAILS_RELEASE_PROTOCOL_ALLOWED}" | sed '/^$/d')
|
|
||||||
fi
|
|
||||||
|
|
||||||
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
|
||||||
@@ -311,6 +263,12 @@ jobs:
|
|||||||
|
|
||||||
port="${FTP_PORT:-22}"
|
port="${FTP_PORT:-22}"
|
||||||
|
|
||||||
|
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' '### SFTP connectivity test'
|
||||||
printf '%s\n' 'Attempting non-destructive SFTP session'
|
printf '%s\n' 'Attempting non-destructive SFTP session'
|
||||||
@@ -319,10 +277,12 @@ jobs:
|
|||||||
|
|
||||||
set +e
|
set +e
|
||||||
if [ "${use_key}" = true ]; then
|
if [ "${use_key}" = true ]; then
|
||||||
printf 'pwd\nbye\n' | sftp -vv -oBatchMode=yes -oStrictHostKeyChecking=no -P "${port}" -i "${key_file}" "${FTP_USER}@${FTP_HOST}" >/tmp/sftp_check.log 2>&1
|
printf 'pwd
|
||||||
|
bye\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
|
||||||
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 -vv -oBatchMode=no -oStrictHostKeyChecking=no -P "${port}" "${FTP_USER}@${FTP_HOST}" >/tmp/sftp_check.log 2>&1
|
printf 'pwd
|
||||||
|
bye\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
|
||||||
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
|
||||||
@@ -359,55 +319,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Load guardrails definition
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
url="${GUARDRAILS_DEFINITION_URL}"
|
|
||||||
{
|
|
||||||
printf '%s\n' '### Guardrails policy source'
|
|
||||||
printf '%s\n' "${url}"
|
|
||||||
printf '\n'
|
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
|
|
||||||
if ! curl -fsSL "${url}" -o /tmp/repo_guardrails.definition.json; then
|
|
||||||
printf '%s\n' 'Warning: Unable to fetch guardrails definition. Falling back to workflow defaults.' >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
printf '%s\n' 'GUARDRAILS_LOADED=false' >> "${GITHUB_ENV}"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
python3 - <<'PY'
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
path = "/tmp/repo_guardrails.definition.json"
|
|
||||||
with open(path, "r", encoding="utf-8") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
|
|
||||||
env_path = os.environ.get("GITHUB_ENV")
|
|
||||||
if not env_path:
|
|
||||||
raise SystemExit("GITHUB_ENV not set")
|
|
||||||
|
|
||||||
def put_multiline(key: str, values):
|
|
||||||
vals = [str(v) for v in (values or []) if str(v).strip()]
|
|
||||||
marker = f"EOF_{uuid.uuid4().hex}"
|
|
||||||
with open(env_path, "a", encoding="utf-8") as w:
|
|
||||||
w.write(f"{key}<<{marker}\n")
|
|
||||||
for v in vals:
|
|
||||||
w.write(v + "\n")
|
|
||||||
w.write(f"{marker}\n\n")
|
|
||||||
|
|
||||||
put_multiline("GUARDRAILS_SCRIPTS_ALLOWED_DIRS", data.get("scripts", {}).get("allowed_top_level_dirs"))
|
|
||||||
put_multiline("GUARDRAILS_SCRIPTS_RECOMMENDED_DIRS", data.get("scripts", {}).get("recommended_dirs"))
|
|
||||||
put_multiline("GUARDRAILS_SCRIPTS_REQUIRED_VALIDATE_FILES", data.get("scripts", {}).get("required_validate_files_when_present"))
|
|
||||||
|
|
||||||
with open(env_path, "a", encoding="utf-8") as w:
|
|
||||||
w.write("GUARDRAILS_LOADED=true\n")
|
|
||||||
|
|
||||||
print("Guardrails definition loaded")
|
|
||||||
PY
|
|
||||||
|
|
||||||
- name: Scripts folder checks
|
- name: Scripts folder checks
|
||||||
env:
|
env:
|
||||||
PROFILE_RAW: ${{ github.event.inputs.profile }}
|
PROFILE_RAW: ${{ github.event.inputs.profile }}
|
||||||
@@ -437,17 +348,8 @@ jobs:
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
recommended_dirs=("scripts/fix" "scripts/lib" "scripts/release" "scripts/run" "scripts/validate")
|
IFS=',' read -r -a recommended_dirs <<< "${SCRIPTS_RECOMMENDED_DIRS}"
|
||||||
allowed_dirs=("scripts" "scripts/fix" "scripts/lib" "scripts/release" "scripts/run" "scripts/validate")
|
IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}"
|
||||||
|
|
||||||
if [ "${GUARDRAILS_LOADED:-false}" = 'true' ]; then
|
|
||||||
if [ -n "${GUARDRAILS_SCRIPTS_RECOMMENDED_DIRS:-}" ]; then
|
|
||||||
mapfile -t recommended_dirs < <(printf '%s\n' "${GUARDRAILS_SCRIPTS_RECOMMENDED_DIRS}" | sed '/^$/d')
|
|
||||||
fi
|
|
||||||
if [ -n "${GUARDRAILS_SCRIPTS_ALLOWED_DIRS:-}" ]; then
|
|
||||||
mapfile -t allowed_dirs < <(printf '%s\n' "${GUARDRAILS_SCRIPTS_ALLOWED_DIRS}" | sed '/^$/d')
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
missing_dirs=()
|
missing_dirs=()
|
||||||
unapproved_dirs=()
|
unapproved_dirs=()
|
||||||
@@ -530,40 +432,11 @@ jobs:
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# NOTE: File and path requirements are enforced locally in this script.
|
IFS=',' read -r -a required_files <<< "${REPO_REQUIRED_FILES}"
|
||||||
# Do not source required/optional file lists from external definition files.
|
IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}"
|
||||||
required_files=(
|
IFS=',' read -r -a required_paths <<< "${REPO_REQUIRED_PATHS}"
|
||||||
README.md
|
IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"
|
||||||
LICENSE
|
IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES}"
|
||||||
CHANGELOG.md
|
|
||||||
CONTRIBUTING.md
|
|
||||||
CODE_OF_CONDUCT.md
|
|
||||||
docs/docs-index.md
|
|
||||||
)
|
|
||||||
|
|
||||||
optional_files=(
|
|
||||||
SECURITY.md
|
|
||||||
GOVERNANCE.md
|
|
||||||
.editorconfig
|
|
||||||
.gitattributes
|
|
||||||
.gitignore
|
|
||||||
)
|
|
||||||
|
|
||||||
required_paths=(
|
|
||||||
.github/workflows
|
|
||||||
scripts
|
|
||||||
docs
|
|
||||||
dev
|
|
||||||
)
|
|
||||||
|
|
||||||
disallowed_dirs=(
|
|
||||||
src
|
|
||||||
)
|
|
||||||
|
|
||||||
disallowed_files=(
|
|
||||||
TODO.md
|
|
||||||
todo.md
|
|
||||||
)
|
|
||||||
|
|
||||||
missing_required=()
|
missing_required=()
|
||||||
missing_optional=()
|
missing_optional=()
|
||||||
@@ -634,25 +507,25 @@ jobs:
|
|||||||
export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")"
|
export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")"
|
||||||
|
|
||||||
report_json="$(python3 - <<'PY'
|
report_json="$(python3 - <<'PY'
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
profile = os.environ.get('PROFILE_RAW') or 'all'
|
profile = os.environ.get('PROFILE_RAW') or 'all'
|
||||||
|
|
||||||
missing_required = os.environ.get('MISSING_REQUIRED', '').splitlines() if os.environ.get('MISSING_REQUIRED') else []
|
missing_required = os.environ.get('MISSING_REQUIRED', '').splitlines() if os.environ.get('MISSING_REQUIRED') else []
|
||||||
missing_optional = os.environ.get('MISSING_OPTIONAL', '').splitlines() if os.environ.get('MISSING_OPTIONAL') else []
|
missing_optional = os.environ.get('MISSING_OPTIONAL', '').splitlines() if os.environ.get('MISSING_OPTIONAL') else []
|
||||||
content_warnings = os.environ.get('CONTENT_WARNINGS', '').splitlines() if os.environ.get('CONTENT_WARNINGS') else []
|
content_warnings = os.environ.get('CONTENT_WARNINGS', '').splitlines() if os.environ.get('CONTENT_WARNINGS') else []
|
||||||
|
|
||||||
out = {
|
out = {
|
||||||
'profile': profile,
|
'profile': profile,
|
||||||
'missing_required': [x for x in missing_required if x],
|
'missing_required': [x for x in missing_required if x],
|
||||||
'missing_optional': [x for x in missing_optional if x],
|
'missing_optional': [x for x in missing_optional if x],
|
||||||
'content_warnings': [x for x in content_warnings if x],
|
'content_warnings': [x for x in content_warnings if x],
|
||||||
}
|
}
|
||||||
|
|
||||||
print(json.dumps(out, indent=2))
|
print(json.dumps(out, indent=2))
|
||||||
PY
|
PY
|
||||||
)"
|
)"
|
||||||
|
|
||||||
{
|
{
|
||||||
printf '%s\n' '### Guardrails repository health'
|
printf '%s\n' '### Guardrails repository health'
|
||||||
@@ -690,3 +563,5 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}"
|
printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
# EOF
|
||||||
|
|||||||
Reference in New Issue
Block a user