From fc922accf14b0271a06638977d49baa1b72a6594 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com> Date: Tue, 30 Dec 2025 15:08:41 -0600 Subject: [PATCH] Update repo_health.yml --- .github/workflows/repo_health.yml | 698 +----------------------------- 1 file changed, 11 insertions(+), 687 deletions(-) diff --git a/.github/workflows/repo_health.yml b/.github/workflows/repo_health.yml index e683df7..a5a1e4a 100644 --- a/.github/workflows/repo_health.yml +++ b/.github/workflows/repo_health.yml @@ -5,19 +5,6 @@ # # SPDX-License-Identifier: GPL-3.0-or-later # -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# # FILE INFORMATION # DEFGROUP: GitHub.Workflow # INGROUP: MokoStandards.Validation @@ -63,694 +50,31 @@ permissions: jobs: access_check: - name: Access control runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - contents: read - - outputs: - allowed: ${{ steps.perm.outputs.allowed }} - permission: ${{ steps.perm.outputs.permission }} - steps: - - name: Check actor permission admin only + - uses: actions/github-script@v7 id: perm - uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const owner = context.repo.owner; - const repo = context.repo.repo; - const username = context.actor; - const res = await github.rest.repos.getCollaboratorPermissionLevel({ - owner, - repo, - username, + owner: context.repo.owner, + repo: context.repo.repo, + username: context.actor }); - - const permission = (res?.data?.permission || "unknown").toLowerCase(); - const allowed = (permission === "admin"); - - core.setOutput("permission", permission); - core.setOutput("allowed", allowed ? "true" : "false"); - - const lines = []; - lines.push("### Access control"); - lines.push(""); - lines.push(`Actor: ${username}`); - lines.push(`Permission: ${permission}`); - lines.push(`Allowed: ${allowed}`); - lines.push(""); - lines.push("Policy: This workflow runs only for users with admin permission on the repository."); - await core.summary.addRaw(lines.join("\n")).write(); - - - name: Deny execution when not permitted - if: ${{ steps.perm.outputs.allowed != 'true' }} - run: | - set -euo pipefail - printf '%s\n' "ERROR: Access denied. Actor must have admin permission to run this workflow." >> "${GITHUB_STEP_SUMMARY}" - exit 1 - - release_config: - name: Release configuration - runs-on: ubuntu-latest - timeout-minutes: 20 - needs: [access_check] - if: ${{ needs.access_check.outputs.allowed == 'true' }} - permissions: - contents: read - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Load guardrails definition - env: - GUARDRAILS_DEFINITION_URL: ${{ vars.MOKOSTANDARDS_GUARDRAILS_URL || 'https://raw.githubusercontent.com/mokoconsulting-tech/MokoStandards/main/repo-guardrails.definition.json' }} - run: | - set -euo pipefail - - url="${GUARDRAILS_DEFINITION_URL}" - printf '%s\n' "### Guardrails policy source" >> "${GITHUB_STEP_SUMMARY}" - printf '%s\n' "${url}" >> "${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 - env: - PROFILE_RAW: "${{ github.event.inputs.profile }}" - FTP_HOST: "${{ secrets.FTP_HOST }}" - FTP_USER: "${{ secrets.FTP_USER }}" - FTP_KEY: "${{ secrets.FTP_KEY }}" - FTP_PASSWORD: "${{ secrets.FTP_PASSWORD }}" - FTP_PATH: "${{ secrets.FTP_PATH }}" - FTP_PROTOCOL: "${{ secrets.FTP_PROTOCOL }}" - FTP_PORT: "${{ secrets.FTP_PORT }}" - FTP_PATH_SUFFIX: "${{ vars.FTP_PATH_SUFFIX }}" - run: | - set -euo pipefail - - profile="${PROFILE_RAW:-all}" - case "${profile}" in - all|release|scripts|repo) ;; - *) - printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" - exit 1 - ;; - esac - - if [ "${profile}" = "scripts" ] || [ "${profile}" = "repo" ]; then - printf '%s\n' "Profile ${profile} selected. Skipping release configuration checks." >> "${GITHUB_STEP_SUMMARY}" - exit 0 - fi - - required=("FTP_HOST" "FTP_USER" "FTP_KEY" "FTP_PATH") - optional=("FTP_PASSWORD" "FTP_PROTOCOL" "FTP_PORT" "FTP_PATH_SUFFIX") - - 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_optional=() - - for k in "${required[@]}"; do - v="${!k:-}" - [ -z "${v}" ] && missing+=("${k}") - done - - for k in "${optional[@]}"; do - v="${!k:-}" - [ -z "${v}" ] && missing_optional+=("${k}") - done - - 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 - ok=false - for ap in "${allowed_proto[@]}"; do - [ "${proto}" = "${ap}" ] && ok=true - done - [ "${ok}" = false ] && missing+=("FTP_PROTOCOL_INVALID") - fi - - 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}" - fi - - if [ "${#missing[@]}" -gt 0 ]; then - { - printf '%s\n' "### Missing required release configuration" - for m in "${missing[@]}"; do - printf '%s\n' "- ${m}" - done - printf '%s\n' "ERROR: Guardrails failed. Missing required release configuration." - } >> "${GITHUB_STEP_SUMMARY}" - exit 1 - fi - - { - printf '%s\n' "### Guardrails release configuration" - printf '%s\n' "All required release variables present." - } >> "${GITHUB_STEP_SUMMARY}" - - - name: Guardrails SFTP connectivity - env: - PROFILE_RAW: "${{ github.event.inputs.profile }}" - FTP_HOST: "${{ secrets.FTP_HOST }}" - FTP_USER: "${{ secrets.FTP_USER }}" - FTP_KEY: "${{ secrets.FTP_KEY }}" - FTP_PASSWORD: "${{ secrets.FTP_PASSWORD }}" - FTP_PORT: "${{ secrets.FTP_PORT }}" - run: | - set -euo pipefail - - profile="${PROFILE_RAW:-all}" - case "${profile}" in - all|release|scripts|repo) ;; - *) - printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" - exit 1 - ;; - esac - - if [ "${profile}" = "scripts" ] || [ "${profile}" = "repo" ]; then - printf '%s\n' "Profile ${profile} selected. Skipping SFTP connectivity check." >> "${GITHUB_STEP_SUMMARY}" - exit 0 - fi - - mkdir -p "$HOME/.ssh" - key_file="$HOME/.ssh/ci_sftp_key" - printf '%s\n' "${FTP_KEY}" > "${key_file}" - chmod 600 "${key_file}" - - if [ -n "${FTP_PASSWORD:-}" ]; then - first_line="$(head -n 1 "${key_file}" || true)" - if printf '%s' "${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}" - exit 1 - fi - ssh-keygen -p -P "${FTP_PASSWORD}" -N "" -f "${key_file}" >/dev/null - fi - - port="${FTP_PORT:-22}" - - { - printf '%s\n' "### SFTP connectivity test" - printf '%s\n' "Attempting non-destructive SFTP session" - } >> "${GITHUB_STEP_SUMMARY}" - - set +e - 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=$? - 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 '\n' - printf '%s\n' "Last SFTP output" - tail -n 20 /tmp/sftp_check.log || true - } >> "${GITHUB_STEP_SUMMARY}" - exit 1 - - scripts_config: - name: Scripts governance - runs-on: ubuntu-latest - timeout-minutes: 15 - needs: [access_check] - if: ${{ needs.access_check.outputs.allowed == 'true' }} - permissions: - contents: read - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Load guardrails definition - env: - GUARDRAILS_DEFINITION_URL: ${{ vars.MOKOSTANDARDS_GUARDRAILS_URL || 'https://raw.githubusercontent.com/mokoconsulting-tech/MokoStandards/main/repo-guardrails.definition.json' }} - run: | - set -euo pipefail - - url="${GUARDRAILS_DEFINITION_URL}" - printf '%s\n' "### Guardrails policy source" >> "${GITHUB_STEP_SUMMARY}" - printf '%s\n' "${url}" >> "${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 governance - env: - PROFILE_RAW: "${{ github.event.inputs.profile }}" - run: | - set -euo pipefail - - profile="${PROFILE_RAW:-all}" - case "${profile}" in - all|release|scripts|repo) ;; - *) - printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" - exit 1 - ;; - esac - - if [ "${profile}" = "release" ] || [ "${profile}" = "repo" ]; then - printf '%s\n' "Profile ${profile} selected. Skipping scripts checks." >> "${GITHUB_STEP_SUMMARY}" - exit 0 - fi - - if [ ! -d "scripts" ]; then - { - printf '%s\n' "### Scripts folder not present" - printf '%s\n' "Warning: scripts/ directory is optional. No scripts governance enforced." - } >> "${GITHUB_STEP_SUMMARY}" - exit 0 - fi - - recommended_dirs=("scripts/fix" "scripts/lib" "scripts/release" "scripts/run" "scripts/validate") - allowed_dirs=("scripts" "scripts/fix" "scripts/lib" "scripts/release" "scripts/run" "scripts/validate") - - 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=() - unapproved_dirs=() - - for d in "${recommended_dirs[@]}"; do - [ ! -d "${d}" ] && missing_dirs+=("${d}/") - done - - while IFS= read -r d; do - allowed=false - for a in "${allowed_dirs[@]}"; do - [ "${d}" = "${a}" ] && allowed=true - done - [ "${allowed}" = false ] && unapproved_dirs+=("${d}/") - done < <(find scripts -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##') - - if [ "${#missing_dirs[@]}" -gt 0 ]; then - { - printf '%s\n' "### Scripts governance warnings" - printf '%s\n' "Missing recommended script directories:" - for m in "${missing_dirs[@]}"; do - printf '%s\n' "- ${m}" - done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - if [ "${#unapproved_dirs[@]}" -gt 0 ]; then - { - printf '%s\n' "### Scripts governance warnings" - printf '%s\n' "Unapproved script directories detected:" - for m in "${unapproved_dirs[@]}"; do - printf '%s\n' "- ${m}" - done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - { - printf '%s\n' "### Scripts governance summary" - printf '%s\n' "| Area | Status | Notes |" - printf '%s\n' "|------|--------|-------|" - if [ "${#missing_dirs[@]}" -gt 0 ]; then - printf '%s\n' "| Recommended directories | Warning | Missing recommended subfolders |" - else - printf '%s\n' "| Recommended directories | OK | All recommended 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." - } >> "${GITHUB_STEP_SUMMARY}" + const allowed = res.data.permission === 'admin'; + core.setOutput('allowed', allowed ? 'true' : 'false'); repo_health: - name: Repository health - runs-on: ubuntu-latest - timeout-minutes: 15 - needs: [access_check] + needs: access_check if: ${{ needs.access_check.outputs.allowed == 'true' }} - permissions: - contents: read - + runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Load guardrails definition - env: - GUARDRAILS_DEFINITION_URL: ${{ vars.MOKOSTANDARDS_GUARDRAILS_URL || 'https://raw.githubusercontent.com/mokoconsulting-tech/MokoStandards/main/repo-guardrails.definition.json' }} - run: | - set -euo pipefail - - url="${GUARDRAILS_DEFINITION_URL}" - printf '%s\n' "### Guardrails policy source" >> "${GITHUB_STEP_SUMMARY}" - printf '%s\n' "${url}" >> "${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_REQUIRED_FILES", data.get("repo", {}).get("required_files")) - put_multiline("GUARDRAILS_OPTIONAL_FILES", data.get("repo", {}).get("optional_files")) - put_multiline("GUARDRAILS_REQUIRED_PATHS", data.get("repo", {}).get("required_paths")) - put_multiline("GUARDRAILS_DISALLOWED_DIRS", data.get("repo", {}).get("paths", {}).get("disallowed_dirs")) - - with open(env_path, "a", encoding="utf-8") as w: - w.write("GUARDRAILS_LOADED=true\n") - - print("Guardrails definition loaded") - PY + - uses: actions/checkout@v4 - name: Repo health checks - env: - PROFILE_RAW: "${{ github.event.inputs.profile }}" run: | set -euo pipefail - - profile="${PROFILE_RAW:-all}" - case "${profile}" in - all|release|scripts|repo) ;; - *) - printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" - exit 1 - ;; - esac - - if [ "${profile}" = "release" ] || [ "${profile}" = "scripts" ]; then - printf '%s\n' "Profile ${profile} selected. Skipping repository health checks." >> "${GITHUB_STEP_SUMMARY}" - exit 0 - fi - - 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" - ) - - disallowed_dirs=("src") - - if [ "${GUARDRAILS_LOADED:-false}" = "true" ]; then - if [ -n "${GUARDRAILS_REQUIRED_FILES:-}" ]; then - mapfile -t required_files < <(printf '%s\n' "${GUARDRAILS_REQUIRED_FILES}" | sed '/^$/d') - fi - if [ -n "${GUARDRAILS_OPTIONAL_FILES:-}" ]; then - mapfile -t optional_files < <(printf '%s\n' "${GUARDRAILS_OPTIONAL_FILES}" | sed '/^$/d') - fi - if [ -n "${GUARDRAILS_REQUIRED_PATHS:-}" ]; then - mapfile -t required_paths < <(printf '%s\n' "${GUARDRAILS_REQUIRED_PATHS}" | sed '/^$/d') - fi - if [ -n "${GUARDRAILS_DISALLOWED_DIRS:-}" ]; then - mapfile -t disallowed_dirs < <(printf '%s\n' "${GUARDRAILS_DISALLOWED_DIRS}" | sed '/^$/d') - fi - fi - - missing_required=() - missing_optional=() - - for f in "${required_files[@]}"; do - [ ! -f "${f}" ] && missing_required+=("${f}") - done - - for f in "${optional_files[@]}"; do - [ ! -f "${f}" ] && missing_optional+=("${f}") - done - - for p in "${required_paths[@]}"; do - [ ! -d "${p}" ] && missing_required+=("${p}/") - done - - for d in "${disallowed_dirs[@]}"; do - if [ -d "${d}" ]; then - missing_required+=("${d}/ (disallowed)") - fi - done - - git fetch origin --prune - - dev_paths=() - dev_branches=() - - while IFS= read -r b; do - name="${b#origin/}" - if [ "${name}" = "dev" ]; then - dev_branches+=("${name}") - else - dev_paths+=("${name}") - fi - done < <(git branch -r --list "origin/dev*" | sed 's/^ *//') - - if [ "${#dev_paths[@]}" -eq 0 ]; then - missing_required+=("dev/* branch (e.g. dev/01.00.00)") - fi - - if [ "${#dev_branches[@]}" -gt 0 ]; then - missing_required+=("invalid branch dev (must be dev/)") - fi - - content_warnings=() - - if [ -f "CHANGELOG.md" ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then - content_warnings+=("CHANGELOG.md missing '# Changelog' header") - fi - - if [ -f "LICENSE" ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then - content_warnings+=("LICENSE does not look like a GPL text") - fi - - if [ -f "README.md" ] && ! grep -qiE 'moko|Moko' README.md; then - content_warnings+=("README.md missing expected brand keyword") - fi - - export PROFILE_RAW="${profile}" - export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")" - export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")" - export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")" - - report_json="$(python3 - <<'PY' - import json - import os - - profile = os.environ.get("PROFILE_RAW") or "all" - - 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 [] - content_warnings = os.environ.get("CONTENT_WARNINGS", "").splitlines() if os.environ.get("CONTENT_WARNINGS") else [] - - out = { - "profile": profile, - "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 - )" - - { - printf '%s\n' "### Guardrails repository health" - printf '\n' - printf '%s\n' "### Guardrails report (JSON)" - printf '%s\n' '```json' - printf '%s\n' "${report_json}" - printf '%s\n' '```' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - - if [ "${#missing_required[@]}" -gt 0 ]; then - { - 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." - } >> "${GITHUB_STEP_SUMMARY}" - exit 1 - fi - - if [ "${#missing_optional[@]}" -gt 0 ]; then - { - printf '%s\n' "### Missing optional repo artifacts" - for m in "${missing_optional[@]}"; do - printf '%s\n' "- ${m}" - done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - if [ "${#content_warnings[@]}" -gt 0 ]; then - { - printf '%s\n' "### Repo content warnings" - for m in "${content_warnings[@]}"; do - printf '%s\n' "- ${m}" - done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - printf '%s\n' "Repository health guardrails passed." >> "${GITHUB_STEP_SUMMARY}" + +# EOF