Compare commits

...

12 Commits

Author SHA1 Message Date
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
jmiller 7532b9ff55 Merge pull request 'feat(settings): manifest auto-sync on push + wiki pages' (#510) from feat/315-manifest-settings into dev
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
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Failing after 32s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 2m10s
2026-06-06 14:48:01 +00:00
Jonathan Miller dd6e114c70 chore: move CLAUDE.md to .mokogitea/ directory
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
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Branch Cleanup / Delete merged branch (pull_request) Failing after 0s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Failing after 25s
Also includes:
- Auto-sync manifest.xml to DB on push to default branch
- Wiki pages for custom fields, custom statuses, manifest settings
- Updated wiki home page with all current features

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 09:43:15 -05:00
Jonathan Miller 1f6af9dd0a chore: move CLAUDE.md to .mokogitea/ directory
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
Relocate CLAUDE.md from repo root to .mokogitea/ per project convention.
Content updated with focused, repo-specific architecture and rules.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 09:31:19 -05:00
jmiller 2d9ca59599 Merge pull request 'feat(settings): repo manifest settings with auto-migration and API (#315)' (#504) from feat/315-manifest-settings into dev
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
2026-06-06 14:15:08 +00:00
jmiller 7e615516eb Merge pull request 'feat(issues): custom status definitions with automated actions (#502)' (#503) from feat/502-custom-issue-statuses into dev
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
2026-06-06 14:13:35 +00:00
Jonathan Miller 34fe0c5934 fix(api): use correct APIContext error methods for manifest endpoint
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
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Failing after 1s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Failing after 32s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 09:08:23 -05:00
Jonathan Miller 3aaa7c0843 feat(settings): repo manifest settings with auto-migration and API (#315)
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
Add a "Manifest" page in repo settings that stores moko-platform manifest
fields (identity, governance, build) in the database. Includes:

- RepoManifest model with all manifest.xml fields
- Migration v347 adding repo_manifest table
- Auto-detect and migrate .mokogitea/manifest.xml on first settings visit
- Repo settings UI with Identity/Governance/Build sections
- REST API: GET/PUT /api/v1/repos/{owner}/{repo}/manifest
  for Actions workflows and moko-platform CLI

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 09:02:23 -05:00
Jonathan Miller c568e199ed feat(issues): custom status definitions with automated actions (#502)
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
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Failing after 27s
Add org-level custom issue status definitions that appear in the issue
sidebar. Each status has a name, color, description, and an optional
"closes issue" flag that automatically closes/reopens the issue when
the status is selected.

Includes:
- IssueStatusDef model with CRUD operations
- Migration v346 adding issue_status_def table + status_id on issues
- Org settings UI for managing statuses
- Issue sidebar dropdown for selecting status
- Auto close/reopen when status has closes_issue flag

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 08:24:44 -05:00
jmiller 37ae3c5ec5 chore: add .mokogitea/workflows/pre-release.yml from moko-platform [skip ci] 2026-06-06 12:31:54 +00:00
jmiller b9937fabd9 Merge pull request 'feat(ui): tabbed view for root markdown files alongside README (#500)' (#501) from feat/500-root-file-tabs into dev
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
2026-06-06 11:50:12 +00:00
32 changed files with 1781 additions and 19 deletions
+42
View File
@@ -0,0 +1,42 @@
# MokoGitea
Fork of Gitea — self-hosted Git service at git.mokoconsulting.tech. Go backend + TypeScript frontend.
## Quick Reference
| Field | Value |
|---|---|
| **Language** | Go 1.26+ / TypeScript |
| **Module** | `code.mokoconsulting.tech/MokoConsulting/MokoGitea` |
| **Branch** | develop on `dev`, merge to `main` (protected) |
| **Wiki** | [MokoGitea Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/wiki) |
## Commands
```bash
make help # List all available targets
make fmt # Format .go files
make lint-go # Lint Go code
make lint-js # Lint TypeScript
make tidy # After go.mod changes
make build # Build binary
# Testing
go test -run '^TestName$' ./modulepath/ # Single Go test
pnpm exec vitest <path-filter> # Single JS test
GITEA_TEST_E2E_FLAGS='<filepath>' make test-e2e # Single Playwright test
```
## Rules
- Add current year copyright header on new `.go` files
- No trailing whitespace in edited files
- Conventional Commits for commit messages and PR titles
- Never force-push, amend, or squash unless asked — use new commits
- Preserve existing code comments
- TypeScript: use `!` (non-null assertion) not `?.`/`??` when value is known to exist
- CSS: prefer `flex-*` helpers over per-child `tw-ml-*`/`tw-mr-*` margins
- Add `Co-Authored-By` lines to all commits
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
- **Attribution**: `Authored-by: Moko Consulting`
- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)
+1 -1
View File
@@ -4,7 +4,7 @@
<name>MokoGitea</name>
<org>MokoConsulting</org>
<description>Moko fork of Gitea — adding project board REST API endpoints and custom enhancements</description>
<version>05.47.00</version>
<version>05.48.00</version>
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
</identity>
<governance>
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Automation
# VERSION: 05.47.00
# VERSION: 05.48.00
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
+241
View File
@@ -0,0 +1,241 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /templates/workflows/universal/pre-release.yml.template
# VERSION: 05.01.00
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
name: "Universal: Pre-Release"
on:
pull_request:
types: [closed]
branches:
- dev
pull_request_target:
types: [synchronize, opened, reopened]
branches:
- main
workflow_dispatch:
inputs:
stability:
description: 'Pre-release channel'
required: true
type: choice
options:
- development
- alpha
- beta
- release-candidate
permissions:
contents: write
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
jobs:
build:
name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
runs-on: release
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') ||
(github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main')
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }}
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }}
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
# Use pre-installed /opt/moko-platform if available (updated by cron every 6h)
if [ -f “/opt/moko-platform/cli/version_bump.php” ] && [ -f “/opt/moko-platform/vendor/autoload.php” ]; then
echo “Using pre-installed /opt/moko-platform”
echo “MOKO_CLI=/opt/moko-platform/cli” >> “$GITHUB_ENV”
else
echo “Falling back to fresh clone”
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \
“https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git” \
/tmp/moko-platform-api
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
echo “MOKO_CLI=/tmp/moko-platform-api/cli” >> “$GITHUB_ENV”
fi
- name: Detect platform
id: platform
run: |
php ${MOKO_CLI}/manifest_read.php --path . --github-output
- name: Resolve metadata and bump version
id: meta
run: |
# Auto-detect stability: RC for PRs targeting main, else use input or default to development
if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then
STABILITY="release-candidate"
else
STABILITY="${{ inputs.stability || 'development' }}"
fi
case "$STABILITY" in
development) SUFFIX="-dev"; TAG="development" ;;
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
beta) SUFFIX="-beta"; TAG="beta" ;;
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
esac
# Bump version via CLI: patch for dev/alpha/beta, minor for RC
case "$STABILITY" in
release-candidate) BUMP="minor" ;;
*) BUMP="patch" ;;
esac
php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true
# Set stability suffix and verify consistency
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01")
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
php ${MOKO_CLI}/version_set_platform.php \
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
# Append suffix for output
if [ -n "$SUFFIX" ]; then
VERSION="${VERSION}${SUFFIX}"
fi
# Commit version bump
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git add -A
git diff --cached --quiet || {
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
git push origin HEAD 2>&1
}
# Auto-detect element via manifest_element.php
php ${MOKO_CLI}/manifest_element.php \
--path . --version "$VERSION" --stability "$STABILITY" \
--repo "${GITEA_REPO}" --github-output
# Read back element outputs
EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
[ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
- name: Create release
id: release
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_create.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --branch dev --prerelease
- name: Update release notes from CHANGELOG.md
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
if [ -f "CHANGELOG.md" ]; then
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
else
NOTES="Release ${VERSION}"
fi
# Update release body via API
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
"${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -n "$RELEASE_ID" ]; then
python3 -c "
import json, urllib.request
body = open('/dev/stdin').read()
payload = json.dumps({'body': body}).encode()
req = urllib.request.Request(
'${API_BASE}/releases/${RELEASE_ID}',
data=payload, method='PATCH',
headers={
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
'Content-Type': 'application/json'
})
urllib.request.urlopen(req)
" <<< "$NOTES"
echo "Release notes updated from CHANGELOG.md"
fi
- name: Build package and upload
id: package
run: |
VERSION="${{ steps.meta.outputs.version }}"
TAG="${{ steps.meta.outputs.tag }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_package.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --output /tmp || true
# updates.xml is generated dynamically by MokoGitea license server
# No need to build, commit, or sync updates.xml from workflows
- name: "Delete lesser pre-release channels (cascade)"
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
php ${MOKO_CLI}/release_cascade.php \
--stability "${{ steps.meta.outputs.stability }}" \
--token "${TOKEN}" \
--api-base "${API_BASE}"
- name: Summary
if: always()
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
SHA256="${{ steps.package.outputs.sha256_zip }}"
echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
-16
View File
@@ -1,16 +0,0 @@
- Use `make help` to find available development targets
- Run `make fmt` to format `.go` files, and run `make lint-go` to lint them
- Run `make lint-js` to lint `.ts` files
- Run `make tidy` after any `go.mod` changes
- Run single go tests with `go test -run '^TestName$' ./modulepath/`
- Run single js test files with `pnpm exec vitest <path-filter>`
- Run single playwright e2e test files with `GITEA_TEST_E2E_FLAGS='<filepath>' make test-e2e`
- Add the current year into the copyright header of new `.go` files
- Ensure no trailing whitespace in edited files
- Use Conventional Commits format for commit messages and PR titles (e.g. `type(scope): subject`)
- Never force-push, amend, or squash unless asked. Use new commits and normal push for pull request updates
- Preserve existing code comments, do not remove or rewrite comments that are still relevant
- In TypeScript, use `!` (non-null assertion) instead of `?.`/`??` when a value is known to always exist
- For CSS layout, prefer `flex-*` helpers over per-child `tw-ml-*` / `tw-mr-*` margins; fall back to `tw-*` utilities when specificity requires `!important`
- Include authorship attribution in issue and pull request comments
- Add `Co-Authored-By` lines to all commits, indicating name and model used
-1
View File
@@ -1 +0,0 @@
@AGENTS.md
+2
View File
@@ -76,6 +76,8 @@ type Issue struct {
Assignee *user_model.User `xorm:"-"`
isAssigneeLoaded bool `xorm:"-"`
IsClosed bool `xorm:"INDEX"`
StatusID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'status_id'"`
Status *IssueStatusDef `xorm:"-"`
IsRead bool `xorm:"-"`
IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not.
PullRequest *PullRequest `xorm:"-"`
+104
View File
@@ -0,0 +1,104 @@
// 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(IssueStatusDef))
}
// IssueStatusDef defines a custom issue status at the org level.
type IssueStatusDef 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)"` // hex color, e.g. "#e11d48"
Description string `xorm:"TEXT"`
ClosesIssue bool `xorm:"NOT NULL DEFAULT false 'closes_issue'"`
SortOrder int `xorm:"NOT NULL DEFAULT 0 'sort_order'"`
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 (IssueStatusDef) TableName() string {
return "issue_status_def"
}
// ──────────────────────────────────────────────────────────────────────
// Queries
// ──────────────────────────────────────────────────────────────────────
// GetIssueStatusDefsByOrg returns active status definitions for an org.
func GetIssueStatusDefsByOrg(ctx context.Context, orgID int64) ([]*IssueStatusDef, error) {
defs := make([]*IssueStatusDef, 0, 10)
return defs, db.GetEngine(ctx).
Where("org_id = ? AND is_active = ?", orgID, true).
OrderBy("sort_order ASC, id ASC").
Find(&defs)
}
// GetAllIssueStatusDefsByOrg returns all status definitions (including inactive).
func GetAllIssueStatusDefsByOrg(ctx context.Context, orgID int64) ([]*IssueStatusDef, error) {
defs := make([]*IssueStatusDef, 0, 10)
return defs, db.GetEngine(ctx).
Where("org_id = ?", orgID).
OrderBy("sort_order ASC, id ASC").
Find(&defs)
}
// GetIssueStatusDefByID returns a single status definition.
func GetIssueStatusDefByID(ctx context.Context, id int64) (*IssueStatusDef, error) {
def := new(IssueStatusDef)
has, err := db.GetEngine(ctx).ID(id).Get(def)
if err != nil {
return nil, err
}
if !has {
return nil, db.ErrNotExist{Resource: "IssueStatusDef", ID: id}
}
return def, nil
}
// ──────────────────────────────────────────────────────────────────────
// CRUD
// ──────────────────────────────────────────────────────────────────────
// CreateIssueStatusDef creates a new status definition.
func CreateIssueStatusDef(ctx context.Context, def *IssueStatusDef) error {
_, err := db.GetEngine(ctx).Insert(def)
return err
}
// UpdateIssueStatusDef updates a status definition.
func UpdateIssueStatusDef(ctx context.Context, def *IssueStatusDef) error {
_, err := db.GetEngine(ctx).ID(def.ID).AllCols().Update(def)
return err
}
// DeleteIssueStatusDef deletes a status definition and clears references on issues.
func DeleteIssueStatusDef(ctx context.Context, id int64) error {
// Clear status_id on all issues that reference this definition
if _, err := db.GetEngine(ctx).Exec("UPDATE issue SET status_id = 0 WHERE status_id = ?", id); err != nil {
return err
}
_, err := db.GetEngine(ctx).ID(id).Delete(new(IssueStatusDef))
return err
}
// ──────────────────────────────────────────────────────────────────────
// Issue status helpers
// ──────────────────────────────────────────────────────────────────────
// SetIssueStatusID updates the status_id on an issue.
func SetIssueStatusID(ctx context.Context, issueID, statusID int64) error {
_, err := db.GetEngine(ctx).Exec("UPDATE issue SET status_id = ? WHERE id = ?", statusID, issueID)
return err
}
+2
View File
@@ -423,6 +423,8 @@ func prepareMigrationTasks() []*migration {
newMigration(343, "Add custom field tables for issue custom fields", v1_27.AddCustomFieldTables),
newMigration(344, "Add domain_restriction to license_package table", v1_27.AddDomainRestrictionToLicensePackage),
newMigration(345, "Migrate custom fields to org-level with scope", v1_27.MigrateCustomFieldsToOrgLevel),
newMigration(346, "Add issue status definitions table", v1_27.AddIssueStatusDefTable),
newMigration(347, "Add repo manifest table", v1_27.AddRepoManifestTable),
}
return preparedMigrations
}
+34
View File
@@ -0,0 +1,34 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import (
"xorm.io/xorm"
)
// AddIssueStatusDefTable creates the issue_status_def table and adds
// status_id to the issue table.
func AddIssueStatusDefTable(x *xorm.Engine) error {
type IssueStatusDef 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"`
ClosesIssue bool `xorm:"NOT NULL DEFAULT false 'closes_issue'"`
SortOrder int `xorm:"NOT NULL DEFAULT 0 'sort_order'"`
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(IssueStatusDef)); err != nil {
return err
}
// Add status_id column to issue table
type Issue struct {
StatusID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'status_id'"`
}
return x.Sync(new(Issue))
}
+32
View File
@@ -0,0 +1,32 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import (
"xorm.io/xorm"
)
// AddRepoManifestTable creates the repo_manifest table for storing
// moko-platform manifest settings per repository.
func AddRepoManifestTable(x *xorm.Engine) error {
type RepoManifest struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE INDEX NOT NULL 'repo_id'"`
Name string `xorm:"TEXT 'name'"`
Org string `xorm:"TEXT 'org'"`
Description string `xorm:"TEXT 'description'"`
Version string `xorm:"TEXT 'version'"`
LicenseSPDX string `xorm:"VARCHAR(50) 'license_spdx'"`
LicenseName string `xorm:"TEXT 'license_name'"`
Platform string `xorm:"VARCHAR(50) 'platform'"`
StandardsVersion string `xorm:"VARCHAR(20) 'standards_version'"`
StandardsSource string `xorm:"TEXT 'standards_source'"`
Language string `xorm:"VARCHAR(50) 'language'"`
PackageType string `xorm:"VARCHAR(50) 'package_type'"`
EntryPoint string `xorm:"TEXT 'entry_point'"`
CreatedUnix int64 `xorm:"INDEX CREATED 'created_unix'"`
UpdatedUnix int64 `xorm:"UPDATED 'updated_unix'"`
}
return x.Sync(new(RepoManifest))
}
+83
View File
@@ -0,0 +1,83 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package repo
import (
"context"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
)
func init() {
db.RegisterModel(new(RepoManifest))
}
// RepoManifest stores moko-platform manifest settings for a repository.
// These fields correspond to the .mokogitea/manifest.xml schema and are
// exposed via API for use by Actions workflows and the moko-platform CLI.
type RepoManifest struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE INDEX NOT NULL 'repo_id'"`
// identity section
Name string `xorm:"TEXT 'name'"` // project name
Org string `xorm:"TEXT 'org'"` // organization name
Description string `xorm:"TEXT 'description'"` // project description
Version string `xorm:"TEXT 'version'"` // current version string
LicenseSPDX string `xorm:"VARCHAR(50) 'license_spdx'"` // SPDX identifier, e.g. "GPL-3.0-or-later"
LicenseName string `xorm:"TEXT 'license_name'"` // human-readable license name
// governance section
Platform string `xorm:"VARCHAR(50) 'platform'"` // go, php, node, python, etc.
StandardsVersion string `xorm:"VARCHAR(20) 'standards_version'"` // moko-platform standards version
StandardsSource string `xorm:"TEXT 'standards_source'"` // URL to standards repo
// build section
Language string `xorm:"VARCHAR(50) 'language'"` // Go, PHP, TypeScript, etc.
PackageType string `xorm:"VARCHAR(50) 'package_type'"` // application, library, plugin, module, component, package
EntryPoint string `xorm:"TEXT 'entry_point'"` // build entry point path
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"`
}
func (RepoManifest) TableName() string {
return "repo_manifest"
}
// GetRepoManifest returns the manifest for a repo, or nil if none exists.
func GetRepoManifest(ctx context.Context, repoID int64) (*RepoManifest, error) {
m := new(RepoManifest)
has, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Get(m)
if err != nil {
return nil, err
}
if !has {
return nil, nil
}
return m, nil
}
// CreateOrUpdateRepoManifest upserts a repo manifest.
func CreateOrUpdateRepoManifest(ctx context.Context, m *RepoManifest) error {
existing := new(RepoManifest)
has, err := db.GetEngine(ctx).Where("repo_id = ?", m.RepoID).Get(existing)
if err != nil {
return err
}
if has {
m.ID = existing.ID
_, err = db.GetEngine(ctx).ID(m.ID).AllCols().Update(m)
return err
}
_, err = db.GetEngine(ctx).Insert(m)
return err
}
// DeleteRepoManifest deletes the manifest for a repo.
func DeleteRepoManifest(ctx context.Context, repoID int64) error {
_, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Delete(new(RepoManifest))
return err
}
+35
View File
@@ -1582,6 +1582,7 @@
"repo.issues.edit": "Edit",
"repo.issues.cancel": "Cancel",
"repo.issues.save": "Save",
"repo.issues.status": "Status",
"repo.issues.label_title": "Name",
"repo.issues.label_description": "Description",
"repo.issues.label_color": "Color",
@@ -2728,6 +2729,25 @@
"repo.settings.support_url": "Support / Product Page URL",
"repo.settings.support_url_help": "Shown when downloads are gated. Can point to your wiki, product page, or external support site.",
"repo.settings.custom_fields": "Custom Fields",
"repo.settings.manifest": "Manifest",
"repo.settings.manifest_desc": "Project identity, governance, and build settings from the moko-platform manifest. These are accessible via API for Actions workflows and the moko-platform CLI.",
"repo.settings.manifest_identity": "Identity",
"repo.settings.manifest_name": "Project Name",
"repo.settings.manifest_org": "Organization",
"repo.settings.manifest_description": "Description",
"repo.settings.manifest_version": "Version",
"repo.settings.manifest_license_spdx": "License (SPDX)",
"repo.settings.manifest_license_name": "License Name",
"repo.settings.manifest_governance": "Governance",
"repo.settings.manifest_platform": "Platform",
"repo.settings.manifest_standards_version": "Standards Version",
"repo.settings.manifest_standards_source": "Standards Source",
"repo.settings.manifest_build": "Build",
"repo.settings.manifest_language": "Language",
"repo.settings.manifest_package_type": "Package Type",
"repo.settings.manifest_entry_point": "Entry Point",
"repo.settings.manifest_save": "Save Manifest",
"repo.settings.manifest_saved": "Manifest settings saved.",
"repo.settings.metadata": "Metadata",
"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.",
@@ -2917,6 +2937,21 @@
"org.settings.custom_field_created": "Custom field created.",
"org.settings.custom_field_updated": "Custom field updated.",
"org.settings.custom_field_deleted": "Custom field deleted.",
"org.settings.issue_statuses": "Issue Statuses",
"org.settings.issue_statuses_desc": "Define custom issue statuses for all repositories in this organization. Statuses appear in the issue sidebar and can automatically close or reopen issues.",
"org.settings.issue_statuses_empty": "No custom issue statuses defined yet.",
"org.settings.issue_status_add": "Add Status",
"org.settings.issue_status_name": "Status Name",
"org.settings.issue_status_color": "Color",
"org.settings.issue_status_description": "Description",
"org.settings.issue_status_closes_issue": "Closes issue",
"org.settings.issue_status_closes_issue_help": "When this status is selected, the issue will be automatically closed.",
"org.settings.issue_status_closes": "Closes",
"org.settings.issue_status_sort_order": "Sort Order",
"org.settings.issue_status_inactive": "Inactive",
"org.settings.issue_status_created": "Issue status created.",
"org.settings.issue_status_updated": "Issue status updated.",
"org.settings.issue_status_deleted": "Issue status deleted.",
"org.settings.update_streams": "Update Server",
"org.settings.licensing": "Update Server",
"org.settings.licensing_desc": "Manage update feeds and optional license key gating across all repositories in this organization.",
+3
View File
@@ -1479,6 +1479,9 @@ func Routes() *web.Router {
Delete(reqToken(), repo.DeleteTopic)
}, reqAdmin())
}, reqAnyRepoReader())
m.Combo("/manifest", reqRepoReader(unit.TypeCode)).
Get(repo.GetRepoManifest).
Put(reqToken(), reqAdmin(), repo.UpdateRepoManifest)
// MokoGitea badge engine
m.Get("/badge/{type}.svg", repo.GetRepoBadge)
m.Get("/issue_templates", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(), repo.GetIssueTemplates)
+125
View File
@@ -0,0 +1,125 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package repo
import (
"encoding/json"
"net/http"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
// apiManifest is the JSON representation of a repo manifest.
type apiManifest struct {
Name string `json:"name"`
Org string `json:"org"`
Description string `json:"description"`
Version string `json:"version"`
LicenseSPDX string `json:"license_spdx"`
LicenseName string `json:"license_name"`
Platform string `json:"platform"`
StandardsVersion string `json:"standards_version"`
StandardsSource string `json:"standards_source"`
Language string `json:"language"`
PackageType string `json:"package_type"`
EntryPoint string `json:"entry_point"`
}
// GetRepoManifest returns the manifest settings for a repository.
func GetRepoManifest(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/manifest repository repoGetManifest
// ---
// summary: Get repo manifest settings
// produces:
// - application/json
// responses:
// "200":
// "$ref": "#/responses/Manifest"
// "404":
// "$ref": "#/responses/notFound"
m, err := repo_model.GetRepoManifest(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if m == nil {
// Return defaults from repo metadata.
ctx.JSON(http.StatusOK, &apiManifest{
Name: ctx.Repo.Repository.Name,
Org: ctx.Repo.Repository.OwnerName,
Description: ctx.Repo.Repository.Description,
})
return
}
ctx.JSON(http.StatusOK, &apiManifest{
Name: m.Name,
Org: m.Org,
Description: m.Description,
Version: m.Version,
LicenseSPDX: m.LicenseSPDX,
LicenseName: m.LicenseName,
Platform: m.Platform,
StandardsVersion: m.StandardsVersion,
StandardsSource: m.StandardsSource,
Language: m.Language,
PackageType: m.PackageType,
EntryPoint: m.EntryPoint,
})
}
// UpdateRepoManifest updates the manifest settings for a repository.
func UpdateRepoManifest(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/manifest repository repoUpdateManifest
// ---
// summary: Update repo manifest settings
// consumes:
// - application/json
// produces:
// - application/json
// responses:
// "200":
// "$ref": "#/responses/Manifest"
var req apiManifest
if err := json.NewDecoder(ctx.Req.Body).Decode(&req); err != nil {
ctx.APIError(http.StatusBadRequest, err)
return
}
m := &repo_model.RepoManifest{
RepoID: ctx.Repo.Repository.ID,
Name: req.Name,
Org: req.Org,
Description: req.Description,
Version: req.Version,
LicenseSPDX: req.LicenseSPDX,
LicenseName: req.LicenseName,
Platform: req.Platform,
StandardsVersion: req.StandardsVersion,
StandardsSource: req.StandardsSource,
Language: req.Language,
PackageType: req.PackageType,
EntryPoint: req.EntryPoint,
}
if err := repo_model.CreateOrUpdateRepoManifest(ctx, m); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, &apiManifest{
Name: m.Name,
Org: m.Org,
Description: m.Description,
Version: m.Version,
LicenseSPDX: m.LicenseSPDX,
LicenseName: m.LicenseName,
Platform: m.Platform,
StandardsVersion: m.StandardsVersion,
StandardsSource: m.StandardsSource,
Language: m.Language,
PackageType: m.PackageType,
EntryPoint: m.EntryPoint,
})
}
+112
View File
@@ -0,0 +1,112 @@
// 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 tplOrgIssueStatuses templates.TplName = "org/settings/issue_statuses"
// SettingsIssueStatuses shows the org-level issue statuses management page.
func SettingsIssueStatuses(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("org.settings.issue_statuses")
ctx.Data["PageIsOrgSettings"] = true
ctx.Data["PageIsSettingsIssueStatuses"] = true
defs, err := issues_model.GetAllIssueStatusDefsByOrg(ctx, ctx.Org.Organization.ID)
if err != nil {
ctx.ServerError("GetAllIssueStatusDefsByOrg", err)
return
}
ctx.Data["IssueStatuses"] = defs
ctx.HTML(http.StatusOK, tplOrgIssueStatuses)
}
// SettingsIssueStatusesCreatePost creates a new org-level issue status.
func SettingsIssueStatusesCreatePost(ctx *context.Context) {
sortOrder, _ := strconv.Atoi(ctx.FormString("sort_order"))
def := &issues_model.IssueStatusDef{
OrgID: ctx.Org.Organization.ID,
Name: ctx.FormString("name"),
Color: ctx.FormString("color"),
Description: ctx.FormString("description"),
ClosesIssue: ctx.FormString("closes_issue") == "on",
SortOrder: sortOrder,
IsActive: true,
}
if def.Name == "" {
ctx.Flash.Error("Status name is required")
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-statuses")
return
}
if err := issues_model.CreateIssueStatusDef(ctx, def); err != nil {
ctx.ServerError("CreateIssueStatusDef", err)
return
}
ctx.Flash.Success(ctx.Tr("org.settings.issue_status_created"))
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-statuses")
}
// SettingsIssueStatusesEditPost updates an org-level issue status.
func SettingsIssueStatusesEditPost(ctx *context.Context) {
id := ctx.PathParamInt64("id")
def, err := issues_model.GetIssueStatusDefByID(ctx, id)
if err != nil {
ctx.ServerError("GetIssueStatusDefByID", 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.ClosesIssue = ctx.FormString("closes_issue") == "on"
def.IsActive = ctx.FormString("is_active") == "on"
sortOrder, _ := strconv.Atoi(ctx.FormString("sort_order"))
def.SortOrder = sortOrder
if err := issues_model.UpdateIssueStatusDef(ctx, def); err != nil {
ctx.ServerError("UpdateIssueStatusDef", err)
return
}
ctx.Flash.Success(ctx.Tr("org.settings.issue_status_updated"))
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-statuses")
}
// SettingsIssueStatusesDeletePost deletes an org-level issue status.
func SettingsIssueStatusesDeletePost(ctx *context.Context) {
id := ctx.PathParamInt64("id")
def, err := issues_model.GetIssueStatusDefByID(ctx, id)
if err != nil {
ctx.ServerError("GetIssueStatusDefByID", err)
return
}
if def.OrgID != ctx.Org.Organization.ID {
ctx.NotFound(nil)
return
}
if err := issues_model.DeleteIssueStatusDef(ctx, id); err != nil {
ctx.ServerError("DeleteIssueStatusDef", err)
return
}
ctx.Flash.Success(ctx.Tr("org.settings.issue_status_deleted"))
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-statuses")
}
+59
View File
@@ -0,0 +1,59 @@
// 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/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
issue_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/issue"
)
// UpdateIssueCustomStatus handles POST to set a custom status on an issue.
// If the chosen status has ClosesIssue=true, the issue is automatically closed.
// If the chosen status has ClosesIssue=false and the issue is closed, it is reopened.
func UpdateIssueCustomStatus(ctx *context.Context) {
issueID := ctx.PathParamInt64("id")
statusID := ctx.FormInt64("status_id")
issue, err := issues_model.GetIssueByID(ctx, issueID)
if err != nil {
ctx.ServerError("GetIssueByID", err)
return
}
// Validate the status belongs to this repo's org (or is being cleared).
if statusID > 0 {
statusDef, err := issues_model.GetIssueStatusDefByID(ctx, statusID)
if err != nil {
ctx.ServerError("GetIssueStatusDefByID", err)
return
}
if statusDef.OrgID != ctx.Repo.Repository.OwnerID {
ctx.NotFound(nil)
return
}
// Handle automatic close/reopen based on the status definition.
if statusDef.ClosesIssue && !issue.IsClosed {
if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil {
log.Error("UpdateIssueCustomStatus: CloseIssue: %v", err)
}
} else if !statusDef.ClosesIssue && issue.IsClosed {
if err := issue_service.ReopenIssue(ctx, issue, ctx.Doer, ""); err != nil {
log.Error("UpdateIssueCustomStatus: ReopenIssue: %v", err)
}
}
}
if err := issues_model.SetIssueStatusID(ctx, issueID, statusID); err != nil {
ctx.ServerError("SetIssueStatusID", err)
return
}
ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, issue.Index), http.StatusSeeOther)
}
+8
View File
@@ -364,6 +364,14 @@ func ViewIssue(ctx *context.Context) {
}
ctx.Data["CustomFieldValues"] = customFieldValues
ctx.Data["CustomFieldOptions"] = fieldOptions
// Load custom issue status definitions for the sidebar.
issueStatusDefs, isErr := issues_model.GetIssueStatusDefsByOrg(ctx, ctx.Repo.Repository.OwnerID)
if isErr != nil {
log.Error("ViewIssue: GetIssueStatusDefsByOrg: %v", isErr)
}
ctx.Data["IssueStatusDefs"] = issueStatusDefs
upload.AddUploadContext(ctx, "comment")
if err := issue.LoadAttributes(ctx); err != nil {
+163
View File
@@ -0,0 +1,163 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package setting
import (
"encoding/xml"
"fmt"
"net/http"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
const tplSettingsManifest templates.TplName = "repo/settings/manifest"
// manifestXML mirrors the .mokogitea/manifest.xml schema for XML parsing.
type manifestXML struct {
XMLName xml.Name `xml:"moko-platform"`
Identity manifestIdentity `xml:"identity"`
Governance manifestGovernance `xml:"governance"`
Build manifestBuild `xml:"build"`
}
type manifestIdentity struct {
Name string `xml:"name"`
Org string `xml:"org"`
Description string `xml:"description"`
Version string `xml:"version"`
License manifestLicense `xml:"license"`
}
type manifestLicense struct {
SPDX string `xml:"spdx,attr"`
Name string `xml:",chardata"`
}
type manifestGovernance struct {
Platform string `xml:"platform"`
StandardsVersion string `xml:"standards-version"`
StandardsSource string `xml:"standards-source"`
}
type manifestBuild struct {
Language string `xml:"language"`
PackageType string `xml:"package-type"`
EntryPoint string `xml:"entry-point"`
}
// ManifestSettings displays the repo manifest settings page.
// On first visit, if no manifest exists in DB but .mokogitea/manifest.xml
// exists in the repo, it auto-migrates the XML values into the database.
func ManifestSettings(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.manifest")
ctx.Data["PageIsSettingsManifest"] = true
repoID := ctx.Repo.Repository.ID
manifest, err := repo_model.GetRepoManifest(ctx, repoID)
if err != nil {
ctx.ServerError("GetRepoManifest", err)
return
}
// Auto-detect and migrate .mokogitea/manifest.xml if no DB record exists.
if manifest == nil {
manifest = tryMigrateManifestXML(ctx)
}
if manifest == nil {
// No manifest found — provide empty defaults from repo metadata.
manifest = &repo_model.RepoManifest{
RepoID: repoID,
Name: ctx.Repo.Repository.Name,
Org: ctx.Repo.Repository.OwnerName,
Description: ctx.Repo.Repository.Description,
}
}
ctx.Data["Manifest"] = manifest
ctx.HTML(http.StatusOK, tplSettingsManifest)
}
// ManifestSettingsPost saves manifest settings from the form.
func ManifestSettingsPost(ctx *context.Context) {
manifest := &repo_model.RepoManifest{
RepoID: ctx.Repo.Repository.ID,
Name: ctx.FormString("name"),
Org: ctx.FormString("org"),
Description: ctx.FormString("description"),
Version: ctx.FormString("version"),
LicenseSPDX: ctx.FormString("license_spdx"),
LicenseName: ctx.FormString("license_name"),
Platform: ctx.FormString("platform"),
StandardsVersion: ctx.FormString("standards_version"),
StandardsSource: ctx.FormString("standards_source"),
Language: ctx.FormString("language"),
PackageType: ctx.FormString("package_type"),
EntryPoint: ctx.FormString("entry_point"),
}
if err := repo_model.CreateOrUpdateRepoManifest(ctx, manifest); err != nil {
ctx.ServerError("CreateOrUpdateRepoManifest", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.manifest_saved"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/manifest")
}
// tryMigrateManifestXML reads .mokogitea/manifest.xml from the repo,
// parses it, and stores the values in the DB. Returns nil if no file found.
func tryMigrateManifestXML(ctx *context.Context) *repo_model.RepoManifest {
if ctx.Repo.GitRepo == nil || ctx.Repo.Commit == nil {
return nil
}
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(".mokogitea/manifest.xml")
if err != nil || entry == nil {
return nil // no manifest.xml found — not an error
}
reader, err := entry.Blob().DataAsync()
if err != nil {
log.Error("ManifestMigrate: read blob: %v", err)
return nil
}
defer reader.Close()
var mxml manifestXML
if err := xml.NewDecoder(reader).Decode(&mxml); err != nil {
log.Error("ManifestMigrate: parse XML: %v", err)
return nil
}
manifest := &repo_model.RepoManifest{
RepoID: ctx.Repo.Repository.ID,
Name: mxml.Identity.Name,
Org: mxml.Identity.Org,
Description: mxml.Identity.Description,
Version: mxml.Identity.Version,
LicenseSPDX: mxml.Identity.License.SPDX,
LicenseName: mxml.Identity.License.Name,
Platform: mxml.Governance.Platform,
StandardsVersion: mxml.Governance.StandardsVersion,
StandardsSource: mxml.Governance.StandardsSource,
Language: mxml.Build.Language,
PackageType: mxml.Build.PackageType,
EntryPoint: mxml.Build.EntryPoint,
}
if err := repo_model.CreateOrUpdateRepoManifest(ctx, manifest); err != nil {
log.Error("ManifestMigrate: save to DB: %v", err)
return nil
}
log.Info("ManifestMigrate: migrated .mokogitea/manifest.xml for repo %s/%s",
ctx.Repo.Repository.OwnerName, ctx.Repo.Repository.Name)
ctx.Flash.Info(fmt.Sprintf("Manifest settings imported from .mokogitea/manifest.xml. You can now delete the file from the repository."))
return manifest
}
+8
View File
@@ -1067,6 +1067,12 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Post("/{id}/edit", org.SettingsCustomFieldsEditPost)
m.Post("/{id}/delete", org.SettingsCustomFieldsDeletePost)
})
m.Group("/issue-statuses", func() {
m.Get("", org.SettingsIssueStatuses)
m.Post("", org.SettingsIssueStatusesCreatePost)
m.Post("/{id}/edit", org.SettingsIssueStatusesEditPost)
m.Post("/{id}/delete", org.SettingsIssueStatusesDeletePost)
})
}, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "PageIsOrgSettings", true))
}, context.OrgAssignment(context.OrgAssignmentOptions{RequireOwner: true}))
}, reqSignIn)
@@ -1193,6 +1199,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Combo("/advanced").Get(repo_setting.AdvancedSettings).Post(web.Bind(forms.RepoSettingForm{}), repo_setting.SettingsPost)
}, repo_setting.SettingsCtxData)
m.Combo("/licensing").Get(repo_setting.LicensingSettings).Post(repo_setting.LicensingSettingsPost)
m.Combo("/manifest").Get(repo_setting.ManifestSettings).Post(repo_setting.ManifestSettingsPost)
m.Combo("/metadata").Get(repo_setting.Metadata).Post(repo_setting.MetadataPost)
m.Group("/collaboration", func() {
@@ -1399,6 +1406,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Post("/assignee", reqRepoIssuesOrPullsWriter, repo.UpdateIssueAssignee)
m.Post("/status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueStatus)
m.Post("/{id}/custom-fields/{field_id}", reqRepoIssuesOrPullsWriter, repo.UpdateIssueCustomField)
m.Post("/{id}/custom-status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueCustomStatus)
m.Post("/delete", reqRepoAdmin, repo.BatchDeleteIssues)
m.Delete("/unpin/{index}", reqRepoAdmin, repo.IssueUnpin)
m.Post("/move_pin", reqRepoAdmin, repo.IssuePinMove)
+98
View File
@@ -0,0 +1,98 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package repository
import (
"context"
"encoding/xml"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
)
// manifestXML mirrors the .mokogitea/manifest.xml schema for XML parsing.
type manifestXML struct {
XMLName xml.Name `xml:"moko-platform"`
Identity manifestIdentity `xml:"identity"`
Governance manifestGovernance `xml:"governance"`
Build manifestBuild `xml:"build"`
}
type manifestIdentity struct {
Name string `xml:"name"`
Org string `xml:"org"`
Description string `xml:"description"`
Version string `xml:"version"`
License manifestLicense `xml:"license"`
}
type manifestLicense struct {
SPDX string `xml:"spdx,attr"`
Name string `xml:",chardata"`
}
type manifestGovernance struct {
Platform string `xml:"platform"`
StandardsVersion string `xml:"standards-version"`
StandardsSource string `xml:"standards-source"`
}
type manifestBuild struct {
Language string `xml:"language"`
PackageType string `xml:"package-type"`
EntryPoint string `xml:"entry-point"`
}
// SyncManifestFromCommit reads .mokogitea/manifest.xml from the given commit
// and upserts the values into the repo_manifest database table.
// This is called on push to the default branch to keep the database in sync
// with the XML file. If no manifest.xml exists, this is a no-op.
func SyncManifestFromCommit(ctx context.Context, repo *repo_model.Repository, commit *git.Commit) {
if commit == nil {
return
}
entry, err := commit.GetTreeEntryByPath(".mokogitea/manifest.xml")
if err != nil || entry == nil {
return // no manifest.xml — not an error
}
reader, err := entry.Blob().DataAsync()
if err != nil {
log.Error("SyncManifest: read blob for %s: %v", repo.FullName(), err)
return
}
defer reader.Close()
var mxml manifestXML
decoder := xml.NewDecoder(reader)
if err := decoder.Decode(&mxml); err != nil {
log.Error("SyncManifest: parse XML for %s: %v", repo.FullName(), err)
return
}
manifest := &repo_model.RepoManifest{
RepoID: repo.ID,
Name: mxml.Identity.Name,
Org: mxml.Identity.Org,
Description: mxml.Identity.Description,
Version: mxml.Identity.Version,
LicenseSPDX: mxml.Identity.License.SPDX,
LicenseName: mxml.Identity.License.Name,
Platform: mxml.Governance.Platform,
StandardsVersion: mxml.Governance.StandardsVersion,
StandardsSource: mxml.Governance.StandardsSource,
Language: mxml.Build.Language,
PackageType: mxml.Build.PackageType,
EntryPoint: mxml.Build.EntryPoint,
}
if err := repo_model.CreateOrUpdateRepoManifest(ctx, manifest); err != nil {
log.Error("SyncManifest: save for %s: %v", repo.FullName(), err)
return
}
log.Info("SyncManifest: synced .mokogitea/manifest.xml for %s", repo.FullName())
}
+2
View File
@@ -193,6 +193,8 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
if err := DelRepoDivergenceFromCache(ctx, repo.ID); err != nil {
log.Error("DelRepoDivergenceFromCache: %v", err)
}
// Auto-sync .mokogitea/manifest.xml to database on default branch push
SyncManifestFromCommit(ctx, repo, newCommit)
} else {
if err := DelDivergenceFromCache(repo.ID, branch); err != nil {
log.Error("DelDivergenceFromCache: %v", err)
@@ -0,0 +1,93 @@
{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings issue-statuses")}}
<h4 class="ui top attached header">
{{ctx.Locale.Tr "org.settings.issue_statuses"}}
</h4>
<div class="ui attached segment">
<p class="text grey">{{ctx.Locale.Tr "org.settings.issue_statuses_desc"}}</p>
{{if .IssueStatuses}}
<table class="ui compact table">
<thead>
<tr>
<th>{{ctx.Locale.Tr "org.settings.issue_status_color"}}</th>
<th>{{ctx.Locale.Tr "org.settings.issue_status_name"}}</th>
<th>{{ctx.Locale.Tr "org.settings.issue_status_closes_issue"}}</th>
<th>{{ctx.Locale.Tr "org.settings.issue_status_sort_order"}}</th>
<th></th>
</tr>
</thead>
<tbody>
{{range .IssueStatuses}}
<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">{{ctx.Locale.Tr "org.settings.issue_status_inactive"}}</span>{{end}}
{{if .Description}}<br><small class="text grey">{{.Description}}</small>{{end}}
</td>
<td>
{{if .ClosesIssue}}
<span class="ui mini purple label">{{ctx.Locale.Tr "org.settings.issue_status_closes"}}</span>
{{else}}
<span class="text grey">-</span>
{{end}}
</td>
<td>{{.SortOrder}}</td>
<td class="tw-text-right">
<form method="post" action="{{$.OrgLink}}/settings/issue-statuses/{{.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_statuses_empty"}}</p>
</div>
{{end}}
<div class="divider"></div>
<h5>{{ctx.Locale.Tr "org.settings.issue_status_add"}}</h5>
<form class="ui form" method="post" action="{{.OrgLink}}/settings/issue-statuses">
{{.CsrfTokenHtml}}
<div class="three fields">
<div class="required field">
<label>{{ctx.Locale.Tr "org.settings.issue_status_name"}}</label>
<input name="name" required placeholder="e.g. In Progress, Won't Fix, Blocked">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.issue_status_color"}}</label>
<input name="color" type="color" value="#0075ff">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.issue_status_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_status_description"}}</label>
<input name="description" placeholder="Help text shown to users">
</div>
<div class="field">
<div class="ui checkbox tw-mt-4">
<input name="closes_issue" type="checkbox">
<label>{{ctx.Locale.Tr "org.settings.issue_status_closes_issue"}}</label>
</div>
<p class="help">{{ctx.Locale.Tr "org.settings.issue_status_closes_issue_help"}}</p>
</div>
</div>
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "org.settings.issue_status_add"}}</button>
</form>
</div>
{{template "org/settings/layout_footer" .}}
+3
View File
@@ -31,6 +31,9 @@
<a class="{{if .PageIsSettingsCustomFields}}active {{end}}item" href="{{.OrgLink}}/settings/custom-fields">
{{svg "octicon-list-unordered"}} {{ctx.Locale.Tr "org.settings.custom_fields"}}
</a>
<a class="{{if .PageIsSettingsIssueStatuses}}active {{end}}item" href="{{.OrgLink}}/settings/issue-statuses">
{{svg "octicon-tasklist"}} {{ctx.Locale.Tr "org.settings.issue_statuses"}}
</a>
{{if .EnableActions}}
<details class="item toggleable-item" {{if or .PageIsOrgSettingsActionsGeneral .PageIsSharedSettingsRunners .PageIsSharedSettingsSecrets .PageIsSharedSettingsVariables}}open{{end}}>
<summary>{{svg "octicon-play"}} {{ctx.Locale.Tr "actions.actions"}}</summary>
@@ -0,0 +1,33 @@
{{if .IssueStatusDefs}}
<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.status"}}</span>
{{$canModify := .HasIssuesOrPullsWritePermission}}
{{if $canModify}}
<form method="post" action="{{.RepoLink}}/issues/{{.Issue.ID}}/custom-status" class="tw-inline">
{{$.CsrfTokenHtml}}
<select name="status_id" class="ui compact mini dropdown tw-max-w-48" onchange="this.form.submit()">
<option value="0">—</option>
{{range .IssueStatusDefs}}
<option value="{{.ID}}" {{if eq .ID $.Issue.StatusID}}selected{{end}}
{{if .Color}}style="border-left: 3px solid {{.Color}}"{{end}}>
{{.Name}}{{if .ClosesIssue}}{{end}}
</option>
{{end}}
</select>
</form>
{{else}}
{{$found := false}}
{{range .IssueStatusDefs}}
{{if eq .ID $.Issue.StatusID}}
{{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}}
@@ -7,6 +7,8 @@
{{template "repo/issue/sidebar/label_list" $.IssuePageMetaData}}
{{template "repo/issue/sidebar/issue_status" $}}
{{template "repo/issue/sidebar/custom_fields" $}}
{{template "repo/issue/sidebar/milestone_list" $.IssuePageMetaData}}
+88
View File
@@ -0,0 +1,88 @@
{{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings manifest")}}
<h4 class="ui top attached header">
{{ctx.Locale.Tr "repo.settings.manifest"}}
</h4>
<div class="ui attached segment">
<p class="text grey">{{ctx.Locale.Tr "repo.settings.manifest_desc"}}</p>
<form class="ui form" method="post" action="{{.RepoLink}}/settings/manifest">
{{.CsrfTokenHtml}}
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.manifest_identity"}}</h5>
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_name"}}</label>
<input name="name" value="{{.Manifest.Name}}" placeholder="Project name">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_org"}}</label>
<input name="org" value="{{.Manifest.Org}}" placeholder="Organization">
</div>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_description"}}</label>
<input name="description" value="{{.Manifest.Description}}" placeholder="Project description">
</div>
<div class="three fields">
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_version"}}</label>
<input name="version" value="{{.Manifest.Version}}" placeholder="e.g. 06.00.00">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_license_spdx"}}</label>
<input name="license_spdx" value="{{.Manifest.LicenseSPDX}}" placeholder="e.g. GPL-3.0-or-later">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_license_name"}}</label>
<input name="license_name" value="{{.Manifest.LicenseName}}" placeholder="e.g. GNU General Public License v3">
</div>
</div>
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.manifest_governance"}}</h5>
<div class="three fields">
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_platform"}}</label>
<select name="platform" class="ui dropdown">
<option value="">—</option>
{{$platform := .Manifest.Platform}}
{{range $val := StringUtils.Split "go,php,node,python,ruby,java,dotnet,rust" ","}}
<option value="{{$val}}" {{if eq $val $platform}}selected{{end}}>{{$val}}</option>
{{end}}
</select>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_standards_version"}}</label>
<input name="standards_version" value="{{.Manifest.StandardsVersion}}" placeholder="e.g. 05.00.00">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_standards_source"}}</label>
<input name="standards_source" value="{{.Manifest.StandardsSource}}" placeholder="URL to standards repo">
</div>
</div>
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.manifest_build"}}</h5>
<div class="three fields">
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_language"}}</label>
<input name="language" value="{{.Manifest.Language}}" placeholder="e.g. Go, PHP, TypeScript">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_package_type"}}</label>
<select name="package_type" class="ui dropdown">
<option value="">—</option>
{{$pkgType := .Manifest.PackageType}}
{{range $val := StringUtils.Split "application,library,plugin,module,component,package,template" ","}}
<option value="{{$val}}" {{if eq $val $pkgType}}selected{{end}}>{{$val}}</option>
{{end}}
</select>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_entry_point"}}</label>
<input name="entry_point" value="{{.Manifest.EntryPoint}}" placeholder="e.g. ./ or src/index.ts">
</div>
</div>
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "repo.settings.manifest_save"}}</button>
</form>
</div>
{{template "repo/settings/layout_footer" .}}
+3
View File
@@ -12,6 +12,9 @@
{{svg "octicon-broadcast"}} {{ctx.Locale.Tr "repo.settings.licensing_section"}}
</a>
{{end}}
<a class="{{if .PageIsSettingsManifest}}active {{end}}item" href="{{.RepoLink}}/settings/manifest">
{{svg "octicon-file-code"}} {{ctx.Locale.Tr "repo.settings.manifest"}}
</a>
<a class="{{if .PageIsSettingsMetadata}}active {{end}}item" href="{{.RepoLink}}/settings/metadata">
{{svg "octicon-list-unordered"}} {{ctx.Locale.Tr "repo.settings.metadata"}}
</a>
+136
View File
@@ -0,0 +1,136 @@
# Custom Fields
Custom fields allow organizations to define structured metadata that appears in issue sidebars and repository settings across all repos in the organization.
## Overview
Custom fields are defined at the **organization level** in Org Settings > Custom Fields. Each field has a scope:
- **Issue scope** — appears in the issue sidebar for inline editing
- **Repo scope** — appears in Repository Settings > Metadata for repo-level values
## Field Types
| Type | Description | Example |
|------|-------------|---------|
| `text` | Free-form text input | "Affected Component" |
| `number` | Numeric input | "Story Points" |
| `date` | Date picker | "Due Date" |
| `dropdown` | Select from predefined options | "Priority: Low/Medium/High/Critical" |
| `checkbox` | Boolean toggle | "Requires QA" |
| `url` | URL input | "Design Link" |
## Org Settings
Navigate to **Organization Settings > Custom Fields** to manage field definitions.
Each field has:
| Field | Description |
|-------|-------------|
| Name | Display name |
| Scope | `issue` (sidebar) or `repo` (metadata) |
| Type | One of: text, number, date, dropdown, checkbox, url |
| Options | JSON array for dropdown options (e.g., `["Low","Medium","High"]`) |
| Description | Help text (shown as tooltip) |
| Sort Order | Controls display order |
| Is Active | Inactive fields are hidden from new forms but preserved on existing entities |
## Issue Sidebar
Issue-scoped fields appear in the sidebar between labels and milestones. Dropdown fields auto-submit on change. Text/number/date fields display their current value.
Each field renders as an inline form posting to:
```
POST /{owner}/{repo}/issues/{issue_id}/custom-fields/{field_id}
```
## Repository Metadata
Repo-scoped fields appear on the **Repository Settings > Metadata** page. All fields for the org are shown with their current values for the repository. Values are saved via form POST.
## Issue Template Integration
Custom fields can be pre-filled from issue template YAML frontmatter:
```yaml
name: Bug Report
about: Report a bug
custom_fields:
Priority: High
Affected Component: Backend
```
When a new issue is created from this template, the sidebar shows the custom fields with the specified defaults pre-selected.
## API
### Issue-Level Custom Fields
| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/v1/repos/{owner}/{repo}/issues/{index}/custom-fields` | Get field values for an issue |
| PUT | `/api/v1/repos/{owner}/{repo}/issues/{index}/custom-fields` | Set field values (name-value map) |
### Repo-Level Metadata
| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/v1/repos/{owner}/{repo}/metadata` | Get repo metadata field values |
| PUT | `/api/v1/repos/{owner}/{repo}/metadata` | Set repo metadata field values |
### Org-Level Definitions
| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/v1/orgs/{org}/custom-fields` | List all field definitions |
| POST | `/api/v1/orgs/{org}/custom-fields` | Create a field definition |
| DELETE | `/api/v1/orgs/{org}/custom-fields/{id}` | Delete a field definition |
## Database
### Tables
**`custom_field_def`** — field definitions (org-level)
| Column | Type | Description |
|--------|------|-------------|
| id | bigint | Primary key |
| owner_id | bigint | Org ID (0 = legacy repo-level) |
| repo_id | bigint | 0 for org-level definitions |
| scope | varchar(10) | `issue` or `repo` |
| name | varchar | Field name |
| field_type | varchar(20) | text, number, date, dropdown, checkbox, url |
| description | text | Help text |
| options | text | JSON array for dropdown options |
| required | bool | Whether the field is required |
| sort_order | int | Display order |
| is_active | bool | Visibility flag |
**`custom_field_value`** — field values (per entity)
| Column | Type | Description |
|--------|------|-------------|
| id | bigint | Primary key |
| entity_id | bigint | Issue ID or Repo ID |
| entity_type | varchar(10) | `issue` or `repo` |
| field_id | bigint | FK to custom_field_def |
| value | text | The stored value |
### Cascade on Delete
When a field definition is deleted, all associated values in `custom_field_value` are also deleted.
## Relationship to Other Systems
| System | Relationship |
|--------|-------------|
| Update Server | Repo-scoped custom fields with specific names (Extension Name, Display Name, etc.) are read by the update feed generators as the highest-priority metadata source. |
| Manifest Settings | Manifest fields follow the moko-platform schema and are separate from custom fields. Custom fields are user-defined; manifest fields are standardized. |
| Issue Statuses | Custom statuses are a separate feature with their own dedicated table and UI, not implemented as custom fields. |
---
| Revision | Date | Author | Description |
|---|---|---|---|
| 1.0 | 2026-06-06 | Jonathan Miller (@jmiller) | Initial version |
+107
View File
@@ -0,0 +1,107 @@
# Custom Issue Statuses
Custom issue statuses extend Gitea's binary Open/Closed model with org-defined workflow states. Each status has a name, color, and an optional "closes issue" flag that triggers automatic close/reopen when selected.
## Overview
Statuses are defined at the **organization level** and appear in the issue sidebar for all repositories under that organization. This is the same pattern as org-level labels and custom fields.
### Key Concepts
- **Status definitions** are managed in Org Settings > Issue Statuses
- **Status selection** appears as a dropdown in the issue sidebar
- **Auto close/reopen** — selecting a status with `closes_issue = true` automatically closes the issue; switching to a non-closing status reopens it
- **Status is supplemental** — the existing Open/Closed binary state is preserved; statuses add granularity on top
## Org Settings
Navigate to **Organization Settings > Issue Statuses** to manage status definitions.
Each status has:
| Field | Description |
|-------|-------------|
| Name | Display name (e.g., "In Progress", "Won't Fix", "Blocked") |
| Color | Hex color for visual distinction (e.g., `#2563eb`) |
| Description | Help text shown to users |
| Closes Issue | When checked, selecting this status automatically closes the issue |
| Sort Order | Controls display order in dropdowns (ascending) |
| Is Active | Inactive statuses are hidden from dropdowns but preserved on existing issues |
### Example Statuses
| Status | Color | Closes Issue | Use Case |
|--------|-------|:------------:|----------|
| In Progress | Blue | No | Work is actively being done |
| Needs Info | Yellow | No | Waiting for more information from reporter |
| Blocked | Red | No | Cannot proceed due to external dependency |
| Won't Fix | Gray | Yes | Decided not to address this issue |
| Duplicate | Purple | Yes | Already tracked in another issue |
| Resolved | Green | Yes | Fix has been implemented and verified |
## Issue Sidebar
When an organization has custom statuses defined, a **Status** dropdown appears in the issue sidebar between labels and custom fields. The dropdown:
- Shows all active status definitions for the repo's organization
- Auto-submits on change (no save button needed)
- Displays a colored left border on each option
- Shows a power symbol on statuses that close the issue
- Selecting "—" (empty) clears the status
### Auto Close/Reopen Behavior
| Current State | Selected Status | Result |
|:---:|---|---|
| Open | Status with `closes_issue = true` | Issue is closed automatically |
| Closed | Status with `closes_issue = false` | Issue is reopened automatically |
| Open | Status with `closes_issue = false` | Status set, issue stays open |
| Closed | Status with `closes_issue = true` | Status set, issue stays closed |
All close/reopen actions go through the standard Gitea service layer, so webhooks, notifications, and timeline events fire normally.
## Database
### Tables
**`issue_status_def`** (migration v346) — org-level status definitions
| Column | Type | Description |
|--------|------|-------------|
| id | bigint | Primary key |
| org_id | bigint | Organization ID |
| name | varchar | Status name |
| color | varchar(7) | Hex color |
| description | text | Help text |
| closes_issue | bool | Auto-close flag |
| sort_order | int | Display order |
| is_active | bool | Visibility flag |
**`issue`** table — added `status_id` column (bigint, default 0)
### Cascade on Delete
When a status definition is deleted, all issues referencing it have their `status_id` set to 0 (cleared). Issues are not closed or reopened during deletion.
## Routes
### Web Routes (Org Settings)
| Method | Path | Handler |
|--------|------|---------|
| GET | `/org/{org}/settings/issue-statuses` | `SettingsIssueStatuses` |
| POST | `/org/{org}/settings/issue-statuses` | `SettingsIssueStatusesCreatePost` |
| POST | `/org/{org}/settings/issue-statuses/{id}/edit` | `SettingsIssueStatusesEditPost` |
| POST | `/org/{org}/settings/issue-statuses/{id}/delete` | `SettingsIssueStatusesDeletePost` |
### Web Routes (Issue Sidebar)
| Method | Path | Handler |
|--------|------|---------|
| POST | `/{owner}/{repo}/issues/{id}/custom-status` | `UpdateIssueCustomStatus` |
---
| Revision | Date | Author | Description |
|---|---|---|---|
| 1.0 | 2026-06-06 | Jonathan Miller (@jmiller) | Initial version |
+50
View File
@@ -0,0 +1,50 @@
# MokoGitea
Moko Consulting's custom fork of [Gitea](https://gitea.com), extending the self-hosted Git service with commercial licensing, update feeds, custom issue workflows, and org-level management features.
| Field | Value |
|-----|-----|
| **Language** | Go |
| **License** | MIT |
| **Upstream** | Gitea 1.26.1 |
| **Version** | v1.26.1-moko.06.04.00 |
| **Platform** | [Gitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) |
---
## Features
- **Commercial License System** — Package-based license keys with download gating, domain restriction, key expiry, and payment webhook API
- **Update Server** — Built-in update feeds for Joomla, WordPress, Dolibarr, Composer, Drupal, PrestaShop, and WHMCS
- **Custom Issue Statuses** — Org-defined workflow states (In Progress, Blocked, Won't Fix) with auto close/reopen
- **Custom Fields** — Org-level field definitions for issues (sidebar) and repos (metadata) with dropdown, text, number, date, checkbox, and URL types
- **Manifest Settings** — Per-repo identity/governance/build metadata with REST API for CI/CD integration
- **Well-Known File Tabs** — README/LICENSE/CONTRIBUTING/SECURITY/CHANGELOG tabs on repo home page
- **Org-Level Branch Protection** — Organization-scoped rulesets that cascade to all repos. Supports glob patterns. Full CRUD API
- **Enterprise Sub-Orgs** — Parent-child organization hierarchy
- **Three-Level Visibility** — Public (200), Private (403), Hidden (404) for repositories
- **Configurable Help/Support URLs** — Replace hardcoded docs.gitea.com links via HELP_URL and SUPPORT_URL in app.ini
- **Project Board API** — REST API endpoints for managing project boards, columns, and cards
- **Custom branding** — Moko Consulting visual identity (logos, colors, footer)
## Pages
| Page | Description |
|---|---|
| [Branding](Branding) | Custom branding and visual identity details |
| [Custom Fields](Custom-Fields) | Org-level custom fields for issues and repos |
| [Custom Issue Statuses](Custom-Issue-Statuses) | Org-defined workflow states with auto close/reopen |
| [Deployment](Deployment) | Production deployment guide |
| [Manifest Settings](Manifest-Settings) | Per-repo manifest settings and REST API |
| [Org Branch Protection API](Org-Branch-Protection-API) | Org-level branch protection rulesets and API reference |
| [Project API](Project-API) | Custom API endpoint reference for project boards |
| [Roadmap](Roadmap) | Development roadmap and planned features |
---
| Revision | Date | Author | Description |
|---|---|---|---|
| 4.0 | 2026-06-06 | Jonathan Miller (@jmiller) | Add manifest settings, custom statuses, custom fields, well-known tabs, update version to v1.26.1-moko.06.04.00 |
| 3.0 | 2026-05-12 | Jonathan Miller (@jmiller) | Add org branch protection, help URLs, version convention |
| 2.0 | 2026-05-10 | Jonathan Miller (@jmiller) | Rewrite with detailed features and fork documentation |
| 1.0 | 2026-05-09 | Jonathan Miller (@jmiller) | Initial version |
+111
View File
@@ -0,0 +1,111 @@
# Manifest Settings
The manifest settings feature provides a centralized way to store and manage project identity, governance, and build metadata for each repository. Settings are stored in the database and exposed via both a web UI and REST API.
## Overview
Each repository can have a manifest that describes:
- **Identity** — project name, organization, description, version, and license
- **Governance** — platform type, moko-platform standards version, and standards source URL
- **Build** — language, package type, and entry point
These settings replace the legacy `.mokogitea/manifest.xml` file-based approach.
## Repo Settings Page
Navigate to **Repository Settings > Manifest** to view and edit manifest fields.
| Section | Fields |
|---------|--------|
| Identity | Name, Org, Description, Version, License SPDX, License Name |
| Governance | Platform, Standards Version, Standards Source |
| Build | Language, Package Type, Entry Point |
### Auto-Migration from manifest.xml
On first visit to the Manifest settings page, if no manifest exists in the database but a `.mokogitea/manifest.xml` file exists in the repository, the system will:
1. Parse the XML and extract all fields
2. Store them in the database
3. Display a flash message indicating migration was successful
4. The manifest.xml file can then be manually deleted from the repository
If a field already has a value in the database (e.g., from org-level custom fields), the existing value is preserved and the manifest.xml value is skipped.
## REST API
The manifest API allows Actions workflows and the moko-platform CLI to read and write manifest settings programmatically.
### Get Manifest
```
GET /api/v1/repos/{owner}/{repo}/manifest
Authorization: token {access_token}
```
Returns the current manifest settings. If no manifest has been saved, returns defaults derived from repository metadata (name, owner, description).
**Response:**
```json
{
"name": "MokoGitea",
"org": "MokoConsulting",
"description": "Moko fork of Gitea",
"version": "06.04.00",
"license_spdx": "GPL-3.0-or-later",
"license_name": "GNU General Public License v3",
"platform": "go",
"standards_version": "05.00.00",
"standards_source": "https://code.mokoconsulting.tech/MokoConsulting/moko-platform",
"language": "Go",
"package_type": "application",
"entry_point": "./"
}
```
### Update Manifest
```
PUT /api/v1/repos/{owner}/{repo}/manifest
Authorization: token {access_token}
Content-Type: application/json
```
Requires repo admin permission. Accepts the same JSON structure as the GET response. Creates or updates the manifest.
### Usage in Actions Workflows
```yaml
steps:
- name: Read manifest version
run: |
VERSION=$(curl -s "$GITEA_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY/manifest" \
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" | jq -r '.version')
echo "Current version: $VERSION"
- name: Bump version
run: |
curl -s -X PUT "$GITEA_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY/manifest" \
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
-H "Content-Type: application/json" \
-d "{\"version\": \"$NEW_VERSION\"}"
```
## Database
Manifest settings are stored in the `repo_manifest` table (migration v347). One row per repository, keyed by `repo_id`.
## Relationship to Other Systems
| System | Relationship |
|--------|-------------|
| Update Server | The update server generators read from both manifest settings and update_stream_config. Manifest provides identity metadata; update_stream_config provides feed-specific settings. |
| Custom Fields | Repo-scoped custom fields (org settings) are separate from manifest fields. Custom fields are user-defined; manifest fields follow the moko-platform schema. |
| moko-platform CLI | The CLI reads manifest settings via the API for version bumping, build decisions, and cross-repo syncing (see issue #505). |
---
| Revision | Date | Author | Description |
|---|---|---|---|
| 1.0 | 2026-06-06 | Jonathan Miller (@jmiller) | Initial version |