Compare commits
72 Commits
1178eaec62
..
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ca4996307 | |||
| f86527994b | |||
| c016c603b4 | |||
| 3cd6146fa0 | |||
| 902ee39e90 | |||
| aae7b65329 | |||
| 35f9cd2882 | |||
| b8ad5398a3 | |||
| 09aa8d8201 | |||
| b6e88e4baf | |||
| d274aabb4f | |||
| 33ebcd7726 | |||
| bcfae6d370 | |||
| 6bb6e2ffd8 | |||
| 90fb6169d0 | |||
| 5f6d25ff7b | |||
| 9adcac546f | |||
| b6b4d6f525 | |||
| 74279c55e3 | |||
| 78803e60df | |||
| 75316bf80a | |||
| 37d59e7b59 | |||
| 18fc79fa0a | |||
| 931d685593 | |||
| 9121f1b36a | |||
| f3ce51d629 | |||
| 1f505b48c7 | |||
| 4b07ccc578 | |||
| afda7abcbe | |||
| 798d9c3ae0 | |||
| 4d42205cc8 | |||
| e0f22dd397 | |||
| ecda05aa46 | |||
| 3a492c5bd5 | |||
| 9eadade2f4 | |||
| 2cc4f7c047 | |||
| c947ebcb49 | |||
| 74a5fe2b80 | |||
| 50c472991a | |||
| eef6292832 | |||
| 7dabf844a8 | |||
| 7d03541201 | |||
| 27aeb19dda | |||
| d4a2c33c37 | |||
| e59290802a | |||
| d1b2fca784 | |||
| 1d857d8205 | |||
| 4f9aeb7b85 | |||
| 971c5fc7a7 | |||
| cd36065464 | |||
| 0debc72356 | |||
| 1ef6ef5fd4 | |||
| 62a44a3668 | |||
| 3c456dfe85 | |||
| 7b75ce9564 | |||
| 3abd239397 | |||
| 1e69927cec | |||
| c71e622e11 | |||
| 2ba5e42113 | |||
| 7240deb822 | |||
| 727cff9eb8 | |||
| 4c715d8424 | |||
| c0acdd1f58 | |||
| c73109e2e6 | |||
| 3a159b7da6 | |||
| 7c8b20b779 | |||
| 4e8af85178 | |||
| aa1a67c4cb | |||
| 5642057c80 | |||
| 4dd27ccdb8 | |||
| 71a7ab04e5 | |||
| d6dc7533ff |
@@ -39,4 +39,4 @@ GITEA_TEST_E2E_FLAGS='<filepath>' make test-e2e # Single Playwright test
|
||||
- Add `Co-Authored-By` lines to all commits
|
||||
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
|
||||
- **Attribution**: `Authored-by: Moko Consulting`
|
||||
- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)
|
||||
- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/mokoplatform/wiki/Home)
|
||||
|
||||
@@ -8,7 +8,7 @@ contact_links:
|
||||
url: https://mokoconsulting.tech/
|
||||
about: Get help or ask questions through our website
|
||||
- name: 📚 MokoStandards Documentation
|
||||
url: https://code.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
url: https://code.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
about: View our coding standards and best practices
|
||||
- name: 🔒 Report a Security Vulnerability
|
||||
url: https://code.mokoconsulting.tech/mokoconsulting-tech/.github-private/security/advisories/new
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Automation
|
||||
# REPO: https://code.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# INGROUP: mokoplatform.Automation
|
||||
# REPO: https://code.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
# PATH: /.gitea/workflows/branch-protection.yml
|
||||
# BRIEF: Apply standardised branch protection rules to all governed repositories
|
||||
#
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
API="${GITEA_URL}/api/v1"
|
||||
|
||||
# Platform/standards/infra repos to exclude
|
||||
EXCLUDE="gitea-org-config org-profile gitea-private .mokogitea-private MokoStandards moko-platform MokoTesting"
|
||||
EXCLUDE="gitea-org-config org-profile gitea-private .mokogitea-private MokoStandards mokoplatform MokoTesting"
|
||||
EXCLUDE="$EXCLUDE MokoStandards-Template-Client MokoStandards-Template-Dolibarr MokoStandards-Template-Generic MokoStandards-Template-Joomla MokoDoliProjTemplate"
|
||||
|
||||
if [ -n "${{ inputs.repos }}" ]; then
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<moko-platform xmlns="https://standards.mokoconsulting.tech/moko-platform/1.0" schema-version="1.0">
|
||||
<mokoplatform xmlns="https://standards.mokoconsulting.tech/mokoplatform/1.0" schema-version="1.0">
|
||||
<identity>
|
||||
<name>MokoGitea</name>
|
||||
<org>MokoConsulting</org>
|
||||
<description>Moko fork of Gitea -- adding project board REST API endpoints and custom enhancements</description>
|
||||
<version>05.47.00</version>
|
||||
<description>Moko fork of Gitea - adding project board REST API endpoints and custom enhancements</description>
|
||||
<version>06.12.02</version>
|
||||
<version-prefix>v1.26.1+MOKO</version-prefix>
|
||||
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||
</identity>
|
||||
<governance>
|
||||
<platform>go</platform>
|
||||
<standards-version>05.00.00</standards-version>
|
||||
<standards-source>https://code.mokoconsulting.tech/MokoConsulting/moko-platform</standards-source>
|
||||
<standards-source>https://code.mokoconsulting.tech/MokoConsulting/mokoplatform</standards-source>
|
||||
</governance>
|
||||
<build>
|
||||
<language>Go</language>
|
||||
<package-type>application</package-type>
|
||||
<entry-point>./</entry-point>
|
||||
</build>
|
||||
</moko-platform>
|
||||
</mokoplatform>
|
||||
|
||||
@@ -445,9 +445,10 @@ server.tool(
|
||||
assignees: z.array(z.string()).optional().describe('Usernames to assign'),
|
||||
status_id: z.number().optional().describe('Custom status definition ID'),
|
||||
priority_id: z.number().optional().describe('Custom priority definition ID'),
|
||||
type_id: z.number().optional().describe('Custom type definition ID'),
|
||||
...ConnectionParam,
|
||||
},
|
||||
async ({ owner, repo, title, body: issueBody, labels, milestone, assignees, status_id, priority_id, connection }) => {
|
||||
async ({ owner, repo, title, body: issueBody, labels, milestone, assignees, status_id, priority_id, type_id, connection }) => {
|
||||
const c = clientFor(connection);
|
||||
|
||||
// Search for existing issue with same title to prevent duplicates
|
||||
@@ -483,6 +484,7 @@ server.tool(
|
||||
if (issueData?.id) {
|
||||
if (status_id !== undefined) await c.post(`/repos/${owner}/${repo}/issues/${issueData.id}/custom-status`, { status_id });
|
||||
if (priority_id !== undefined) await c.post(`/repos/${owner}/${repo}/issues/${issueData.id}/custom-priority`, { priority_id });
|
||||
if (type_id !== undefined) await c.post(`/repos/${owner}/${repo}/issues/${issueData.id}/custom-type`, { type_id });
|
||||
}
|
||||
const out = formatResponse(res);
|
||||
out.content[0].text = `Updated existing issue #${existing.number} (duplicate prevented)\n${out.content[0].text}`;
|
||||
@@ -501,6 +503,7 @@ server.tool(
|
||||
if (newIssue?.id) {
|
||||
if (status_id !== undefined) await c.post(`/repos/${owner}/${repo}/issues/${newIssue.id}/custom-status`, { status_id });
|
||||
if (priority_id !== undefined) await c.post(`/repos/${owner}/${repo}/issues/${newIssue.id}/custom-priority`, { priority_id });
|
||||
if (type_id !== undefined) await c.post(`/repos/${owner}/${repo}/issues/${newIssue.id}/custom-type`, { type_id });
|
||||
}
|
||||
return formatResponse(res);
|
||||
},
|
||||
@@ -2006,6 +2009,52 @@ server.tool(
|
||||
},
|
||||
);
|
||||
|
||||
// ── Issue Types (org-level) ──────────────────────────────────────────────
|
||||
|
||||
server.tool(
|
||||
'gitea_org_issue_types_list',
|
||||
'List custom issue type definitions for an organization',
|
||||
{
|
||||
org: z.string().describe('Organization name'),
|
||||
...ConnectionParam,
|
||||
},
|
||||
async ({ org, connection }) => {
|
||||
const c = clientFor(connection);
|
||||
return formatResponse(await c.get(`/orgs/${org}/issue-types`));
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
'gitea_issue_set_type',
|
||||
'Set custom type on an issue',
|
||||
{
|
||||
owner: z.string().describe('Repository owner'),
|
||||
repo: z.string().describe('Repository name'),
|
||||
issue_id: z.number().describe('Internal issue ID'),
|
||||
type_id: z.number().describe('Type definition ID (0 to clear)'),
|
||||
...ConnectionParam,
|
||||
},
|
||||
async ({ owner, repo, issue_id, type_id, connection }) => {
|
||||
const c = clientFor(connection);
|
||||
return formatResponse(await c.post(`/repos/${owner}/${repo}/issues/${issue_id}/custom-type`, { type_id }));
|
||||
},
|
||||
);
|
||||
|
||||
// ── Security ────────────────────────────────────────────────────────────
|
||||
|
||||
server.tool(
|
||||
'gitea_security_alerts',
|
||||
'List security alerts for a repository',
|
||||
{
|
||||
...OwnerRepo,
|
||||
...ConnectionParam,
|
||||
},
|
||||
async ({ owner, repo, connection }) => {
|
||||
const c = clientFor(connection);
|
||||
return formatResponse(await c.get(`/repos/${owner}/${repo}/security/alerts`));
|
||||
},
|
||||
);
|
||||
|
||||
// ── Start Server ────────────────────────────────────────────────────────
|
||||
|
||||
async function main(): Promise<void> {
|
||||
|
||||
@@ -1,324 +1 @@
|
||||
# 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-tech/moko-platform
|
||||
# PATH: /templates/workflows/universal/auto-release.yml.template
|
||||
# VERSION: 05.00.00
|
||||
# BRIEF: Universal build & release � detects platform from manifest.xml
|
||||
#
|
||||
# +========================================================================+
|
||||
# | UNIVERSAL BUILD & RELEASE PIPELINE |
|
||||
# +========================================================================+
|
||||
# | |
|
||||
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
||||
# | |
|
||||
# | Platform-specific: |
|
||||
# | joomla: XML manifest, type-prefixed packages |
|
||||
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
||||
# | generic: README-only, no update stream |
|
||||
# | |
|
||||
# +========================================================================+
|
||||
|
||||
name: "Universal: Build & Release"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, closed]
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
action:
|
||||
description: 'Action to perform'
|
||||
required: false
|
||||
type: choice
|
||||
default: release
|
||||
options:
|
||||
- release
|
||||
- promote-rc
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────
|
||||
promote-rc:
|
||||
name: Promote to RC
|
||||
runs-on: release
|
||||
if: >-
|
||||
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
run: |
|
||||
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
|
||||
|
||||
- name: Rename branch to rc
|
||||
run: |
|
||||
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}" \
|
||||
--pr "${{ github.event.pull_request.number }}"
|
||||
|
||||
- name: Checkout rc and configure git
|
||||
run: |
|
||||
git fetch origin rc
|
||||
git checkout rc
|
||||
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"
|
||||
|
||||
- name: Publish RC release
|
||||
run: |
|
||||
php ${MOKO_CLI}/release_publish.php \
|
||||
--path . --stability rc --bump minor --branch rc \
|
||||
--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" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
||||
release:
|
||||
name: Build & Release Pipeline
|
||||
runs-on: release
|
||||
if: >-
|
||||
github.event.pull_request.merged == true ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure git for bot pushes
|
||||
run: |
|
||||
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"
|
||||
|
||||
- name: Check for merge conflict markers
|
||||
run: |
|
||||
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
||||
if [ -n "$CONFLICTS" ]; then
|
||||
echo "::error::Merge conflict markers found — aborting release"
|
||||
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
echo "No conflict markers found"
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
||||
run: |
|
||||
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
|
||||
|
||||
- name: "Publish stable release"
|
||||
run: |
|
||||
php ${MOKO_CLI}/release_publish.php \
|
||||
--path . --stability stable --bump minor --branch main \
|
||||
--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"
|
||||
if: >-
|
||||
steps.version.outputs.skip != 'true' &&
|
||||
secrets.GH_MIRROR_TOKEN != ''
|
||||
continue-on-error: true
|
||||
run: |
|
||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||
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 ${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" \
|
||||
--branch main 2>&1 || true
|
||||
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
|
||||
- name: "Step 10: Push main to GitHub mirror"
|
||||
if: >-
|
||||
steps.version.outputs.skip != 'true' &&
|
||||
secrets.GH_MIRROR_TOKEN != ''
|
||||
continue-on-error: true
|
||||
run: |
|
||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
|
||||
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
|
||||
git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
|
||||
git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
|
||||
git fetch origin main --depth=1
|
||||
git push github origin/main:refs/heads/main --force 2>/dev/null \
|
||||
&& echo "main branch pushed to GitHub mirror" \
|
||||
|| echo "WARNING: GitHub mirror push failed"
|
||||
|
||||
- name: "Step 11: Delete rc branch and recreate dev from main"
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
# Delete rc branch (ephemeral — created by promote-rc)
|
||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||
"${API_BASE}/branches/rc" 2>/dev/null \
|
||||
&& echo "Deleted rc branch" || echo "rc branch not found"
|
||||
|
||||
# Delete dev branch
|
||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
|
||||
|
||||
# Recreate dev from main (now includes version bump + changelog promotion)
|
||||
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${API_BASE}/branches" \
|
||||
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
|
||||
|
||||
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: "Step 12: Create version branch from main"
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||
BRANCH_NAME="version/${VERSION}"
|
||||
MAIN_SHA=$(git rev-parse HEAD)
|
||||
|
||||
# Delete old version branch if it exists (same version re-release)
|
||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
|
||||
|
||||
# Create version/XX.YY.ZZ from main
|
||||
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
|
||||
|
||||
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
|
||||
|
||||
# -- Dolibarr post-release: Reset dev version -----------------------------
|
||||
- name: "Post-release: Reset dev version"
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
php ${MOKO_CLI}/version_reset_dev.php \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
||||
--branch dev --path . 2>&1 || true
|
||||
|
||||
# -- Summary --------------------------------------------------------------
|
||||
- name: Pipeline Summary
|
||||
if: always()
|
||||
run: |
|
||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
|
||||
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
|
||||
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
|
||||
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
|
||||
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
placeholder
|
||||
@@ -5,7 +5,7 @@
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Universal
|
||||
# REPO: https://code.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# REPO: https://code.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
# PATH: /.mokogitea/workflows/branch-cleanup.yml
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Delete feature branches after PR merge
|
||||
|
||||
@@ -11,7 +11,7 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version tag (e.g. v1.26.1-moko.05.01.00)'
|
||||
description: 'Version tag (e.g. v1.26.1+MOKO06.12.00)'
|
||||
required: true
|
||||
default: 'latest'
|
||||
environment:
|
||||
@@ -180,10 +180,21 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Extract moko version from tag (e.g. v1.26.1-moko.05.01.01 -> 05.01.01)
|
||||
MOKO_VER=$(echo "$TAG" | sed -n 's/.*-moko\.\(.*\)/\1/p')
|
||||
# Extract project version by stripping the version prefix from the tag.
|
||||
# Reads prefix from manifest API (e.g. "v1.26.1+MOKO"), falls back to legacy pattern.
|
||||
API_BASE="https://${REGISTRY}/api/v1/repos/MokoConsulting/MokoGitea"
|
||||
PREFIX=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API_BASE}/manifest" | python3 -c "import json,sys; print(json.load(sys.stdin).get('version_prefix',''))" 2>/dev/null || true)
|
||||
|
||||
if [ -n "$PREFIX" ]; then
|
||||
MOKO_VER="${TAG#$PREFIX}"
|
||||
else
|
||||
# Legacy fallback: strip everything up to and including "-moko."
|
||||
MOKO_VER=$(echo "$TAG" | sed -n 's/.*-moko\.\(.*\)/\1/p')
|
||||
fi
|
||||
|
||||
if [ -z "$MOKO_VER" ]; then
|
||||
echo "Could not extract moko version from tag: $TAG"
|
||||
echo "Could not extract version from tag: $TAG (prefix: ${PREFIX:-none})"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Automation
|
||||
# VERSION: 05.47.00
|
||||
# INGROUP: mokoplatform.Automation
|
||||
# VERSION: 06.12.02
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
name: "Universal: Issue Branch"
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
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.GITEA_TOKEN }}
|
||||
+508
-508
File diff suppressed because it is too large
Load Diff
@@ -1,243 +1,245 @@
|
||||
# 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
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokoplatform.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||
# VERSION: 05.01.00
|
||||
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
|
||||
|
||||
name: "Universal: Pre-Release"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- alpha
|
||||
- beta
|
||||
- rc
|
||||
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 || github.ref_name }})"
|
||||
runs-on: release
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
github.event_name == 'push'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
ref: ${{ github.ref_name }}
|
||||
|
||||
- name: Setup mokoplatform tools
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
run: |
|
||||
# Use pre-installed /opt/mokoplatform if available (updated by cron every 6h)
|
||||
if [ -f /opt/mokoplatform/cli/version_bump.php ] && [ -f /opt/mokoplatform/cli/manifest_element.php ] && [ -f /opt/mokoplatform/vendor/autoload.php ]; then
|
||||
echo Using pre-installed /opt/mokoplatform
|
||||
echo MOKO_CLI=/opt/mokoplatform/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/mokoplatform-api
|
||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokoplatform.git
|
||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokoplatform-api
|
||||
cd /tmp/mokoplatform-api && composer install --no-dev --no-interaction --quiet
|
||||
echo MOKO_CLI=/tmp/mokoplatform-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 from branch name on push, or use input on dispatch
|
||||
if [ "${{ github.event_name }}" = "push" ]; then
|
||||
case "${{ github.ref_name }}" in
|
||||
rc) STABILITY="release-candidate" ;;
|
||||
alpha) STABILITY="alpha" ;;
|
||||
beta) STABILITY="beta" ;;
|
||||
*) STABILITY="development" ;;
|
||||
esac
|
||||
else
|
||||
STABILITY="${{ inputs.stability || 'development' }}"
|
||||
fi
|
||||
|
||||
case "$STABILITY" in
|
||||
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 "${{ github.ref_name }}" --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
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoPlatform.Universal
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
# PATH: /.mokogitea/workflows/rc-revert.yml
|
||||
# VERSION: 09.23.00
|
||||
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
|
||||
|
||||
name: "RC Revert"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
revert:
|
||||
name: Rename rc/ back to dev/
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.pull_request.merged == false &&
|
||||
startsWith(github.event.pull_request.head.ref, 'rc/')
|
||||
|
||||
steps:
|
||||
- name: Rename branch
|
||||
run: |
|
||||
BRANCH="${{ github.event.pull_request.head.ref }}"
|
||||
SUFFIX="${BRANCH#rc/}"
|
||||
DEV_BRANCH="dev/${SUFFIX}"
|
||||
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
# Create dev/ branch from rc/ branch
|
||||
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"new_branch_name\": \"${DEV_BRANCH}\", \"old_branch_name\": \"${BRANCH}\"}" \
|
||||
"${API}" 2>/dev/null || true)
|
||||
|
||||
if [ "$STATUS" = "201" ]; then
|
||||
echo "Created branch: ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Delete rc/ branch
|
||||
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
|
||||
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
"${API}/${ENCODED}" 2>/dev/null || true)
|
||||
|
||||
if [ "$STATUS" = "204" ]; then
|
||||
echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "::warning::Failed to delete ${BRANCH} (HTTP ${STATUS})"
|
||||
fi
|
||||
|
||||
echo "### RC Reverted" >> $GITHUB_STEP_SUMMARY
|
||||
echo "${BRANCH} → ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||
File diff suppressed because it is too large
Load Diff
+58
-22
@@ -3,6 +3,64 @@
|
||||
All notable changes to MokoGitea are documented here. Versions follow the format
|
||||
`v{upstream}-moko.{major}.{minor}` (e.g. `v1.26.1-moko.06.03`).
|
||||
|
||||
## [v1.26.1-moko.06.12] - 2026-06-07
|
||||
|
||||
* FEATURES
|
||||
* feat(security): dependency vulnerability scanner - parses go.mod, package.json, composer.json, requirements.txt and checks against OSV.dev API (#551)
|
||||
* feat(cdn): built-in CDN for release asset delivery via cdn.mokoconsulting.tech with per-asset public/private toggles (#561)
|
||||
* feat(cdn): IP/CIDR and referrer domain allowlists for CDN abuse prevention
|
||||
* feat(cdn): releases in update streams excluded from CDN (update server takes precedence)
|
||||
|
||||
* FIXES
|
||||
* fix(licensing): hide "Require license key" option for Joomla update servers (Joomla limitation)
|
||||
* fix(settings): remove duplicate description from manifest page (#559)
|
||||
|
||||
* INFRASTRUCTURE
|
||||
* chore: rename moko-platform to MokoPlatform across codebase (#548)
|
||||
* CDN CNAME: cdn.mokoconsulting.tech with auto-TLS via Let's Encrypt
|
||||
* Nginx reverse proxy for CDN hostname on production server
|
||||
* DreamHost MCP server path and API key configured
|
||||
|
||||
## [v1.26.1-moko.06.10] - 2026-06-06
|
||||
|
||||
* FEATURES
|
||||
* feat(issues): first-class Type field with 12 auto-seeded defaults (Bug, Feature, Enhancement, Task, Documentation, Security, Roadmap, Client, Dolibarr, Infrastructure, Joomla, WaaS)
|
||||
* feat(issues): first-class Status field with 13 auto-seeded defaults including 7 Pending states
|
||||
* feat(issues): first-class Priority field with 4 auto-seeded defaults (Critical, High, Medium, Low)
|
||||
* feat(issues): Type/Status/Priority colored badges in issue list view
|
||||
* feat(issues): status dropdown replaces close/reopen button in comment form
|
||||
* feat(security): built-in security scanning platform with secret scanner (15 patterns)
|
||||
* feat(security): Security tab in repo navigation with alerts, scan controls
|
||||
* feat(wiki): hierarchical folder navigation with sidebar tree and breadcrumbs
|
||||
* feat(ui): well-known file tabs (README/LICENSE/CONTRIBUTING/SECURITY/CHANGELOG)
|
||||
* feat(settings): repo manifest settings with REST API and auto-sync on push
|
||||
* feat(mcp): public MCP server published to npm (@mokoconsulting/mokogitea-mcp)
|
||||
* feat(mcp): SSE transport, env var config, Docker support, 120+ tools
|
||||
* feat(mcp): issue dedup on create, type_id/status_id/priority_id params
|
||||
|
||||
* MIGRATIONS
|
||||
* All org labels migrated to first-class Type/Status/Priority fields and deleted
|
||||
* Type custom field (id=9) migrated to type_id and deleted
|
||||
* Status custom field (id=1) deleted (replaced by first-class field)
|
||||
* Priority labels migrated to priority_id
|
||||
* Pending labels migrated to status definitions
|
||||
* Scope labels migrated to type definitions
|
||||
* Manifests populated for all 61 repos via API
|
||||
|
||||
* FIXES
|
||||
* fix(ui): dashboard issue count badges use label spans instead of strong tags
|
||||
* fix(wiki): directory check before raw redirect for folder navigation
|
||||
* fix(wiki): proper display names in sidebar tree (strip dash markers)
|
||||
* fix: replace non-ASCII em dashes with hyphens for hook compatibility
|
||||
* fix: hookify __init__.py for stop hook JSON validation
|
||||
|
||||
* INFRASTRUCTURE
|
||||
* npm: @mokoconsulting/mokogitea-mcp@1.1.0 and @mokoconsulting/mokowaas-mcp@1.0.0
|
||||
* MCP servers consolidated under mokoplatform/mcp/servers/
|
||||
* Remote MCP repos renamed to hyphens
|
||||
* Wiki restructured into features/, api/, operations/ folders
|
||||
* Swagger API docs enabled at /api/swagger
|
||||
|
||||
## [v1.26.1-moko.06.04] - 2026-06-06
|
||||
|
||||
* FEATURES
|
||||
@@ -165,25 +223,3 @@ All notable changes to MokoGitea are documented here. Versions follow the format
|
||||
* Reopened 9 closed issues lacking documented testing proof
|
||||
* Created `pending: testing` label for features awaiting verification
|
||||
* Established policy: issues must not be closed without documented testing proof
|
||||
|
||||
## [1.26.1](https://github.com/go-gitea/gitea/releases/tag/v1.26.1) - 2026-04-21
|
||||
|
||||
* BUGFIXES
|
||||
* Add event.schedule context for schedule actions task (#37320) (#37348)
|
||||
* Fix an issue where changing an organization's visibility caused problems when users had forked its repositories. (#37324) (#37344)
|
||||
* Use modern "git update-index --cacheinfo" syntax to support more file names (#37338) (#37343)
|
||||
* Fix URL related escaping for oauth2 (#37334) (#37340)
|
||||
* When the requested arch rpm is missing fall back to noarch (#37236) (#37339)
|
||||
* Fix actions concurrency groups cross-branch leak (#37311) (#37331)
|
||||
* Fix bug when accessing user badges (#37321) (#37329)
|
||||
* Fix AppFullLink (#37325) (#37328)
|
||||
* Fix container auth for public instance (#37290) (#37294)
|
||||
* Enhance GetActionWorkflow to support fallback references (#37189) (#37283)
|
||||
* Fix vite manifest update masking build errors (#37279) (#37310)
|
||||
* Fix Mermaid diagrams failing when node labels contain line breaks (#37296) (#37299)
|
||||
* Use TriggerEvent instead of Event in workflow runs API response for scheduled runs (#37288) #37360
|
||||
* Add URL to Learn more about blocking a user. (#37355) #37367
|
||||
* Fix button layout shift when collapsing file tree in editor (#37363) #37375
|
||||
* Fix org team assignee/reviewer lookups for team member permissions (#37365) #37391
|
||||
* Fix repo init README EOL (#37388) #37399
|
||||
* Fix: dump with default zip type produces uncompressed zip (#37401)#37402
|
||||
|
||||
@@ -18,7 +18,7 @@ Custom Gitea fork with Project Board API
|
||||
|
||||
---
|
||||
|
||||
**Category:** Infrastructure | **Platform:** [moko-platform wiki](https://code.mokoconsulting.tech/MokoConsulting/moko-platform/wiki)
|
||||
**Category:** Infrastructure | **Platform:** [MokoPlatform wiki](https://code.mokoconsulting.tech/MokoConsulting/MokoPlatform/wiki)
|
||||
|
||||
---
|
||||
|
||||
@@ -40,4 +40,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/moko-platform/wiki/Home)*
|
||||
*[Moko Consulting](https://mokoconsulting.tech) -- [MokoStandards](https://code.mokoconsulting.tech/MokoConsulting/MokoPlatform/wiki/Home)*
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// Copyright 2026 The MokoGitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
||||
)
|
||||
|
||||
// OrgSetting stores AI configuration for an organization.
|
||||
type OrgSetting struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
OrgID int64 `xorm:"UNIQUE NOT NULL"`
|
||||
Enabled bool `xorm:"NOT NULL DEFAULT true"`
|
||||
APIKeyEncrypted string `xorm:"TEXT"`
|
||||
Model string `xorm:"VARCHAR(50) NOT NULL DEFAULT 'claude-sonnet-4-6'"`
|
||||
RateLimitRequests int `xorm:"NOT NULL DEFAULT 100"`
|
||||
RateLimitTokensMonth int64 `xorm:"NOT NULL DEFAULT 5000000"`
|
||||
CreatedUnix timeutil.TimeStamp
|
||||
UpdatedUnix timeutil.TimeStamp
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(OrgSetting))
|
||||
db.RegisterModel(new(RepoSetting))
|
||||
db.RegisterModel(new(UsageLog))
|
||||
}
|
||||
|
||||
// TableName returns the table name for OrgSetting.
|
||||
func (OrgSetting) TableName() string {
|
||||
return "ai_org_setting"
|
||||
}
|
||||
|
||||
// RepoSetting stores AI configuration for a repository.
|
||||
type RepoSetting struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"UNIQUE NOT NULL"`
|
||||
Enabled bool `xorm:"NOT NULL DEFAULT true"`
|
||||
AutoReview bool `xorm:"NOT NULL DEFAULT true"`
|
||||
Strictness string `xorm:"VARCHAR(20) NOT NULL DEFAULT 'standard'"`
|
||||
IgnorePatterns string `xorm:"TEXT"`
|
||||
CreatedUnix timeutil.TimeStamp
|
||||
UpdatedUnix timeutil.TimeStamp
|
||||
}
|
||||
|
||||
// TableName returns the table name for RepoSetting.
|
||||
func (RepoSetting) TableName() string {
|
||||
return "ai_repo_setting"
|
||||
}
|
||||
|
||||
// UsageLog records AI token usage per action.
|
||||
type UsageLog struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
OrgID int64 `xorm:"INDEX NOT NULL"`
|
||||
RepoID int64 `xorm:"INDEX NOT NULL"`
|
||||
TriggeredByID int64
|
||||
ActionType string `xorm:"VARCHAR(20) NOT NULL"` // review, chat, agent
|
||||
Model string `xorm:"VARCHAR(50)"`
|
||||
TokensInput int64
|
||||
TokensOutput int64
|
||||
DurationMs int64
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for UsageLog.
|
||||
func (UsageLog) TableName() string {
|
||||
return "ai_usage_log"
|
||||
}
|
||||
|
||||
// GetOrgSetting returns the AI settings for an org, or nil if not configured.
|
||||
func GetOrgSetting(ctx context.Context, orgID int64) (*OrgSetting, error) {
|
||||
setting := &OrgSetting{OrgID: orgID}
|
||||
has, err := db.GetEngine(ctx).Where("org_id = ?", orgID).Get(setting)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, nil
|
||||
}
|
||||
return setting, nil
|
||||
}
|
||||
|
||||
// GetRepoSetting returns the AI settings for a repo, or nil if not configured.
|
||||
func GetRepoSetting(ctx context.Context, repoID int64) (*RepoSetting, error) {
|
||||
setting := &RepoSetting{RepoID: repoID}
|
||||
has, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Get(setting)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, nil
|
||||
}
|
||||
return setting, nil
|
||||
}
|
||||
|
||||
// CreateOrgSetting inserts a new org AI setting.
|
||||
func CreateOrgSetting(ctx context.Context, setting *OrgSetting) error {
|
||||
setting.CreatedUnix = timeutil.TimeStampNow()
|
||||
setting.UpdatedUnix = timeutil.TimeStampNow()
|
||||
_, err := db.GetEngine(ctx).Insert(setting)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateOrgSetting updates an existing org AI setting.
|
||||
func UpdateOrgSetting(ctx context.Context, setting *OrgSetting) error {
|
||||
setting.UpdatedUnix = timeutil.TimeStampNow()
|
||||
_, err := db.GetEngine(ctx).ID(setting.ID).AllCols().Update(setting)
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateRepoSetting inserts a new repo AI setting.
|
||||
func CreateRepoSetting(ctx context.Context, setting *RepoSetting) error {
|
||||
setting.CreatedUnix = timeutil.TimeStampNow()
|
||||
setting.UpdatedUnix = timeutil.TimeStampNow()
|
||||
_, err := db.GetEngine(ctx).Insert(setting)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateRepoSetting updates an existing repo AI setting.
|
||||
func UpdateRepoSetting(ctx context.Context, setting *RepoSetting) error {
|
||||
setting.UpdatedUnix = timeutil.TimeStampNow()
|
||||
_, err := db.GetEngine(ctx).ID(setting.ID).AllCols().Update(setting)
|
||||
return err
|
||||
}
|
||||
|
||||
// LogUsage records an AI usage event.
|
||||
func LogUsage(ctx context.Context, log *UsageLog) error {
|
||||
log.CreatedUnix = timeutil.TimeStampNow()
|
||||
_, err := db.GetEngine(ctx).Insert(log)
|
||||
return err
|
||||
}
|
||||
@@ -428,6 +428,8 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(348, "Add issue priority definitions table", v1_27.AddIssuePriorityDefTable),
|
||||
newMigration(349, "Add security scanning tables", v1_27.AddSecurityScanningTables),
|
||||
newMigration(350, "Add issue type definitions table", v1_27.AddIssueTypeDefTable),
|
||||
newMigration(351, "Add CDN public flag to attachments", v1_27.AddAttachmentCDNPublic),
|
||||
newMigration(352, "Add version prefix and element name to repo manifest", v1_27.AddManifestVersionPrefixAndElement),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright 2026 The MokoGitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_27
|
||||
|
||||
import (
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type aiOrgSetting struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
OrgID int64 `xorm:"UNIQUE NOT NULL"`
|
||||
Enabled bool `xorm:"NOT NULL DEFAULT true"`
|
||||
APIKeyEncrypted string `xorm:"TEXT"`
|
||||
Model string `xorm:"VARCHAR(50) NOT NULL DEFAULT 'claude-sonnet-4-6'"`
|
||||
RateLimitRequests int `xorm:"NOT NULL DEFAULT 100"`
|
||||
RateLimitTokensMonth int64 `xorm:"NOT NULL DEFAULT 5000000"`
|
||||
CreatedUnix timeutil.TimeStamp
|
||||
UpdatedUnix timeutil.TimeStamp
|
||||
}
|
||||
|
||||
func (aiOrgSetting) TableName() string {
|
||||
return "ai_org_setting"
|
||||
}
|
||||
|
||||
type aiRepoSetting struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"UNIQUE NOT NULL"`
|
||||
Enabled bool `xorm:"NOT NULL DEFAULT true"`
|
||||
AutoReview bool `xorm:"NOT NULL DEFAULT true"`
|
||||
Strictness string `xorm:"VARCHAR(20) NOT NULL DEFAULT 'standard'"`
|
||||
IgnorePatterns string `xorm:"TEXT"`
|
||||
CreatedUnix timeutil.TimeStamp
|
||||
UpdatedUnix timeutil.TimeStamp
|
||||
}
|
||||
|
||||
func (aiRepoSetting) TableName() string {
|
||||
return "ai_repo_setting"
|
||||
}
|
||||
|
||||
type aiUsageLog struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
OrgID int64 `xorm:"INDEX NOT NULL"`
|
||||
RepoID int64 `xorm:"INDEX NOT NULL"`
|
||||
TriggeredByID int64
|
||||
ActionType string `xorm:"VARCHAR(20) NOT NULL"`
|
||||
Model string `xorm:"VARCHAR(50)"`
|
||||
TokensInput int64
|
||||
TokensOutput int64
|
||||
DurationMs int64
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX"`
|
||||
}
|
||||
|
||||
func (aiUsageLog) TableName() string {
|
||||
return "ai_usage_log"
|
||||
}
|
||||
|
||||
func AddAITables(x *xorm.Engine) error {
|
||||
return x.Sync(new(aiOrgSetting), new(aiRepoSetting), new(aiUsageLog))
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
// AddRepoManifestTable creates the repo_manifest table for storing
|
||||
// moko-platform manifest settings per repository.
|
||||
// mokoplatform manifest settings per repository.
|
||||
func AddRepoManifestTable(x *xorm.Engine) error {
|
||||
type RepoManifest struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package v1_27
|
||||
|
||||
import "xorm.io/xorm"
|
||||
|
||||
// AddAttachmentCDNPublic adds the cdn_public column to the attachment table.
|
||||
func AddAttachmentCDNPublic(x *xorm.Engine) error {
|
||||
type Attachment struct {
|
||||
CDNPublic bool `xorm:"NOT NULL DEFAULT false 'cdn_public'"`
|
||||
}
|
||||
return x.Sync(new(Attachment))
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package v1_27
|
||||
|
||||
import "xorm.io/xorm"
|
||||
|
||||
// AddManifestVersionPrefixAndElement adds version_prefix and element_name columns to repo_manifest.
|
||||
func AddManifestVersionPrefixAndElement(x *xorm.Engine) error {
|
||||
type RepoManifest struct {
|
||||
VersionPrefix string `xorm:"TEXT 'version_prefix'"`
|
||||
ElementName string `xorm:"TEXT 'element_name'"`
|
||||
}
|
||||
return x.Sync(new(RepoManifest))
|
||||
}
|
||||
@@ -31,6 +31,7 @@ type Attachment struct {
|
||||
Name string
|
||||
DownloadCount int64 `xorm:"DEFAULT 0"`
|
||||
Size int64 `xorm:"DEFAULT 0"`
|
||||
CDNPublic bool `xorm:"NOT NULL DEFAULT false 'cdn_public'"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
CustomDownloadURL string `xorm:"-"`
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@ func init() {
|
||||
db.RegisterModel(new(RepoManifest))
|
||||
}
|
||||
|
||||
// RepoManifest stores moko-platform manifest settings for a repository.
|
||||
// RepoManifest stores mokoplatform manifest settings for a repository.
|
||||
// These fields correspond to the .mokogitea/manifest.xml schema and are
|
||||
// exposed via API for use by Actions workflows and the moko-platform CLI.
|
||||
// exposed via API for use by Actions workflows and the mokoplatform CLI.
|
||||
type RepoManifest struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"UNIQUE INDEX NOT NULL 'repo_id'"`
|
||||
@@ -31,9 +31,13 @@ type RepoManifest struct {
|
||||
|
||||
// governance section
|
||||
Platform string `xorm:"VARCHAR(50) 'platform'"` // go, php, node, python, etc.
|
||||
StandardsVersion string `xorm:"VARCHAR(20) 'standards_version'"` // moko-platform standards version
|
||||
StandardsVersion string `xorm:"VARCHAR(20) 'standards_version'"` // mokoplatform standards version
|
||||
StandardsSource string `xorm:"TEXT 'standards_source'"` // URL to standards repo
|
||||
|
||||
// versioning
|
||||
VersionPrefix string `xorm:"TEXT 'version_prefix'"` // tag prefix stripped for version display, e.g. "v1.26.1-moko."
|
||||
ElementName string `xorm:"TEXT 'element_name'"` // full element name override, e.g. "pkg_mokowaas" (auto-constructed if empty)
|
||||
|
||||
// build section
|
||||
Language string `xorm:"VARCHAR(50) 'language'"` // Go, PHP, TypeScript, etc.
|
||||
PackageType string `xorm:"VARCHAR(50) 'package_type'"` // application, library, plugin, module, component, package
|
||||
@@ -47,6 +51,45 @@ func (RepoManifest) TableName() string {
|
||||
return "repo_manifest"
|
||||
}
|
||||
|
||||
// joomlaTypePrefix maps Joomla extension types to their element name prefixes.
|
||||
var joomlaTypePrefix = map[string]string{
|
||||
"component": "com_",
|
||||
"module": "mod_",
|
||||
"plugin": "plg_",
|
||||
"package": "pkg_",
|
||||
"template": "tpl_",
|
||||
"library": "lib_",
|
||||
"file": "file_",
|
||||
}
|
||||
|
||||
// AutoElementName returns the auto-constructed Joomla element name (e.g. pkg_mokowaas).
|
||||
func (m *RepoManifest) AutoElementName() string {
|
||||
if m.Name == "" || m.PackageType == "" {
|
||||
return ""
|
||||
}
|
||||
if prefix, ok := joomlaTypePrefix[m.PackageType]; ok {
|
||||
return prefix + m.Name
|
||||
}
|
||||
return m.Name
|
||||
}
|
||||
|
||||
// FullElementName returns the effective element name: override if set, otherwise auto-constructed.
|
||||
func (m *RepoManifest) FullElementName() string {
|
||||
if m.ElementName != "" {
|
||||
return m.ElementName
|
||||
}
|
||||
return m.AutoElementName()
|
||||
}
|
||||
|
||||
// ElementNameMismatch returns true if an override is set that differs from the auto-constructed name.
|
||||
func (m *RepoManifest) ElementNameMismatch() bool {
|
||||
if m.ElementName == "" {
|
||||
return false
|
||||
}
|
||||
auto := m.AutoElementName()
|
||||
return auto != "" && m.ElementName != auto
|
||||
}
|
||||
|
||||
// GetRepoManifest returns the manifest for a repo, or nil if none exists.
|
||||
func GetRepoManifest(ctx context.Context, repoID int64) (*RepoManifest, error) {
|
||||
m := new(RepoManifest)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright 2026 The MokoGitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
// AI settings
|
||||
var (
|
||||
AI = struct {
|
||||
Enabled bool
|
||||
DefaultModel string `ini:"DEFAULT_MODEL"`
|
||||
DefaultKey string `ini:"DEFAULT_API_KEY"`
|
||||
ClaudeBinPath string `ini:"CLAUDE_BIN_PATH"`
|
||||
}{
|
||||
Enabled: false,
|
||||
DefaultModel: "claude-sonnet-4-6",
|
||||
}
|
||||
)
|
||||
|
||||
func loadAIFrom(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("ai")
|
||||
AI.Enabled = sec.Key("ENABLED").MustBool(AI.Enabled)
|
||||
AI.DefaultModel = sec.Key("DEFAULT_MODEL").MustString(AI.DefaultModel)
|
||||
AI.DefaultKey = sec.Key("DEFAULT_API_KEY").String()
|
||||
AI.ClaudeBinPath = sec.Key("CLAUDE_BIN_PATH").MustString("claude")
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package setting
|
||||
|
||||
import "time"
|
||||
|
||||
// CDN holds configuration for the built-in CDN asset delivery system.
|
||||
var CDN = struct {
|
||||
Enabled bool
|
||||
Domain string // e.g. "cdn.mokoconsulting.tech"
|
||||
CacheTTL time.Duration // Cache-Control max-age for CDN responses
|
||||
AllowedOrigins []string // CORS origins allowed to fetch CDN assets
|
||||
AllowedIPs []string // IP/CIDR allowlist (empty = allow all)
|
||||
AllowedDomains []string // Referrer domain allowlist (empty = allow all)
|
||||
MaxFileSize int64 // max file size to serve (bytes)
|
||||
}{
|
||||
Enabled: false,
|
||||
Domain: "",
|
||||
CacheTTL: 24 * time.Hour,
|
||||
MaxFileSize: 100 * 1024 * 1024, // 100MB
|
||||
}
|
||||
|
||||
func loadCDNFrom(cfg ConfigProvider) {
|
||||
sec := cfg.Section("cdn")
|
||||
CDN.Enabled = sec.Key("ENABLED").MustBool(false)
|
||||
CDN.Domain = sec.Key("DOMAIN").String()
|
||||
CDN.CacheTTL = sec.Key("CACHE_TTL").MustDuration(CDN.CacheTTL)
|
||||
CDN.MaxFileSize = sec.Key("MAX_FILE_SIZE").MustInt64(CDN.MaxFileSize)
|
||||
|
||||
CDN.AllowedOrigins = sec.Key("ALLOWED_ORIGINS").Strings(",")
|
||||
CDN.AllowedIPs = sec.Key("ALLOWED_IPS").Strings(",")
|
||||
CDN.AllowedDomains = sec.Key("ALLOWED_DOMAINS").Strings(",")
|
||||
}
|
||||
@@ -178,6 +178,7 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error {
|
||||
loadOtherFrom(cfg)
|
||||
loadUpdateCheckerFrom(cfg)
|
||||
loadNtfyFrom(cfg)
|
||||
loadCDNFrom(cfg)
|
||||
loadLoginNotificationFrom(cfg)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2734,12 +2734,19 @@
|
||||
"repo.settings.support_url_help": "Shown when downloads are gated. Can point to your wiki, product page, or external support site.",
|
||||
"repo.settings.custom_fields": "Custom Fields",
|
||||
"repo.settings.manifest": "Manifest",
|
||||
"repo.settings.manifest_desc": "Project identity, governance, and build settings from the moko-platform manifest. These are accessible via API for Actions workflows and the moko-platform CLI.",
|
||||
"repo.settings.manifest_desc": "Project identity, governance, and build settings from the MokoPlatform manifest. These are accessible via API for Actions workflows and the MokoPlatform CLI.",
|
||||
"repo.settings.manifest_identity": "Identity",
|
||||
"repo.settings.manifest_name": "Project Name",
|
||||
"repo.settings.manifest_element_name": "Element Name",
|
||||
"repo.settings.manifest_element_name_help": "Base name used to construct the Joomla element identifier (e.g. 'mokowaas'). Combined with the extension type to produce the full element name.",
|
||||
"repo.settings.manifest_element_full": "Full Element Name",
|
||||
"repo.settings.manifest_element_full_help": "Auto-constructed from type + name. Leave blank to use the default, or override for non-standard naming.",
|
||||
"repo.settings.manifest_element_mismatch": "Warning: this overrides the auto-constructed name '%s'. Make sure this matches your Joomla extension's element identifier.",
|
||||
"repo.settings.manifest_package_type_help": "Maps to the Joomla extension type and determines the element prefix (com_, mod_, plg_, pkg_, tpl_, lib_, file_).",
|
||||
"repo.settings.manifest_org": "Organization",
|
||||
"repo.settings.manifest_description": "Description",
|
||||
"repo.settings.manifest_version": "Version",
|
||||
"repo.settings.manifest_version_prefix": "Version Prefix",
|
||||
"repo.settings.manifest_license_spdx": "License (SPDX)",
|
||||
"repo.settings.manifest_license_name": "License Name",
|
||||
"repo.settings.manifest_governance": "Governance",
|
||||
@@ -2831,6 +2838,8 @@
|
||||
"repo.release.message": "Describe this release",
|
||||
"repo.release.prerelease_desc": "Mark as Pre-Release",
|
||||
"repo.release.prerelease_helper": "Mark this release unsuitable for production use.",
|
||||
"repo.release.cdn_public": "CDN",
|
||||
"repo.release.cdn_public_tooltip": "Make this asset available via the CDN. Disabled when the release is assigned to an update stream.",
|
||||
"repo.release.cancel": "Cancel",
|
||||
"repo.release.publish": "Publish Release",
|
||||
"repo.release.save_draft": "Save Draft",
|
||||
@@ -2992,6 +3001,18 @@
|
||||
"org.settings.issue_priority_created": "Issue priority created.",
|
||||
"org.settings.issue_priority_updated": "Issue priority updated.",
|
||||
"org.settings.issue_priority_deleted": "Issue priority deleted.",
|
||||
"org.settings.issue_types": "Issue Types",
|
||||
"org.settings.issue_types_desc": "Define issue types for all repositories in this organization.",
|
||||
"org.settings.issue_types_empty": "No custom issue types defined yet.",
|
||||
"org.settings.issue_type_add": "Add Type",
|
||||
"org.settings.issue_type_name": "Type Name",
|
||||
"org.settings.issue_type_color": "Color",
|
||||
"org.settings.issue_type_description": "Description",
|
||||
"org.settings.issue_type_default": "Default",
|
||||
"org.settings.issue_type_sort_order": "Sort Order",
|
||||
"org.settings.issue_type_created": "Issue type created.",
|
||||
"org.settings.issue_type_updated": "Issue type updated.",
|
||||
"org.settings.issue_type_deleted": "Issue type deleted.",
|
||||
"org.settings.update_streams": "Update Server",
|
||||
"org.settings.licensing": "Update Server",
|
||||
"org.settings.licensing_desc": "Manage update feeds and optional license key gating across all repositories in this organization.",
|
||||
|
||||
@@ -19,6 +19,8 @@ type apiManifest struct {
|
||||
Version string `json:"version"`
|
||||
LicenseSPDX string `json:"license_spdx"`
|
||||
LicenseName string `json:"license_name"`
|
||||
VersionPrefix string `json:"version_prefix"`
|
||||
ElementName string `json:"element_name"`
|
||||
Platform string `json:"platform"`
|
||||
StandardsVersion string `json:"standards_version"`
|
||||
StandardsSource string `json:"standards_source"`
|
||||
@@ -60,6 +62,8 @@ func GetRepoManifest(ctx *context.APIContext) {
|
||||
Version: m.Version,
|
||||
LicenseSPDX: m.LicenseSPDX,
|
||||
LicenseName: m.LicenseName,
|
||||
VersionPrefix: m.VersionPrefix,
|
||||
ElementName: m.FullElementName(),
|
||||
Platform: m.Platform,
|
||||
StandardsVersion: m.StandardsVersion,
|
||||
StandardsSource: m.StandardsSource,
|
||||
@@ -95,6 +99,8 @@ func UpdateRepoManifest(ctx *context.APIContext) {
|
||||
Version: req.Version,
|
||||
LicenseSPDX: req.LicenseSPDX,
|
||||
LicenseName: req.LicenseName,
|
||||
VersionPrefix: req.VersionPrefix,
|
||||
ElementName: req.ElementName,
|
||||
Platform: req.Platform,
|
||||
StandardsVersion: req.StandardsVersion,
|
||||
StandardsSource: req.StandardsSource,
|
||||
@@ -115,6 +121,8 @@ func UpdateRepoManifest(ctx *context.APIContext) {
|
||||
Version: m.Version,
|
||||
LicenseSPDX: m.LicenseSPDX,
|
||||
LicenseName: m.LicenseName,
|
||||
VersionPrefix: m.VersionPrefix,
|
||||
ElementName: m.FullElementName(),
|
||||
Platform: m.Platform,
|
||||
StandardsVersion: m.StandardsVersion,
|
||||
StandardsSource: m.StandardsSource,
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
)
|
||||
|
||||
const tplOrgIssueTypes templates.TplName = "org/settings/issue_types"
|
||||
|
||||
func SettingsIssueTypes(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("org.settings.issue_types")
|
||||
ctx.Data["PageIsOrgSettings"] = true
|
||||
ctx.Data["PageIsSettingsIssueTypes"] = true
|
||||
|
||||
defs, err := issues_model.GetAllIssueTypeDefsByOrg(ctx, ctx.Org.Organization.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetAllIssueTypeDefsByOrg", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["IssueTypes"] = defs
|
||||
ctx.HTML(http.StatusOK, tplOrgIssueTypes)
|
||||
}
|
||||
|
||||
func SettingsIssueTypesCreatePost(ctx *context.Context) {
|
||||
sortOrder, _ := strconv.Atoi(ctx.FormString("sort_order"))
|
||||
def := &issues_model.IssueTypeDef{
|
||||
OrgID: ctx.Org.Organization.ID,
|
||||
Name: ctx.FormString("name"),
|
||||
Color: ctx.FormString("color"),
|
||||
Description: ctx.FormString("description"),
|
||||
SortOrder: sortOrder,
|
||||
IsDefault: ctx.FormString("is_default") == "on",
|
||||
IsActive: true,
|
||||
}
|
||||
if def.Name == "" {
|
||||
ctx.Flash.Error("Type name is required")
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-types")
|
||||
return
|
||||
}
|
||||
if err := issues_model.CreateIssueTypeDef(ctx, def); err != nil {
|
||||
ctx.ServerError("CreateIssueTypeDef", err)
|
||||
return
|
||||
}
|
||||
ctx.Flash.Success(ctx.Tr("org.settings.issue_type_created"))
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-types")
|
||||
}
|
||||
|
||||
func SettingsIssueTypesEditPost(ctx *context.Context) {
|
||||
id := ctx.PathParamInt64("id")
|
||||
def, err := issues_model.GetIssueTypeDefByID(ctx, id)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetIssueTypeDefByID", err)
|
||||
return
|
||||
}
|
||||
if def.OrgID != ctx.Org.Organization.ID {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
def.Name = ctx.FormString("name")
|
||||
def.Color = ctx.FormString("color")
|
||||
def.Description = ctx.FormString("description")
|
||||
def.IsDefault = ctx.FormString("is_default") == "on"
|
||||
def.IsActive = ctx.FormString("is_active") == "on"
|
||||
sortOrder, _ := strconv.Atoi(ctx.FormString("sort_order"))
|
||||
def.SortOrder = sortOrder
|
||||
if err := issues_model.UpdateIssueTypeDef(ctx, def); err != nil {
|
||||
ctx.ServerError("UpdateIssueTypeDef", err)
|
||||
return
|
||||
}
|
||||
ctx.Flash.Success(ctx.Tr("org.settings.issue_type_updated"))
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-types")
|
||||
}
|
||||
|
||||
func SettingsIssueTypesDeletePost(ctx *context.Context) {
|
||||
id := ctx.PathParamInt64("id")
|
||||
def, err := issues_model.GetIssueTypeDefByID(ctx, id)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetIssueTypeDefByID", err)
|
||||
return
|
||||
}
|
||||
if def.OrgID != ctx.Org.Organization.ID {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
if err := issues_model.DeleteIssueTypeDef(ctx, id); err != nil {
|
||||
ctx.ServerError("DeleteIssueTypeDef", err)
|
||||
return
|
||||
}
|
||||
ctx.Flash.Success(ctx.Tr("org.settings.issue_type_deleted"))
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-types")
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
licenses_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/httplib"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/storage"
|
||||
)
|
||||
|
||||
// CDNHandler serves release assets via the CDN hostname.
|
||||
// URL format: /:owner/:repo/releases/:tag/:filename
|
||||
// Only assets with cdn_public=true are served.
|
||||
func CDNHandler(w http.ResponseWriter, req *http.Request) {
|
||||
if !setting.CDN.Enabled {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
if !cdnCheckIPAllowed(req) {
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if !cdnCheckReferrerAllowed(req) {
|
||||
http.Error(w, "Forbidden: referrer not allowed", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse: /:owner/:repo/releases/:tag/:filename
|
||||
urlPath := strings.TrimPrefix(req.URL.Path, "/")
|
||||
parts := strings.SplitN(urlPath, "/", 6)
|
||||
|
||||
// Minimum: owner/repo/releases/tag/filename = 5 parts
|
||||
if len(parts) < 5 || parts[2] != "releases" {
|
||||
http.Error(w, "Not Found: expected /:owner/:repo/releases/:tag/:filename", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
ownerName := parts[0]
|
||||
repoName := parts[1]
|
||||
tagName := parts[3]
|
||||
fileName := parts[4]
|
||||
// Allow filenames with slashes (parts[5] if present)
|
||||
if len(parts) == 6 {
|
||||
fileName = parts[4] + "/" + parts[5]
|
||||
}
|
||||
|
||||
// Load repository
|
||||
repo, err := repo_model.GetRepositoryByOwnerAndName(req.Context(), ownerName, repoName)
|
||||
if err != nil {
|
||||
if repo_model.IsErrRepoNotExist(err) {
|
||||
http.NotFound(w, req)
|
||||
} else {
|
||||
log.Error("CDN: GetRepositoryByOwnerAndName %s/%s: %v", ownerName, repoName, err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Look up the release by tag
|
||||
release, err := repo_model.GetRelease(req.Context(), repo.ID, tagName)
|
||||
if err != nil {
|
||||
if repo_model.IsErrReleaseNotExist(err) {
|
||||
http.NotFound(w, req)
|
||||
} else {
|
||||
log.Error("CDN: GetRelease %s/%s tag=%s: %v", ownerName, repoName, tagName, err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Don't serve draft releases via CDN
|
||||
if release.IsDraft {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
// If the release is assigned to an update stream, CDN is disabled -
|
||||
// the update server handles distribution for streamed releases.
|
||||
if stream := licenses_model.GetReleaseStream(req.Context(), release.ID); stream != "" {
|
||||
http.Error(w, "Forbidden: release is served via update stream", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Find the specific attachment by filename
|
||||
attach, err := repo_model.GetAttachmentByReleaseIDFileName(req.Context(), release.ID, fileName)
|
||||
if err != nil || attach == nil {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
// Only serve assets marked as CDN public
|
||||
if !attach.CDNPublic {
|
||||
http.Error(w, "Forbidden: asset is not CDN-enabled", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Check file size limit
|
||||
if setting.CDN.MaxFileSize > 0 && attach.Size > setting.CDN.MaxFileSize {
|
||||
http.Error(w, "File too large for CDN delivery", http.StatusRequestEntityTooLarge)
|
||||
return
|
||||
}
|
||||
|
||||
// CORS headers
|
||||
if len(setting.CDN.AllowedOrigins) > 0 {
|
||||
origin := req.Header.Get("Origin")
|
||||
for _, allowed := range setting.CDN.AllowedOrigins {
|
||||
if allowed == "*" || allowed == origin {
|
||||
w.Header().Set("Access-Control-Allow-Origin", allowed)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
}
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS")
|
||||
w.Header().Set("Access-Control-Max-Age", "86400")
|
||||
|
||||
if req.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
// ETag based on attachment UUID (immutable for same content)
|
||||
etag := `"` + attach.UUID + `"`
|
||||
w.Header().Set("Etag", etag)
|
||||
|
||||
// 304 Not Modified check
|
||||
if inm := req.Header.Get("If-None-Match"); inm != "" {
|
||||
for item := range strings.SplitSeq(inm, ",") {
|
||||
item = strings.TrimPrefix(strings.TrimSpace(item), "W/")
|
||||
if item == etag {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Last-Modified
|
||||
lastModified := attach.CreatedUnix.AsTimePtr()
|
||||
if lastModified != nil {
|
||||
w.Header().Set("Last-Modified", lastModified.UTC().Format(http.TimeFormat))
|
||||
}
|
||||
|
||||
// CDN cache headers
|
||||
cacheTTL := int(setting.CDN.CacheTTL.Seconds())
|
||||
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d, no-transform", cacheTTL))
|
||||
|
||||
// Increment download count
|
||||
if err := attach.IncreaseDownloadCount(req.Context()); err != nil {
|
||||
log.Error("CDN: IncreaseDownloadCount: %v", err)
|
||||
}
|
||||
|
||||
// Try direct storage URL (S3/object storage)
|
||||
if setting.Attachment.Storage.ServeDirect() {
|
||||
u, err := storage.Attachments.ServeDirectURL(attach.RelativePath(), attach.Name, req.Method, nil)
|
||||
if u != nil && err == nil {
|
||||
http.Redirect(w, req, u.String(), http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Serve from local storage
|
||||
fr, err := storage.Attachments.Open(attach.RelativePath())
|
||||
if err != nil {
|
||||
log.Error("CDN: storage.Open %s: %v", attach.RelativePath(), err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer fr.Close()
|
||||
|
||||
httplib.ServeUserContentByFile(req, w, fr, httplib.ServeHeaderOptions{
|
||||
Filename: attach.Name,
|
||||
CacheIsPublic: true,
|
||||
CacheDuration: setting.CDN.CacheTTL,
|
||||
})
|
||||
}
|
||||
|
||||
// cdnCheckIPAllowed checks if the request IP is in the configured allowlist.
|
||||
func cdnCheckIPAllowed(req *http.Request) bool {
|
||||
if len(setting.CDN.AllowedIPs) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
remoteIP := cdnGetRemoteIP(req)
|
||||
if remoteIP == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, cidr := range setting.CDN.AllowedIPs {
|
||||
cidr = strings.TrimSpace(cidr)
|
||||
if cidr == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.Contains(cidr, "/") {
|
||||
if remoteIP.Equal(net.ParseIP(cidr)) {
|
||||
return true
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
_, network, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
log.Warn("CDN: invalid CIDR in AllowedIPs: %s", cidr)
|
||||
continue
|
||||
}
|
||||
if network.Contains(remoteIP) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// cdnCheckReferrerAllowed checks if the request referrer domain is allowed.
|
||||
func cdnCheckReferrerAllowed(req *http.Request) bool {
|
||||
if len(setting.CDN.AllowedDomains) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
referer := req.Header.Get("Referer")
|
||||
if referer == "" {
|
||||
return true // direct requests always allowed
|
||||
}
|
||||
|
||||
for _, domain := range setting.CDN.AllowedDomains {
|
||||
domain = strings.TrimSpace(strings.ToLower(domain))
|
||||
if domain == "" {
|
||||
continue
|
||||
}
|
||||
if domain == "*" {
|
||||
return true
|
||||
}
|
||||
refLower := strings.ToLower(referer)
|
||||
if strings.Contains(refLower, "://"+domain+"/") || strings.Contains(refLower, "://"+domain+":") ||
|
||||
strings.HasSuffix(refLower, "://"+domain) {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(domain, "*.") {
|
||||
baseDomain := domain[2:]
|
||||
if strings.Contains(refLower, "."+baseDomain+"/") || strings.Contains(refLower, "."+baseDomain+":") ||
|
||||
strings.HasSuffix(refLower, "."+baseDomain) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// cdnGetRemoteIP extracts the client IP, checking proxy headers.
|
||||
func cdnGetRemoteIP(req *http.Request) net.IP {
|
||||
if xff := req.Header.Get("X-Forwarded-For"); xff != "" {
|
||||
parts := strings.SplitN(xff, ",", 2)
|
||||
if ip := net.ParseIP(strings.TrimSpace(parts[0])); ip != nil {
|
||||
return ip
|
||||
}
|
||||
}
|
||||
if xri := req.Header.Get("X-Real-IP"); xri != "" {
|
||||
if ip := net.ParseIP(strings.TrimSpace(xri)); ip != nil {
|
||||
return ip
|
||||
}
|
||||
}
|
||||
host, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||
if err != nil {
|
||||
return net.ParseIP(req.RemoteAddr)
|
||||
}
|
||||
return net.ParseIP(host)
|
||||
}
|
||||
@@ -596,7 +596,10 @@ func EditRelease(ctx *context.Context) {
|
||||
ctx.Data["content"] = rel.Note
|
||||
ctx.Data["prerelease"] = rel.IsPrerelease
|
||||
ctx.Data["IsDraft"] = rel.IsDraft
|
||||
ctx.Data["ReleaseStream"] = licenses_model.GetReleaseStream(ctx, rel.ID)
|
||||
releaseStream := licenses_model.GetReleaseStream(ctx, rel.ID)
|
||||
ctx.Data["ReleaseStream"] = releaseStream
|
||||
ctx.Data["ReleaseHasStream"] = releaseStream != ""
|
||||
ctx.Data["CDNEnabled"] = setting.CDN.Enabled
|
||||
|
||||
rel.Repo = ctx.Repo.Repository
|
||||
if err := rel.LoadAttributes(ctx); err != nil {
|
||||
@@ -683,6 +686,28 @@ func EditReleasePost(ctx *context.Context) {
|
||||
} else {
|
||||
_ = licenses_model.DeleteReleaseStream(ctx, rel.ID)
|
||||
}
|
||||
|
||||
// Update per-asset CDN visibility flags.
|
||||
if setting.CDN.Enabled {
|
||||
const cdnPrefix = "attachment-cdn-"
|
||||
cdnUUIDs := make(map[string]bool)
|
||||
for k := range ctx.Req.Form {
|
||||
if strings.HasPrefix(k, cdnPrefix) {
|
||||
cdnUUIDs[k[len(cdnPrefix):]] = true
|
||||
}
|
||||
}
|
||||
// Load all attachments for this release to update cdn_public
|
||||
if err := repo_model.GetReleaseAttachments(ctx, rel); err == nil {
|
||||
for _, attach := range rel.Attachments {
|
||||
wantCDN := cdnUUIDs[attach.UUID]
|
||||
if attach.CDNPublic != wantCDN {
|
||||
attach.CDNPublic = wantCDN
|
||||
_ = repo_model.UpdateAttachmentByUUID(ctx, attach, "cdn_public")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/releases")
|
||||
}
|
||||
|
||||
|
||||
@@ -18,18 +18,19 @@ const tplSettingsManifest templates.TplName = "repo/settings/manifest"
|
||||
|
||||
// manifestXML mirrors the .mokogitea/manifest.xml schema for XML parsing.
|
||||
type manifestXML struct {
|
||||
XMLName xml.Name `xml:"moko-platform"`
|
||||
XMLName xml.Name `xml:"mokoplatform"`
|
||||
Identity manifestIdentity `xml:"identity"`
|
||||
Governance manifestGovernance `xml:"governance"`
|
||||
Build manifestBuild `xml:"build"`
|
||||
}
|
||||
|
||||
type manifestIdentity struct {
|
||||
Name string `xml:"name"`
|
||||
Org string `xml:"org"`
|
||||
Description string `xml:"description"`
|
||||
Version string `xml:"version"`
|
||||
License manifestLicense `xml:"license"`
|
||||
Name string `xml:"name"`
|
||||
Org string `xml:"org"`
|
||||
Description string `xml:"description"`
|
||||
Version string `xml:"version"`
|
||||
VersionPrefix string `xml:"version-prefix"`
|
||||
License manifestLicense `xml:"license"`
|
||||
}
|
||||
|
||||
type manifestLicense struct {
|
||||
@@ -88,10 +89,12 @@ func ManifestSettingsPost(ctx *context.Context) {
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Name: ctx.FormString("name"),
|
||||
Org: ctx.FormString("org"),
|
||||
Description: ctx.FormString("description"),
|
||||
Description: ctx.Repo.Repository.Description,
|
||||
Version: ctx.FormString("version"),
|
||||
LicenseSPDX: ctx.FormString("license_spdx"),
|
||||
LicenseName: ctx.FormString("license_name"),
|
||||
VersionPrefix: ctx.FormString("version_prefix"),
|
||||
ElementName: ctx.FormString("element_name"),
|
||||
Platform: ctx.FormString("platform"),
|
||||
StandardsVersion: ctx.FormString("standards_version"),
|
||||
StandardsSource: ctx.FormString("standards_source"),
|
||||
@@ -142,6 +145,7 @@ func tryMigrateManifestXML(ctx *context.Context) *repo_model.RepoManifest {
|
||||
Version: mxml.Identity.Version,
|
||||
LicenseSPDX: mxml.Identity.License.SPDX,
|
||||
LicenseName: mxml.Identity.License.Name,
|
||||
VersionPrefix: mxml.Identity.VersionPrefix,
|
||||
Platform: mxml.Governance.Platform,
|
||||
StandardsVersion: mxml.Governance.StandardsVersion,
|
||||
StandardsSource: mxml.Governance.StandardsSource,
|
||||
|
||||
@@ -260,6 +260,20 @@ func Routes() *web.Router {
|
||||
// GetHead allows a HEAD request redirect to GET if HEAD method is not defined for that route
|
||||
routes.BeforeRouting(chi_middleware.GetHead)
|
||||
|
||||
// CDN hostname handler - intercepts requests on the CDN domain before any
|
||||
// session/auth middleware runs, serving only CDN-public release assets.
|
||||
if setting.CDN.Enabled && setting.CDN.Domain != "" {
|
||||
routes.BeforeRouting(func(resp http.ResponseWriter, req *http.Request) {
|
||||
host := req.Host
|
||||
if idx := strings.Index(host, ":"); idx >= 0 {
|
||||
host = host[:idx]
|
||||
}
|
||||
if strings.EqualFold(host, setting.CDN.Domain) {
|
||||
repo.CDNHandler(resp, req)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
routes.Head("/", misc.DummyOK) // for health check - doesn't need to be passed through gzip handler
|
||||
routes.Methods("GET, HEAD", "/assets/site-manifest.json", misc.SiteManifest)
|
||||
routes.Methods("GET, HEAD, OPTIONS", "/assets/*", routing.MarkLogLevelTrace, public.AssetsCors(), public.FileHandlerFunc())
|
||||
@@ -1079,6 +1093,12 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
m.Post("/{id}/edit", org.SettingsIssuePrioritiesEditPost)
|
||||
m.Post("/{id}/delete", org.SettingsIssuePrioritiesDeletePost)
|
||||
})
|
||||
m.Group("/issue-types", func() {
|
||||
m.Get("", org.SettingsIssueTypes)
|
||||
m.Post("", org.SettingsIssueTypesCreatePost)
|
||||
m.Post("/{id}/edit", org.SettingsIssueTypesEditPost)
|
||||
m.Post("/{id}/delete", org.SettingsIssueTypesDeletePost)
|
||||
})
|
||||
}, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "PageIsOrgSettings", true))
|
||||
}, context.OrgAssignment(context.OrgAssignmentOptions{RequireOwner: true}))
|
||||
}, reqSignIn)
|
||||
|
||||
@@ -16,8 +16,11 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
auth_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/auth"
|
||||
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/httplib"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/optional"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/auth/source/oauth2"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
|
||||
@@ -166,6 +169,18 @@ func (ctx *Context) notFoundInternal(logMsg string, logErr error) {
|
||||
ctx.Data["IsRepo"] = ctx.Repo.Repository != nil
|
||||
ctx.Data["Title"] = "Page Not Found"
|
||||
ctx.Data["ErrorMsg"] = "" // FIXME: the template never renders this message, need to fix in the future (and show safe messages to end users)
|
||||
ctx.Data["CurrentURL"] = ctx.Req.URL.RequestURI()
|
||||
|
||||
// Load OAuth2 providers for the login form on error pages
|
||||
if !ctx.IsSigned {
|
||||
oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
|
||||
if err != nil {
|
||||
log.Error("NotFound: GetOAuth2Providers: %v", err)
|
||||
}
|
||||
ctx.Data["OAuth2Providers"] = oauth2Providers
|
||||
ctx.Data["EnableSSPI"] = auth_model.IsSSPIEnabled(ctx)
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusNotFound, "status/404")
|
||||
}
|
||||
|
||||
@@ -187,6 +202,17 @@ func (ctx *Context) Forbidden() {
|
||||
ctx.Data["IsRepo"] = ctx.Repo.Repository != nil
|
||||
ctx.Data["Title"] = "Access Denied"
|
||||
ctx.Data["CurrentURL"] = ctx.Req.URL.RequestURI()
|
||||
|
||||
// Load OAuth2 providers for the login form on the 403 page
|
||||
if !ctx.IsSigned {
|
||||
oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
|
||||
if err != nil {
|
||||
log.Error("Forbidden: GetOAuth2Providers: %v", err)
|
||||
}
|
||||
ctx.Data["OAuth2Providers"] = oauth2Providers
|
||||
ctx.Data["EnableSSPI"] = auth_model.IsSSPIEnabled(ctx)
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusForbidden, "status/403")
|
||||
}
|
||||
|
||||
|
||||
@@ -14,18 +14,20 @@ import (
|
||||
|
||||
// manifestXML mirrors the .mokogitea/manifest.xml schema for XML parsing.
|
||||
type manifestXML struct {
|
||||
XMLName xml.Name `xml:"moko-platform"`
|
||||
XMLName xml.Name `xml:"mokoplatform"`
|
||||
Identity manifestIdentity `xml:"identity"`
|
||||
Governance manifestGovernance `xml:"governance"`
|
||||
Build manifestBuild `xml:"build"`
|
||||
}
|
||||
|
||||
type manifestIdentity struct {
|
||||
Name string `xml:"name"`
|
||||
Org string `xml:"org"`
|
||||
Description string `xml:"description"`
|
||||
Version string `xml:"version"`
|
||||
License manifestLicense `xml:"license"`
|
||||
Name string `xml:"name"`
|
||||
Org string `xml:"org"`
|
||||
Description string `xml:"description"`
|
||||
Version string `xml:"version"`
|
||||
VersionPrefix string `xml:"version-prefix"`
|
||||
ElementName string `xml:"element-name"`
|
||||
License manifestLicense `xml:"license"`
|
||||
}
|
||||
|
||||
type manifestLicense struct {
|
||||
@@ -79,6 +81,8 @@ func SyncManifestFromCommit(ctx context.Context, repo *repo_model.Repository, co
|
||||
Org: mxml.Identity.Org,
|
||||
Description: mxml.Identity.Description,
|
||||
Version: mxml.Identity.Version,
|
||||
VersionPrefix: mxml.Identity.VersionPrefix,
|
||||
ElementName: mxml.Identity.ElementName,
|
||||
LicenseSPDX: mxml.Identity.License.SPDX,
|
||||
LicenseName: mxml.Identity.License.Name,
|
||||
Platform: mxml.Governance.Platform,
|
||||
|
||||
@@ -0,0 +1,541 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
security_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/security"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
||||
)
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Dependency manifest parsers
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
// dependency represents a single package with version.
|
||||
type dependency struct {
|
||||
Name string
|
||||
Version string
|
||||
Ecosystem string // "Go", "npm", "PyPI", "Packagist"
|
||||
FilePath string // which manifest file it came from
|
||||
}
|
||||
|
||||
// manifestParser extracts dependencies from a file's contents.
|
||||
type manifestParser struct {
|
||||
FileName string
|
||||
Ecosystem string
|
||||
Parse func(content string, filePath string) []dependency
|
||||
}
|
||||
|
||||
var manifestParsers = []manifestParser{
|
||||
{"go.mod", "Go", parseGoMod},
|
||||
{"package.json", "npm", parsePackageJSON},
|
||||
{"composer.json", "Packagist", parseComposerJSON},
|
||||
{"requirements.txt", "PyPI", parseRequirementsTxt},
|
||||
}
|
||||
|
||||
// parseGoMod extracts dependencies from go.mod.
|
||||
func parseGoMod(content, filePath string) []dependency {
|
||||
var deps []dependency
|
||||
inRequire := false
|
||||
|
||||
for _, line := range strings.Split(content, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
if line == ")" {
|
||||
inRequire = false
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, "require (") || strings.HasPrefix(line, "require(") {
|
||||
inRequire = true
|
||||
continue
|
||||
}
|
||||
|
||||
if inRequire {
|
||||
// Lines like: github.com/foo/bar v1.2.3
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) >= 2 && !strings.HasPrefix(parts[0], "//") {
|
||||
deps = append(deps, dependency{
|
||||
Name: parts[0],
|
||||
Version: parts[1],
|
||||
Ecosystem: "Go",
|
||||
FilePath: filePath,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Single-line require: require github.com/foo/bar v1.2.3
|
||||
if strings.HasPrefix(line, "require ") && !strings.Contains(line, "(") {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) >= 3 {
|
||||
deps = append(deps, dependency{
|
||||
Name: parts[1],
|
||||
Version: parts[2],
|
||||
Ecosystem: "Go",
|
||||
FilePath: filePath,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return deps
|
||||
}
|
||||
|
||||
// parsePackageJSON extracts dependencies from package.json.
|
||||
func parsePackageJSON(content, filePath string) []dependency {
|
||||
var pkg struct {
|
||||
Dependencies map[string]string `json:"dependencies"`
|
||||
DevDependencies map[string]string `json:"devDependencies"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(content), &pkg); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var deps []dependency
|
||||
for name, version := range pkg.Dependencies {
|
||||
deps = append(deps, dependency{
|
||||
Name: name,
|
||||
Version: cleanSemver(version),
|
||||
Ecosystem: "npm",
|
||||
FilePath: filePath,
|
||||
})
|
||||
}
|
||||
for name, version := range pkg.DevDependencies {
|
||||
deps = append(deps, dependency{
|
||||
Name: name,
|
||||
Version: cleanSemver(version),
|
||||
Ecosystem: "npm",
|
||||
FilePath: filePath,
|
||||
})
|
||||
}
|
||||
return deps
|
||||
}
|
||||
|
||||
// parseComposerJSON extracts dependencies from composer.json.
|
||||
func parseComposerJSON(content, filePath string) []dependency {
|
||||
var pkg struct {
|
||||
Require map[string]string `json:"require"`
|
||||
RequireDev map[string]string `json:"require-dev"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(content), &pkg); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var deps []dependency
|
||||
for name, version := range pkg.Require {
|
||||
if name == "php" || strings.HasPrefix(name, "ext-") {
|
||||
continue // skip platform requirements
|
||||
}
|
||||
deps = append(deps, dependency{
|
||||
Name: name,
|
||||
Version: cleanSemver(version),
|
||||
Ecosystem: "Packagist",
|
||||
FilePath: filePath,
|
||||
})
|
||||
}
|
||||
for name, version := range pkg.RequireDev {
|
||||
if name == "php" || strings.HasPrefix(name, "ext-") {
|
||||
continue
|
||||
}
|
||||
deps = append(deps, dependency{
|
||||
Name: name,
|
||||
Version: cleanSemver(version),
|
||||
Ecosystem: "Packagist",
|
||||
FilePath: filePath,
|
||||
})
|
||||
}
|
||||
return deps
|
||||
}
|
||||
|
||||
// parseRequirementsTxt extracts dependencies from requirements.txt.
|
||||
func parseRequirementsTxt(content, filePath string) []dependency {
|
||||
var deps []dependency
|
||||
for _, line := range strings.Split(content, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "-") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle: package==1.0.0, package>=1.0.0, package~=1.0.0
|
||||
for _, sep := range []string{"==", ">=", "~=", "<=", "!="} {
|
||||
if idx := strings.Index(line, sep); idx > 0 {
|
||||
name := strings.TrimSpace(line[:idx])
|
||||
version := strings.TrimSpace(line[idx+len(sep):])
|
||||
// Strip any trailing constraints like ",<2.0"
|
||||
if ci := strings.Index(version, ","); ci > 0 {
|
||||
version = version[:ci]
|
||||
}
|
||||
deps = append(deps, dependency{
|
||||
Name: name,
|
||||
Version: version,
|
||||
Ecosystem: "PyPI",
|
||||
FilePath: filePath,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return deps
|
||||
}
|
||||
|
||||
// cleanSemver strips npm/composer range prefixes (^, ~, >=) to get a plain version.
|
||||
func cleanSemver(v string) string {
|
||||
v = strings.TrimSpace(v)
|
||||
v = strings.TrimLeft(v, "^~>=<!")
|
||||
v = strings.TrimSpace(v)
|
||||
// If it has " || " or " - " (ranges), take the first version
|
||||
if idx := strings.Index(v, " "); idx > 0 {
|
||||
v = v[:idx]
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// OSV.dev API client
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
const osvBatchURL = "https://api.osv.dev/v1/querybatch"
|
||||
const osvMaxBatch = 1000 // OSV batch limit
|
||||
|
||||
var osvClient = &http.Client{Timeout: 30 * time.Second}
|
||||
|
||||
// osvQuery is a single query in a batch request.
|
||||
type osvQuery struct {
|
||||
Package *osvPackage `json:"package"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type osvPackage struct {
|
||||
Name string `json:"name"`
|
||||
Ecosystem string `json:"ecosystem"`
|
||||
}
|
||||
|
||||
// osvBatchRequest is the batch query body.
|
||||
type osvBatchRequest struct {
|
||||
Queries []osvQuery `json:"queries"`
|
||||
}
|
||||
|
||||
// osvBatchResponse is the batch response.
|
||||
type osvBatchResponse struct {
|
||||
Results []osvResult `json:"results"`
|
||||
}
|
||||
|
||||
type osvResult struct {
|
||||
Vulns []osvVuln `json:"vulns"`
|
||||
}
|
||||
|
||||
type osvVuln struct {
|
||||
ID string `json:"id"`
|
||||
Summary string `json:"summary"`
|
||||
Details string `json:"details"`
|
||||
Severity []osvSeverity `json:"severity"`
|
||||
Aliases []string `json:"aliases"`
|
||||
}
|
||||
|
||||
type osvSeverity struct {
|
||||
Type string `json:"type"` // "CVSS_V3", "CVSS_V2"
|
||||
Score string `json:"score"` // CVSS vector string
|
||||
}
|
||||
|
||||
// queryOSV sends a batch of dependencies to OSV.dev and returns vulnerabilities.
|
||||
func queryOSV(deps []dependency) (*osvBatchResponse, error) {
|
||||
queries := make([]osvQuery, 0, len(deps))
|
||||
for _, d := range deps {
|
||||
if d.Version == "" || d.Version == "*" || d.Version == "latest" {
|
||||
continue // can't query without a concrete version
|
||||
}
|
||||
queries = append(queries, osvQuery{
|
||||
Package: &osvPackage{Name: d.Name, Ecosystem: d.Ecosystem},
|
||||
Version: d.Version,
|
||||
})
|
||||
}
|
||||
|
||||
if len(queries) == 0 {
|
||||
return &osvBatchResponse{}, nil
|
||||
}
|
||||
|
||||
body, err := json.Marshal(osvBatchRequest{Queries: queries})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal OSV request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := osvClient.Post(osvBatchURL, "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("OSV API request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
|
||||
return nil, fmt.Errorf("OSV API returned %d: %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
|
||||
var result osvBatchResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("decode OSV response: %w", err)
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Severity mapping
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
// mapCVSSSeverity converts a CVSS v3 base score to an AlertSeverity.
|
||||
func mapCVSSSeverity(vulnSeverities []osvSeverity) security_model.AlertSeverity {
|
||||
for _, s := range vulnSeverities {
|
||||
if s.Type == "CVSS_V3" {
|
||||
score := extractCVSSBaseScore(s.Score)
|
||||
switch {
|
||||
case score >= 9.0:
|
||||
return security_model.SeverityCritical
|
||||
case score >= 7.0:
|
||||
return security_model.SeverityHigh
|
||||
case score >= 4.0:
|
||||
return security_model.SeverityMedium
|
||||
case score > 0:
|
||||
return security_model.SeverityLow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No CVSS score available - default to medium
|
||||
return security_model.SeverityMedium
|
||||
}
|
||||
|
||||
// extractCVSSBaseScore parses the base score from a CVSS v3 vector string.
|
||||
// Vector format: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
|
||||
// We compute a simplified score from the vector metrics.
|
||||
func extractCVSSBaseScore(vector string) float64 {
|
||||
if vector == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
// CVSS v3 vectors encode severity in metrics. Use a simplified
|
||||
// lookup based on the most impactful metrics.
|
||||
parts := make(map[string]string)
|
||||
for _, segment := range strings.Split(vector, "/") {
|
||||
kv := strings.SplitN(segment, ":", 2)
|
||||
if len(kv) == 2 {
|
||||
parts[kv[0]] = kv[1]
|
||||
}
|
||||
}
|
||||
|
||||
// Simplified scoring based on key CVSS v3 metrics
|
||||
var score float64
|
||||
|
||||
// Attack Vector (AV)
|
||||
switch parts["AV"] {
|
||||
case "N": // Network
|
||||
score += 3.0
|
||||
case "A": // Adjacent
|
||||
score += 2.0
|
||||
case "L": // Local
|
||||
score += 1.0
|
||||
case "P": // Physical
|
||||
score += 0.5
|
||||
}
|
||||
|
||||
// Attack Complexity (AC)
|
||||
switch parts["AC"] {
|
||||
case "L": // Low
|
||||
score += 1.5
|
||||
case "H": // High
|
||||
score += 0.5
|
||||
}
|
||||
|
||||
// Privileges Required (PR)
|
||||
switch parts["PR"] {
|
||||
case "N": // None
|
||||
score += 1.5
|
||||
case "L": // Low
|
||||
score += 1.0
|
||||
case "H": // High
|
||||
score += 0.5
|
||||
}
|
||||
|
||||
// Impact metrics (C/I/A)
|
||||
for _, metric := range []string{"C", "I", "A"} {
|
||||
switch parts[metric] {
|
||||
case "H":
|
||||
score += 1.2
|
||||
case "L":
|
||||
score += 0.5
|
||||
}
|
||||
}
|
||||
|
||||
// Cap at 10.0
|
||||
if score > 10.0 {
|
||||
score = 10.0
|
||||
}
|
||||
return score
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// DependencyScanner
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
// DependencyScanner checks project dependencies against known vulnerabilities.
|
||||
type DependencyScanner struct{}
|
||||
|
||||
// NewDependencyScanner creates a new dependency vulnerability scanner.
|
||||
func NewDependencyScanner() *DependencyScanner {
|
||||
return &DependencyScanner{}
|
||||
}
|
||||
|
||||
func (s *DependencyScanner) Type() security_model.ScannerType {
|
||||
return security_model.ScannerDependency
|
||||
}
|
||||
|
||||
func (s *DependencyScanner) ScanCommit(commit *git.Commit) ([]Finding, error) {
|
||||
return s.ScanTree(commit)
|
||||
}
|
||||
|
||||
func (s *DependencyScanner) ScanTree(commit *git.Commit) ([]Finding, error) {
|
||||
if commit == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Step 1: Find and parse manifest files
|
||||
entries, err := commit.ListEntriesRecursiveFast()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ListEntriesRecursiveFast: %w", err)
|
||||
}
|
||||
|
||||
var allDeps []dependency
|
||||
for _, entry := range entries {
|
||||
if !entry.IsRegular() {
|
||||
continue
|
||||
}
|
||||
|
||||
path := entry.Name()
|
||||
baseName := path
|
||||
if idx := strings.LastIndex(path, "/"); idx >= 0 {
|
||||
baseName = path[idx+1:]
|
||||
}
|
||||
|
||||
// Skip vendored/nested files
|
||||
lower := strings.ToLower(path)
|
||||
if strings.Contains(lower, "vendor/") || strings.Contains(lower, "node_modules/") ||
|
||||
strings.Contains(lower, "testdata/") {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, parser := range manifestParsers {
|
||||
if baseName == parser.FileName {
|
||||
reader, err := entry.Blob().DataAsync()
|
||||
if err != nil {
|
||||
log.Trace("DependencyScanner: skip %s: %v", path, err)
|
||||
continue
|
||||
}
|
||||
content, err := io.ReadAll(io.LimitReader(reader, 5*1024*1024)) // 5MB limit
|
||||
reader.Close()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
deps := parser.Parse(string(content), path)
|
||||
allDeps = append(allDeps, deps...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(allDeps) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
log.Info("DependencyScanner: found %d dependencies across manifest files", len(allDeps))
|
||||
|
||||
// Step 2: Query OSV in batches
|
||||
var findings []Finding
|
||||
for i := 0; i < len(allDeps); i += osvMaxBatch {
|
||||
end := i + osvMaxBatch
|
||||
if end > len(allDeps) {
|
||||
end = len(allDeps)
|
||||
}
|
||||
batch := allDeps[i:end]
|
||||
|
||||
resp, err := queryOSV(batch)
|
||||
if err != nil {
|
||||
log.Error("DependencyScanner: OSV query failed: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Step 3: Map results to findings
|
||||
// OSV batch response indices correspond 1:1 with the query indices.
|
||||
// But we may have skipped deps with empty versions, so build the
|
||||
// queryable subset to align indices.
|
||||
queryable := make([]dependency, 0, len(batch))
|
||||
for _, d := range batch {
|
||||
if d.Version != "" && d.Version != "*" && d.Version != "latest" {
|
||||
queryable = append(queryable, d)
|
||||
}
|
||||
}
|
||||
|
||||
for j, result := range resp.Results {
|
||||
if j >= len(queryable) {
|
||||
break
|
||||
}
|
||||
dep := queryable[j]
|
||||
|
||||
for _, vuln := range result.Vulns {
|
||||
severity := mapCVSSSeverity(vuln.Severity)
|
||||
|
||||
// Build CVE alias for rule ID (prefer CVE over GHSA)
|
||||
ruleID := vuln.ID
|
||||
for _, alias := range vuln.Aliases {
|
||||
if strings.HasPrefix(alias, "CVE-") {
|
||||
ruleID = alias
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
title := fmt.Sprintf("%s in %s@%s", ruleID, dep.Name, dep.Version)
|
||||
|
||||
description := vuln.Summary
|
||||
if description == "" {
|
||||
description = vuln.Details
|
||||
}
|
||||
// Truncate long descriptions
|
||||
if len(description) > 500 {
|
||||
description = description[:497] + "..."
|
||||
}
|
||||
|
||||
// Metadata JSON
|
||||
meta, _ := json.Marshal(map[string]string{
|
||||
"vuln_id": vuln.ID,
|
||||
"ecosystem": dep.Ecosystem,
|
||||
"package": dep.Name,
|
||||
"version": dep.Version,
|
||||
})
|
||||
|
||||
fingerprint := fmt.Sprintf("%x", sha256.Sum256([]byte(vuln.ID+":"+dep.Name+":"+dep.Version)))
|
||||
|
||||
findings = append(findings, Finding{
|
||||
Scanner: security_model.ScannerDependency,
|
||||
Severity: severity,
|
||||
RuleID: ruleID,
|
||||
Title: title,
|
||||
Description: description,
|
||||
FilePath: dep.FilePath,
|
||||
CommitSHA: commit.ID.String(),
|
||||
Fingerprint: fingerprint[:32],
|
||||
Metadata: string(meta),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return findings, nil
|
||||
}
|
||||
@@ -32,8 +32,10 @@ func ScanOnPush(ctx context.Context, repo *repo_model.Repository, commit *git.Co
|
||||
if cfg.SecretScanner {
|
||||
scanners = append(scanners, NewSecretScanner())
|
||||
}
|
||||
if cfg.DependScanner {
|
||||
scanners = append(scanners, NewDependencyScanner())
|
||||
}
|
||||
// Future scanners added here:
|
||||
// if cfg.DependScanner { scanners = append(scanners, NewDependencyScanner()) }
|
||||
// if cfg.CodeScanner { scanners = append(scanners, NewCodeScanner()) }
|
||||
|
||||
if len(scanners) == 0 {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings issue-types")}}
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "org.settings.issue_types"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<p class="text grey">{{ctx.Locale.Tr "org.settings.issue_types_desc"}}</p>
|
||||
|
||||
{{if .IssueTypes}}
|
||||
<table class="ui compact table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ctx.Locale.Tr "org.settings.issue_type_color"}}</th>
|
||||
<th>{{ctx.Locale.Tr "org.settings.issue_type_name"}}</th>
|
||||
<th>{{ctx.Locale.Tr "org.settings.issue_type_default"}}</th>
|
||||
<th>{{ctx.Locale.Tr "org.settings.issue_type_sort_order"}}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .IssueTypes}}
|
||||
<tr {{if not .IsActive}}class="tw-opacity-50"{{end}}>
|
||||
<td>
|
||||
{{if .Color}}<span class="tw-inline-block tw-w-4 tw-h-4 tw-rounded" style="background-color: {{.Color}}"></span>{{else}}<span class="text grey">-</span>{{end}}
|
||||
</td>
|
||||
<td>
|
||||
<strong>{{.Name}}</strong>
|
||||
{{if not .IsActive}}<span class="ui mini grey label">Inactive</span>{{end}}
|
||||
{{if .Description}}<br><small class="text grey">{{.Description}}</small>{{end}}
|
||||
</td>
|
||||
<td>
|
||||
{{if .IsDefault}}<span class="ui mini blue label">{{ctx.Locale.Tr "org.settings.issue_type_default"}}</span>{{else}}<span class="text grey">-</span>{{end}}
|
||||
</td>
|
||||
<td>{{.SortOrder}}</td>
|
||||
<td class="tw-text-right">
|
||||
<form method="post" action="{{$.OrgLink}}/settings/issue-types/{{.ID}}/delete" class="tw-inline">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<button class="ui tiny red icon button" type="submit" title="{{ctx.Locale.Tr "remove"}}">{{svg "octicon-trash" 14}}</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<div class="empty-placeholder"><p>{{ctx.Locale.Tr "org.settings.issue_types_empty"}}</p></div>
|
||||
{{end}}
|
||||
|
||||
<div class="divider"></div>
|
||||
<h5>{{ctx.Locale.Tr "org.settings.issue_type_add"}}</h5>
|
||||
<form class="ui form" method="post" action="{{.OrgLink}}/settings/issue-types">
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="three fields">
|
||||
<div class="required field">
|
||||
<label>{{ctx.Locale.Tr "org.settings.issue_type_name"}}</label>
|
||||
<input name="name" required placeholder="e.g. Bug, Feature, Task">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "org.settings.issue_type_color"}}</label>
|
||||
<input name="color" type="color" value="#2563eb">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "org.settings.issue_type_sort_order"}}</label>
|
||||
<input name="sort_order" type="number" value="0" min="0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "org.settings.issue_type_description"}}</label>
|
||||
<input name="description" placeholder="Help text">
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox tw-mt-4">
|
||||
<input name="is_default" type="checkbox">
|
||||
<label>{{ctx.Locale.Tr "org.settings.issue_type_default"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "org.settings.issue_type_add"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
{{template "org/settings/layout_footer" .}}
|
||||
@@ -37,6 +37,9 @@
|
||||
<a class="{{if .PageIsSettingsIssuePriorities}}active {{end}}item" href="{{.OrgLink}}/settings/issue-priorities">
|
||||
{{svg "octicon-flame"}} {{ctx.Locale.Tr "org.settings.issue_priorities"}}
|
||||
</a>
|
||||
<a class="{{if .PageIsSettingsIssueTypes}}active {{end}}item" href="{{.OrgLink}}/settings/issue-types">
|
||||
{{svg "octicon-tag"}} {{ctx.Locale.Tr "org.settings.issue_types"}}
|
||||
</a>
|
||||
{{if .EnableActions}}
|
||||
<details class="item toggleable-item" {{if or .PageIsOrgSettingsActionsGeneral .PageIsSharedSettingsRunners .PageIsSharedSettingsSecrets .PageIsSharedSettingsVariables}}open{{end}}>
|
||||
<summary>{{svg "octicon-play"}} {{ctx.Locale.Tr "actions.actions"}}</summary>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
<p class="help">{{ctx.Locale.Tr "org.settings.enable_licensing_help"}}</p>
|
||||
</div>
|
||||
|
||||
{{if and (ne .StreamConfig.Platform "joomla") (ne .StreamConfig.Platform "both") (ne .StreamConfig.Platform "")}}
|
||||
<div class="inline field">
|
||||
<div class="ui checkbox">
|
||||
<input name="require_key" type="checkbox" {{if .StreamConfig.RequireKey}}checked{{end}}>
|
||||
@@ -26,6 +27,7 @@
|
||||
</div>
|
||||
<p class="help">{{ctx.Locale.Tr "org.settings.require_key_help"}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "org.settings.feed_visibility"}}</label>
|
||||
|
||||
@@ -86,6 +86,12 @@
|
||||
<span data-tooltip-content="{{ctx.Locale.Tr "repo.release.download_count" (ctx.Locale.PrettyNumber .DownloadCount)}}">
|
||||
{{svg "octicon-info"}}
|
||||
</span>
|
||||
{{if $.CDNEnabled}}
|
||||
<label class="tw-flex tw-items-center tw-gap-1 tw-ml-2" data-tooltip-content="{{ctx.Locale.Tr "repo.release.cdn_public_tooltip"}}">
|
||||
<input type="checkbox" name="attachment-cdn-{{.UUID}}" {{if .CDNPublic}}checked{{end}} {{if $.ReleaseHasStream}}disabled{{end}}>
|
||||
<span class="tw-text-text-light tw-text-xs">{{ctx.Locale.Tr "repo.release.cdn_public"}}</span>
|
||||
</label>
|
||||
{{end}}
|
||||
</div>
|
||||
<a class="ui mini compact red button" data-global-click="onReleaseEditAttachmentDelete" data-id="{{.ID}}" data-uuid="{{.UUID}}">
|
||||
{{ctx.Locale.Tr "remove"}}
|
||||
|
||||
@@ -31,13 +31,15 @@
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.update_platform_help"}}</p>
|
||||
</div>
|
||||
|
||||
{{if and .RepoUpdateConfig (ne .RepoUpdateConfig.Platform "joomla") (ne .RepoUpdateConfig.Platform "both") (ne .RepoUpdateConfig.Platform "")}}
|
||||
<div class="inline field">
|
||||
<div class="ui checkbox">
|
||||
<input name="require_update_key" type="checkbox" {{if and .RepoUpdateConfig .RepoUpdateConfig.RequireKey}}checked{{end}}>
|
||||
<input name="require_update_key" type="checkbox" {{if .RepoUpdateConfig.RequireKey}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.require_update_key"}}</label>
|
||||
</div>
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.require_update_key_help"}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="inline field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.download_gating"}}</label>
|
||||
|
||||
@@ -11,23 +11,29 @@
|
||||
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.manifest_identity"}}</h5>
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.manifest_name"}}</label>
|
||||
<input name="name" value="{{.Manifest.Name}}" placeholder="Project name">
|
||||
{{if eq .Manifest.Platform "joomla"}}
|
||||
<label>{{ctx.Locale.Tr "repo.settings.manifest_element_name"}}</label>
|
||||
<input name="name" value="{{.Manifest.Name}}" placeholder="e.g. mokowaas">
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.manifest_element_name_help"}}</p>
|
||||
{{else}}
|
||||
<label>{{ctx.Locale.Tr "repo.settings.manifest_name"}}</label>
|
||||
<input name="name" value="{{.Manifest.Name}}" placeholder="Project name">
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.manifest_org"}}</label>
|
||||
<input name="org" value="{{.Manifest.Org}}" placeholder="Organization">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.manifest_description"}}</label>
|
||||
<input name="description" value="{{.Manifest.Description}}" placeholder="Project description">
|
||||
</div>
|
||||
<div class="three fields">
|
||||
<div class="four fields">
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.manifest_version"}}</label>
|
||||
<input name="version" value="{{.Manifest.Version}}" placeholder="e.g. 06.00.00">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.manifest_version_prefix"}}</label>
|
||||
<input name="version_prefix" value="{{.Manifest.VersionPrefix}}" placeholder="e.g. v1.26.1-moko.">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.manifest_license_spdx"}}</label>
|
||||
<input name="license_spdx" value="{{.Manifest.LicenseSPDX}}" placeholder="e.g. GPL-3.0-or-later">
|
||||
@@ -45,7 +51,7 @@
|
||||
<select name="platform" class="ui dropdown">
|
||||
<option value="">—</option>
|
||||
{{$platform := .Manifest.Platform}}
|
||||
{{range $val := StringUtils.Split "go,php,node,python,ruby,java,dotnet,rust" ","}}
|
||||
{{range $val := StringUtils.Split "joomla,wordpress,dolibarr,go,mcp,platform,generic" ","}}
|
||||
<option value="{{$val}}" {{if eq $val $platform}}selected{{end}}>{{$val}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
@@ -64,18 +70,47 @@
|
||||
<div class="three fields">
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.manifest_language"}}</label>
|
||||
<input name="language" value="{{.Manifest.Language}}" placeholder="e.g. Go, PHP, TypeScript">
|
||||
<select name="language" class="ui dropdown">
|
||||
<option value="">—</option>
|
||||
{{$lang := .Manifest.Language}}
|
||||
{{range $val := StringUtils.Split "Go,PHP,TypeScript,JavaScript,Python,Ruby,Java,C#,Rust,Shell,SQL,CSS,HTML" ","}}
|
||||
<option value="{{$val}}" {{if eq $val $lang}}selected{{end}}>{{$val}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
{{if eq .Manifest.Platform "joomla"}}
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.manifest_package_type"}}</label>
|
||||
<select name="package_type" class="ui dropdown">
|
||||
<option value="">—</option>
|
||||
{{$pkgType := .Manifest.PackageType}}
|
||||
{{range $val := StringUtils.Split "application,library,plugin,module,component,package,template" ","}}
|
||||
{{range $val := StringUtils.Split "component,module,plugin,package,template,library,file" ","}}
|
||||
<option value="{{$val}}" {{if eq $val $pkgType}}selected{{end}}>{{$val}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.manifest_package_type_help"}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.manifest_element_full"}}</label>
|
||||
<input name="element_name" value="{{.Manifest.ElementName}}" placeholder="{{.Manifest.AutoElementName}}">
|
||||
{{if .Manifest.ElementNameMismatch}}
|
||||
<p class="help tw-text-yellow-600">{{ctx.Locale.Tr "repo.settings.manifest_element_mismatch" .Manifest.AutoElementName}}</p>
|
||||
{{else}}
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.manifest_element_full_help"}}</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.manifest_element_full"}}</label>
|
||||
<input name="element_name" value="{{.Manifest.ElementName}}" placeholder="{{.Manifest.AutoElementName}}">
|
||||
{{if .Manifest.ElementNameMismatch}}
|
||||
<p class="help tw-text-yellow-600">{{ctx.Locale.Tr "repo.settings.manifest_element_mismatch" .Manifest.AutoElementName}}</p>
|
||||
{{else}}
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.manifest_element_full_help"}}</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.manifest_entry_point"}}</label>
|
||||
<input name="entry_point" value="{{.Manifest.EntryPoint}}" placeholder="e.g. ./ or src/index.ts">
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
</div>
|
||||
<button class="ui primary fluid button tw-mt-2" type="submit">{{ctx.Locale.Tr "sign_in"}}</button>
|
||||
</form>
|
||||
{{if or .OAuth2Providers .EnableSSPI}}
|
||||
<div class="divider"></div>
|
||||
{{template "user/auth/external_auth_methods" .}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,27 @@
|
||||
<a class="tw-block tw-my-4" href="{{.NotFoundGoBackURL}}">{{ctx.Locale.Tr "go_back"}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{if not .IsSigned}}
|
||||
<div class="tw-max-w-sm tw-mx-auto tw-mt-4">
|
||||
<form class="ui form" action="{{AppSubUrl}}/user/login" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="redirect_to" value="{{.CurrentURL}}">
|
||||
<div class="required field">
|
||||
<label>{{ctx.Locale.Tr "home.uname_holder"}}</label>
|
||||
<input type="text" name="user_name" required autofocus>
|
||||
</div>
|
||||
<div class="required field">
|
||||
<label>{{ctx.Locale.Tr "password"}}</label>
|
||||
<input type="password" name="password" required>
|
||||
</div>
|
||||
<button class="ui primary fluid button tw-mt-2" type="submit">{{ctx.Locale.Tr "sign_in"}}</button>
|
||||
</form>
|
||||
{{if or .OAuth2Providers .EnableSSPI}}
|
||||
<div class="divider"></div>
|
||||
{{template "user/auth/external_auth_methods" .}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,23 +28,42 @@ Each status has:
|
||||
| Sort Order | Controls display order in dropdowns (ascending) |
|
||||
| Is Active | Inactive statuses are hidden from dropdowns but preserved on existing issues |
|
||||
|
||||
### Example Statuses
|
||||
### Default Statuses (auto-seeded)
|
||||
|
||||
| Status | Color | Closes Issue | Use Case |
|
||||
|--------|-------|:------------:|----------|
|
||||
| In Progress | Blue | No | Work is actively being done |
|
||||
| Needs Info | Yellow | No | Waiting for more information from reporter |
|
||||
| Blocked | Red | No | Cannot proceed due to external dependency |
|
||||
| Won't Fix | Gray | Yes | Decided not to address this issue |
|
||||
| Duplicate | Purple | Yes | Already tracked in another issue |
|
||||
| Resolved | Green | Yes | Fix has been implemented and verified |
|
||||
| Needs Info | Yellow | No | Waiting for more information |
|
||||
| Blocked | Red | No | Cannot proceed due to dependency |
|
||||
| Resolved | Green | Yes | Fix implemented and verified |
|
||||
| Won't Fix | Gray | Yes | Decided not to address |
|
||||
| Duplicate | Purple | Yes | Already tracked elsewhere |
|
||||
| Pending: Design | Lavender | No | Waiting on design work |
|
||||
| Pending: Testing | Yellow | No | Waiting for testing |
|
||||
| Pending: Review | Green | No | Waiting for code review |
|
||||
| Pending: Feedback | Pink | No | Waiting for feedback |
|
||||
| Pending: Documentation | Purple | No | Waiting for docs |
|
||||
| Pending: Deployment | Blue | No | Ready to deploy |
|
||||
| Pending: Dependency | Light Blue | No | Blocked by external dependency |
|
||||
|
||||
Statuses are auto-seeded when an org first accesses them. Admins can add, edit, reorder, or deactivate statuses.
|
||||
|
||||
## Comment Form Integration
|
||||
|
||||
The status dropdown **replaces the close/reopen button** in the comment form footer for issues with org statuses:
|
||||
- Open issues show all statuses plus a "Close" option
|
||||
- Selecting a status with `closes_issue = true` auto-closes the issue
|
||||
- Closed issues show only "Reopen" for non-admin users
|
||||
- Admins see the full dropdown on closed issues including "Reopen"
|
||||
- PRs still use the standard close/reopen button
|
||||
|
||||
## Issue List Badges
|
||||
|
||||
Status shows as a colored badge on each issue in the issue list view, alongside Type and Priority badges.
|
||||
|
||||
## Issue Sidebar
|
||||
|
||||
When an organization has custom statuses defined, a **Status** dropdown appears in the issue sidebar between labels and custom fields. The dropdown:
|
||||
|
||||
- Shows all active status definitions for the repo's organization
|
||||
- Auto-submits on change (no save button needed)
|
||||
Status also appears as a read-only display in the sidebar (the editable control is in the comment form). The dropdown:
|
||||
- Displays a colored left border on each option
|
||||
- Shows a power symbol on statuses that close the issue
|
||||
- Selecting "—" (empty) clears the status
|
||||
|
||||
+7
-3
@@ -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.07.03 |
|
||||
| **Version** | v1.26.1-moko.06.11.01 |
|
||||
| **Platform** | [Gitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) |
|
||||
|
||||
---
|
||||
@@ -16,10 +16,13 @@ Moko Consulting's custom fork of [Gitea](https://gitea.com), extending the self-
|
||||
|
||||
- **Commercial License System** — Package-based license keys with download gating, domain restriction, key expiry, and payment webhook API
|
||||
- **Update Server** — Built-in update feeds for Joomla, WordPress, Dolibarr, Composer, Drupal, PrestaShop, and WHMCS
|
||||
- **Custom Issue Statuses** — Org-defined workflow states (In Progress, Blocked, Won't Fix) with auto close/reopen
|
||||
- **First-Class Issue Fields** — Type (12 types), Status (13 statuses), Priority (4 levels) as built-in fields with auto-seed defaults, colored badges in issue list, and sidebar dropdowns
|
||||
- **Security Scanning** — Built-in security scanner with secret detection (15 patterns), push-time scanning, alert management, and Security tab in repo navigation
|
||||
- **Custom Fields** — Org-level field definitions for issues (sidebar) and repos (metadata) with dropdown, text, number, date, checkbox, and URL types
|
||||
- **Manifest Settings** — Per-repo identity/governance/build metadata with REST API for CI/CD integration
|
||||
- **Manifest Settings** — Per-repo identity/governance/build metadata with REST API and auto-sync on push
|
||||
- **Wiki Folders** — Hierarchical folder navigation with sidebar tree, breadcrumbs, and index page fallback
|
||||
- **Well-Known File Tabs** — README/LICENSE/CONTRIBUTING/SECURITY/CHANGELOG tabs on repo home page
|
||||
- **MCP Server** — 120+ tool MCP server published to npm (@mokoconsulting/mokogitea-mcp) with SSE transport
|
||||
- **Org-Level Branch Protection** — Organization-scoped rulesets that cascade to all repos. Supports glob patterns. Full CRUD API
|
||||
- **Enterprise Sub-Orgs** — Parent-child organization hierarchy
|
||||
- **Three-Level Visibility** — Public (200), Private (403), Hidden (404) for repositories
|
||||
@@ -39,6 +42,7 @@ Moko Consulting's custom fork of [Gitea](https://gitea.com), extending the self-
|
||||
| [Org Branch Protection API](Org-Branch-Protection-API) | Org-level branch protection rulesets and API reference |
|
||||
| [Project API](Project-API) | Custom API endpoint reference for project boards |
|
||||
| [Roadmap](Roadmap) | Development roadmap and planned features |
|
||||
| [features/](features) | Feature documentation folder |
|
||||
|
||||
---
|
||||
|
||||
|
||||
+24
-17
@@ -1,42 +1,49 @@
|
||||
# MokoGitea Roadmap
|
||||
|
||||
## Recently Completed (v1.26.1-moko.06.07)
|
||||
## Recently Completed (v1.26.1-moko.06.11)
|
||||
|
||||
- First-class Type field (12 types) replacing labels and custom fields
|
||||
- First-class Status field (13 statuses) with auto close/reopen
|
||||
- First-class Priority field (4 levels) with auto-seed defaults
|
||||
- All org labels migrated to first-class fields and deleted
|
||||
- Type/Status/Priority colored badges in issue list view
|
||||
- Security scanning platform with 15 secret detection patterns
|
||||
- Security tab in repo navigation (admin-only)
|
||||
- Wiki hierarchical folder navigation with sidebar tree
|
||||
- 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
|
||||
- Repo manifest settings with REST API and auto-sync on push
|
||||
- MCP server published to npm (@mokoconsulting/mokogitea-mcp) with SSE transport
|
||||
- Dashboard issue count badges fixed
|
||||
- Status dropdown replaces close/reopen button
|
||||
- Org settings page for Issue Types
|
||||
- MCP SSE endpoint hosted at git.mokoconsulting.tech/mcp/
|
||||
- npm auto-publish workflow on MCP source changes
|
||||
- OAuth providers on 403/404 error pages
|
||||
- All stale branches cleaned up (main + dev only)
|
||||
|
||||
## In Progress
|
||||
|
||||
- Rename moko-platform to MokoPlatform
|
||||
- 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)
|
||||
- Bulk migrate remaining 41 flat wikis to folders
|
||||
|
||||
## Planned
|
||||
|
||||
- Standard status presets and cross-org migration (#507)
|
||||
- Auto-create default teams on org creation (#513)
|
||||
- Update server reads from repo_manifest (#512)
|
||||
- Dependency vulnerability scanner module
|
||||
- Code security analysis scanner module
|
||||
- Payment gateways for license keys (#135)
|
||||
- Independent visibility controls for issues/wiki/projects (#133)
|
||||
|
||||
## Infrastructure
|
||||
|
||||
- MCP SSE endpoint at git.mokoconsulting.tech/mcp
|
||||
- MCP SSE endpoint hosted 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 |
|
||||
|---|---|---|---|
|
||||
| 3.0 | 2026-06-06 | Jonathan Miller (@jmiller) | First-class fields, security scanning, wiki folders, MCP release |
|
||||
| 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