@mokoconsulting/mcp-mokogitea-api 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitattributes +94 -0
- package/.gitmessage +9 -0
- package/.mokogitea/ISSUE_TEMPLATE/adr.md +110 -0
- package/.mokogitea/ISSUE_TEMPLATE/bug_report.md +48 -0
- package/.mokogitea/ISSUE_TEMPLATE/config.yml +18 -0
- package/.mokogitea/ISSUE_TEMPLATE/documentation.md +52 -0
- package/.mokogitea/ISSUE_TEMPLATE/enterprise_support.md +85 -0
- package/.mokogitea/ISSUE_TEMPLATE/feature_request.md +51 -0
- package/.mokogitea/ISSUE_TEMPLATE/firewall-request.md +190 -0
- package/.mokogitea/ISSUE_TEMPLATE/mcp_api_integration.md +48 -0
- package/.mokogitea/ISSUE_TEMPLATE/mcp_connection_issue.md +67 -0
- package/.mokogitea/ISSUE_TEMPLATE/mcp_tool_request.md +49 -0
- package/.mokogitea/ISSUE_TEMPLATE/question.md +82 -0
- package/.mokogitea/ISSUE_TEMPLATE/rfc.md +126 -0
- package/.mokogitea/ISSUE_TEMPLATE/security.md +51 -0
- package/.mokogitea/ISSUE_TEMPLATE/version.md +24 -0
- package/.mokogitea/auto-assign.yml +76 -0
- package/.mokogitea/auto-dev-issue.yml +207 -0
- package/.mokogitea/auto-release.yml +337 -0
- package/.mokogitea/branch-protection.yml +251 -0
- package/.mokogitea/changelog-validation.yml +101 -0
- package/.mokogitea/codeql-analysis.yml +115 -0
- package/.mokogitea/copilot-agent.yml +44 -0
- package/.mokogitea/deploy-demo.yml +734 -0
- package/.mokogitea/deploy-dev.yml +700 -0
- package/.mokogitea/enterprise-firewall-setup.yml +758 -0
- package/.mokogitea/manifest.xml +25 -0
- package/.mokogitea/mcp-auto-release.yml +278 -0
- package/.mokogitea/mcp-build-test.yml +65 -0
- package/.mokogitea/mcp-sdk-check.yml +109 -0
- package/.mokogitea/mcp-tool-inventory.yml +61 -0
- package/.mokogitea/pr-branch-check.yml +90 -0
- package/.mokogitea/repository-cleanup.yml +525 -0
- package/.mokogitea/standards-compliance.yml +2614 -0
- package/.mokogitea/sync-version-on-merge.yml +133 -0
- package/.mokogitea/workflows/auto-assign.yml +76 -0
- package/.mokogitea/workflows/auto-bump.yml +66 -0
- package/.mokogitea/workflows/auto-dev-issue.yml +207 -0
- package/.mokogitea/workflows/auto-release.yml +341 -0
- package/.mokogitea/workflows/branch-cleanup.yml +48 -0
- package/.mokogitea/workflows/cascade-dev.yml +10 -0
- package/.mokogitea/workflows/changelog-validation.yml +101 -0
- package/.mokogitea/workflows/ci-generic.yml +204 -0
- package/.mokogitea/workflows/cleanup.yml +87 -0
- package/.mokogitea/workflows/codeql-analysis.yml +115 -0
- package/.mokogitea/workflows/copilot-agent.yml +44 -0
- package/.mokogitea/workflows/deploy-manual.yml +126 -0
- package/.mokogitea/workflows/enterprise-firewall-setup.yml +758 -0
- package/.mokogitea/workflows/gitleaks.yml +96 -0
- package/.mokogitea/workflows/issue-branch.yml +73 -0
- package/.mokogitea/workflows/mcp-auto-release.yml +280 -0
- package/.mokogitea/workflows/mcp-build-test.yml +65 -0
- package/.mokogitea/workflows/mcp-sdk-check.yml +109 -0
- package/.mokogitea/workflows/mcp-tool-inventory.yml +61 -0
- package/.mokogitea/workflows/notify.yml +70 -0
- package/.mokogitea/workflows/npm-publish.yml +51 -0
- package/.mokogitea/workflows/pr-check.yml +508 -0
- package/.mokogitea/workflows/pre-release.yml +11 -0
- package/.mokogitea/workflows/repo-health.yml +711 -0
- package/.mokogitea/workflows/repository-cleanup.yml +525 -0
- package/.mokogitea/workflows/security-audit.yml +82 -0
- package/.mokogitea/workflows/standards-compliance.yml +2614 -0
- package/.mokogitea/workflows/sync-version-on-merge.yml +130 -0
- package/.mokogitea/workflows/update-server.yml +312 -0
- package/CHANGELOG.md +145 -0
- package/CLAUDE.md +43 -0
- package/CONTRIBUTING.md +161 -0
- package/README.md +286 -0
- package/SECURITY.md +91 -0
- package/automation/ci-issue-reporter.sh +237 -0
- package/config.example.json +13 -0
- package/dist/client.d.ts +15 -0
- package/dist/client.js +104 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +48 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1119 -0
- package/dist/types.d.ts +20 -0
- package/dist/types.js +16 -0
- package/package.json +34 -0
- package/scripts/setup.mjs +40 -0
- package/src/client.ts +120 -0
- package/src/config.ts +58 -0
- package/src/index.ts +1712 -0
- package/src/types.ts +37 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
|
3
|
+
#
|
|
4
|
+
# This file is part of a Moko Consulting project.
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
7
|
+
#
|
|
8
|
+
# FILE INFORMATION
|
|
9
|
+
# DEFGROUP: Gitea.Workflow
|
|
10
|
+
# INGROUP: moko-platform.Validation
|
|
11
|
+
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
|
12
|
+
# PATH: /templates/workflows/joomla/repo_health.yml.template
|
|
13
|
+
# VERSION: 09.23.00
|
|
14
|
+
# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts.
|
|
15
|
+
# ============================================================================
|
|
16
|
+
|
|
17
|
+
name: "Generic: Repo Health"
|
|
18
|
+
|
|
19
|
+
defaults:
|
|
20
|
+
run:
|
|
21
|
+
shell: bash
|
|
22
|
+
|
|
23
|
+
on:
|
|
24
|
+
workflow_dispatch:
|
|
25
|
+
inputs:
|
|
26
|
+
profile:
|
|
27
|
+
description: 'Validation profile: all, scripts, or repo'
|
|
28
|
+
required: true
|
|
29
|
+
default: all
|
|
30
|
+
type: choice
|
|
31
|
+
options:
|
|
32
|
+
- all
|
|
33
|
+
- scripts
|
|
34
|
+
- repo
|
|
35
|
+
pull_request:
|
|
36
|
+
push:
|
|
37
|
+
|
|
38
|
+
permissions:
|
|
39
|
+
contents: read
|
|
40
|
+
|
|
41
|
+
env:
|
|
42
|
+
# Scripts governance policy
|
|
43
|
+
SCRIPTS_REQUIRED_DIRS:
|
|
44
|
+
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
|
|
45
|
+
|
|
46
|
+
# Repo health policy
|
|
47
|
+
REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.mokogitea/workflows/
|
|
48
|
+
REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/
|
|
49
|
+
REPO_DISALLOWED_DIRS:
|
|
50
|
+
REPO_DISALLOWED_FILES: TODO.md,todo.md
|
|
51
|
+
|
|
52
|
+
# Extended checks toggles
|
|
53
|
+
EXTENDED_CHECKS: "true"
|
|
54
|
+
|
|
55
|
+
# File / directory variables
|
|
56
|
+
DOCS_INDEX: docs/docs-index.md
|
|
57
|
+
SCRIPT_DIR: scripts
|
|
58
|
+
WORKFLOWS_DIR: .mokogitea/workflows
|
|
59
|
+
SHELLCHECK_PATTERN: '*.sh'
|
|
60
|
+
SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml'
|
|
61
|
+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
62
|
+
|
|
63
|
+
jobs:
|
|
64
|
+
access_check:
|
|
65
|
+
name: Access control
|
|
66
|
+
runs-on: ubuntu-latest
|
|
67
|
+
timeout-minutes: 10
|
|
68
|
+
permissions:
|
|
69
|
+
contents: read
|
|
70
|
+
|
|
71
|
+
outputs:
|
|
72
|
+
allowed: ${{ steps.perm.outputs.allowed }}
|
|
73
|
+
permission: ${{ steps.perm.outputs.permission }}
|
|
74
|
+
|
|
75
|
+
steps:
|
|
76
|
+
- name: Check actor permission (admin only)
|
|
77
|
+
id: perm
|
|
78
|
+
env:
|
|
79
|
+
TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
|
|
80
|
+
REPO: ${{ github.repository }}
|
|
81
|
+
ACTOR: ${{ github.actor }}
|
|
82
|
+
run: |
|
|
83
|
+
set -euo pipefail
|
|
84
|
+
ALLOWED=false
|
|
85
|
+
PERMISSION=unknown
|
|
86
|
+
METHOD=""
|
|
87
|
+
|
|
88
|
+
# Hardcoded authorized users — always allowed
|
|
89
|
+
case "$ACTOR" in
|
|
90
|
+
jmiller|gitea-actions[bot])
|
|
91
|
+
ALLOWED=true
|
|
92
|
+
PERMISSION=admin
|
|
93
|
+
METHOD="hardcoded allowlist"
|
|
94
|
+
;;
|
|
95
|
+
*)
|
|
96
|
+
# Detect platform and check permissions via API
|
|
97
|
+
API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}"
|
|
98
|
+
RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \
|
|
99
|
+
"${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}')
|
|
100
|
+
PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown")
|
|
101
|
+
if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "maintain" ] || [ "$PERMISSION" = "owner" ]; then
|
|
102
|
+
ALLOWED=true
|
|
103
|
+
fi
|
|
104
|
+
METHOD="collaborator API"
|
|
105
|
+
;;
|
|
106
|
+
esac
|
|
107
|
+
|
|
108
|
+
echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT"
|
|
109
|
+
echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT"
|
|
110
|
+
|
|
111
|
+
{
|
|
112
|
+
echo "## Access Authorization"
|
|
113
|
+
echo ""
|
|
114
|
+
echo "| Field | Value |"
|
|
115
|
+
echo "|-------|-------|"
|
|
116
|
+
echo "| **Actor** | \`${ACTOR}\` |"
|
|
117
|
+
echo "| **Repository** | \`${REPO}\` |"
|
|
118
|
+
echo "| **Permission** | \`${PERMISSION}\` |"
|
|
119
|
+
echo "| **Method** | ${METHOD} |"
|
|
120
|
+
echo "| **Authorized** | ${ALLOWED} |"
|
|
121
|
+
echo ""
|
|
122
|
+
if [ "$ALLOWED" = "true" ]; then
|
|
123
|
+
echo "${ACTOR} authorized (${METHOD})"
|
|
124
|
+
else
|
|
125
|
+
echo "${ACTOR} is NOT authorized. Requires admin or maintain role."
|
|
126
|
+
fi
|
|
127
|
+
} >> "${GITHUB_STEP_SUMMARY}"
|
|
128
|
+
|
|
129
|
+
- name: Deny execution when not permitted
|
|
130
|
+
if: ${{ steps.perm.outputs.allowed != 'true' }}
|
|
131
|
+
run: |
|
|
132
|
+
set -euo pipefail
|
|
133
|
+
printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}"
|
|
134
|
+
exit 1
|
|
135
|
+
|
|
136
|
+
scripts_governance:
|
|
137
|
+
name: Scripts governance
|
|
138
|
+
needs: access_check
|
|
139
|
+
if: ${{ needs.access_check.outputs.allowed == 'true' }}
|
|
140
|
+
runs-on: ubuntu-latest
|
|
141
|
+
timeout-minutes: 15
|
|
142
|
+
permissions:
|
|
143
|
+
contents: read
|
|
144
|
+
|
|
145
|
+
steps:
|
|
146
|
+
- name: Checkout
|
|
147
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
148
|
+
with:
|
|
149
|
+
fetch-depth: 0
|
|
150
|
+
|
|
151
|
+
- name: Scripts folder checks
|
|
152
|
+
env:
|
|
153
|
+
PROFILE_RAW: ${{ github.event.inputs.profile }}
|
|
154
|
+
run: |
|
|
155
|
+
set -euo pipefail
|
|
156
|
+
|
|
157
|
+
profile="${PROFILE_RAW:-all}"
|
|
158
|
+
case "${profile}" in
|
|
159
|
+
all|scripts|repo) ;;
|
|
160
|
+
*)
|
|
161
|
+
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
|
162
|
+
exit 1
|
|
163
|
+
;;
|
|
164
|
+
esac
|
|
165
|
+
|
|
166
|
+
if [ "${profile}" = 'repo' ]; then
|
|
167
|
+
{
|
|
168
|
+
printf '%s\n' '### Scripts governance'
|
|
169
|
+
printf '%s\n' "Profile: ${profile}"
|
|
170
|
+
printf '%s\n' 'Status: SKIPPED'
|
|
171
|
+
printf '%s\n' 'Reason: profile excludes scripts governance'
|
|
172
|
+
printf '\n'
|
|
173
|
+
} >> "${GITHUB_STEP_SUMMARY}"
|
|
174
|
+
exit 0
|
|
175
|
+
fi
|
|
176
|
+
|
|
177
|
+
if [ ! -d "${SCRIPT_DIR}" ]; then
|
|
178
|
+
{
|
|
179
|
+
printf '%s\n' '### Scripts governance'
|
|
180
|
+
printf '%s\n' 'Status: OK (advisory)'
|
|
181
|
+
printf '%s\n' 'scripts/ directory not present. No scripts governance enforced.'
|
|
182
|
+
printf '\n'
|
|
183
|
+
} >> "${GITHUB_STEP_SUMMARY}"
|
|
184
|
+
exit 0
|
|
185
|
+
fi
|
|
186
|
+
|
|
187
|
+
if [ -n "${SCRIPTS_REQUIRED_DIRS:-}" ]; then IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"; else required_dirs=(); fi
|
|
188
|
+
IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}"
|
|
189
|
+
|
|
190
|
+
missing_dirs=()
|
|
191
|
+
unapproved_dirs=()
|
|
192
|
+
|
|
193
|
+
for d in "${required_dirs[@]}"; do
|
|
194
|
+
req="${d%/}"
|
|
195
|
+
[ ! -d "${req}" ] && missing_dirs+=("${req}/")
|
|
196
|
+
done
|
|
197
|
+
|
|
198
|
+
while IFS= read -r d; do
|
|
199
|
+
allowed=false
|
|
200
|
+
for a in "${allowed_dirs[@]}"; do
|
|
201
|
+
a_norm="${a%/}"
|
|
202
|
+
[ "${d%/}" = "${a_norm}" ] && allowed=true
|
|
203
|
+
done
|
|
204
|
+
[ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/")
|
|
205
|
+
done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##')
|
|
206
|
+
|
|
207
|
+
{
|
|
208
|
+
printf '%s\n' '### Scripts governance'
|
|
209
|
+
printf '%s\n' "Profile: ${profile}"
|
|
210
|
+
printf '%s\n' '| Area | Status | Notes |'
|
|
211
|
+
printf '%s\n' '|---|---|---|'
|
|
212
|
+
|
|
213
|
+
if [ "${#missing_dirs[@]}" -gt 0 ]; then
|
|
214
|
+
printf '%s\n' '| Required directories | Warning | Missing required subfolders |'
|
|
215
|
+
else
|
|
216
|
+
printf '%s\n' '| Required directories | OK | All required subfolders present |'
|
|
217
|
+
fi
|
|
218
|
+
|
|
219
|
+
if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
|
|
220
|
+
printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |'
|
|
221
|
+
else
|
|
222
|
+
printf '%s\n' '| Directory policy | OK | No unapproved directories |'
|
|
223
|
+
fi
|
|
224
|
+
|
|
225
|
+
printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |'
|
|
226
|
+
printf '\n'
|
|
227
|
+
|
|
228
|
+
if [ "${#missing_dirs[@]}" -gt 0 ]; then
|
|
229
|
+
printf '%s\n' 'Missing required script directories:'
|
|
230
|
+
for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done
|
|
231
|
+
printf '\n'
|
|
232
|
+
else
|
|
233
|
+
printf '%s\n' 'Missing required script directories: none.'
|
|
234
|
+
printf '\n'
|
|
235
|
+
fi
|
|
236
|
+
|
|
237
|
+
if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
|
|
238
|
+
printf '%s\n' 'Unapproved script directories detected:'
|
|
239
|
+
for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done
|
|
240
|
+
printf '\n'
|
|
241
|
+
else
|
|
242
|
+
printf '%s\n' 'Unapproved script directories detected: none.'
|
|
243
|
+
printf '\n'
|
|
244
|
+
fi
|
|
245
|
+
|
|
246
|
+
printf '%s\n' 'Scripts governance completed in advisory mode.'
|
|
247
|
+
printf '\n'
|
|
248
|
+
} >> "${GITHUB_STEP_SUMMARY}"
|
|
249
|
+
|
|
250
|
+
repo_health:
|
|
251
|
+
name: Repository health
|
|
252
|
+
needs: access_check
|
|
253
|
+
if: ${{ needs.access_check.outputs.allowed == 'true' }}
|
|
254
|
+
runs-on: ubuntu-latest
|
|
255
|
+
timeout-minutes: 20
|
|
256
|
+
permissions:
|
|
257
|
+
contents: read
|
|
258
|
+
|
|
259
|
+
steps:
|
|
260
|
+
- name: Checkout
|
|
261
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
262
|
+
with:
|
|
263
|
+
fetch-depth: 0
|
|
264
|
+
|
|
265
|
+
- name: Repository health checks
|
|
266
|
+
env:
|
|
267
|
+
PROFILE_RAW: ${{ github.event.inputs.profile }}
|
|
268
|
+
run: |
|
|
269
|
+
set -euo pipefail
|
|
270
|
+
|
|
271
|
+
profile="${PROFILE_RAW:-all}"
|
|
272
|
+
case "${profile}" in
|
|
273
|
+
all|scripts|repo) ;;
|
|
274
|
+
*)
|
|
275
|
+
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
|
276
|
+
exit 1
|
|
277
|
+
;;
|
|
278
|
+
esac
|
|
279
|
+
|
|
280
|
+
if [ "${profile}" = 'scripts' ]; then
|
|
281
|
+
{
|
|
282
|
+
printf '%s\n' '### Repository health'
|
|
283
|
+
printf '%s\n' "Profile: ${profile}"
|
|
284
|
+
printf '%s\n' 'Status: SKIPPED'
|
|
285
|
+
printf '%s\n' 'Reason: profile excludes repository health'
|
|
286
|
+
printf '\n'
|
|
287
|
+
} >> "${GITHUB_STEP_SUMMARY}"
|
|
288
|
+
exit 0
|
|
289
|
+
fi
|
|
290
|
+
|
|
291
|
+
IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}"
|
|
292
|
+
IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}"
|
|
293
|
+
if [ -n "${REPO_DISALLOWED_DIRS:-}" ]; then IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"; else disallowed_dirs=(); fi
|
|
294
|
+
IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES:-}"
|
|
295
|
+
|
|
296
|
+
missing_required=()
|
|
297
|
+
missing_optional=()
|
|
298
|
+
|
|
299
|
+
# Source directory: src/ or htdocs/ (either is valid for extension repos)
|
|
300
|
+
SOURCE_DIR=""
|
|
301
|
+
if [ -d "src" ]; then
|
|
302
|
+
SOURCE_DIR="src"
|
|
303
|
+
elif [ -d "htdocs" ]; then
|
|
304
|
+
SOURCE_DIR="htdocs"
|
|
305
|
+
elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then
|
|
306
|
+
# Platform/tooling repos don't need src/
|
|
307
|
+
SOURCE_DIR=""
|
|
308
|
+
else
|
|
309
|
+
missing_required+=("src/ or htdocs/ (source directory required)")
|
|
310
|
+
fi
|
|
311
|
+
|
|
312
|
+
for item in "${required_artifacts[@]}"; do
|
|
313
|
+
if printf '%s' "${item}" | grep -q '/$'; then
|
|
314
|
+
d="${item%/}"
|
|
315
|
+
[ ! -d "${d}" ] && missing_required+=("${item}")
|
|
316
|
+
else
|
|
317
|
+
[ ! -f "${item}" ] && missing_required+=("${item}")
|
|
318
|
+
fi
|
|
319
|
+
done
|
|
320
|
+
|
|
321
|
+
for f in "${optional_files[@]}"; do
|
|
322
|
+
if printf '%s' "${f}" | grep -q '/$'; then
|
|
323
|
+
d="${f%/}"
|
|
324
|
+
[ ! -d "${d}" ] && missing_optional+=("${f}")
|
|
325
|
+
else
|
|
326
|
+
[ ! -f "${f}" ] && missing_optional+=("${f}")
|
|
327
|
+
fi
|
|
328
|
+
done
|
|
329
|
+
|
|
330
|
+
for d in "${disallowed_dirs[@]}"; do
|
|
331
|
+
d_norm="${d%/}"
|
|
332
|
+
[ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)")
|
|
333
|
+
done
|
|
334
|
+
|
|
335
|
+
for f in "${disallowed_files[@]}"; do
|
|
336
|
+
[ -f "${f}" ] && missing_required+=("${f} (disallowed)")
|
|
337
|
+
done
|
|
338
|
+
|
|
339
|
+
git fetch origin --prune
|
|
340
|
+
|
|
341
|
+
dev_paths=()
|
|
342
|
+
dev_branches=()
|
|
343
|
+
|
|
344
|
+
while IFS= read -r b; do
|
|
345
|
+
name="${b#origin/}"
|
|
346
|
+
if [ "${name}" = 'dev' ]; then
|
|
347
|
+
dev_branches+=("${name}")
|
|
348
|
+
else
|
|
349
|
+
dev_paths+=("${name}")
|
|
350
|
+
fi
|
|
351
|
+
done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//')
|
|
352
|
+
|
|
353
|
+
if [ "${#dev_paths[@]}" -eq 0 ] && [ "${#dev_branches[@]}" -eq 0 ]; then
|
|
354
|
+
missing_required+=("dev or dev/* branch")
|
|
355
|
+
fi
|
|
356
|
+
|
|
357
|
+
content_warnings=()
|
|
358
|
+
|
|
359
|
+
if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then
|
|
360
|
+
content_warnings+=("CHANGELOG.md missing '# Changelog' header")
|
|
361
|
+
fi
|
|
362
|
+
|
|
363
|
+
if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then
|
|
364
|
+
content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)")
|
|
365
|
+
fi
|
|
366
|
+
|
|
367
|
+
if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then
|
|
368
|
+
content_warnings+=("LICENSE does not look like a GPL text")
|
|
369
|
+
fi
|
|
370
|
+
|
|
371
|
+
if [ -f 'README.md' ] && ! grep -qiE 'moko|Moko' README.md; then
|
|
372
|
+
content_warnings+=("README.md missing expected brand keyword")
|
|
373
|
+
fi
|
|
374
|
+
|
|
375
|
+
export PROFILE_RAW="${profile}"
|
|
376
|
+
export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")"
|
|
377
|
+
export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")"
|
|
378
|
+
export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")"
|
|
379
|
+
|
|
380
|
+
report_json=$(printf '{"profile":"%s","missing_required":%d,"missing_optional":%d,"content_warnings":%d}' "$profile" "${#missing_required[@]}" "${#missing_optional[@]}" "${#content_warnings[@]}")
|
|
381
|
+
|
|
382
|
+
{
|
|
383
|
+
printf '%s\n' '### Repository health'
|
|
384
|
+
printf '%s\n' "Profile: ${profile}"
|
|
385
|
+
printf '%s\n' '| Metric | Value |'
|
|
386
|
+
printf '%s\n' '|---|---|'
|
|
387
|
+
printf '%s\n' "| Missing required | ${#missing_required[@]} |"
|
|
388
|
+
printf '%s\n' "| Missing optional | ${#missing_optional[@]} |"
|
|
389
|
+
printf '%s\n' "| Content warnings | ${#content_warnings[@]} |"
|
|
390
|
+
printf '\n'
|
|
391
|
+
|
|
392
|
+
printf '%s\n' '### Guardrails report (JSON)'
|
|
393
|
+
printf '%s\n' '```json'
|
|
394
|
+
printf '%s\n' "${report_json}"
|
|
395
|
+
printf '%s\n' '```'
|
|
396
|
+
printf '\n'
|
|
397
|
+
} >> "${GITHUB_STEP_SUMMARY}"
|
|
398
|
+
|
|
399
|
+
if [ "${#missing_required[@]}" -gt 0 ]; then
|
|
400
|
+
{
|
|
401
|
+
printf '%s\n' '### Missing required repo artifacts'
|
|
402
|
+
for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done
|
|
403
|
+
printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.'
|
|
404
|
+
printf '\n'
|
|
405
|
+
} >> "${GITHUB_STEP_SUMMARY}"
|
|
406
|
+
exit 1
|
|
407
|
+
fi
|
|
408
|
+
|
|
409
|
+
if [ "${#missing_optional[@]}" -gt 0 ]; then
|
|
410
|
+
{
|
|
411
|
+
printf '%s\n' '### Missing optional repo artifacts'
|
|
412
|
+
for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
|
|
413
|
+
printf '\n'
|
|
414
|
+
} >> "${GITHUB_STEP_SUMMARY}"
|
|
415
|
+
fi
|
|
416
|
+
|
|
417
|
+
if [ "${#content_warnings[@]}" -gt 0 ]; then
|
|
418
|
+
{
|
|
419
|
+
printf '%s\n' '### Repo content warnings'
|
|
420
|
+
for m in "${content_warnings[@]}"; do printf '%s\n' "- ${m}"; done
|
|
421
|
+
printf '\n'
|
|
422
|
+
} >> "${GITHUB_STEP_SUMMARY}"
|
|
423
|
+
fi
|
|
424
|
+
|
|
425
|
+
# -- Joomla-specific checks --
|
|
426
|
+
joomla_findings=()
|
|
427
|
+
|
|
428
|
+
MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)"
|
|
429
|
+
if [ -z "${MANIFEST}" ]; then
|
|
430
|
+
joomla_findings+=("Joomla XML manifest not found (no *.xml with <extension> tag)")
|
|
431
|
+
else
|
|
432
|
+
if ! grep -qP '<version>' "${MANIFEST}"; then
|
|
433
|
+
joomla_findings+=("XML manifest: <version> tag missing")
|
|
434
|
+
fi
|
|
435
|
+
if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then
|
|
436
|
+
joomla_findings+=("XML manifest: type attribute missing or invalid")
|
|
437
|
+
fi
|
|
438
|
+
if ! grep -qP '<name>' "${MANIFEST}"; then
|
|
439
|
+
joomla_findings+=("XML manifest: <name> tag missing")
|
|
440
|
+
fi
|
|
441
|
+
if ! grep -qP '<author>' "${MANIFEST}"; then
|
|
442
|
+
joomla_findings+=("XML manifest: <author> tag missing")
|
|
443
|
+
fi
|
|
444
|
+
if ! grep -qP '<namespace' "${MANIFEST}"; then
|
|
445
|
+
joomla_findings+=("XML manifest: <namespace> missing (required for Joomla 5+)")
|
|
446
|
+
fi
|
|
447
|
+
fi
|
|
448
|
+
|
|
449
|
+
INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)"
|
|
450
|
+
if [ "${INI_COUNT}" -eq 0 ]; then
|
|
451
|
+
joomla_findings+=("No .ini language files found")
|
|
452
|
+
fi
|
|
453
|
+
|
|
454
|
+
if [ ! -f 'updates.xml' ]; then
|
|
455
|
+
joomla_findings+=("updates.xml missing in root (required for Joomla update server)")
|
|
456
|
+
fi
|
|
457
|
+
|
|
458
|
+
if [ -n "${SOURCE_DIR}" ]; then
|
|
459
|
+
INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site")
|
|
460
|
+
for dir in "${INDEX_DIRS[@]}"; do
|
|
461
|
+
if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then
|
|
462
|
+
joomla_findings+=("${dir}/index.html missing (directory listing protection)")
|
|
463
|
+
fi
|
|
464
|
+
done
|
|
465
|
+
fi
|
|
466
|
+
|
|
467
|
+
if [ "${#joomla_findings[@]}" -gt 0 ]; then
|
|
468
|
+
{
|
|
469
|
+
printf '%s\n' '### Joomla extension checks'
|
|
470
|
+
printf '%s\n' '| Check | Status |'
|
|
471
|
+
printf '%s\n' '|---|---|'
|
|
472
|
+
for f in "${joomla_findings[@]}"; do
|
|
473
|
+
printf '%s\n' "| ${f} | Warning |"
|
|
474
|
+
done
|
|
475
|
+
printf '\n'
|
|
476
|
+
} >> "${GITHUB_STEP_SUMMARY}"
|
|
477
|
+
else
|
|
478
|
+
{
|
|
479
|
+
printf '%s\n' '### Joomla extension checks'
|
|
480
|
+
printf '%s\n' 'All Joomla-specific checks passed.'
|
|
481
|
+
printf '\n'
|
|
482
|
+
} >> "${GITHUB_STEP_SUMMARY}"
|
|
483
|
+
fi
|
|
484
|
+
|
|
485
|
+
extended_enabled="${EXTENDED_CHECKS:-true}"
|
|
486
|
+
extended_findings=()
|
|
487
|
+
|
|
488
|
+
if [ "${extended_enabled}" = 'true' ]; then
|
|
489
|
+
if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then
|
|
490
|
+
:
|
|
491
|
+
else
|
|
492
|
+
extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)")
|
|
493
|
+
fi
|
|
494
|
+
|
|
495
|
+
if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then
|
|
496
|
+
bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)"
|
|
497
|
+
if [ -n "${bad_refs}" ]; then
|
|
498
|
+
extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt")
|
|
499
|
+
{
|
|
500
|
+
printf '%s\n' '### Workflow pinning advisory'
|
|
501
|
+
printf '%s\n' 'Found uses: entries pinned to main/master:'
|
|
502
|
+
printf '%s\n' '```'
|
|
503
|
+
printf '%s\n' "${bad_refs}"
|
|
504
|
+
printf '%s\n' '```'
|
|
505
|
+
printf '\n'
|
|
506
|
+
} >> "${GITHUB_STEP_SUMMARY}"
|
|
507
|
+
fi
|
|
508
|
+
fi
|
|
509
|
+
|
|
510
|
+
if [ -f "${DOCS_INDEX}" ]; then
|
|
511
|
+
missing_links=""
|
|
512
|
+
while IFS= read -r docline; do
|
|
513
|
+
for link in $(echo "$docline" | grep -oE '\]\([^)]+\)' | sed 's/\](//' | sed 's/)$//' || true); do
|
|
514
|
+
case "$link" in http://*|https://*|"#"*|mailto:*) continue ;; esac
|
|
515
|
+
linkpath="${link%%#*}"
|
|
516
|
+
linkpath="${linkpath%%\?*}"
|
|
517
|
+
[ -z "$linkpath" ] && continue
|
|
518
|
+
if [ "${linkpath:0:1}" = "/" ]; then
|
|
519
|
+
testpath="${linkpath#/}"
|
|
520
|
+
else
|
|
521
|
+
testpath="$(dirname "${DOCS_INDEX}")/${linkpath}"
|
|
522
|
+
fi
|
|
523
|
+
[ ! -e "$testpath" ] && missing_links="${missing_links}${testpath} "
|
|
524
|
+
done
|
|
525
|
+
done < "${DOCS_INDEX}"
|
|
526
|
+
if [ -n "${missing_links}" ]; then
|
|
527
|
+
extended_findings+=("docs/docs-index.md contains broken relative links")
|
|
528
|
+
{
|
|
529
|
+
printf '%s\n' '### Docs index link integrity'
|
|
530
|
+
printf '%s\n' 'Broken relative links:'
|
|
531
|
+
for bl in ${missing_links}; do
|
|
532
|
+
printf '%s\n' "- ${bl}"
|
|
533
|
+
done
|
|
534
|
+
printf '\n'
|
|
535
|
+
} >> "${GITHUB_STEP_SUMMARY}"
|
|
536
|
+
fi
|
|
537
|
+
fi
|
|
538
|
+
|
|
539
|
+
if [ -d "${SCRIPT_DIR}" ]; then
|
|
540
|
+
if ! command -v shellcheck >/dev/null 2>&1; then
|
|
541
|
+
sudo apt-get update -qq
|
|
542
|
+
sudo apt-get install -y shellcheck >/dev/null
|
|
543
|
+
fi
|
|
544
|
+
|
|
545
|
+
sc_out=''
|
|
546
|
+
while IFS= read -r shf; do
|
|
547
|
+
[ -z "${shf}" ] && continue
|
|
548
|
+
out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)"
|
|
549
|
+
if [ -n "${out_one}" ]; then
|
|
550
|
+
sc_out="${sc_out}${out_one}\n"
|
|
551
|
+
fi
|
|
552
|
+
done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort)
|
|
553
|
+
|
|
554
|
+
if [ -n "${sc_out}" ]; then
|
|
555
|
+
extended_findings+=("ShellCheck warnings detected (advisory)")
|
|
556
|
+
sc_head="$(printf '%s' "${sc_out}" | head -n 200)"
|
|
557
|
+
{
|
|
558
|
+
printf '%s\n' '### ShellCheck (advisory)'
|
|
559
|
+
printf '%s\n' '```'
|
|
560
|
+
printf '%s\n' "${sc_head}"
|
|
561
|
+
printf '%s\n' '```'
|
|
562
|
+
printf '\n'
|
|
563
|
+
} >> "${GITHUB_STEP_SUMMARY}"
|
|
564
|
+
fi
|
|
565
|
+
fi
|
|
566
|
+
|
|
567
|
+
spdx_missing=()
|
|
568
|
+
IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}"
|
|
569
|
+
spdx_args=()
|
|
570
|
+
for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done
|
|
571
|
+
|
|
572
|
+
while IFS= read -r f; do
|
|
573
|
+
[ -z "${f}" ] && continue
|
|
574
|
+
if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then
|
|
575
|
+
spdx_missing+=("${f}")
|
|
576
|
+
fi
|
|
577
|
+
done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true)
|
|
578
|
+
|
|
579
|
+
if [ "${#spdx_missing[@]}" -gt 0 ]; then
|
|
580
|
+
extended_findings+=("SPDX header missing in some tracked files (advisory)")
|
|
581
|
+
{
|
|
582
|
+
printf '%s\n' '### SPDX header advisory'
|
|
583
|
+
printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):'
|
|
584
|
+
for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done
|
|
585
|
+
printf '\n'
|
|
586
|
+
} >> "${GITHUB_STEP_SUMMARY}"
|
|
587
|
+
fi
|
|
588
|
+
|
|
589
|
+
stale_cutoff_days=180
|
|
590
|
+
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}' | head -50)"
|
|
591
|
+
if [ -n "${stale_branches}" ]; then
|
|
592
|
+
extended_findings+=("Stale remote branches detected (advisory)")
|
|
593
|
+
{
|
|
594
|
+
printf '%s\n' '### Git hygiene advisory'
|
|
595
|
+
printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):"
|
|
596
|
+
while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}"
|
|
597
|
+
printf '\n'
|
|
598
|
+
} >> "${GITHUB_STEP_SUMMARY}"
|
|
599
|
+
fi
|
|
600
|
+
fi
|
|
601
|
+
|
|
602
|
+
{
|
|
603
|
+
printf '%s\n' '### Guardrails coverage matrix'
|
|
604
|
+
printf '%s\n' '| Domain | Status | Notes |'
|
|
605
|
+
printf '%s\n' '|---|---|---|'
|
|
606
|
+
printf '%s\n' '| Access control | OK | Admin-only execution gate |'
|
|
607
|
+
printf '%s\n' '| Release policy | N/A | Releases handled by MokoGitea |'
|
|
608
|
+
printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |'
|
|
609
|
+
printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |'
|
|
610
|
+
printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |'
|
|
611
|
+
if [ "${extended_enabled}" = 'true' ]; then
|
|
612
|
+
if [ "${#extended_findings[@]}" -gt 0 ]; then
|
|
613
|
+
printf '%s\n' '| Extended checks | Warning | See extended findings below |'
|
|
614
|
+
else
|
|
615
|
+
printf '%s\n' '| Extended checks | OK | No findings |'
|
|
616
|
+
fi
|
|
617
|
+
else
|
|
618
|
+
printf '%s\n' '| Extended checks | SKIPPED | EXTENDED_CHECKS disabled |'
|
|
619
|
+
fi
|
|
620
|
+
printf '\n'
|
|
621
|
+
} >> "${GITHUB_STEP_SUMMARY}"
|
|
622
|
+
|
|
623
|
+
if [ "${extended_enabled}" = 'true' ] && [ "${#extended_findings[@]}" -gt 0 ]; then
|
|
624
|
+
{
|
|
625
|
+
printf '%s\n' '### Extended findings (advisory)'
|
|
626
|
+
for f in "${extended_findings[@]}"; do printf '%s\n' "- ${f}"; done
|
|
627
|
+
printf '\n'
|
|
628
|
+
} >> "${GITHUB_STEP_SUMMARY}"
|
|
629
|
+
fi
|
|
630
|
+
|
|
631
|
+
printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}"
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
site-health:
|
|
635
|
+
name: Site Health
|
|
636
|
+
runs-on: ubuntu-latest
|
|
637
|
+
if: github.event_name == 'workflow_dispatch'
|
|
638
|
+
steps:
|
|
639
|
+
- uses: actions/checkout@v4
|
|
640
|
+
|
|
641
|
+
- name: Setup PHP
|
|
642
|
+
uses: shivammathur/setup-php@v2
|
|
643
|
+
with:
|
|
644
|
+
php-version: '8.3'
|
|
645
|
+
|
|
646
|
+
- name: Uptime check
|
|
647
|
+
if: env.URLS != ''
|
|
648
|
+
run: |
|
|
649
|
+
echo "$URLS" > /tmp/urls.txt
|
|
650
|
+
php monitoring/uptime-probe.php --urls /tmp/urls.txt --timeout 15 || echo "::warning::Some sites are down"
|
|
651
|
+
rm -f /tmp/urls.txt
|
|
652
|
+
env:
|
|
653
|
+
URLS: ${{ vars.MONITORED_URLS }}
|
|
654
|
+
|
|
655
|
+
- name: SSL certificate check
|
|
656
|
+
if: env.DOMAINS != ''
|
|
657
|
+
run: |
|
|
658
|
+
echo "$DOMAINS" > /tmp/domains.txt
|
|
659
|
+
php monitoring/ssl-check.php --domains /tmp/domains.txt --warn-days 30 || echo "::warning::SSL certificates expiring soon"
|
|
660
|
+
rm -f /tmp/domains.txt
|
|
661
|
+
env:
|
|
662
|
+
DOMAINS: ${{ vars.MONITORED_DOMAINS }}
|
|
663
|
+
|
|
664
|
+
- name: Summary
|
|
665
|
+
if: always()
|
|
666
|
+
run: |
|
|
667
|
+
echo "### Site Health" >> $GITHUB_STEP_SUMMARY
|
|
668
|
+
echo "Uptime and SSL checks completed." >> $GITHUB_STEP_SUMMARY
|
|
669
|
+
|
|
670
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
671
|
+
# Issue Reporter — file issues for failed gates
|
|
672
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
673
|
+
report-issues:
|
|
674
|
+
name: "Report Issues"
|
|
675
|
+
runs-on: ubuntu-latest
|
|
676
|
+
needs: [access_check, scripts_governance, repo_health]
|
|
677
|
+
if: >-
|
|
678
|
+
always() &&
|
|
679
|
+
(needs.scripts_governance.result == 'failure' ||
|
|
680
|
+
needs.repo_health.result == 'failure')
|
|
681
|
+
|
|
682
|
+
steps:
|
|
683
|
+
- name: Checkout
|
|
684
|
+
uses: actions/checkout@v4
|
|
685
|
+
with:
|
|
686
|
+
sparse-checkout: automation/ci-issue-reporter.sh
|
|
687
|
+
sparse-checkout-cone-mode: false
|
|
688
|
+
|
|
689
|
+
- name: "File issues for failed gates"
|
|
690
|
+
env:
|
|
691
|
+
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
692
|
+
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
693
|
+
run: |
|
|
694
|
+
chmod +x automation/ci-issue-reporter.sh
|
|
695
|
+
REPORTER="./automation/ci-issue-reporter.sh"
|
|
696
|
+
WF="Repo Health"
|
|
697
|
+
|
|
698
|
+
report_gate() {
|
|
699
|
+
local gate="$1" result="$2" details="$3"
|
|
700
|
+
if [ "$result" = "failure" ]; then
|
|
701
|
+
"$REPORTER" --gate "$gate" --details "$details" --workflow "$WF" --severity error
|
|
702
|
+
fi
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
report_gate "Scripts Governance" \
|
|
706
|
+
"${{ needs.scripts_governance.result }}" \
|
|
707
|
+
"Scripts directory policy violations detected. Review required and allowed directories."
|
|
708
|
+
|
|
709
|
+
report_gate "Repository Health" \
|
|
710
|
+
"${{ needs.repo_health.result }}" \
|
|
711
|
+
"Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary."
|