This commit is contained in:
2026-01-03 13:22:10 -06:00
2 changed files with 93 additions and 72 deletions

View File

@@ -29,75 +29,75 @@
name: Continuous Integration name: Continuous Integration
on: on:
push: push:
branches: branches:
- main - main
- dev/** - dev/**
- rc/** - rc/**
- version/** - version/**
pull_request: pull_request:
branches: branches:
- main - main
- dev/** - dev/**
- rc/** - rc/**
- version/** - version/**
permissions: permissions:
contents: read contents: read
jobs: jobs:
ci: ci:
name: Repository Validation Pipeline name: Repository Validation Pipeline
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
CI: true CI: true
PROFILE: all PROFILE: all
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Normalize line endings - name: Normalize line endings
run: | run: |
git config --global core.autocrlf false git config --global core.autocrlf false
- name: Verify script executability - name: Verify script executability
run: | run: |
chmod +x scripts/**/*.sh || true chmod +x scripts/**/*.sh || true
- name: Required validations - name: Required validations
run: | run: |
set -e set -e
scripts/validate/manifest.sh scripts/validate/manifest.sh
scripts/validate/xml_wellformed.sh scripts/validate/xml_wellformed.sh
- name: Optional validations - name: Optional validations
run: | run: |
set +e set +e
scripts/validate/changelog.sh scripts/validate/changelog.sh
scripts/validate/language_structure.sh scripts/validate/language_structure.sh
scripts/validate/license_headers.sh scripts/validate/license_headers.sh
scripts/validate/no_secrets.sh scripts/validate/no_secrets.sh
scripts/validate/paths.sh scripts/validate/paths.sh
scripts/validate/php_syntax.sh scripts/validate/php_syntax.sh
scripts/validate/tabs.sh scripts/validate/tabs.sh
scripts/validate/version_alignment.sh scripts/validate/version_alignment.sh
- name: CI summary - name: CI summary
if: always() if: always()
run: | run: |
{ {
echo "### CI Execution Summary" echo "### CI Execution Summary"
echo "" echo ""
echo "- Repository: $GITHUB_REPOSITORY" echo "- Repository: $GITHUB_REPOSITORY"
echo "- Branch: $GITHUB_REF_NAME" echo "- Branch: $GITHUB_REF_NAME"
echo "- Commit: $GITHUB_SHA" echo "- Commit: $GITHUB_SHA"
echo "- Runner: ubuntu-latest" echo "- Runner: ubuntu-latest"
echo "" echo ""
echo "CI completed. Review logs above for validation outcomes." echo "CI completed. Review logs above for validation outcomes."
} >> "$GITHUB_STEP_SUMMARY" } >> "$GITHUB_STEP_SUMMARY"

View File

