feat: wiki search, metadata deploy fields, workflow cleanup #694
@@ -1,76 +0,0 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
name: "Publish to Composer"
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
- '[0-9]*.[0-9]*.[0-9]*'
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish Package
|
||||
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 PHP
|
||||
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 php-zip php-curl composer >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer install --no-dev --no-interaction --prefer-dist --quiet
|
||||
|
||||
- name: Determine version
|
||||
id: version
|
||||
run: |
|
||||
VERSION=$(php -r "echo json_decode(file_get_contents('composer.json'))->version;")
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "Package version: ${VERSION}"
|
||||
|
||||
# Gitea Composer Registry — auto-publishes from tags
|
||||
# The tag push itself registers the package at:
|
||||
# https://git.mokoconsulting.tech/api/packages/MokoConsulting/composer
|
||||
- name: Verify Gitea registry
|
||||
run: |
|
||||
echo "Gitea Composer registry auto-publishes from tags."
|
||||
echo "Package available at: ${GITEA_URL}/api/packages/MokoConsulting/composer"
|
||||
echo "Install: composer require mokoconsulting/mokocli"
|
||||
|
||||
# Packagist — notify of new version
|
||||
- name: Notify Packagist
|
||||
if: secrets.PACKAGIST_TOKEN != ''
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
echo "Notifying Packagist of version ${VERSION}..."
|
||||
curl -sf -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"repository":{"url":"https://git.mokoconsulting.tech/MokoConsulting/mokocli"}}' \
|
||||
"https://packagist.org/api/update-package?username=mokoconsulting&apiToken=${{ secrets.PACKAGIST_TOKEN }}" \
|
||||
&& echo "Packagist notified" \
|
||||
|| echo "::warning::Packagist notification failed (package may not be registered yet)"
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
echo "## Composer Package Published" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Registry | Status |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|----------|--------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Gitea | \`composer require mokoconsulting/mokocli:${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Packagist | \`composer require mokoconsulting/mokocli\` |" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -1,126 +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.Deploy
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
|
||||
# PATH: /templates/workflows/joomla/deploy-manual.yml.template
|
||||
# VERSION: 04.07.00
|
||||
# BRIEF: Manual SFTP deploy to dev server for Joomla repos
|
||||
|
||||
name: "Universal: Deploy to Dev (Manual)"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
clear_remote:
|
||||
description: 'Delete all remote files before uploading'
|
||||
required: false
|
||||
default: 'false'
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: SFTP Deploy to Dev
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Setup PHP
|
||||
run: |
|
||||
php -v && composer --version
|
||||
|
||||
- name: Setup MokoStandards tools
|
||||
env:
|
||||
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
||||
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
|
||||
run: |
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
||||
/tmp/mokostandards-api 2>/dev/null || true
|
||||
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
|
||||
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
||||
fi
|
||||
|
||||
- name: Check FTP configuration
|
||||
id: check
|
||||
env:
|
||||
HOST: ${{ vars.DEV_FTP_HOST }}
|
||||
PATH_VAR: ${{ vars.DEV_FTP_PATH }}
|
||||
PORT: ${{ vars.DEV_FTP_PORT }}
|
||||
run: |
|
||||
if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then
|
||||
echo "DEV_FTP_HOST or DEV_FTP_PATH not configured -- cannot deploy"
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
echo "host=$HOST" >> "$GITHUB_OUTPUT"
|
||||
|
||||
REMOTE="${PATH_VAR%/}"
|
||||
echo "remote=$REMOTE" >> "$GITHUB_OUTPUT"
|
||||
|
||||
[ -z "$PORT" ] && PORT="22"
|
||||
echo "port=$PORT" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Deploy via SFTP
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
env:
|
||||
SFTP_KEY: ${{ secrets.DEV_FTP_KEY }}
|
||||
SFTP_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
|
||||
SFTP_USER: ${{ vars.DEV_FTP_USERNAME }}
|
||||
run: |
|
||||
SOURCE_DIR="src"
|
||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ -- nothing to deploy"; exit 0; }
|
||||
|
||||
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
|
||||
"${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \
|
||||
> /tmp/sftp-config.json
|
||||
|
||||
if [ -n "$SFTP_KEY" ]; then
|
||||
echo "$SFTP_KEY" > /tmp/deploy_key
|
||||
chmod 600 /tmp/deploy_key
|
||||
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
|
||||
else
|
||||
printf ',"password":"%s"}' "$SFTP_PASS" >> /tmp/sftp-config.json
|
||||
fi
|
||||
|
||||
DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
|
||||
[ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote)
|
||||
|
||||
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
|
||||
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
|
||||
php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
|
||||
else
|
||||
php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
|
||||
fi
|
||||
|
||||
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
if [ "${{ steps.check.outputs.skip }}" = "true" ]; then
|
||||
echo "### Deploy Skipped -- FTP not configured" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Host | \`${{ steps.check.outputs.host }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Remote | \`${{ steps.check.outputs.remote }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Clear | ${{ inputs.clear_remote }} |" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
@@ -1,92 +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-tech/MokoStandards-API
|
||||
# PATH: /templates/workflows/gitleaks.yml.template
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
||||
#
|
||||
# +========================================================================+
|
||||
# | SECRET SCANNING |
|
||||
# +========================================================================+
|
||||
# | |
|
||||
# | Scans commits for leaked secrets using Gitleaks. |
|
||||
# | |
|
||||
# | - PR scan: only new commits in the PR |
|
||||
# | - Scheduled: full repo scan weekly |
|
||||
# | - Alerts via ntfy on findings |
|
||||
# | |
|
||||
# +========================================================================+
|
||||
|
||||
name: "Universal: Secret Scanning"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 5 * * 1' # Weekly Monday 05:00 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
||||
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
|
||||
|
||||
jobs:
|
||||
gitleaks:
|
||||
name: Gitleaks 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
|
||||
gitleaks version
|
||||
|
||||
- name: Scan for secrets
|
||||
id: scan
|
||||
run: |
|
||||
echo "### Secret Scanning" >> $GITHUB_STEP_SUMMARY
|
||||
ARGS="--source . --verbose --report-format json --report-path /tmp/gitleaks-report.json"
|
||||
|
||||
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||
# Scan only PR commits
|
||||
ARGS="$ARGS --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}"
|
||||
echo "Scanning PR commits only" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "Full repository scan" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
if gitleaks detect $ARGS 2>&1; then
|
||||
echo "result=clean" >> "$GITHUB_OUTPUT"
|
||||
echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "result=found" >> "$GITHUB_OUTPUT"
|
||||
FINDINGS=$(jq length /tmp/gitleaks-report.json 2>/dev/null || echo "unknown")
|
||||
echo "**${FINDINGS} potential secret(s) detected.**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Review the findings and rotate any exposed credentials immediately." >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Notify on findings
|
||||
if: failure() && steps.scan.outputs.result == 'found'
|
||||
run: |
|
||||
REPO="${{ github.event.repository.name }}"
|
||||
curl -sS \
|
||||
-H "Title: ${REPO} — secrets detected in code" \
|
||||
-H "Tags: rotating_light,key" \
|
||||
-H "Priority: urgent" \
|
||||
-d "Gitleaks found potential secrets. Review and rotate credentials immediately." \
|
||||
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
||||
@@ -1,70 +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.Notifications
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||
# PATH: /.gitea/workflows/notify.yml
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
||||
|
||||
name: "Universal: Notifications"
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- "Joomla Build & Release"
|
||||
- "Joomla Extension CI"
|
||||
- "Deploy"
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
||||
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-releases' }}
|
||||
|
||||
jobs:
|
||||
notify:
|
||||
name: Send Notification
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.workflow_run.conclusion == 'success' ||
|
||||
github.event.workflow_run.conclusion == 'failure'
|
||||
|
||||
steps:
|
||||
- name: Notify on success (releases only)
|
||||
if: >-
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
contains(github.event.workflow_run.name, 'Release')
|
||||
run: |
|
||||
REPO="${{ github.event.repository.name }}"
|
||||
WORKFLOW="${{ github.event.workflow_run.name }}"
|
||||
URL="${{ github.event.workflow_run.html_url }}"
|
||||
|
||||
curl -sS \
|
||||
-H "Title: ${REPO} released" \
|
||||
-H "Tags: white_check_mark,package" \
|
||||
-H "Priority: default" \
|
||||
-H "Click: ${URL}" \
|
||||
-d "${WORKFLOW} completed successfully." \
|
||||
"${NTFY_URL}/${NTFY_TOPIC}"
|
||||
|
||||
- name: Notify on failure
|
||||
if: github.event.workflow_run.conclusion == 'failure'
|
||||
run: |
|
||||
REPO="${{ github.event.repository.name }}"
|
||||
WORKFLOW="${{ github.event.workflow_run.name }}"
|
||||
URL="${{ github.event.workflow_run.html_url }}"
|
||||
|
||||
curl -sS \
|
||||
-H "Title: ${REPO} workflow failed" \
|
||||
-H "Tags: x,warning" \
|
||||
-H "Priority: high" \
|
||||
-H "Click: ${URL}" \
|
||||
-d "${WORKFLOW} failed. Check the run for details." \
|
||||
"${NTFY_URL}/${NTFY_TOPIC}"
|
||||
@@ -1,51 +0,0 @@
|
||||
name: Publish MCP to npm
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- '.mokogitea/mcp/**'
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install and build
|
||||
working-directory: .mokogitea/mcp
|
||||
run: |
|
||||
npm ci
|
||||
npx tsc
|
||||
|
||||
- name: Check version change
|
||||
id: version
|
||||
working-directory: .mokogitea/mcp
|
||||
run: |
|
||||
LOCAL_VERSION=$(node -p "require('./package.json').version")
|
||||
NPM_VERSION=$(npm view @mokoconsulting/mokogitea-mcp version 2>/dev/null || echo "0.0.0")
|
||||
if [ "$LOCAL_VERSION" != "$NPM_VERSION" ]; then
|
||||
echo "changed=true" >> $GITHUB_OUTPUT
|
||||
echo "Version changed: $NPM_VERSION -> $LOCAL_VERSION"
|
||||
else
|
||||
echo "changed=false" >> $GITHUB_OUTPUT
|
||||
echo "Version unchanged: $LOCAL_VERSION"
|
||||
fi
|
||||
|
||||
- name: Publish to npm
|
||||
if: steps.version.outputs.changed == 'true'
|
||||
working-directory: .mokogitea/mcp
|
||||
run: npm publish --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish to Gitea registry
|
||||
if: steps.version.outputs.changed == 'true'
|
||||
working-directory: .mokogitea/mcp
|
||||
run: |
|
||||
npm publish --registry ${{ github.server_url }}/api/packages/${{ github.repository_owner }}/npm/ \
|
||||
--//$(echo "${{ github.server_url }}" | sed 's|https://||')/api/packages/${{ github.repository_owner }}/npm/:_authToken=${{ secrets.MOKOGITEA_TOKEN }}
|
||||
@@ -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: /.gitea/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
|
||||
@@ -1,73 +0,0 @@
|
||||
# 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}
|
||||
@@ -3,6 +3,17 @@
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Wiki full-text search: case-insensitive search across all wiki page titles and content (#550)
|
||||
- Wiki search API: GET /wiki/search?q=term with paginated JSON results (#550)
|
||||
- Metadata deploy fields: deploy_host, deploy_port, deploy_user, deploy_path, docker_image, docker_registry, container_name, health_url (#692)
|
||||
- Metadata API partial updates: PUT /metadata now merges only sent fields instead of replacing all
|
||||
- Wiki revision diff: line-by-line diff view per commit in wiki page history (#667)
|
||||
- Wiki categories: YAML frontmatter `categories:` with category index page (#668)
|
||||
- Wiki template transclusion: `{{template:Name|key=val}}` with `_Template/` folder (#671)
|
||||
- Wiki enhanced ToC: collapsible, inline via frontmatter, sticky sidebar (#673)
|
||||
- Wiki folder ACL: `_access.yml` per-folder write protection (#674)
|
||||
- Wiki print view and ZIP export of all wiki pages (#675)
|
||||
- Wiki features documentation page in org wiki (standards/Wiki-Features)
|
||||
- DLID licensing system: license, entitlement, activation, product_tier, audit_log tables (v359 migration)
|
||||
- License CRUD with CRC32-checksummed DLID generation and format validation
|
||||
- Entitlement model with tier-based rebuild and custom entitlement preservation
|
||||
@@ -20,6 +31,8 @@
|
||||
- Wiki page rename with automatic redirects via YAML frontmatter (#672)
|
||||
|
||||
### Fixed
|
||||
- Licensing API: handle DB write errors in UpdateLicense, UpdateTier, DeleteTier instead of silently discarding
|
||||
- Wiki API: fix findEntryForFile URL-decode fallback for non-ASCII page names
|
||||
- Metadata settings template 500 error: removed reference to deleted Version field
|
||||
- Wiki recent changes: use commit.MessageTitle() instead of commit.Message()
|
||||
- Wiki backlinks: proper URL encoding for subdirectory pages
|
||||
@@ -30,6 +43,9 @@
|
||||
- Issue status seed defaults: Open, In Progress, Waiting, In Review, Closed, Won't Fix
|
||||
- Pre-release workflow: auto-bump skipped for non-Joomla repos (platform check)
|
||||
|
||||
### Removed
|
||||
- Workflows: gitleaks.yml, npm-publish.yml, notify.yml, workflow-sync-trigger.yml, composer-publish.yml, deploy-manual.yml, security-audit.yml (not applicable to Go repo)
|
||||
|
||||
## [06.19.00] --- 2026-06-20
|
||||
|
||||
## [06.19.00] --- 2026-06-20
|
||||
|
||||
@@ -1,38 +1,29 @@
|
||||
# MokoGitea
|
||||
|
||||
Moko fork of Gitea — adding project board REST API endpoints and custom enhancements
|
||||
Custom Gitea fork with enhanced wiki system, DLID licensing, issue statuses, org metadata, and project board API.
|
||||
|
||||
  
|
||||
|
||||
|
||||
Custom Gitea fork with Project Board API
|
||||
 
|
||||
|
||||
---
|
||||
|
||||
## Pages
|
||||
## Key Features
|
||||
|
||||
- [Branding](https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/wiki/Branding)
|
||||
- [Deployment](https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/wiki/Deployment)
|
||||
- [Project API](Project API)
|
||||
- [roadmap](https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/wiki/roadmap)
|
||||
|
||||
---
|
||||
|
||||
**Category:** Infrastructure | **Platform:** [MokoPlatform wiki](https://code.mokoconsulting.tech/MokoConsulting/MokoPlatform/wiki)
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
---
|
||||
- **Wiki System** -- wikilinks, categories, backlinks, template transclusion, revision diffs, rename redirects, folder ACL, enhanced ToC, print view, ZIP export ([details](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki/standards/Wiki-Features))
|
||||
- **DLID Licensing** -- license management, entitlements, domain activations, ed25519-signed downloads
|
||||
- **Issue Statuses** -- custom workflow statuses per org with required baseline protection
|
||||
- **Org Metadata** -- per-repo metadata API (public GET, admin PUT), platform detection for versioning
|
||||
- **Project Board API** -- REST endpoints for project columns and cards
|
||||
- **Dev Deploy Gate** -- builds deploy to dev environment first, production checks dev health
|
||||
|
||||
## Documentation
|
||||
|
||||
Full documentation is available on the [Wiki](https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/wiki).
|
||||
- [Org Wiki](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki/) -- standards, CLI reference, API docs
|
||||
- [Wiki Features](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki/standards/Wiki-Features) -- all 10 wiki enhancements
|
||||
- [Licensing API](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki/api/Licensing-API)
|
||||
|
||||
## Contributing
|
||||
|
||||
See the wiki for development guidelines and contribution instructions.
|
||||
See the [org wiki](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki/) for development guidelines, coding standards, and contribution instructions.
|
||||
|
||||
## License
|
||||
|
||||
@@ -40,4 +31,4 @@ This project is licensed under the GNU General Public License v3.0 or later -- s
|
||||
|
||||
---
|
||||
|
||||
*[Moko Consulting](https://mokoconsulting.tech) -- [MokoStandards](https://code.mokoconsulting.tech/MokoConsulting/MokoPlatform/wiki/Home)*
|
||||
*[Moko Consulting](https://mokoconsulting.tech)*
|
||||
|
||||
@@ -436,6 +436,7 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(356, "Rename package_type to extension_type in repo manifest", v1_27.RenamePackageTypeToExtensionType),
|
||||
newMigration(357, "Drop display_name from repo manifest and update stream config", v1_27.DropDisplayNameColumns),
|
||||
newMigration(358, "Add licensing tables (license, entitlement, activation, product_tier)", v1_27.AddLicensingTables),
|
||||
newMigration(359, "Add deploy fields to repo manifest", v1_27.AddDeployFieldsToRepoManifest),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package v1_27
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// AddDeployFieldsToRepoManifest adds deploy configuration columns to repo_manifest.
|
||||
func AddDeployFieldsToRepoManifest(x *xorm.Engine) error {
|
||||
type RepoManifest struct {
|
||||
DeployHost string `xorm:"VARCHAR(255) 'deploy_host'"`
|
||||
DeployPort string `xorm:"VARCHAR(10) 'deploy_port'"`
|
||||
DeployUser string `xorm:"VARCHAR(100) 'deploy_user'"`
|
||||
DeployPath string `xorm:"TEXT 'deploy_path'"`
|
||||
DockerImage string `xorm:"VARCHAR(255) 'docker_image'"`
|
||||
DockerRegistry string `xorm:"VARCHAR(255) 'docker_registry'"`
|
||||
ContainerName string `xorm:"VARCHAR(100) 'container_name'"`
|
||||
HealthURL string `xorm:"TEXT 'health_url'"`
|
||||
}
|
||||
return x.Sync(new(RepoManifest))
|
||||
}
|
||||
@@ -50,6 +50,16 @@ type RepoMetadata struct {
|
||||
ExtensionType string `xorm:"VARCHAR(50) 'extension_type'"` // component, module, plugin, package, template, library, file
|
||||
EntryPoint string `xorm:"TEXT 'entry_point'"` // build entry point path
|
||||
|
||||
// deploy section
|
||||
DeployHost string `xorm:"VARCHAR(255) 'deploy_host'"` // SSH host for deploy
|
||||
DeployPort string `xorm:"VARCHAR(10) 'deploy_port'"` // SSH port (default 2918)
|
||||
DeployUser string `xorm:"VARCHAR(100) 'deploy_user'"` // SSH user
|
||||
DeployPath string `xorm:"TEXT 'deploy_path'"` // remote path for source/compose
|
||||
DockerImage string `xorm:"VARCHAR(255) 'docker_image'"` // e.g. mokoconsulting/mokogitea
|
||||
DockerRegistry string `xorm:"VARCHAR(255) 'docker_registry'"` // e.g. git.mokoconsulting.tech
|
||||
ContainerName string `xorm:"VARCHAR(100) 'container_name'"` // Docker container name
|
||||
HealthURL string `xorm:"TEXT 'health_url'"` // health check URL after deploy
|
||||
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"`
|
||||
}
|
||||
|
||||
@@ -1314,6 +1314,7 @@ func Routes() *web.Router {
|
||||
m.Get("/revisions/*", repo.ListPageRevisions)
|
||||
m.Post("/new", reqToken(), mustNotBeArchived, reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.NewWikiPage)
|
||||
m.Get("/pages", repo.ListWikiPages)
|
||||
m.Get("/search", repo.SearchWikiPages)
|
||||
}, mustEnableWiki)
|
||||
m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup)
|
||||
m.Post("/markdown", reqToken(), bind(api.MarkdownOption{}), misc.Markdown)
|
||||
|
||||
@@ -207,7 +207,10 @@ func UpdateLicense(ctx *context.APIContext) {
|
||||
}
|
||||
if len(cols) > 0 {
|
||||
cols = append(cols, "updated_at")
|
||||
db.GetEngine(ctx).ID(id).Cols(cols...).Update(license)
|
||||
if _, err := db.GetEngine(ctx).ID(id).Cols(cols...).Update(license); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, licenseToJSON(ctx, license))
|
||||
@@ -399,7 +402,10 @@ func UpdateTier(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
if len(cols) > 0 {
|
||||
db.GetEngine(ctx).ID(id).Cols(cols...).Update(tier)
|
||||
if _, err := db.GetEngine(ctx).ID(id).Cols(cols...).Update(tier); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, tierToJSON(tier))
|
||||
@@ -427,7 +433,10 @@ func DeleteTier(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
db.GetEngine(ctx).ID(id).Delete(new(licensing_model.ProductTier))
|
||||
if _, err := db.GetEngine(ctx).ID(id).Delete(new(licensing_model.ProductTier)); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,16 @@ type apiMetadata struct {
|
||||
Language string `json:"language"`
|
||||
ExtensionType string `json:"extension_type"`
|
||||
EntryPoint string `json:"entry_point"`
|
||||
|
||||
// deploy
|
||||
DeployHost string `json:"deploy_host,omitempty"`
|
||||
DeployPort string `json:"deploy_port,omitempty"`
|
||||
DeployUser string `json:"deploy_user,omitempty"`
|
||||
DeployPath string `json:"deploy_path,omitempty"`
|
||||
DockerImage string `json:"docker_image,omitempty"`
|
||||
DockerRegistry string `json:"docker_registry,omitempty"`
|
||||
ContainerName string `json:"container_name,omitempty"`
|
||||
HealthURL string `json:"health_url,omitempty"`
|
||||
}
|
||||
|
||||
// GetRepoMetadata returns the manifest settings for a repository.
|
||||
@@ -81,6 +91,14 @@ func GetRepoMetadata(ctx *context.APIContext) {
|
||||
Language: m.Language,
|
||||
ExtensionType: m.ExtensionType,
|
||||
EntryPoint: m.EntryPoint,
|
||||
DeployHost: m.DeployHost,
|
||||
DeployPort: m.DeployPort,
|
||||
DeployUser: m.DeployUser,
|
||||
DeployPath: m.DeployPath,
|
||||
DockerImage: m.DockerImage,
|
||||
DockerRegistry: m.DockerRegistry,
|
||||
ContainerName: m.ContainerName,
|
||||
HealthURL: m.HealthURL,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -96,35 +114,59 @@ func UpdateRepoMetadata(ctx *context.APIContext) {
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Manifest"
|
||||
var req apiMetadata
|
||||
if err := json.NewDecoder(ctx.Req.Body).Decode(&req); err != nil {
|
||||
// Decode into a map to detect which fields were actually sent.
|
||||
var raw map[string]any
|
||||
if err := json.NewDecoder(ctx.Req.Body).Decode(&raw); err != nil {
|
||||
ctx.APIError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
m := &repo_model.RepoMetadata{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Name: req.Name,
|
||||
Org: req.Org,
|
||||
Description: req.Description,
|
||||
|
||||
LicenseSPDX: req.LicenseSPDX,
|
||||
LicenseName: req.LicenseName,
|
||||
VersionPrefix: req.VersionPrefix,
|
||||
ElementName: req.ElementName,
|
||||
Platform: req.Platform,
|
||||
StandardsVersion: req.StandardsVersion,
|
||||
StandardsSource: req.StandardsSource,
|
||||
Maintainer: req.Maintainer,
|
||||
MaintainerURL: req.MaintainerURL,
|
||||
InfoURL: req.InfoURL,
|
||||
TargetVersion: req.TargetVersion,
|
||||
PHPMinimum: req.PHPMinimum,
|
||||
Language: req.Language,
|
||||
ExtensionType: req.ExtensionType,
|
||||
EntryPoint: req.EntryPoint,
|
||||
// Load existing metadata (or create defaults).
|
||||
m, _ := repo_model.GetRepoMetadata(ctx, ctx.Repo.Repository.ID)
|
||||
if m == nil {
|
||||
m = &repo_model.RepoMetadata{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Name: ctx.Repo.Repository.Name,
|
||||
Org: ctx.Repo.Repository.OwnerName,
|
||||
Description: ctx.Repo.Repository.Description,
|
||||
}
|
||||
}
|
||||
|
||||
// Apply only the fields present in the request.
|
||||
setStr := func(key string, target *string) {
|
||||
if v, ok := raw[key]; ok {
|
||||
if s, ok := v.(string); ok {
|
||||
*target = s
|
||||
}
|
||||
}
|
||||
}
|
||||
setStr("name", &m.Name)
|
||||
setStr("org", &m.Org)
|
||||
setStr("description", &m.Description)
|
||||
setStr("license_spdx", &m.LicenseSPDX)
|
||||
setStr("license_name", &m.LicenseName)
|
||||
setStr("version_prefix", &m.VersionPrefix)
|
||||
setStr("element_name", &m.ElementName)
|
||||
setStr("platform", &m.Platform)
|
||||
setStr("standards_version", &m.StandardsVersion)
|
||||
setStr("standards_source", &m.StandardsSource)
|
||||
setStr("maintainer", &m.Maintainer)
|
||||
setStr("maintainer_url", &m.MaintainerURL)
|
||||
setStr("info_url", &m.InfoURL)
|
||||
setStr("target_version", &m.TargetVersion)
|
||||
setStr("php_minimum", &m.PHPMinimum)
|
||||
setStr("language", &m.Language)
|
||||
setStr("extension_type", &m.ExtensionType)
|
||||
setStr("entry_point", &m.EntryPoint)
|
||||
setStr("deploy_host", &m.DeployHost)
|
||||
setStr("deploy_port", &m.DeployPort)
|
||||
setStr("deploy_user", &m.DeployUser)
|
||||
setStr("deploy_path", &m.DeployPath)
|
||||
setStr("docker_image", &m.DockerImage)
|
||||
setStr("docker_registry", &m.DockerRegistry)
|
||||
setStr("container_name", &m.ContainerName)
|
||||
setStr("health_url", &m.HealthURL)
|
||||
|
||||
if err := repo_model.CreateOrUpdateRepoMetadata(ctx, m); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
@@ -151,5 +193,13 @@ func UpdateRepoMetadata(ctx *context.APIContext) {
|
||||
Language: m.Language,
|
||||
ExtensionType: m.ExtensionType,
|
||||
EntryPoint: m.EntryPoint,
|
||||
DeployHost: m.DeployHost,
|
||||
DeployPort: m.DeployPort,
|
||||
DeployUser: m.DeployUser,
|
||||
DeployPath: m.DeployPath,
|
||||
DockerImage: m.DockerImage,
|
||||
DockerRegistry: m.DockerRegistry,
|
||||
ContainerName: m.ContainerName,
|
||||
HealthURL: m.HealthURL,
|
||||
})
|
||||
}
|
||||
|
||||
+140
-1
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
|
||||
@@ -461,10 +462,148 @@ func ListPageRevisions(ctx *context.APIContext) {
|
||||
ctx.JSON(http.StatusOK, convert.ToWikiCommitList(commitsHistory, commitsCount))
|
||||
}
|
||||
|
||||
// SearchWikiPages searches wiki page titles and content.
|
||||
func SearchWikiPages(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/wiki/search repository repoSearchWikiPages
|
||||
// ---
|
||||
// summary: Search wiki pages
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: q
|
||||
// in: query
|
||||
// description: search query
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// description: "SearchResults"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
query := strings.TrimSpace(ctx.FormString("q"))
|
||||
if query == "" {
|
||||
ctx.JSON(http.StatusOK, []interface{}{})
|
||||
return
|
||||
}
|
||||
|
||||
wikiRepo, commit := findWikiRepoCommit(ctx)
|
||||
if wikiRepo != nil {
|
||||
defer wikiRepo.Close()
|
||||
}
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
queryLower := strings.ToLower(query)
|
||||
|
||||
type WikiSearchResult struct {
|
||||
PageName string `json:"page_name"`
|
||||
PageURL string `json:"page_url"`
|
||||
Context string `json:"context,omitempty"`
|
||||
}
|
||||
|
||||
entries, err := commit.ListEntriesRecursiveFast()
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
var results []WikiSearchResult
|
||||
for _, entry := range entries {
|
||||
if !entry.IsRegular() || !strings.HasSuffix(entry.Name(), ".md") {
|
||||
continue
|
||||
}
|
||||
baseName := strings.TrimSuffix(entry.Name(), ".md")
|
||||
// Extract just the filename without path for special file check
|
||||
parts := strings.Split(baseName, "/")
|
||||
shortName := parts[len(parts)-1]
|
||||
if shortName == "_Sidebar" || shortName == "_Footer" {
|
||||
continue
|
||||
}
|
||||
|
||||
blob := entry.Blob()
|
||||
content, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
titleMatch := strings.Contains(strings.ToLower(baseName), queryLower)
|
||||
contentMatch := strings.Contains(strings.ToLower(content), queryLower)
|
||||
|
||||
if !titleMatch && !contentMatch {
|
||||
continue
|
||||
}
|
||||
|
||||
contextLine := ""
|
||||
if contentMatch {
|
||||
for _, line := range strings.Split(content, "\n") {
|
||||
if strings.Contains(strings.ToLower(line), queryLower) {
|
||||
contextLine = strings.TrimSpace(line)
|
||||
if len(contextLine) > 200 {
|
||||
contextLine = contextLine[:200] + "..."
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wikiName, err := wiki_service.GitPathToWebPath(entry.Name())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
_, displayName := wiki_service.WebPathToUserTitle(wikiName)
|
||||
|
||||
results = append(results, WikiSearchResult{
|
||||
PageName: displayName,
|
||||
PageURL: string(wikiName),
|
||||
Context: contextLine,
|
||||
})
|
||||
}
|
||||
|
||||
// Pagination
|
||||
page := max(ctx.FormInt("page"), 1)
|
||||
limit := ctx.FormInt("limit")
|
||||
if limit <= 0 {
|
||||
limit = setting.API.DefaultPagingNum
|
||||
}
|
||||
total := len(results)
|
||||
start := (page - 1) * limit
|
||||
end := start + limit
|
||||
if start > total {
|
||||
start = total
|
||||
}
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
|
||||
ctx.SetLinkHeader(int64(total), limit)
|
||||
ctx.SetTotalCountHeader(int64(total))
|
||||
ctx.JSON(http.StatusOK, results[start:end])
|
||||
}
|
||||
|
||||
// findEntryForFile finds the tree entry for a target filepath.
|
||||
func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) {
|
||||
entry, err := commit.GetTreeEntryByPath(target)
|
||||
if err != nil {
|
||||
if err != nil && !git.IsErrNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
if entry != nil {
|
||||
|
||||
@@ -123,6 +123,14 @@ func saveMetadata(ctx *context.Context) {
|
||||
manifest.Maintainer = existing.Maintainer
|
||||
manifest.MaintainerURL = existing.MaintainerURL
|
||||
manifest.Language = existing.Language
|
||||
manifest.DeployHost = existing.DeployHost
|
||||
manifest.DeployPort = existing.DeployPort
|
||||
manifest.DeployUser = existing.DeployUser
|
||||
manifest.DeployPath = existing.DeployPath
|
||||
manifest.DockerImage = existing.DockerImage
|
||||
manifest.DockerRegistry = existing.DockerRegistry
|
||||
manifest.ContainerName = existing.ContainerName
|
||||
manifest.HealthURL = existing.HealthURL
|
||||
}
|
||||
|
||||
if err := repo_model.CreateOrUpdateRepoMetadata(ctx, manifest); err != nil {
|
||||
|
||||
@@ -51,6 +51,7 @@ const (
|
||||
tplWikiNew templates.TplName = "repo/wiki/new"
|
||||
tplWikiPages templates.TplName = "repo/wiki/pages"
|
||||
tplWikiBacklinks templates.TplName = "repo/wiki/backlinks"
|
||||
tplWikiSearch templates.TplName = "repo/wiki/search"
|
||||
tplWikiRecentChanges templates.TplName = "repo/wiki/recent"
|
||||
tplWikiDiff templates.TplName = "repo/wiki/diff"
|
||||
tplWikiCategory templates.TplName = "repo/wiki/category"
|
||||
@@ -542,6 +543,9 @@ func Wiki(ctx *context.Context) {
|
||||
case "_backlinks":
|
||||
WikiBacklinks(ctx)
|
||||
return
|
||||
case "_search":
|
||||
WikiSearch(ctx)
|
||||
return
|
||||
case "_recent":
|
||||
WikiRecentChanges(ctx)
|
||||
return
|
||||
@@ -753,6 +757,112 @@ func WikiBacklinks(ctx *context.Context) {
|
||||
ctx.HTML(http.StatusOK, tplWikiBacklinks)
|
||||
}
|
||||
|
||||
// WikiSearch performs full-text search across all wiki pages.
|
||||
func WikiSearch(ctx *context.Context) {
|
||||
ctx.Data["CanWriteWiki"] = ctx.Repo.Permission.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsArchived
|
||||
|
||||
query := strings.TrimSpace(ctx.FormString("q"))
|
||||
ctx.Data["Query"] = query
|
||||
ctx.Data["Title"] = "Wiki search"
|
||||
|
||||
if !repo_service.HasWiki(ctx, ctx.Repo.Repository) {
|
||||
ctx.HTML(http.StatusOK, tplWikiStart)
|
||||
return
|
||||
}
|
||||
|
||||
type SearchResult struct {
|
||||
PageName string
|
||||
PageURL string
|
||||
Context string
|
||||
}
|
||||
|
||||
if query == "" {
|
||||
ctx.Data["Results"] = []SearchResult{}
|
||||
ctx.Data["ResultCount"] = 0
|
||||
ctx.HTML(http.StatusOK, tplWikiSearch)
|
||||
return
|
||||
}
|
||||
|
||||
_, commit, err := findWikiRepoCommit(ctx)
|
||||
if err != nil {
|
||||
if !git.IsErrNotExist(err) {
|
||||
ctx.ServerError("findWikiRepoCommit", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
queryLower := strings.ToLower(query)
|
||||
|
||||
var results []SearchResult
|
||||
|
||||
var searchEntries func(entries git.Entries, prefix string)
|
||||
searchEntries = func(entries git.Entries, prefix string) {
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
subTree := entry.Tree()
|
||||
if subTree == nil {
|
||||
continue
|
||||
}
|
||||
children, _ := subTree.ListEntries()
|
||||
searchEntries(children, prefix+entry.Name()+"/")
|
||||
continue
|
||||
}
|
||||
if !entry.IsRegular() || !strings.HasSuffix(entry.Name(), ".md") {
|
||||
continue
|
||||
}
|
||||
baseName := strings.TrimSuffix(entry.Name(), ".md")
|
||||
if baseName == "_Sidebar" || baseName == "_Footer" {
|
||||
continue
|
||||
}
|
||||
|
||||
fullGitPath := prefix + entry.Name()
|
||||
fullName := prefix + baseName
|
||||
|
||||
blob := entry.Blob()
|
||||
content, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Case-insensitive search: check title and content
|
||||
titleMatch := strings.Contains(strings.ToLower(fullName), queryLower)
|
||||
contentMatch := strings.Contains(strings.ToLower(content), queryLower)
|
||||
|
||||
if !titleMatch && !contentMatch {
|
||||
continue
|
||||
}
|
||||
|
||||
contextLine := ""
|
||||
if contentMatch {
|
||||
for _, line := range strings.Split(content, "\n") {
|
||||
if strings.Contains(strings.ToLower(line), queryLower) {
|
||||
contextLine = strings.TrimSpace(line)
|
||||
if len(contextLine) > 200 {
|
||||
contextLine = contextLine[:200] + "..."
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wpName, _ := wiki_service.GitPathToWebPath(fullGitPath)
|
||||
_, displayName := wiki_service.WebPathToUserTitle(wpName)
|
||||
results = append(results, SearchResult{
|
||||
PageName: displayName,
|
||||
PageURL: string(wpName),
|
||||
Context: contextLine,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
entries, _ := commit.ListEntries()
|
||||
searchEntries(entries, "")
|
||||
|
||||
ctx.Data["Results"] = results
|
||||
ctx.Data["ResultCount"] = len(results)
|
||||
ctx.HTML(http.StatusOK, tplWikiSearch)
|
||||
}
|
||||
|
||||
func WikiRecentChanges(ctx *context.Context) {
|
||||
ctx.Data["CanWriteWiki"] = ctx.Repo.Permission.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsArchived
|
||||
ctx.Data["Title"] = "Recent changes"
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
{{template "base/head" .}}
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content repository wiki">
|
||||
{{template "repo/header" .}}
|
||||
<div class="ui container">
|
||||
<div class="repo-button-row">
|
||||
<div class="tw-flex tw-items-center tw-gap-2">
|
||||
<a class="ui small button" href="{{.RepoLink}}/wiki/">
|
||||
{{svg "octicon-arrow-left" 14}} Back to wiki
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>{{svg "octicon-search" 20}} Wiki search</h2>
|
||||
|
||||
<form class="ui form tw-mb-4" action="{{.RepoLink}}/wiki/" method="get">
|
||||
<input type="hidden" name="action" value="_search">
|
||||
<div class="ui action input tw-w-full">
|
||||
<input type="text" name="q" value="{{.Query}}" placeholder="Search wiki pages..." autofocus>
|
||||
<button class="ui primary button" type="submit">{{svg "octicon-search" 14}} Search</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{{if .Query}}
|
||||
{{if .Results}}
|
||||
<div class="ui relaxed divided list">
|
||||
{{range .Results}}
|
||||
<div class="item">
|
||||
<div class="content">
|
||||
<a class="header" href="{{$.RepoLink}}/wiki/{{.PageURL}}">{{.PageName}}</a>
|
||||
{{if .Context}}
|
||||
<div class="description">
|
||||
<code class="tw-text-sm">{{.Context}}</code>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<p class="tw-mt-4 text grey">{{.ResultCount}} {{if eq .ResultCount 1}}result{{else}}results{{end}} for "{{.Query}}"</p>
|
||||
{{else}}
|
||||
<div class="ui placeholder segment">
|
||||
<div class="ui icon header">
|
||||
{{svg "octicon-search" 48}}
|
||||
<br>
|
||||
No results for "{{.Query}}"
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{template "base/footer" .}}
|
||||
@@ -20,6 +20,7 @@
|
||||
</div>
|
||||
<div class="scrolling menu">
|
||||
<a class="item muted" href="{{.RepoLink}}/wiki/?action=_pages">{{ctx.Locale.Tr "repo.wiki.pages"}}</a>
|
||||
<a class="item muted" href="{{.RepoLink}}/wiki/?action=_search">{{svg "octicon-search" 14}} Search wiki</a>
|
||||
<a class="item muted" href="{{.RepoLink}}/wiki/?action=_recent">{{svg "octicon-history" 14}} Recent changes</a>
|
||||
t <a class="item muted" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_print" target="_blank">{{svg "octicon-browser" 14}} Print view</a>
|
||||
<a class="item muted" href="{{.RepoLink}}/wiki/?action=_export&format=zip">{{svg "octicon-download" 14}} Export wiki (ZIP)</a>
|
||||
|
||||
Reference in New Issue
Block a user