Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fd85c3de9d | |||
| cbd173be9a | |||
| aed51982a2 | |||
| f55a1f4e58 | |||
| 69064ea47c | |||
| c9038d1f2b | |||
| e70b566be9 | |||
| bf388bec1f | |||
| b3c36e77c2 | |||
| bde1cf37ea | |||
| 5fa554029f | |||
| 55664103b2 | |||
| 5d6337dedf | |||
| 48603d0c55 | |||
| 128dd72f60 | |||
| a11ce8f8ec | |||
| c471e0d239 | |||
| 183c648b13 | |||
| fb2a1ef186 | |||
| 7b9a7a30ce | |||
| 29d25e8c47 | |||
| 0198c4237f | |||
| 0e8caff9ea | |||
| 6f619b4d05 | |||
| d1cea2a3c3 | |||
| f96db8f91a | |||
| 135ccdefef | |||
| 18a0a99ea2 | |||
| 0253c523ed | |||
| 51442cb879 | |||
| 023777483e | |||
| f63d5347a1 | |||
| 09382cd6da | |||
| 3ec0dad478 | |||
| 1ca4e7dab6 | |||
| eca24b196d | |||
| e810346af6 | |||
| 8ee23e2805 | |||
| 923161598c | |||
| 9c0ac13fda | |||
| 1339f6a4bf | |||
| e3df79c7f3 | |||
| 61fb670b25 | |||
| eb1c38930c | |||
| 6061e634f1 | |||
| c762ef329b | |||
| a6f8728580 | |||
| 5fa5bdeef3 | |||
| 3fdaa2f3f6 | |||
| 912bce0a9c | |||
| b02712903a | |||
| 79c94e1af5 | |||
| 61d95a94a3 | |||
| b19bd810aa | |||
| 1332817c9a | |||
| 70c7833e7b | |||
| 125a3f82e7 | |||
| 066a73a582 | |||
| e99c623666 | |||
| 4acc277ebb | |||
| 2dcb28543e | |||
| 9f26477521 | |||
| 0b62dd09bd | |||
| f7541d1253 | |||
| 78973339a4 | |||
| 1639517652 | |||
| 93fe0456b7 | |||
| 8f6df9e01a | |||
| 0043021c0d | |||
| 8008615cd6 | |||
| e1143b50d3 | |||
| 5c18486d09 | |||
| 45e30536ac | |||
| 07f6ba8b7b | |||
| 349a3c5eb6 | |||
| efb4f94082 | |||
| 2bacfbdac6 | |||
| 0f3b6d1541 | |||
| a4dc760261 | |||
| 006d11ec00 | |||
| 745b4c3567 | |||
| b3e24b4047 | |||
| 568602a07e |
@@ -0,0 +1,251 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: moko-platform.Automation
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
|
# PATH: /.gitea/workflows/branch-protection.yml
|
||||||
|
# BRIEF: Apply standardised branch protection rules to all governed repositories
|
||||||
|
#
|
||||||
|
# +========================================================================+
|
||||||
|
# | BRANCH PROTECTION SETUP |
|
||||||
|
# +========================================================================+
|
||||||
|
# | |
|
||||||
|
# | Applies protection rules for: main, dev, rc, beta, alpha |
|
||||||
|
# | |
|
||||||
|
# | main — Require PR, block rejected reviews, no force push |
|
||||||
|
# | dev — Allow push, no force push, no delete |
|
||||||
|
# | rc — Allow push, no force push, no delete |
|
||||||
|
# | beta — Allow push, no force push, no delete |
|
||||||
|
# | alpha — Allow push, no force push, no delete |
|
||||||
|
# | |
|
||||||
|
# | jmiller has override authority on all branches. |
|
||||||
|
# | |
|
||||||
|
# +========================================================================+
|
||||||
|
|
||||||
|
name: Branch Protection Setup
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 2 * * 1' # Weekly Monday 02:00 UTC
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
dry_run:
|
||||||
|
description: 'Preview mode (no changes)'
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
repos:
|
||||||
|
description: 'Comma-separated repo names (empty = all governed repos)'
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: ''
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITEA_URL: https://git.mokoconsulting.tech
|
||||||
|
GITEA_ORG: MokoConsulting
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
protect:
|
||||||
|
name: Apply Branch Protection Rules
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Determine target repos
|
||||||
|
id: repos
|
||||||
|
env:
|
||||||
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
API="${GITEA_URL}/api/v1"
|
||||||
|
|
||||||
|
# Platform/standards/infra repos to exclude
|
||||||
|
EXCLUDE="gitea-org-config org-profile gitea-private .mokogitea-private MokoStandards moko-platform MokoTesting"
|
||||||
|
EXCLUDE="$EXCLUDE MokoStandards-Template-Client MokoStandards-Template-Dolibarr MokoStandards-Template-Generic MokoStandards-Template-Joomla MokoDoliProjTemplate"
|
||||||
|
|
||||||
|
if [ -n "${{ inputs.repos }}" ]; then
|
||||||
|
# User-specified repos
|
||||||
|
REPOS=$(echo "${{ inputs.repos }}" | tr ',' ' ')
|
||||||
|
else
|
||||||
|
# Fetch all org repos
|
||||||
|
PAGE=1
|
||||||
|
REPOS=""
|
||||||
|
while true; do
|
||||||
|
BATCH=$(curl -sS \
|
||||||
|
-H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
"${API}/orgs/${GITEA_ORG}/repos?page=${PAGE}&limit=50" \
|
||||||
|
| jq -r '.[].name // empty')
|
||||||
|
[ -z "$BATCH" ] && break
|
||||||
|
REPOS="$REPOS $BATCH"
|
||||||
|
PAGE=$((PAGE + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
# Filter out excluded repos
|
||||||
|
FILTERED=""
|
||||||
|
for REPO in $REPOS; do
|
||||||
|
SKIP=false
|
||||||
|
for EX in $EXCLUDE; do
|
||||||
|
if [ "$REPO" = "$EX" ]; then
|
||||||
|
SKIP=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$SKIP" = "false" ]; then
|
||||||
|
FILTERED="$FILTERED $REPO"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
REPOS="$FILTERED"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "repos=$REPOS" >> "$GITHUB_OUTPUT"
|
||||||
|
COUNT=$(echo "$REPOS" | wc -w)
|
||||||
|
echo "📋 Target repos (${COUNT}): $REPOS"
|
||||||
|
|
||||||
|
- name: Apply protection rules
|
||||||
|
env:
|
||||||
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
|
DRY_RUN: ${{ inputs.dry_run || 'false' }}
|
||||||
|
run: |
|
||||||
|
API="${GITEA_URL}/api/v1"
|
||||||
|
REPOS="${{ steps.repos.outputs.repos }}"
|
||||||
|
|
||||||
|
SUCCESS=0
|
||||||
|
FAILED=0
|
||||||
|
SKIPPED=0
|
||||||
|
|
||||||
|
# ── Rule definitions ──────────────────────────────────────
|
||||||
|
# Only the CI bot (jmiller token) can push directly.
|
||||||
|
# All human contributors must use PRs.
|
||||||
|
# Force push disabled on all branches.
|
||||||
|
|
||||||
|
RULE_MAIN='{
|
||||||
|
"rule_name": "main",
|
||||||
|
"enable_push": true,
|
||||||
|
"enable_push_whitelist": true,
|
||||||
|
"push_whitelist_usernames": ["jmiller"],
|
||||||
|
"enable_force_push": false,
|
||||||
|
"enable_force_push_allowlist": false,
|
||||||
|
"force_push_allowlist_usernames": [],
|
||||||
|
"enable_merge_whitelist": false,
|
||||||
|
"required_approvals": 0,
|
||||||
|
"dismiss_stale_approvals": true,
|
||||||
|
"block_on_rejected_reviews": true,
|
||||||
|
"block_on_outdated_branch": false,
|
||||||
|
"priority": 1
|
||||||
|
}'
|
||||||
|
|
||||||
|
RULE_DEV='{
|
||||||
|
"rule_name": "dev",
|
||||||
|
"enable_push": true,
|
||||||
|
"enable_push_whitelist": true,
|
||||||
|
"push_whitelist_usernames": ["jmiller"],
|
||||||
|
"enable_force_push": false,
|
||||||
|
"enable_force_push_allowlist": false,
|
||||||
|
"force_push_allowlist_usernames": [],
|
||||||
|
"enable_merge_whitelist": false,
|
||||||
|
"required_approvals": 0,
|
||||||
|
"block_on_rejected_reviews": false,
|
||||||
|
"priority": 2
|
||||||
|
}'
|
||||||
|
|
||||||
|
RULE_RC='{
|
||||||
|
"rule_name": "rc",
|
||||||
|
"enable_push": true,
|
||||||
|
"enable_push_whitelist": true,
|
||||||
|
"push_whitelist_usernames": ["jmiller"],
|
||||||
|
"enable_force_push": false,
|
||||||
|
"enable_force_push_allowlist": false,
|
||||||
|
"force_push_allowlist_usernames": [],
|
||||||
|
"enable_merge_whitelist": false,
|
||||||
|
"required_approvals": 0,
|
||||||
|
"block_on_rejected_reviews": false,
|
||||||
|
"priority": 3
|
||||||
|
}'
|
||||||
|
|
||||||
|
RULE_BETA='{
|
||||||
|
"rule_name": "beta",
|
||||||
|
"enable_push": true,
|
||||||
|
"enable_push_whitelist": true,
|
||||||
|
"push_whitelist_usernames": ["jmiller"],
|
||||||
|
"enable_force_push": false,
|
||||||
|
"enable_force_push_allowlist": false,
|
||||||
|
"force_push_allowlist_usernames": [],
|
||||||
|
"enable_merge_whitelist": false,
|
||||||
|
"required_approvals": 0,
|
||||||
|
"block_on_rejected_reviews": false,
|
||||||
|
"priority": 4
|
||||||
|
}'
|
||||||
|
|
||||||
|
RULE_ALPHA='{
|
||||||
|
"rule_name": "alpha",
|
||||||
|
"enable_push": true,
|
||||||
|
"enable_push_whitelist": true,
|
||||||
|
"push_whitelist_usernames": ["jmiller"],
|
||||||
|
"enable_force_push": false,
|
||||||
|
"enable_force_push_allowlist": false,
|
||||||
|
"force_push_allowlist_usernames": [],
|
||||||
|
"enable_merge_whitelist": false,
|
||||||
|
"required_approvals": 0,
|
||||||
|
"block_on_rejected_reviews": false,
|
||||||
|
"priority": 5
|
||||||
|
}'
|
||||||
|
|
||||||
|
RULES=("$RULE_MAIN" "$RULE_DEV" "$RULE_RC" "$RULE_BETA" "$RULE_ALPHA")
|
||||||
|
RULE_NAMES=("main" "dev" "rc" "beta" "alpha")
|
||||||
|
|
||||||
|
# ── Apply rules to each repo ──────────────────────────────
|
||||||
|
for REPO in $REPOS; do
|
||||||
|
echo ""
|
||||||
|
echo "═══ ${REPO} ═══"
|
||||||
|
|
||||||
|
for i in "${!RULES[@]}"; do
|
||||||
|
RULE="${RULES[$i]}"
|
||||||
|
NAME="${RULE_NAMES[$i]}"
|
||||||
|
|
||||||
|
if [ "$DRY_RUN" = "true" ]; then
|
||||||
|
echo " [DRY RUN] Would apply rule: ${NAME}"
|
||||||
|
SKIPPED=$((SKIPPED + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Delete existing rule if present (idempotent recreate)
|
||||||
|
ENCODED_NAME=$(echo "$NAME" | sed 's|/|%2F|g')
|
||||||
|
curl -sS -o /dev/null -w "" \
|
||||||
|
-X DELETE \
|
||||||
|
-H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
"${API}/repos/${GITEA_ORG}/${REPO}/branch_protections/${ENCODED_NAME}" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Create rule
|
||||||
|
RESPONSE=$(curl -sS -w "\n%{http_code}" \
|
||||||
|
-X POST \
|
||||||
|
-H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$RULE" \
|
||||||
|
"${API}/repos/${GITEA_ORG}/${REPO}/branch_protections")
|
||||||
|
|
||||||
|
HTTP=$(echo "$RESPONSE" | tail -1)
|
||||||
|
BODY=$(echo "$RESPONSE" | sed '$d')
|
||||||
|
|
||||||
|
if [ "$HTTP" = "201" ]; then
|
||||||
|
echo " ✅ ${NAME}"
|
||||||
|
SUCCESS=$((SUCCESS + 1))
|
||||||
|
else
|
||||||
|
echo " ❌ ${NAME} (HTTP ${HTTP}): $(echo "$BODY" | jq -r '.message // .' 2>/dev/null | head -1)"
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Summary ───────────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "════════════════════════════════════════"
|
||||||
|
echo " ✅ Success: ${SUCCESS}"
|
||||||
|
echo " ❌ Failed: ${FAILED}"
|
||||||
|
echo " ⏭️ Skipped: ${SKIPPED}"
|
||||||
|
echo "════════════════════════════════════════"
|
||||||
|
|
||||||
|
if [ "$FAILED" -gt 0 ]; then
|
||||||
|
echo "::warning::${FAILED} rule(s) failed to apply"
|
||||||
|
fi
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!--
|
|
||||||
MokoStandards Repository Manifest
|
|
||||||
Template: MCP Server
|
|
||||||
See: https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home
|
|
||||||
-->
|
|
||||||
<moko-platform xmlns="https://standards.mokoconsulting.tech/moko-platform/1.0" schema-version="1.0">
|
|
||||||
<identity>
|
|
||||||
<name>mcp-windows</name>
|
|
||||||
<org>MokoConsulting</org>
|
|
||||||
<description>MCP server for Windows desktop system operations</description>
|
|
||||||
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
|
||||||
</identity>
|
|
||||||
<governance>
|
|
||||||
<platform>mcp</platform>
|
|
||||||
<standards-version>05.00.00</standards-version>
|
|
||||||
<standards-source>https://git.mokoconsulting.tech/MokoConsulting/moko-platform</standards-source>
|
|
||||||
</governance>
|
|
||||||
<build>
|
|
||||||
<language>TypeScript</language>
|
|
||||||
<package-type>mcp-server</package-type>
|
|
||||||
<entry-point>src/</entry-point>
|
|
||||||
</build>
|
|
||||||
</moko-platform>
|
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: mokocli.Release
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
|
# PATH: /.mokogitea/workflows/auto-bump.yml
|
||||||
|
# VERSION: 09.02.00
|
||||||
|
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
||||||
|
|
||||||
|
name: "Universal: Auto Version Bump"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
- rc
|
||||||
|
- 'feature/**'
|
||||||
|
- 'patch/**'
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
bump:
|
||||||
|
name: Version Bump
|
||||||
|
runs-on: release
|
||||||
|
if: >-
|
||||||
|
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||||
|
!contains(github.event.head_commit.message, '[skip bump]') &&
|
||||||
|
!startsWith(github.event.head_commit.message, 'Merge pull request')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Setup mokocli tools
|
||||||
|
run: |
|
||||||
|
if ! command -v composer &> /dev/null; then
|
||||||
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
if [ -d "/opt/mokocli/cli" ]; then
|
||||||
|
echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV"
|
||||||
|
else
|
||||||
|
git clone --depth 1 --branch main --quiet \
|
||||||
|
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \
|
||||||
|
/tmp/mokocli
|
||||||
|
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
||||||
|
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Bump version
|
||||||
|
run: |
|
||||||
|
php ${MOKO_CLI}/version_auto_bump.php \
|
||||||
|
--path . --branch "${GITHUB_REF_NAME}" \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
--repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,48 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: MokoStandards.Universal
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
|
# PATH: /.mokogitea/workflows/branch-cleanup.yml
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: Delete feature branches after PR merge
|
||||||
|
|
||||||
|
name: "Branch Cleanup"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cleanup:
|
||||||
|
name: Delete merged branch
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >-
|
||||||
|
github.event.pull_request.merged == true &&
|
||||||
|
github.event.pull_request.head.ref != 'dev' &&
|
||||||
|
github.event.pull_request.head.ref != 'main'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Delete source branch
|
||||||
|
run: |
|
||||||
|
BRANCH="${{ github.event.pull_request.head.ref }}"
|
||||||
|
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
|
||||||
|
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
|
||||||
|
|
||||||
|
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
|
||||||
|
-H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
"${API}/${ENCODED}" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ "$STATUS" = "204" ]; then
|
||||||
|
echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [ "$STATUS" = "404" ]; then
|
||||||
|
echo "Branch already deleted: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "::warning::Failed to delete branch ${BRANCH} (HTTP ${STATUS})"
|
||||||
|
fi
|
||||||
@@ -1,213 +1,10 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
# DISABLED — auto-release Step 11 recreates dev from main after every release.
|
||||||
#
|
# Cascade-dev is redundant and causes version conflicts when both main and dev
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# have different version numbers in templateDetails.xml / manifest.xml.
|
||||||
#
|
name: "Cascade Main → Dev (DISABLED)"
|
||||||
# FILE INFORMATION
|
on: workflow_dispatch
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: MokoStandards.Maintenance
|
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
|
||||||
# PATH: /templates/workflows/cascade-dev.yml.template
|
|
||||||
# VERSION: 02.00.00
|
|
||||||
# BRIEF: Forward-merge main → all open branches after every push to main
|
|
||||||
#
|
|
||||||
# +========================================================================+
|
|
||||||
# | CASCADE MAIN → ALL BRANCHES |
|
|
||||||
# +========================================================================+
|
|
||||||
# | |
|
|
||||||
# | Triggers on every push to main (PR merges, bot commits, etc.) |
|
|
||||||
# | |
|
|
||||||
# | 1. List all branches matching: dev, rc/*, beta/*, alpha/* |
|
|
||||||
# | 2. For each: create PR (main → branch), auto-merge if clean |
|
|
||||||
# | 3. On conflict: leave PR open for manual resolution |
|
|
||||||
# | |
|
|
||||||
# +========================================================================+
|
|
||||||
|
|
||||||
name: "Universal: Cascade Main → Dev"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
|
||||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cascade:
|
noop:
|
||||||
name: Cascade main → branches
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: >-
|
|
||||||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
|
||||||
!contains(github.event.head_commit.message, '[skip cascade]')
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Discover target branches
|
- run: echo "Cascade disabled — auto-release handles dev recreation"
|
||||||
id: branches
|
|
||||||
env:
|
|
||||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
|
||||||
run: |
|
|
||||||
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
|
|
||||||
# Fetch all branches (paginated)
|
|
||||||
PAGE=1
|
|
||||||
ALL_BRANCHES=""
|
|
||||||
while true; do
|
|
||||||
BATCH=$(curl -sS \
|
|
||||||
-H "Authorization: token ${GA_TOKEN}" \
|
|
||||||
"${API}/branches?page=${PAGE}&limit=50" \
|
|
||||||
| jq -r '.[].name // empty')
|
|
||||||
[ -z "$BATCH" ] && break
|
|
||||||
ALL_BRANCHES="$ALL_BRANCHES $BATCH"
|
|
||||||
PAGE=$((PAGE + 1))
|
|
||||||
done
|
|
||||||
|
|
||||||
# Filter to cascade targets: dev, dev/*, rc/*, beta/*, alpha/*
|
|
||||||
TARGETS=""
|
|
||||||
for BRANCH in $ALL_BRANCHES; do
|
|
||||||
case "$BRANCH" in
|
|
||||||
dev|dev/*|rc/*|beta/*|alpha/*)
|
|
||||||
TARGETS="$TARGETS $BRANCH"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
TARGETS=$(echo "$TARGETS" | xargs) # trim whitespace
|
|
||||||
|
|
||||||
if [ -z "$TARGETS" ]; then
|
|
||||||
echo "targets=" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "ℹ️ No cascade target branches found"
|
|
||||||
else
|
|
||||||
echo "targets=$TARGETS" >> "$GITHUB_OUTPUT"
|
|
||||||
COUNT=$(echo "$TARGETS" | wc -w)
|
|
||||||
echo "📋 Found ${COUNT} target branch(es): ${TARGETS}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Cascade to all target branches
|
|
||||||
if: steps.branches.outputs.targets != ''
|
|
||||||
env:
|
|
||||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
|
||||||
run: |
|
|
||||||
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
SHORT_SHA="${GITHUB_SHA:0:7}"
|
|
||||||
TARGETS="${{ steps.branches.outputs.targets }}"
|
|
||||||
|
|
||||||
SUCCESS=0
|
|
||||||
CONFLICTS=0
|
|
||||||
SKIPPED=0
|
|
||||||
FAILED=0
|
|
||||||
|
|
||||||
for BRANCH in $TARGETS; do
|
|
||||||
echo ""
|
|
||||||
echo "═══ main → ${BRANCH} ═══"
|
|
||||||
|
|
||||||
# Check if branch is already up to date
|
|
||||||
ENCODED_BRANCH=$(echo "$BRANCH" | sed 's|/|%2F|g')
|
|
||||||
RESPONSE=$(curl -sS \
|
|
||||||
-H "Authorization: token ${GA_TOKEN}" \
|
|
||||||
"${API}/compare/${ENCODED_BRANCH}...main")
|
|
||||||
|
|
||||||
AHEAD=$(echo "$RESPONSE" | jq '.total_commits // 0')
|
|
||||||
|
|
||||||
if [ "$AHEAD" -eq 0 ]; then
|
|
||||||
echo " ✅ Already up to date"
|
|
||||||
SKIPPED=$((SKIPPED + 1))
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo " ℹ️ main is ${AHEAD} commit(s) ahead"
|
|
||||||
|
|
||||||
# Check for existing cascade PR
|
|
||||||
EXISTING=$(curl -sS \
|
|
||||||
-H "Authorization: token ${GA_TOKEN}" \
|
|
||||||
"${API}/pulls?state=open&head=${GITEA_ORG}:main&base=${ENCODED_BRANCH}&limit=1")
|
|
||||||
|
|
||||||
EXISTING_COUNT=$(echo "$EXISTING" | jq 'length')
|
|
||||||
PR_NUMBER=""
|
|
||||||
|
|
||||||
if [ "$EXISTING_COUNT" -gt 0 ]; then
|
|
||||||
PR_NUMBER=$(echo "$EXISTING" | jq -r '.[0].number')
|
|
||||||
echo " ℹ️ Reusing existing PR #${PR_NUMBER}"
|
|
||||||
else
|
|
||||||
# Create cascade PR
|
|
||||||
PR_RESPONSE=$(curl -sS -w "\n%{http_code}" \
|
|
||||||
-X POST \
|
|
||||||
-H "Authorization: token ${GA_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{
|
|
||||||
\"title\": \"chore: cascade main → ${BRANCH} (${SHORT_SHA}) [skip ci]\",
|
|
||||||
\"body\": \"## Automatic cascade\\n\\nForward-merging \`main\` (${SHORT_SHA}) into \`${BRANCH}\`.\\n\\nIf conflicts exist, resolve manually and merge.\\n\\n> Auto-created by **Cascade Main → Dev**.\",
|
|
||||||
\"head\": \"main\",
|
|
||||||
\"base\": \"${BRANCH}\"
|
|
||||||
}" \
|
|
||||||
"${API}/pulls")
|
|
||||||
|
|
||||||
HTTP_CODE=$(echo "$PR_RESPONSE" | tail -1)
|
|
||||||
BODY=$(echo "$PR_RESPONSE" | sed '$d')
|
|
||||||
PR_NUMBER=$(echo "$BODY" | jq -r '.number // empty')
|
|
||||||
|
|
||||||
if [ "$HTTP_CODE" != "201" ] || [ -z "$PR_NUMBER" ]; then
|
|
||||||
MSG=$(echo "$BODY" | jq -r '.message // .' 2>/dev/null | head -1)
|
|
||||||
echo " ❌ Failed to create PR (HTTP ${HTTP_CODE}): ${MSG}"
|
|
||||||
FAILED=$((FAILED + 1))
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo " ✅ Created PR #${PR_NUMBER}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Try auto-merge
|
|
||||||
PR_DATA=$(curl -sS \
|
|
||||||
-H "Authorization: token ${GA_TOKEN}" \
|
|
||||||
"${API}/pulls/${PR_NUMBER}")
|
|
||||||
|
|
||||||
MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable // false')
|
|
||||||
|
|
||||||
if [ "$MERGEABLE" != "true" ]; then
|
|
||||||
echo " ⚠️ Conflicts — PR #${PR_NUMBER} left open"
|
|
||||||
CONFLICTS=$((CONFLICTS + 1))
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
MERGE_RESPONSE=$(curl -sS -w "\n%{http_code}" \
|
|
||||||
-X POST \
|
|
||||||
-H "Authorization: token ${GA_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{
|
|
||||||
\"Do\": \"merge\",
|
|
||||||
\"merge_message_field\": \"chore: cascade main → ${BRANCH} [skip ci]\",
|
|
||||||
\"delete_branch_after_merge\": false
|
|
||||||
}" \
|
|
||||||
"${API}/pulls/${PR_NUMBER}/merge")
|
|
||||||
|
|
||||||
MERGE_HTTP=$(echo "$MERGE_RESPONSE" | tail -1)
|
|
||||||
|
|
||||||
if [ "$MERGE_HTTP" = "200" ] || [ "$MERGE_HTTP" = "204" ]; then
|
|
||||||
echo " ✅ Merged — ${BRANCH} is in sync"
|
|
||||||
SUCCESS=$((SUCCESS + 1))
|
|
||||||
else
|
|
||||||
MERGE_BODY=$(echo "$MERGE_RESPONSE" | sed '$d')
|
|
||||||
echo " ⚠️ Merge failed (HTTP ${MERGE_HTTP}) — PR #${PR_NUMBER} left open"
|
|
||||||
CONFLICTS=$((CONFLICTS + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Summary
|
|
||||||
echo ""
|
|
||||||
echo "════════════════════════════════════════"
|
|
||||||
echo " ✅ Merged: ${SUCCESS}"
|
|
||||||
echo " ⚠️ Conflicts: ${CONFLICTS}"
|
|
||||||
echo " ⏭️ Up to date: ${SKIPPED}"
|
|
||||||
echo " ❌ Failed: ${FAILED}"
|
|
||||||
echo "════════════════════════════════════════"
|
|
||||||
|
|
||||||
if [ "$FAILED" -gt 0 ]; then
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: MokoStandards.CI
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||||
|
# PATH: /.gitea/workflows/ci-generic.yml
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: CI pipeline — lint, validate, and test for generic projects (PHP + Node.js)
|
||||||
|
|
||||||
|
name: "Generic: Project CI"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# ── Lint & Validate ───────────────────────────────────────────────────
|
||||||
|
lint:
|
||||||
|
name: Lint & Validate
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Detect toolchain
|
||||||
|
id: detect
|
||||||
|
run: |
|
||||||
|
HAS_PHP=false
|
||||||
|
HAS_NODE=false
|
||||||
|
[ -f "composer.json" ] && HAS_PHP=true
|
||||||
|
[ -f "package.json" ] && HAS_NODE=true
|
||||||
|
echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Toolchain: PHP=$HAS_PHP Node=$HAS_NODE"
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
if: steps.detect.outputs.has_php == 'true'
|
||||||
|
run: |
|
||||||
|
if ! command -v php &> /dev/null; then
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
php -v
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
if: steps.detect.outputs.has_node == 'true'
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Install PHP dependencies
|
||||||
|
if: steps.detect.outputs.has_php == 'true'
|
||||||
|
run: |
|
||||||
|
if [ -f "composer.json" ]; then
|
||||||
|
composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Install Node.js dependencies
|
||||||
|
if: steps.detect.outputs.has_node == 'true'
|
||||||
|
run: |
|
||||||
|
if [ -f "package.json" ]; then
|
||||||
|
npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: PHP syntax check
|
||||||
|
if: steps.detect.outputs.has_php == 'true'
|
||||||
|
run: |
|
||||||
|
ERRORS=0
|
||||||
|
while IFS= read -r -d '' file; do
|
||||||
|
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
|
||||||
|
echo "::error file=${file}::PHP syntax error"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./node_modules/*" -print0)
|
||||||
|
|
||||||
|
echo "## PHP Lint" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "$ERRORS" -eq 0 ]; then
|
||||||
|
echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "${ERRORS} file(s) with syntax errors." >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: TypeScript/JavaScript lint
|
||||||
|
if: steps.detect.outputs.has_node == 'true'
|
||||||
|
run: |
|
||||||
|
if [ -f "node_modules/.bin/eslint" ]; then
|
||||||
|
npx eslint src/ --quiet 2>&1 || { echo "::error::ESLint errors found"; exit 1; }
|
||||||
|
echo "## ESLint" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "All files passed ESLint." >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [ -f ".eslintrc.json" ] || [ -f ".eslintrc.js" ] || [ -f "eslint.config.js" ]; then
|
||||||
|
echo "::warning::ESLint config found but eslint not installed"
|
||||||
|
else
|
||||||
|
echo "No ESLint configured — skipping"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: TypeScript compile check
|
||||||
|
if: steps.detect.outputs.has_node == 'true'
|
||||||
|
run: |
|
||||||
|
if [ -f "tsconfig.json" ] && [ -f "node_modules/.bin/tsc" ]; then
|
||||||
|
npx tsc --noEmit 2>&1 || { echo "::error::TypeScript compilation errors"; exit 1; }
|
||||||
|
echo "## TypeScript" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "TypeScript compilation passed." >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: PHPStan static analysis
|
||||||
|
if: steps.detect.outputs.has_php == 'true'
|
||||||
|
run: |
|
||||||
|
if [ -f "phpstan.neon" ] && [ -f "vendor/bin/phpstan" ]; then
|
||||||
|
vendor/bin/phpstan analyse --no-progress 2>&1 || { echo "::warning::PHPStan found issues"; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Tests ─────────────────────────────────────────────────────────────
|
||||||
|
test:
|
||||||
|
name: Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: lint
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Detect toolchain
|
||||||
|
id: detect
|
||||||
|
run: |
|
||||||
|
HAS_PHP=false
|
||||||
|
HAS_NODE=false
|
||||||
|
[ -f "composer.json" ] && HAS_PHP=true
|
||||||
|
[ -f "package.json" ] && HAS_NODE=true
|
||||||
|
echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
if: steps.detect.outputs.has_php == 'true'
|
||||||
|
run: |
|
||||||
|
if ! command -v php &> /dev/null; then
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
if: steps.detect.outputs.has_node == 'true'
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
[ -f "composer.json" ] && composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true
|
||||||
|
[ -f "package.json" ] && { npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true; }
|
||||||
|
|
||||||
|
- name: Run PHP tests
|
||||||
|
if: steps.detect.outputs.has_php == 'true'
|
||||||
|
run: |
|
||||||
|
if [ -f "vendor/bin/phpunit" ]; then
|
||||||
|
vendor/bin/phpunit --testdox 2>&1
|
||||||
|
echo "## PHPUnit" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Tests passed." >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then
|
||||||
|
echo "::warning::PHPUnit config found but phpunit not installed"
|
||||||
|
else
|
||||||
|
echo "No PHPUnit configured — skipping"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run Node.js tests
|
||||||
|
if: steps.detect.outputs.has_node == 'true'
|
||||||
|
run: |
|
||||||
|
if jq -e '.scripts.test' package.json > /dev/null 2>&1; then
|
||||||
|
npm test 2>&1
|
||||||
|
echo "## Node.js Tests" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Tests passed." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "No test script in package.json — skipping"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build check
|
||||||
|
run: |
|
||||||
|
if [ -f "Makefile" ]; then
|
||||||
|
make build 2>&1 || echo "::warning::Build failed or not configured"
|
||||||
|
elif [ -f "package.json" ] && jq -e '.scripts.build' package.json > /dev/null 2>&1; then
|
||||||
|
npm run build 2>&1 || echo "::warning::Build failed"
|
||||||
|
fi
|
||||||
@@ -25,10 +25,6 @@
|
|||||||
name: "Universal: Secret Scanning"
|
name: "Universal: Secret Scanning"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- 'dev/**'
|
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 5 * * 1' # Weekly Monday 05:00 UTC
|
- cron: '0 5 * * 1' # Weekly Monday 05:00 UTC
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: mokocli.Automation
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: Auto-create feature branch when an issue is opened
|
||||||
|
|
||||||
|
name: "Universal: Issue Branch"
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
create-branch:
|
||||||
|
name: Create feature branch
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Create branch and comment
|
||||||
|
run: |
|
||||||
|
TOKEN="${{ secrets.GA_TOKEN }}"
|
||||||
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
|
ISSUE_NUM="${{ github.event.issue.number }}"
|
||||||
|
ISSUE_TITLE="${{ github.event.issue.title }}"
|
||||||
|
|
||||||
|
# Build slug from title: lowercase, replace non-alnum with dash, trim
|
||||||
|
SLUG=$(echo "${ISSUE_TITLE}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//' | cut -c1-40)
|
||||||
|
BRANCH="feature/${ISSUE_NUM}-${SLUG}"
|
||||||
|
|
||||||
|
# Check dev branch exists
|
||||||
|
DEV_EXISTS=$(curl -sf -o /dev/null -w '%{http_code}' \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API}/branches/dev" 2>/dev/null || echo "000")
|
||||||
|
|
||||||
|
if [ "${DEV_EXISTS}" != "200" ]; then
|
||||||
|
echo "No dev branch -- skipping"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create branch from dev
|
||||||
|
HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/branches" \
|
||||||
|
-d "{\"new_branch_name\":\"${BRANCH}\",\"old_branch_name\":\"dev\"}" 2>/dev/null || echo "000")
|
||||||
|
|
||||||
|
if [ "${HTTP}" = "201" ]; then
|
||||||
|
echo "Created branch: ${BRANCH}"
|
||||||
|
|
||||||
|
# Comment on issue with branch link
|
||||||
|
REPO_URL="${GITEA_URL}/${{ github.repository }}"
|
||||||
|
BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`"
|
||||||
|
|
||||||
|
curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/issues/${ISSUE_NUM}/comments" \
|
||||||
|
-d "{\"body\":\"${BODY}\"}" > /dev/null 2>&1
|
||||||
|
|
||||||
|
echo "Commented on issue #${ISSUE_NUM}"
|
||||||
|
else
|
||||||
|
echo "Failed to create branch (HTTP ${HTTP}) -- may already exist"
|
||||||
|
fi
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoStandards.Notifications
|
# INGROUP: MokoStandards.Notifications
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||||
# PATH: /.mokogitea/workflows/notify.yml
|
# PATH: /.gitea/workflows/notify.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
||||||
|
|
||||||
@@ -18,7 +18,6 @@ on:
|
|||||||
- "Joomla Build & Release"
|
- "Joomla Build & Release"
|
||||||
- "Joomla Extension CI"
|
- "Joomla Extension CI"
|
||||||
- "Deploy"
|
- "Deploy"
|
||||||
- "Cascade Main → Dev"
|
|
||||||
types:
|
types:
|
||||||
- completed
|
- completed
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
name: "Publish to npm"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||||
|
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >-
|
||||||
|
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||||
|
!contains(github.event.head_commit.message, '[skip publish]')
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Auto-bump patch version
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
|
PKG_NAME=$(node -p "require('./package.json').name")
|
||||||
|
CURRENT=$(node -p "require('./package.json').version")
|
||||||
|
PUBLISHED=$(npm view "${PKG_NAME}@latest" version 2>/dev/null || echo "0.0.0")
|
||||||
|
|
||||||
|
if [ "$CURRENT" != "$PUBLISHED" ]; then
|
||||||
|
echo "Version ${CURRENT} not yet published, using as-is."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Bump locally to get the new version
|
||||||
|
npm version patch --no-git-tag-version
|
||||||
|
NEW_VER=$(node -p "require('./package.json').version")
|
||||||
|
echo "Bumping ${CURRENT} -> ${NEW_VER}"
|
||||||
|
|
||||||
|
# Push via Gitea API: branch + PR + merge
|
||||||
|
BRANCH="chore/npm-version-bump"
|
||||||
|
FILEPATH="package.json"
|
||||||
|
CONTENT=$(base64 -w 0 < package.json)
|
||||||
|
COMMIT_MSG="chore: bump to ${NEW_VER} [skip ci]"
|
||||||
|
|
||||||
|
# Get current file SHA on main
|
||||||
|
FILE_SHA=$(curl -sf -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API_BASE}/contents/${FILEPATH}?ref=main" \
|
||||||
|
| python3 -c "import json,sys; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
|
||||||
|
|
||||||
|
# Create chore branch from main
|
||||||
|
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API_BASE}/branches" \
|
||||||
|
-d "{\"new_branch_name\":\"${BRANCH}\",\"old_branch_name\":\"main\"}" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Get file SHA on chore branch (may differ if branch already existed)
|
||||||
|
BRANCH_SHA=$(curl -sf -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API_BASE}/contents/${FILEPATH}?ref=${BRANCH}" \
|
||||||
|
| python3 -c "import json,sys; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
|
||||||
|
[ -n "$BRANCH_SHA" ] && FILE_SHA="$BRANCH_SHA"
|
||||||
|
|
||||||
|
# Push package.json to chore branch
|
||||||
|
PAYLOAD="{\"content\":\"${CONTENT}\",\"message\":\"${COMMIT_MSG}\",\"branch\":\"${BRANCH}\",\"sha\":\"${FILE_SHA}\"}"
|
||||||
|
HTTP=$(curl -sf -o /dev/null -w "%{http_code}" -X PUT \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$PAYLOAD" \
|
||||||
|
"${API_BASE}/contents/${FILEPATH}" 2>/dev/null || echo "ERR")
|
||||||
|
echo "File push: HTTP ${HTTP}"
|
||||||
|
|
||||||
|
# Create PR
|
||||||
|
PR_NUM=$(curl -sf -X POST -H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API_BASE}/pulls" \
|
||||||
|
-d "{\"title\":\"${COMMIT_MSG}\",\"head\":\"${BRANCH}\",\"base\":\"main\"}" \
|
||||||
|
| python3 -c "import json,sys; print(json.load(sys.stdin).get('number',''))" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -n "$PR_NUM" ]; then
|
||||||
|
# Merge PR
|
||||||
|
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API_BASE}/pulls/${PR_NUM}/merge" \
|
||||||
|
-d "{\"Do\":\"merge\",\"merge_message_field\":\"${COMMIT_MSG}\"}" 2>/dev/null
|
||||||
|
echo "Version bumped via PR #${PR_NUM} (merged)"
|
||||||
|
else
|
||||||
|
echo "::warning::Could not create PR for version bump — publishing with current version"
|
||||||
|
fi
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
run: npm publish --access public
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
@@ -4,10 +4,10 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoStandards.CI
|
# INGROUP: moko-platform.CI
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||||
# PATH: /templates/workflows/universal/pr-check.yml.template
|
# PATH: /templates/workflows/universal/pr-check.yml.template
|
||||||
# VERSION: 05.00.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: PR gate — branch policy + code validation before merge
|
# BRIEF: PR gate — branch policy + code validation before merge
|
||||||
|
|
||||||
name: "Universal: PR Check"
|
name: "Universal: PR Check"
|
||||||
@@ -52,22 +52,22 @@ jobs:
|
|||||||
REASON="Fix branches must target 'dev', not '${BASE}'"
|
REASON="Fix branches must target 'dev', not '${BASE}'"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
patch/*)
|
||||||
|
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
hotfix/*)
|
hotfix/*)
|
||||||
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
|
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
|
||||||
ALLOWED=false
|
ALLOWED=false
|
||||||
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
|
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
alpha/*|beta/*)
|
rc)
|
||||||
if [ "$BASE" != "dev" ]; then
|
|
||||||
ALLOWED=false
|
|
||||||
REASON="Pre-release branches must target 'dev', not '${BASE}'"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
rc/*)
|
|
||||||
if [ "$BASE" != "main" ]; then
|
if [ "$BASE" != "main" ]; then
|
||||||
ALLOWED=false
|
ALLOWED=false
|
||||||
REASON="Release candidate branches must target 'main', not '${BASE}'"
|
REASON="RC branch can only merge into 'main', not '${BASE}'"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
dev)
|
dev)
|
||||||
@@ -96,6 +96,32 @@ jobs:
|
|||||||
echo "Branch policy: OK (${HEAD} → ${BASE})"
|
echo "Branch policy: OK (${HEAD} → ${BASE})"
|
||||||
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
|
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# ── Secret Scanning ──────────────────────────────────────────────────
|
||||||
|
gitleaks:
|
||||||
|
name: Secret Scan
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install Gitleaks
|
||||||
|
run: |
|
||||||
|
GITLEAKS_VERSION="8.21.2"
|
||||||
|
curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
|
||||||
|
| tar -xz -C /usr/local/bin gitleaks
|
||||||
|
|
||||||
|
- name: Scan PR commits for secrets
|
||||||
|
run: |
|
||||||
|
if gitleaks detect --source . --verbose \
|
||||||
|
--log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} 2>&1; then
|
||||||
|
echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "::error::Potential secrets detected in PR commits"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Code Validation ────────────────────────────────────────────────────
|
# ── Code Validation ────────────────────────────────────────────────────
|
||||||
validate:
|
validate:
|
||||||
name: Validate PR
|
name: Validate PR
|
||||||
@@ -105,11 +131,25 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Check for merge conflict markers
|
||||||
|
run: |
|
||||||
|
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
||||||
|
if [ -n "$CONFLICTS" ]; then
|
||||||
|
echo "::error::Merge conflict markers found in source files"
|
||||||
|
echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "No conflict markers found"
|
||||||
|
|
||||||
- name: Detect platform
|
- name: Detect platform
|
||||||
id: platform
|
id: platform
|
||||||
run: |
|
run: |
|
||||||
# Parse manifest for platform detection
|
# Read platform from XML manifest (<platform> tag) or plain text fallback
|
||||||
PLATFORM=$(php /tmp/mokostandards-api/cli/manifest_read.php --path . --field platform 2>/dev/null)
|
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
|
||||||
|
[ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
|
||||||
[ -z "$PLATFORM" ] && PLATFORM="generic"
|
[ -z "$PLATFORM" ] && PLATFORM="generic"
|
||||||
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
@@ -133,6 +173,98 @@ jobs:
|
|||||||
echo "PHP lint: ${ERRORS} error(s)"
|
echo "PHP lint: ${ERRORS} error(s)"
|
||||||
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
|
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
|
||||||
|
|
||||||
|
- name: Joomla JEXEC guard check
|
||||||
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
ERRORS=0
|
||||||
|
while IFS= read -r -d '' file; do
|
||||||
|
# Skip vendor, node_modules, and index.html stub files
|
||||||
|
case "$file" in ./vendor/*|./node_modules/*) continue ;; esac
|
||||||
|
# Check first 10 lines for JEXEC or JPATH guard
|
||||||
|
if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then
|
||||||
|
echo "::error file=${file}::Missing JEXEC guard: ${file}"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done < <(find . -name "*.php" -path "*/src/*" -not -path "./.git/*" -not -path "./vendor/*" -print0)
|
||||||
|
if [ "$ERRORS" -gt 0 ]; then
|
||||||
|
echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard"
|
||||||
|
echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "${ERRORS} file(s) in src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "JEXEC guard: OK"
|
||||||
|
|
||||||
|
- name: Joomla directory listing protection
|
||||||
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
MISSING=0
|
||||||
|
SOURCE_DIR="src"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && exit 0
|
||||||
|
while IFS= read -r dir; do
|
||||||
|
if [ ! -f "${dir}/index.html" ]; then
|
||||||
|
echo "::warning::Missing index.html in ${dir} (directory listing protection)"
|
||||||
|
MISSING=$((MISSING + 1))
|
||||||
|
fi
|
||||||
|
done < <(find "$SOURCE_DIR" -type d -not -path "./.git/*" -not -path "*/vendor/*" -not -path "*/node_modules/*")
|
||||||
|
if [ "$MISSING" -gt 0 ]; then
|
||||||
|
echo "## Directory Protection" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "${MISSING} director(ies) missing index.html" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
echo "Directory protection: ${MISSING} missing (advisory)"
|
||||||
|
|
||||||
|
- name: Joomla script file and asset checks
|
||||||
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
ERRORS=0
|
||||||
|
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||||
|
[ -z "$MANIFEST" ] && exit 0
|
||||||
|
MANIFEST_DIR=$(dirname "$MANIFEST")
|
||||||
|
|
||||||
|
# Check scriptfile exists if declared
|
||||||
|
SCRIPTFILE=$(sed -n 's/.*<scriptfile>\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null)
|
||||||
|
if [ -n "$SCRIPTFILE" ]; then
|
||||||
|
if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then
|
||||||
|
echo "::error::Manifest declares <scriptfile>${SCRIPTFILE}</scriptfile> but file not found at ${MANIFEST_DIR}/${SCRIPTFILE}"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "Script file: ${MANIFEST_DIR}/${SCRIPTFILE} (OK)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Require joomla.asset.json and validate it
|
||||||
|
ASSET_JSON=$(find "$MANIFEST_DIR" -name "joomla.asset.json" -not -path "./.git/*" 2>/dev/null | head -1)
|
||||||
|
if [ -z "$ASSET_JSON" ]; then
|
||||||
|
echo "::error::joomla.asset.json not found — Joomla asset system is required"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
if command -v php &> /dev/null; then
|
||||||
|
php -r "json_decode(file_get_contents('$ASSET_JSON')); if(json_last_error()!==JSON_ERROR_NONE){echo json_last_error_msg();exit(1);}" 2>&1 || {
|
||||||
|
echo "::error::joomla.asset.json is not valid JSON"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
echo "joomla.asset.json: valid"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate all XML files in src/ are well-formed
|
||||||
|
XML_ERRORS=0
|
||||||
|
if command -v php &> /dev/null; then
|
||||||
|
while IFS= read -r -d '' xmlfile; do
|
||||||
|
if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$xmlfile'); if(!\$x){foreach(libxml_get_errors() as \$e) echo trim(\$e->message) . ' in $xmlfile'; exit(1);}" 2>&1; then
|
||||||
|
XML_ERRORS=$((XML_ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done < <(find "$MANIFEST_DIR" -name "*.xml" -not -path "./.git/*" -print0)
|
||||||
|
fi
|
||||||
|
if [ "$XML_ERRORS" -gt 0 ]; then
|
||||||
|
echo "::error::${XML_ERRORS} XML file(s) are malformed"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "XML well-formedness: OK"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ "$ERRORS" -gt 0 ] && exit 1
|
||||||
|
echo "Joomla asset checks: OK"
|
||||||
|
|
||||||
- name: Validate platform manifest
|
- name: Validate platform manifest
|
||||||
run: |
|
run: |
|
||||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
@@ -150,6 +282,13 @@ jobs:
|
|||||||
for ELEMENT in name version description; do
|
for ELEMENT in name version description; do
|
||||||
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
|
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
|
||||||
done
|
done
|
||||||
|
# Block legacy raw/branch update server URLs on MokoGitea
|
||||||
|
RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true)
|
||||||
|
if [ -n "$RAW_URLS" ]; then
|
||||||
|
echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the Gitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)"
|
||||||
|
echo "$RAW_URLS"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
echo "Joomla manifest valid"
|
echo "Joomla manifest valid"
|
||||||
;;
|
;;
|
||||||
dolibarr)
|
dolibarr)
|
||||||
@@ -182,6 +321,160 @@ jobs:
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
- name: Validate Joomla language files
|
||||||
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
ERRORS=0
|
||||||
|
WARNINGS=0
|
||||||
|
|
||||||
|
# Require both en-GB and en-US language directories
|
||||||
|
LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1)
|
||||||
|
if [ -z "$LANG_ROOT" ]; then
|
||||||
|
echo "No language/ directory found — skipping"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "$LANG_ROOT/en-GB" ]; then
|
||||||
|
echo "::error::Missing en-GB language directory (${LANG_ROOT}/en-GB)"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
if [ ! -d "$LANG_ROOT/en-US" ]; then
|
||||||
|
echo "::error::Missing en-US language directory (${LANG_ROOT}/en-US)"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check that en-GB and en-US have matching .ini files
|
||||||
|
if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then
|
||||||
|
for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do
|
||||||
|
[ ! -f "$GB_INI" ] && continue
|
||||||
|
US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")"
|
||||||
|
if [ ! -f "$US_INI" ]; then
|
||||||
|
echo "::error::$(basename "$GB_INI") exists in en-GB but missing from en-US"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
for US_INI in "$LANG_ROOT/en-US"/*.ini; do
|
||||||
|
[ ! -f "$US_INI" ] && continue
|
||||||
|
GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")"
|
||||||
|
if [ ! -f "$GB_INI" ]; then
|
||||||
|
echo "::error::$(basename "$US_INI") exists in en-US but missing from en-GB"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find all .ini language files
|
||||||
|
INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null)
|
||||||
|
if [ -z "$INI_FILES" ]; then
|
||||||
|
echo "No .ini language files found"
|
||||||
|
[ "$ERRORS" -gt 0 ] && exit 1
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Found $(echo "$INI_FILES" | wc -l) language file(s)"
|
||||||
|
|
||||||
|
for FILE in $INI_FILES; do
|
||||||
|
FNAME=$(basename "$FILE")
|
||||||
|
LINENUM=0
|
||||||
|
SEEN_KEYS=""
|
||||||
|
|
||||||
|
while IFS= read -r line || [ -n "$line" ]; do
|
||||||
|
LINENUM=$((LINENUM + 1))
|
||||||
|
|
||||||
|
# Skip empty lines and comments
|
||||||
|
[ -z "$line" ] && continue
|
||||||
|
echo "$line" | grep -qE '^\s*;' && continue
|
||||||
|
echo "$line" | grep -qE '^\s*$' && continue
|
||||||
|
|
||||||
|
# Must match KEY="VALUE" format
|
||||||
|
if ! echo "$line" | grep -qE '^[A-Z_][A-Z0-9_]*=".*"$'; then
|
||||||
|
echo "::error file=${FILE},line=${LINENUM}::Malformed line: ${line}"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract key and check for duplicates
|
||||||
|
KEY=$(echo "$line" | sed 's/=.*//')
|
||||||
|
if echo "$SEEN_KEYS" | grep -qx "$KEY"; then
|
||||||
|
echo "::error file=${FILE},line=${LINENUM}::Duplicate key: ${KEY}"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
SEEN_KEYS="${SEEN_KEYS}
|
||||||
|
${KEY}"
|
||||||
|
done < "$FILE"
|
||||||
|
|
||||||
|
echo " ${FILE}: checked ${LINENUM} lines"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Cross-check en-GB vs en-US key consistency
|
||||||
|
GB_DIR=$(find . -path "*/language/en-GB" -type d -not -path "./.git/*" 2>/dev/null | head -1)
|
||||||
|
US_DIR=$(find . -path "*/language/en-US" -type d -not -path "./.git/*" 2>/dev/null | head -1)
|
||||||
|
|
||||||
|
if [ -n "$GB_DIR" ] && [ -n "$US_DIR" ]; then
|
||||||
|
for GB_FILE in "$GB_DIR"/*.ini; do
|
||||||
|
[ ! -f "$GB_FILE" ] && continue
|
||||||
|
FNAME=$(basename "$GB_FILE")
|
||||||
|
US_FILE="$US_DIR/$FNAME"
|
||||||
|
[ ! -f "$US_FILE" ] && continue
|
||||||
|
|
||||||
|
GB_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$GB_FILE" 2>/dev/null | sort)
|
||||||
|
US_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$US_FILE" 2>/dev/null | sort)
|
||||||
|
|
||||||
|
# Keys in en-GB but not en-US
|
||||||
|
MISSING_US=$(comm -23 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
|
||||||
|
if [ -n "$MISSING_US" ]; then
|
||||||
|
echo "::warning::Keys in en-GB/$FNAME but missing from en-US/$FNAME:"
|
||||||
|
echo "$MISSING_US" | while read -r k; do echo " - $k"; done
|
||||||
|
WARNINGS=$((WARNINGS + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Keys in en-US but not en-GB
|
||||||
|
MISSING_GB=$(comm -13 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
|
||||||
|
if [ -n "$MISSING_GB" ]; then
|
||||||
|
echo "::warning::Keys in en-US/$FNAME but missing from en-GB/$FNAME:"
|
||||||
|
echo "$MISSING_GB" | while read -r k; do echo " - $k"; done
|
||||||
|
WARNINGS=$((WARNINGS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "### Language File Validation"
|
||||||
|
echo "| Metric | Count |"
|
||||||
|
echo "|---|---|"
|
||||||
|
echo "| Files checked | $(echo "$INI_FILES" | wc -l) |"
|
||||||
|
echo "| Errors | ${ERRORS} |"
|
||||||
|
echo "| Warnings | ${WARNINGS} |"
|
||||||
|
} >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
if [ "$ERRORS" -gt 0 ]; then
|
||||||
|
echo "::error::Language validation failed with ${ERRORS} error(s)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Language files: OK (${WARNINGS} warning(s))"
|
||||||
|
|
||||||
|
- name: Check changelog has unreleased entry
|
||||||
|
run: |
|
||||||
|
if [ ! -f "CHANGELOG.md" ]; then
|
||||||
|
echo "::warning::No CHANGELOG.md found"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
# Check for content under [Unreleased] section
|
||||||
|
if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
|
||||||
|
echo "::error::CHANGELOG.md missing [Unreleased] section"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
|
||||||
|
UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
|
||||||
|
if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
|
||||||
|
echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
|
||||||
|
echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
|
||||||
|
|
||||||
- name: Verify package source
|
- name: Verify package source
|
||||||
run: |
|
run: |
|
||||||
SOURCE_DIR="src"
|
SOURCE_DIR="src"
|
||||||
@@ -194,31 +487,48 @@ jobs:
|
|||||||
echo "Source: ${FILE_COUNT} files"
|
echo "Source: ${FILE_COUNT} files"
|
||||||
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
|
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
|
||||||
|
|
||||||
# ── Changelog Gate ────────────────────────────────────────────────────
|
# ── Pre-Release RC Build ─────────────────────────────────────────────────
|
||||||
changelog:
|
pre-release:
|
||||||
name: Changelog Updated
|
name: Build RC Package
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.base_ref == 'main'
|
needs: [branch-policy, validate]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Trigger RC pre-release
|
||||||
with:
|
env:
|
||||||
fetch-depth: 0
|
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
- name: Check CHANGELOG.md was updated
|
BRANCH: ${{ github.head_ref }}
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
run: |
|
run: |
|
||||||
BASE="${{ github.event.pull_request.base.sha }}"
|
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
|
||||||
HEAD="${{ github.event.pull_request.head.sha }}"
|
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
if git diff --name-only "$BASE" "$HEAD" | grep -q "^CHANGELOG.md$"; then
|
# ── Issue Reporter ──────────────────────────────────────────────────────
|
||||||
echo "CHANGELOG.md updated"
|
report-issues:
|
||||||
else
|
name: Report Issues
|
||||||
# Allow [skip changelog] in PR title or body
|
runs-on: ubuntu-latest
|
||||||
PR_TITLE="${{ github.event.pull_request.title }}"
|
needs: [branch-policy, validate]
|
||||||
PR_BODY="${{ github.event.pull_request.body }}"
|
if: >-
|
||||||
if echo "$PR_TITLE $PR_BODY" | grep -qi "\[skip changelog\]"; then
|
always() &&
|
||||||
echo "::warning::Changelog skip requested via [skip changelog]"
|
needs.validate.result == 'failure'
|
||||||
exit 0
|
|
||||||
fi
|
steps:
|
||||||
echo "::error::CHANGELOG.md must be updated before merging to main. Add [skip changelog] to the PR title to bypass."
|
- name: Checkout
|
||||||
exit 1
|
uses: actions/checkout@v4
|
||||||
fi
|
with:
|
||||||
|
sparse-checkout: automation/ci-issue-reporter.sh
|
||||||
|
sparse-checkout-cone-mode: false
|
||||||
|
|
||||||
|
- name: "File issue for PR validation failure"
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
run: |
|
||||||
|
chmod +x automation/ci-issue-reporter.sh
|
||||||
|
./automation/ci-issue-reporter.sh \
|
||||||
|
--gate "PR Validation" \
|
||||||
|
--workflow "PR Check" \
|
||||||
|
--severity error \
|
||||||
|
--details "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed."
|
||||||
|
|||||||
@@ -4,15 +4,26 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Release
|
# INGROUP: mokocli.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||||
# VERSION: 05.01.00
|
# VERSION: 05.01.00
|
||||||
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
|
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
|
||||||
|
|
||||||
name: "Universal: Pre-Release"
|
name: "Universal: Pre-Release"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
- 'fix/**'
|
||||||
|
- 'patch/**'
|
||||||
|
- 'hotfix/**'
|
||||||
|
- 'bugfix/**'
|
||||||
|
- 'chore/**'
|
||||||
|
- alpha
|
||||||
|
- beta
|
||||||
|
- rc
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
stability:
|
stability:
|
||||||
@@ -35,56 +46,74 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: "Build Pre-Release (${{ inputs.stability }})"
|
name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})"
|
||||||
runs-on: release
|
runs-on: release
|
||||||
|
if: >-
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
github.event_name == 'push'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.GA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
ref: ${{ github.ref_name }}
|
||||||
|
|
||||||
- name: Setup tools
|
- name: Setup mokocli tools
|
||||||
|
env:
|
||||||
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
run: |
|
run: |
|
||||||
# Update moko-platform CLI tools if available; install PHP if missing
|
# Use pre-installed /opt/mokocli if available (updated by cron every 6h)
|
||||||
if command -v moko-platform-update &> /dev/null; then
|
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/cli/manifest_element.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
|
||||||
moko-platform-update
|
echo Using pre-installed /opt/mokocli
|
||||||
elif [ -d "/opt/moko-platform" ]; then
|
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
|
||||||
cd /opt/moko-platform && git pull origin main --quiet 2>/dev/null || true
|
|
||||||
else
|
else
|
||||||
if ! command -v php &> /dev/null; then
|
echo Falling back to fresh clone
|
||||||
sudo apt-get update -qq
|
if ! command -v composer > /dev/null 2>&1; then
|
||||||
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl >/dev/null 2>&1
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
git clone --depth 1 --branch main --quiet \
|
rm -rf /tmp/mokocli
|
||||||
"https://x-access-token:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
|
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
|
||||||
/tmp/moko-platform-api
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
||||||
fi
|
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
||||||
# Set MOKO_CLI to whichever path exists
|
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
||||||
if [ -d "/opt/moko-platform/cli" ]; then
|
|
||||||
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
|
|
||||||
else
|
|
||||||
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Detect platform
|
- name: Detect platform
|
||||||
id: platform
|
id: platform
|
||||||
run: |
|
run: |
|
||||||
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1 | tr -d '[:space:]')
|
# Auto-detect and update platform if not set in manifest
|
||||||
[ -z "$PLATFORM" ] && PLATFORM="generic"
|
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
|
||||||
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
||||||
MANIFEST=$(find ./src -maxdepth 1 -name "pkg_*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
|
||||||
[ -z "$MANIFEST" ] && MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "*/packages/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
- name: Check platform eligibility (Joomla only)
|
||||||
[ -z "$MANIFEST" ] && MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
id: eligibility
|
||||||
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
|
run: |
|
||||||
echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT"
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT"
|
if [[ "$PLATFORM" == joomla* ]] || [[ "$PLATFORM" == "joomla" ]]; then
|
||||||
|
echo "proceed=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "proceed=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "::notice::Platform '$PLATFORM' — non-Joomla, skipping pre-release auto-bump"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Resolve metadata and bump version
|
- name: Resolve metadata and bump version
|
||||||
id: meta
|
id: meta
|
||||||
|
if: steps.eligibility.outputs.proceed == 'true'
|
||||||
run: |
|
run: |
|
||||||
STABILITY="${{ inputs.stability }}"
|
# Auto-detect stability from branch name on push, or use input on dispatch
|
||||||
|
if [ "${{ github.event_name }}" = "push" ]; then
|
||||||
|
case "${{ github.ref_name }}" in
|
||||||
|
rc) STABILITY="release-candidate" ;;
|
||||||
|
alpha) STABILITY="alpha" ;;
|
||||||
|
beta) STABILITY="beta" ;;
|
||||||
|
*) STABILITY="development" ;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
STABILITY="${{ inputs.stability || 'development' }}"
|
||||||
|
fi
|
||||||
|
|
||||||
case "$STABILITY" in
|
case "$STABILITY" in
|
||||||
development) SUFFIX="-dev"; TAG="development" ;;
|
development) SUFFIX="-dev"; TAG="development" ;;
|
||||||
@@ -93,60 +122,50 @@ jobs:
|
|||||||
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
|
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Patch bump via CLI tool
|
# Bump version via CLI: patch for dev/alpha/beta, minor for RC
|
||||||
php ${MOKO_CLI}/version_bump.php --path .
|
case "$STABILITY" in
|
||||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
|
release-candidate) BUMP="minor" ;;
|
||||||
[ -z "$VERSION" ] && VERSION="00.00.01"
|
*) BUMP="patch" ;;
|
||||||
TODAY=$(date +%Y-%m-%d)
|
esac
|
||||||
|
|
||||||
# Update platform-specific manifest
|
php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true
|
||||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
|
||||||
MANIFEST="${{ steps.platform.outputs.manifest }}"
|
# Set stability suffix and verify consistency
|
||||||
MOD_FILE="${{ steps.platform.outputs.mod_file }}"
|
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01")
|
||||||
|
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||||
|
|
||||||
php ${MOKO_CLI}/version_set_platform.php \
|
php ${MOKO_CLI}/version_set_platform.php \
|
||||||
--path . --version "$VERSION" --branch "${{ github.ref_name }}" 2>/dev/null || true
|
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
|
||||||
|
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
||||||
|
|
||||||
|
# Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml
|
||||||
|
php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true
|
||||||
|
|
||||||
|
# Append suffix for output
|
||||||
|
if [ -n "$SUFFIX" ]; then
|
||||||
|
VERSION="${VERSION}${SUFFIX}"
|
||||||
|
fi
|
||||||
|
|
||||||
# Commit version bump
|
# Commit version bump
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
git config --local user.name "gitea-actions[bot]"
|
git config --local user.name "gitea-actions[bot]"
|
||||||
git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
git add -A
|
git add -A
|
||||||
git diff --cached --quiet || {
|
git diff --cached --quiet || {
|
||||||
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
|
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
|
||||||
git push origin HEAD 2>&1
|
git push origin HEAD 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Auto-detect element (platform-aware)
|
# Auto-detect element via manifest_element.php
|
||||||
EXT_ELEMENT=""
|
php ${MOKO_CLI}/manifest_element.php \
|
||||||
case "$PLATFORM" in
|
--path . --version "$VERSION" --stability "$STABILITY" \
|
||||||
joomla)
|
--repo "${GITEA_REPO}" --github-output
|
||||||
if [ -n "$MANIFEST" ]; then
|
|
||||||
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
|
|
||||||
if [ -z "$EXT_ELEMENT" ]; then
|
|
||||||
EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
|
|
||||||
case "$EXT_ELEMENT" in
|
|
||||||
templatedetails|manifest) EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
dolibarr)
|
|
||||||
if [ -n "$MOD_FILE" ]; then
|
|
||||||
MOD_BASENAME=$(basename "$MOD_FILE" .class.php)
|
|
||||||
EXT_ELEMENT=$(echo "$MOD_BASENAME" | sed 's/^mod//' | tr '[:upper:]' '[:lower:]')
|
|
||||||
else
|
|
||||||
EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
|
# Read back element outputs
|
||||||
|
EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
|
||||||
|
ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
|
||||||
|
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
||||||
|
[ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
|
||||||
|
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
||||||
@@ -154,204 +173,78 @@ jobs:
|
|||||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||||
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
|
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
|
||||||
echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
|
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
|
||||||
|
|
||||||
- name: Build package
|
- name: Create release
|
||||||
run: |
|
|
||||||
SOURCE_DIR="src"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
|
||||||
if [ ! -d "$SOURCE_DIR" ]; then
|
|
||||||
echo "::error::No src/ or htdocs/ directory"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
MANIFEST="${{ steps.meta.outputs.manifest }}"
|
|
||||||
EXT_TYPE=""
|
|
||||||
if [ -n "$MANIFEST" ]; then
|
|
||||||
EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
fi
|
|
||||||
|
|
||||||
EXCLUDES="sftp-config* .ftpignore *.ppk *.pem *.key .env* *.local .build-trigger"
|
|
||||||
|
|
||||||
mkdir -p build/package
|
|
||||||
|
|
||||||
if [ "$EXT_TYPE" = "package" ] && [ -d "${SOURCE_DIR}/packages" ]; then
|
|
||||||
echo "=== Building Joomla PACKAGE (multi-extension) ==="
|
|
||||||
for ext_dir in "${SOURCE_DIR}"/packages/*/; do
|
|
||||||
[ ! -d "$ext_dir" ] && continue
|
|
||||||
EXT_NAME=$(basename "$ext_dir")
|
|
||||||
echo " Packaging sub-extension: ${EXT_NAME}"
|
|
||||||
cd "$ext_dir"
|
|
||||||
zip -r "../../build/package/${EXT_NAME}.zip" . -x $EXCLUDES
|
|
||||||
cd "$OLDPWD"
|
|
||||||
done
|
|
||||||
for f in "${SOURCE_DIR}"/*.xml "${SOURCE_DIR}"/*.php; do
|
|
||||||
[ -f "$f" ] && cp "$f" build/package/
|
|
||||||
done
|
|
||||||
else
|
|
||||||
echo "=== Building standard extension ==="
|
|
||||||
rsync -a \
|
|
||||||
--exclude='sftp-config*' \
|
|
||||||
--exclude='.ftpignore' \
|
|
||||||
--exclude='*.ppk' \
|
|
||||||
--exclude='*.pem' \
|
|
||||||
--exclude='*.key' \
|
|
||||||
--exclude='.env*' \
|
|
||||||
--exclude='*.local' \
|
|
||||||
--exclude='.build-trigger' \
|
|
||||||
"${SOURCE_DIR}/" build/package/
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Create ZIP
|
|
||||||
id: zip
|
|
||||||
run: |
|
|
||||||
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
|
|
||||||
cd build/package
|
|
||||||
zip -r "../${ZIP_NAME}" .
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
SHA256=$(sha256sum "${ZIP_NAME}" | cut -d' ' -f1)
|
|
||||||
echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "ZIP: ${ZIP_NAME} (SHA: ${SHA256:0:16}...)"
|
|
||||||
|
|
||||||
- name: Create or replace Gitea release
|
|
||||||
id: release
|
id: release
|
||||||
|
if: steps.eligibility.outputs.proceed == 'true'
|
||||||
run: |
|
run: |
|
||||||
TAG="${{ steps.meta.outputs.tag }}"
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
SHA256="${{ steps.zip.outputs.sha256 }}"
|
php ${MOKO_CLI}/release_create.php \
|
||||||
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
|
--path . --version "$VERSION" --tag "$TAG" \
|
||||||
EXT_ELEMENT="${{ steps.meta.outputs.ext_element }}"
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
TOKEN="${{ secrets.GA_TOKEN }}"
|
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
|
||||||
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
BRANCH=$(git branch --show-current)
|
|
||||||
|
|
||||||
BODY="## ${VERSION} ($(date +%Y-%m-%d))
|
- name: Update release notes from CHANGELOG.md
|
||||||
**Channel:** ${STABILITY}
|
if: steps.eligibility.outputs.proceed == 'true'
|
||||||
**SHA-256:** \`${SHA256}\`"
|
|
||||||
|
|
||||||
# Delete existing release
|
|
||||||
EXISTING_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \
|
|
||||||
"${API}/releases/tags/${TAG}" | jq -r '.id // empty' 2>/dev/null)
|
|
||||||
if [ -n "$EXISTING_ID" ]; then
|
|
||||||
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
|
|
||||||
"${API}/releases/${EXISTING_ID}" 2>/dev/null || true
|
|
||||||
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
|
|
||||||
"${API}/tags/${TAG}" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create release
|
|
||||||
RELEASE_ID=$(curl -sS -X POST -H "Authorization: token ${TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${API}/releases" \
|
|
||||||
-d "$(jq -n \
|
|
||||||
--arg tag "$TAG" \
|
|
||||||
--arg target "$BRANCH" \
|
|
||||||
--arg name "${EXT_ELEMENT} ${VERSION} (${STABILITY})" \
|
|
||||||
--arg body "$BODY" \
|
|
||||||
'{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: true}'
|
|
||||||
)" | jq -r '.id')
|
|
||||||
|
|
||||||
echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
# Upload ZIP
|
|
||||||
curl -sS -X POST -H "Authorization: token ${TOKEN}" \
|
|
||||||
-H "Content-Type: application/octet-stream" \
|
|
||||||
"${API}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \
|
|
||||||
--data-binary "@build/${ZIP_NAME}"
|
|
||||||
|
|
||||||
echo "Released: ${EXT_ELEMENT} ${VERSION} (${STABILITY})"
|
|
||||||
|
|
||||||
- name: Update updates.xml
|
|
||||||
if: steps.platform.outputs.platform == 'joomla'
|
|
||||||
run: |
|
run: |
|
||||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
|
||||||
SHA256="${{ steps.zip.outputs.sha256 }}"
|
|
||||||
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
|
|
||||||
TAG="${{ steps.meta.outputs.tag }}"
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
if [ ! -f "updates.xml" ]; then
|
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
|
||||||
echo "No updates.xml -- skipping"
|
if [ -f "CHANGELOG.md" ]; then
|
||||||
exit 0
|
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
||||||
|
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
|
||||||
|
else
|
||||||
|
NOTES="Release ${VERSION}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Map stability to XML tag name
|
# Update release body via API
|
||||||
case "$STABILITY" in
|
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
development) XML_TAG="development" ;;
|
"${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||||
alpha) XML_TAG="alpha" ;;
|
|
||||||
beta) XML_TAG="beta" ;;
|
|
||||||
release-candidate) XML_TAG="rc" ;;
|
|
||||||
*) XML_TAG="$STABILITY" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${TAG}/${ZIP_NAME}"
|
if [ -n "$RELEASE_ID" ]; then
|
||||||
|
python3 -c "
|
||||||
# Use PHP to update the channel in updates.xml
|
import json, urllib.request
|
||||||
php -r '
|
body = open('/dev/stdin').read()
|
||||||
$xml_tag = $argv[1];
|
payload = json.dumps({'body': body}).encode()
|
||||||
$version = $argv[2];
|
req = urllib.request.Request(
|
||||||
$sha256 = $argv[3];
|
'${API_BASE}/releases/${RELEASE_ID}',
|
||||||
$url = $argv[4];
|
data=payload, method='PATCH',
|
||||||
$date = date("Y-m-d");
|
headers={
|
||||||
|
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
|
||||||
$content = file_get_contents("updates.xml");
|
'Content-Type': 'application/json'
|
||||||
$pattern = "/(<update>(?:(?!<\/update>).)*?<tag>" . preg_quote($xml_tag) . "<\/tag>.*?<\/update>)/s";
|
})
|
||||||
|
urllib.request.urlopen(req)
|
||||||
$content = preg_replace_callback($pattern, function($m) use ($version, $sha256, $url, $date) {
|
" <<< "$NOTES"
|
||||||
$block = $m[0];
|
echo "Release notes updated from CHANGELOG.md"
|
||||||
$block = preg_replace("/<version>[^<]*<\/version>/", "<version>{$version}</version>", $block);
|
|
||||||
if (strpos($block, "<sha256>") !== false) {
|
|
||||||
$block = preg_replace("/<sha256>[^<]*<\/sha256>/", "<sha256>{$sha256}</sha256>", $block);
|
|
||||||
} else {
|
|
||||||
$block = str_replace("</downloads>", "</downloads>\n <sha256>{$sha256}</sha256>", $block);
|
|
||||||
}
|
|
||||||
$block = preg_replace("/(<downloadurl[^>]*>)[^<]*(<\/downloadurl>)/", "\${1}{$url}\${2}", $block);
|
|
||||||
return $block;
|
|
||||||
}, $content);
|
|
||||||
|
|
||||||
file_put_contents("updates.xml", $content);
|
|
||||||
echo "Updated {$xml_tag} channel: version={$version}\n";
|
|
||||||
' "$XML_TAG" "$VERSION" "$SHA256" "$DOWNLOAD_URL"
|
|
||||||
|
|
||||||
# Commit and push
|
|
||||||
if ! git diff --quiet updates.xml 2>/dev/null; then
|
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
|
||||||
git config --local user.name "gitea-actions[bot]"
|
|
||||||
git add updates.xml
|
|
||||||
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
|
|
||||||
git push origin HEAD 2>&1 || echo "WARNING: push failed"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: "Sync updates.xml to all branches"
|
- name: Build package and upload
|
||||||
if: steps.platform.outputs.platform == 'joomla'
|
id: package
|
||||||
|
if: steps.eligibility.outputs.proceed == 'true'
|
||||||
run: |
|
run: |
|
||||||
CURRENT_BRANCH="${{ github.ref_name }}"
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
git config --local user.name "gitea-actions[bot]"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
php ${MOKO_CLI}/release_package.php \
|
||||||
|
--path . --version "$VERSION" --tag "$TAG" \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
|
--repo "${GITEA_REPO}" --output /tmp || true
|
||||||
|
|
||||||
for BRANCH in main dev; do
|
# updates.xml is generated dynamically by MokoGitea license server
|
||||||
[ "$BRANCH" = "$CURRENT_BRANCH" ] && continue
|
# No need to build, commit, or sync updates.xml from workflows
|
||||||
echo "Syncing updates.xml -> ${BRANCH}"
|
|
||||||
git fetch origin "${BRANCH}" 2>/dev/null || continue
|
|
||||||
git checkout "origin/${BRANCH}" -- . 2>/dev/null || continue
|
|
||||||
git checkout "${CURRENT_BRANCH}" -- updates.xml
|
|
||||||
if ! git diff --quiet updates.xml 2>/dev/null; then
|
|
||||||
git add updates.xml
|
|
||||||
git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]"
|
|
||||||
git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed"
|
|
||||||
fi
|
|
||||||
git checkout "${CURRENT_BRANCH}" 2>/dev/null
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: "Delete lesser pre-release channels (cascade)"
|
- name: "Delete lesser pre-release channels (cascade)"
|
||||||
|
if: steps.eligibility.outputs.proceed == 'true'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
TOKEN="${{ secrets.GA_TOKEN }}"
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
php ${MOKO_CLI}/release_cascade.php \
|
php ${MOKO_CLI}/release_cascade.php \
|
||||||
--stability "${{ steps.meta.outputs.stability }}" \
|
--stability "${{ steps.meta.outputs.stability }}" \
|
||||||
@@ -364,7 +257,7 @@ jobs:
|
|||||||
VERSION="${{ steps.meta.outputs.version }}"
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
|
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
|
||||||
SHA256="${{ steps.zip.outputs.sha256 }}"
|
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
||||||
echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
|
echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: mokocli.Universal
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
|
# PATH: /.mokogitea/workflows/rc-revert.yml
|
||||||
|
# VERSION: 09.23.00
|
||||||
|
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
|
||||||
|
|
||||||
|
name: "RC Revert"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
revert:
|
||||||
|
name: Rename rc/ back to dev/
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >-
|
||||||
|
github.event.pull_request.merged == false &&
|
||||||
|
startsWith(github.event.pull_request.head.ref, 'rc/')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Rename branch
|
||||||
|
run: |
|
||||||
|
BRANCH="${{ github.event.pull_request.head.ref }}"
|
||||||
|
SUFFIX="${BRANCH#rc/}"
|
||||||
|
DEV_BRANCH="dev/${SUFFIX}"
|
||||||
|
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
|
||||||
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
|
# Create dev/ branch from rc/ branch
|
||||||
|
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"new_branch_name\": \"${DEV_BRANCH}\", \"old_branch_name\": \"${BRANCH}\"}" \
|
||||||
|
"${API}" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ "$STATUS" = "201" ]; then
|
||||||
|
echo "Created branch: ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Delete rc/ branch
|
||||||
|
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
|
||||||
|
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API}/${ENCODED}" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ "$STATUS" = "204" ]; then
|
||||||
|
echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "::warning::Failed to delete ${BRANCH} (HTTP ${STATUS})"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "### RC Reverted" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "${BRANCH} → ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -7,19 +7,15 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoStandards.Validation
|
# INGROUP: mokocli.Validation
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
|
||||||
# PATH: /templates/workflows/joomla/repo_health.yml.template
|
# PATH: /templates/workflows/joomla/repo_health.yml.template
|
||||||
# VERSION: 04.06.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts.
|
# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts.
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
name: "Generic: Repo Health"
|
name: "Generic: Repo Health"
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: repo-health-${{ github.repository }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -28,32 +24,28 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
profile:
|
profile:
|
||||||
description: 'Validation profile: all, release, scripts, or repo'
|
description: 'Validation profile: all, scripts, or repo'
|
||||||
required: true
|
required: true
|
||||||
default: all
|
default: all
|
||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- all
|
- all
|
||||||
- release
|
|
||||||
- scripts
|
- scripts
|
||||||
- repo
|
- repo
|
||||||
pull_request:
|
pull_request:
|
||||||
push:
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
env:
|
env:
|
||||||
# Release policy - Repository Variables Only
|
|
||||||
RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX
|
|
||||||
RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX
|
|
||||||
|
|
||||||
# Scripts governance policy
|
# Scripts governance policy
|
||||||
SCRIPTS_REQUIRED_DIRS:
|
SCRIPTS_REQUIRED_DIRS:
|
||||||
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
|
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
|
||||||
|
|
||||||
# Repo health policy
|
# Repo health policy
|
||||||
REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.gitea/workflows/
|
REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.mokogitea/workflows/
|
||||||
REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/
|
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
|
||||||
@@ -64,7 +56,7 @@ env:
|
|||||||
# File / directory variables
|
# File / directory variables
|
||||||
DOCS_INDEX: docs/docs-index.md
|
DOCS_INDEX: docs/docs-index.md
|
||||||
SCRIPT_DIR: scripts
|
SCRIPT_DIR: scripts
|
||||||
WORKFLOWS_DIR: .gitea/workflows
|
WORKFLOWS_DIR: .mokogitea/workflows
|
||||||
SHELLCHECK_PATTERN: '*.sh'
|
SHELLCHECK_PATTERN: '*.sh'
|
||||||
SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml'
|
SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml'
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
@@ -85,7 +77,7 @@ jobs:
|
|||||||
- name: Check actor permission (admin only)
|
- name: Check actor permission (admin only)
|
||||||
id: perm
|
id: perm
|
||||||
env:
|
env:
|
||||||
TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
|
||||||
REPO: ${{ github.repository }}
|
REPO: ${{ github.repository }}
|
||||||
ACTOR: ${{ github.actor }}
|
ACTOR: ${{ github.actor }}
|
||||||
run: |
|
run: |
|
||||||
@@ -142,101 +134,6 @@ jobs:
|
|||||||
printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}"
|
printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}"
|
||||||
exit 1
|
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Guardrails release vars
|
|
||||||
env:
|
|
||||||
PROFILE_RAW: ${{ github.event.inputs.profile }}
|
|
||||||
RS_FTP_PATH_SUFFIX: ${{ vars.RS_FTP_PATH_SUFFIX }}
|
|
||||||
DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
profile="${PROFILE_RAW:-all}"
|
|
||||||
case "${profile}" in
|
|
||||||
all|release|scripts|repo) ;;
|
|
||||||
*)
|
|
||||||
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then
|
|
||||||
{
|
|
||||||
printf '%s\n' '### Release configuration (Repository Variables)'
|
|
||||||
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_REPO_VARS}"
|
|
||||||
IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_REPO_VARS}"
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
{
|
|
||||||
printf '%s\n' '### Release configuration (Repository Variables)'
|
|
||||||
printf '%s\n' "Profile: ${profile}"
|
|
||||||
printf '%s\n' '| Variable | Status |'
|
|
||||||
printf '%s\n' '|---|---|'
|
|
||||||
printf '%s\n' "| RS_FTP_PATH_SUFFIX | ${RS_FTP_PATH_SUFFIX:-NOT SET} |"
|
|
||||||
printf '%s\n' "| DEV_FTP_SUFFIX | ${DEV_FTP_SUFFIX:-NOT SET} |"
|
|
||||||
printf '\n'
|
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
|
|
||||||
if [ "${#missing_optional[@]}" -gt 0 ]; then
|
|
||||||
{
|
|
||||||
printf '%s\n' '### Missing optional repository variables'
|
|
||||||
for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
|
|
||||||
printf '\n'
|
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "${#missing[@]}" -gt 0 ]; then
|
|
||||||
{
|
|
||||||
printf '%s\n' '### Missing required repository variables'
|
|
||||||
for m in "${missing[@]}"; do printf '%s\n' "- ${m}"; done
|
|
||||||
printf '%s\n' 'ERROR: Guardrails failed. Missing required repository variables.'
|
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
{
|
|
||||||
printf '%s\n' '### Repository variables validation result'
|
|
||||||
printf '%s\n' 'Status: OK'
|
|
||||||
printf '%s\n' 'All required repository variables present.'
|
|
||||||
printf '%s\n' ''
|
|
||||||
printf '%s\n' '**Note**: Organization secrets (RS_FTP_HOST, RS_FTP_USER, etc.) are validated at deployment time, not in repository health checks.'
|
|
||||||
printf '\n'
|
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
|
|
||||||
scripts_governance:
|
scripts_governance:
|
||||||
name: Scripts governance
|
name: Scripts governance
|
||||||
needs: access_check
|
needs: access_check
|
||||||
@@ -260,14 +157,14 @@ jobs:
|
|||||||
|
|
||||||
profile="${PROFILE_RAW:-all}"
|
profile="${PROFILE_RAW:-all}"
|
||||||
case "${profile}" in
|
case "${profile}" in
|
||||||
all|release|scripts|repo) ;;
|
all|scripts|repo) ;;
|
||||||
*)
|
*)
|
||||||
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if [ "${profile}" = 'release' ] || [ "${profile}" = 'repo' ]; then
|
if [ "${profile}" = 'repo' ]; then
|
||||||
{
|
{
|
||||||
printf '%s\n' '### Scripts governance'
|
printf '%s\n' '### Scripts governance'
|
||||||
printf '%s\n' "Profile: ${profile}"
|
printf '%s\n' "Profile: ${profile}"
|
||||||
@@ -288,7 +185,7 @@ jobs:
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"
|
if [ -n "${SCRIPTS_REQUIRED_DIRS:-}" ]; then IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"; else required_dirs=(); fi
|
||||||
IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}"
|
IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}"
|
||||||
|
|
||||||
missing_dirs=()
|
missing_dirs=()
|
||||||
@@ -374,14 +271,14 @@ jobs:
|
|||||||
|
|
||||||
profile="${PROFILE_RAW:-all}"
|
profile="${PROFILE_RAW:-all}"
|
||||||
case "${profile}" in
|
case "${profile}" in
|
||||||
all|release|scripts|repo) ;;
|
all|scripts|repo) ;;
|
||||||
*)
|
*)
|
||||||
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if [ "${profile}" = 'release' ] || [ "${profile}" = 'scripts' ]; then
|
if [ "${profile}" = 'scripts' ]; then
|
||||||
{
|
{
|
||||||
printf '%s\n' '### Repository health'
|
printf '%s\n' '### Repository health'
|
||||||
printf '%s\n' "Profile: ${profile}"
|
printf '%s\n' "Profile: ${profile}"
|
||||||
@@ -392,23 +289,27 @@ jobs:
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Source directory: src/ or htdocs/ (either is valid)
|
IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}"
|
||||||
|
IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}"
|
||||||
|
if [ -n "${REPO_DISALLOWED_DIRS:-}" ]; then IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"; else disallowed_dirs=(); fi
|
||||||
|
IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES:-}"
|
||||||
|
|
||||||
|
missing_required=()
|
||||||
|
missing_optional=()
|
||||||
|
|
||||||
|
# Source directory: src/ or htdocs/ (either is valid for extension repos)
|
||||||
|
SOURCE_DIR=""
|
||||||
if [ -d "src" ]; then
|
if [ -d "src" ]; then
|
||||||
SOURCE_DIR="src"
|
SOURCE_DIR="src"
|
||||||
elif [ -d "htdocs" ]; then
|
elif [ -d "htdocs" ]; then
|
||||||
SOURCE_DIR="htdocs"
|
SOURCE_DIR="htdocs"
|
||||||
|
elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then
|
||||||
|
# Platform/tooling repos don't need src/
|
||||||
|
SOURCE_DIR=""
|
||||||
else
|
else
|
||||||
missing_required+=("src/ or htdocs/ (source directory required)")
|
missing_required+=("src/ or htdocs/ (source directory required)")
|
||||||
fi
|
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
|
for item in "${required_artifacts[@]}"; do
|
||||||
if printf '%s' "${item}" | grep -q '/$'; then
|
if printf '%s' "${item}" | grep -q '/$'; then
|
||||||
d="${item%/}"
|
d="${item%/}"
|
||||||
@@ -450,12 +351,8 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//')
|
done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//')
|
||||||
|
|
||||||
if [ "${#dev_paths[@]}" -eq 0 ]; then
|
if [ "${#dev_paths[@]}" -eq 0 ] && [ "${#dev_branches[@]}" -eq 0 ]; then
|
||||||
missing_required+=("dev/* branch (e.g. dev/01.00.00)")
|
missing_required+=("dev or dev/* branch")
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "${#dev_branches[@]}" -gt 0 ]; then
|
|
||||||
missing_required+=("invalid branch dev (must be dev/<version>)")
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
content_warnings=()
|
content_warnings=()
|
||||||
@@ -481,26 +378,7 @@ jobs:
|
|||||||
export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")"
|
export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")"
|
||||||
export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")"
|
export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")"
|
||||||
|
|
||||||
report_json="$(python3 - <<'PY'
|
report_json=$(printf '{"profile":"%s","missing_required":%d,"missing_optional":%d,"content_warnings":%d}' "$profile" "${#missing_required[@]}" "${#missing_optional[@]}" "${#content_warnings[@]}")
|
||||||
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' '### Repository health'
|
||||||
@@ -578,12 +456,14 @@ jobs:
|
|||||||
joomla_findings+=("updates.xml missing in root (required for Joomla update server)")
|
joomla_findings+=("updates.xml missing in root (required for Joomla update server)")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ -n "${SOURCE_DIR}" ]; then
|
||||||
INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site")
|
INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site")
|
||||||
for dir in "${INDEX_DIRS[@]}"; do
|
for dir in "${INDEX_DIRS[@]}"; do
|
||||||
if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then
|
if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then
|
||||||
joomla_findings+=("${dir}/index.html missing (directory listing protection)")
|
joomla_findings+=("${dir}/index.html missing (directory listing protection)")
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "${#joomla_findings[@]}" -gt 0 ]; then
|
if [ "${#joomla_findings[@]}" -gt 0 ]; then
|
||||||
{
|
{
|
||||||
@@ -629,43 +509,29 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -f "${DOCS_INDEX}" ]; then
|
if [ -f "${DOCS_INDEX}" ]; then
|
||||||
missing_links="$(python3 - <<'PY'
|
missing_links=""
|
||||||
import os
|
while IFS= read -r docline; do
|
||||||
import re
|
for link in $(echo "$docline" | grep -oE '\]\([^)]+\)' | sed 's/\](//' | sed 's/)$//' || true); do
|
||||||
|
case "$link" in http://*|https://*|"#"*|mailto:*) continue ;; esac
|
||||||
idx = os.environ.get('DOCS_INDEX', 'docs/docs-index.md')
|
linkpath="${link%%#*}"
|
||||||
base = os.getcwd()
|
linkpath="${linkpath%%\?*}"
|
||||||
|
[ -z "$linkpath" ] && continue
|
||||||
bad = []
|
if [ "${linkpath:0:1}" = "/" ]; then
|
||||||
pat = re.compile(r'\[[^\]]+\]\(([^)]+)\)')
|
testpath="${linkpath#/}"
|
||||||
|
else
|
||||||
with open(idx, 'r', encoding='utf-8') as f:
|
testpath="$(dirname "${DOCS_INDEX}")/${linkpath}"
|
||||||
for line in f:
|
fi
|
||||||
for m in pat.findall(line):
|
[ ! -e "$testpath" ] && missing_links="${missing_links}${testpath} "
|
||||||
link = m.strip()
|
done
|
||||||
if link.startswith('http://') or link.startswith('https://') or link.startswith('#') or link.startswith('mailto:'):
|
done < "${DOCS_INDEX}"
|
||||||
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
|
if [ -n "${missing_links}" ]; then
|
||||||
extended_findings+=("docs/docs-index.md contains broken relative links")
|
extended_findings+=("docs/docs-index.md contains broken relative links")
|
||||||
{
|
{
|
||||||
printf '%s\n' '### Docs index link integrity'
|
printf '%s\n' '### Docs index link integrity'
|
||||||
printf '%s\n' 'Broken relative links:'
|
printf '%s\n' 'Broken relative links:'
|
||||||
while IFS= read -r l; do [ -n "${l}" ] && printf '%s\n' "- ${l}"; done <<< "${missing_links}"
|
for bl in ${missing_links}; do
|
||||||
|
printf '%s\n' "- ${bl}"
|
||||||
|
done
|
||||||
printf '\n'
|
printf '\n'
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
fi
|
fi
|
||||||
@@ -739,7 +605,7 @@ jobs:
|
|||||||
printf '%s\n' '| Domain | Status | Notes |'
|
printf '%s\n' '| Domain | Status | Notes |'
|
||||||
printf '%s\n' '|---|---|---|'
|
printf '%s\n' '|---|---|---|'
|
||||||
printf '%s\n' '| Access control | OK | Admin-only execution gate |'
|
printf '%s\n' '| Access control | OK | Admin-only execution gate |'
|
||||||
printf '%s\n' '| Release variables | OK | Repository variables validation |'
|
printf '%s\n' '| Release policy | N/A | Releases handled by MokoGitea |'
|
||||||
printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |'
|
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 required artifacts | OK | Required, optional, disallowed enforcement |'
|
||||||
printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |'
|
printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |'
|
||||||
@@ -764,3 +630,83 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}"
|
printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
|
||||||
|
site-health:
|
||||||
|
name: Site Health
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'workflow_dispatch'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.3'
|
||||||
|
|
||||||
|
- name: Uptime check
|
||||||
|
if: env.URLS != ''
|
||||||
|
run: |
|
||||||
|
echo "$URLS" > /tmp/urls.txt
|
||||||
|
php monitoring/uptime-probe.php --urls /tmp/urls.txt --timeout 15 || echo "::warning::Some sites are down"
|
||||||
|
rm -f /tmp/urls.txt
|
||||||
|
env:
|
||||||
|
URLS: ${{ vars.MONITORED_URLS }}
|
||||||
|
|
||||||
|
- name: SSL certificate check
|
||||||
|
if: env.DOMAINS != ''
|
||||||
|
run: |
|
||||||
|
echo "$DOMAINS" > /tmp/domains.txt
|
||||||
|
php monitoring/ssl-check.php --domains /tmp/domains.txt --warn-days 30 || echo "::warning::SSL certificates expiring soon"
|
||||||
|
rm -f /tmp/domains.txt
|
||||||
|
env:
|
||||||
|
DOMAINS: ${{ vars.MONITORED_DOMAINS }}
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo "### Site Health" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Uptime and SSL checks completed." >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════
|
||||||
|
# Issue Reporter — file issues for failed gates
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════
|
||||||
|
report-issues:
|
||||||
|
name: "Report Issues"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [access_check, scripts_governance, repo_health]
|
||||||
|
if: >-
|
||||||
|
always() &&
|
||||||
|
(needs.scripts_governance.result == 'failure' ||
|
||||||
|
needs.repo_health.result == 'failure')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
sparse-checkout: automation/ci-issue-reporter.sh
|
||||||
|
sparse-checkout-cone-mode: false
|
||||||
|
|
||||||
|
- name: "File issues for failed gates"
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
run: |
|
||||||
|
chmod +x automation/ci-issue-reporter.sh
|
||||||
|
REPORTER="./automation/ci-issue-reporter.sh"
|
||||||
|
WF="Repo Health"
|
||||||
|
|
||||||
|
report_gate() {
|
||||||
|
local gate="$1" result="$2" details="$3"
|
||||||
|
if [ "$result" = "failure" ]; then
|
||||||
|
"$REPORTER" --gate "$gate" --details "$details" --workflow "$WF" --severity error
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
report_gate "Scripts Governance" \
|
||||||
|
"${{ needs.scripts_governance.result }}" \
|
||||||
|
"Scripts directory policy violations detected. Review required and allowed directories."
|
||||||
|
|
||||||
|
report_gate "Repository Health" \
|
||||||
|
"${{ needs.repo_health.result }}" \
|
||||||
|
"Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary."
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: MokoStandards.Security
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
|
||||||
# PATH: /.mokogitea/workflows/security-audit.yml
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Dependency vulnerability scanning for composer and npm packages
|
|
||||||
|
|
||||||
name: "Universal: Security Audit"
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 6 * * 1' # Weekly on Monday at 06:00 UTC
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- 'composer.json'
|
|
||||||
- 'composer.lock'
|
|
||||||
- 'package.json'
|
|
||||||
- 'package-lock.json'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
env:
|
|
||||||
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
|
||||||
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
audit:
|
|
||||||
name: Dependency Audit
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Composer audit
|
|
||||||
if: hashFiles('composer.lock') != ''
|
|
||||||
run: |
|
|
||||||
echo "=== Composer Security Audit ==="
|
|
||||||
if ! command -v composer &> /dev/null; then
|
|
||||||
sudo apt-get update -qq
|
|
||||||
sudo apt-get install -y -qq php-cli composer >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
composer audit --format=plain 2>&1 | tee /tmp/composer-audit.txt
|
|
||||||
RESULT=$?
|
|
||||||
if [ $RESULT -ne 0 ]; then
|
|
||||||
echo "::warning::Composer vulnerabilities found"
|
|
||||||
echo "composer_vulnerable=true" >> "$GITHUB_ENV"
|
|
||||||
else
|
|
||||||
echo "No known vulnerabilities in composer dependencies"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: NPM audit
|
|
||||||
if: hashFiles('package-lock.json') != ''
|
|
||||||
run: |
|
|
||||||
echo "=== NPM Security Audit ==="
|
|
||||||
npm audit --production 2>&1 | tee /tmp/npm-audit.txt || true
|
|
||||||
if npm audit --production 2>&1 | grep -q "found 0 vulnerabilities"; then
|
|
||||||
echo "No known vulnerabilities in npm dependencies"
|
|
||||||
else
|
|
||||||
echo "::warning::NPM vulnerabilities found"
|
|
||||||
echo "npm_vulnerable=true" >> "$GITHUB_ENV"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Notify on vulnerabilities
|
|
||||||
if: env.composer_vulnerable == 'true' || env.npm_vulnerable == 'true'
|
|
||||||
run: |
|
|
||||||
REPO="${{ github.event.repository.name }}"
|
|
||||||
curl -sS \
|
|
||||||
-H "Title: ${REPO} has vulnerable dependencies" \
|
|
||||||
-H "Tags: lock,warning" \
|
|
||||||
-H "Priority: high" \
|
|
||||||
-d "Security audit found vulnerabilities. Review dependency updates." \
|
|
||||||
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
|
||||||
@@ -44,7 +44,7 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GH_TOKEN || github.token }}
|
token: ${{ secrets.GH_MIRROR_TOKEN || github.token }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up PHP
|
- name: Set up PHP
|
||||||
@@ -55,8 +55,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup MokoStandards tools
|
- name: Setup MokoStandards tools
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
GH_TOKEN: ${{ secrets.GH_MIRROR_TOKEN || github.token }}
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}'
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN || github.token }}"}}'
|
||||||
run: |
|
run: |
|
||||||
git clone --depth 1 --branch version/04 --quiet \
|
git clone --depth 1 --branch version/04 --quiet \
|
||||||
"https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \
|
"https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \
|
||||||
@@ -106,7 +106,7 @@ jobs:
|
|||||||
--create-issue \
|
--create-issue \
|
||||||
--repo "${{ github.repository }}"
|
--repo "${{ github.repository }}"
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
GH_TOKEN: ${{ secrets.GH_MIRROR_TOKEN || github.token }}
|
||||||
|
|
||||||
- name: Commit updated files
|
- name: Commit updated files
|
||||||
if: ${{ steps.readme_version.outputs.skip != 'true' && inputs.dry_run != true }}
|
if: ${{ steps.readme_version.outputs.skip != 'true' && inputs.dry_run != true }}
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow.Template
|
||||||
|
# INGROUP: MokoStandards.CI
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
|
||||||
|
# PATH: /.mokogitea/workflows/version-set.yml
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: Set or reset the extension version across all version-bearing files
|
||||||
|
|
||||||
|
name: "Joomla: Set Version"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: "Version number (e.g. 01.00.00)"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
branch:
|
||||||
|
description: "Branch to update (default: current)"
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
set-version:
|
||||||
|
name: Set Version to ${{ inputs.version }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Validate version format
|
||||||
|
run: |
|
||||||
|
VERSION="${{ inputs.version }}"
|
||||||
|
if ! echo "$VERSION" | grep -qP '^\d{2}\.\d{2}\.\d{2}$'; then
|
||||||
|
echo "::error::Invalid version format '${VERSION}' — expected XX.YY.ZZ (e.g. 01.00.00)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "VERSION=${VERSION}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.MOKOGITEA_TOKEN || secrets.GA_TOKEN || github.token }}
|
||||||
|
ref: ${{ inputs.branch || github.ref }}
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Update manifest version
|
||||||
|
run: |
|
||||||
|
MANIFEST=""
|
||||||
|
for XML_FILE in $(find . -maxdepth 3 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
|
||||||
|
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
|
||||||
|
MANIFEST="$XML_FILE"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$MANIFEST" ]; then
|
||||||
|
echo "::warning::No Joomla extension manifest found — skipping manifest update"
|
||||||
|
else
|
||||||
|
OLD_VER=$(grep -oP '<version>\K[^<]+' "$MANIFEST" | head -1)
|
||||||
|
sed -i "s|<version>${OLD_VER}</version>|<version>${VERSION}</version>|" "$MANIFEST"
|
||||||
|
echo "Manifest: ${OLD_VER} → ${VERSION} (${MANIFEST})"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Update README.md version
|
||||||
|
run: |
|
||||||
|
if [ -f "README.md" ]; then
|
||||||
|
if grep -qP '^\s*VERSION:\s*\d' README.md; then
|
||||||
|
sed -i -E "s/(VERSION:\s*)[0-9]{2}\.[0-9]{2}\.[0-9]{2}/\1${VERSION}/" README.md
|
||||||
|
echo "README.md version updated to ${VERSION}"
|
||||||
|
else
|
||||||
|
echo "::warning::No VERSION line found in README.md — skipping"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Update CHANGELOG.md
|
||||||
|
run: |
|
||||||
|
if [ -f "CHANGELOG.md" ]; then
|
||||||
|
DATE=$(date +%Y-%m-%d)
|
||||||
|
# Check if this version already has an entry
|
||||||
|
if grep -q "^\#\# \[${VERSION}\]" CHANGELOG.md; then
|
||||||
|
echo "CHANGELOG.md already has entry for ${VERSION} — skipping"
|
||||||
|
else
|
||||||
|
# Insert new version entry after [Unreleased] or at the top after header
|
||||||
|
if grep -q '^\#\# \[Unreleased\]' CHANGELOG.md; then
|
||||||
|
sed -i "/^\#\# \[Unreleased\]/a\\\\n## [${VERSION}] --- ${DATE}" CHANGELOG.md
|
||||||
|
else
|
||||||
|
sed -i "/^\# Changelog/a\\\\n## [Unreleased]\n\n## [${VERSION}] --- ${DATE}" CHANGELOG.md
|
||||||
|
fi
|
||||||
|
echo "CHANGELOG.md: added entry for ${VERSION}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "::warning::No CHANGELOG.md found — skipping"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Update FILE INFORMATION blocks
|
||||||
|
run: |
|
||||||
|
# Update VERSION in file header blocks (# VERSION: XX.YY.ZZ)
|
||||||
|
find . -maxdepth 1 -type f \( -name "*.yml" -o -name "*.yaml" -o -name "*.php" -o -name "*.md" \) \
|
||||||
|
-not -path "./.git/*" -not -path "./vendor/*" -print0 2>/dev/null | \
|
||||||
|
while IFS= read -r -d '' FILE; do
|
||||||
|
if head -20 "$FILE" | grep -qP '^\s*#?\s*VERSION:\s*\d{2}\.\d{2}\.\d{2}'; then
|
||||||
|
sed -i -E "s/(#?\s*VERSION:\s*)[0-9]{2}\.[0-9]{2}\.[0-9]{2}/\1${VERSION}/" "$FILE"
|
||||||
|
echo "Updated FILE INFORMATION VERSION in ${FILE}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Commit and push
|
||||||
|
run: |
|
||||||
|
git config user.name "Moko Consulting [bot]"
|
||||||
|
git config user.email "hello@mokoconsulting.tech"
|
||||||
|
git add -A
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "No version changes detected — nothing to commit"
|
||||||
|
else
|
||||||
|
git commit -m "chore: set version to ${VERSION} [skip bump]
|
||||||
|
|
||||||
|
Authored-by: Moko Consulting"
|
||||||
|
git push
|
||||||
|
echo "### Version Set" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Version updated to \`${VERSION}\` on branch \`${GITHUB_REF_NAME}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: mokocli.Universal
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
|
# PATH: /.mokogitea/workflows/workflow-sync-trigger.yml
|
||||||
|
# VERSION: 01.01.00
|
||||||
|
# BRIEF: Trigger workflow sync to live repos when a PR is merged to main
|
||||||
|
|
||||||
|
name: "Universal: Workflow Sync Trigger"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync:
|
||||||
|
name: Sync workflows to live repos
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >-
|
||||||
|
github.event.pull_request.merged == true &&
|
||||||
|
!contains(github.event.pull_request.title, '[skip sync]')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Determine platform from repo name
|
||||||
|
id: platform
|
||||||
|
run: |
|
||||||
|
REPO="${{ github.event.repository.name }}"
|
||||||
|
case "$REPO" in
|
||||||
|
Template-Joomla) PLATFORM="joomla" ;;
|
||||||
|
Template-Dolibarr) PLATFORM="dolibarr" ;;
|
||||||
|
Template-Go) PLATFORM="go" ;;
|
||||||
|
Template-MCP) PLATFORM="mcp" ;;
|
||||||
|
Template-Generic) PLATFORM="" ;;
|
||||||
|
*) PLATFORM="" ;;
|
||||||
|
esac
|
||||||
|
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Platform: ${PLATFORM:-all}"
|
||||||
|
|
||||||
|
- name: Clone mokocli
|
||||||
|
env:
|
||||||
|
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
GITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
|
||||||
|
git clone --depth 1 "${GITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
cd /tmp/mokocli
|
||||||
|
composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
||||||
|
|
||||||
|
- name: Run workflow sync
|
||||||
|
env:
|
||||||
|
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
ARGS="--token ${MOKOGITEA_TOKEN}"
|
||||||
|
ARGS="${ARGS} --org ${{ vars.GITEA_ORG || github.repository_owner }}"
|
||||||
|
ARGS="${ARGS} --phase repos"
|
||||||
|
|
||||||
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
|
if [ -n "$PLATFORM" ]; then
|
||||||
|
ARGS="${ARGS} --platform-filter ${PLATFORM}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
php /tmp/mokocli/cli/workflow_sync.php ${ARGS}
|
||||||
+161
@@ -0,0 +1,161 @@
|
|||||||
|
# Contributing to Moko Consulting Projects
|
||||||
|
|
||||||
|
Thank you for your interest in contributing. All Moko Consulting repositories follow this universal workflow and version policy.
|
||||||
|
|
||||||
|
## Branching Workflow
|
||||||
|
|
||||||
|
```
|
||||||
|
feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step by step
|
||||||
|
|
||||||
|
1. **Create a feature branch** from `dev`:
|
||||||
|
```bash
|
||||||
|
git checkout dev && git pull
|
||||||
|
git checkout -b feature/my-change
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Work and commit** on your feature branch. Push to origin.
|
||||||
|
|
||||||
|
3. **Open a PR**: `feature/my-change` → `dev`. After review and checks, merge it.
|
||||||
|
|
||||||
|
4. **When ready for release**, open a **draft PR**: `dev` → `main`.
|
||||||
|
- This automatically renames the source branch to `rc` (release candidate)
|
||||||
|
- An RC pre-release is built and uploaded
|
||||||
|
|
||||||
|
5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage:
|
||||||
|
- Rename `dev` to `alpha` for early testing → alpha pre-release is built
|
||||||
|
- Rename `alpha` to `beta` for feature-complete testing → beta pre-release is built
|
||||||
|
- When the draft PR is created, the branch is renamed to `rc`
|
||||||
|
|
||||||
|
6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`.
|
||||||
|
|
||||||
|
7. **Merging to main** triggers the stable release pipeline:
|
||||||
|
- Minor version bump (e.g., `02.09.xx` → `02.10.00`)
|
||||||
|
- Stability suffix stripped (clean version)
|
||||||
|
- Gitea release created with ZIP/tar.gz packages
|
||||||
|
- `updates.xml` updated (Joomla extensions)
|
||||||
|
- `dev` branch recreated from `main`
|
||||||
|
|
||||||
|
### Branch summary
|
||||||
|
|
||||||
|
| Branch | Purpose | Created by |
|
||||||
|
|--------|---------|-----------|
|
||||||
|
| `feature/*` | New features and fixes | Developer |
|
||||||
|
| `dev` | Integration branch | Auto-recreated after release |
|
||||||
|
| `alpha` | Alpha pre-release testing | Manual rename from `dev` |
|
||||||
|
| `beta` | Beta pre-release testing | Manual rename from `alpha` |
|
||||||
|
| `rc` | Release candidate | Auto-renamed on draft PR to main |
|
||||||
|
| `main` | Stable releases | Protected, merge only |
|
||||||
|
| `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI |
|
||||||
|
|
||||||
|
### Protected branches
|
||||||
|
|
||||||
|
| Branch | Direct push | Merge via |
|
||||||
|
|--------|------------|-----------|
|
||||||
|
| `main` | Blocked (CI bot whitelisted) | PR merge only |
|
||||||
|
| `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* |
|
||||||
|
| `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR |
|
||||||
|
| `alpha` | Blocked (CI bot whitelisted) | Manual rename |
|
||||||
|
| `beta` | Blocked (CI bot whitelisted) | Manual rename |
|
||||||
|
| `feature/*` | Open | N/A (source branch) |
|
||||||
|
|
||||||
|
## Version Policy
|
||||||
|
|
||||||
|
### Format
|
||||||
|
|
||||||
|
All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded:
|
||||||
|
|
||||||
|
- **XX** — Major version (breaking changes)
|
||||||
|
- **YY** — Minor version (new features, bumped on release to main)
|
||||||
|
- **ZZ** — Patch version (auto-incremented on every push to dev/feature branches)
|
||||||
|
|
||||||
|
Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major.
|
||||||
|
|
||||||
|
### Stability suffixes
|
||||||
|
|
||||||
|
Each branch appends a suffix to indicate stability:
|
||||||
|
|
||||||
|
| Branch | Suffix | Example |
|
||||||
|
|--------|--------|---------|
|
||||||
|
| `main` | (none) | `02.09.00` |
|
||||||
|
| `dev` | `-dev` | `02.09.01-dev` |
|
||||||
|
| `feature/*` | `-dev` | `02.09.01-dev` |
|
||||||
|
| `alpha` | `-alpha` | `02.09.01-alpha` |
|
||||||
|
| `beta` | `-beta` | `02.09.01-beta` |
|
||||||
|
| `rc` | `-rc` | `02.09.01-rc` |
|
||||||
|
|
||||||
|
### Auto version bump
|
||||||
|
|
||||||
|
On every push to `dev`, `feature/*`, or `patch/*`:
|
||||||
|
|
||||||
|
1. Patch version incremented
|
||||||
|
2. Stability suffix `-dev` applied
|
||||||
|
3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.)
|
||||||
|
4. Commit created with `[skip ci]` to avoid loops
|
||||||
|
|
||||||
|
### Release version flow
|
||||||
|
|
||||||
|
Version bumps happen at specific release events:
|
||||||
|
|
||||||
|
| Event | Bump | Example |
|
||||||
|
|-------|------|---------|
|
||||||
|
| Feature merged to dev | Patch bump after dev release | `02.09.01-dev` → release → `02.09.02-dev` |
|
||||||
|
| Dev promoted to RC | Minor bump | `02.09.02-dev` → `02.10.00-rc` |
|
||||||
|
| RC merged to main | Minor bump | `02.10.00-rc` → `02.11.00` (stable) |
|
||||||
|
| Dev recreated from main | Patch bump | `02.11.00` → `02.11.01-dev` |
|
||||||
|
|
||||||
|
### Release stream copies
|
||||||
|
|
||||||
|
When a higher-stability release is published, copies are created for all lesser streams with the same base version:
|
||||||
|
|
||||||
|
- **RC `02.10.00-rc`** also creates: `02.10.00-dev`, `02.10.00-alpha`, `02.10.00-beta`
|
||||||
|
- **Stable `02.11.00`** also creates: `02.11.00-dev`, `02.11.00-alpha`, `02.11.00-beta`, `02.11.00-rc`
|
||||||
|
|
||||||
|
This ensures Joomla sites on ANY stability channel see the update (Joomla only shows versions higher than what's installed).
|
||||||
|
|
||||||
|
### Version files
|
||||||
|
|
||||||
|
The version tools update all files containing version stamps:
|
||||||
|
|
||||||
|
- `.mokogitea/manifest.xml` (canonical source)
|
||||||
|
- Joomla XML manifests (`<version>` tag)
|
||||||
|
- `README.md`, `CHANGELOG.md` (`VERSION:` pattern)
|
||||||
|
- `package.json`, `pyproject.toml`
|
||||||
|
- Any text file with a `VERSION: XX.YY.ZZ` label
|
||||||
|
|
||||||
|
Files synced from other repos (with a `# REPO:` header) are not touched.
|
||||||
|
|
||||||
|
## Code Standards
|
||||||
|
|
||||||
|
- **PHP**: PSR-12, tabs for indentation
|
||||||
|
- **Copyright**: all files must include the Moko Consulting copyright header
|
||||||
|
- **License**: SPDX identifier `GPL-3.0-or-later` (or as specified per repo)
|
||||||
|
- **Attribution**: use `Authored-by: Moko Consulting` in commits, not individual names
|
||||||
|
|
||||||
|
## Commit Messages
|
||||||
|
|
||||||
|
Use conventional commit format:
|
||||||
|
|
||||||
|
```
|
||||||
|
type(scope): short description
|
||||||
|
|
||||||
|
Optional body with context.
|
||||||
|
|
||||||
|
Authored-by: Moko Consulting
|
||||||
|
```
|
||||||
|
|
||||||
|
Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `ci`
|
||||||
|
|
||||||
|
Special flags in commit messages:
|
||||||
|
- `[skip ci]` — skip all CI workflows
|
||||||
|
- `[skip bump]` — skip auto version bump only
|
||||||
|
|
||||||
|
## Reporting Issues
|
||||||
|
|
||||||
|
Use the repository's issue tracker with the appropriate template.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Moko Consulting <hello@mokoconsulting.tech>*
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
   
|
   
|
||||||
|
|
||||||
Template repository for creating MokoStandards-compliant Model Context Protocol (MCP) API servers. Provides the scaffolding, configuration patterns, and architecture conventions used by all Moko Consulting MCP servers, enabling AI assistants like Claude to interact with REST APIs through a standardized tool interface.
|
Template repository for creating MokoCLI-compliant Model Context Protocol (MCP) API servers. Provides the scaffolding, configuration patterns, and architecture conventions used by all Moko Consulting MCP servers, enabling AI assistants like Claude to interact with REST APIs through a standardized tool interface.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ Template repository for creating MokoStandards-compliant Model Context Protocol
|
|||||||
- **REST API bridge architecture** -- standardized pattern for bridging any REST API as MCP tools, with typed request/response handling
|
- **REST API bridge architecture** -- standardized pattern for bridging any REST API as MCP tools, with typed request/response handling
|
||||||
- **TypeScript-first** -- full TypeScript setup with `tsconfig.json`, strict mode, and ES module output
|
- **TypeScript-first** -- full TypeScript setup with `tsconfig.json`, strict mode, and ES module output
|
||||||
- **Build tooling** -- Makefile with `build`, `dev`, `clean`, and `lint` targets; npm scripts for compilation and development
|
- **Build tooling** -- Makefile with `build`, `dev`, `clean`, and `lint` targets; npm scripts for compilation and development
|
||||||
- **MokoStandards compliant** -- follows all Moko Consulting governance conventions: file headers, commit messages, `.gitattributes`, `.gitmessage`, PR/issue templates
|
- **MokoCLI compliant** -- follows all Moko Consulting governance conventions: file headers, commit messages, `.gitattributes`, `.gitmessage`, PR/issue templates
|
||||||
- **Example configuration** -- `config.example.json` demonstrates the connection configuration schema
|
- **Example configuration** -- `config.example.json` demonstrates the connection configuration schema
|
||||||
- **Documentation templates** -- wiki pages for API reference, architecture overview, and installation pre-generated
|
- **Documentation templates** -- wiki pages for API reference, architecture overview, and installation pre-generated
|
||||||
|
|
||||||
@@ -77,4 +77,4 @@ This project is licensed under the GNU General Public License v3.0 or later -- s
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*[Moko Consulting](https://mokoconsulting.tech) -- [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
*[Moko Consulting](https://mokoconsulting.tech) -- [MokoCLI](https://git.mokoconsulting.tech/MokoConsulting/mokocli)*
|
||||||
|
|||||||
+3
-3
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "@mokoconsulting/mcp_windows",
|
"name": "@mokoconsulting/mcp-windows",
|
||||||
"version": "3.0.0",
|
"version": "3.0.2",
|
||||||
"description": "MCP server for Windows desktop system operations",
|
"description": "MCP server for Windows desktop system operations",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
"mcp_windows": "dist/index.js"
|
"mcp-windows": "dist/index.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
|
|||||||
@@ -1,123 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
*
|
|
||||||
* This file is part of a Moko Consulting project.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* FILE INFORMATION
|
|
||||||
* DEFGROUP: mcp_windows.Scripts
|
|
||||||
* INGROUP: mcp_windows
|
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mcp_windows
|
|
||||||
* PATH: /scripts/setup.mjs
|
|
||||||
* VERSION: 01.00.00
|
|
||||||
* BRIEF: Interactive setup — prompts for API connection details and writes config
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { createInterface } from 'node:readline/promises';
|
|
||||||
import { readFile, writeFile } from 'node:fs/promises';
|
|
||||||
import { resolve } from 'node:path';
|
|
||||||
import { homedir } from 'node:os';
|
|
||||||
|
|
||||||
// ── Customize these ─────────────────────────────────────────────────────
|
|
||||||
const PROJECT_NAME = 'mcp_windows';
|
|
||||||
const CONFIG_PATH = resolve(homedir(), `.${PROJECT_NAME}.json`);
|
|
||||||
const API_LABEL = 'Windows Desktop'; // e.g. "Dolibarr", "Joomla"
|
|
||||||
const AUTH_FIELD = 'apiKey'; // field name in config
|
|
||||||
const AUTH_PROMPT = 'API key'; // what to ask the user for
|
|
||||||
// ────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
||||||
|
|
||||||
async function prompt(question, defaultValue) {
|
|
||||||
const suffix = defaultValue ? ` [${defaultValue}]` : '';
|
|
||||||
const answer = await rl.question(`${question}${suffix}: `);
|
|
||||||
return answer.trim() || defaultValue || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function promptRequired(question) {
|
|
||||||
let answer = '';
|
|
||||||
while (!answer) {
|
|
||||||
answer = (await rl.question(`${question}: `)).trim();
|
|
||||||
if (!answer) {
|
|
||||||
console.log(' This field is required.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return answer;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
console.log('');
|
|
||||||
console.log(`=== ${PROJECT_NAME} Setup ===`);
|
|
||||||
console.log('');
|
|
||||||
console.log('This will create your configuration file at:');
|
|
||||||
console.log(` ${CONFIG_PATH}`);
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
let existing = null;
|
|
||||||
try {
|
|
||||||
const raw = await readFile(CONFIG_PATH, 'utf-8');
|
|
||||||
existing = JSON.parse(raw);
|
|
||||||
console.log('Existing config found. You can add a new connection or overwrite.');
|
|
||||||
console.log(` Current connections: ${Object.keys(existing.connections).join(', ')}`);
|
|
||||||
console.log('');
|
|
||||||
} catch {
|
|
||||||
// No existing config
|
|
||||||
}
|
|
||||||
|
|
||||||
const connectionName = await prompt('Connection name', 'production');
|
|
||||||
const baseUrl = await promptRequired(`${API_LABEL} URL (e.g. https://api.example.com)`);
|
|
||||||
const authValue = await promptRequired(`${API_LABEL} ${AUTH_PROMPT}`);
|
|
||||||
|
|
||||||
const cleanUrl = baseUrl.replace(/\/+$/, '');
|
|
||||||
|
|
||||||
const insecureAnswer = await prompt('Skip TLS verification for self-signed certs? (y/N)', 'N');
|
|
||||||
const insecure = insecureAnswer.toLowerCase() === 'y';
|
|
||||||
|
|
||||||
const connection = { baseUrl: cleanUrl, [AUTH_FIELD]: authValue };
|
|
||||||
if (insecure) {
|
|
||||||
connection.insecure = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let config;
|
|
||||||
if (existing) {
|
|
||||||
config = existing;
|
|
||||||
config.connections[connectionName] = connection;
|
|
||||||
const setDefault = await prompt(`Set "${connectionName}" as default connection? (y/N)`, 'N');
|
|
||||||
if (setDefault.toLowerCase() === 'y') {
|
|
||||||
config.defaultConnection = connectionName;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
config = {
|
|
||||||
defaultConnection: connectionName,
|
|
||||||
connections: {
|
|
||||||
[connectionName]: connection,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
await writeFile(CONFIG_PATH, JSON.stringify(config, null, '\t') + '\n', 'utf-8');
|
|
||||||
|
|
||||||
console.log('');
|
|
||||||
console.log(`Config written to ${CONFIG_PATH}`);
|
|
||||||
console.log(` Connection "${connectionName}" configured for ${cleanUrl}`);
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
const addAnother = await prompt('Add another connection? (y/N)', 'N');
|
|
||||||
if (addAnother.toLowerCase() === 'y') {
|
|
||||||
rl.close();
|
|
||||||
const { execFileSync } = await import('node:child_process');
|
|
||||||
execFileSync('node', [new URL(import.meta.url).pathname], { stdio: 'inherit' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Setup complete. You can now use the MCP server.');
|
|
||||||
console.log('');
|
|
||||||
rl.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((err) => {
|
|
||||||
console.error(`Setup failed: ${err.message}`);
|
|
||||||
rl.close();
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user