Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e3b2df4aac | |||
| 6cd4a19ed6 | |||
| 4d73c6a939 | |||
| df91ed2aac | |||
| 8639d85fe7 | |||
| e7a79d973e | |||
| 6c5394107e | |||
| 0a158e9ec3 | |||
| 48c354fbb4 | |||
| 41c42b968e | |||
| d64dc7cf45 | |||
| 6010841ee7 | |||
| 3857f1339d | |||
| 328ff92c52 | |||
| af7d6d78a8 | |||
| 01ef500793 | |||
| 1438dc7838 | |||
| a6245ff075 | |||
| a4b7b5276c | |||
| 64b3dbe50b | |||
| 22586b7a06 |
@@ -17,7 +17,7 @@
|
||||
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
||||
# | |
|
||||
# | Platform-specific: |
|
||||
# | joomla: XML manifest, updates.xml, type-prefixed packages |
|
||||
# | joomla: XML manifest, type-prefixed packages |
|
||||
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
||||
# | generic: README-only, no update stream |
|
||||
# | |
|
||||
@@ -71,20 +71,25 @@ jobs:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
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
|
||||
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
||||
echo Using pre-installed /opt/moko-platform
|
||||
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
||||
else
|
||||
echo Falling back to fresh clone
|
||||
if ! command -v composer > /dev/null 2>&1; 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
|
||||
rm -rf /tmp/moko-platform-api
|
||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api
|
||||
composer install --no-dev --no-interaction --quiet
|
||||
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
||||
fi
|
||||
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||
rm -rf /tmp/moko-platform-api
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||
/tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api
|
||||
composer install --no-dev --no-interaction --quiet
|
||||
|
||||
- name: Rename branch to rc
|
||||
run: |
|
||||
php /tmp/moko-platform-api/cli/branch_rename.php \
|
||||
php ${MOKO_CLI}/branch_rename.php \
|
||||
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
||||
@@ -100,16 +105,15 @@ jobs:
|
||||
|
||||
- name: Publish RC release
|
||||
run: |
|
||||
php /tmp/moko-platform-api/cli/release_publish.php \
|
||||
php ${MOKO_CLI}/release_publish.php \
|
||||
--path . --stability rc --bump minor --branch rc \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--skip-update-stream
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Branch renamed to rc, minor bump, RC release built (updates.xml managed by Gitea Pages)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
||||
release:
|
||||
@@ -151,25 +155,60 @@ jobs:
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
||||
run: |
|
||||
# Ensure PHP + Composer are available
|
||||
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
|
||||
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
||||
echo Using pre-installed /opt/moko-platform
|
||||
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
||||
else
|
||||
echo Falling back to fresh clone
|
||||
if ! command -v composer > /dev/null 2>&1; 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
|
||||
rm -rf /tmp/moko-platform-api
|
||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api
|
||||
composer install --no-dev --no-interaction --quiet
|
||||
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
||||
fi
|
||||
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||
rm -rf /tmp/moko-platform-api
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||
/tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api
|
||||
composer install --no-dev --no-interaction --quiet
|
||||
|
||||
|
||||
- name: "Publish stable release"
|
||||
run: |
|
||||
php /tmp/moko-platform-api/cli/release_publish.php \
|
||||
php ${MOKO_CLI}/release_publish.php \
|
||||
--path . --stability stable --bump minor --branch main \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--skip-update-stream
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
- name: Update release notes from CHANGELOG.md
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
|
||||
# Extract [Unreleased] section from changelog
|
||||
if [ -f "CHANGELOG.md" ]; then
|
||||
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
||||
[ -z "$NOTES" ] && NOTES="Stable release"
|
||||
else
|
||||
NOTES="Stable release"
|
||||
fi
|
||||
|
||||
# Update release body via API
|
||||
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
"${API_BASE}/releases/tags/stable" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||
|
||||
if [ -n "$RELEASE_ID" ]; then
|
||||
python3 -c "
|
||||
import json, urllib.request
|
||||
body = open('/dev/stdin').read()
|
||||
payload = json.dumps({'body': body}).encode()
|
||||
req = urllib.request.Request(
|
||||
'${API_BASE}/releases/${RELEASE_ID}',
|
||||
data=payload, method='PATCH',
|
||||
headers={
|
||||
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
urllib.request.urlopen(req)
|
||||
" <<< "$NOTES"
|
||||
echo "Release notes updated from CHANGELOG.md"
|
||||
fi
|
||||
|
||||
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
||||
- name: "Step 9: Mirror release to GitHub"
|
||||
@@ -182,7 +221,7 @@ jobs:
|
||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
php /tmp/moko-platform-api/cli/release_mirror.php \
|
||||
php ${MOKO_CLI}/release_mirror.php \
|
||||
--version "$VERSION" --tag "$RELEASE_TAG" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
||||
@@ -256,7 +295,7 @@ jobs:
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
php /tmp/moko-platform-api/cli/version_reset_dev.php \
|
||||
php ${MOKO_CLI}/version_reset_dev.php \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
||||
--branch dev --path . 2>&1 || true
|
||||
|
||||
|
||||
@@ -1,241 +1,243 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||
# VERSION: 05.01.00
|
||||
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
|
||||
|
||||
name: "Universal: Pre-Release"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches:
|
||||
- dev
|
||||
pull_request_target:
|
||||
types: [synchronize, opened, reopened]
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
stability:
|
||||
description: 'Pre-release channel'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- development
|
||||
- alpha
|
||||
- beta
|
||||
- release-candidate
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
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:
|
||||
build:
|
||||
name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
|
||||
runs-on: release
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') ||
|
||||
(github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main')
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }}
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
run: |
|
||||
# Use pre-installed /opt/moko-platform if available (updated by cron every 6h)
|
||||
if [ -f “/opt/moko-platform/cli/version_bump.php” ] && [ -f “/opt/moko-platform/vendor/autoload.php” ]; then
|
||||
echo “Using pre-installed /opt/moko-platform”
|
||||
echo “MOKO_CLI=/opt/moko-platform/cli” >> “$GITHUB_ENV”
|
||||
else
|
||||
echo “Falling back to fresh clone”
|
||||
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
|
||||
rm -rf /tmp/moko-platform-api
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
“https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git” \
|
||||
/tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
||||
echo “MOKO_CLI=/tmp/moko-platform-api/cli” >> “$GITHUB_ENV”
|
||||
fi
|
||||
|
||||
- name: Detect platform
|
||||
id: platform
|
||||
run: |
|
||||
php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
||||
|
||||
- name: Resolve metadata and bump version
|
||||
id: meta
|
||||
run: |
|
||||
# Auto-detect stability: RC for PRs targeting main, else use input or default to development
|
||||
if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then
|
||||
STABILITY="release-candidate"
|
||||
else
|
||||
STABILITY="${{ inputs.stability || 'development' }}"
|
||||
fi
|
||||
|
||||
case "$STABILITY" in
|
||||
development) SUFFIX="-dev"; TAG="development" ;;
|
||||
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
|
||||
beta) SUFFIX="-beta"; TAG="beta" ;;
|
||||
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
|
||||
esac
|
||||
|
||||
# Bump version via CLI: patch for dev/alpha/beta, minor for RC
|
||||
case "$STABILITY" in
|
||||
release-candidate) BUMP="minor" ;;
|
||||
*) BUMP="patch" ;;
|
||||
esac
|
||||
|
||||
php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true
|
||||
|
||||
# Set stability suffix and verify consistency
|
||||
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 \
|
||||
--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
|
||||
|
||||
# Append suffix for output
|
||||
if [ -n "$SUFFIX" ]; then
|
||||
VERSION="${VERSION}${SUFFIX}"
|
||||
fi
|
||||
|
||||
# Commit version bump
|
||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "gitea-actions[bot]"
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
git add -A
|
||||
git diff --cached --quiet || {
|
||||
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
|
||||
git push origin HEAD 2>&1
|
||||
}
|
||||
|
||||
# Auto-detect element via manifest_element.php
|
||||
php ${MOKO_CLI}/manifest_element.php \
|
||||
--path . --version "$VERSION" --stability "$STABILITY" \
|
||||
--repo "${GITEA_REPO}" --github-output
|
||||
|
||||
# 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 "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
||||
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
||||
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
|
||||
|
||||
- name: Create release
|
||||
id: release
|
||||
run: |
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
php ${MOKO_CLI}/release_create.php \
|
||||
--path . --version "$VERSION" --tag "$TAG" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||
--repo "${GITEA_REPO}" --branch dev --prerelease
|
||||
|
||||
- name: Update release notes from CHANGELOG.md
|
||||
run: |
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
|
||||
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
|
||||
if [ -f "CHANGELOG.md" ]; then
|
||||
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
||||
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
|
||||
else
|
||||
NOTES="Release ${VERSION}"
|
||||
fi
|
||||
|
||||
# Update release body via API
|
||||
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
"${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||
|
||||
if [ -n "$RELEASE_ID" ]; then
|
||||
python3 -c "
|
||||
import json, urllib.request
|
||||
body = open('/dev/stdin').read()
|
||||
payload = json.dumps({'body': body}).encode()
|
||||
req = urllib.request.Request(
|
||||
'${API_BASE}/releases/${RELEASE_ID}',
|
||||
data=payload, method='PATCH',
|
||||
headers={
|
||||
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
urllib.request.urlopen(req)
|
||||
" <<< "$NOTES"
|
||||
echo "Release notes updated from CHANGELOG.md"
|
||||
fi
|
||||
|
||||
- name: Build package and upload
|
||||
id: package
|
||||
run: |
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
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
|
||||
|
||||
# updates.xml is generated dynamically by MokoGitea license server
|
||||
# No need to build, commit, or sync updates.xml from workflows
|
||||
|
||||
- name: "Delete lesser pre-release channels (cascade)"
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
php ${MOKO_CLI}/release_cascade.php \
|
||||
--stability "${{ steps.meta.outputs.stability }}" \
|
||||
--token "${TOKEN}" \
|
||||
--api-base "${API_BASE}"
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
|
||||
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
||||
echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||
# VERSION: 05.01.00
|
||||
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
|
||||
|
||||
name: "Universal: Pre-Release"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches:
|
||||
- dev
|
||||
pull_request_target:
|
||||
types: [synchronize, opened, reopened]
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
stability:
|
||||
description: 'Pre-release channel'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- development
|
||||
- alpha
|
||||
- beta
|
||||
- release-candidate
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
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:
|
||||
build:
|
||||
name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
|
||||
runs-on: release
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') ||
|
||||
(github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main')
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }}
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
run: |
|
||||
# Use pre-installed /opt/moko-platform if available (updated by cron every 6h)
|
||||
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/cli/manifest_element.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
||||
echo Using pre-installed /opt/moko-platform
|
||||
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
||||
else
|
||||
echo Falling back to fresh clone
|
||||
if ! command -v composer > /dev/null 2>&1; 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
|
||||
rm -rf /tmp/moko-platform-api
|
||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
||||
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Detect platform
|
||||
id: platform
|
||||
run: |
|
||||
php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
||||
|
||||
- name: Resolve metadata and bump version
|
||||
id: meta
|
||||
run: |
|
||||
# Auto-detect stability: RC for PRs targeting main, else use input or default to development
|
||||
if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then
|
||||
STABILITY="release-candidate"
|
||||
else
|
||||
STABILITY="${{ inputs.stability || 'development' }}"
|
||||
fi
|
||||
|
||||
case "$STABILITY" in
|
||||
development) SUFFIX="-dev"; TAG="development" ;;
|
||||
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
|
||||
beta) SUFFIX="-beta"; TAG="beta" ;;
|
||||
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
|
||||
esac
|
||||
|
||||
# Bump version via CLI: patch for dev/alpha/beta, minor for RC
|
||||
case "$STABILITY" in
|
||||
release-candidate) BUMP="minor" ;;
|
||||
*) BUMP="patch" ;;
|
||||
esac
|
||||
|
||||
php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true
|
||||
|
||||
# Set stability suffix and verify consistency
|
||||
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 \
|
||||
--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
|
||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "gitea-actions[bot]"
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
git add -A
|
||||
git diff --cached --quiet || {
|
||||
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
|
||||
git push origin HEAD 2>&1
|
||||
}
|
||||
|
||||
# Auto-detect element via manifest_element.php
|
||||
php ${MOKO_CLI}/manifest_element.php \
|
||||
--path . --version "$VERSION" --stability "$STABILITY" \
|
||||
--repo "${GITEA_REPO}" --github-output
|
||||
|
||||
# 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 "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
||||
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
||||
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
|
||||
|
||||
- name: Create release
|
||||
id: release
|
||||
run: |
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
php ${MOKO_CLI}/release_create.php \
|
||||
--path . --version "$VERSION" --tag "$TAG" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||
--repo "${GITEA_REPO}" --branch dev --prerelease
|
||||
|
||||
- name: Update release notes from CHANGELOG.md
|
||||
run: |
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
|
||||
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
|
||||
if [ -f "CHANGELOG.md" ]; then
|
||||
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
||||
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
|
||||
else
|
||||
NOTES="Release ${VERSION}"
|
||||
fi
|
||||
|
||||
# Update release body via API
|
||||
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
"${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||
|
||||
if [ -n "$RELEASE_ID" ]; then
|
||||
python3 -c "
|
||||
import json, urllib.request
|
||||
body = open('/dev/stdin').read()
|
||||
payload = json.dumps({'body': body}).encode()
|
||||
req = urllib.request.Request(
|
||||
'${API_BASE}/releases/${RELEASE_ID}',
|
||||
data=payload, method='PATCH',
|
||||
headers={
|
||||
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
urllib.request.urlopen(req)
|
||||
" <<< "$NOTES"
|
||||
echo "Release notes updated from CHANGELOG.md"
|
||||
fi
|
||||
|
||||
- name: Build package and upload
|
||||
id: package
|
||||
run: |
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
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
|
||||
|
||||
# updates.xml is generated dynamically by MokoGitea license server
|
||||
# No need to build, commit, or sync updates.xml from workflows
|
||||
|
||||
- name: "Delete lesser pre-release channels (cascade)"
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
php ${MOKO_CLI}/release_cascade.php \
|
||||
--stability "${{ steps.meta.outputs.stability }}" \
|
||||
--token "${TOKEN}" \
|
||||
--api-base "${API_BASE}"
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
|
||||
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
||||
echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
@@ -33,12 +33,38 @@ func (IssuePriorityDef) TableName() string {
|
||||
}
|
||||
|
||||
// GetIssuePriorityDefsByOrg returns active priority definitions for an org.
|
||||
// If none exist, seeds the org with default priorities automatically.
|
||||
func GetIssuePriorityDefsByOrg(ctx context.Context, orgID int64) ([]*IssuePriorityDef, error) {
|
||||
defs := make([]*IssuePriorityDef, 0, 10)
|
||||
return defs, db.GetEngine(ctx).
|
||||
if err := db.GetEngine(ctx).
|
||||
Where("org_id = ? AND is_active = ?", orgID, true).
|
||||
OrderBy("sort_order ASC, id ASC").
|
||||
Find(&defs)
|
||||
Find(&defs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(defs) == 0 && orgID > 0 {
|
||||
if err := seedDefaultIssuePriorities(ctx, orgID); err != nil {
|
||||
return defs, nil // non-fatal
|
||||
}
|
||||
return GetIssuePriorityDefsByOrg(ctx, orgID)
|
||||
}
|
||||
return defs, nil
|
||||
}
|
||||
|
||||
// seedDefaultIssuePriorities creates the standard priority presets for an org.
|
||||
func seedDefaultIssuePriorities(ctx context.Context, orgID int64) error {
|
||||
defaults := []*IssuePriorityDef{
|
||||
{OrgID: orgID, Name: "Critical", Color: "#dc2626", Description: "Requires immediate attention", SortOrder: 1, IsActive: true},
|
||||
{OrgID: orgID, Name: "High", Color: "#f97316", Description: "Should be addressed soon", SortOrder: 2, IsActive: true},
|
||||
{OrgID: orgID, Name: "Medium", Color: "#eab308", Description: "Normal priority", SortOrder: 3, IsDefault: true, IsActive: true},
|
||||
{OrgID: orgID, Name: "Low", Color: "#2563eb", Description: "Can wait", SortOrder: 4, IsActive: true},
|
||||
}
|
||||
for _, d := range defaults {
|
||||
if _, err := db.GetEngine(ctx).Insert(d); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAllIssuePriorityDefsByOrg returns all priority definitions (including inactive).
|
||||
|
||||
@@ -37,12 +37,40 @@ func (IssueStatusDef) TableName() string {
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
// GetIssueStatusDefsByOrg returns active status definitions for an org.
|
||||
// If none exist, seeds the org with default statuses automatically.
|
||||
func GetIssueStatusDefsByOrg(ctx context.Context, orgID int64) ([]*IssueStatusDef, error) {
|
||||
defs := make([]*IssueStatusDef, 0, 10)
|
||||
return defs, db.GetEngine(ctx).
|
||||
if err := db.GetEngine(ctx).
|
||||
Where("org_id = ? AND is_active = ?", orgID, true).
|
||||
OrderBy("sort_order ASC, id ASC").
|
||||
Find(&defs)
|
||||
Find(&defs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(defs) == 0 && orgID > 0 {
|
||||
if err := seedDefaultIssueStatuses(ctx, orgID); err != nil {
|
||||
return defs, nil // non-fatal
|
||||
}
|
||||
return GetIssueStatusDefsByOrg(ctx, orgID)
|
||||
}
|
||||
return defs, nil
|
||||
}
|
||||
|
||||
// seedDefaultIssueStatuses creates the standard status presets for an org.
|
||||
func seedDefaultIssueStatuses(ctx context.Context, orgID int64) error {
|
||||
defaults := []*IssueStatusDef{
|
||||
{OrgID: orgID, Name: "In Progress", Color: "#2563eb", Description: "Work is actively being done", SortOrder: 1, IsActive: true},
|
||||
{OrgID: orgID, Name: "Needs Info", Color: "#f59e0b", Description: "Waiting for more information", SortOrder: 2, IsActive: true},
|
||||
{OrgID: orgID, Name: "Blocked", Color: "#dc2626", Description: "Cannot proceed due to dependency", SortOrder: 3, IsActive: true},
|
||||
{OrgID: orgID, Name: "Resolved", Color: "#16a34a", Description: "Fix implemented and verified", ClosesIssue: true, SortOrder: 4, IsActive: true},
|
||||
{OrgID: orgID, Name: "Won't Fix", Color: "#6b7280", Description: "Decided not to address", ClosesIssue: true, SortOrder: 5, IsActive: true},
|
||||
{OrgID: orgID, Name: "Duplicate", Color: "#8b5cf6", Description: "Already tracked elsewhere", ClosesIssue: true, SortOrder: 6, IsActive: true},
|
||||
}
|
||||
for _, d := range defaults {
|
||||
if _, err := db.GetEngine(ctx).Insert(d); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAllIssueStatusDefsByOrg returns all status definitions (including inactive).
|
||||
|
||||
@@ -1992,6 +1992,7 @@
|
||||
"repo.wiki.page_already_exists": "A wiki page with the same name already exists.",
|
||||
"repo.wiki.reserved_page": "The wiki page name \"%s\" is reserved.",
|
||||
"repo.wiki.pages": "Pages",
|
||||
"repo.wiki.folder_empty": "This folder is empty.",
|
||||
"repo.wiki.last_updated": "Last updated %s",
|
||||
"repo.wiki.page_name_desc": "Enter a name for this Wiki page. Some special names are: 'Home', '_Sidebar' and '_Footer'.",
|
||||
"repo.wiki.original_git_entry_tooltip": "View original Git file instead of using friendly link.",
|
||||
|
||||
@@ -184,6 +184,45 @@ func NewComment(ctx *context.Context) {
|
||||
}
|
||||
} // end if: handle close or reopen
|
||||
|
||||
// Handle custom status from the status dropdown (replaces close button for issues with org statuses).
|
||||
if statusIDStr := ctx.Req.FormValue("status_id"); statusIDStr != "" && statusIDStr != "" {
|
||||
if statusIDStr == "reopen" {
|
||||
// Reopen via dropdown
|
||||
if issue.IsClosed {
|
||||
if err := issue_service.ReopenIssue(ctx, issue, ctx.Doer, ""); err != nil {
|
||||
log.Error("ReopenIssue via status dropdown: %v", err)
|
||||
}
|
||||
if err := issues_model.SetIssueStatusID(ctx, issue.ID, 0); err != nil {
|
||||
log.Error("SetIssueStatusID: %v", err)
|
||||
}
|
||||
}
|
||||
} else if statusIDStr == "close" {
|
||||
// Plain close via dropdown
|
||||
if !issue.IsClosed {
|
||||
if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil {
|
||||
log.Error("CloseIssue via status dropdown: %v", err)
|
||||
}
|
||||
}
|
||||
} else if statusID, err := strconv.ParseInt(statusIDStr, 10, 64); err == nil && statusID > 0 {
|
||||
// Custom status selected
|
||||
statusDef, err := issues_model.GetIssueStatusDefByID(ctx, statusID)
|
||||
if err == nil && statusDef.OrgID == ctx.Repo.Repository.OwnerID {
|
||||
if err := issues_model.SetIssueStatusID(ctx, issue.ID, statusID); err != nil {
|
||||
log.Error("SetIssueStatusID: %v", err)
|
||||
}
|
||||
if statusDef.ClosesIssue && !issue.IsClosed {
|
||||
if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil {
|
||||
log.Error("CloseIssue via custom status: %v", err)
|
||||
}
|
||||
} else if !statusDef.ClosesIssue && issue.IsClosed {
|
||||
if err := issue_service.ReopenIssue(ctx, issue, ctx.Doer, ""); err != nil {
|
||||
log.Error("ReopenIssue via custom status: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.JSONRedirect(redirect)
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +77,20 @@ type PageMeta struct {
|
||||
UpdatedUnix timeutil.TimeStamp
|
||||
}
|
||||
|
||||
// WikiTreeNode represents a node in the wiki folder tree for sidebar navigation.
|
||||
type WikiTreeNode struct {
|
||||
Name string
|
||||
SubURL string
|
||||
IsDir bool
|
||||
Children []*WikiTreeNode
|
||||
}
|
||||
|
||||
// WikiBreadcrumb represents a breadcrumb segment.
|
||||
type WikiBreadcrumb struct {
|
||||
Name string
|
||||
SubURL string
|
||||
}
|
||||
|
||||
// findEntryForFile finds the tree entry for a target filepath.
|
||||
func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) {
|
||||
entry, err := commit.GetTreeEntryByPath(target)
|
||||
@@ -232,6 +246,43 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
|
||||
isSideBar := pageName == "_Sidebar"
|
||||
isFooter := pageName == "_Footer"
|
||||
|
||||
// Build breadcrumbs for the current path
|
||||
breadcrumbs := buildWikiBreadcrumbs(pageName)
|
||||
ctx.Data["WikiBreadcrumbs"] = breadcrumbs
|
||||
|
||||
// Build folder tree for sidebar navigation
|
||||
wikiTree := buildWikiTree(commit)
|
||||
ctx.Data["WikiTree"] = wikiTree
|
||||
|
||||
// Check if path is a directory first (before file lookup)
|
||||
dirEntry, _ := commit.GetTreeEntryByPath(string(pageName))
|
||||
if dirEntry != nil && dirEntry.IsDir() {
|
||||
// Path is a directory - try index files or show folder listing
|
||||
var entry *git.TreeEntry
|
||||
foundIndex := false
|
||||
for _, indexName := range []string{"README", "Home", "index"} {
|
||||
indexPath := wiki_service.WebPath(string(pageName) + "/" + indexName)
|
||||
idxEntry, _, idxNoEntry, _ := wikiEntryByName(ctx, commit, indexPath)
|
||||
if !idxNoEntry && idxEntry != nil {
|
||||
pageName = indexPath
|
||||
entry = idxEntry
|
||||
_, displayName = wiki_service.WebPathToUserTitle(pageName)
|
||||
ctx.Data["PageURL"] = wiki_service.WebPathToURLPath(pageName)
|
||||
ctx.Data["Title"] = displayName
|
||||
foundIndex = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundIndex {
|
||||
ctx.Data["IsWikiFolder"] = true
|
||||
ctx.Data["WikiFolderPath"] = string(pageName)
|
||||
folderEntries := listWikiFolderEntries(commit, string(pageName))
|
||||
ctx.Data["WikiFolderEntries"] = folderEntries
|
||||
return wikiGitRepo, nil
|
||||
}
|
||||
_ = entry // will be used below via pageName lookup
|
||||
}
|
||||
|
||||
// lookup filename in wiki - get gitTree entry , real filename
|
||||
entry, pageFilename, noEntry, isRaw := wikiEntryByName(ctx, commit, pageName)
|
||||
if noEntry {
|
||||
@@ -479,6 +530,14 @@ func Wiki(ctx *context.Context) {
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
// Folder listing - no entry but IsWikiFolder flag is set
|
||||
if ctx.Data["IsWikiFolder"] != nil {
|
||||
if wikiGitRepo != nil {
|
||||
defer wikiGitRepo.Close()
|
||||
}
|
||||
ctx.HTML(http.StatusOK, tplWikiView)
|
||||
return
|
||||
}
|
||||
if entry == nil {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.wiki")
|
||||
ctx.HTML(http.StatusOK, tplWikiStart)
|
||||
@@ -752,3 +811,134 @@ func DeleteWikiPagePost(ctx *context.Context) {
|
||||
|
||||
ctx.JSONRedirect(ctx.Repo.RepoLink + "/wiki/")
|
||||
}
|
||||
|
||||
// buildWikiBreadcrumbs creates breadcrumb segments from a wiki path.
|
||||
func buildWikiBreadcrumbs(pageName wiki_service.WebPath) []WikiBreadcrumb {
|
||||
parts := strings.Split(string(pageName), "/")
|
||||
crumbs := make([]WikiBreadcrumb, 0, len(parts))
|
||||
for i, part := range parts {
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
subURL := strings.Join(parts[:i+1], "/")
|
||||
crumbs = append(crumbs, WikiBreadcrumb{
|
||||
Name: part,
|
||||
SubURL: subURL,
|
||||
})
|
||||
}
|
||||
return crumbs
|
||||
}
|
||||
|
||||
// buildWikiTree builds a hierarchical folder tree from the wiki git repo.
|
||||
func buildWikiTree(commit *git.Commit) []*WikiTreeNode {
|
||||
if commit == nil {
|
||||
return nil
|
||||
}
|
||||
entries, err := commit.ListEntries()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
root := make(map[string]*WikiTreeNode)
|
||||
var topLevel []*WikiTreeNode
|
||||
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
if entry.IsDir() {
|
||||
node := &WikiTreeNode{
|
||||
Name: name,
|
||||
SubURL: name,
|
||||
IsDir: true,
|
||||
}
|
||||
// List children of this directory
|
||||
subTree := entry.Tree()
|
||||
if subTree != nil {
|
||||
children, _ := subTree.ListEntries()
|
||||
for _, child := range children {
|
||||
childName := child.Name()
|
||||
if child.IsDir() {
|
||||
node.Children = append(node.Children, &WikiTreeNode{
|
||||
Name: childName,
|
||||
SubURL: name + "/" + childName,
|
||||
IsDir: true,
|
||||
})
|
||||
} else if strings.HasSuffix(childName, ".md") {
|
||||
wpChild, err := wiki_service.GitPathToWebPath(childName)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
_, childDisplay := wiki_service.WebPathToUserTitle(wpChild)
|
||||
if childDisplay == "_Sidebar" || childDisplay == "_Footer" {
|
||||
continue
|
||||
}
|
||||
node.Children = append(node.Children, &WikiTreeNode{
|
||||
Name: childDisplay,
|
||||
SubURL: name + "/" + string(wpChild),
|
||||
IsDir: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
root[name] = node
|
||||
topLevel = append(topLevel, node)
|
||||
} else if strings.HasSuffix(name, ".md") {
|
||||
wpName, err := wiki_service.GitPathToWebPath(name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
_, displayName := wiki_service.WebPathToUserTitle(wpName)
|
||||
if displayName == "_Sidebar" || displayName == "_Footer" {
|
||||
continue
|
||||
}
|
||||
node := &WikiTreeNode{
|
||||
Name: displayName,
|
||||
SubURL: string(wpName),
|
||||
IsDir: false,
|
||||
}
|
||||
topLevel = append(topLevel, node)
|
||||
}
|
||||
}
|
||||
return topLevel
|
||||
}
|
||||
|
||||
// listWikiFolderEntries lists the pages and subfolders in a wiki directory.
|
||||
func listWikiFolderEntries(commit *git.Commit, treePath string) []PageMeta {
|
||||
if commit == nil {
|
||||
return nil
|
||||
}
|
||||
tree, err := commit.SubTree(treePath)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
entries, err := tree.ListEntries()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var pages []PageMeta
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
if entry.IsDir() {
|
||||
pages = append(pages, PageMeta{
|
||||
Name: name + "/",
|
||||
SubURL: treePath + "/" + name,
|
||||
GitEntryName: name,
|
||||
})
|
||||
} else if strings.HasSuffix(name, ".md") {
|
||||
wpName, err := wiki_service.GitPathToWebPath(name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
_, displayName := wiki_service.WebPathToUserTitle(wpName)
|
||||
if displayName == "_Sidebar" || displayName == "_Footer" {
|
||||
continue
|
||||
}
|
||||
pages = append(pages, PageMeta{
|
||||
Name: displayName,
|
||||
SubURL: treePath + "/" + string(wpName),
|
||||
GitEntryName: name,
|
||||
})
|
||||
}
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
+27
-11
@@ -144,21 +144,23 @@ func WebPathToURLPath(s WebPath) string {
|
||||
|
||||
func WebPathFromRequest(s string) WebPath {
|
||||
s = util.PathJoinRelX(s)
|
||||
// The old wiki code's behavior is always using %2F, instead of subdirectory.
|
||||
s = strings.ReplaceAll(s, "/", "%2F")
|
||||
// MokoGitea: support real subdirectories for hierarchical wiki navigation.
|
||||
// Slashes are preserved as path separators, not escaped to %2F.
|
||||
return WebPath(s)
|
||||
}
|
||||
|
||||
var multiHyphenRe = regexp.MustCompile(`-{2,}`)
|
||||
var nonSlugRe = regexp.MustCompile(`[^a-zA-Z0-9+.\-]`)
|
||||
var nonSlugReWithSlash = regexp.MustCompile(`[^a-zA-Z0-9+.\-/]`)
|
||||
|
||||
// sanitizeWikiTitle converts a user-provided title into a clean, URL-friendly slug.
|
||||
// Spaces and special characters become hyphens, consecutive hyphens collapse to one.
|
||||
// Preserves: letters, digits, hyphens, plus signs (+), and dots (.)
|
||||
// Preserves: letters, digits, hyphens, plus signs (+), dots (.), and slashes (/).
|
||||
func sanitizeWikiTitle(title string) string {
|
||||
title = strings.TrimSpace(title)
|
||||
title = strings.ReplaceAll(title, " ", "-")
|
||||
title = nonSlugRe.ReplaceAllString(title, "-")
|
||||
// Preserve slashes as directory separators
|
||||
title = nonSlugReWithSlash.ReplaceAllString(title, "-")
|
||||
title = multiHyphenRe.ReplaceAllString(title, "-")
|
||||
title = strings.NewReplacer("-+-", "-", "+-", "-", "-+", "-").Replace(title) // clean stray plus signs
|
||||
title = strings.Trim(title, "-+.")
|
||||
@@ -166,14 +168,28 @@ func sanitizeWikiTitle(title string) string {
|
||||
}
|
||||
|
||||
func UserTitleToWebPath(base, title string) WebPath {
|
||||
// TODO: no support for subdirectory, because the old wiki code's behavior is always using %2F, instead of subdirectory.
|
||||
// So we do not add the support for writing slashes in title at the moment.
|
||||
title = sanitizeWikiTitle(title)
|
||||
title = util.PathJoinRelX(base, escapeSegToWeb(title, false))
|
||||
if title == "" || title == "." {
|
||||
title = "unnamed"
|
||||
// MokoGitea: support subdirectories - slashes in title create folder structure.
|
||||
// Split on /, sanitize each segment, rejoin.
|
||||
parts := strings.Split(title, "/")
|
||||
sanitized := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
p = sanitizeWikiTitle(p)
|
||||
if p != "" {
|
||||
sanitized = append(sanitized, escapeSegToWeb(p, false))
|
||||
}
|
||||
}
|
||||
return WebPath(title)
|
||||
result := strings.Join(sanitized, "/")
|
||||
if base != "" {
|
||||
result = util.PathJoinRelX(base, result)
|
||||
}
|
||||
if result == "" || result == "." {
|
||||
result = "unnamed"
|
||||
}
|
||||
return WebPath(result)
|
||||
}
|
||||
|
||||
// ToWikiPageMetaData converts meta information to a WikiPageMetaData
|
||||
|
||||
@@ -85,24 +85,53 @@
|
||||
<div class="field footer">
|
||||
<div class="flex-text-block tw-justify-end">
|
||||
{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .DisableStatusChange)}}
|
||||
{{$btnIconColor := ""}}{{$btnIcon := ""}}{{$btnTextNoComment := ""}}{{$btnTextWithComment := ""}}{{$btnValue := ""}}
|
||||
{{if .Issue.IsClosed}}
|
||||
{{$btnValue = "reopen"}}
|
||||
{{$btnIconColor = "tw-text-green"}}
|
||||
{{$btnIcon = Iif .Issue.IsPull "octicon-git-pull-request" "octicon-issue-reopened"}}
|
||||
{{$btnTextNoComment = ctx.Locale.Tr (Iif .Issue.IsPull "repo.pulls.reopen" "repo.issues.reopen_issue")}}
|
||||
{{$btnTextWithComment = ctx.Locale.Tr "repo.issues.reopen_comment_issue"}}{{/* general: Reopen with Comment */}}
|
||||
{{if and .IssueStatusDefs (not .Issue.IsPull)}}
|
||||
{{if and .Issue.IsClosed (not .HasIssuesOrPullsWritePermission)}}
|
||||
<button id="status-button" class="ui button" name="status_id" value="reopen">
|
||||
<span class="status-button-icon tw-text-green">{{svg "octicon-issue-reopened"}}</span>
|
||||
<span class="status-button-text">{{ctx.Locale.Tr "repo.issues.reopen_issue"}}</span>
|
||||
</button>
|
||||
{{else}}
|
||||
<select name="status_id" class="ui compact dropdown" style="min-width:140px;padding:7px 10px;border:1px solid var(--color-secondary);border-radius:4px;background:var(--color-body);">
|
||||
<option value="">-- {{ctx.Locale.Tr "repo.issues.status"}} --</option>
|
||||
{{range .IssueStatusDefs}}
|
||||
{{if not .ClosesIssue}}
|
||||
<option value="{{.ID}}" {{if eq .ID $.Issue.StatusID}}selected{{end}}>{{.Name}}</option>
|
||||
{{end}}
|
||||
{{end}}
|
||||
<option disabled>---</option>
|
||||
{{range .IssueStatusDefs}}
|
||||
{{if .ClosesIssue}}
|
||||
<option value="{{.ID}}" {{if eq .ID $.Issue.StatusID}}selected{{end}}>{{.Name}} (close)</option>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{if not $.Issue.IsClosed}}
|
||||
<option value="close">{{ctx.Locale.Tr "repo.issues.close"}}</option>
|
||||
{{else}}
|
||||
<option value="reopen">{{ctx.Locale.Tr "repo.issues.reopen_issue"}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
{{end}}
|
||||
{{else}}
|
||||
{{$btnValue = "close"}}
|
||||
{{$btnIconColor = "tw-text-red"}}
|
||||
{{$btnIcon = Iif .Issue.IsPull "octicon-git-pull-request-closed" "octicon-issue-closed"}}
|
||||
{{$btnTextNoComment = ctx.Locale.Tr (Iif .Issue.IsPull "repo.pulls.close" "repo.issues.close")}}
|
||||
{{$btnTextWithComment = ctx.Locale.Tr "repo.issues.close_comment_issue"}}{{/* general: Close with Comment */}}
|
||||
{{$btnIconColor := ""}}{{$btnIcon := ""}}{{$btnTextNoComment := ""}}{{$btnTextWithComment := ""}}{{$btnValue := ""}}
|
||||
{{if .Issue.IsClosed}}
|
||||
{{$btnValue = "reopen"}}
|
||||
{{$btnIconColor = "tw-text-green"}}
|
||||
{{$btnIcon = Iif .Issue.IsPull "octicon-git-pull-request" "octicon-issue-reopened"}}
|
||||
{{$btnTextNoComment = ctx.Locale.Tr (Iif .Issue.IsPull "repo.pulls.reopen" "repo.issues.reopen_issue")}}
|
||||
{{$btnTextWithComment = ctx.Locale.Tr "repo.issues.reopen_comment_issue"}}
|
||||
{{else}}
|
||||
{{$btnValue = "close"}}
|
||||
{{$btnIconColor = "tw-text-red"}}
|
||||
{{$btnIcon = Iif .Issue.IsPull "octicon-git-pull-request-closed" "octicon-issue-closed"}}
|
||||
{{$btnTextNoComment = ctx.Locale.Tr (Iif .Issue.IsPull "repo.pulls.close" "repo.issues.close")}}
|
||||
{{$btnTextWithComment = ctx.Locale.Tr "repo.issues.close_comment_issue"}}
|
||||
{{end}}
|
||||
<button id="status-button" class="ui button" data-status="{{$btnTextNoComment}}" data-status-and-comment="{{$btnTextWithComment}}" name="status" value="{{$btnValue}}">
|
||||
<span class="status-button-icon {{$btnIconColor}}">{{svg $btnIcon}}</span>
|
||||
<span class="status-button-text">{{$btnTextNoComment}}</span>
|
||||
</button>
|
||||
{{end}}
|
||||
<button id="status-button" class="ui button" data-status="{{$btnTextNoComment}}" data-status-and-comment="{{$btnTextWithComment}}" name="status" value="{{$btnValue}}">
|
||||
<span class="status-button-icon {{$btnIconColor}}">{{svg $btnIcon}}</span>
|
||||
<span class="status-button-text">{{$btnTextNoComment}}</span>
|
||||
</button>
|
||||
{{end}}
|
||||
<button id="comment-button" class="ui primary button">
|
||||
{{ctx.Locale.Tr "repo.issues.create_comment"}}
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
|
||||
{{template "repo/issue/sidebar/label_list" $.IssuePageMetaData}}
|
||||
|
||||
{{template "repo/issue/sidebar/issue_status" $}}
|
||||
|
||||
{{template "repo/issue/sidebar/issue_priority" $}}
|
||||
|
||||
{{template "repo/issue/sidebar/custom_fields" $}}
|
||||
|
||||
@@ -55,12 +55,51 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{if .WikiBreadcrumbs}}
|
||||
{{if gt (len .WikiBreadcrumbs) 1}}
|
||||
<div class="tw-mb-2">
|
||||
<span class="breadcrumb">
|
||||
<a class="section" href="{{.RepoLink}}/wiki/">{{svg "octicon-book" 14}} Wiki</a>
|
||||
{{range .WikiBreadcrumbs}}
|
||||
<span class="breadcrumb-divider">/</span>
|
||||
<a class="section" href="{{$.RepoLink}}/wiki/{{.SubURL}}">{{.Name}}</a>
|
||||
{{end}}
|
||||
</span>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{if .FormatWarning}}
|
||||
<div class="ui negative message">
|
||||
<p>{{.FormatWarning}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .IsWikiFolder}}
|
||||
<h4 class="ui top attached header">
|
||||
{{svg "octicon-file-directory" 16 "tw-mr-2"}}{{.WikiFolderPath}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
{{if .WikiFolderEntries}}
|
||||
<div class="wiki-folder-listing">
|
||||
{{range .WikiFolderEntries}}
|
||||
<div class="tw-py-1">
|
||||
{{if (StringUtils.HasSuffix .Name "/")}}
|
||||
{{svg "octicon-file-directory" 16 "tw-mr-1"}}
|
||||
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}"><strong>{{.Name}}</strong></a>
|
||||
{{else}}
|
||||
{{svg "octicon-file" 16 "tw-mr-1"}}
|
||||
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}">{{.Name}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<p class="text grey">This folder is empty.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="wiki-content-parts">
|
||||
{{if .WikiSidebarTocHTML}}
|
||||
<div class="render-content markup wiki-content-sidebar wiki-content-toc">
|
||||
@@ -68,11 +107,45 @@
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="render-content markup wiki-content-main {{if or .WikiSidebarTocHTML .WikiSidebarHTML}}with-sidebar{{end}}">
|
||||
<div class="render-content markup wiki-content-main {{if or .WikiSidebarTocHTML .WikiSidebarHTML .WikiTree}}with-sidebar{{end}}">
|
||||
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus}}
|
||||
{{.WikiContentHTML}}
|
||||
</div>
|
||||
|
||||
{{if .WikiTree}}
|
||||
<div class="render-content markup wiki-content-sidebar wiki-content-tree">
|
||||
<strong>{{svg "octicon-list-unordered" 14}} Pages</strong>
|
||||
<ul class="wiki-tree-list">
|
||||
{{range .WikiTree}}
|
||||
<li>
|
||||
{{if .IsDir}}
|
||||
{{svg "octicon-file-directory" 14}}
|
||||
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}"><strong>{{.Name}}</strong></a>
|
||||
{{if .Children}}
|
||||
<ul>
|
||||
{{range .Children}}
|
||||
<li>
|
||||
{{if .IsDir}}
|
||||
{{svg "octicon-file-directory" 14}}
|
||||
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}"><strong>{{.Name}}</strong></a>
|
||||
{{else}}
|
||||
{{svg "octicon-file" 14}}
|
||||
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}" {{if eq $.PageURL .SubURL}}class="active"{{end}}>{{.Name}}</a>
|
||||
{{end}}
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
{{else}}
|
||||
{{svg "octicon-file" 14}}
|
||||
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}" {{if eq $.PageURL .SubURL}}class="active"{{end}}>{{.Name}}</a>
|
||||
{{end}}
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .WikiSidebarHTML}}
|
||||
<div class="render-content markup wiki-content-sidebar">
|
||||
{{if and .CanWriteWiki (not .Repository.IsMirror)}}
|
||||
|
||||
@@ -50,6 +50,33 @@
|
||||
border-left-style: dashed;
|
||||
}
|
||||
|
||||
.repository.wiki .wiki-tree-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0.5em 0 0 0;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.repository.wiki .wiki-tree-list ul {
|
||||
list-style: none;
|
||||
padding: 0 0 0 1.2em;
|
||||
margin: 0;
|
||||
border-left: 1px dashed var(--color-secondary);
|
||||
}
|
||||
|
||||
.repository.wiki .wiki-tree-list li {
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.repository.wiki .wiki-tree-list a.active {
|
||||
font-weight: bold;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.repository.wiki .wiki-folder-listing {
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.repository.wiki .wiki-content-main.with-sidebar,
|
||||
.repository.wiki .wiki-content-sidebar {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# Custom Branding
|
||||
|
||||
## Logo & Favicon
|
||||
|
||||
Located in the container at `/var/lib/gitea/custom/public/assets/img/`:
|
||||
- `logo.svg` — Navbar logo
|
||||
- `logo.png` — Fallback logo
|
||||
- `favicon.png` — Browser tab icon
|
||||
- `favicon.svg` — SVG favicon
|
||||
|
||||
Source: Moko Consulting CRM favicon
|
||||
|
||||
## Landing Page
|
||||
|
||||
- `LANDING_PAGE = organizations` in `app.ini`
|
||||
- Custom JS redirects home/logo clicks to `/explore/organizations`
|
||||
- After login, users see the organizations list
|
||||
|
||||
## Themes
|
||||
|
||||
Custom CSS themes at `/var/lib/gitea/custom/public/assets/css/`:
|
||||
- `theme-moko-dark.css`
|
||||
- `theme-moko-light.css`
|
||||
- `theme-moko-auto.css`
|
||||
|
||||
---
|
||||
|
||||
*Repo: [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||
|
||||
| Revision | Date | Author | Description |
|
||||
|---|---|---|---|
|
||||
| 1.0 | 2026-05-09 | Moko Consulting | Initial version |
|
||||
@@ -0,0 +1,48 @@
|
||||
# Deployment
|
||||
|
||||
## Docker Image
|
||||
|
||||
MokoGitea runs as a custom Docker image built from the `moko/1.25.5-project-api` branch.
|
||||
|
||||
### Build
|
||||
```bash
|
||||
cd /opt/MokoGitea
|
||||
git pull
|
||||
docker build -t mokogitea:1.25.5-project-api -f Dockerfile.rootless .
|
||||
```
|
||||
|
||||
### Deploy
|
||||
The docker-compose at `/opt/gitea/docker-compose.yml` references the image:
|
||||
```yaml
|
||||
services:
|
||||
gitea:
|
||||
image: mokogitea:1.25.5-project-api
|
||||
```
|
||||
|
||||
### Update Process
|
||||
1. Pull latest from `moko/1.25.5-project-api`
|
||||
2. Rebuild Docker image
|
||||
3. `docker compose down gitea && docker compose up -d gitea`
|
||||
|
||||
## Volumes
|
||||
|
||||
| Path | Purpose |
|
||||
|------|---------|
|
||||
| `./gitea/data` | Repository data, LFS, avatars |
|
||||
| `./gitea/conf` | `app.ini` configuration |
|
||||
|
||||
## Custom Files
|
||||
|
||||
Located at `/var/lib/gitea/custom/`:
|
||||
- `templates/custom/header.tmpl` — Branding, logo redirect
|
||||
- `public/assets/img/logo.svg` — Moko logo
|
||||
- `public/assets/img/favicon.png` — Moko favicon
|
||||
- `public/assets/css/theme-moko-*.css` — Custom themes
|
||||
|
||||
---
|
||||
|
||||
*Repo: [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||
|
||||
| Revision | Date | Author | Description |
|
||||
|---|---|---|---|
|
||||
| 1.0 | 2026-05-09 | Moko Consulting | Initial version |
|
||||
+1
-1
@@ -7,7 +7,7 @@ Moko Consulting's custom fork of [Gitea](https://gitea.com), extending the self-
|
||||
| **Language** | Go |
|
||||
| **License** | MIT |
|
||||
| **Upstream** | Gitea 1.26.1 |
|
||||
| **Version** | v1.26.1-moko.06.04.00 |
|
||||
| **Version** | v1.26.1-moko.06.07.03 |
|
||||
| **Platform** | [Gitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) |
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
# Org-Level Branch Protection API
|
||||
|
||||
## Overview
|
||||
|
||||
MokoGitea v1261.0.0 introduces **organization-level branch protection rulesets** that cascade automatically to all repositories within an organization. This eliminates the need to configure identical branch protection rules on each repo individually.
|
||||
|
||||
## How Inheritance Works
|
||||
|
||||
1. **Repo rules take precedence** — If a repo has its own protection rule for a branch pattern (e.g., `main`), the org rule is ignored for that repo.
|
||||
2. **Org rules are the fallback** — If no repo-level rule matches a branch, the system checks org-level rules.
|
||||
3. **Team-based only** — Org rules reference teams, not individual users (use repo-level rules for per-user whitelists).
|
||||
|
||||
## API Endpoints
|
||||
|
||||
All endpoints require authentication (`token`) and org ownership permissions.
|
||||
|
||||
### List Rules
|
||||
|
||||
```
|
||||
GET /api/v1/orgs/{org}/branch_protections
|
||||
```
|
||||
|
||||
### Create Rule
|
||||
|
||||
```
|
||||
POST /api/v1/orgs/{org}/branch_protections
|
||||
```
|
||||
|
||||
**Body:**
|
||||
```json
|
||||
{
|
||||
"rule_name": "main",
|
||||
"enable_push": true,
|
||||
"enable_push_whitelist": true,
|
||||
"push_whitelist_teams": ["developers"],
|
||||
"enable_merge_whitelist": true,
|
||||
"merge_whitelist_teams": ["maintainers"],
|
||||
"required_approvals": 2,
|
||||
"block_on_rejected_reviews": true,
|
||||
"block_on_outdated_branch": true,
|
||||
"dismiss_stale_approvals": true,
|
||||
"require_signed_commits": false
|
||||
}
|
||||
```
|
||||
|
||||
### Get Rule
|
||||
|
||||
```
|
||||
GET /api/v1/orgs/{org}/branch_protections/{name}
|
||||
```
|
||||
|
||||
### Update Rule
|
||||
|
||||
```
|
||||
PATCH /api/v1/orgs/{org}/branch_protections/{name}
|
||||
```
|
||||
|
||||
Only fields included in the request body are updated.
|
||||
|
||||
### Delete Rule
|
||||
|
||||
```
|
||||
DELETE /api/v1/orgs/{org}/branch_protections/{name}
|
||||
```
|
||||
|
||||
## Glob Patterns
|
||||
|
||||
Rule names support glob patterns for matching multiple branches:
|
||||
|
||||
| Pattern | Matches |
|
||||
|---------|---------|
|
||||
| `main` | Exactly `main` |
|
||||
| `dev` | Exactly `dev` |
|
||||
| `rc/*` | `rc/1.0`, `rc/2.0-beta`, etc. |
|
||||
| `beta/*` | `beta/feature-x`, etc. |
|
||||
| `release/**` | `release/v1`, `release/v1/hotfix`, etc. |
|
||||
|
||||
## Example: Protect All Standard Branches
|
||||
|
||||
```bash
|
||||
TOKEN="your-token"
|
||||
ORG="MokoConsulting"
|
||||
API="https://git.mokoconsulting.tech/api/v1"
|
||||
|
||||
for BRANCH in main dev "rc/*" "beta/*" "alpha/*"; do
|
||||
curl -X POST "$API/orgs/$ORG/branch_protections" \
|
||||
-H "Authorization: token $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"rule_name\": \"$BRANCH\",
|
||||
\"enable_push\": true,
|
||||
\"enable_push_whitelist\": true,
|
||||
\"push_whitelist_teams\": [\"developers\"],
|
||||
\"required_approvals\": 1,
|
||||
\"block_on_rejected_reviews\": true,
|
||||
\"block_on_outdated_branch\": true
|
||||
}"
|
||||
done
|
||||
```
|
||||
|
||||
## Configuration: Help & Support URLs
|
||||
|
||||
Also new in v1261.0.0 — configurable help/support links in `app.ini`:
|
||||
|
||||
```ini
|
||||
[DEFAULT]
|
||||
HELP_URL = https://docs.mokoconsulting.tech
|
||||
SUPPORT_URL = https://mokoconsulting.tech/support
|
||||
```
|
||||
|
||||
These replace the hardcoded `docs.gitea.com` links in the navigation bar and are visible in **Site Admin > Configuration**.
|
||||
|
||||
## Version Convention
|
||||
|
||||
MokoGitea uses `1261.xx.xx` versioning where `1261` represents the fork starting point from upstream Gitea. Minor and patch numbers track MokoGitea-specific releases.
|
||||
@@ -0,0 +1,202 @@
|
||||
# Project Board API Reference
|
||||
|
||||
Complete REST API for managing Gitea project boards, columns, and issue cards. This API was added by MokoGitea and is not available in upstream Gitea.
|
||||
|
||||
## Authentication
|
||||
|
||||
All write endpoints require a token with `issue` scope:
|
||||
```
|
||||
Authorization: token YOUR_TOKEN
|
||||
```
|
||||
|
||||
## Projects
|
||||
|
||||
### List Projects
|
||||
```
|
||||
GET /api/v1/repos/{owner}/{repo}/projects
|
||||
```
|
||||
Query parameters:
|
||||
- `state` — `open` (default), `closed`, or `all`
|
||||
- `page` — page number (1-based)
|
||||
- `limit` — results per page
|
||||
|
||||
Response: Array of Project objects
|
||||
|
||||
### Create Project
|
||||
```
|
||||
POST /api/v1/repos/{owner}/{repo}/projects
|
||||
```
|
||||
Body:
|
||||
```json
|
||||
{
|
||||
"title": "Sprint Q2 2026",
|
||||
"description": "Second quarter sprint",
|
||||
"board_type": 1,
|
||||
"card_type": 0
|
||||
}
|
||||
```
|
||||
- `board_type`: 0=none, 1=basic kanban, 2=bug triage
|
||||
- `card_type`: 0=text only, 1=images and text
|
||||
|
||||
### Get Project
|
||||
```
|
||||
GET /api/v1/repos/{owner}/{repo}/projects/{id}
|
||||
```
|
||||
|
||||
### Update Project
|
||||
```
|
||||
PATCH /api/v1/repos/{owner}/{repo}/projects/{id}
|
||||
```
|
||||
Body:
|
||||
```json
|
||||
{
|
||||
"title": "Updated Title",
|
||||
"description": "Updated description"
|
||||
}
|
||||
```
|
||||
|
||||
### Delete Project
|
||||
```
|
||||
DELETE /api/v1/repos/{owner}/{repo}/projects/{id}
|
||||
```
|
||||
|
||||
### Close/Reopen Project
|
||||
```
|
||||
POST /api/v1/repos/{owner}/{repo}/projects/{id}/close
|
||||
POST /api/v1/repos/{owner}/{repo}/projects/{id}/reopen
|
||||
```
|
||||
|
||||
## Columns
|
||||
|
||||
### List Columns
|
||||
```
|
||||
GET /api/v1/repos/{owner}/{repo}/projects/{id}/columns
|
||||
```
|
||||
|
||||
### Create Column
|
||||
```
|
||||
POST /api/v1/repos/{owner}/{repo}/projects/{id}/columns
|
||||
```
|
||||
Body:
|
||||
```json
|
||||
{
|
||||
"title": "Backlog",
|
||||
"color": "#0075ca"
|
||||
}
|
||||
```
|
||||
|
||||
### Delete Column
|
||||
```
|
||||
DELETE /api/v1/repos/{owner}/{repo}/projects/{id}/columns/{columnId}
|
||||
```
|
||||
|
||||
## Issue Cards
|
||||
|
||||
### List Issues in Column
|
||||
```
|
||||
GET /api/v1/repos/{owner}/{repo}/projects/{id}/columns/{columnId}/issues
|
||||
```
|
||||
|
||||
Response: Array of ProjectColumnIssue objects with `issue_id`, `project_id`, `column_id`, `sorting`
|
||||
|
||||
### Add Issue to Column
|
||||
```
|
||||
POST /api/v1/repos/{owner}/{repo}/projects/{id}/columns/{columnId}/issues
|
||||
```
|
||||
Body:
|
||||
```json
|
||||
{
|
||||
"issue_id": 42
|
||||
}
|
||||
```
|
||||
|
||||
### Move Issue Between Columns
|
||||
```
|
||||
PATCH /api/v1/repos/{owner}/{repo}/projects/{id}/issues/{issueId}/move
|
||||
```
|
||||
Body:
|
||||
```json
|
||||
{
|
||||
"column_id": 5,
|
||||
"sorting": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Remove Issue from Project
|
||||
```
|
||||
DELETE /api/v1/repos/{owner}/{repo}/projects/{id}/issues/{issueId}
|
||||
```
|
||||
|
||||
## Data Types
|
||||
|
||||
### Project
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Roadmap",
|
||||
"description": "Development roadmap",
|
||||
"owner_id": 2,
|
||||
"repo_id": 68,
|
||||
"creator_id": 1,
|
||||
"is_closed": false,
|
||||
"created_at": "2026-05-08T00:06:45Z",
|
||||
"updated_at": "2026-05-08T00:06:45Z",
|
||||
"closed_at": null
|
||||
}
|
||||
```
|
||||
|
||||
### ProjectColumn
|
||||
```json
|
||||
{
|
||||
"id": 7,
|
||||
"title": "Backlog",
|
||||
"sorting": 0,
|
||||
"color": "#0075ca",
|
||||
"project_id": 1,
|
||||
"default": false,
|
||||
"created_at": "2026-05-08T00:06:58Z",
|
||||
"updated_at": "2026-05-08T00:06:58Z"
|
||||
}
|
||||
```
|
||||
|
||||
### ProjectColumnIssue
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"issue_id": 42,
|
||||
"project_id": 1,
|
||||
"column_id": 7,
|
||||
"sorting": 0
|
||||
}
|
||||
```
|
||||
|
||||
## MCP Integration
|
||||
|
||||
The `project-mcp` server wraps this API. Key tool: `project_setup_roadmap` creates a full project board with columns and loads all open issues in one call.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Create a project
|
||||
curl -X POST -H "Authorization: token TOKEN" \
|
||||
https://git.mokoconsulting.tech/api/v1/repos/MokoConsulting/MokoCRM/projects \
|
||||
-d '{"title":"Roadmap","board_type":1}'
|
||||
|
||||
# Add columns
|
||||
curl -X POST -H "Authorization: token TOKEN" \
|
||||
https://git.mokoconsulting.tech/api/v1/repos/MokoConsulting/MokoCRM/projects/1/columns \
|
||||
-d '{"title":"Backlog"}'
|
||||
|
||||
# Add an issue
|
||||
curl -X POST -H "Authorization: token TOKEN" \
|
||||
https://git.mokoconsulting.tech/api/v1/repos/MokoConsulting/MokoCRM/projects/1/columns/1/issues \
|
||||
-d '{"issue_id":42}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Repo: [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||
|
||||
| Revision | Date | Author | Description |
|
||||
|---|---|---|---|
|
||||
| 1.0 | 2026-05-09 | Moko Consulting | Initial version |
|
||||
@@ -0,0 +1,42 @@
|
||||
# MokoGitea Roadmap
|
||||
|
||||
## Recently Completed (v1.26.1-moko.06.07)
|
||||
|
||||
- Well-known file tabs (README/LICENSE/CONTRIBUTING/SECURITY/CHANGELOG)
|
||||
- Custom issue statuses with auto close/reopen
|
||||
- Org-level issue priorities (Critical/High/Medium/Low)
|
||||
- Repo manifest settings with REST API
|
||||
- Manifest auto-sync on push to default branch
|
||||
- Status dropdown replaces close button
|
||||
- Auto-seed default statuses and priorities for orgs
|
||||
- MCP server published to npm (@mokoconsulting/mokogitea-mcp)
|
||||
- MCP SSE transport for hosted deployments
|
||||
|
||||
## In Progress
|
||||
|
||||
- Granular role-based permissions for all features (#9)
|
||||
- Built-in secret scanning (#508)
|
||||
- Enterprise Wiki with hierarchical folder navigation (#79)
|
||||
- Wire moko-platform CLI to manifest API (#505)
|
||||
|
||||
## Planned
|
||||
|
||||
- Standard status presets and cross-org migration (#507)
|
||||
- Auto-create default teams on org creation (#513)
|
||||
- Update server reads from repo_manifest (#512)
|
||||
- Payment gateways for license keys (#135)
|
||||
- Independent visibility controls for issues/wiki/projects (#133)
|
||||
|
||||
## Infrastructure
|
||||
|
||||
- MCP SSE endpoint at git.mokoconsulting.tech/mcp
|
||||
- Smithery/Claude Code marketplace listing
|
||||
- Docker image for MCP server
|
||||
- npm auto-publish workflow on release
|
||||
|
||||
---
|
||||
|
||||
| Revision | Date | Author | Description |
|
||||
|---|---|---|---|
|
||||
| 2.0 | 2026-06-06 | Jonathan Miller (@jmiller) | Complete rewrite with current features and priorities |
|
||||
| 1.0 | 2026-05-09 | Jonathan Miller (@jmiller) | Initial version |
|
||||
Reference in New Issue
Block a user