@@ -29,7 +29,7 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
profile: 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. all runs release, scripts, and repo health. 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 required: true
default: all default: all
type: choice type: choice
@@ -72,7 +72,7 @@ env:
# Repo health policy # Repo health policy
# Files are listed as-is; directories must end with a trailing slash. # 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_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_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/
REPO_DISALLOWED_DIRS: REPO_DISALLOWED_DIRS:
REPO_DISALLOWED_FILES: TODO.md,todo.md REPO_DISALLOWED_FILES: TODO.md,todo.md
@@ -82,6 +82,13 @@ env:
# Operational toggles # Operational toggles
SFTP_VERBOSE: "false" 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: jobs:
access_check: access_check:
name: Access control name: Access control
@@ -417,7 +424,7 @@ jobs:
exit 0 exit 0
fi fi
if [ ! -d scripts ]; then if [ ! -d "${SCRIPT_DIR}" ]; then
{ {
printf '%s\n' '### Scripts governance' printf '%s\n' '### Scripts governance'
printf '%s\n' 'Status: OK (advisory)' printf '%s\n' 'Status: OK (advisory)'
@@ -445,7 +452,7 @@ jobs:
[ "${d%/}" = "${a_norm}" ] && allowed=true [ "${d%/}" = "${a_norm}" ] && allowed=true
done done
[ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/") [ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/")
done < <(find scripts -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##') done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##')
{ {
printf '%s\n' '### Scripts governance' printf '%s\n' '### Scripts governance'
@@ -548,8 +555,14 @@ jobs:
fi fi
done done
# Optional entries: handle files and directories (trailing slash indicates dir)
for f in "${optional_files[@]}"; do for f in "${optional_files[@]}"; do
[ ! -f "${f}" ] && missing_optional+=("${f}") if printf '%s' "${f}" | grep -q '/$'; then
d="${f%/}"
[ ! -d "${d}" ] && missing_optional+=("${f}")
else
[ ! -f "${f}" ] && missing_optional+=("${f}")
fi
done done
for d in "${disallowed_dirs[@]}"; do for d in "${disallowed_dirs[@]}"; do
@@ -566,6 +579,8 @@ jobs:
dev_paths=() dev_paths=()
dev_branches=() dev_branches=()
# Look for remote branches matching origin/dev*.
# A plain origin/dev is considered invalid; we require dev/<something> branches.
while IFS= read -r b; do while IFS= read -r b; do
name="${b#origin/}" name="${b#origin/}"
if [ "${name}" = 'dev' ]; then if [ "${name}" = 'dev' ]; then
@@ -575,10 +590,12 @@ jobs:
fi fi
done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//') done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//')
# If there are no dev/* branches, fail the guardrail.
if [ "${#dev_paths[@]}" -eq 0 ]; then if [ "${#dev_paths[@]}" -eq 0 ]; then
missing_required+=("dev/* branch (e.g. dev/01.00.00)") missing_required+=("dev/* branch (e.g. dev/01.00.00)")
fi fi
# If a plain dev branch exists (origin/dev), flag it as invalid.
if [ "${#dev_branches[@]}" -gt 0 ]; then if [ "${#dev_branches[@]}" -gt 0 ]; then
missing_required+=("invalid branch dev (must be dev/<version>)") missing_required+=("invalid branch dev (must be dev/<version>)")
fi fi
@@ -682,8 +699,8 @@ jobs:
fi fi
# Workflow pinning advisory: flag uses @main/@master # Workflow pinning advisory: flag uses @main/@master
if ls .github/workflows/*.yml >/dev/null 2>&1; then 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' .github/workflows 2>/dev/null || true)" 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 if [ -n "${bad_refs}" ]; then
extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt") extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt")
{ {
@@ -698,12 +715,12 @@ jobs:
fi fi
# Docs index link integrity (docs/docs-index.md) # Docs index link integrity (docs/docs-index.md)
if [ -f 'docs/docs-index.md' ]; then if [ -f "${DOCS_INDEX}" ]; then
missing_links="$(python3 - <<'PY' missing_links="$(python3 - <<'PY'
import os import os
import re import re
idx = 'docs/docs-index.md' idx = os.environ.get('DOCS_INDEX', 'docs/docs-index.md')
base = os.getcwd() base = os.getcwd()
bad = [] bad = []
@@ -742,7 +759,7 @@ jobs:
fi fi
# ShellCheck advisory # ShellCheck advisory
if [ -d 'scripts' ]; then if [ -d "${SCRIPT_DIR}" ]; then
if ! command -v shellcheck >/dev/null 2>&1; then if ! command -v shellcheck >/dev/null 2>&1; then
sudo apt-get update -qq sudo apt-get update -qq
sudo apt-get install -y shellcheck >/dev/null sudo apt-get install -y shellcheck >/dev/null
@@ -755,7 +772,7 @@ jobs:
if [ -n "${out_one}" ]; then if [ -n "${out_one}" ]; then
sc_out="${sc_out}${out_one}\n" sc_out="${sc_out}${out_one}\n"
fi fi
done < <(find scripts -type f -name '*.sh' 2>/dev/null | sort) done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort)
if [ -n "${sc_out}" ]; then if [ -n "${sc_out}" ]; then
extended_findings+=("ShellCheck warnings detected (advisory)") extended_findings+=("ShellCheck warnings detected (advisory)")
@@ -772,12 +789,16 @@ jobs:
# SPDX header advisory for common source types # SPDX header advisory for common source types
spdx_missing=() 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 while IFS= read -r f; do
[ -z "${f}" ] && continue [ -z "${f}" ] && continue
if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then
spdx_missing+=("${f}") spdx_missing+=("${f}")
fi fi
done < <(git ls-files '*.sh' '*.php' '*.js' '*.ts' '*.css' '*.xml' '*.yml' '*.yaml' 2>/dev/null || true) done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true)
if [ "${#spdx_missing[@]}" -gt 0 ]; then if [ "${#spdx_missing[@]}" -gt 0 ]; then
extended_findings+=("SPDX header missing in some tracked files (advisory)") extended_findings+=("SPDX header missing in some tracked files (advisory)")
@@ -791,7 +812,7 @@ jobs:
# Git hygiene advisory: branches older than 180 days (remote) # Git hygiene advisory: branches older than 180 days (remote)
stale_cutoff_days=180 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 > days*86400) print $1}' | sed 's#^origin/##' | grep -v '^HEAD$' | head -n 50 || true)" 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 if [ -n "${stale_branches}" ]; then
extended_findings+=("Stale remote branches detected (advisory)") extended_findings+=("Stale remote branches detected (advisory)")
{ {