882
.github/workflows/ci.yml
vendored
882
.github/workflows/ci.yml
vendored
@@ -1,103 +1,847 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
# ============================================================================
|
||||||
|
# Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
#
|
#
|
||||||
# This file is part of a Moko Consulting project.
|
# This file is part of a Moko Consulting project.
|
||||||
#
|
#
|
||||||
# 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.CI
|
# INGROUP: MokoStandards.Validation
|
||||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
||||||
# PATH: /.github/workflows/ci.yml
|
# PATH: /.github/workflows/repo_health.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Continuous integration workflow enforcing repository standards.
|
# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts.
|
||||||
# NOTE:
|
# NOTE: Field is user-managed.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
name: Continuous Integration
|
name: Repo Health
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: repo-health-${{ github.repository }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
profile:
|
||||||
|
description: Which configuration profile to validate. release checks SFTP variables used by release pipeline. scripts checks baseline script prerequisites. repo runs repository health only. al[...]
|
||||||
|
required: true
|
||||||
|
default: all
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- all
|
||||||
|
- release
|
||||||
|
- scripts
|
||||||
|
- repo
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- .github/workflows/**
|
||||||
|
- scripts/**
|
||||||
|
- docs/**
|
||||||
|
- dev/**
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
paths:
|
||||||
|
- .github/workflows/**
|
||||||
|
- scripts/**
|
||||||
|
- docs/**
|
||||||
- dev/**
|
- dev/**
|
||||||
- rc/**
|
|
||||||
- version/**
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- dev/**
|
|
||||||
- rc/**
|
|
||||||
- version/**
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
jobs:
|
|
||||||
ci:
|
|
||||||
name: Repository Validation Pipeline
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CI: true
|
# Global policy variables baked into workflow
|
||||||
PROFILE: all
|
ALLOWED_SFTP_PROTOCOLS: sftp
|
||||||
|
|
||||||
|
# Release policy
|
||||||
|
RELEASE_REQUIRED_VARS: FTP_HOST,FTP_USER,FTP_PATH
|
||||||
|
RELEASE_OPTIONAL_VARS: FTP_KEY,FTP_PASSWORD,FTP_PROTOCOL,FTP_PORT,FTP_PATH_SUFFIX
|
||||||
|
|
||||||
|
# Scripts governance policy
|
||||||
|
# Note: directories listed without a trailing slash.
|
||||||
|
SCRIPTS_REQUIRED_DIRS:
|
||||||
|
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
|
||||||
|
|
||||||
|
# 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,.github/workflows/,src/
|
||||||
|
REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/,dev/
|
||||||
|
REPO_DISALLOWED_DIRS:
|
||||||
|
REPO_DISALLOWED_FILES: TODO.md,todo.md
|
||||||
|
|
||||||
|
# Extended checks toggles
|
||||||
|
EXTENDED_CHECKS: "true"
|
||||||
|
|
||||||
|
# Operational toggles
|
||||||
|
SFTP_VERBOSE: "false"
|
||||||
|
|
||||||
|
# File / directory variables (moved to top-level env)
|
||||||
|
DOCS_INDEX: docs/docs-index.md
|
||||||
|
SCRIPT_DIR: scripts
|
||||||
|
WORKFLOWS_DIR: .github/workflows
|
||||||
|
SHELLCHECK_PATTERN: '*.sh'
|
||||||
|
SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml'
|
||||||
|
|
||||||
|
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:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Check actor permission (admin only)
|
||||||
|
id: perm
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
const res = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||||
|
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: ${context.actor}`);
|
||||||
|
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. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
release_config:
|
||||||
|
name: Release configuration
|
||||||
|
needs: access_check
|
||||||
|
if: ${{ needs.access_check.outputs.allowed == 'true' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Normalize line endings
|
- name: Guardrails release 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: |
|
||||||
git config --global core.autocrlf false
|
set -euo pipefail
|
||||||
|
|
||||||
- name: Verify script executability
|
profile="${PROFILE_RAW:-all}"
|
||||||
run: |
|
case "${profile}" in
|
||||||
chmod +x scripts/**/*.sh || true
|
all|release|scripts|repo) ;;
|
||||||
|
*)
|
||||||
|
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
- name: Required validations
|
if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Release configuration'
|
||||||
|
printf '%s\n' "Profile: ${profile}"
|
||||||
|
printf '%s\n' 'Status: SKIPPED'
|
||||||
|
printf '%s\n' 'Reason: profile excludes release validation'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
IFS=',' read -r -a required <<< "${RELEASE_REQUIRED_VARS}"
|
||||||
|
IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_VARS}"
|
||||||
|
IFS=',' read -r -a allowed_proto <<< "${ALLOWED_SFTP_PROTOCOLS}"
|
||||||
|
|
||||||
|
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}"
|
||||||
|
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
|
||||||
|
|
||||||
|
target_path="${FTP_PATH:-}"
|
||||||
|
if [ -n "${FTP_PATH_SUFFIX:-}" ]; then
|
||||||
|
target_path="${target_path%/}/${FTP_PATH_SUFFIX#/}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
auth_method='none'
|
||||||
|
[ -n "${FTP_KEY:-}" ] && auth_method='key'
|
||||||
|
[ -z "${FTP_KEY:-}" ] && [ -n "${FTP_PASSWORD:-}" ] && auth_method='password'
|
||||||
|
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Release configuration'
|
||||||
|
printf '%s\n' "Profile: ${profile}"
|
||||||
|
printf '%s\n' '| Control | Value |'
|
||||||
|
printf '%s\n' '|---|---|'
|
||||||
|
printf '%s\n' "| Protocol | ${proto} |"
|
||||||
|
printf '%s\n' "| Port | ${FTP_PORT:-22} |"
|
||||||
|
printf '%s\n' "| Auth | ${auth_method} |"
|
||||||
|
printf '%s\n' "| Path (resolved) | ${target_path} |"
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
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}"
|
||||||
|
else
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Optional release configuration'
|
||||||
|
printf '%s\n' 'None missing.'
|
||||||
|
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' '### Release configuration result'
|
||||||
|
printf '%s\n' 'Status: OK'
|
||||||
|
printf '%s\n' 'All required release variables present.'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${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 }}
|
||||||
|
FTP_PATH: ${{ secrets.FTP_PATH }}
|
||||||
|
FTP_PATH_SUFFIX: ${{ vars.FTP_PATH_SUFFIX }}
|
||||||
run: |
|
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' '### SFTP connectivity'
|
||||||
|
printf '%s\n' "Profile: ${profile}"
|
||||||
|
printf '%s\n' 'Status: SKIPPED'
|
||||||
|
printf '%s\n' 'Reason: profile excludes release validation'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
port="${FTP_PORT:-22}"
|
||||||
|
|
||||||
|
target_path="${FTP_PATH}"
|
||||||
|
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)
|
||||||
|
|
||||||
|
auth_method='none'
|
||||||
|
if [ -n "${FTP_KEY:-}" ]; then
|
||||||
|
auth_method='key'
|
||||||
|
elif [ -n "${FTP_PASSWORD:-}" ]; then
|
||||||
|
auth_method='password'
|
||||||
|
fi
|
||||||
|
|
||||||
|
{
|
||||||
|
printf '%s\n' '### SFTP connectivity'
|
||||||
|
printf '%s\n' '| Control | Value |'
|
||||||
|
printf '%s\n' '|---|---|'
|
||||||
|
printf '%s\n' "| Host | ${FTP_HOST} |"
|
||||||
|
printf '%s\n' "| User | ${FTP_USER} |"
|
||||||
|
printf '%s\n' "| Port | ${port} |"
|
||||||
|
printf '%s\n' "| Auth | ${auth_method} |"
|
||||||
|
printf '%s\n' "| Path (resolved) | ${target_path} |"
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
sftp_cmds="$(printf 'cd %s\npwd\nbye\n' "${target_path}")"
|
||||||
|
|
||||||
|
set +e
|
||||||
|
if [ -n "${FTP_KEY:-}" ]; then
|
||||||
|
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\n' "${first_line}" | grep -q '^PuTTY-User-Key-File-'; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### SFTP connectivity result'
|
||||||
|
printf '%s\n' 'Status: FAILED'
|
||||||
|
printf '%s\n' 'Reason: FTP_KEY appears to be a PuTTY PPK. Provide an OpenSSH private key.'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
ssh-keygen -p -P "${FTP_PASSWORD}" -N '' -f "${key_file}" >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
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=$?
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
{
|
||||||
|
printf '%s\n' '### SFTP connectivity result'
|
||||||
|
printf '%s\n' 'Status: FAILED'
|
||||||
|
printf '%s\n' 'Reason: No FTP_KEY or FTP_PASSWORD provided for SFTP authentication.'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
scripts/validate/manifest.sh
|
|
||||||
scripts/validate/xml_wellformed.sh
|
|
||||||
|
|
||||||
- name: Optional validations
|
|
||||||
run: |
|
|
||||||
set +e
|
|
||||||
|
|
||||||
scripts/validate/changelog.sh
|
|
||||||
scripts/validate/language_structure.sh
|
|
||||||
scripts/validate/license_headers.sh
|
|
||||||
scripts/validate/no_secrets.sh
|
|
||||||
scripts/validate/paths.sh
|
|
||||||
scripts/validate/php_syntax.sh
|
|
||||||
scripts/validate/tabs.sh
|
|
||||||
scripts/validate/version_alignment.sh
|
|
||||||
|
|
||||||
- name: CI summary
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
{
|
{
|
||||||
echo "### CI Execution Summary"
|
printf '%s\n' '### SFTP connectivity result'
|
||||||
echo ""
|
if [ "${sftp_rc}" -eq 0 ]; then
|
||||||
echo "- Repository: $GITHUB_REPOSITORY"
|
printf '%s\n' 'Status: SUCCESS'
|
||||||
echo "- Branch: $GITHUB_REF_NAME"
|
printf '%s\n' 'Validated host connectivity and remote path access.'
|
||||||
echo "- Commit: $GITHUB_SHA"
|
else
|
||||||
echo "- Runner: ubuntu-latest"
|
printf '%s\n' "Status: FAILED (exit code ${sftp_rc})"
|
||||||
echo ""
|
printf '\n'
|
||||||
echo "CI completed. Review logs above for validation outcomes."
|
printf '%s\n' 'Last SFTP output'
|
||||||
} >> "$GITHUB_STEP_SUMMARY"
|
tail -n 60 /tmp/sftp_check.log || true
|
||||||
|
fi
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
[ "${sftp_rc}" -eq 0 ] || exit 1
|
||||||
|
|
||||||
|
scripts_governance:
|
||||||
|
name: Scripts governance
|
||||||
|
needs: access_check
|
||||||
|
if: ${{ needs.access_check.outputs.allowed == 'true' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Scripts folder 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}" = 'repo' ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Scripts governance'
|
||||||
|
printf '%s\n' "Profile: ${profile}"
|
||||||
|
printf '%s\n' 'Status: SKIPPED'
|
||||||
|
printf '%s\n' 'Reason: profile excludes scripts governance'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "${SCRIPT_DIR}" ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Scripts governance'
|
||||||
|
printf '%s\n' 'Status: OK (advisory)'
|
||||||
|
printf '%s\n' 'scripts/ directory not present. No scripts governance enforced.'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"
|
||||||
|
IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}"
|
||||||
|
|
||||||
|
missing_dirs=()
|
||||||
|
unapproved_dirs=()
|
||||||
|
|
||||||
|
for d in "${required_dirs[@]}"; do
|
||||||
|
req="${d%/}"
|
||||||
|
[ ! -d "${req}" ] && missing_dirs+=("${req}/")
|
||||||
|
done
|
||||||
|
|
||||||
|
while IFS= read -r d; do
|
||||||
|
allowed=false
|
||||||
|
for a in "${allowed_dirs[@]}"; do
|
||||||
|
a_norm="${a%/}"
|
||||||
|
[ "${d%/}" = "${a_norm}" ] && allowed=true
|
||||||
|
done
|
||||||
|
[ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/")
|
||||||
|
done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##')
|
||||||
|
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Scripts governance'
|
||||||
|
printf '%s\n' "Profile: ${profile}"
|
||||||
|
printf '%s\n' '| Area | Status | Notes |'
|
||||||
|
printf '%s\n' '|---|---|---|'
|
||||||
|
|
||||||
|
if [ "${#missing_dirs[@]}" -gt 0 ]; then
|
||||||
|
printf '%s\n' '| Required directories | Warning | Missing required subfolders |'
|
||||||
|
else
|
||||||
|
printf '%s\n' '| Required directories | OK | All required 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'
|
||||||
|
|
||||||
|
if [ "${#missing_dirs[@]}" -gt 0 ]; then
|
||||||
|
printf '%s\n' 'Missing required script directories:'
|
||||||
|
for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done
|
||||||
|
printf '\n'
|
||||||
|
else
|
||||||
|
printf '%s\n' 'Missing required script directories: none.'
|
||||||
|
printf '\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
|
||||||
|
printf '%s\n' 'Unapproved script directories detected:'
|
||||||
|
for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done
|
||||||
|
printf '\n'
|
||||||
|
else
|
||||||
|
printf '%s\n' 'Unapproved script directories detected: none.'
|
||||||
|
printf '\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' 'Scripts governance completed in advisory mode.'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
repo_health:
|
||||||
|
name: Repository health
|
||||||
|
needs: access_check
|
||||||
|
if: ${{ needs.access_check.outputs.allowed == 'true' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Repository 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' '### Repository health'
|
||||||
|
printf '%s\n' "Profile: ${profile}"
|
||||||
|
printf '%s\n' 'Status: SKIPPED'
|
||||||
|
printf '%s\n' 'Reason: profile excludes repository health'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}"
|
||||||
|
IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}"
|
||||||
|
IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"
|
||||||
|
IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES}"
|
||||||
|
|
||||||
|
missing_required=()
|
||||||
|
missing_optional=()
|
||||||
|
|
||||||
|
for item in "${required_artifacts[@]}"; do
|
||||||
|
if printf '%s' "${item}" | grep -q '/$'; then
|
||||||
|
d="${item%/}"
|
||||||
|
[ ! -d "${d}" ] && missing_required+=("${item}")
|
||||||
|
else
|
||||||
|
[ ! -f "${item}" ] && missing_required+=("${item}")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
for f in "${optional_files[@]}"; do
|
||||||
|
[ ! -f "${f}" ] && missing_optional+=("${f}")
|
||||||
|
done
|
||||||
|
|
||||||
|
for d in "${disallowed_dirs[@]}"; do
|
||||||
|
d_norm="${d%/}"
|
||||||
|
[ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)")
|
||||||
|
done
|
||||||
|
|
||||||
|
for f in "${disallowed_files[@]}"; do
|
||||||
|
[ -f "${f}" ] && missing_required+=("${f} (disallowed)")
|
||||||
|
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/<version>)")
|
||||||
|
fi
|
||||||
|
|
||||||
|
content_warnings=()
|
||||||
|
|
||||||
|
if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then
|
||||||
|
content_warnings+=("CHANGELOG.md missing '# Changelog' header")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then
|
||||||
|
content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)")
|
||||||
|
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' '### Repository health'
|
||||||
|
printf '%s\n' "Profile: ${profile}"
|
||||||
|
printf '%s\n' '| Metric | Value |'
|
||||||
|
printf '%s\n' '|---|---|'
|
||||||
|
printf '%s\n' "| Missing required | ${#missing_required[@]} |"
|
||||||
|
printf '%s\n' "| Missing optional | ${#missing_optional[@]} |"
|
||||||
|
printf '%s\n' "| Content warnings | ${#content_warnings[@]} |"
|
||||||
|
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.'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${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
|
||||||
|
|
||||||
|
extended_enabled="${EXTENDED_CHECKS:-true}"
|
||||||
|
extended_findings=()
|
||||||
|
|
||||||
|
if [ "${extended_enabled}" = 'true' ]; then
|
||||||
|
# CODEOWNERS presence
|
||||||
|
if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then
|
||||||
|
:
|
||||||
|
else
|
||||||
|
extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Workflow pinning advisory: flag uses @main/@master
|
||||||
|
if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then
|
||||||
|
bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)"
|
||||||
|
if [ -n "${bad_refs}" ]; then
|
||||||
|
extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt")
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Workflow pinning advisory'
|
||||||
|
printf '%s\n' 'Found uses: entries pinned to main/master:'
|
||||||
|
printf '%s\n' '```'
|
||||||
|
printf '%s\n' "${bad_refs}"
|
||||||
|
printf '%s\n' '```'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Docs index link integrity (docs/docs-index.md)
|
||||||
|
if [ -f "${DOCS_INDEX}" ]; then
|
||||||
|
missing_links="$(python3 - <<'PY'
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
idx = os.environ.get('DOCS_INDEX', 'docs/docs-index.md')
|
||||||
|
base = os.getcwd()
|
||||||
|
|
||||||
|
bad = []
|
||||||
|
pat = re.compile(r'\[[^\]]+\]\(([^)]+)\)')
|
||||||
|
|
||||||
|
with open(idx, 'r', encoding='utf-8') as f:
|
||||||
|
for line in f:
|
||||||
|
for m in pat.findall(line):
|
||||||
|
link = m.strip()
|
||||||
|
if link.startswith('http://') or link.startswith('https://') or link.startswith('#') or link.startswith('mailto:'):
|
||||||
|
continue
|
||||||
|
if link.startswith('/'):
|
||||||
|
rel = link.lstrip('/')
|
||||||
|
else:
|
||||||
|
rel = os.path.normpath(os.path.join(os.path.dirname(idx), link))
|
||||||
|
rel = rel.split('#', 1)[0]
|
||||||
|
rel = rel.split('?', 1)[0]
|
||||||
|
if not rel:
|
||||||
|
continue
|
||||||
|
p = os.path.join(base, rel)
|
||||||
|
if not os.path.exists(p):
|
||||||
|
bad.append(rel)
|
||||||
|
|
||||||
|
print('\n'.join(sorted(set(bad))))
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
if [ -n "${missing_links}" ]; then
|
||||||
|
extended_findings+=("docs/docs-index.md contains broken relative links")
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Docs index link integrity'
|
||||||
|
printf '%s\n' 'Broken relative links:'
|
||||||
|
while IFS= read -r l; do [ -n "${l}" ] && printf '%s\n' "- ${l}"; done <<< "${missing_links}"
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ShellCheck advisory
|
||||||
|
if [ -d "${SCRIPT_DIR}" ]; then
|
||||||
|
if ! command -v shellcheck >/dev/null 2>&1; then
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y shellcheck >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
sc_out=''
|
||||||
|
while IFS= read -r shf; do
|
||||||
|
[ -z "${shf}" ] && continue
|
||||||
|
out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)"
|
||||||
|
if [ -n "${out_one}" ]; then
|
||||||
|
sc_out="${sc_out}${out_one}\n"
|
||||||
|
fi
|
||||||
|
done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort)
|
||||||
|
|
||||||
|
if [ -n "${sc_out}" ]; then
|
||||||
|
extended_findings+=("ShellCheck warnings detected (advisory)")
|
||||||
|
sc_head="$(printf '%s' "${sc_out}" | head -n 200)"
|
||||||
|
{
|
||||||
|
printf '%s\n' '### ShellCheck (advisory)'
|
||||||
|
printf '%s\n' '```'
|
||||||
|
printf '%s\n' "${sc_head}"
|
||||||
|
printf '%s\n' '```'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# SPDX header advisory for common source types
|
||||||
|
spdx_missing=()
|
||||||
|
IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}"
|
||||||
|
spdx_args=()
|
||||||
|
for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done
|
||||||
|
|
||||||
|
while IFS= read -r f; do
|
||||||
|
[ -z "${f}" ] && continue
|
||||||
|
if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then
|
||||||
|
spdx_missing+=("${f}")
|
||||||
|
fi
|
||||||
|
done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ "${#spdx_missing[@]}" -gt 0 ]; then
|
||||||
|
extended_findings+=("SPDX header missing in some tracked files (advisory)")
|
||||||
|
{
|
||||||
|
printf '%s\n' '### SPDX header advisory'
|
||||||
|
printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):'
|
||||||
|
for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Git hygiene advisory: branches older than 180 days (remote)
|
||||||
|
stale_cutoff_days=180
|
||||||
|
stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 [...]
|
||||||
|
if [ -n "${stale_branches}" ]; then
|
||||||
|
extended_findings+=("Stale remote branches detected (advisory)")
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Git hygiene advisory'
|
||||||
|
printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):"
|
||||||
|
while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}"
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Guardrails coverage matrix'
|
||||||
|
printf '%s\n' '| Domain | Status | Notes |'
|
||||||
|
printf '%s\n' '|---|---|---|'
|
||||||
|
printf '%s\n' '| Access control | OK | Admin-only execution gate |'
|
||||||
|
printf '%s\n' '| Release configuration | OK | Variable presence and protocol gate |'
|
||||||
|
printf '%s\n' '| SFTP connectivity | OK | Connectivity plus remote path resolution |'
|
||||||
|
printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |'
|
||||||
|
printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |'
|
||||||
|
printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |'
|
||||||
|
if [ "${extended_enabled}" = 'true' ]; then
|
||||||
|
if [ "${#extended_findings[@]}" -gt 0 ]; then
|
||||||
|
printf '%s\n' '| Extended checks | Warning | See extended findings below |'
|
||||||
|
else
|
||||||
|
printf '%s\n' '| Extended checks | OK | No findings |'
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
printf '%s\n' '| Extended checks | SKIPPED | EXTENDED_CHECKS disabled |'
|
||||||
|
fi
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
if [ "${extended_enabled}" = 'true' ] && [ "${#extended_findings[@]}" -gt 0 ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Extended findings (advisory)'
|
||||||
|
for f in "${extended_findings[@]}"; do printf '%s\n' "- ${f}"; done
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|||||||
Reference in New Issue
Block a user