Compare commits

..

12 Commits

Author SHA1 Message Date
gitea-actions[bot] 727cff9eb8 chore(release): build 05.50.00 [skip ci] 2026-06-06 19:14:38 +00:00
jmiller 4c715d8424 Merge pull request 'release: v1.26.1-moko.06.07.02' (#529) from rc/v1.26.1-moko.06.07.02 into main
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Deploy MokoGitea / deploy (push) Failing after 13m45s
2026-06-06 19:13:55 +00:00
gitea-actions[bot] c0acdd1f58 chore(release): build 05.49.00 [skip ci] 2026-06-06 18:43:15 +00:00
jmiller c73109e2e6 Merge pull request 'release: v1.26.1-moko.06.07.01' (#527) from rc/v1.26.1-moko.06.07.01 into main
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Deploy MokoGitea / deploy (push) Failing after 33s
2026-06-06 18:42:28 +00:00
jmiller 3a159b7da6 Merge pull request 'release: v1.26.1-moko.06.07' (#525) from rc/v1.26.1-moko.06.07 into main
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Deploy MokoGitea / deploy (push) Failing after 33s
2026-06-06 17:56:51 +00:00
jmiller 7c8b20b779 Merge pull request 'release: v1.26.1-moko.06.06.02' (#522) from rc/v1.26.1-moko.06.06.02 into main
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Deploy MokoGitea / deploy (push) Failing after 34s
2026-06-06 17:41:13 +00:00
jmiller 4e8af85178 Merge pull request 'release: v1.26.1-moko.06.06.01' (#520) from rc/v1.26.1-moko.06.06.01 into main
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Deploy MokoGitea / deploy (push) Failing after 27s
2026-06-06 17:30:28 +00:00
jmiller aa1a67c4cb Merge pull request 'release: v1.26.1-moko.06.06' (#518) from rc/v1.26.1-moko.06.06 into main
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Deploy MokoGitea / deploy (push) Failing after 40s
2026-06-06 17:08:02 +00:00
Jonathan Miller 5642057c80 chore: resolve manifest conflict (use RC version)
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Deploy MokoGitea / deploy (push) Failing after 29s
2026-06-06 16:25:53 +00:00
Jonathan Miller 4dd27ccdb8 Merge release v1.26.1-moko.06.05.01 (#515) 2026-06-06 16:24:36 +00:00
gitea-actions[bot] 71a7ab04e5 chore(release): build 05.48.00 [skip ci] 2026-06-06 14:50:05 +00:00
jmiller d6dc7533ff Merge pull request 'release: v1.26.1-moko.06.05' (#511) from rc/v1.26.1-moko.06.05 into main
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Deploy MokoGitea / deploy (push) Failing after 27s
2026-06-06 14:49:11 +00:00
52 changed files with 327 additions and 2854 deletions
+2 -2
View File
@@ -3,8 +3,8 @@
<identity> <identity>
<name>MokoGitea</name> <name>MokoGitea</name>
<org>MokoConsulting</org> <org>MokoConsulting</org>
<description>Moko fork of Gitea -- adding project board REST API endpoints and custom enhancements</description> <description>Moko fork of Gitea adding project board REST API endpoints and custom enhancements</description>
<version>05.47.00</version> <version>05.50.00</version>
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license> <license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
</identity> </identity>
<governance> <governance>
+1 -50
View File
@@ -445,10 +445,9 @@ server.tool(
assignees: z.array(z.string()).optional().describe('Usernames to assign'), assignees: z.array(z.string()).optional().describe('Usernames to assign'),
status_id: z.number().optional().describe('Custom status definition ID'), status_id: z.number().optional().describe('Custom status definition ID'),
priority_id: z.number().optional().describe('Custom priority definition ID'), priority_id: z.number().optional().describe('Custom priority definition ID'),
type_id: z.number().optional().describe('Custom type definition ID'),
...ConnectionParam, ...ConnectionParam,
}, },
async ({ owner, repo, title, body: issueBody, labels, milestone, assignees, status_id, priority_id, type_id, connection }) => { async ({ owner, repo, title, body: issueBody, labels, milestone, assignees, status_id, priority_id, connection }) => {
const c = clientFor(connection); const c = clientFor(connection);
// Search for existing issue with same title to prevent duplicates // Search for existing issue with same title to prevent duplicates
@@ -484,7 +483,6 @@ server.tool(
if (issueData?.id) { if (issueData?.id) {
if (status_id !== undefined) await c.post(`/repos/${owner}/${repo}/issues/${issueData.id}/custom-status`, { status_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 (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); const out = formatResponse(res);
out.content[0].text = `Updated existing issue #${existing.number} (duplicate prevented)\n${out.content[0].text}`; out.content[0].text = `Updated existing issue #${existing.number} (duplicate prevented)\n${out.content[0].text}`;
@@ -503,7 +501,6 @@ server.tool(
if (newIssue?.id) { if (newIssue?.id) {
if (status_id !== undefined) await c.post(`/repos/${owner}/${repo}/issues/${newIssue.id}/custom-status`, { status_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 (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); return formatResponse(res);
}, },
@@ -2009,52 +2006,6 @@ 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 ──────────────────────────────────────────────────────── // ── Start Server ────────────────────────────────────────────────────────
async function main(): Promise<void> { async function main(): Promise<void> {
+31 -70
View File
@@ -17,7 +17,7 @@
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | # | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
# | | # | |
# | Platform-specific: | # | Platform-specific: |
# | joomla: XML manifest, type-prefixed packages | # | joomla: XML manifest, updates.xml, type-prefixed packages |
# | dolibarr: mod*.class.php, update.txt, dev version reset | # | dolibarr: mod*.class.php, update.txt, dev version reset |
# | generic: README-only, no update stream | # | generic: README-only, no update stream |
# | | # | |
@@ -71,25 +71,20 @@ jobs:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: | run: |
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then if ! command -v composer &> /dev/null; then
echo Using pre-installed /opt/moko-platform 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
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 fi
# Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api
composer install --no-dev --no-interaction --quiet
- name: Rename branch to rc - name: Rename branch to rc
run: | run: |
php ${MOKO_CLI}/branch_rename.php \ php /tmp/moko-platform-api/cli/branch_rename.php \
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \ --token "${{ secrets.MOKOGITEA_TOKEN }}" \
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
@@ -105,15 +100,16 @@ jobs:
- name: Publish RC release - name: Publish RC release
run: | run: |
php ${MOKO_CLI}/release_publish.php \ php /tmp/moko-platform-api/cli/release_publish.php \
--path . --stability rc --bump minor --branch rc \ --path . --stability rc --bump minor --branch rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --token "${{ secrets.MOKOGITEA_TOKEN }}" \
--skip-update-stream
- name: Summary - name: Summary
if: always() if: always()
run: | run: |
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY echo "Branch renamed to rc, minor bump, RC release built (updates.xml managed by Gitea Pages)" >> $GITHUB_STEP_SUMMARY
# ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── # ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
release: release:
@@ -155,60 +151,25 @@ jobs:
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
run: | run: |
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then # Ensure PHP + Composer are available
echo Using pre-installed /opt/moko-platform if ! command -v composer &> /dev/null; then
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV 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
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 fi
# Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api
composer install --no-dev --no-interaction --quiet
- name: "Publish stable release" - name: "Publish stable release"
run: | run: |
php ${MOKO_CLI}/release_publish.php \ php /tmp/moko-platform-api/cli/release_publish.php \
--path . --stability stable --bump minor --branch main \ --path . --stability stable --bump minor --branch main \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --token "${{ secrets.MOKOGITEA_TOKEN }}" \
--skip-update-stream
- 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) -------------------------------- # -- STEP 9: Mirror to GitHub (stable only) --------------------------------
- name: "Step 9: Mirror release to GitHub" - name: "Step 9: Mirror release to GitHub"
@@ -221,7 +182,7 @@ jobs:
RELEASE_TAG="${{ steps.version.outputs.release_tag }}" RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_mirror.php \ php /tmp/moko-platform-api/cli/release_mirror.php \
--version "$VERSION" --tag "$RELEASE_TAG" \ --version "$VERSION" --tag "$RELEASE_TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \ --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
@@ -295,7 +256,7 @@ jobs:
continue-on-error: true continue-on-error: true
run: | run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/version_reset_dev.php \ php /tmp/moko-platform-api/cli/version_reset_dev.php \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
--branch dev --path . 2>&1 || true --branch dev --path . 2>&1 || true
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Automation # INGROUP: moko-platform.Automation
# VERSION: 05.47.00 # VERSION: 05.50.00
# BRIEF: Auto-create feature branch when an issue is opened # BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch" name: "Universal: Issue Branch"
-51
View File
@@ -1,51 +0,0 @@
name: Publish MCP to npm
on:
push:
branches: [main]
paths:
- '.mokogitea/mcp/**'
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- name: Install and build
working-directory: .mokogitea/mcp
run: |
npm ci
npx tsc
- name: Check version change
id: version
working-directory: .mokogitea/mcp
run: |
LOCAL_VERSION=$(node -p "require('./package.json').version")
NPM_VERSION=$(npm view @mokoconsulting/mokogitea-mcp version 2>/dev/null || echo "0.0.0")
if [ "$LOCAL_VERSION" != "$NPM_VERSION" ]; then
echo "changed=true" >> $GITHUB_OUTPUT
echo "Version changed: $NPM_VERSION -> $LOCAL_VERSION"
else
echo "changed=false" >> $GITHUB_OUTPUT
echo "Version unchanged: $LOCAL_VERSION"
fi
- name: Publish to npm
if: steps.version.outputs.changed == 'true'
working-directory: .mokogitea/mcp
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish to Gitea registry
if: steps.version.outputs.changed == 'true'
working-directory: .mokogitea/mcp
run: |
npm publish --registry ${{ github.server_url }}/api/packages/${{ github.repository_owner }}/npm/ \
--//$(echo "${{ github.server_url }}" | sed 's|https://||')/api/packages/${{ github.repository_owner }}/npm/:_authToken=${{ secrets.GITEA_TOKEN }}
+241 -243
View File
@@ -1,243 +1,241 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> # Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# #
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release # INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /templates/workflows/universal/pre-release.yml.template # PATH: /templates/workflows/universal/pre-release.yml.template
# VERSION: 05.01.00 # VERSION: 05.01.00
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch # BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
name: "Universal: Pre-Release" name: "Universal: Pre-Release"
on: on:
pull_request: pull_request:
types: [closed] types: [closed]
branches: branches:
- dev - dev
pull_request_target: pull_request_target:
types: [synchronize, opened, reopened] types: [synchronize, opened, reopened]
branches: branches:
- main - main
workflow_dispatch: workflow_dispatch:
inputs: inputs:
stability: stability:
description: 'Pre-release channel' description: 'Pre-release channel'
required: true required: true
type: choice type: choice
options: options:
- development - development
- alpha - alpha
- beta - beta
- release-candidate - release-candidate
permissions: permissions:
contents: write contents: write
env: env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
jobs: jobs:
build: build:
name: "Build Pre-Release (${{ inputs.stability || 'development' }})" name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
runs-on: release runs-on: release
if: >- if: >-
github.event_name == 'workflow_dispatch' || 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' && 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') (github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main')
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }} token: ${{ secrets.MOKOGITEA_TOKEN }}
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }} ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }}
- name: Setup moko-platform tools - name: Setup moko-platform tools
env: env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: | run: |
# Use pre-installed /opt/moko-platform if available (updated by cron every 6h) # 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 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 Using pre-installed /opt/moko-platform
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
else else
echo Falling back to fresh clone echo Falling back to fresh clone
if ! command -v composer > /dev/null 2>&1; then if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1 sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi fi
rm -rf /tmp/moko-platform-api 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 \
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api “https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git” \
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet /tmp/moko-platform-api
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
fi echo “MOKO_CLI=/tmp/moko-platform-api/cli” >> “$GITHUB_ENV”
fi
- name: Detect platform
id: platform - name: Detect platform
run: | id: platform
php ${MOKO_CLI}/manifest_read.php --path . --github-output run: |
php ${MOKO_CLI}/manifest_read.php --path . --github-output
- name: Resolve metadata and bump version
id: meta - name: Resolve metadata and bump version
run: | id: meta
# Auto-detect stability: RC for PRs targeting main, else use input or default to development run: |
if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then # Auto-detect stability: RC for PRs targeting main, else use input or default to development
STABILITY="release-candidate" if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then
else STABILITY="release-candidate"
STABILITY="${{ inputs.stability || 'development' }}" else
fi STABILITY="${{ inputs.stability || 'development' }}"
fi
case "$STABILITY" in
development) SUFFIX="-dev"; TAG="development" ;; case "$STABILITY" in
alpha) SUFFIX="-alpha"; TAG="alpha" ;; development) SUFFIX="-dev"; TAG="development" ;;
beta) SUFFIX="-beta"; TAG="beta" ;; alpha) SUFFIX="-alpha"; TAG="alpha" ;;
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; beta) SUFFIX="-beta"; TAG="beta" ;;
esac release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
esac
# Bump version via CLI: patch for dev/alpha/beta, minor for RC
case "$STABILITY" in # Bump version via CLI: patch for dev/alpha/beta, minor for RC
release-candidate) BUMP="minor" ;; case "$STABILITY" in
*) BUMP="patch" ;; release-candidate) BUMP="minor" ;;
esac *) BUMP="patch" ;;
esac
php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true
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") # Set stability suffix and verify consistency
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') 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_set_platform.php \
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true --path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
# Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml
php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true # Append suffix for output
if [ -n "$SUFFIX" ]; then
# Append suffix for output VERSION="${VERSION}${SUFFIX}"
if [ -n "$SUFFIX" ]; then fi
VERSION="${VERSION}${SUFFIX}"
fi # Commit version bump
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
# Commit version bump git config --local user.name "gitea-actions[bot]"
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git config --local user.name "gitea-actions[bot]" git add -A
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" git diff --cached --quiet || {
git add -A git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
git diff --cached --quiet || { git push origin HEAD 2>&1
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 \
# Auto-detect element via manifest_element.php --path . --version "$VERSION" --stability "$STABILITY" \
php ${MOKO_CLI}/manifest_element.php \ --repo "${GITEA_REPO}" --github-output
--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)
# Read back element outputs ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
[ -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 "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT" echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ===" - name: Create release
id: release
- name: Create release run: |
id: release TAG="${{ steps.meta.outputs.tag }}"
run: | VERSION="${{ steps.meta.outputs.version }}"
TAG="${{ steps.meta.outputs.tag }}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
VERSION="${{ steps.meta.outputs.version }}" php ${MOKO_CLI}/release_create.php \
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" --path . --version "$VERSION" --tag "$TAG" \
php ${MOKO_CLI}/release_create.php \ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--path . --version "$VERSION" --tag "$TAG" \ --repo "${GITEA_REPO}" --branch dev --prerelease
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --branch dev --prerelease - name: Update release notes from CHANGELOG.md
run: |
- name: Update release notes from CHANGELOG.md TAG="${{ steps.meta.outputs.tag }}"
run: | VERSION="${{ steps.meta.outputs.version }}"
TAG="${{ steps.meta.outputs.tag }}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
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
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading) NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
if [ -f "CHANGELOG.md" ]; then [ -z "$NOTES" ] && NOTES="Release ${VERSION}"
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) else
[ -z "$NOTES" ] && NOTES="Release ${VERSION}" NOTES="Release ${VERSION}"
else fi
NOTES="Release ${VERSION}"
fi # Update release body via API
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
# Update release body via API "${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
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 "
if [ -n "$RELEASE_ID" ]; then import json, urllib.request
python3 -c " body = open('/dev/stdin').read()
import json, urllib.request payload = json.dumps({'body': body}).encode()
body = open('/dev/stdin').read() req = urllib.request.Request(
payload = json.dumps({'body': body}).encode() '${API_BASE}/releases/${RELEASE_ID}',
req = urllib.request.Request( data=payload, method='PATCH',
'${API_BASE}/releases/${RELEASE_ID}', headers={
data=payload, method='PATCH', 'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
headers={ 'Content-Type': 'application/json'
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}', })
'Content-Type': 'application/json' urllib.request.urlopen(req)
}) " <<< "$NOTES"
urllib.request.urlopen(req) echo "Release notes updated from CHANGELOG.md"
" <<< "$NOTES" fi
echo "Release notes updated from CHANGELOG.md"
fi - name: Build package and upload
id: package
- name: Build package and upload run: |
id: package VERSION="${{ steps.meta.outputs.version }}"
run: | TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TAG="${{ steps.meta.outputs.tag }}" php ${MOKO_CLI}/release_package.php \
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" --path . --version "$VERSION" --tag "$TAG" \
php ${MOKO_CLI}/release_package.php \ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--path . --version "$VERSION" --tag "$TAG" \ --repo "${GITEA_REPO}" --output /tmp || true
--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
# 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
- name: "Delete lesser pre-release channels (cascade)" run: |
continue-on-error: true API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
run: | TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
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 }}" \
php ${MOKO_CLI}/release_cascade.php \ --token "${TOKEN}" \
--stability "${{ steps.meta.outputs.stability }}" \ --api-base "${API_BASE}"
--token "${TOKEN}" \
--api-base "${API_BASE}" - name: Summary
if: always()
- name: Summary run: |
if: always() VERSION="${{ steps.meta.outputs.version }}"
run: | STABILITY="${{ steps.meta.outputs.stability }}"
VERSION="${{ steps.meta.outputs.version }}" ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
STABILITY="${{ steps.meta.outputs.stability }}" SHA256="${{ steps.package.outputs.sha256_zip }}"
ZIP_NAME="${{ steps.meta.outputs.zip_name }}" echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
SHA256="${{ steps.package.outputs.sha256_zip }}" echo "" >> $GITHUB_STEP_SUMMARY
echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
-40
View File
@@ -3,46 +3,6 @@
All notable changes to MokoGitea are documented here. Versions follow the format 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`). `v{upstream}-moko.{major}.{minor}` (e.g. `v1.26.1-moko.06.03`).
## [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 moko-platform/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 ## [v1.26.1-moko.06.04] - 2026-06-06
* FEATURES * FEATURES
-2
View File
@@ -80,8 +80,6 @@ type Issue struct {
Status *IssueStatusDef `xorm:"-"` Status *IssueStatusDef `xorm:"-"`
PriorityID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'priority_id'"` PriorityID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'priority_id'"`
PriorityDef *IssuePriorityDef `xorm:"-"` PriorityDef *IssuePriorityDef `xorm:"-"`
TypeID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'type_id'"`
TypeDef *IssueTypeDef `xorm:"-"`
IsRead bool `xorm:"-"` IsRead bool `xorm:"-"`
IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not. IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not.
PullRequest *PullRequest `xorm:"-"` PullRequest *PullRequest `xorm:"-"`
-114
View File
@@ -1,114 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package issues
import (
"context"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
)
func init() {
db.RegisterModel(new(IssueTypeDef))
}
// IssueTypeDef defines a custom issue type at the org level.
type IssueTypeDef struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'org_id'"`
Name string `xorm:"NOT NULL"`
Color string `xorm:"VARCHAR(7)"`
Description string `xorm:"TEXT"`
SortOrder int `xorm:"NOT NULL DEFAULT 0 'sort_order'"`
IsDefault bool `xorm:"NOT NULL DEFAULT false 'is_default'"`
IsActive bool `xorm:"NOT NULL DEFAULT true 'is_active'"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"`
}
func (IssueTypeDef) TableName() string {
return "issue_type_def"
}
// GetIssueTypeDefsByOrg returns active type definitions for an org.
// Auto-seeds defaults if none exist.
func GetIssueTypeDefsByOrg(ctx context.Context, orgID int64) ([]*IssueTypeDef, error) {
defs := make([]*IssueTypeDef, 0, 10)
if err := db.GetEngine(ctx).
Where("org_id = ? AND is_active = ?", orgID, true).
OrderBy("sort_order ASC, id ASC").
Find(&defs); err != nil {
return nil, err
}
if len(defs) == 0 && orgID > 0 {
if err := seedDefaultIssueTypes(ctx, orgID); err != nil {
return defs, nil
}
return GetIssueTypeDefsByOrg(ctx, orgID)
}
return defs, nil
}
// GetAllIssueTypeDefsByOrg returns all type definitions (including inactive).
func GetAllIssueTypeDefsByOrg(ctx context.Context, orgID int64) ([]*IssueTypeDef, error) {
defs := make([]*IssueTypeDef, 0, 10)
return defs, db.GetEngine(ctx).
Where("org_id = ?", orgID).
OrderBy("sort_order ASC, id ASC").
Find(&defs)
}
// GetIssueTypeDefByID returns a single type definition.
func GetIssueTypeDefByID(ctx context.Context, id int64) (*IssueTypeDef, error) {
def := new(IssueTypeDef)
has, err := db.GetEngine(ctx).ID(id).Get(def)
if err != nil {
return nil, err
}
if !has {
return nil, db.ErrNotExist{Resource: "IssueTypeDef", ID: id}
}
return def, nil
}
func CreateIssueTypeDef(ctx context.Context, def *IssueTypeDef) error {
_, err := db.GetEngine(ctx).Insert(def)
return err
}
func UpdateIssueTypeDef(ctx context.Context, def *IssueTypeDef) error {
_, err := db.GetEngine(ctx).ID(def.ID).AllCols().Update(def)
return err
}
func DeleteIssueTypeDef(ctx context.Context, id int64) error {
if _, err := db.GetEngine(ctx).Exec("UPDATE issue SET type_id = 0 WHERE type_id = ?", id); err != nil {
return err
}
_, err := db.GetEngine(ctx).ID(id).Delete(new(IssueTypeDef))
return err
}
func SetIssueTypeID(ctx context.Context, issueID, typeID int64) error {
_, err := db.GetEngine(ctx).Exec("UPDATE issue SET type_id = ? WHERE id = ?", typeID, issueID)
return err
}
func seedDefaultIssueTypes(ctx context.Context, orgID int64) error {
defaults := []*IssueTypeDef{
{OrgID: orgID, Name: "Bug", Color: "#dc2626", SortOrder: 1, IsActive: true},
{OrgID: orgID, Name: "Feature", Color: "#2563eb", SortOrder: 2, IsDefault: true, IsActive: true},
{OrgID: orgID, Name: "Enhancement", Color: "#16a34a", SortOrder: 3, IsActive: true},
{OrgID: orgID, Name: "Task", Color: "#6b7280", SortOrder: 4, IsActive: true},
{OrgID: orgID, Name: "Documentation", Color: "#8b5cf6", SortOrder: 5, IsActive: true},
{OrgID: orgID, Name: "Security", Color: "#e11d48", SortOrder: 6, IsActive: true},
}
for _, d := range defaults {
if _, err := db.GetEngine(ctx).Insert(d); err != nil {
return err
}
}
return nil
}
-2
View File
@@ -426,8 +426,6 @@ func prepareMigrationTasks() []*migration {
newMigration(346, "Add issue status definitions table", v1_27.AddIssueStatusDefTable), newMigration(346, "Add issue status definitions table", v1_27.AddIssueStatusDefTable),
newMigration(347, "Add repo manifest table", v1_27.AddRepoManifestTable), newMigration(347, "Add repo manifest table", v1_27.AddRepoManifestTable),
newMigration(348, "Add issue priority definitions table", v1_27.AddIssuePriorityDefTable), 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),
} }
return preparedMigrations return preparedMigrations
} }
-49
View File
@@ -1,49 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import (
"xorm.io/xorm"
)
// AddSecurityScanningTables creates security_alert and security_scanner_config tables.
func AddSecurityScanningTables(x *xorm.Engine) error {
type SecurityAlert struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX NOT NULL 'repo_id'"`
Scanner string `xorm:"VARCHAR(20) NOT NULL 'scanner'"`
Severity string `xorm:"VARCHAR(10) NOT NULL 'severity'"`
Status string `xorm:"VARCHAR(10) NOT NULL DEFAULT 'active' 'status'"`
RuleID string `xorm:"VARCHAR(100) NOT NULL 'rule_id'"`
Title string `xorm:"TEXT NOT NULL 'title'"`
Description string `xorm:"TEXT 'description'"`
FilePath string `xorm:"TEXT 'file_path'"`
LineNumber int `xorm:"'line_number'"`
CommitSHA string `xorm:"VARCHAR(64) 'commit_sha'"`
Fingerprint string `xorm:"VARCHAR(64) INDEX 'fingerprint'"`
Metadata string `xorm:"TEXT 'metadata'"`
ResolvedBy int64 `xorm:"'resolved_by'"`
CreatedUnix int64 `xorm:"INDEX CREATED 'created_unix'"`
UpdatedUnix int64 `xorm:"UPDATED 'updated_unix'"`
}
if err := x.Sync(new(SecurityAlert)); err != nil {
return err
}
type SecurityScannerConfig struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE INDEX NOT NULL 'repo_id'"`
Enabled bool `xorm:"NOT NULL DEFAULT true 'enabled'"`
BlockOnPush bool `xorm:"NOT NULL DEFAULT false 'block_on_push'"`
SecretScanner bool `xorm:"NOT NULL DEFAULT true 'secret_scanner'"`
DependScanner bool `xorm:"NOT NULL DEFAULT true 'depend_scanner'"`
CodeScanner bool `xorm:"NOT NULL DEFAULT false 'code_scanner'"`
ConfigScanner bool `xorm:"NOT NULL DEFAULT false 'config_scanner'"`
LicenseScanner bool `xorm:"NOT NULL DEFAULT false 'license_scanner'"`
CustomPatterns string `xorm:"TEXT 'custom_patterns'"`
CreatedUnix int64 `xorm:"INDEX CREATED 'created_unix'"`
UpdatedUnix int64 `xorm:"UPDATED 'updated_unix'"`
}
return x.Sync(new(SecurityScannerConfig))
}
-29
View File
@@ -1,29 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import "xorm.io/xorm"
// AddIssueTypeDefTable creates the issue_type_def table and adds type_id to issues.
func AddIssueTypeDefTable(x *xorm.Engine) error {
type IssueTypeDef struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'org_id'"`
Name string `xorm:"NOT NULL"`
Color string `xorm:"VARCHAR(7)"`
Description string `xorm:"TEXT"`
SortOrder int `xorm:"NOT NULL DEFAULT 0 'sort_order'"`
IsDefault bool `xorm:"NOT NULL DEFAULT false 'is_default'"`
IsActive bool `xorm:"NOT NULL DEFAULT true 'is_active'"`
CreatedUnix int64 `xorm:"INDEX CREATED 'created_unix'"`
UpdatedUnix int64 `xorm:"UPDATED 'updated_unix'"`
}
if err := x.Sync(new(IssueTypeDef)); err != nil {
return err
}
type Issue struct {
TypeID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'type_id'"`
}
return x.Sync(new(Issue))
}
-219
View File
@@ -1,219 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package security
import (
"context"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
)
func init() {
db.RegisterModel(new(SecurityAlert))
db.RegisterModel(new(SecurityScannerConfig))
}
// AlertSeverity represents the severity level of a security finding.
type AlertSeverity string
const (
SeverityCritical AlertSeverity = "critical"
SeverityHigh AlertSeverity = "high"
SeverityMedium AlertSeverity = "medium"
SeverityLow AlertSeverity = "low"
SeverityInfo AlertSeverity = "info"
)
// AlertStatus represents the lifecycle state of an alert.
type AlertStatus string
const (
AlertStatusActive AlertStatus = "active"
AlertStatusResolved AlertStatus = "resolved"
AlertStatusDismissed AlertStatus = "dismissed"
)
// ScannerType identifies which scanner produced a finding.
type ScannerType string
const (
ScannerSecret ScannerType = "secret"
ScannerDependency ScannerType = "dependency"
ScannerCode ScannerType = "code"
ScannerConfig ScannerType = "config"
ScannerLicense ScannerType = "license"
)
// SecurityAlert stores a single security finding for a repository.
type SecurityAlert struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX NOT NULL 'repo_id'"`
Scanner ScannerType `xorm:"VARCHAR(20) NOT NULL 'scanner'"`
Severity AlertSeverity `xorm:"VARCHAR(10) NOT NULL 'severity'"`
Status AlertStatus `xorm:"VARCHAR(10) NOT NULL DEFAULT 'active' 'status'"`
RuleID string `xorm:"VARCHAR(100) NOT NULL 'rule_id'"` // e.g. "aws-access-key", "cve-2024-1234"
Title string `xorm:"TEXT NOT NULL 'title'"`
Description string `xorm:"TEXT 'description'"`
FilePath string `xorm:"TEXT 'file_path'"`
LineNumber int `xorm:"'line_number'"`
CommitSHA string `xorm:"VARCHAR(64) 'commit_sha'"`
Fingerprint string `xorm:"VARCHAR(64) INDEX 'fingerprint'"` // dedup key: hash of rule+file+content
Metadata string `xorm:"TEXT 'metadata'"` // JSON extra data
ResolvedBy int64 `xorm:"'resolved_by'"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"`
}
func (SecurityAlert) TableName() string {
return "security_alert"
}
// SecurityScannerConfig stores per-repo scanner settings.
type SecurityScannerConfig struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE INDEX NOT NULL 'repo_id'"`
Enabled bool `xorm:"NOT NULL DEFAULT true 'enabled'"`
BlockOnPush bool `xorm:"NOT NULL DEFAULT false 'block_on_push'"` // reject push if secrets found
SecretScanner bool `xorm:"NOT NULL DEFAULT true 'secret_scanner'"`
DependScanner bool `xorm:"NOT NULL DEFAULT true 'depend_scanner'"`
CodeScanner bool `xorm:"NOT NULL DEFAULT false 'code_scanner'"`
ConfigScanner bool `xorm:"NOT NULL DEFAULT false 'config_scanner'"`
LicenseScanner bool `xorm:"NOT NULL DEFAULT false 'license_scanner'"`
CustomPatterns string `xorm:"TEXT 'custom_patterns'"` // JSON array of custom regex patterns
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"`
}
func (SecurityScannerConfig) TableName() string {
return "security_scanner_config"
}
// ──────────────────────────────────────────────────────────────────────
// Alert queries
// ──────────────────────────────────────────────────────────────────────
// GetActiveAlerts returns all active alerts for a repo.
func GetActiveAlerts(ctx context.Context, repoID int64) ([]*SecurityAlert, error) {
alerts := make([]*SecurityAlert, 0, 20)
return alerts, db.GetEngine(ctx).
Where("repo_id = ? AND status = ?", repoID, AlertStatusActive).
OrderBy("severity ASC, created_unix DESC").
Find(&alerts)
}
// GetAllAlerts returns all alerts for a repo (including resolved/dismissed).
func GetAllAlerts(ctx context.Context, repoID int64) ([]*SecurityAlert, error) {
alerts := make([]*SecurityAlert, 0, 50)
return alerts, db.GetEngine(ctx).
Where("repo_id = ?", repoID).
OrderBy("status ASC, severity ASC, created_unix DESC").
Find(&alerts)
}
// GetAlertByID returns a single alert.
func GetAlertByID(ctx context.Context, id int64) (*SecurityAlert, error) {
alert := new(SecurityAlert)
has, err := db.GetEngine(ctx).ID(id).Get(alert)
if err != nil {
return nil, err
}
if !has {
return nil, db.ErrNotExist{Resource: "SecurityAlert", ID: id}
}
return alert, nil
}
// GetAlertCountsByRepo returns count of active alerts grouped by severity.
func GetAlertCountsByRepo(ctx context.Context, repoID int64) (map[AlertSeverity]int64, error) {
type result struct {
Severity AlertSeverity `xorm:"severity"`
Count int64 `xorm:"count"`
}
var results []result
err := db.GetEngine(ctx).
Table("security_alert").
Select("severity, COUNT(*) as count").
Where("repo_id = ? AND status = ?", repoID, AlertStatusActive).
GroupBy("severity").
Find(&results)
if err != nil {
return nil, err
}
counts := make(map[AlertSeverity]int64)
for _, r := range results {
counts[r.Severity] = r.Count
}
return counts, nil
}
// CreateOrUpdateAlert creates a new alert or updates if fingerprint exists.
func CreateOrUpdateAlert(ctx context.Context, alert *SecurityAlert) error {
if alert.Fingerprint != "" {
existing := new(SecurityAlert)
has, err := db.GetEngine(ctx).
Where("repo_id = ? AND fingerprint = ?", alert.RepoID, alert.Fingerprint).
Get(existing)
if err != nil {
return err
}
if has {
// Update existing - refresh commit SHA and keep active
existing.CommitSHA = alert.CommitSHA
existing.LineNumber = alert.LineNumber
existing.Status = AlertStatusActive
_, err = db.GetEngine(ctx).ID(existing.ID).
Cols("commit_sha", "line_number", "status").Update(existing)
return err
}
}
_, err := db.GetEngine(ctx).Insert(alert)
return err
}
// UpdateAlertStatus changes the status of an alert.
func UpdateAlertStatus(ctx context.Context, id int64, status AlertStatus, resolvedBy int64) error {
_, err := db.GetEngine(ctx).ID(id).
Cols("status", "resolved_by").
Update(&SecurityAlert{Status: status, ResolvedBy: resolvedBy})
return err
}
// ──────────────────────────────────────────────────────────────────────
// Scanner config queries
// ──────────────────────────────────────────────────────────────────────
// GetScannerConfig returns the scanner config for a repo, or defaults.
func GetScannerConfig(ctx context.Context, repoID int64) (*SecurityScannerConfig, error) {
cfg := new(SecurityScannerConfig)
has, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Get(cfg)
if err != nil {
return nil, err
}
if !has {
return &SecurityScannerConfig{
RepoID: repoID,
Enabled: true,
SecretScanner: true,
DependScanner: true,
}, nil
}
return cfg, nil
}
// SaveScannerConfig creates or updates scanner config.
func SaveScannerConfig(ctx context.Context, cfg *SecurityScannerConfig) error {
existing := new(SecurityScannerConfig)
has, err := db.GetEngine(ctx).Where("repo_id = ?", cfg.RepoID).Get(existing)
if err != nil {
return err
}
if has {
cfg.ID = existing.ID
_, err = db.GetEngine(ctx).ID(cfg.ID).AllCols().Update(cfg)
return err
}
_, err = db.GetEngine(ctx).Insert(cfg)
return err
}
-37
View File
@@ -1584,7 +1584,6 @@
"repo.issues.save": "Save", "repo.issues.save": "Save",
"repo.issues.status": "Status", "repo.issues.status": "Status",
"repo.issues.priority": "Priority", "repo.issues.priority": "Priority",
"repo.issues.type": "Type",
"repo.issues.label_title": "Name", "repo.issues.label_title": "Name",
"repo.issues.label_description": "Description", "repo.issues.label_description": "Description",
"repo.issues.label_color": "Color", "repo.issues.label_color": "Color",
@@ -1970,7 +1969,6 @@
"repo.signing.wont_sign.approved": "The merge will not be signed as the PR is not approved.", "repo.signing.wont_sign.approved": "The merge will not be signed as the PR is not approved.",
"repo.ext_wiki": "Access to External Wiki", "repo.ext_wiki": "Access to External Wiki",
"repo.ext_wiki.desc": "Link to an external wiki.", "repo.ext_wiki.desc": "Link to an external wiki.",
"repo.security": "Security",
"repo.wiki": "Wiki", "repo.wiki": "Wiki",
"repo.wiki.welcome": "Welcome to the Wiki.", "repo.wiki.welcome": "Welcome to the Wiki.",
"repo.wiki.welcome_desc": "The wiki lets you write and share documentation with collaborators.", "repo.wiki.welcome_desc": "The wiki lets you write and share documentation with collaborators.",
@@ -1994,7 +1992,6 @@
"repo.wiki.page_already_exists": "A wiki page with the same name already exists.", "repo.wiki.page_already_exists": "A wiki page with the same name already exists.",
"repo.wiki.reserved_page": "The wiki page name \"%s\" is reserved.", "repo.wiki.reserved_page": "The wiki page name \"%s\" is reserved.",
"repo.wiki.pages": "Pages", "repo.wiki.pages": "Pages",
"repo.wiki.folder_empty": "This folder is empty.",
"repo.wiki.last_updated": "Last updated %s", "repo.wiki.last_updated": "Last updated %s",
"repo.wiki.page_name_desc": "Enter a name for this Wiki page. Some special names are: 'Home', '_Sidebar' and '_Footer'.", "repo.wiki.page_name_desc": "Enter a name for this Wiki page. Some special names are: 'Home', '_Sidebar' and '_Footer'.",
"repo.wiki.original_git_entry_tooltip": "View original Git file instead of using friendly link.", "repo.wiki.original_git_entry_tooltip": "View original Git file instead of using friendly link.",
@@ -2752,28 +2749,6 @@
"repo.settings.manifest_entry_point": "Entry Point", "repo.settings.manifest_entry_point": "Entry Point",
"repo.settings.manifest_save": "Save Manifest", "repo.settings.manifest_save": "Save Manifest",
"repo.settings.manifest_saved": "Manifest settings saved.", "repo.settings.manifest_saved": "Manifest settings saved.",
"repo.settings.security": "Security",
"repo.settings.security_desc": "Security scanning detects secrets, vulnerabilities, and code issues across the repository.",
"repo.settings.security_scanners": "Scanners",
"repo.settings.security_enabled": "Enable security scanning",
"repo.settings.security_secret_scanner": "Secret Scanner - API keys, tokens, passwords, private keys",
"repo.settings.security_depend_scanner": "Dependency Scanner - CVEs in dependencies (coming soon)",
"repo.settings.security_code_scanner": "Code Scanner - SQL injection, XSS, command injection (coming soon)",
"repo.settings.security_config_scanner": "Config Scanner - Insecure settings, debug modes (coming soon)",
"repo.settings.security_license_scanner": "License Scanner - License compliance (coming soon)",
"repo.settings.security_block_on_push": "Block pushes with critical findings",
"repo.settings.security_block_on_push_help": "Reject pushes to the default branch if critical secrets are detected.",
"repo.settings.security_save": "Save Settings",
"repo.settings.security_saved": "Security settings saved.",
"repo.settings.security_alerts": "Security Alerts",
"repo.settings.security_scan_now": "Scan Now",
"repo.settings.security_scan_complete": "Security scan complete.",
"repo.settings.security_severity": "Severity",
"repo.settings.security_scanner_type": "Scanner",
"repo.settings.security_finding": "Finding",
"repo.settings.security_file": "File",
"repo.settings.security_status": "Status",
"repo.settings.security_no_alerts": "No security alerts found. Run a scan or push to the default branch to check.",
"repo.settings.metadata": "Metadata", "repo.settings.metadata": "Metadata",
"repo.settings.metadata_saved": "Repository metadata saved.", "repo.settings.metadata_saved": "Repository metadata saved.",
"repo.settings.metadata_empty": "No metadata fields defined. Org admins can add fields in Organization Settings > Custom Fields.", "repo.settings.metadata_empty": "No metadata fields defined. Org admins can add fields in Organization Settings > Custom Fields.",
@@ -2992,18 +2967,6 @@
"org.settings.issue_priority_created": "Issue priority created.", "org.settings.issue_priority_created": "Issue priority created.",
"org.settings.issue_priority_updated": "Issue priority updated.", "org.settings.issue_priority_updated": "Issue priority updated.",
"org.settings.issue_priority_deleted": "Issue priority deleted.", "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.update_streams": "Update Server",
"org.settings.licensing": "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.", "org.settings.licensing_desc": "Manage update feeds and optional license key gating across all repositories in this organization.",
-98
View File
@@ -1,98 +0,0 @@
// 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")
}
-43
View File
@@ -1,43 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package repo
import (
"fmt"
"net/http"
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
// UpdateIssueCustomType handles POST to set a custom type on an issue.
func UpdateIssueCustomType(ctx *context.Context) {
issueID := ctx.PathParamInt64("id")
typeID := ctx.FormInt64("type_id")
issue, err := issues_model.GetIssueByID(ctx, issueID)
if err != nil {
ctx.ServerError("GetIssueByID", err)
return
}
if typeID > 0 {
typeDef, err := issues_model.GetIssueTypeDefByID(ctx, typeID)
if err != nil {
ctx.ServerError("GetIssueTypeDefByID", err)
return
}
if typeDef.OrgID != ctx.Repo.Repository.OwnerID {
ctx.NotFound(nil)
return
}
}
if err := issues_model.SetIssueTypeID(ctx, issueID, typeID); err != nil {
ctx.ServerError("SetIssueTypeID", err)
return
}
ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, issue.Index), http.StatusSeeOther)
}
-8
View File
@@ -536,14 +536,6 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID int64, projectI
} }
ctx.Data["CustomFieldDefs"] = customFieldDefs ctx.Data["CustomFieldDefs"] = customFieldDefs
ctx.Data["CustomFieldFilters"] = customFieldFilters ctx.Data["CustomFieldFilters"] = customFieldFilters
// Load first-class field definitions for issue list badges
issueStatusDefs, _ := issues_model.GetIssueStatusDefsByOrg(ctx, repo.OwnerID)
ctx.Data["IssueStatusDefs"] = issueStatusDefs
issuePriorityDefs, _ := issues_model.GetIssuePriorityDefsByOrg(ctx, repo.OwnerID)
ctx.Data["IssuePriorityDefs"] = issuePriorityDefs
issueTypeDefs, _ := issues_model.GetIssueTypeDefsByOrg(ctx, repo.OwnerID)
ctx.Data["IssueTypeDefs"] = issueTypeDefs
// Build a query string fragment for cf_ params so they survive pagination/sort changes. // Build a query string fragment for cf_ params so they survive pagination/sort changes.
cfQuery := make(url.Values) cfQuery := make(url.Values)
for fieldID, value := range customFieldFilters { for fieldID, value := range customFieldFilters {
-7
View File
@@ -379,13 +379,6 @@ func ViewIssue(ctx *context.Context) {
} }
ctx.Data["IssuePriorityDefs"] = issuePriorityDefs ctx.Data["IssuePriorityDefs"] = issuePriorityDefs
// Load custom issue type definitions for the sidebar.
issueTypeDefs, itErr := issues_model.GetIssueTypeDefsByOrg(ctx, ctx.Repo.Repository.OwnerID)
if itErr != nil {
log.Error("ViewIssue: GetIssueTypeDefsByOrg: %v", itErr)
}
ctx.Data["IssueTypeDefs"] = issueTypeDefs
upload.AddUploadContext(ctx, "comment") upload.AddUploadContext(ctx, "comment")
if err := issue.LoadAttributes(ctx); err != nil { if err := issue.LoadAttributes(ctx); err != nil {
-88
View File
@@ -1,88 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package repo
import (
"net/http"
security_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/security"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
security_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/security"
)
const tplRepoSecurity templates.TplName = "repo/security"
// Security renders the repo-level security tab showing alerts and scan controls.
func Security(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.security")
ctx.Data["PageIsSecurity"] = true
repoID := ctx.Repo.Repository.ID
cfg, err := security_model.GetScannerConfig(ctx, repoID)
if err != nil {
ctx.ServerError("GetScannerConfig", err)
return
}
ctx.Data["ScannerConfig"] = cfg
alerts, err := security_model.GetAllAlerts(ctx, repoID)
if err != nil {
ctx.ServerError("GetAllAlerts", err)
return
}
ctx.Data["SecurityAlerts"] = alerts
counts, err := security_model.GetAlertCountsByRepo(ctx, repoID)
if err != nil {
ctx.ServerError("GetAlertCountsByRepo", err)
return
}
ctx.Data["AlertCounts"] = counts
ctx.HTML(http.StatusOK, tplRepoSecurity)
}
// SecurityScanNow triggers an immediate scan from the security tab.
func SecurityScanNow(ctx *context.Context) {
commit := ctx.Repo.Commit
if commit == nil {
ctx.Flash.Error("No commits found")
ctx.Redirect(ctx.Repo.RepoLink + "/security")
return
}
security_service.ScanOnPush(ctx, ctx.Repo.Repository, commit)
ctx.Flash.Success(ctx.Tr("repo.settings.security_scan_complete"))
ctx.Redirect(ctx.Repo.RepoLink + "/security")
}
// SecurityAlertUpdate changes alert status from the security tab.
func SecurityAlertUpdateTab(ctx *context.Context) {
id := ctx.PathParamInt64("id")
status := security_model.AlertStatus(ctx.FormString("status"))
if status != security_model.AlertStatusResolved && status != security_model.AlertStatusDismissed {
status = security_model.AlertStatusDismissed
}
alert, err := security_model.GetAlertByID(ctx, id)
if err != nil {
ctx.ServerError("GetAlertByID", err)
return
}
if alert.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound(nil)
return
}
if err := security_model.UpdateAlertStatus(ctx, id, status, ctx.Doer.ID); err != nil {
ctx.ServerError("UpdateAlertStatus", err)
return
}
ctx.Flash.Success("Alert updated")
ctx.Redirect(ctx.Repo.RepoLink + "/security")
}
+1 -1
View File
@@ -88,7 +88,7 @@ func ManifestSettingsPost(ctx *context.Context) {
RepoID: ctx.Repo.Repository.ID, RepoID: ctx.Repo.Repository.ID,
Name: ctx.FormString("name"), Name: ctx.FormString("name"),
Org: ctx.FormString("org"), Org: ctx.FormString("org"),
Description: ctx.Repo.Repository.Description, Description: ctx.FormString("description"),
Version: ctx.FormString("version"), Version: ctx.FormString("version"),
LicenseSPDX: ctx.FormString("license_spdx"), LicenseSPDX: ctx.FormString("license_spdx"),
LicenseName: ctx.FormString("license_name"), LicenseName: ctx.FormString("license_name"),
-111
View File
@@ -1,111 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package setting
import (
"net/http"
security_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/security"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
security_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/security"
)
const tplSettingsSecurity templates.TplName = "repo/settings/security"
// SecuritySettings displays the repo security scanning settings and alerts.
func SecuritySettings(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.security")
ctx.Data["PageIsSettingsSecurity"] = true
repoID := ctx.Repo.Repository.ID
cfg, err := security_model.GetScannerConfig(ctx, repoID)
if err != nil {
ctx.ServerError("GetScannerConfig", err)
return
}
ctx.Data["ScannerConfig"] = cfg
alerts, err := security_model.GetAllAlerts(ctx, repoID)
if err != nil {
ctx.ServerError("GetAllAlerts", err)
return
}
ctx.Data["SecurityAlerts"] = alerts
counts, err := security_model.GetAlertCountsByRepo(ctx, repoID)
if err != nil {
ctx.ServerError("GetAlertCountsByRepo", err)
return
}
ctx.Data["AlertCounts"] = counts
ctx.HTML(http.StatusOK, tplSettingsSecurity)
}
// SecuritySettingsPost saves security scanner configuration.
func SecuritySettingsPost(ctx *context.Context) {
cfg := &security_model.SecurityScannerConfig{
RepoID: ctx.Repo.Repository.ID,
Enabled: ctx.FormString("enabled") == "on",
BlockOnPush: ctx.FormString("block_on_push") == "on",
SecretScanner: ctx.FormString("secret_scanner") == "on",
DependScanner: ctx.FormString("depend_scanner") == "on",
CodeScanner: ctx.FormString("code_scanner") == "on",
ConfigScanner: ctx.FormString("config_scanner") == "on",
LicenseScanner: ctx.FormString("license_scanner") == "on",
}
if err := security_model.SaveScannerConfig(ctx, cfg); err != nil {
ctx.ServerError("SaveScannerConfig", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.security_saved"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/security")
}
// SecurityScanNow triggers an immediate scan of the repository.
func SecurityScanNow(ctx *context.Context) {
commit := ctx.Repo.Commit
if commit == nil {
ctx.Flash.Error("No commits found in repository")
ctx.Redirect(ctx.Repo.RepoLink + "/settings/security")
return
}
security_service.ScanOnPush(ctx, ctx.Repo.Repository, commit)
ctx.Flash.Success(ctx.Tr("repo.settings.security_scan_complete"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/security")
}
// SecurityAlertUpdate changes the status of a security alert.
func SecurityAlertUpdate(ctx *context.Context) {
id := ctx.PathParamInt64("id")
status := security_model.AlertStatus(ctx.FormString("status"))
if status != security_model.AlertStatusResolved && status != security_model.AlertStatusDismissed {
status = security_model.AlertStatusDismissed
}
alert, err := security_model.GetAlertByID(ctx, id)
if err != nil {
ctx.ServerError("GetAlertByID", err)
return
}
if alert.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound(nil)
return
}
if err := security_model.UpdateAlertStatus(ctx, id, status, ctx.Doer.ID); err != nil {
ctx.ServerError("UpdateAlertStatus", err)
return
}
ctx.Flash.Success("Alert updated")
ctx.Redirect(ctx.Repo.RepoLink + "/settings/security")
}
-190
View File
@@ -77,20 +77,6 @@ type PageMeta struct {
UpdatedUnix timeutil.TimeStamp UpdatedUnix timeutil.TimeStamp
} }
// WikiTreeNode represents a node in the wiki folder tree for sidebar navigation.
type WikiTreeNode struct {
Name string
SubURL string
IsDir bool
Children []*WikiTreeNode
}
// WikiBreadcrumb represents a breadcrumb segment.
type WikiBreadcrumb struct {
Name string
SubURL string
}
// findEntryForFile finds the tree entry for a target filepath. // findEntryForFile finds the tree entry for a target filepath.
func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) { func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) {
entry, err := commit.GetTreeEntryByPath(target) entry, err := commit.GetTreeEntryByPath(target)
@@ -246,43 +232,6 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
isSideBar := pageName == "_Sidebar" isSideBar := pageName == "_Sidebar"
isFooter := pageName == "_Footer" isFooter := pageName == "_Footer"
// Build breadcrumbs for the current path
breadcrumbs := buildWikiBreadcrumbs(pageName)
ctx.Data["WikiBreadcrumbs"] = breadcrumbs
// Build folder tree for sidebar navigation
wikiTree := buildWikiTree(commit)
ctx.Data["WikiTree"] = wikiTree
// Check if path is a directory first (before file lookup)
dirEntry, _ := commit.GetTreeEntryByPath(string(pageName))
if dirEntry != nil && dirEntry.IsDir() {
// Path is a directory - try index files or show folder listing
var entry *git.TreeEntry
foundIndex := false
for _, indexName := range []string{"README", "Home", "index"} {
indexPath := wiki_service.WebPath(string(pageName) + "/" + indexName)
idxEntry, _, idxNoEntry, _ := wikiEntryByName(ctx, commit, indexPath)
if !idxNoEntry && idxEntry != nil {
pageName = indexPath
entry = idxEntry
_, displayName = wiki_service.WebPathToUserTitle(pageName)
ctx.Data["PageURL"] = wiki_service.WebPathToURLPath(pageName)
ctx.Data["Title"] = displayName
foundIndex = true
break
}
}
if !foundIndex {
ctx.Data["IsWikiFolder"] = true
ctx.Data["WikiFolderPath"] = string(pageName)
folderEntries := listWikiFolderEntries(commit, string(pageName))
ctx.Data["WikiFolderEntries"] = folderEntries
return wikiGitRepo, nil
}
_ = entry // will be used below via pageName lookup
}
// lookup filename in wiki - get gitTree entry , real filename // lookup filename in wiki - get gitTree entry , real filename
entry, pageFilename, noEntry, isRaw := wikiEntryByName(ctx, commit, pageName) entry, pageFilename, noEntry, isRaw := wikiEntryByName(ctx, commit, pageName)
if noEntry { if noEntry {
@@ -530,14 +479,6 @@ func Wiki(ctx *context.Context) {
if ctx.Written() { if ctx.Written() {
return return
} }
// Folder listing - no entry but IsWikiFolder flag is set
if ctx.Data["IsWikiFolder"] != nil {
if wikiGitRepo != nil {
defer wikiGitRepo.Close()
}
ctx.HTML(http.StatusOK, tplWikiView)
return
}
if entry == nil { if entry == nil {
ctx.Data["Title"] = ctx.Tr("repo.wiki") ctx.Data["Title"] = ctx.Tr("repo.wiki")
ctx.HTML(http.StatusOK, tplWikiStart) ctx.HTML(http.StatusOK, tplWikiStart)
@@ -811,134 +752,3 @@ func DeleteWikiPagePost(ctx *context.Context) {
ctx.JSONRedirect(ctx.Repo.RepoLink + "/wiki/") ctx.JSONRedirect(ctx.Repo.RepoLink + "/wiki/")
} }
// buildWikiBreadcrumbs creates breadcrumb segments from a wiki path.
func buildWikiBreadcrumbs(pageName wiki_service.WebPath) []WikiBreadcrumb {
parts := strings.Split(string(pageName), "/")
crumbs := make([]WikiBreadcrumb, 0, len(parts))
for i, part := range parts {
if part == "" {
continue
}
subURL := strings.Join(parts[:i+1], "/")
crumbs = append(crumbs, WikiBreadcrumb{
Name: part,
SubURL: subURL,
})
}
return crumbs
}
// buildWikiTree builds a hierarchical folder tree from the wiki git repo.
func buildWikiTree(commit *git.Commit) []*WikiTreeNode {
if commit == nil {
return nil
}
entries, err := commit.ListEntries()
if err != nil {
return nil
}
root := make(map[string]*WikiTreeNode)
var topLevel []*WikiTreeNode
for _, entry := range entries {
name := entry.Name()
if entry.IsDir() {
node := &WikiTreeNode{
Name: name,
SubURL: name,
IsDir: true,
}
// List children of this directory
subTree := entry.Tree()
if subTree != nil {
children, _ := subTree.ListEntries()
for _, child := range children {
childName := child.Name()
if child.IsDir() {
node.Children = append(node.Children, &WikiTreeNode{
Name: childName,
SubURL: name + "/" + childName,
IsDir: true,
})
} else if strings.HasSuffix(childName, ".md") {
wpChild, err := wiki_service.GitPathToWebPath(childName)
if err != nil {
continue
}
_, childDisplay := wiki_service.WebPathToUserTitle(wpChild)
if childDisplay == "_Sidebar" || childDisplay == "_Footer" {
continue
}
node.Children = append(node.Children, &WikiTreeNode{
Name: childDisplay,
SubURL: name + "/" + string(wpChild),
IsDir: false,
})
}
}
}
root[name] = node
topLevel = append(topLevel, node)
} else if strings.HasSuffix(name, ".md") {
wpName, err := wiki_service.GitPathToWebPath(name)
if err != nil {
continue
}
_, displayName := wiki_service.WebPathToUserTitle(wpName)
if displayName == "_Sidebar" || displayName == "_Footer" {
continue
}
node := &WikiTreeNode{
Name: displayName,
SubURL: string(wpName),
IsDir: false,
}
topLevel = append(topLevel, node)
}
}
return topLevel
}
// listWikiFolderEntries lists the pages and subfolders in a wiki directory.
func listWikiFolderEntries(commit *git.Commit, treePath string) []PageMeta {
if commit == nil {
return nil
}
tree, err := commit.SubTree(treePath)
if err != nil {
return nil
}
entries, err := tree.ListEntries()
if err != nil {
return nil
}
var pages []PageMeta
for _, entry := range entries {
name := entry.Name()
if entry.IsDir() {
pages = append(pages, PageMeta{
Name: name + "/",
SubURL: treePath + "/" + name,
GitEntryName: name,
})
} else if strings.HasSuffix(name, ".md") {
wpName, err := wiki_service.GitPathToWebPath(name)
if err != nil {
continue
}
_, displayName := wiki_service.WebPathToUserTitle(wpName)
if displayName == "_Sidebar" || displayName == "_Footer" {
continue
}
pages = append(pages, PageMeta{
Name: displayName,
SubURL: treePath + "/" + string(wpName),
GitEntryName: name,
})
}
}
return pages
}
-19
View File
@@ -1079,12 +1079,6 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Post("/{id}/edit", org.SettingsIssuePrioritiesEditPost) m.Post("/{id}/edit", org.SettingsIssuePrioritiesEditPost)
m.Post("/{id}/delete", org.SettingsIssuePrioritiesDeletePost) 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)) }, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "PageIsOrgSettings", true))
}, context.OrgAssignment(context.OrgAssignmentOptions{RequireOwner: true})) }, context.OrgAssignment(context.OrgAssignmentOptions{RequireOwner: true}))
}, reqSignIn) }, reqSignIn)
@@ -1213,11 +1207,6 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Combo("/licensing").Get(repo_setting.LicensingSettings).Post(repo_setting.LicensingSettingsPost) m.Combo("/licensing").Get(repo_setting.LicensingSettings).Post(repo_setting.LicensingSettingsPost)
m.Combo("/manifest").Get(repo_setting.ManifestSettings).Post(repo_setting.ManifestSettingsPost) m.Combo("/manifest").Get(repo_setting.ManifestSettings).Post(repo_setting.ManifestSettingsPost)
m.Combo("/metadata").Get(repo_setting.Metadata).Post(repo_setting.MetadataPost) m.Combo("/metadata").Get(repo_setting.Metadata).Post(repo_setting.MetadataPost)
m.Group("/security", func() {
m.Combo("").Get(repo_setting.SecuritySettings).Post(repo_setting.SecuritySettingsPost)
m.Post("/scan", repo_setting.SecurityScanNow)
m.Post("/alert/{id}", repo_setting.SecurityAlertUpdate)
})
m.Group("/collaboration", func() { m.Group("/collaboration", func() {
m.Combo("").Get(repo_setting.Collaboration).Post(repo_setting.CollaborationPost) m.Combo("").Get(repo_setting.Collaboration).Post(repo_setting.CollaborationPost)
@@ -1425,7 +1414,6 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Post("/{id}/custom-fields/{field_id}", reqRepoIssuesOrPullsWriter, repo.UpdateIssueCustomField) m.Post("/{id}/custom-fields/{field_id}", reqRepoIssuesOrPullsWriter, repo.UpdateIssueCustomField)
m.Post("/{id}/custom-status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueCustomStatus) m.Post("/{id}/custom-status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueCustomStatus)
m.Post("/{id}/custom-priority", reqRepoIssuesOrPullsWriter, repo.UpdateIssueCustomPriority) m.Post("/{id}/custom-priority", reqRepoIssuesOrPullsWriter, repo.UpdateIssueCustomPriority)
m.Post("/{id}/custom-type", reqRepoIssuesOrPullsWriter, repo.UpdateIssueCustomType)
m.Post("/delete", reqRepoAdmin, repo.BatchDeleteIssues) m.Post("/delete", reqRepoAdmin, repo.BatchDeleteIssues)
m.Delete("/unpin/{index}", reqRepoAdmin, repo.IssueUnpin) m.Delete("/unpin/{index}", reqRepoAdmin, repo.IssueUnpin)
m.Post("/move_pin", reqRepoAdmin, repo.IssuePinMove) m.Post("/move_pin", reqRepoAdmin, repo.IssuePinMove)
@@ -1684,13 +1672,6 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
}) })
// end "/{username}/{reponame}/wiki" // end "/{username}/{reponame}/wiki"
m.Group("/{username}/{reponame}/security", func() {
m.Get("", repo.Security)
m.Post("/scan", reqRepoAdmin, repo.SecurityScanNow)
m.Post("/alert/{id}", reqRepoAdmin, repo.SecurityAlertUpdateTab)
}, reqSignIn, context.RepoAssignment, reqRepoAdmin)
// end "/{username}/{reponame}/security"
m.Group("/{username}/{reponame}/activity", func() { m.Group("/{username}/{reponame}/activity", func() {
// activity has its own permission checks // activity has its own permission checks
m.Get("", repo.Activity) m.Get("", repo.Activity)
-26
View File
@@ -16,11 +16,8 @@ import (
"syscall" "syscall"
"time" "time"
auth_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/auth"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user" user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/httplib" "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/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
@@ -169,18 +166,6 @@ func (ctx *Context) notFoundInternal(logMsg string, logErr error) {
ctx.Data["IsRepo"] = ctx.Repo.Repository != nil ctx.Data["IsRepo"] = ctx.Repo.Repository != nil
ctx.Data["Title"] = "Page Not Found" 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["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") ctx.HTML(http.StatusNotFound, "status/404")
} }
@@ -202,17 +187,6 @@ func (ctx *Context) Forbidden() {
ctx.Data["IsRepo"] = ctx.Repo.Repository != nil ctx.Data["IsRepo"] = ctx.Repo.Repository != nil
ctx.Data["Title"] = "Access Denied" ctx.Data["Title"] = "Access Denied"
ctx.Data["CurrentURL"] = ctx.Req.URL.RequestURI() 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") ctx.HTML(http.StatusForbidden, "status/403")
} }
-3
View File
@@ -27,7 +27,6 @@ import (
issue_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/issue" issue_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/issue"
notify_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/notify" notify_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/notify"
pull_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/pull" pull_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/pull"
security_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/security"
) )
// pushQueue represents a queue to handle update pull request tests // pushQueue represents a queue to handle update pull request tests
@@ -196,8 +195,6 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
} }
// Auto-sync .mokogitea/manifest.xml to database on default branch push // Auto-sync .mokogitea/manifest.xml to database on default branch push
SyncManifestFromCommit(ctx, repo, newCommit) SyncManifestFromCommit(ctx, repo, newCommit)
// Run security scanners on default branch push
security_service.ScanOnPush(ctx, repo, newCommit)
} else { } else {
if err := DelDivergenceFromCache(repo.ID, branch); err != nil { if err := DelDivergenceFromCache(repo.ID, branch); err != nil {
log.Error("DelDivergenceFromCache: %v", err) log.Error("DelDivergenceFromCache: %v", err)
-75
View File
@@ -1,75 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package security
import (
"context"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
security_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/security"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
)
// ScanOnPush runs enabled scanners against a commit pushed to the default branch.
// Called from services/repository/push.go on default branch pushes.
func ScanOnPush(ctx context.Context, repo *repo_model.Repository, commit *git.Commit) {
if commit == nil {
return
}
cfg, err := security_model.GetScannerConfig(ctx, repo.ID)
if err != nil {
log.Error("SecurityScan: GetScannerConfig for %s: %v", repo.FullName(), err)
return
}
if !cfg.Enabled {
return
}
var scanners []Scanner
if cfg.SecretScanner {
scanners = append(scanners, NewSecretScanner())
}
// Future scanners added here:
// if cfg.DependScanner { scanners = append(scanners, NewDependencyScanner()) }
// if cfg.CodeScanner { scanners = append(scanners, NewCodeScanner()) }
if len(scanners) == 0 {
return
}
totalFindings := 0
for _, s := range scanners {
findings, err := s.ScanTree(commit)
if err != nil {
log.Error("SecurityScan: %s scanner for %s: %v", s.Type(), repo.FullName(), err)
continue
}
for _, f := range findings {
alert := &security_model.SecurityAlert{
RepoID: repo.ID,
Scanner: f.Scanner,
Severity: f.Severity,
RuleID: f.RuleID,
Title: f.Title,
Description: f.Description,
FilePath: f.FilePath,
LineNumber: f.LineNumber,
CommitSHA: f.CommitSHA,
Fingerprint: f.Fingerprint,
Metadata: f.Metadata,
}
if err := security_model.CreateOrUpdateAlert(ctx, alert); err != nil {
log.Error("SecurityScan: CreateOrUpdateAlert: %v", err)
}
totalFindings++
}
}
if totalFindings > 0 {
log.Warn("SecurityScan: %d findings in %s", totalFindings, repo.FullName())
}
}
-35
View File
@@ -1,35 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package security
import (
security_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/security"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
)
// Finding represents a single security issue found by a scanner.
type Finding struct {
Scanner security_model.ScannerType
Severity security_model.AlertSeverity
RuleID string
Title string
Description string
FilePath string
LineNumber int
CommitSHA string
Fingerprint string // unique identifier for dedup
Metadata string // JSON extra data
}
// Scanner is the interface all security scanner modules implement.
type Scanner interface {
// Type returns the scanner type identifier.
Type() security_model.ScannerType
// ScanCommit scans a single commit and returns findings.
ScanCommit(commit *git.Commit) ([]Finding, error)
// ScanTree scans the full repository tree and returns findings.
ScanTree(commit *git.Commit) ([]Finding, error)
}
-203
View File
@@ -1,203 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package security
import (
"bufio"
"crypto/sha256"
"fmt"
"io"
"regexp"
"strings"
security_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/security"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
)
// SecretRule defines a pattern to match against file contents.
type SecretRule struct {
ID string
Title string
Pattern *regexp.Regexp
Severity security_model.AlertSeverity
Description string
}
// DefaultSecretRules contains the built-in secret detection patterns.
var DefaultSecretRules = []SecretRule{
// AWS
{ID: "aws-access-key", Title: "AWS Access Key ID", Severity: security_model.SeverityCritical,
Pattern: regexp.MustCompile(`AKIA[0-9A-Z]{16}`), Description: "AWS access key ID detected"},
{ID: "aws-secret-key", Title: "AWS Secret Access Key", Severity: security_model.SeverityCritical,
Pattern: regexp.MustCompile(`(?i)aws_secret_access_key\s*[=:]\s*['"]?[A-Za-z0-9/+=]{40}`), Description: "AWS secret access key detected"},
// Generic tokens/keys
{ID: "private-key", Title: "Private Key", Severity: security_model.SeverityCritical,
Pattern: regexp.MustCompile(`-----BEGIN (RSA|EC|OPENSSH|DSA|PGP) PRIVATE KEY-----`), Description: "Private key file detected"},
{ID: "generic-api-key", Title: "Generic API Key", Severity: security_model.SeverityHigh,
Pattern: regexp.MustCompile(`(?i)(api[_-]?key|apikey)\s*[=:]\s*['"]?[A-Za-z0-9_\-]{20,}`), Description: "API key assignment detected"},
{ID: "generic-secret", Title: "Generic Secret", Severity: security_model.SeverityHigh,
Pattern: regexp.MustCompile(`(?i)(secret|password|passwd|pwd)\s*[=:]\s*['"][^'"]{8,}['"]`), Description: "Hardcoded secret or password detected"},
{ID: "generic-token", Title: "Generic Token", Severity: security_model.SeverityHigh,
Pattern: regexp.MustCompile(`(?i)(token|auth_token|access_token)\s*[=:]\s*['"]?[A-Za-z0-9_\-.]{20,}`), Description: "Token assignment detected"},
// GitHub/Gitea
{ID: "github-pat", Title: "GitHub Personal Access Token", Severity: security_model.SeverityCritical,
Pattern: regexp.MustCompile(`ghp_[A-Za-z0-9]{36}`), Description: "GitHub personal access token detected"},
{ID: "github-oauth", Title: "GitHub OAuth Token", Severity: security_model.SeverityCritical,
Pattern: regexp.MustCompile(`gho_[A-Za-z0-9]{36}`), Description: "GitHub OAuth token detected"},
// Stripe
{ID: "stripe-secret", Title: "Stripe Secret Key", Severity: security_model.SeverityCritical,
Pattern: regexp.MustCompile(`sk_live_[A-Za-z0-9]{24,}`), Description: "Stripe live secret key detected"},
{ID: "stripe-publishable", Title: "Stripe Publishable Key", Severity: security_model.SeverityLow,
Pattern: regexp.MustCompile(`pk_live_[A-Za-z0-9]{24,}`), Description: "Stripe live publishable key detected (usually safe but flagged)"},
// JWT
{ID: "jwt-token", Title: "JWT Token", Severity: security_model.SeverityMedium,
Pattern: regexp.MustCompile(`eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}`), Description: "JWT token detected"},
// Connection strings
{ID: "connection-string", Title: "Connection String with Password", Severity: security_model.SeverityCritical,
Pattern: regexp.MustCompile(`(?i)(mysql|postgres|postgresql|mongodb|redis|amqp|smtp)://[^:]+:[^@]+@[^\s]+`), Description: "Database/service connection string with embedded password"},
// Google
{ID: "google-api-key", Title: "Google API Key", Severity: security_model.SeverityHigh,
Pattern: regexp.MustCompile(`AIza[0-9A-Za-z_-]{35}`), Description: "Google API key detected"},
// Slack
{ID: "slack-webhook", Title: "Slack Webhook URL", Severity: security_model.SeverityMedium,
Pattern: regexp.MustCompile(`https://hooks\.slack\.com/services/T[A-Z0-9]+/B[A-Z0-9]+/[A-Za-z0-9]+`), Description: "Slack webhook URL detected"},
// SendGrid
{ID: "sendgrid-api-key", Title: "SendGrid API Key", Severity: security_model.SeverityHigh,
Pattern: regexp.MustCompile(`SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}`), Description: "SendGrid API key detected"},
// PayPal
{ID: "paypal-client-secret", Title: "PayPal Client Secret", Severity: security_model.SeverityCritical,
Pattern: regexp.MustCompile(`(?i)paypal.*secret\s*[=:]\s*['"]?[A-Za-z0-9_-]{20,}`), Description: "PayPal client secret detected"},
}
// Files to skip during scanning.
var skipExtensions = map[string]bool{
".png": true, ".jpg": true, ".jpeg": true, ".gif": true, ".ico": true,
".svg": true, ".woff": true, ".woff2": true, ".ttf": true, ".eot": true,
".zip": true, ".tar": true, ".gz": true, ".bz2": true, ".7z": true,
".pdf": true, ".doc": true, ".docx": true, ".xls": true, ".xlsx": true,
".exe": true, ".dll": true, ".so": true, ".dylib": true, ".o": true,
".min.js": true, ".min.css": true,
}
var skipPaths = []string{
"vendor/", "node_modules/", ".git/", "dist/", "build/",
"go.sum", "package-lock.json", "composer.lock", "yarn.lock",
}
// SecretScanner implements the Scanner interface for secret detection.
type SecretScanner struct {
Rules []SecretRule
}
// NewSecretScanner creates a scanner with default rules.
func NewSecretScanner() *SecretScanner {
return &SecretScanner{Rules: DefaultSecretRules}
}
func (s *SecretScanner) Type() security_model.ScannerType {
return security_model.ScannerSecret
}
func (s *SecretScanner) ScanCommit(commit *git.Commit) ([]Finding, error) {
// For push-time scanning, we scan the diff of the commit
return s.ScanTree(commit)
}
func (s *SecretScanner) ScanTree(commit *git.Commit) ([]Finding, error) {
if commit == nil {
return nil, nil
}
entries, err := commit.ListEntriesRecursiveFast()
if err != nil {
return nil, fmt.Errorf("ListEntriesRecursiveFast: %w", err)
}
var findings []Finding
for _, entry := range entries {
if !entry.IsRegular() {
continue
}
path := entry.Name()
if shouldSkipFile(path) {
continue
}
// Skip large files (> 1MB)
if entry.Blob().Size() > 1024*1024 {
continue
}
reader, err := entry.Blob().DataAsync()
if err != nil {
log.Trace("SecretScanner: skip %s: %v", path, err)
continue
}
fileFindings := s.scanReader(reader, path, commit.ID.String())
reader.Close()
findings = append(findings, fileFindings...)
}
return findings, nil
}
func (s *SecretScanner) scanReader(r io.Reader, filePath, commitSHA string) []Finding {
var findings []Finding
scanner := bufio.NewScanner(r)
lineNum := 0
for scanner.Scan() {
lineNum++
line := scanner.Text()
for _, rule := range s.Rules {
if rule.Pattern.MatchString(line) {
fingerprint := fmt.Sprintf("%x", sha256.Sum256([]byte(rule.ID+":"+filePath+":"+line)))
findings = append(findings, Finding{
Scanner: security_model.ScannerSecret,
Severity: rule.Severity,
RuleID: rule.ID,
Title: rule.Title,
Description: rule.Description,
FilePath: filePath,
LineNumber: lineNum,
CommitSHA: commitSHA,
Fingerprint: fingerprint[:32],
})
break // one finding per line per file
}
}
}
return findings
}
func shouldSkipFile(path string) bool {
lower := strings.ToLower(path)
for _, skip := range skipPaths {
if strings.HasPrefix(lower, skip) || strings.Contains(lower, "/"+skip) {
return true
}
}
for ext := range skipExtensions {
if strings.HasSuffix(lower, ext) {
return true
}
}
return false
}
+11 -27
View File
@@ -144,23 +144,21 @@ func WebPathToURLPath(s WebPath) string {
func WebPathFromRequest(s string) WebPath { func WebPathFromRequest(s string) WebPath {
s = util.PathJoinRelX(s) s = util.PathJoinRelX(s)
// MokoGitea: support real subdirectories for hierarchical wiki navigation. // The old wiki code's behavior is always using %2F, instead of subdirectory.
// Slashes are preserved as path separators, not escaped to %2F. s = strings.ReplaceAll(s, "/", "%2F")
return WebPath(s) return WebPath(s)
} }
var multiHyphenRe = regexp.MustCompile(`-{2,}`) var multiHyphenRe = regexp.MustCompile(`-{2,}`)
var nonSlugRe = regexp.MustCompile(`[^a-zA-Z0-9+.\-]`) var nonSlugRe = regexp.MustCompile(`[^a-zA-Z0-9+.\-]`)
var nonSlugReWithSlash = regexp.MustCompile(`[^a-zA-Z0-9+.\-/]`)
// sanitizeWikiTitle converts a user-provided title into a clean, URL-friendly slug. // sanitizeWikiTitle converts a user-provided title into a clean, URL-friendly slug.
// Spaces and special characters become hyphens, consecutive hyphens collapse to one. // Spaces and special characters become hyphens, consecutive hyphens collapse to one.
// Preserves: letters, digits, hyphens, plus signs (+), dots (.), and slashes (/). // Preserves: letters, digits, hyphens, plus signs (+), and dots (.)
func sanitizeWikiTitle(title string) string { func sanitizeWikiTitle(title string) string {
title = strings.TrimSpace(title) title = strings.TrimSpace(title)
title = strings.ReplaceAll(title, " ", "-") title = strings.ReplaceAll(title, " ", "-")
// Preserve slashes as directory separators title = nonSlugRe.ReplaceAllString(title, "-")
title = nonSlugReWithSlash.ReplaceAllString(title, "-")
title = multiHyphenRe.ReplaceAllString(title, "-") title = multiHyphenRe.ReplaceAllString(title, "-")
title = strings.NewReplacer("-+-", "-", "+-", "-", "-+", "-").Replace(title) // clean stray plus signs title = strings.NewReplacer("-+-", "-", "+-", "-", "-+", "-").Replace(title) // clean stray plus signs
title = strings.Trim(title, "-+.") title = strings.Trim(title, "-+.")
@@ -168,28 +166,14 @@ func sanitizeWikiTitle(title string) string {
} }
func UserTitleToWebPath(base, title string) WebPath { func UserTitleToWebPath(base, title string) WebPath {
// MokoGitea: support subdirectories - slashes in title create folder structure. // TODO: no support for subdirectory, because the old wiki code's behavior is always using %2F, instead of subdirectory.
// Split on /, sanitize each segment, rejoin. // So we do not add the support for writing slashes in title at the moment.
parts := strings.Split(title, "/") title = sanitizeWikiTitle(title)
sanitized := make([]string, 0, len(parts)) title = util.PathJoinRelX(base, escapeSegToWeb(title, false))
for _, p := range parts { if title == "" || title == "." {
p = strings.TrimSpace(p) title = "unnamed"
if p == "" {
continue
}
p = sanitizeWikiTitle(p)
if p != "" {
sanitized = append(sanitized, escapeSegToWeb(p, false))
}
} }
result := strings.Join(sanitized, "/") return WebPath(title)
if base != "" {
result = util.PathJoinRelX(base, result)
}
if result == "" || result == "." {
result = "unnamed"
}
return WebPath(result)
} }
// ToWikiPageMetaData converts meta information to a WikiPageMetaData // ToWikiPageMetaData converts meta information to a WikiPageMetaData
-81
View File
@@ -1,81 +0,0 @@
{{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" .}}
-3
View File
@@ -37,9 +37,6 @@
<a class="{{if .PageIsSettingsIssuePriorities}}active {{end}}item" href="{{.OrgLink}}/settings/issue-priorities"> <a class="{{if .PageIsSettingsIssuePriorities}}active {{end}}item" href="{{.OrgLink}}/settings/issue-priorities">
{{svg "octicon-flame"}} {{ctx.Locale.Tr "org.settings.issue_priorities"}} {{svg "octicon-flame"}} {{ctx.Locale.Tr "org.settings.issue_priorities"}}
</a> </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}} {{if .EnableActions}}
<details class="item toggleable-item" {{if or .PageIsOrgSettingsActionsGeneral .PageIsSharedSettingsRunners .PageIsSharedSettingsSecrets .PageIsSharedSettingsVariables}}open{{end}}> <details class="item toggleable-item" {{if or .PageIsOrgSettingsActionsGeneral .PageIsSharedSettingsRunners .PageIsSharedSettingsSecrets .PageIsSharedSettingsVariables}}open{{end}}>
<summary>{{svg "octicon-play"}} {{ctx.Locale.Tr "actions.actions"}}</summary> <summary>{{svg "octicon-play"}} {{ctx.Locale.Tr "actions.actions"}}</summary>
-9
View File
@@ -122,15 +122,6 @@
</a> </a>
{{end}} {{end}}
{{if and .Permission.IsAdmin .IsSigned}}
<a class="{{if .PageIsSecurity}}active {{end}}item" href="{{.RepoLink}}/security">
{{svg "octicon-shield"}} {{ctx.Locale.Tr "repo.security"}}
{{if .SecurityAlertCount}}
<span class="ui small label red">{{CountFmt .SecurityAlertCount}}</span>
{{end}}
</a>
{{end}}
{{if .Permission.CanRead ctx.Consts.RepoUnitTypePackages}} {{if .Permission.CanRead ctx.Consts.RepoUnitTypePackages}}
<a href="{{.RepoLink}}/packages" class="{{if .IsPackagesPage}}active {{end}}item"> <a href="{{.RepoLink}}/packages" class="{{if .IsPackagesPage}}active {{end}}item">
{{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}} {{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}}
@@ -1,33 +0,0 @@
{{if .IssueTypeDefs}}
<div class="divider"></div>
<div class="tw-flex tw-items-center tw-justify-between tw-gap-2">
<span class="text grey tw-text-sm">{{ctx.Locale.Tr "repo.issues.type"}}</span>
{{$canModify := and .FieldEditFlags .FieldEditFlags.CustomFields}}
{{if $canModify}}
<form method="post" action="{{.RepoLink}}/issues/{{.Issue.ID}}/custom-type" class="tw-inline">
{{$.CsrfTokenHtml}}
<select name="type_id" class="ui compact mini dropdown tw-max-w-48" onchange="this.form.submit()">
<option value="0">-</option>
{{range .IssueTypeDefs}}
<option value="{{.ID}}" {{if eq .ID $.Issue.TypeID}}selected{{end}}
{{if .Color}}style="border-left: 3px solid {{.Color}}"{{end}}>
{{.Name}}
</option>
{{end}}
</select>
</form>
{{else}}
{{$found := false}}
{{range .IssueTypeDefs}}
{{if eq .ID $.Issue.TypeID}}
{{if .Color}}<span class="tw-inline-block tw-w-3 tw-h-3 tw-rounded" style="background-color: {{.Color}}"></span>{{end}}
<span class="tw-text-sm">{{.Name}}</span>
{{$found = true}}
{{end}}
{{end}}
{{if not $found}}
<span class="tw-text-sm text grey">-</span>
{{end}}
{{end}}
</div>
{{end}}
+15 -24
View File
@@ -86,32 +86,23 @@
<div class="flex-text-block tw-justify-end"> <div class="flex-text-block tw-justify-end">
{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .DisableStatusChange)}} {{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .DisableStatusChange)}}
{{if and .IssueStatusDefs (not .Issue.IsPull)}} {{if and .IssueStatusDefs (not .Issue.IsPull)}}
{{if and .Issue.IsClosed (not .HasIssuesOrPullsWritePermission)}} <select name="status_id" class="ui compact dropdown" style="min-width:140px;padding:7px 10px;border:1px solid var(--color-secondary);border-radius:4px;background:var(--color-body);">
<button id="status-button" class="ui button" name="status_id" value="reopen"> <option value="">-- {{ctx.Locale.Tr "repo.issues.status"}} --</option>
<span class="status-button-icon tw-text-green">{{svg "octicon-issue-reopened"}}</span> {{range .IssueStatusDefs}}
<span class="status-button-text">{{ctx.Locale.Tr "repo.issues.reopen_issue"}}</span> {{if not .ClosesIssue}}
</button> <option value="{{.ID}}" {{if eq .ID $.Issue.StatusID}}selected{{end}}>{{.Name}}</option>
{{else}}
<select name="status_id" class="ui compact dropdown" style="min-width:140px;padding:7px 10px;border:1px solid var(--color-secondary);border-radius:4px;background:var(--color-body);">
<option value="">-- {{ctx.Locale.Tr "repo.issues.status"}} --</option>
{{range .IssueStatusDefs}}
{{if not .ClosesIssue}}
<option value="{{.ID}}" {{if eq .ID $.Issue.StatusID}}selected{{end}}>{{.Name}}</option>
{{end}}
{{end}} {{end}}
<option disabled>---</option> {{end}}
{{range .IssueStatusDefs}} <option disabled>---</option>
{{if .ClosesIssue}} {{range .IssueStatusDefs}}
<option value="{{.ID}}" {{if eq .ID $.Issue.StatusID}}selected{{end}}>{{.Name}} (close)</option> {{if .ClosesIssue}}
{{end}} <option value="{{.ID}}" {{if eq .ID $.Issue.StatusID}}selected{{end}}>{{.Name}} (close)</option>
{{end}} {{end}}
{{if not $.Issue.IsClosed}} {{end}}
<option value="close">{{ctx.Locale.Tr "repo.issues.close"}}</option> {{if .Issue.IsClosed}}
{{else}} <option value="reopen">{{ctx.Locale.Tr "repo.issues.reopen_issue"}}</option>
<option value="reopen">{{ctx.Locale.Tr "repo.issues.reopen_issue"}}</option> {{end}}
{{end}} </select>
</select>
{{end}}
{{else}} {{else}}
{{$btnIconColor := ""}}{{$btnIcon := ""}}{{$btnTextNoComment := ""}}{{$btnTextWithComment := ""}}{{$btnValue := ""}} {{$btnIconColor := ""}}{{$btnIcon := ""}}{{$btnTextNoComment := ""}}{{$btnTextWithComment := ""}}{{$btnValue := ""}}
{{if .Issue.IsClosed}} {{if .Issue.IsClosed}}
@@ -9,8 +9,6 @@
{{template "repo/issue/sidebar/issue_priority" $}} {{template "repo/issue/sidebar/issue_priority" $}}
{{template "repo/issue/sidebar/issue_type" $}}
{{template "repo/issue/sidebar/custom_fields" $}} {{template "repo/issue/sidebar/custom_fields" $}}
{{template "repo/issue/sidebar/milestone_list" $.IssuePageMetaData}} {{template "repo/issue/sidebar/milestone_list" $.IssuePageMetaData}}
-99
View File
@@ -1,99 +0,0 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content repository security">
{{template "repo/header" .}}
<div class="ui container">
<div class="tw-flex tw-justify-between tw-items-center tw-mb-4">
<h2>{{svg "octicon-shield" 20 "tw-mr-2"}}{{ctx.Locale.Tr "repo.security"}}</h2>
{{if .Permission.IsAdmin}}
<div class="tw-flex tw-gap-2">
<form method="post" action="{{.RepoLink}}/security/scan" class="tw-inline">
{{.CsrfTokenHtml}}
<button class="ui small primary button" type="submit">{{svg "octicon-sync" 14}} {{ctx.Locale.Tr "repo.settings.security_scan_now"}}</button>
</form>
<a class="ui small button" href="{{.RepoLink}}/settings/security">{{svg "octicon-gear" 14}} Settings</a>
</div>
{{end}}
</div>
{{if .AlertCounts}}
<div class="tw-flex tw-gap-3 tw-mb-4">
{{range $sev, $count := .AlertCounts}}
<div class="ui {{if eq $sev "critical"}}red{{else if eq $sev "high"}}orange{{else if eq $sev "medium"}}yellow{{else if eq $sev "low"}}blue{{else}}grey{{end}} label">
{{$sev}}: {{$count}}
</div>
{{end}}
</div>
{{end}}
{{if .SecurityAlerts}}
<table class="ui compact table">
<thead>
<tr>
<th>{{ctx.Locale.Tr "repo.settings.security_severity"}}</th>
<th>{{ctx.Locale.Tr "repo.settings.security_scanner_type"}}</th>
<th>{{ctx.Locale.Tr "repo.settings.security_finding"}}</th>
<th>{{ctx.Locale.Tr "repo.settings.security_file"}}</th>
<th>{{ctx.Locale.Tr "repo.settings.security_status"}}</th>
{{if .Permission.IsAdmin}}<th></th>{{end}}
</tr>
</thead>
<tbody>
{{range .SecurityAlerts}}
<tr {{if ne .Status "active"}}class="tw-opacity-50"{{end}}>
<td>
<span class="ui mini {{if eq .Severity "critical"}}red{{else if eq .Severity "high"}}orange{{else if eq .Severity "medium"}}yellow{{else if eq .Severity "low"}}blue{{else}}grey{{end}} label">
{{.Severity}}
</span>
</td>
<td>{{.Scanner}}</td>
<td>
<strong>{{.Title}}</strong>
{{if .Description}}<br><small class="text grey">{{.Description}}</small>{{end}}
</td>
<td>
{{if .FilePath}}
<a href="{{$.RepoLink}}/src/branch/{{$.BranchName}}/{{.FilePath}}{{if .LineNumber}}#L{{.LineNumber}}{{end}}">
<code class="tw-text-xs">{{.FilePath}}{{if .LineNumber}}:{{.LineNumber}}{{end}}</code>
</a>
{{end}}
</td>
<td>
{{if eq .Status "active"}}
<span class="ui mini red label">Active</span>
{{else if eq .Status "resolved"}}
<span class="ui mini green label">Resolved</span>
{{else}}
<span class="ui mini grey label">Dismissed</span>
{{end}}
</td>
{{if $.Permission.IsAdmin}}
<td class="tw-text-right">
{{if eq .Status "active"}}
<form method="post" action="{{$.RepoLink}}/security/alert/{{.ID}}" class="tw-inline">
{{$.CsrfTokenHtml}}
<input type="hidden" name="status" value="resolved">
<button class="ui tiny green icon button" type="submit" title="Resolve">{{svg "octicon-check" 14}}</button>
</form>
<form method="post" action="{{$.RepoLink}}/security/alert/{{.ID}}" class="tw-inline">
{{$.CsrfTokenHtml}}
<input type="hidden" name="status" value="dismissed">
<button class="ui tiny grey icon button" type="submit" title="Dismiss">{{svg "octicon-x" 14}}</button>
</form>
{{end}}
</td>
{{end}}
</tr>
{{end}}
</tbody>
</table>
{{else}}
<div class="ui segment">
<div class="empty-placeholder">
<p>{{svg "octicon-shield-check" 48}}</p>
<p>{{ctx.Locale.Tr "repo.settings.security_no_alerts"}}</p>
</div>
</div>
{{end}}
</div>
</div>
{{template "base/footer" .}}
+4
View File
@@ -19,6 +19,10 @@
<input name="org" value="{{.Manifest.Org}}" placeholder="Organization"> <input name="org" value="{{.Manifest.Org}}" placeholder="Organization">
</div> </div>
</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="three fields">
<div class="field"> <div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_version"}}</label> <label>{{ctx.Locale.Tr "repo.settings.manifest_version"}}</label>
-3
View File
@@ -18,9 +18,6 @@
<a class="{{if .PageIsSettingsMetadata}}active {{end}}item" href="{{.RepoLink}}/settings/metadata"> <a class="{{if .PageIsSettingsMetadata}}active {{end}}item" href="{{.RepoLink}}/settings/metadata">
{{svg "octicon-list-unordered"}} {{ctx.Locale.Tr "repo.settings.metadata"}} {{svg "octicon-list-unordered"}} {{ctx.Locale.Tr "repo.settings.metadata"}}
</a> </a>
<a class="{{if .PageIsSettingsSecurity}}active {{end}}item" href="{{.RepoLink}}/settings/security">
{{svg "octicon-shield"}} {{ctx.Locale.Tr "repo.settings.security"}}
</a>
{{if or .Repository.IsPrivate .Permission.HasAnyUnitPublicAccess}} {{if or .Repository.IsPrivate .Permission.HasAnyUnitPublicAccess}}
<a class="{{if .PageIsSettingsPublicAccess}}active {{end}}item" href="{{.RepoLink}}/settings/public_access"> <a class="{{if .PageIsSettingsPublicAccess}}active {{end}}item" href="{{.RepoLink}}/settings/public_access">
{{svg "octicon-eye"}} {{ctx.Locale.Tr "repo.settings.public_access"}} {{svg "octicon-eye"}} {{ctx.Locale.Tr "repo.settings.public_access"}}
-140
View File
@@ -1,140 +0,0 @@
{{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings security")}}
<h4 class="ui top attached header">
{{svg "octicon-shield" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.settings.security"}}
</h4>
<div class="ui attached segment">
<p class="text grey">{{ctx.Locale.Tr "repo.settings.security_desc"}}</p>
{{if .AlertCounts}}
<div class="tw-flex tw-gap-3 tw-mb-4">
{{range $sev, $count := .AlertCounts}}
<div class="ui mini {{if eq $sev "critical"}}red{{else if eq $sev "high"}}orange{{else if eq $sev "medium"}}yellow{{else if eq $sev "low"}}blue{{else}}grey{{end}} label">
{{$sev}}: {{$count}}
</div>
{{end}}
</div>
{{end}}
<form class="ui form" method="post" action="{{.RepoLink}}/settings/security">
{{.CsrfTokenHtml}}
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.security_scanners"}}</h5>
<div class="inline field">
<div class="ui checkbox">
<input name="enabled" type="checkbox" {{if .ScannerConfig.Enabled}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.security_enabled"}}</label>
</div>
</div>
<div class="inline field">
<div class="ui checkbox">
<input name="secret_scanner" type="checkbox" {{if .ScannerConfig.SecretScanner}}checked{{end}}>
<label>{{svg "octicon-key" 14}} {{ctx.Locale.Tr "repo.settings.security_secret_scanner"}}</label>
</div>
</div>
<div class="inline field">
<div class="ui checkbox">
<input name="depend_scanner" type="checkbox" {{if .ScannerConfig.DependScanner}}checked{{end}}>
<label>{{svg "octicon-package" 14}} {{ctx.Locale.Tr "repo.settings.security_depend_scanner"}}</label>
</div>
</div>
<div class="inline field">
<div class="ui checkbox">
<input name="code_scanner" type="checkbox" {{if .ScannerConfig.CodeScanner}}checked{{end}}>
<label>{{svg "octicon-code" 14}} {{ctx.Locale.Tr "repo.settings.security_code_scanner"}}</label>
</div>
</div>
<div class="inline field">
<div class="ui checkbox">
<input name="config_scanner" type="checkbox" {{if .ScannerConfig.ConfigScanner}}checked{{end}}>
<label>{{svg "octicon-gear" 14}} {{ctx.Locale.Tr "repo.settings.security_config_scanner"}}</label>
</div>
</div>
<div class="inline field">
<div class="ui checkbox">
<input name="license_scanner" type="checkbox" {{if .ScannerConfig.LicenseScanner}}checked{{end}}>
<label>{{svg "octicon-law" 14}} {{ctx.Locale.Tr "repo.settings.security_license_scanner"}}</label>
</div>
</div>
<div class="divider"></div>
<div class="inline field">
<div class="ui checkbox">
<input name="block_on_push" type="checkbox" {{if .ScannerConfig.BlockOnPush}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.security_block_on_push"}}</label>
</div>
<p class="help">{{ctx.Locale.Tr "repo.settings.security_block_on_push_help"}}</p>
</div>
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "repo.settings.security_save"}}</button>
</form>
</div>
<h4 class="ui top attached header tw-mt-4">
{{ctx.Locale.Tr "repo.settings.security_alerts"}}
<form method="post" action="{{.RepoLink}}/settings/security/scan" class="tw-float-right tw-inline">
{{.CsrfTokenHtml}}
<button class="ui mini primary button" type="submit">{{svg "octicon-sync" 14}} {{ctx.Locale.Tr "repo.settings.security_scan_now"}}</button>
</form>
</h4>
<div class="ui attached segment">
{{if .SecurityAlerts}}
<table class="ui compact table">
<thead>
<tr>
<th>{{ctx.Locale.Tr "repo.settings.security_severity"}}</th>
<th>{{ctx.Locale.Tr "repo.settings.security_scanner_type"}}</th>
<th>{{ctx.Locale.Tr "repo.settings.security_finding"}}</th>
<th>{{ctx.Locale.Tr "repo.settings.security_file"}}</th>
<th>{{ctx.Locale.Tr "repo.settings.security_status"}}</th>
<th></th>
</tr>
</thead>
<tbody>
{{range .SecurityAlerts}}
<tr {{if ne .Status "active"}}class="tw-opacity-50"{{end}}>
<td>
<span class="ui mini {{if eq .Severity "critical"}}red{{else if eq .Severity "high"}}orange{{else if eq .Severity "medium"}}yellow{{else if eq .Severity "low"}}blue{{else}}grey{{end}} label">
{{.Severity}}
</span>
</td>
<td>{{.Scanner}}</td>
<td>
<strong>{{.Title}}</strong>
{{if .Description}}<br><small class="text grey">{{.Description}}</small>{{end}}
</td>
<td>
{{if .FilePath}}
<code class="tw-text-xs">{{.FilePath}}{{if .LineNumber}}:{{.LineNumber}}{{end}}</code>
{{end}}
</td>
<td>
{{if eq .Status "active"}}
<span class="ui mini red label">Active</span>
{{else if eq .Status "resolved"}}
<span class="ui mini green label">Resolved</span>
{{else}}
<span class="ui mini grey label">Dismissed</span>
{{end}}
</td>
<td class="tw-text-right">
{{if eq .Status "active"}}
<form method="post" action="{{$.RepoLink}}/settings/security/alert/{{.ID}}" class="tw-inline">
{{$.CsrfTokenHtml}}
<input type="hidden" name="status" value="resolved">
<button class="ui tiny green icon button" type="submit" title="Resolve">{{svg "octicon-check" 14}}</button>
</form>
<form method="post" action="{{$.RepoLink}}/settings/security/alert/{{.ID}}" class="tw-inline">
{{$.CsrfTokenHtml}}
<input type="hidden" name="status" value="dismissed">
<button class="ui tiny grey icon button" type="submit" title="Dismiss">{{svg "octicon-x" 14}}</button>
</form>
{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
<div class="empty-placeholder">
<p>{{ctx.Locale.Tr "repo.settings.security_no_alerts"}}</p>
</div>
{{end}}
</div>
{{template "repo/settings/layout_footer" .}}
+1 -74
View File
@@ -55,51 +55,12 @@
</div> </div>
</div> </div>
</div> </div>
{{if .WikiBreadcrumbs}}
{{if gt (len .WikiBreadcrumbs) 1}}
<div class="tw-mb-2">
<span class="breadcrumb">
<a class="section" href="{{.RepoLink}}/wiki/">{{svg "octicon-book" 14}} Wiki</a>
{{range .WikiBreadcrumbs}}
<span class="breadcrumb-divider">/</span>
<a class="section" href="{{$.RepoLink}}/wiki/{{.SubURL}}">{{.Name}}</a>
{{end}}
</span>
</div>
{{end}}
{{end}}
{{if .FormatWarning}} {{if .FormatWarning}}
<div class="ui negative message"> <div class="ui negative message">
<p>{{.FormatWarning}}</p> <p>{{.FormatWarning}}</p>
</div> </div>
{{end}} {{end}}
{{if .IsWikiFolder}}
<h4 class="ui top attached header">
{{svg "octicon-file-directory" 16 "tw-mr-2"}}{{.WikiFolderPath}}
</h4>
<div class="ui attached segment">
{{if .WikiFolderEntries}}
<div class="wiki-folder-listing">
{{range .WikiFolderEntries}}
<div class="tw-py-1">
{{if (StringUtils.HasSuffix .Name "/")}}
{{svg "octicon-file-directory" 16 "tw-mr-1"}}
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}"><strong>{{.Name}}</strong></a>
{{else}}
{{svg "octicon-file" 16 "tw-mr-1"}}
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}">{{.Name}}</a>
{{end}}
</div>
{{end}}
</div>
{{else}}
<p class="text grey">This folder is empty.</p>
{{end}}
</div>
{{end}}
<div class="wiki-content-parts"> <div class="wiki-content-parts">
{{if .WikiSidebarTocHTML}} {{if .WikiSidebarTocHTML}}
<div class="render-content markup wiki-content-sidebar wiki-content-toc"> <div class="render-content markup wiki-content-sidebar wiki-content-toc">
@@ -107,45 +68,11 @@
</div> </div>
{{end}} {{end}}
<div class="render-content markup wiki-content-main {{if or .WikiSidebarTocHTML .WikiSidebarHTML .WikiTree}}with-sidebar{{end}}"> <div class="render-content markup wiki-content-main {{if or .WikiSidebarTocHTML .WikiSidebarHTML}}with-sidebar{{end}}">
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus}} {{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus}}
{{.WikiContentHTML}} {{.WikiContentHTML}}
</div> </div>
{{if .WikiTree}}
<div class="render-content markup wiki-content-sidebar wiki-content-tree">
<strong>{{svg "octicon-list-unordered" 14}} Pages</strong>
<ul class="wiki-tree-list">
{{range .WikiTree}}
<li>
{{if .IsDir}}
{{svg "octicon-file-directory" 14}}
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}"><strong>{{.Name}}</strong></a>
{{if .Children}}
<ul>
{{range .Children}}
<li>
{{if .IsDir}}
{{svg "octicon-file-directory" 14}}
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}"><strong>{{.Name}}</strong></a>
{{else}}
{{svg "octicon-file" 14}}
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}" {{if eq $.PageURL .SubURL}}class="active"{{end}}>{{.Name}}</a>
{{end}}
</li>
{{end}}
</ul>
{{end}}
{{else}}
{{svg "octicon-file" 14}}
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}" {{if eq $.PageURL .SubURL}}class="active"{{end}}>{{.Name}}</a>
{{end}}
</li>
{{end}}
</ul>
</div>
{{end}}
{{if .WikiSidebarHTML}} {{if .WikiSidebarHTML}}
<div class="render-content markup wiki-content-sidebar"> <div class="render-content markup wiki-content-sidebar">
{{if and .CanWriteWiki (not .Repository.IsMirror)}} {{if and .CanWriteWiki (not .Repository.IsMirror)}}
-3
View File
@@ -26,9 +26,6 @@
{{range .Labels}} {{range .Labels}}
<a href="?q={{$.Keyword}}&type={{$.ViewType}}&state={{$.State}}&labels={{.ID}}{{if ne $.listType "milestone"}}&milestone={{$.MilestoneID}}{{end}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.RenderUtils.RenderLabel .}}</a> <a href="?q={{$.Keyword}}&type={{$.ViewType}}&state={{$.State}}&labels={{.ID}}{{if ne $.listType "milestone"}}&milestone={{$.MilestoneID}}{{end}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.RenderUtils.RenderLabel .}}</a>
{{end}} {{end}}
{{if and .TypeID $.IssueTypeDefs}}{{range $.IssueTypeDefs}}{{if eq .ID $.TypeID}}<span class="ui mini label" {{if .Color}}style="background-color: {{.Color}}; color: white"{{end}}>{{.Name}}</span>{{end}}{{end}}{{end}}
{{if and .PriorityID $.IssuePriorityDefs}}{{range $.IssuePriorityDefs}}{{if eq .ID $.PriorityID}}<span class="ui mini label" {{if .Color}}style="background-color: {{.Color}}; color: white"{{end}}>{{.Name}}</span>{{end}}{{end}}{{end}}
{{if and .StatusID $.IssueStatusDefs}}{{range $.IssueStatusDefs}}{{if eq .ID $.StatusID}}<span class="ui mini label" {{if .Color}}style="background-color: {{.Color}}; color: white"{{end}}>{{.Name}}</span>{{end}}{{end}}{{end}}
</span> </span>
</div> </div>
{{if .TotalTrackedTime}} {{if .TotalTrackedTime}}
-4
View File
@@ -23,10 +23,6 @@
</div> </div>
<button class="ui primary fluid button tw-mt-2" type="submit">{{ctx.Locale.Tr "sign_in"}}</button> <button class="ui primary fluid button tw-mt-2" type="submit">{{ctx.Locale.Tr "sign_in"}}</button>
</form> </form>
{{if or .OAuth2Providers .EnableSSPI}}
<div class="divider"></div>
{{template "user/auth/external_auth_methods" .}}
{{end}}
</div> </div>
{{end}} {{end}}
</div> </div>
-21
View File
@@ -11,27 +11,6 @@
<a class="tw-block tw-my-4" href="{{.NotFoundGoBackURL}}">{{ctx.Locale.Tr "go_back"}}</a> <a class="tw-block tw-my-4" href="{{.NotFoundGoBackURL}}">{{ctx.Locale.Tr "go_back"}}</a>
{{end}} {{end}}
</div> </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> </div>
</div> </div>
+6 -6
View File
@@ -9,29 +9,29 @@
<div class="ui secondary vertical filter menu tw-bg-transparent"> <div class="ui secondary vertical filter menu tw-bg-transparent">
<a class="{{if eq .ViewType "your_repositories"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "your_repositories"}}"> <a class="{{if eq .ViewType "your_repositories"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "your_repositories"}}">
{{ctx.Locale.Tr "home.issues.in_your_repos"}} {{ctx.Locale.Tr "home.issues.in_your_repos"}}
<span class="ui small label">{{CountFmt .IssueStats.YourRepositoriesCount}}</span> <strong>{{CountFmt .IssueStats.YourRepositoriesCount}}</strong>
</a> </a>
<a class="{{if eq .ViewType "assigned"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "assigned"}}"> <a class="{{if eq .ViewType "assigned"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "assigned"}}">
{{ctx.Locale.Tr "repo.issues.filter_type.assigned_to_you"}} {{ctx.Locale.Tr "repo.issues.filter_type.assigned_to_you"}}
<span class="ui small label">{{CountFmt .IssueStats.AssignCount}}</span> <strong>{{CountFmt .IssueStats.AssignCount}}</strong>
</a> </a>
<a class="{{if eq .ViewType "created_by"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "created_by"}}"> <a class="{{if eq .ViewType "created_by"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "created_by"}}">
{{ctx.Locale.Tr "repo.issues.filter_type.created_by_you"}} {{ctx.Locale.Tr "repo.issues.filter_type.created_by_you"}}
<span class="ui small label">{{CountFmt .IssueStats.CreateCount}}</span> <strong>{{CountFmt .IssueStats.CreateCount}}</strong>
</a> </a>
{{if .PageIsPulls}} {{if .PageIsPulls}}
<a class="{{if eq .ViewType "review_requested"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "review_requested"}}"> <a class="{{if eq .ViewType "review_requested"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "review_requested"}}">
{{ctx.Locale.Tr "repo.issues.filter_type.review_requested"}} {{ctx.Locale.Tr "repo.issues.filter_type.review_requested"}}
<span class="ui small label">{{CountFmt .IssueStats.ReviewRequestedCount}}</span> <strong>{{CountFmt .IssueStats.ReviewRequestedCount}}</strong>
</a> </a>
<a class="{{if eq .ViewType "reviewed_by"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "reviewed_by"}}"> <a class="{{if eq .ViewType "reviewed_by"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "reviewed_by"}}">
{{ctx.Locale.Tr "repo.issues.filter_type.reviewed_by_you"}} {{ctx.Locale.Tr "repo.issues.filter_type.reviewed_by_you"}}
<span class="ui small label">{{CountFmt .IssueStats.ReviewedCount}}</span> <strong>{{CountFmt .IssueStats.ReviewedCount}}</strong>
</a> </a>
{{end}} {{end}}
<a class="{{if eq .ViewType "mentioned"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "mentioned"}}"> <a class="{{if eq .ViewType "mentioned"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "mentioned"}}">
{{ctx.Locale.Tr "repo.issues.filter_type.mentioning_you"}} {{ctx.Locale.Tr "repo.issues.filter_type.mentioning_you"}}
<span class="ui small label">{{CountFmt .IssueStats.MentionCount}}</span> <strong>{{CountFmt .IssueStats.MentionCount}}</strong>
</a> </a>
</div> </div>
</div> </div>
-27
View File
@@ -50,33 +50,6 @@
border-left-style: dashed; border-left-style: dashed;
} }
.repository.wiki .wiki-tree-list {
list-style: none;
padding: 0;
margin: 0.5em 0 0 0;
font-size: 0.9em;
}
.repository.wiki .wiki-tree-list ul {
list-style: none;
padding: 0 0 0 1.2em;
margin: 0;
border-left: 1px dashed var(--color-secondary);
}
.repository.wiki .wiki-tree-list li {
padding: 2px 0;
}
.repository.wiki .wiki-tree-list a.active {
font-weight: bold;
color: var(--color-primary);
}
.repository.wiki .wiki-folder-listing {
font-size: 0.95em;
}
@media (max-width: 767.98px) { @media (max-width: 767.98px) {
.repository.wiki .wiki-content-main.with-sidebar, .repository.wiki .wiki-content-main.with-sidebar,
.repository.wiki .wiki-content-sidebar { .repository.wiki .wiki-content-sidebar {
-32
View File
@@ -1,32 +0,0 @@
# Custom Branding
## Logo & Favicon
Located in the container at `/var/lib/gitea/custom/public/assets/img/`:
- `logo.svg` — Navbar logo
- `logo.png` — Fallback logo
- `favicon.png` — Browser tab icon
- `favicon.svg` — SVG favicon
Source: Moko Consulting CRM favicon
## Landing Page
- `LANDING_PAGE = organizations` in `app.ini`
- Custom JS redirects home/logo clicks to `/explore/organizations`
- After login, users see the organizations list
## Themes
Custom CSS themes at `/var/lib/gitea/custom/public/assets/css/`:
- `theme-moko-dark.css`
- `theme-moko-light.css`
- `theme-moko-auto.css`
---
*Repo: [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
| Revision | Date | Author | Description |
|---|---|---|---|
| 1.0 | 2026-05-09 | Moko Consulting | Initial version |
+10 -29
View File
@@ -28,42 +28,23 @@ Each status has:
| Sort Order | Controls display order in dropdowns (ascending) | | Sort Order | Controls display order in dropdowns (ascending) |
| Is Active | Inactive statuses are hidden from dropdowns but preserved on existing issues | | Is Active | Inactive statuses are hidden from dropdowns but preserved on existing issues |
### Default Statuses (auto-seeded) ### Example Statuses
| Status | Color | Closes Issue | Use Case | | Status | Color | Closes Issue | Use Case |
|--------|-------|:------------:|----------| |--------|-------|:------------:|----------|
| In Progress | Blue | No | Work is actively being done | | In Progress | Blue | No | Work is actively being done |
| Needs Info | Yellow | No | Waiting for more information | | Needs Info | Yellow | No | Waiting for more information from reporter |
| Blocked | Red | No | Cannot proceed due to dependency | | Blocked | Red | No | Cannot proceed due to external dependency |
| Resolved | Green | Yes | Fix implemented and verified | | Won't Fix | Gray | Yes | Decided not to address this issue |
| Won't Fix | Gray | Yes | Decided not to address | | Duplicate | Purple | Yes | Already tracked in another issue |
| Duplicate | Purple | Yes | Already tracked elsewhere | | Resolved | Green | Yes | Fix has been implemented and verified |
| 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 ## Issue Sidebar
Status also appears as a read-only display in the sidebar (the editable control is in the comment form). The dropdown: 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)
- Displays a colored left border on each option - Displays a colored left border on each option
- Shows a power symbol on statuses that close the issue - Shows a power symbol on statuses that close the issue
- Selecting "—" (empty) clears the status - Selecting "—" (empty) clears the status
-48
View File
@@ -1,48 +0,0 @@
# Deployment
## Docker Image
MokoGitea runs as a custom Docker image built from the `moko/1.25.5-project-api` branch.
### Build
```bash
cd /opt/MokoGitea
git pull
docker build -t mokogitea:1.25.5-project-api -f Dockerfile.rootless .
```
### Deploy
The docker-compose at `/opt/gitea/docker-compose.yml` references the image:
```yaml
services:
gitea:
image: mokogitea:1.25.5-project-api
```
### Update Process
1. Pull latest from `moko/1.25.5-project-api`
2. Rebuild Docker image
3. `docker compose down gitea && docker compose up -d gitea`
## Volumes
| Path | Purpose |
|------|---------|
| `./gitea/data` | Repository data, LFS, avatars |
| `./gitea/conf` | `app.ini` configuration |
## Custom Files
Located at `/var/lib/gitea/custom/`:
- `templates/custom/header.tmpl` — Branding, logo redirect
- `public/assets/img/logo.svg` — Moko logo
- `public/assets/img/favicon.png` — Moko favicon
- `public/assets/css/theme-moko-*.css` — Custom themes
---
*Repo: [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
| Revision | Date | Author | Description |
|---|---|---|---|
| 1.0 | 2026-05-09 | Moko Consulting | Initial version |
+3 -7
View File
@@ -7,7 +7,7 @@ Moko Consulting's custom fork of [Gitea](https://gitea.com), extending the self-
| **Language** | Go | | **Language** | Go |
| **License** | MIT | | **License** | MIT |
| **Upstream** | Gitea 1.26.1 | | **Upstream** | Gitea 1.26.1 |
| **Version** | v1.26.1-moko.06.11.01 | | **Version** | v1.26.1-moko.06.04.00 |
| **Platform** | [Gitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) | | **Platform** | [Gitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) |
--- ---
@@ -16,13 +16,10 @@ 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 - **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 - **Update Server** — Built-in update feeds for Joomla, WordPress, Dolibarr, Composer, Drupal, PrestaShop, and WHMCS
- **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 - **Custom Issue Statuses** — Org-defined workflow states (In Progress, Blocked, Won't Fix) with auto close/reopen
- **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 - **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 and auto-sync on push - **Manifest Settings** — Per-repo identity/governance/build metadata with REST API for CI/CD integration
- **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 - **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 - **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 - **Enterprise Sub-Orgs** — Parent-child organization hierarchy
- **Three-Level Visibility** — Public (200), Private (403), Hidden (404) for repositories - **Three-Level Visibility** — Public (200), Private (403), Hidden (404) for repositories
@@ -42,7 +39,6 @@ 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 | | [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 | | [Project API](Project-API) | Custom API endpoint reference for project boards |
| [Roadmap](Roadmap) | Development roadmap and planned features | | [Roadmap](Roadmap) | Development roadmap and planned features |
| [features/](features) | Feature documentation folder |
--- ---
-115
View File
@@ -1,115 +0,0 @@
# Org-Level Branch Protection API
## Overview
MokoGitea v1261.0.0 introduces **organization-level branch protection rulesets** that cascade automatically to all repositories within an organization. This eliminates the need to configure identical branch protection rules on each repo individually.
## How Inheritance Works
1. **Repo rules take precedence** — If a repo has its own protection rule for a branch pattern (e.g., `main`), the org rule is ignored for that repo.
2. **Org rules are the fallback** — If no repo-level rule matches a branch, the system checks org-level rules.
3. **Team-based only** — Org rules reference teams, not individual users (use repo-level rules for per-user whitelists).
## API Endpoints
All endpoints require authentication (`token`) and org ownership permissions.
### List Rules
```
GET /api/v1/orgs/{org}/branch_protections
```
### Create Rule
```
POST /api/v1/orgs/{org}/branch_protections
```
**Body:**
```json
{
"rule_name": "main",
"enable_push": true,
"enable_push_whitelist": true,
"push_whitelist_teams": ["developers"],
"enable_merge_whitelist": true,
"merge_whitelist_teams": ["maintainers"],
"required_approvals": 2,
"block_on_rejected_reviews": true,
"block_on_outdated_branch": true,
"dismiss_stale_approvals": true,
"require_signed_commits": false
}
```
### Get Rule
```
GET /api/v1/orgs/{org}/branch_protections/{name}
```
### Update Rule
```
PATCH /api/v1/orgs/{org}/branch_protections/{name}
```
Only fields included in the request body are updated.
### Delete Rule
```
DELETE /api/v1/orgs/{org}/branch_protections/{name}
```
## Glob Patterns
Rule names support glob patterns for matching multiple branches:
| Pattern | Matches |
|---------|---------|
| `main` | Exactly `main` |
| `dev` | Exactly `dev` |
| `rc/*` | `rc/1.0`, `rc/2.0-beta`, etc. |
| `beta/*` | `beta/feature-x`, etc. |
| `release/**` | `release/v1`, `release/v1/hotfix`, etc. |
## Example: Protect All Standard Branches
```bash
TOKEN="your-token"
ORG="MokoConsulting"
API="https://git.mokoconsulting.tech/api/v1"
for BRANCH in main dev "rc/*" "beta/*" "alpha/*"; do
curl -X POST "$API/orgs/$ORG/branch_protections" \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"rule_name\": \"$BRANCH\",
\"enable_push\": true,
\"enable_push_whitelist\": true,
\"push_whitelist_teams\": [\"developers\"],
\"required_approvals\": 1,
\"block_on_rejected_reviews\": true,
\"block_on_outdated_branch\": true
}"
done
```
## Configuration: Help & Support URLs
Also new in v1261.0.0 — configurable help/support links in `app.ini`:
```ini
[DEFAULT]
HELP_URL = https://docs.mokoconsulting.tech
SUPPORT_URL = https://mokoconsulting.tech/support
```
These replace the hardcoded `docs.gitea.com` links in the navigation bar and are visible in **Site Admin > Configuration**.
## Version Convention
MokoGitea uses `1261.xx.xx` versioning where `1261` represents the fork starting point from upstream Gitea. Minor and patch numbers track MokoGitea-specific releases.
-202
View File
@@ -1,202 +0,0 @@
# Project Board API Reference
Complete REST API for managing Gitea project boards, columns, and issue cards. This API was added by MokoGitea and is not available in upstream Gitea.
## Authentication
All write endpoints require a token with `issue` scope:
```
Authorization: token YOUR_TOKEN
```
## Projects
### List Projects
```
GET /api/v1/repos/{owner}/{repo}/projects
```
Query parameters:
- `state``open` (default), `closed`, or `all`
- `page` — page number (1-based)
- `limit` — results per page
Response: Array of Project objects
### Create Project
```
POST /api/v1/repos/{owner}/{repo}/projects
```
Body:
```json
{
"title": "Sprint Q2 2026",
"description": "Second quarter sprint",
"board_type": 1,
"card_type": 0
}
```
- `board_type`: 0=none, 1=basic kanban, 2=bug triage
- `card_type`: 0=text only, 1=images and text
### Get Project
```
GET /api/v1/repos/{owner}/{repo}/projects/{id}
```
### Update Project
```
PATCH /api/v1/repos/{owner}/{repo}/projects/{id}
```
Body:
```json
{
"title": "Updated Title",
"description": "Updated description"
}
```
### Delete Project
```
DELETE /api/v1/repos/{owner}/{repo}/projects/{id}
```
### Close/Reopen Project
```
POST /api/v1/repos/{owner}/{repo}/projects/{id}/close
POST /api/v1/repos/{owner}/{repo}/projects/{id}/reopen
```
## Columns
### List Columns
```
GET /api/v1/repos/{owner}/{repo}/projects/{id}/columns
```
### Create Column
```
POST /api/v1/repos/{owner}/{repo}/projects/{id}/columns
```
Body:
```json
{
"title": "Backlog",
"color": "#0075ca"
}
```
### Delete Column
```
DELETE /api/v1/repos/{owner}/{repo}/projects/{id}/columns/{columnId}
```
## Issue Cards
### List Issues in Column
```
GET /api/v1/repos/{owner}/{repo}/projects/{id}/columns/{columnId}/issues
```
Response: Array of ProjectColumnIssue objects with `issue_id`, `project_id`, `column_id`, `sorting`
### Add Issue to Column
```
POST /api/v1/repos/{owner}/{repo}/projects/{id}/columns/{columnId}/issues
```
Body:
```json
{
"issue_id": 42
}
```
### Move Issue Between Columns
```
PATCH /api/v1/repos/{owner}/{repo}/projects/{id}/issues/{issueId}/move
```
Body:
```json
{
"column_id": 5,
"sorting": 0
}
```
### Remove Issue from Project
```
DELETE /api/v1/repos/{owner}/{repo}/projects/{id}/issues/{issueId}
```
## Data Types
### Project
```json
{
"id": 1,
"title": "Roadmap",
"description": "Development roadmap",
"owner_id": 2,
"repo_id": 68,
"creator_id": 1,
"is_closed": false,
"created_at": "2026-05-08T00:06:45Z",
"updated_at": "2026-05-08T00:06:45Z",
"closed_at": null
}
```
### ProjectColumn
```json
{
"id": 7,
"title": "Backlog",
"sorting": 0,
"color": "#0075ca",
"project_id": 1,
"default": false,
"created_at": "2026-05-08T00:06:58Z",
"updated_at": "2026-05-08T00:06:58Z"
}
```
### ProjectColumnIssue
```json
{
"id": 1,
"issue_id": 42,
"project_id": 1,
"column_id": 7,
"sorting": 0
}
```
## MCP Integration
The `project-mcp` server wraps this API. Key tool: `project_setup_roadmap` creates a full project board with columns and loads all open issues in one call.
## Quick Start
```bash
# Create a project
curl -X POST -H "Authorization: token TOKEN" \
https://git.mokoconsulting.tech/api/v1/repos/MokoConsulting/MokoCRM/projects \
-d '{"title":"Roadmap","board_type":1}'
# Add columns
curl -X POST -H "Authorization: token TOKEN" \
https://git.mokoconsulting.tech/api/v1/repos/MokoConsulting/MokoCRM/projects/1/columns \
-d '{"title":"Backlog"}'
# Add an issue
curl -X POST -H "Authorization: token TOKEN" \
https://git.mokoconsulting.tech/api/v1/repos/MokoConsulting/MokoCRM/projects/1/columns/1/issues \
-d '{"issue_id":42}'
```
---
*Repo: [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
| Revision | Date | Author | Description |
|---|---|---|---|
| 1.0 | 2026-05-09 | Moko Consulting | Initial version |
-49
View File
@@ -1,49 +0,0 @@
# MokoGitea Roadmap
## 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)
- 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)
- 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)
- MCP SSE endpoint hosted at git.mokoconsulting.tech/mcp
- Smithery/Claude Code marketplace listing
---
| 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 |