Update release_pipeline.yml

This commit is contained in:
2025-12-30 14:23:34 -06:00
parent ffe899d4c0
commit 1a5a3a6a4a

View File

@@ -5,13 +5,26 @@
# #
# SPDX-License-Identifier: GPL-3.0-or-later # 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 <https://www.gnu.org/licenses/>.
#
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: GitHub.Workflow # DEFGROUP: GitHub.Workflow
# INGROUP: MokoStandards.Validation # INGROUP: MokoStandards.Validation
# 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 Joomla repository guardrails by validating release configuration, scripts governance, and core repository health. # BRIEF: Enforces Joomla repository guardrails by validating release configuration, required validation scripts, tooling availability, and core repository health artifacts.
# ============================================================================ # ============================================================================
name: Repo Health name: Repo Health
@@ -28,11 +41,15 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
profile: profile:
description: Which configuration profile to validate description: Which configuration profile to validate. release checks SFTP variables used by release pipeline. scripts checks baseline script prerequisites. repo runs repository health only. all runs release, scripts, and repo health.
required: true required: true
default: all default: all
type: choice type: choice
options: [all, release, scripts, repo] options:
- all
- release
- scripts
- repo
pull_request: pull_request:
paths: paths:
- .github/workflows/** - .github/workflows/**
@@ -47,8 +64,14 @@ jobs:
access_check: access_check:
name: Access control name: Access control
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
outputs: outputs:
allowed: ${{ steps.perm.outputs.allowed }} allowed: ${{ steps.perm.outputs.allowed }}
permission: ${{ steps.perm.outputs.permission }}
steps: steps:
- name: Check actor permission admin only - name: Check actor permission admin only
id: perm id: perm
@@ -60,131 +83,625 @@ jobs:
const repo = context.repo.repo; const repo = context.repo.repo;
const username = context.actor; const username = context.actor;
const res = await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username }); const res = await github.rest.repos.getCollaboratorPermissionLevel({
const permission = (res?.data?.permission || "unknown").toLowerCase(); owner,
const allowed = permission === "admin"; repo,
username,
});
const permission = (res?.data?.permission || "unknown").toLowerCase();
const allowed = (permission === "admin");
core.setOutput("permission", permission);
core.setOutput("allowed", allowed ? "true" : "false"); core.setOutput("allowed", allowed ? "true" : "false");
const lines = [ const lines = [];
"### Access control", lines.push("### Access control");
"", lines.push("");
`Actor: ${username}`, lines.push(`Actor: ${username}`);
`Permission: ${permission}`, lines.push(`Permission: ${permission}`);
`Allowed: ${allowed}`, lines.push(`Allowed: ${allowed}`);
"", lines.push("");
"Policy: Workflow requires admin permission" 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' }}
run: | run: |
echo "ERROR: Access denied. Admin permission required." >> "$GITHUB_STEP_SUMMARY" set -euo pipefail
echo "ERROR: Access denied. Actor must have admin permission to run this workflow." >> "${GITHUB_STEP_SUMMARY}"
exit 1 exit 1
release_config: release_config:
name: Release configuration name: Release configuration
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 20
needs: [access_check] needs: [access_check]
if: ${{ needs.access_check.outputs.allowed == 'true' }} if: ${{ needs.access_check.outputs.allowed == 'true' }}
steps: permissions:
- uses: actions/checkout@v4 contents: read
- name: Guardrails release configuration steps:
$1
- name: Load guardrails definition
env: env:
PROFILE_RAW: ${{ github.event.inputs.profile }} GUARDRAILS_DEFINITION_URL: ${{ vars.MOKOSTANDARDS_GUARDRAILS_URL || 'https://raw.githubusercontent.com/mokoconsulting-tech/MokoStandards/main/repo-guardrails.definition.json' }}
FTP_HOST: ${{ secrets.FTP_HOST }} run: |
FTP_USER: ${{ secrets.FTP_USER }} set -euo pipefail
FTP_KEY: ${{ secrets.FTP_KEY }}
FTP_PASSWORD: ${{ secrets.FTP_PASSWORD }} url="${GUARDRAILS_DEFINITION_URL}"
FTP_PROTOCOL: ${{ secrets.FTP_PROTOCOL }} echo "### Guardrails policy source" >> "${GITHUB_STEP_SUMMARY}"
FTP_PORT: ${{ secrets.FTP_PORT }} echo "${url}" >> "${GITHUB_STEP_SUMMARY}"
if ! curl -fsSL "${url}" -o /tmp/repo_guardrails.definition.json; then
echo "Warning: Unable to fetch guardrails definition. Falling back to workflow defaults." >> "${GITHUB_STEP_SUMMARY}"
echo "GUARDRAILS_LOADED=false" >> "${GITHUB_ENV}"
exit 0
fi
python3 - <<'PY'
import json
import os
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()]
with open(env_path, "a", encoding="utf-8") as w:
w.write(f"{key}<<EOF
")
for v in vals:
w.write(v + "
")
w.write("EOF
")
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"))
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"))
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
")
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: | run: |
set -euo pipefail set -euo pipefail
profile="${PROFILE_RAW:-all}" profile="${PROFILE_RAW:-all}"
if [[ "$profile" == "scripts" || "$profile" == "repo" ]]; then case "${profile}" in
echo "Skipping release checks" >> "$GITHUB_STEP_SUMMARY" all|release|scripts|repo) ;;
*)
echo "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
exit 1
;;
esac
if [ "${profile}" = "scripts" ] || [ "${profile}" = "repo" ]; then
echo "Profile ${profile} selected. Skipping release configuration checks." >> "${GITHUB_STEP_SUMMARY}"
exit 0 exit 0
fi fi
required=(FTP_HOST FTP_USER FTP_KEY) required=("FTP_HOST" "FTP_USER" "FTP_KEY" "FTP_PATH")
optional=("FTP_PASSWORD" "FTP_PROTOCOL" "FTP_PORT" "FTP_PATH_SUFFIX")
missing=() missing=()
missing_optional=()
for k in "${required[@]}"; do for k in "${required[@]}"; do
[[ -z "${!k:-}" ]] && missing+=("$k") v="${!k:-}"
[ -z "${v}" ] && missing+=("${k}")
done done
if [[ "${#missing[@]}" -gt 0 ]]; then for k in "${optional[@]}"; do
echo "### Missing required release configuration" >> "$GITHUB_STEP_SUMMARY" v="${!k:-}"
for m in "${missing[@]}"; do echo "- $m" >> "$GITHUB_STEP_SUMMARY"; done [ -z "${v}" ] && missing_optional+=("${k}")
done
proto="${FTP_PROTOCOL:-sftp}"
if [ -n "${FTP_PROTOCOL:-}" ] && [ "${proto}" != "sftp" ]; then
missing+=("FTP_PROTOCOL_INVALID")
fi
if [ "${#missing_optional[@]}" -gt 0 ]; then
echo "### Missing optional release configuration" >> "${GITHUB_STEP_SUMMARY}"
for m in "${missing_optional[@]}"; do echo "- ${m}" >> "${GITHUB_STEP_SUMMARY}"; done
fi
if [ "${#missing[@]}" -gt 0 ]; then
echo "### Missing required release configuration" >> "${GITHUB_STEP_SUMMARY}"
for m in "${missing[@]}"; do echo "- ${m}" >> "${GITHUB_STEP_SUMMARY}"; done
echo "ERROR: Guardrails failed. Missing required release configuration." >> "${GITHUB_STEP_SUMMARY}"
exit 1 exit 1
fi fi
echoF echo "### Guardrails release configuration" >> "${GITHUB_STEP_SUMMARY}"
echo "Release configuration validated" >> "$GITHUB_STEP_SUMMARY" echo "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) ;;
*)
echo "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
exit 1
;;
esac
if [ "${profile}" = "scripts" ] || [ "${profile}" = "repo" ]; then
echo "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' "${FTP_KEY}" > "${key_file}"
printf '\n' >> "${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
echo "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}"
echo "### SFTP connectivity test" >> "${GITHUB_STEP_SUMMARY}"
echo "Attempting non-destructive SFTP session" >> "${GITHUB_STEP_SUMMARY}"
set +e
printf 'pwd
bye
' | sftp -oBatchMode=yes -oStrictHostKeyChecking=no -P "${port}" -i "${key_file}" "${FTP_USER}@${FTP_HOST}" >/tmp/sftp_check.log 2>&1
sftp_rc=$?
set -e
echo "### SFTP connectivity result" >> "${GITHUB_STEP_SUMMARY}"
if [ "${sftp_rc}" -eq 0 ]; then
echo "Status: SUCCESS" >> "${GITHUB_STEP_SUMMARY}"
exit 0
fi
echo "Status: FAILED (exit code ${sftp_rc})" >> "${GITHUB_STEP_SUMMARY}"
echo "" >> "${GITHUB_STEP_SUMMARY}"
echo "Last SFTP output" >> "${GITHUB_STEP_SUMMARY}"
tail -n 20 /tmp/sftp_check.log >> "${GITHUB_STEP_SUMMARY}" || true
exit 1
scripts_config: scripts_config:
name: Scripts and tooling name: Scripts and tooling
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15
needs: [access_check] needs: [access_check]
if: ${{ needs.access_check.outputs.allowed == 'true' }} if: ${{ needs.access_check.outputs.allowed == 'true' }}
steps: permissions:
- uses: actions/checkout@v4 contents: read
- name: Validate scripts governance steps:
$1
- name: Load guardrails definition
env: env:
PROFILE_RAW: ${{ github.event.inputs.profile }} 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}"
echo "### Guardrails policy source" >> "${GITHUB_STEP_SUMMARY}"
echo "${url}" >> "${GITHUB_STEP_SUMMARY}"
if ! curl -fsSL "${url}" -o /tmp/repo_guardrails.definition.json; then
echo "Warning: Unable to fetch guardrails definition. Falling back to workflow defaults." >> "${GITHUB_STEP_SUMMARY}"
echo "GUARDRAILS_LOADED=false" >> "${GITHUB_ENV}"
exit 0
fi
python3 - <<'PY'
import json
import os
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()]
with open(env_path, "a", encoding="utf-8") as w:
w.write(f"{key}<<EOF\n")
for v in vals:
w.write(f"{v}\n")
w.write("EOF\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"))
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"))
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: Guardrails scripts folder governance
env:
PROFILE_RAW: "${{ github.event.inputs.profile }}"
run: | run: |
set -euo pipefail set -euo pipefail
profile="${PROFILE_RAW:-all}" profile="${PROFILE_RAW:-all}"
[[ "$profile" == "release" || "$profile" == "repo" ]] && exit 0 case "${profile}" in
all|release|scripts|repo) ;;
required_dirs=(scripts/fix scripts/lib scripts/release scripts/run scripts/validate) *)
missing_dirs=() echo "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
for d in "${required_dirs[@]}"; do
[[ ! -d "$d" ]] && missing_dirs+=("$d/")
done
if [[ "${#missing_dirs[@]}" -gt 0 ]]; then
echo "### Missing script directories" >> "$GITHUB_STEP_SUMMARY"
for d in "${missing_dirs[@]}"; do echo "- $d" >> "$GITHUB_STEP_SUMMARY"; done
exit 1 exit 1
;;
esac
if [ "${profile}" = "release" ] || [ "${profile}" = "repo" ]; then
echo "Profile ${profile} selected. Skipping scripts checks." >> "${GITHUB_STEP_SUMMARY}"
exit 0
fi fi
echo "Scripts governance validated" >> "$GITHUB_STEP_SUMMARY" # scripts/ is OPTIONAL and informational only
if [ ! -d "scripts" ]; then
echo "### Scripts folder not present" >> "${GITHUB_STEP_SUMMARY}"
echo "Warning: scripts/ directory is optional. No scripts governance enforced." >> "${GITHUB_STEP_SUMMARY}"
exit 0
fi
required_script_dirs=(
"scripts/fix"
"scripts/lib"
"scripts/release"
"scripts/run"
"scripts/validate"
)
optional_script_dirs=(
"scripts/config"
"scripts/tools"
"scripts/docs"
)
allowed_script_dirs=(
"scripts"
"scripts/fix"
"scripts/lib"
"scripts/release"
"scripts/run"
"scripts/validate"
"scripts/config"
"scripts/tools"
"scripts/docs"
)
missing_dirs=()
unapproved_dirs=()
for d in "${required_script_dirs[@]}"; do
[ ! -d "${d}" ] && missing_dirs+=("${d}/")
done
while IFS= read -r d; do
allowed=false
for a in "${allowed_script_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
echo "### Scripts governance warnings" >> "${GITHUB_STEP_SUMMARY}"
echo "Missing recommended script directories:" >> "${GITHUB_STEP_SUMMARY}"
for m in "${missing_dirs[@]}"; do echo "- ${m}" >> "${GITHUB_STEP_SUMMARY}"; done
fi
if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
echo "### Scripts governance warnings" >> "${GITHUB_STEP_SUMMARY}"
echo "Unapproved script directories detected:" >> "${GITHUB_STEP_SUMMARY}"
for m in "${unapproved_dirs[@]}"; do echo "- ${m}" >> "${GITHUB_STEP_SUMMARY}"; done
fi
{
echo "### Scripts governance summary"
echo "| Area | Status | Notes |"
echo "|------|--------|-------|"
if [ "${#missing_dirs[@]}" -gt 0 ]; then
echo "| Recommended directories | Warning | Missing recommended subfolders |"
else
echo "| Recommended directories | OK | All recommended subfolders present |"
fi
if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
echo "| Directory policy | Warning | Unapproved directories detected |"
else
echo "| Directory policy | OK | No unapproved directories |"
fi
echo "| Enforcement mode | Advisory | scripts folder is optional |"
} >> "${GITHUB_STEP_SUMMARY}"
echo "Scripts governance completed in advisory mode." >> "${GITHUB_STEP_SUMMARY}"
repo_health: repo_health:
name: Repository health name: Repository health
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15
needs: [access_check] needs: [access_check]
if: ${{ needs.access_check.outputs.allowed == 'true' }} if: ${{ needs.access_check.outputs.allowed == 'true' }}
steps: permissions:
- uses: actions/checkout@v4 contents: read
- name: Repository health checks steps:
$1
- name: Load guardrails definition
env: env:
PROFILE_RAW: ${{ github.event.inputs.profile }} 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}"
echo "### Guardrails policy source" >> "${GITHUB_STEP_SUMMARY}"
echo "${url}" >> "${GITHUB_STEP_SUMMARY}"
if ! curl -fsSL "${url}" -o /tmp/repo_guardrails.definition.json; then
echo "Warning: Unable to fetch guardrails definition. Falling back to workflow defaults." >> "${GITHUB_STEP_SUMMARY}"
echo "GUARDRAILS_LOADED=false" >> "${GITHUB_ENV}"
exit 0
fi
python3 - <<'PY'
import json
import os
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()]
with open(env_path, "a", encoding="utf-8") as w:
w.write(f"{key}<<EOF\n")
for v in vals:
w.write(f"{v}\n")
w.write("EOF\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"))
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"))
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: Repo health checks
env:
PROFILE_RAW: "${{ github.event.inputs.profile }}"
run: | run: |
set -euo pipefail set -euo pipefail
profile="${PROFILE_RAW:-all}" profile="${PROFILE_RAW:-all}"
[[ "$profile" == "release" || "$profile" == "scripts" ]] && exit 0 case "${profile}" in
all|release|scripts|repo) ;;
*)
echo "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
exit 1
;;
esac
required_files=(README.md LICENSE CHANGELOG.md CONTRIBUTING.md CODE_OF_CONDUCT.md TODO.md docs/docs-index.md) if [ "${profile}" = "release" ] || [ "${profile}" = "scripts" ]; then
missing=() echo "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"
)
missing_required=()
missing_optional=()
for f in "${required_files[@]}"; do for f in "${required_files[@]}"; do
[[ ! -f "$f" ]] && missing+=("$f") [ ! -f "${f}" ] && missing_required+=("${f}")
done done
if [[ "${#missing[@]}" -gt 0 ]]; then for f in "${optional_files[@]}"; do
echo "### Missing required repository artifacts" >> "$GITHUB_STEP_SUMMARY" [ ! -f "${f}" ] && missing_optional+=("${f}")
for f in "${missing[@]}"; do echo "- $f" >> "$GITHUB_STEP_SUMMARY"; done done
for p in "${required_paths[@]}"; do
[ ! -d "${p}" ] && missing_required+=("${p}/")
done
if [ -d "src" ]; then
missing_required+=("src/ (disallowed, use dev/ only)")
fi
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/<version>)")
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 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"
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", "").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,
"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
)"
{
printf "### Guardrails repository health\n\n"
printf "### Guardrails report (JSON)\n"
printf "```json\n"
printf "%s\n" "${report_json}"
printf "```\n"
} >> "${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 exit 1
fi fi
echo "Repository health validated" >> "$GITHUB_STEP_SUMMARY" 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 "Repository health guardrails passed." >> "${GITHUB_STEP_SUMMARY}"
\n