Compare commits

...

43 Commits

Author SHA1 Message Date
jmiller 1d857d8205 Merge pull request 'chore: update wiki' (#544) from chore/wiki-mcp-update 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 / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
2026-06-06 22:26:54 +00:00
Jonathan Miller 4f9aeb7b85 chore: update wiki - version, first-class fields, security, roadmap
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 1s
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 2s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m43s
- Update version to v1.26.1-moko.06.10.00
- Document 13 default statuses including Pending states
- Document status replacing close button in comment form
- Add Security scanning, MCP, Wiki folders to features list
- Update roadmap with completed and planned items
- Add features/ folder link to pages table
2026-06-06 17:26:19 -05:00
jmiller 1178eaec62 Merge pull request 'feat(issues): first-class Type field + list badges' (#543) from feat/issue-type-first-class 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 3s
2026-06-06 22:13:40 +00:00
Jonathan Miller dd1454c3cf feat(issues): first-class Type field + status/priority/type badges in issue list
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
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
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 3s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m10s
- IssueTypeDef model with auto-seed defaults (Bug, Feature, Enhancement, Task, Documentation, Security)
- Migration v350 adding issue_type_def table + type_id on issues
- Type dropdown in issue sidebar
- Type, Priority, Status colored badges in issue list view
- Status/Priority/Type definitions loaded in issue list handler
2026-06-06 17:12:44 -05:00
jmiller c539bed4d3 Merge pull request 'fix(ui): dashboard issue count badges' (#542) from fix/dashboard-badges 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 21:56:28 +00:00
Jonathan Miller 135b37edf1 fix(ui): use badge labels instead of strong tags for dashboard issue counts
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 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
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 4s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m20s
The sidebar counts (In your repositories, Assigned to you, etc.)
were rendering as bold text concatenated with the label. Now uses
ui small label spans for proper badge styling.
2026-06-06 16:54:26 -05:00
jmiller 48cf445e79 Merge pull request 'feat(security): add Security tab to repo navigation' (#541) from feat/508-security-scanner 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 3s
2026-06-06 21:36:36 +00:00
Jonathan Miller 72708b5a99 feat(security): add Security tab to repo navigation (#508)
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 1s
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 7s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m19s
Add a top-level Security tab in the repo header (visible to admins
only) showing alerts, scan controls, and severity badges. Links to
settings page for scanner configuration. Alert file paths link
directly to the source file.
2026-06-06 16:35:55 -05:00
jmiller 948860e8ac Merge pull request 'feat(security): built-in security scanning platform (#508)' (#540) from feat/508-security-scanner 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 21:24:11 +00:00
Jonathan Miller f7c1904625 feat(security): built-in security scanning platform (#508)
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 1s
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 5s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m44s
Add a pluggable security scanning framework with secret detection
as the first scanner module. Scans run on push to default branch
and on-demand via the Security settings page.

Includes:
- Scanner interface for pluggable scanner types
- Secret scanner with 15 built-in patterns (AWS, GitHub, Stripe, etc.)
- SecurityAlert model with fingerprint-based dedup
- SecurityScannerConfig per-repo settings
- Migration v349 for security tables
- Repo settings Security page with alerts table
- Scan Now button for on-demand scanning
- Alert resolve/dismiss actions
- Push-time scanning in post-receive hook
2026-06-06 16:23:08 -05:00
jmiller e3b2df4aac Merge pull request 'fix(wiki): folder listing template' (#539) from feat/79-wiki-folders 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 20:52:19 +00:00
Jonathan Miller 6cd4a19ed6 fix(wiki): render folder listing template instead of start page
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
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m4s
When navigating to a wiki folder with no index file, Wiki() handler
now renders the wiki view template with IsWikiFolder flag instead of
falling back to the empty wiki start page.
2026-06-06 15:51:24 -05:00
jmiller 4d73c6a939 Merge pull request 'fix(wiki): directory check before raw redirect' (#538) from feat/79-wiki-folders 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 20:35:25 +00:00
Jonathan Miller df91ed2aac fix(wiki): check directory before file lookup to prevent raw redirect
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
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m0s
Directory paths were being found by wikiEntryByName as non-.md entries,
triggering a redirect to /wiki/raw/. Now checks for directories first
and handles index file lookup before the file/raw detection.
2026-06-06 15:34:49 -05:00
jmiller 8639d85fe7 Merge pull request 'fix(wiki): type mismatch in folder listing' (#537) from feat/79-wiki-folders 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 20:28:57 +00:00
Jonathan Miller e7a79d973e fix(wiki): type mismatch in folder listing
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 / Access control (push) Successful in 2s
Generic: Repo Health / Site Health (push) Has been skipped
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 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 54s
2026-06-06 15:28:20 -05:00
jmiller 6c5394107e Merge pull request 'fix(wiki): proper display names in tree' (#536) from feat/79-wiki-folders 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 20:25:41 +00:00
Jonathan Miller 0a158e9ec3 fix(wiki): use GitPathToWebPath for proper display names in tree
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
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 57s
Fixes the .- dash marker showing in sidebar tree and folder listings.
Uses the proper path conversion functions instead of raw TrimSuffix.
2026-06-06 15:24:55 -05:00
jmiller 48c354fbb4 Merge pull request 'fix(wiki): preserve slashes in page titles' (#535) from feat/79-wiki-folders 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 20:16:52 +00:00
Jonathan Miller 41c42b968e fix(wiki): preserve slashes in page titles for folder creation
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
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
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 6s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m0s
UserTitleToWebPath now splits on / and sanitizes each segment
independently, preserving the directory structure. This allows
creating pages like "features/Custom-Fields" as actual nested files.
2026-06-06 15:15:57 -05:00
jmiller d64dc7cf45 Merge pull request 'feat(wiki): hierarchical folder navigation (#79)' (#534) from feat/79-wiki-folders 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 4s
2026-06-06 20:10:24 +00:00
Jonathan Miller 6010841ee7 feat(wiki): hierarchical folder navigation with sidebar tree (#79)
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
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 2s
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) Successful in 1s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m45s
Support real subdirectories in wiki instead of escaping / to %2F.
When navigating to a folder, tries README.md, Home.md, index.md
as index pages. If none found, shows a file/folder listing.

Includes:
- Stop escaping / in WebPathFromRequest (wiki_path.go)
- Folder detection with index file fallback
- Auto-generated sidebar folder tree
- Breadcrumb navigation for nested paths
- Folder listing view when no index page exists
- CSS for tree sidebar and folder listing
2026-06-06 15:09:23 -05:00
jmiller 3857f1339d chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-06-06 19:51:10 +00:00
jmiller 328ff92c52 Merge pull request 'chore: update wiki pages' (#533) from chore/wiki-update 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 19:50:33 +00:00
Jonathan Miller af7d6d78a8 chore: update wiki - roadmap, version, add pending pages
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
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
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 7s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m20s
- Update version to v1.26.1-moko.06.07.03
- Rewrite roadmap with current features and priorities
- Add pending wiki pages (branding, deployment, API docs)
2026-06-06 14:48:42 -05:00
jmiller 01ef500793 chore: sync .mokogitea/workflows/pre-release.yml from moko-platform [skip ci] 2026-06-06 19:48:35 +00:00
Jonathan Miller 1438dc7838 fix(issues): closed issues show reopen-only for non-admins
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
Regular users/issue posters see only a Reopen button on closed issues.
Admins and team members with write permission get the full status
dropdown including Reopen option.
2026-06-06 14:21:39 -05:00
jmiller a6245ff075 Merge pull request 'feat(issues): status dropdown replaces close button' (#528) from feat/status-replaces-close into dev
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
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
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
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
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 2s
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Universal: Build & Release / Promote to RC (pull_request) Failing after 12s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request_target) Failing after 24s
PR RC Release / Build RC Release (pull_request) Failing after 32s
2026-06-06 19:13:28 +00:00
Jonathan Miller a4b7b5276c feat(issues): status dropdown replaces close button for issues
Branch Cleanup / Delete merged branch (pull_request) Failing after 2s
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
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
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 5s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Failing after 14m17s
Move the status dropdown from sidebar to the comment form footer,
replacing the close/reopen button. Status selections that have
closes_issue=true auto-close, non-closing statuses auto-reopen.
Falls back to standard close/reopen button for PRs and repos
without org statuses.
2026-06-06 14:12:27 -05:00
jmiller 64b3dbe50b Merge pull request 'fix(issues): auto-seed default statuses and priorities' (#526) from fix/auto-seed-status-priority 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
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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request_target) Failing after 31s
PR RC Release / Build RC Release (pull_request) Failing after 53s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 1m15s
2026-06-06 18:41:59 +00:00
Jonathan Miller 22586b7a06 fix(issues): auto-seed default statuses and priorities for orgs
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
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
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 7s
Branch Cleanup / Delete merged branch (pull_request) Failing after 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Failing after 50s
Status and priority are first-class fields, not custom fields. They
must always show in the sidebar without requiring manual setup. When
an org has no definitions, the standard presets are auto-created on
first access.
2026-06-06 13:40:53 -05:00
jmiller eccb0de243 Merge pull request 'feat(mcp): public release with SSE, npm, Docker (#523)' (#524) from feat/523-mcp-public into dev
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 (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 / 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
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request_target) Failing after 32s
PR RC Release / Build RC Release (pull_request) Failing after 54s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 38s
2026-06-06 17:56:28 +00:00
Jonathan Miller f80c99db3c feat(mcp): public release with SSE transport, npm package, Docker support (#523)
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 2s
PR RC Release / Build RC Release (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Failing after 52s
- Add SSE transport (sse.ts) for hosted deployments
- Env var config (GITEA_URL/GITEA_TOKEN) for zero-config setup
- Dockerfile for containerized SSE mode
- npm publishing as @mokoconsulting/mokogitea-mcp
- README with quick start, tool reference, config options
- Server factory (server.ts) for SSE reuse
2026-06-06 12:55:21 -05:00
jmiller d32074581d Merge pull request 'fix(mcp): deduplicate issue creation and add status/priority fields' (#521) from fix/mcp-dedup-priority into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (push) 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 / Site Health (push) Has been skipped
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request_target) Failing after 25s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 33s
2026-06-06 17:39:59 +00:00
Jonathan Miller 65432aaec6 fix(mcp): deduplicate issue creation and add status/priority fields
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
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
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 3s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Failing after 25s
- gitea_issue_create searches by title before creating to prevent duplicates
- If duplicate found, updates existing issue instead
- Added status_id and priority_id parameters to issue create
- Status and priority set via dedicated endpoints after create/update
2026-06-06 12:39:17 -05:00
jmiller 01da6a48b1 Merge pull request 'chore: add MokoGitea MCP server source to repo' (#519) from chore/mcp-source 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
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
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 10s
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 27s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request_target) Failing after 27s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 43s
2026-06-06 17:29:59 +00:00
Jonathan Miller 3e86c5181e chore: add MokoGitea MCP server source to repo
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 1s
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 2s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Failing after 24s
Source reference copy of the mokogitea_api MCP server with tools for
manifest, issue statuses, and issue priorities API endpoints.
Runtime lives in moko-platform/mcp/servers/mokogitea_api/.
2026-06-06 12:27:30 -05:00
jmiller 1ffe31e360 Merge pull request 'fix: rename Priority field to PriorityDef to avoid redeclaration' (#517) from feat/509-issue-priority 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 / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
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
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
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 25s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request_target) Failing after 24s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 36s
2026-06-06 17:00:04 +00:00
Jonathan Miller e2e80de6fa fix: rename Priority field to PriorityDef to avoid redeclaration
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
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 4s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Failing after 18s
The Issue struct already has a Priority int field at line 74.
2026-06-06 11:58:53 -05:00
jmiller 759a8f590c Merge pull request 'feat(issues): org-level priority field (#509)' (#516) from feat/509-issue-priority 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 16:53:56 +00:00
Jonathan Miller 55c2f81c58 feat(issues): org-level priority field with customizable levels (#509)
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
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
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Failing after 30s
Add org-level issue priority definitions that appear in the issue
sidebar. Each priority has a name, color, sort order, and optional
default flag. Follows the same architecture as custom statuses (#502).

Includes:
- IssuePriorityDef model with CRUD operations
- Migration v348 adding issue_priority_def table + priority_id on issues
- Org settings UI for managing priorities
- Issue sidebar dropdown for selecting priority

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 11:52:44 -05:00
jmiller b3ee5cc18a Merge pull request 'fix: replace non-ASCII em dashes in CLAUDE.md and manifest.xml' (#514) from fix/ascii-cleanup 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
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
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Universal: Build & Release / Promote to RC (pull_request) Failing after 11s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request_target) Failing after 24s
PR RC Release / Build RC Release (pull_request) Failing after 35s
2026-06-06 16:22:58 +00:00
Jonathan Miller 4e1a90c4e4 fix: replace non-ASCII em dashes with ASCII in CLAUDE.md and manifest.xml
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 1s
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 6s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Failing after 20s
Prevents hook JSON validation failures caused by non-ASCII characters
in files read during stop hooks.
2026-06-06 11:22:11 -05:00
65 changed files with 6972 additions and 325 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
# MokoGitea
Fork of Gitea self-hosted Git service at git.mokoconsulting.tech. Go backend + TypeScript frontend.
Fork of Gitea -- self-hosted Git service at git.mokoconsulting.tech. Go backend + TypeScript frontend.
## Quick Reference
@@ -32,7 +32,7 @@ GITEA_TEST_E2E_FLAGS='<filepath>' make test-e2e # Single Playwright test
- 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
- 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
+1 -1
View File
@@ -3,7 +3,7 @@
<identity>
<name>MokoGitea</name>
<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>
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
</identity>
+129
View File
@@ -0,0 +1,129 @@
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later
DEFGROUP: gitea-api-mcp.Documentation
REPO: https://git.mokoconsulting.tech/MokoConsulting/gitea-api-mcp
-->
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Changed
- **Renamed** package from `@mokoconsulting/gitea-api-mcp` to `@mokoconsulting/mokogitea-api-mcp` to distinguish Moko's forked Gitea MCP from upstream
- **Renamed** McpServer name and bin entry to `mokogitea-api-mcp`
## [0.0] - 2026-05-07
### Added
#### User / Auth (3 tools)
- `gitea_me` -- Get the authenticated user info
- `gitea_user_orgs` -- List organizations the authenticated user belongs to
- `gitea_user_repos` -- List repositories owned by the authenticated user
#### Repositories (8 tools)
- `gitea_repo_get` -- Get repository details
- `gitea_repo_create` -- Create a new repository
- `gitea_repo_delete` -- Delete a repository
- `gitea_repo_edit` -- Edit repository settings
- `gitea_repo_fork` -- Fork a repository
- `gitea_repo_search` -- Search repositories
- `gitea_org_repos` -- List repositories in an organization
- `gitea_list_connections` -- List configured Gitea connections
#### File Contents (5 tools)
- `gitea_file_get` -- Get file contents from a repository
- `gitea_dir_get` -- Get directory contents (file listing) from a repository
- `gitea_file_create_or_update` -- Create or update a file in a repository
- `gitea_file_delete` -- Delete a file from a repository
- `gitea_tree_get` -- Get the git tree for a repository (recursive file listing)
#### Branches (4 tools)
- `gitea_branches_list` -- List branches in a repository
- `gitea_branch_get` -- Get a specific branch
- `gitea_branch_create` -- Create a new branch
- `gitea_branch_delete` -- Delete a branch
#### Commits (2 tools)
- `gitea_commits_list` -- List commits in a repository
- `gitea_commit_get` -- Get a specific commit
#### Issues (7 tools)
- `gitea_issues_list` -- List issues in a repository
- `gitea_issue_get` -- Get a single issue by number
- `gitea_issue_create` -- Create a new issue
- `gitea_issue_update` -- Update an issue
- `gitea_issue_comments_list` -- List comments on an issue
- `gitea_issue_comment_create` -- Add a comment to an issue
- `gitea_issue_search` -- Search issues across all repositories
#### Labels (2 tools)
- `gitea_labels_list` -- List labels in a repository
- `gitea_label_create` -- Create a label
#### Milestones (2 tools)
- `gitea_milestones_list` -- List milestones in a repository
- `gitea_milestone_create` -- Create a milestone
#### Pull Requests (6 tools)
- `gitea_pulls_list` -- List pull requests
- `gitea_pull_get` -- Get a single pull request
- `gitea_pull_create` -- Create a pull request
- `gitea_pull_merge` -- Merge a pull request
- `gitea_pull_files` -- List files changed in a pull request
- `gitea_pull_review_create` -- Create a pull request review
#### Releases (5 tools)
- `gitea_releases_list` -- List releases
- `gitea_release_get` -- Get a single release by ID
- `gitea_release_latest` -- Get the latest release
- `gitea_release_create` -- Create a new release
- `gitea_release_delete` -- Delete a release
#### Tags (3 tools)
- `gitea_tags_list` -- List tags
- `gitea_tag_create` -- Create a tag
- `gitea_tag_delete` -- Delete a tag
#### Actions (2 tools)
- `gitea_actions_runs_list` -- List workflow runs for a repository
- `gitea_actions_run_get` -- Get a specific workflow run
#### Organizations (3 tools)
- `gitea_org_get` -- Get organization details
- `gitea_org_teams_list` -- List teams in an organization
- `gitea_org_members_list` -- List members of an organization
#### Users (2 tools)
- `gitea_user_get` -- Get a user profile
- `gitea_users_search` -- Search users
#### Webhooks (2 tools)
- `gitea_webhooks_list` -- List webhooks for a repository
- `gitea_webhook_create` -- Create a webhook
#### Wiki (2 tools)
- `gitea_wiki_pages_list` -- List wiki pages
- `gitea_wiki_page_get` -- Get a wiki page
#### Notifications (2 tools)
- `gitea_notifications_list` -- List notifications for the authenticated user
- `gitea_notifications_read` -- Mark all notifications as read
#### Generic (2 tools)
- `gitea_api_request` -- Make a raw API request to any Gitea v1 endpoint
- `gitea_list_connections` -- List configured Gitea connections
### Infrastructure
- Multi-connection config support via `~/.gitea-api-mcp.json`
- Token-based authentication (Gitea native `Authorization: token` header)
- Built on `node:https` / `node:http` (zero HTTP dependencies)
- MCP SDK v1.12.x with stdio transport
[0.0.1]: https://git.mokoconsulting.tech/MokoConsulting/gitea-api-mcp/releases/tag/v0.0.1
+18
View File
@@ -0,0 +1,18 @@
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --production=false
COPY tsconfig.json ./
COPY src/ ./src/
RUN npx tsc && npm prune --production
EXPOSE 3100
ENV PORT=3100
ENV NODE_ENV=production
# SSE mode by default for Docker deployments
CMD ["node", "dist/sse.js"]
+116
View File
@@ -0,0 +1,116 @@
# MokoGitea MCP Server
A comprehensive [Model Context Protocol](https://modelcontextprotocol.io) server for [Gitea](https://gitea.com) and [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea). 120+ tools for repos, issues, PRs, projects, releases, custom fields, statuses, priorities, and manifests.
Works with any Gitea instance. MokoGitea-specific features degrade gracefully on vanilla Gitea.
## Quick Start
### npx (no install)
```bash
GITEA_URL=https://gitea.example.com GITEA_TOKEN=your_token npx @mokoconsulting/mokogitea-mcp
```
### Claude Code
Add to `.claude.json`:
```json
{
"mcpServers": {
"mokogitea": {
"command": "npx",
"args": ["@mokoconsulting/mokogitea-mcp"],
"env": {
"GITEA_URL": "https://gitea.example.com",
"GITEA_TOKEN": "your_token"
}
}
}
}
```
### Docker (SSE mode)
```bash
docker run -p 3100:3100 \
-e GITEA_URL=https://gitea.example.com \
-e GITEA_TOKEN=your_token \
mokoconsulting/mokogitea-mcp
```
Connect MCP client to `http://localhost:3100/sse`.
### Multi-instance config
Create `~/.mcp_mokogitea.json`:
```json
{
"defaultConnection": "production",
"connections": {
"production": { "baseUrl": "https://gitea.example.com", "token": "your_token" },
"dev": { "baseUrl": "https://dev.gitea.example.com", "token": "dev_token" }
}
}
```
## Configuration
| Method | Use Case |
|--------|----------|
| `GITEA_URL` + `GITEA_TOKEN` env vars | Single instance, quick setup |
| `~/.mcp_mokogitea.json` config file | Multiple instances |
| `GITEA_API_MCP_CONFIG` env var | Custom config path |
| `GITEA_INSECURE=true` | Skip TLS verification |
## Tools (120+)
### Repositories
`gitea_repo_create` `gitea_repo_get` `gitea_repo_edit` `gitea_repo_delete` `gitea_repo_search` `gitea_repo_fork` `gitea_repo_generate` `gitea_repo_languages` `gitea_repo_contributors` `gitea_repo_topics` `gitea_repo_topics_set`
### Issues
`gitea_issue_create` (dedup by title) `gitea_issue_get` `gitea_issue_update` `gitea_issues_list` `gitea_issue_search` `gitea_issue_comment_create` `gitea_issue_comments_list` `gitea_issue_labels_set` `gitea_issue_bulk_set_status`
### Pull Requests
`gitea_pull_create` `gitea_pull_get` `gitea_pulls_list` `gitea_pull_merge` `gitea_pull_files` `gitea_pull_review_create`
### Branches and Tags
`gitea_branches_list` `gitea_branch_create` `gitea_branch_delete` `gitea_branch_get` `gitea_tags_list` `gitea_tag_create` `gitea_tag_delete`
### Releases
`gitea_releases_list` `gitea_release_create` `gitea_release_get` `gitea_release_latest` `gitea_release_delete` `gitea_release_asset_upload` `gitea_release_asset_delete`
### Files and Trees
`gitea_file_get` `gitea_file_create_or_update` `gitea_file_delete` `gitea_dir_get` `gitea_tree_get` `gitea_bulk_file_push`
### Projects
`gitea_project_list` `gitea_project_create` `gitea_project_get` `gitea_project_update` `gitea_project_delete` `gitea_project_overview` `gitea_project_columns_list` `gitea_project_column_create` `gitea_project_column_delete` `gitea_project_cards_list` `gitea_project_card_add` `gitea_project_card_move` `gitea_project_card_remove`
### Organizations
`gitea_org_get` `gitea_org_repos` `gitea_org_members_list` `gitea_org_teams_list` `gitea_org_labels_list` `gitea_org_label_create`
### Wiki
`gitea_wiki_pages_list` `gitea_wiki_page_get`
### MokoGitea Extensions
`gitea_manifest_get` `gitea_manifest_update` `gitea_org_custom_fields_list` `gitea_org_custom_field_create` `gitea_org_custom_field_delete` `gitea_issue_custom_fields_get` `gitea_issue_custom_fields_set` `gitea_org_issue_statuses_list` `gitea_issue_set_status` `gitea_org_issue_priorities_list` `gitea_issue_set_priority`
### Admin and Other
`gitea_me` `gitea_users_search` `gitea_user_get` `gitea_notifications_list` `gitea_notifications_read` `gitea_commits_list` `gitea_commit_get` `gitea_compare` `gitea_webhooks_list` `gitea_webhook_create` `gitea_admin_users_list` `gitea_admin_orgs_list` `gitea_admin_cron_list` `gitea_admin_cron_run` `gitea_list_connections`
## SSE Server
For hosted deployments:
```
GET / Server info
GET /sse SSE connection endpoint
POST /message Tool call messages
GET /health Health check
```
## License
GPL-3.0-or-later - [Moko Consulting](https://mokoconsulting.tech)
+13
View File
@@ -0,0 +1,13 @@
{
"defaultConnection": "moko",
"connections": {
"moko": {
"baseUrl": "https://git.mokoconsulting.tech",
"token": "your-gitea-access-token"
},
"github-mirror": {
"baseUrl": "https://gitea.example.com",
"token": "your-other-token"
}
}
}
+1198
View File
File diff suppressed because it is too large Load Diff
+58
View File
@@ -0,0 +1,58 @@
{
"name": "@mokoconsulting/mokogitea-mcp",
"version": "1.1.0",
"description": "MCP server for Gitea and MokoGitea - 120+ tools for repos, issues, PRs, projects, releases, custom fields, statuses, priorities, and manifests",
"type": "module",
"main": "dist/index.js",
"bin": {
"mokogitea-mcp": "dist/index.js",
"mokogitea-mcp-sse": "dist/sse.js"
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"start": "node dist/index.js",
"start:sse": "node dist/sse.js",
"setup": "node scripts/setup.mjs",
"clean": "rm -rf dist/"
},
"keywords": [
"mcp",
"gitea",
"mokogitea",
"model-context-protocol",
"claude",
"ai",
"git",
"self-hosted",
"api",
"devops"
],
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.1",
"zod": "^3.24.4"
},
"devDependencies": {
"@types/node": "^22.15.3",
"typescript": "^5.8.3"
},
"engines": {
"node": ">=20.0.0"
},
"license": "GPL-3.0-or-later",
"author": "Moko Consulting <hello@mokoconsulting.tech>",
"homepage": "https://git.mokoconsulting.tech/MokoConsulting/mcp_mokogitea_api",
"repository": {
"type": "git",
"url": "https://git.mokoconsulting.tech/MokoConsulting/mcp_mokogitea_api.git"
},
"files": [
"dist/",
"config.example.json",
"README.md",
"LICENSE"
],
"publishConfig": {
"access": "public"
}
}
+15
View File
@@ -0,0 +1,15 @@
# mcp_mokogitea_api PowerShell Profile
# Source this with: . ./profile.ps1
$env:MCP_ROOT = $PSScriptRoot
$env:TEMP = 'A:\temp'
$env:TMP = 'A:\temp'
function mcp { Set-Location $PSScriptRoot }
function mcp-src { Set-Location (Join-Path $PSScriptRoot 'src') }
function mcp-build { Set-Location $PSScriptRoot; npm run build }
function mcp-dev { Set-Location $PSScriptRoot; npm run dev }
Write-Host "mcp_mokogitea_api profile loaded" -ForegroundColor Cyan
Write-Host " Commands: mcp-build, mcp-dev" -ForegroundColor DarkGray
Write-Host " Navigate: mcp, mcp-src" -ForegroundColor DarkGray
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env node
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
* BRIEF: Interactive setup — prompts for Gitea connection details
*/
import { createInterface } from 'node:readline/promises';
import { readFile, writeFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { homedir } from 'node:os';
const CONFIG_PATH = resolve(homedir(), '.gitea-api-mcp.json');
const rl = createInterface({ input: process.stdin, output: process.stdout });
async function prompt(q, d) { const a = await rl.question(`${q}${d ? ` [${d}]` : ''}: `); return a.trim() || d || ''; }
async function promptRequired(q) { let a = ''; while (!a) { a = (await rl.question(`${q}: `)).trim(); if (!a) console.log(' Required.'); } return a; }
async function main() {
console.log('\n=== gitea-api-mcp Setup ===\n');
let existing = null;
try { existing = JSON.parse(await readFile(CONFIG_PATH, 'utf-8')); console.log(`Existing: ${Object.keys(existing.connections).join(', ')}\n`); } catch {}
const name = await prompt('Connection name', 'moko');
const baseUrl = await promptRequired('Gitea URL (e.g. https://git.mokoconsulting.tech)');
const token = await promptRequired('Access token (Settings > Applications > Generate Token)');
const insecure = (await prompt('Skip TLS verification? (y/N)', 'N')).toLowerCase() === 'y';
const conn = { baseUrl: baseUrl.replace(/\/+$/, ''), token };
if (insecure) conn.insecure = true;
const config = existing ?? { defaultConnection: name, connections: {} };
config.connections[name] = conn;
if (!existing) config.defaultConnection = name;
else if ((await prompt(`Set "${name}" as default? (y/N)`, 'N')).toLowerCase() === 'y') config.defaultConnection = name;
await writeFile(CONFIG_PATH, JSON.stringify(config, null, '\t') + '\n', 'utf-8');
console.log(`\nConfig written to ${CONFIG_PATH}\n`);
rl.close();
}
main().catch(e => { console.error(e.message); rl.close(); process.exit(1); });
+120
View File
@@ -0,0 +1,120 @@
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: gitea-api-mcp.Client
* INGROUP: gitea-api-mcp
* REPO: https://git.mokoconsulting.tech/MokoConsulting/gitea-api-mcp
* PATH: /src/client.ts
* VERSION: 01.00.00
* BRIEF: HTTP client for Gitea REST API v1
*/
import * as https from 'node:https';
import * as http from 'node:http';
import type { GiteaConnection, ApiResponse } from './types.js';
const API_PREFIX = '/api/v1';
const TIMEOUT_MS = 30_000;
export class GiteaClient {
private readonly base_url: string;
private readonly headers: Record<string, string>;
private readonly insecure: boolean;
constructor(conn: GiteaConnection) {
this.base_url = conn.baseUrl.replace(/\/+$/, '') + API_PREFIX;
this.headers = {
'Authorization': `token ${conn.token}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
};
this.insecure = conn.insecure ?? false;
}
async get(endpoint: string, params?: Record<string, string>): Promise<ApiResponse> {
return this.request(this.buildUrl(endpoint, params), 'GET');
}
async post(endpoint: string, body?: unknown): Promise<ApiResponse> {
return this.request(this.buildUrl(endpoint), 'POST', body);
}
async patch(endpoint: string, body: unknown): Promise<ApiResponse> {
return this.request(this.buildUrl(endpoint), 'PATCH', body);
}
async put(endpoint: string, body: unknown): Promise<ApiResponse> {
return this.request(this.buildUrl(endpoint), 'PUT', body);
}
async delete(endpoint: string): Promise<ApiResponse> {
return this.request(this.buildUrl(endpoint), 'DELETE');
}
private buildUrl(endpoint: string, params?: Record<string, string>): string {
const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
const url = new URL(`${this.base_url}${path}`);
if (params) {
for (const [key, value] of Object.entries(params)) {
url.searchParams.set(key, value);
}
}
return url.toString();
}
private request(url: string, method: string, body?: unknown): Promise<ApiResponse> {
return new Promise((resolve, reject) => {
const parsed = new URL(url);
const is_https = parsed.protocol === 'https:';
const transport = is_https ? https : http;
const options: https.RequestOptions = {
hostname: parsed.hostname,
port: parsed.port || (is_https ? 443 : 80),
path: parsed.pathname + parsed.search,
method,
headers: { ...this.headers },
timeout: TIMEOUT_MS,
};
if (this.insecure && is_https) {
options.rejectUnauthorized = false;
}
const payload = body !== undefined ? JSON.stringify(body) : undefined;
if (payload) {
(options.headers as Record<string, string>)['Content-Length'] = Buffer.byteLength(payload).toString();
}
const req = transport.request(options, (res) => {
const chunks: Buffer[] = [];
res.on('data', (chunk: Buffer) => chunks.push(chunk));
res.on('end', () => {
const raw = Buffer.concat(chunks).toString('utf-8');
let data: unknown;
try {
data = JSON.parse(raw);
} catch {
data = raw;
}
resolve({ status: res.statusCode ?? 0, data });
});
});
req.on('error', (err) => reject(err));
req.on('timeout', () => {
req.destroy();
reject(new Error('Request timed out'));
});
if (payload) {
req.write(payload);
}
req.end();
});
}
}
+61
View File
@@ -0,0 +1,61 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { homedir } from 'node:os';
import type { GiteaConfig, GiteaConnection } from './types.js';
const CONFIG_FILENAME = '.mcp_mokogitea.json';
export async function loadConfig(): Promise<GiteaConfig> {
// Priority 1: Environment variables (zero-config single instance)
if (process.env.GITEA_URL && process.env.GITEA_TOKEN) {
const conn: GiteaConnection = {
baseUrl: process.env.GITEA_URL,
token: process.env.GITEA_TOKEN,
insecure: process.env.GITEA_INSECURE === 'true',
};
return {
connections: { default: conn },
defaultConnection: 'default',
};
}
// Priority 2: Config file
const config_path = process.env.GITEA_API_MCP_CONFIG
? resolve(process.env.GITEA_API_MCP_CONFIG)
: resolve(homedir(), CONFIG_FILENAME);
try {
const raw = await readFile(config_path, 'utf-8');
const parsed = JSON.parse(raw) as Partial<GiteaConfig>;
if (!parsed.connections || Object.keys(parsed.connections).length === 0) {
throw new Error('No connections defined in config');
}
return {
connections: parsed.connections,
defaultConnection: parsed.defaultConnection ?? Object.keys(parsed.connections)[0],
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw new Error(
`Failed to load config from ${config_path}: ${message}\n` +
`Option 1: Set GITEA_URL and GITEA_TOKEN environment variables\n` +
`Option 2: Create ${config_path} - see config.example.json for format`,
);
}
}
export function getConnection(config: GiteaConfig, name?: string): GiteaConnection {
const key = name ?? config.defaultConnection;
const conn = config.connections[key];
if (!conn) {
throw new Error(
`Connection "${key}" not found. Available: ${Object.keys(config.connections).join(', ')}`,
);
}
return conn;
}
File diff suppressed because it is too large Load Diff
+16
View File
@@ -0,0 +1,16 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
//
// Creates a configured MCP server instance for use by both stdio and SSE transports.
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type { GiteaConfig } from './types.js';
// Import index.ts to register all tools on its exported `server` singleton,
// then re-export a factory that initializes config and returns the server.
import { server, initConfig } from './index.js';
export function createMcpServer(cfg: GiteaConfig): McpServer {
initConfig(cfg);
return server;
}
+100
View File
@@ -0,0 +1,100 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
//
// SSE transport entry point for MokoGitea MCP server.
// Run with: node dist/sse.js
// Or: GITEA_URL=https://gitea.example.com GITEA_TOKEN=xxx node dist/sse.js
//
// Listens on PORT (default 3100) and serves SSE at /sse with POST at /message.
import { createServer } from 'node:http';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { createMcpServer } from './server.js';
import { loadConfig } from './config.js';
const PORT = parseInt(process.env.PORT ?? '3100', 10);
async function main(): Promise<void> {
const config = await loadConfig();
const transports = new Map<string, SSEServerTransport>();
const httpServer = createServer(async (req, res) => {
// CORS headers for browser clients
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') {
res.writeHead(204);
res.end();
return;
}
// Health check
if (req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok', tools: 120 }));
return;
}
// SSE endpoint - client connects here
if (req.url === '/sse' && req.method === 'GET') {
const transport = new SSEServerTransport('/message', res);
const sessionId = transport.sessionId;
transports.set(sessionId, transport);
const server = createMcpServer(config);
await server.connect(transport);
req.on('close', () => {
transports.delete(sessionId);
});
return;
}
// Message endpoint - client sends tool calls here
if (req.url?.startsWith('/message') && req.method === 'POST') {
const url = new URL(req.url, `http://${req.headers.host}`);
const sessionId = url.searchParams.get('sessionId');
if (!sessionId || !transports.has(sessionId)) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid or missing sessionId' }));
return;
}
const transport = transports.get(sessionId)!;
await transport.handlePostMessage(req, res);
return;
}
// Root - info page
if (req.url === '/' || req.url === '') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
name: '@mokoconsulting/mokogitea-mcp',
version: '1.1.0',
description: 'MCP server for Gitea and MokoGitea - 120+ tools',
endpoints: {
sse: '/sse',
message: '/message',
health: '/health',
},
docs: 'https://git.mokoconsulting.tech/MokoConsulting/mcp_mokogitea_api',
}));
return;
}
res.writeHead(404);
res.end('Not found');
});
httpServer.listen(PORT, () => {
process.stderr.write(`MokoGitea MCP SSE server listening on port ${PORT}\n`);
process.stderr.write(` SSE: http://localhost:${PORT}/sse\n`);
process.stderr.write(` Health: http://localhost:${PORT}/health\n`);
});
}
main().catch((err) => {
process.stderr.write(`Fatal: ${err}\n`);
process.exit(1);
});
+37
View File
@@ -0,0 +1,37 @@
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: gitea-api-mcp.Types
* INGROUP: gitea-api-mcp
* REPO: https://git.mokoconsulting.tech/MokoConsulting/gitea-api-mcp
* PATH: /src/types.ts
* VERSION: 01.00.00
* BRIEF: TypeScript type definitions for Gitea API MCP server
*/
export interface GiteaConnection {
baseUrl: string;
token: string;
/** Skip TLS certificate verification (self-signed certs) */
insecure?: boolean;
}
export interface GitHubBackupConfig {
token: string;
org: string;
}
export interface GiteaConfig {
connections: Record<string, GiteaConnection>;
defaultConnection: string;
github?: GitHubBackupConfig;
}
export interface ApiResponse {
status: number;
data: unknown;
}
+19
View File
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
+70 -31
View File
@@ -17,7 +17,7 @@
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
# | |
# | Platform-specific: |
# | joomla: XML manifest, updates.xml, type-prefixed packages |
# | joomla: XML manifest, type-prefixed packages |
# | dolibarr: mod*.class.php, update.txt, dev version reset |
# | generic: README-only, no update stream |
# | |
@@ -71,20 +71,25 @@ jobs:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
echo Using pre-installed /opt/moko-platform
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
else
echo Falling back to fresh clone
if ! command -v composer > /dev/null 2>&1; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
fi
rm -rf /tmp/moko-platform-api
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
cd /tmp/moko-platform-api
composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
fi
# Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api
composer install --no-dev --no-interaction --quiet
- name: Rename branch to rc
run: |
php /tmp/moko-platform-api/cli/branch_rename.php \
php ${MOKO_CLI}/branch_rename.php \
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
@@ -100,16 +105,15 @@ jobs:
- name: Publish RC release
run: |
php /tmp/moko-platform-api/cli/release_publish.php \
php ${MOKO_CLI}/release_publish.php \
--path . --stability rc --bump minor --branch rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--skip-update-stream
--token "${{ secrets.MOKOGITEA_TOKEN }}"
- name: Summary
if: always()
run: |
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
echo "Branch renamed to rc, minor bump, RC release built (updates.xml managed by Gitea Pages)" >> $GITHUB_STEP_SUMMARY
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
release:
@@ -151,25 +155,60 @@ jobs:
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
run: |
# Ensure PHP + Composer are available
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
echo Using pre-installed /opt/moko-platform
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
else
echo Falling back to fresh clone
if ! command -v composer > /dev/null 2>&1; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
fi
rm -rf /tmp/moko-platform-api
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
cd /tmp/moko-platform-api
composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
fi
# Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api
composer install --no-dev --no-interaction --quiet
- name: "Publish stable release"
run: |
php /tmp/moko-platform-api/cli/release_publish.php \
php ${MOKO_CLI}/release_publish.php \
--path . --stability stable --bump minor --branch main \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--skip-update-stream
--token "${{ secrets.MOKOGITEA_TOKEN }}"
- name: Update release notes from CHANGELOG.md
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Extract [Unreleased] section from changelog
if [ -f "CHANGELOG.md" ]; then
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
[ -z "$NOTES" ] && NOTES="Stable release"
else
NOTES="Stable release"
fi
# Update release body via API
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
"${API_BASE}/releases/tags/stable" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -n "$RELEASE_ID" ]; then
python3 -c "
import json, urllib.request
body = open('/dev/stdin').read()
payload = json.dumps({'body': body}).encode()
req = urllib.request.Request(
'${API_BASE}/releases/${RELEASE_ID}',
data=payload, method='PATCH',
headers={
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
'Content-Type': 'application/json'
})
urllib.request.urlopen(req)
" <<< "$NOTES"
echo "Release notes updated from CHANGELOG.md"
fi
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
- name: "Step 9: Mirror release to GitHub"
@@ -182,7 +221,7 @@ jobs:
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php /tmp/moko-platform-api/cli/release_mirror.php \
php ${MOKO_CLI}/release_mirror.php \
--version "$VERSION" --tag "$RELEASE_TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
@@ -256,7 +295,7 @@ jobs:
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php /tmp/moko-platform-api/cli/version_reset_dev.php \
php ${MOKO_CLI}/version_reset_dev.php \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
--branch dev --path . 2>&1 || true
+243 -241
View File
@@ -1,241 +1,243 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /templates/workflows/universal/pre-release.yml.template
# VERSION: 05.01.00
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
name: "Universal: Pre-Release"
on:
pull_request:
types: [closed]
branches:
- dev
pull_request_target:
types: [synchronize, opened, reopened]
branches:
- main
workflow_dispatch:
inputs:
stability:
description: 'Pre-release channel'
required: true
type: choice
options:
- development
- alpha
- beta
- release-candidate
permissions:
contents: write
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
jobs:
build:
name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
runs-on: release
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') ||
(github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main')
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }}
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }}
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
# Use pre-installed /opt/moko-platform if available (updated by cron every 6h)
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
echo Using pre-installed /opt/moko-platform
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
else
echo Falling back to fresh clone
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \
“https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git” \
/tmp/moko-platform-api
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
echo “MOKO_CLI=/tmp/moko-platform-api/cli” >> “$GITHUB_ENV”
fi
- name: Detect platform
id: platform
run: |
php ${MOKO_CLI}/manifest_read.php --path . --github-output
- name: Resolve metadata and bump version
id: meta
run: |
# Auto-detect stability: RC for PRs targeting main, else use input or default to development
if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then
STABILITY="release-candidate"
else
STABILITY="${{ inputs.stability || 'development' }}"
fi
case "$STABILITY" in
development) SUFFIX="-dev"; TAG="development" ;;
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
beta) SUFFIX="-beta"; TAG="beta" ;;
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
esac
# Bump version via CLI: patch for dev/alpha/beta, minor for RC
case "$STABILITY" in
release-candidate) BUMP="minor" ;;
*) BUMP="patch" ;;
esac
php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true
# Set stability suffix and verify consistency
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01")
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
php ${MOKO_CLI}/version_set_platform.php \
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
# Append suffix for output
if [ -n "$SUFFIX" ]; then
VERSION="${VERSION}${SUFFIX}"
fi
# Commit version bump
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git add -A
git diff --cached --quiet || {
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
git push origin HEAD 2>&1
}
# Auto-detect element via manifest_element.php
php ${MOKO_CLI}/manifest_element.php \
--path . --version "$VERSION" --stability "$STABILITY" \
--repo "${GITEA_REPO}" --github-output
# Read back element outputs
EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
[ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
- name: Create release
id: release
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_create.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --branch dev --prerelease
- name: Update release notes from CHANGELOG.md
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
if [ -f "CHANGELOG.md" ]; then
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
else
NOTES="Release ${VERSION}"
fi
# Update release body via API
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
"${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -n "$RELEASE_ID" ]; then
python3 -c "
import json, urllib.request
body = open('/dev/stdin').read()
payload = json.dumps({'body': body}).encode()
req = urllib.request.Request(
'${API_BASE}/releases/${RELEASE_ID}',
data=payload, method='PATCH',
headers={
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
'Content-Type': 'application/json'
})
urllib.request.urlopen(req)
" <<< "$NOTES"
echo "Release notes updated from CHANGELOG.md"
fi
- name: Build package and upload
id: package
run: |
VERSION="${{ steps.meta.outputs.version }}"
TAG="${{ steps.meta.outputs.tag }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_package.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --output /tmp || true
# updates.xml is generated dynamically by MokoGitea license server
# No need to build, commit, or sync updates.xml from workflows
- name: "Delete lesser pre-release channels (cascade)"
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
php ${MOKO_CLI}/release_cascade.php \
--stability "${{ steps.meta.outputs.stability }}" \
--token "${TOKEN}" \
--api-base "${API_BASE}"
- name: Summary
if: always()
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
SHA256="${{ steps.package.outputs.sha256_zip }}"
echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /templates/workflows/universal/pre-release.yml.template
# VERSION: 05.01.00
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
name: "Universal: Pre-Release"
on:
pull_request:
types: [closed]
branches:
- dev
pull_request_target:
types: [synchronize, opened, reopened]
branches:
- main
workflow_dispatch:
inputs:
stability:
description: 'Pre-release channel'
required: true
type: choice
options:
- development
- alpha
- beta
- release-candidate
permissions:
contents: write
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
jobs:
build:
name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
runs-on: release
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') ||
(github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main')
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }}
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }}
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
# Use pre-installed /opt/moko-platform if available (updated by cron every 6h)
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/cli/manifest_element.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
echo Using pre-installed /opt/moko-platform
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
else
echo Falling back to fresh clone
if ! command -v composer > /dev/null 2>&1; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
fi
rm -rf /tmp/moko-platform-api
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
fi
- name: Detect platform
id: platform
run: |
php ${MOKO_CLI}/manifest_read.php --path . --github-output
- name: Resolve metadata and bump version
id: meta
run: |
# Auto-detect stability: RC for PRs targeting main, else use input or default to development
if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then
STABILITY="release-candidate"
else
STABILITY="${{ inputs.stability || 'development' }}"
fi
case "$STABILITY" in
development) SUFFIX="-dev"; TAG="development" ;;
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
beta) SUFFIX="-beta"; TAG="beta" ;;
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
esac
# Bump version via CLI: patch for dev/alpha/beta, minor for RC
case "$STABILITY" in
release-candidate) BUMP="minor" ;;
*) BUMP="patch" ;;
esac
php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true
# Set stability suffix and verify consistency
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01")
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
php ${MOKO_CLI}/version_set_platform.php \
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
# Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml
php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true
# Append suffix for output
if [ -n "$SUFFIX" ]; then
VERSION="${VERSION}${SUFFIX}"
fi
# Commit version bump
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git add -A
git diff --cached --quiet || {
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
git push origin HEAD 2>&1
}
# Auto-detect element via manifest_element.php
php ${MOKO_CLI}/manifest_element.php \
--path . --version "$VERSION" --stability "$STABILITY" \
--repo "${GITEA_REPO}" --github-output
# Read back element outputs
EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
[ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
- name: Create release
id: release
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_create.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --branch dev --prerelease
- name: Update release notes from CHANGELOG.md
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
if [ -f "CHANGELOG.md" ]; then
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
else
NOTES="Release ${VERSION}"
fi
# Update release body via API
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
"${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -n "$RELEASE_ID" ]; then
python3 -c "
import json, urllib.request
body = open('/dev/stdin').read()
payload = json.dumps({'body': body}).encode()
req = urllib.request.Request(
'${API_BASE}/releases/${RELEASE_ID}',
data=payload, method='PATCH',
headers={
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
'Content-Type': 'application/json'
})
urllib.request.urlopen(req)
" <<< "$NOTES"
echo "Release notes updated from CHANGELOG.md"
fi
- name: Build package and upload
id: package
run: |
VERSION="${{ steps.meta.outputs.version }}"
TAG="${{ steps.meta.outputs.tag }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_package.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --output /tmp || true
# updates.xml is generated dynamically by MokoGitea license server
# No need to build, commit, or sync updates.xml from workflows
- name: "Delete lesser pre-release channels (cascade)"
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
php ${MOKO_CLI}/release_cascade.php \
--stability "${{ steps.meta.outputs.stability }}" \
--token "${TOKEN}" \
--api-base "${API_BASE}"
- name: Summary
if: always()
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
SHA256="${{ steps.package.outputs.sha256_zip }}"
echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
+4
View File
@@ -78,6 +78,10 @@ type Issue struct {
IsClosed bool `xorm:"INDEX"`
StatusID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'status_id'"`
Status *IssueStatusDef `xorm:"-"`
PriorityID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'priority_id'"`
PriorityDef *IssuePriorityDef `xorm:"-"`
TypeID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'type_id'"`
TypeDef *IssueTypeDef `xorm:"-"`
IsRead bool `xorm:"-"`
IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not.
PullRequest *PullRequest `xorm:"-"`
+117
View File
@@ -0,0 +1,117 @@
// 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(IssuePriorityDef))
}
// IssuePriorityDef defines a custom issue priority at the org level.
type IssuePriorityDef 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 (IssuePriorityDef) TableName() string {
return "issue_priority_def"
}
// GetIssuePriorityDefsByOrg returns active priority definitions for an org.
// If none exist, seeds the org with default priorities automatically.
func GetIssuePriorityDefsByOrg(ctx context.Context, orgID int64) ([]*IssuePriorityDef, error) {
defs := make([]*IssuePriorityDef, 0, 10)
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 := seedDefaultIssuePriorities(ctx, orgID); err != nil {
return defs, nil // non-fatal
}
return GetIssuePriorityDefsByOrg(ctx, orgID)
}
return defs, nil
}
// seedDefaultIssuePriorities creates the standard priority presets for an org.
func seedDefaultIssuePriorities(ctx context.Context, orgID int64) error {
defaults := []*IssuePriorityDef{
{OrgID: orgID, Name: "Critical", Color: "#dc2626", Description: "Requires immediate attention", SortOrder: 1, IsActive: true},
{OrgID: orgID, Name: "High", Color: "#f97316", Description: "Should be addressed soon", SortOrder: 2, IsActive: true},
{OrgID: orgID, Name: "Medium", Color: "#eab308", Description: "Normal priority", SortOrder: 3, IsDefault: true, IsActive: true},
{OrgID: orgID, Name: "Low", Color: "#2563eb", Description: "Can wait", SortOrder: 4, IsActive: true},
}
for _, d := range defaults {
if _, err := db.GetEngine(ctx).Insert(d); err != nil {
return err
}
}
return nil
}
// GetAllIssuePriorityDefsByOrg returns all priority definitions (including inactive).
func GetAllIssuePriorityDefsByOrg(ctx context.Context, orgID int64) ([]*IssuePriorityDef, error) {
defs := make([]*IssuePriorityDef, 0, 10)
return defs, db.GetEngine(ctx).
Where("org_id = ?", orgID).
OrderBy("sort_order ASC, id ASC").
Find(&defs)
}
// GetIssuePriorityDefByID returns a single priority definition.
func GetIssuePriorityDefByID(ctx context.Context, id int64) (*IssuePriorityDef, error) {
def := new(IssuePriorityDef)
has, err := db.GetEngine(ctx).ID(id).Get(def)
if err != nil {
return nil, err
}
if !has {
return nil, db.ErrNotExist{Resource: "IssuePriorityDef", ID: id}
}
return def, nil
}
// CreateIssuePriorityDef creates a new priority definition.
func CreateIssuePriorityDef(ctx context.Context, def *IssuePriorityDef) error {
_, err := db.GetEngine(ctx).Insert(def)
return err
}
// UpdateIssuePriorityDef updates a priority definition.
func UpdateIssuePriorityDef(ctx context.Context, def *IssuePriorityDef) error {
_, err := db.GetEngine(ctx).ID(def.ID).AllCols().Update(def)
return err
}
// DeleteIssuePriorityDef deletes a priority definition and clears references on issues.
func DeleteIssuePriorityDef(ctx context.Context, id int64) error {
if _, err := db.GetEngine(ctx).Exec("UPDATE issue SET priority_id = 0 WHERE priority_id = ?", id); err != nil {
return err
}
_, err := db.GetEngine(ctx).ID(id).Delete(new(IssuePriorityDef))
return err
}
// SetIssuePriorityID updates the priority_id on an issue.
func SetIssuePriorityID(ctx context.Context, issueID, priorityID int64) error {
_, err := db.GetEngine(ctx).Exec("UPDATE issue SET priority_id = ? WHERE id = ?", priorityID, issueID)
return err
}
+30 -2
View File
@@ -37,12 +37,40 @@ func (IssueStatusDef) TableName() string {
// ──────────────────────────────────────────────────────────────────────
// GetIssueStatusDefsByOrg returns active status definitions for an org.
// If none exist, seeds the org with default statuses automatically.
func GetIssueStatusDefsByOrg(ctx context.Context, orgID int64) ([]*IssueStatusDef, error) {
defs := make([]*IssueStatusDef, 0, 10)
return defs, db.GetEngine(ctx).
if err := db.GetEngine(ctx).
Where("org_id = ? AND is_active = ?", orgID, true).
OrderBy("sort_order ASC, id ASC").
Find(&defs)
Find(&defs); err != nil {
return nil, err
}
if len(defs) == 0 && orgID > 0 {
if err := seedDefaultIssueStatuses(ctx, orgID); err != nil {
return defs, nil // non-fatal
}
return GetIssueStatusDefsByOrg(ctx, orgID)
}
return defs, nil
}
// seedDefaultIssueStatuses creates the standard status presets for an org.
func seedDefaultIssueStatuses(ctx context.Context, orgID int64) error {
defaults := []*IssueStatusDef{
{OrgID: orgID, Name: "In Progress", Color: "#2563eb", Description: "Work is actively being done", SortOrder: 1, IsActive: true},
{OrgID: orgID, Name: "Needs Info", Color: "#f59e0b", Description: "Waiting for more information", SortOrder: 2, IsActive: true},
{OrgID: orgID, Name: "Blocked", Color: "#dc2626", Description: "Cannot proceed due to dependency", SortOrder: 3, IsActive: true},
{OrgID: orgID, Name: "Resolved", Color: "#16a34a", Description: "Fix implemented and verified", ClosesIssue: true, SortOrder: 4, IsActive: true},
{OrgID: orgID, Name: "Won't Fix", Color: "#6b7280", Description: "Decided not to address", ClosesIssue: true, SortOrder: 5, IsActive: true},
{OrgID: orgID, Name: "Duplicate", Color: "#8b5cf6", Description: "Already tracked elsewhere", ClosesIssue: true, SortOrder: 6, IsActive: true},
}
for _, d := range defaults {
if _, err := db.GetEngine(ctx).Insert(d); err != nil {
return err
}
}
return nil
}
// GetAllIssueStatusDefsByOrg returns all status definitions (including inactive).
+114
View File
@@ -0,0 +1,114 @@
// 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
}
+3
View File
@@ -425,6 +425,9 @@ func prepareMigrationTasks() []*migration {
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),
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
}
+33
View File
@@ -0,0 +1,33 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import (
"xorm.io/xorm"
)
// AddIssuePriorityDefTable creates the issue_priority_def table and adds
// priority_id to the issue table.
func AddIssuePriorityDefTable(x *xorm.Engine) error {
type IssuePriorityDef 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(IssuePriorityDef)); err != nil {
return err
}
type Issue struct {
PriorityID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'priority_id'"`
}
return x.Sync(new(Issue))
}
+49
View File
@@ -0,0 +1,49 @@
// 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
@@ -0,0 +1,29 @@
// 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
@@ -0,0 +1,219 @@
// 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
}
+40
View File
@@ -1583,6 +1583,8 @@
"repo.issues.cancel": "Cancel",
"repo.issues.save": "Save",
"repo.issues.status": "Status",
"repo.issues.priority": "Priority",
"repo.issues.type": "Type",
"repo.issues.label_title": "Name",
"repo.issues.label_description": "Description",
"repo.issues.label_color": "Color",
@@ -1968,6 +1970,7 @@
"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.desc": "Link to an external wiki.",
"repo.security": "Security",
"repo.wiki": "Wiki",
"repo.wiki.welcome": "Welcome to the Wiki.",
"repo.wiki.welcome_desc": "The wiki lets you write and share documentation with collaborators.",
@@ -1991,6 +1994,7 @@
"repo.wiki.page_already_exists": "A wiki page with the same name already exists.",
"repo.wiki.reserved_page": "The wiki page name \"%s\" is reserved.",
"repo.wiki.pages": "Pages",
"repo.wiki.folder_empty": "This folder is empty.",
"repo.wiki.last_updated": "Last updated %s",
"repo.wiki.page_name_desc": "Enter a name for this Wiki page. Some special names are: 'Home', '_Sidebar' and '_Footer'.",
"repo.wiki.original_git_entry_tooltip": "View original Git file instead of using friendly link.",
@@ -2748,6 +2752,28 @@
"repo.settings.manifest_entry_point": "Entry Point",
"repo.settings.manifest_save": "Save Manifest",
"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_saved": "Repository metadata saved.",
"repo.settings.metadata_empty": "No metadata fields defined. Org admins can add fields in Organization Settings > Custom Fields.",
@@ -2952,6 +2978,20 @@
"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.issue_priorities": "Issue Priorities",
"org.settings.issue_priorities_desc": "Define priority levels for all repositories in this organization. Priorities appear in the issue sidebar.",
"org.settings.issue_priorities_empty": "No custom issue priorities defined yet.",
"org.settings.issue_priority_add": "Add Priority",
"org.settings.issue_priority_name": "Priority Name",
"org.settings.issue_priority_color": "Color",
"org.settings.issue_priority_description": "Description",
"org.settings.issue_priority_default": "Default",
"org.settings.issue_priority_default_help": "Auto-assigned to new issues.",
"org.settings.issue_priority_sort_order": "Sort Order",
"org.settings.issue_priority_inactive": "Inactive",
"org.settings.issue_priority_created": "Issue priority created.",
"org.settings.issue_priority_updated": "Issue priority updated.",
"org.settings.issue_priority_deleted": "Issue priority deleted.",
"org.settings.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.",
+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 tplOrgIssuePriorities templates.TplName = "org/settings/issue_priorities"
// SettingsIssuePriorities shows the org-level issue priorities management page.
func SettingsIssuePriorities(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("org.settings.issue_priorities")
ctx.Data["PageIsOrgSettings"] = true
ctx.Data["PageIsSettingsIssuePriorities"] = true
defs, err := issues_model.GetAllIssuePriorityDefsByOrg(ctx, ctx.Org.Organization.ID)
if err != nil {
ctx.ServerError("GetAllIssuePriorityDefsByOrg", err)
return
}
ctx.Data["IssuePriorities"] = defs
ctx.HTML(http.StatusOK, tplOrgIssuePriorities)
}
// SettingsIssuePrioritiesCreatePost creates a new org-level issue priority.
func SettingsIssuePrioritiesCreatePost(ctx *context.Context) {
sortOrder, _ := strconv.Atoi(ctx.FormString("sort_order"))
def := &issues_model.IssuePriorityDef{
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("Priority name is required")
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-priorities")
return
}
if err := issues_model.CreateIssuePriorityDef(ctx, def); err != nil {
ctx.ServerError("CreateIssuePriorityDef", err)
return
}
ctx.Flash.Success(ctx.Tr("org.settings.issue_priority_created"))
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-priorities")
}
// SettingsIssuePrioritiesEditPost updates an org-level issue priority.
func SettingsIssuePrioritiesEditPost(ctx *context.Context) {
id := ctx.PathParamInt64("id")
def, err := issues_model.GetIssuePriorityDefByID(ctx, id)
if err != nil {
ctx.ServerError("GetIssuePriorityDefByID", 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.UpdateIssuePriorityDef(ctx, def); err != nil {
ctx.ServerError("UpdateIssuePriorityDef", err)
return
}
ctx.Flash.Success(ctx.Tr("org.settings.issue_priority_updated"))
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-priorities")
}
// SettingsIssuePrioritiesDeletePost deletes an org-level issue priority.
func SettingsIssuePrioritiesDeletePost(ctx *context.Context) {
id := ctx.PathParamInt64("id")
def, err := issues_model.GetIssuePriorityDefByID(ctx, id)
if err != nil {
ctx.ServerError("GetIssuePriorityDefByID", err)
return
}
if def.OrgID != ctx.Org.Organization.ID {
ctx.NotFound(nil)
return
}
if err := issues_model.DeleteIssuePriorityDef(ctx, id); err != nil {
ctx.ServerError("DeleteIssuePriorityDef", err)
return
}
ctx.Flash.Success(ctx.Tr("org.settings.issue_priority_deleted"))
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-priorities")
}
+39
View File
@@ -184,6 +184,45 @@ func NewComment(ctx *context.Context) {
}
} // end if: handle close or reopen
// Handle custom status from the status dropdown (replaces close button for issues with org statuses).
if statusIDStr := ctx.Req.FormValue("status_id"); statusIDStr != "" && statusIDStr != "" {
if statusIDStr == "reopen" {
// Reopen via dropdown
if issue.IsClosed {
if err := issue_service.ReopenIssue(ctx, issue, ctx.Doer, ""); err != nil {
log.Error("ReopenIssue via status dropdown: %v", err)
}
if err := issues_model.SetIssueStatusID(ctx, issue.ID, 0); err != nil {
log.Error("SetIssueStatusID: %v", err)
}
}
} else if statusIDStr == "close" {
// Plain close via dropdown
if !issue.IsClosed {
if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil {
log.Error("CloseIssue via status dropdown: %v", err)
}
}
} else if statusID, err := strconv.ParseInt(statusIDStr, 10, 64); err == nil && statusID > 0 {
// Custom status selected
statusDef, err := issues_model.GetIssueStatusDefByID(ctx, statusID)
if err == nil && statusDef.OrgID == ctx.Repo.Repository.OwnerID {
if err := issues_model.SetIssueStatusID(ctx, issue.ID, statusID); err != nil {
log.Error("SetIssueStatusID: %v", err)
}
if statusDef.ClosesIssue && !issue.IsClosed {
if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil {
log.Error("CloseIssue via custom status: %v", err)
}
} else if !statusDef.ClosesIssue && issue.IsClosed {
if err := issue_service.ReopenIssue(ctx, issue, ctx.Doer, ""); err != nil {
log.Error("ReopenIssue via custom status: %v", err)
}
}
}
}
}
ctx.JSONRedirect(redirect)
}
+44
View File
@@ -0,0 +1,44 @@
// 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"
)
// UpdateIssueCustomPriority handles POST to set a custom priority on an issue.
func UpdateIssueCustomPriority(ctx *context.Context) {
issueID := ctx.PathParamInt64("id")
priorityID := ctx.FormInt64("priority_id")
issue, err := issues_model.GetIssueByID(ctx, issueID)
if err != nil {
ctx.ServerError("GetIssueByID", err)
return
}
// Validate the priority belongs to this repo's org.
if priorityID > 0 {
priorityDef, err := issues_model.GetIssuePriorityDefByID(ctx, priorityID)
if err != nil {
ctx.ServerError("GetIssuePriorityDefByID", err)
return
}
if priorityDef.OrgID != ctx.Repo.Repository.OwnerID {
ctx.NotFound(nil)
return
}
}
if err := issues_model.SetIssuePriorityID(ctx, issueID, priorityID); err != nil {
ctx.ServerError("SetIssuePriorityID", err)
return
}
ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, issue.Index), http.StatusSeeOther)
}
+43
View File
@@ -0,0 +1,43 @@
// 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,6 +536,14 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID int64, projectI
}
ctx.Data["CustomFieldDefs"] = customFieldDefs
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.
cfQuery := make(url.Values)
for fieldID, value := range customFieldFilters {
+14
View File
@@ -372,6 +372,20 @@ func ViewIssue(ctx *context.Context) {
}
ctx.Data["IssueStatusDefs"] = issueStatusDefs
// Load custom issue priority definitions for the sidebar.
issuePriorityDefs, ipErr := issues_model.GetIssuePriorityDefsByOrg(ctx, ctx.Repo.Repository.OwnerID)
if ipErr != nil {
log.Error("ViewIssue: GetIssuePriorityDefsByOrg: %v", ipErr)
}
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")
if err := issue.LoadAttributes(ctx); err != nil {
+88
View File
@@ -0,0 +1,88 @@
// 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")
}
+111
View File
@@ -0,0 +1,111 @@
// 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,6 +77,20 @@ type PageMeta struct {
UpdatedUnix timeutil.TimeStamp
}
// WikiTreeNode represents a node in the wiki folder tree for sidebar navigation.
type WikiTreeNode struct {
Name string
SubURL string
IsDir bool
Children []*WikiTreeNode
}
// WikiBreadcrumb represents a breadcrumb segment.
type WikiBreadcrumb struct {
Name string
SubURL string
}
// findEntryForFile finds the tree entry for a target filepath.
func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) {
entry, err := commit.GetTreeEntryByPath(target)
@@ -232,6 +246,43 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
isSideBar := pageName == "_Sidebar"
isFooter := pageName == "_Footer"
// Build breadcrumbs for the current path
breadcrumbs := buildWikiBreadcrumbs(pageName)
ctx.Data["WikiBreadcrumbs"] = breadcrumbs
// Build folder tree for sidebar navigation
wikiTree := buildWikiTree(commit)
ctx.Data["WikiTree"] = wikiTree
// Check if path is a directory first (before file lookup)
dirEntry, _ := commit.GetTreeEntryByPath(string(pageName))
if dirEntry != nil && dirEntry.IsDir() {
// Path is a directory - try index files or show folder listing
var entry *git.TreeEntry
foundIndex := false
for _, indexName := range []string{"README", "Home", "index"} {
indexPath := wiki_service.WebPath(string(pageName) + "/" + indexName)
idxEntry, _, idxNoEntry, _ := wikiEntryByName(ctx, commit, indexPath)
if !idxNoEntry && idxEntry != nil {
pageName = indexPath
entry = idxEntry
_, displayName = wiki_service.WebPathToUserTitle(pageName)
ctx.Data["PageURL"] = wiki_service.WebPathToURLPath(pageName)
ctx.Data["Title"] = displayName
foundIndex = true
break
}
}
if !foundIndex {
ctx.Data["IsWikiFolder"] = true
ctx.Data["WikiFolderPath"] = string(pageName)
folderEntries := listWikiFolderEntries(commit, string(pageName))
ctx.Data["WikiFolderEntries"] = folderEntries
return wikiGitRepo, nil
}
_ = entry // will be used below via pageName lookup
}
// lookup filename in wiki - get gitTree entry , real filename
entry, pageFilename, noEntry, isRaw := wikiEntryByName(ctx, commit, pageName)
if noEntry {
@@ -479,6 +530,14 @@ func Wiki(ctx *context.Context) {
if ctx.Written() {
return
}
// Folder listing - no entry but IsWikiFolder flag is set
if ctx.Data["IsWikiFolder"] != nil {
if wikiGitRepo != nil {
defer wikiGitRepo.Close()
}
ctx.HTML(http.StatusOK, tplWikiView)
return
}
if entry == nil {
ctx.Data["Title"] = ctx.Tr("repo.wiki")
ctx.HTML(http.StatusOK, tplWikiStart)
@@ -752,3 +811,134 @@ func DeleteWikiPagePost(ctx *context.Context) {
ctx.JSONRedirect(ctx.Repo.RepoLink + "/wiki/")
}
// buildWikiBreadcrumbs creates breadcrumb segments from a wiki path.
func buildWikiBreadcrumbs(pageName wiki_service.WebPath) []WikiBreadcrumb {
parts := strings.Split(string(pageName), "/")
crumbs := make([]WikiBreadcrumb, 0, len(parts))
for i, part := range parts {
if part == "" {
continue
}
subURL := strings.Join(parts[:i+1], "/")
crumbs = append(crumbs, WikiBreadcrumb{
Name: part,
SubURL: subURL,
})
}
return crumbs
}
// buildWikiTree builds a hierarchical folder tree from the wiki git repo.
func buildWikiTree(commit *git.Commit) []*WikiTreeNode {
if commit == nil {
return nil
}
entries, err := commit.ListEntries()
if err != nil {
return nil
}
root := make(map[string]*WikiTreeNode)
var topLevel []*WikiTreeNode
for _, entry := range entries {
name := entry.Name()
if entry.IsDir() {
node := &WikiTreeNode{
Name: name,
SubURL: name,
IsDir: true,
}
// List children of this directory
subTree := entry.Tree()
if subTree != nil {
children, _ := subTree.ListEntries()
for _, child := range children {
childName := child.Name()
if child.IsDir() {
node.Children = append(node.Children, &WikiTreeNode{
Name: childName,
SubURL: name + "/" + childName,
IsDir: true,
})
} else if strings.HasSuffix(childName, ".md") {
wpChild, err := wiki_service.GitPathToWebPath(childName)
if err != nil {
continue
}
_, childDisplay := wiki_service.WebPathToUserTitle(wpChild)
if childDisplay == "_Sidebar" || childDisplay == "_Footer" {
continue
}
node.Children = append(node.Children, &WikiTreeNode{
Name: childDisplay,
SubURL: name + "/" + string(wpChild),
IsDir: false,
})
}
}
}
root[name] = node
topLevel = append(topLevel, node)
} else if strings.HasSuffix(name, ".md") {
wpName, err := wiki_service.GitPathToWebPath(name)
if err != nil {
continue
}
_, displayName := wiki_service.WebPathToUserTitle(wpName)
if displayName == "_Sidebar" || displayName == "_Footer" {
continue
}
node := &WikiTreeNode{
Name: displayName,
SubURL: string(wpName),
IsDir: false,
}
topLevel = append(topLevel, node)
}
}
return topLevel
}
// listWikiFolderEntries lists the pages and subfolders in a wiki directory.
func listWikiFolderEntries(commit *git.Commit, treePath string) []PageMeta {
if commit == nil {
return nil
}
tree, err := commit.SubTree(treePath)
if err != nil {
return nil
}
entries, err := tree.ListEntries()
if err != nil {
return nil
}
var pages []PageMeta
for _, entry := range entries {
name := entry.Name()
if entry.IsDir() {
pages = append(pages, PageMeta{
Name: name + "/",
SubURL: treePath + "/" + name,
GitEntryName: name,
})
} else if strings.HasSuffix(name, ".md") {
wpName, err := wiki_service.GitPathToWebPath(name)
if err != nil {
continue
}
_, displayName := wiki_service.WebPathToUserTitle(wpName)
if displayName == "_Sidebar" || displayName == "_Footer" {
continue
}
pages = append(pages, PageMeta{
Name: displayName,
SubURL: treePath + "/" + string(wpName),
GitEntryName: name,
})
}
}
return pages
}
+20
View File
@@ -1073,6 +1073,12 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Post("/{id}/edit", org.SettingsIssueStatusesEditPost)
m.Post("/{id}/delete", org.SettingsIssueStatusesDeletePost)
})
m.Group("/issue-priorities", func() {
m.Get("", org.SettingsIssuePriorities)
m.Post("", org.SettingsIssuePrioritiesCreatePost)
m.Post("/{id}/edit", org.SettingsIssuePrioritiesEditPost)
m.Post("/{id}/delete", org.SettingsIssuePrioritiesDeletePost)
})
}, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "PageIsOrgSettings", true))
}, context.OrgAssignment(context.OrgAssignmentOptions{RequireOwner: true}))
}, reqSignIn)
@@ -1201,6 +1207,11 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
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("/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.Combo("").Get(repo_setting.Collaboration).Post(repo_setting.CollaborationPost)
@@ -1407,6 +1418,8 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
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("/{id}/custom-priority", reqRepoIssuesOrPullsWriter, repo.UpdateIssueCustomPriority)
m.Post("/{id}/custom-type", reqRepoIssuesOrPullsWriter, repo.UpdateIssueCustomType)
m.Post("/delete", reqRepoAdmin, repo.BatchDeleteIssues)
m.Delete("/unpin/{index}", reqRepoAdmin, repo.IssueUnpin)
m.Post("/move_pin", reqRepoAdmin, repo.IssuePinMove)
@@ -1665,6 +1678,13 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
})
// 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() {
// activity has its own permission checks
m.Get("", repo.Activity)
+3
View File
@@ -27,6 +27,7 @@ import (
issue_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/issue"
notify_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/notify"
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
@@ -195,6 +196,8 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
}
// Auto-sync .mokogitea/manifest.xml to database on default branch push
SyncManifestFromCommit(ctx, repo, newCommit)
// Run security scanners on default branch push
security_service.ScanOnPush(ctx, repo, newCommit)
} else {
if err := DelDivergenceFromCache(repo.ID, branch); err != nil {
log.Error("DelDivergenceFromCache: %v", err)
+75
View File
@@ -0,0 +1,75 @@
// 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
@@ -0,0 +1,35 @@
// 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
@@ -0,0 +1,203 @@
// 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
}
+27 -11
View File
@@ -144,21 +144,23 @@ func WebPathToURLPath(s WebPath) string {
func WebPathFromRequest(s string) WebPath {
s = util.PathJoinRelX(s)
// The old wiki code's behavior is always using %2F, instead of subdirectory.
s = strings.ReplaceAll(s, "/", "%2F")
// MokoGitea: support real subdirectories for hierarchical wiki navigation.
// Slashes are preserved as path separators, not escaped to %2F.
return WebPath(s)
}
var multiHyphenRe = regexp.MustCompile(`-{2,}`)
var nonSlugRe = regexp.MustCompile(`[^a-zA-Z0-9+.\-]`)
var nonSlugReWithSlash = regexp.MustCompile(`[^a-zA-Z0-9+.\-/]`)
// sanitizeWikiTitle converts a user-provided title into a clean, URL-friendly slug.
// Spaces and special characters become hyphens, consecutive hyphens collapse to one.
// Preserves: letters, digits, hyphens, plus signs (+), and dots (.)
// Preserves: letters, digits, hyphens, plus signs (+), dots (.), and slashes (/).
func sanitizeWikiTitle(title string) string {
title = strings.TrimSpace(title)
title = strings.ReplaceAll(title, " ", "-")
title = nonSlugRe.ReplaceAllString(title, "-")
// Preserve slashes as directory separators
title = nonSlugReWithSlash.ReplaceAllString(title, "-")
title = multiHyphenRe.ReplaceAllString(title, "-")
title = strings.NewReplacer("-+-", "-", "+-", "-", "-+", "-").Replace(title) // clean stray plus signs
title = strings.Trim(title, "-+.")
@@ -166,14 +168,28 @@ func sanitizeWikiTitle(title string) string {
}
func UserTitleToWebPath(base, title string) WebPath {
// TODO: no support for subdirectory, because the old wiki code's behavior is always using %2F, instead of subdirectory.
// So we do not add the support for writing slashes in title at the moment.
title = sanitizeWikiTitle(title)
title = util.PathJoinRelX(base, escapeSegToWeb(title, false))
if title == "" || title == "." {
title = "unnamed"
// MokoGitea: support subdirectories - slashes in title create folder structure.
// Split on /, sanitize each segment, rejoin.
parts := strings.Split(title, "/")
sanitized := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p == "" {
continue
}
p = sanitizeWikiTitle(p)
if p != "" {
sanitized = append(sanitized, escapeSegToWeb(p, false))
}
}
return WebPath(title)
result := strings.Join(sanitized, "/")
if base != "" {
result = util.PathJoinRelX(base, result)
}
if result == "" || result == "." {
result = "unnamed"
}
return WebPath(result)
}
// ToWikiPageMetaData converts meta information to a WikiPageMetaData
@@ -0,0 +1,93 @@
{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings issue-priorities")}}
<h4 class="ui top attached header">
{{ctx.Locale.Tr "org.settings.issue_priorities"}}
</h4>
<div class="ui attached segment">
<p class="text grey">{{ctx.Locale.Tr "org.settings.issue_priorities_desc"}}</p>
{{if .IssuePriorities}}
<table class="ui compact table">
<thead>
<tr>
<th>{{ctx.Locale.Tr "org.settings.issue_priority_color"}}</th>
<th>{{ctx.Locale.Tr "org.settings.issue_priority_name"}}</th>
<th>{{ctx.Locale.Tr "org.settings.issue_priority_default"}}</th>
<th>{{ctx.Locale.Tr "org.settings.issue_priority_sort_order"}}</th>
<th></th>
</tr>
</thead>
<tbody>
{{range .IssuePriorities}}
<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_priority_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_priority_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-priorities/{{.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_priorities_empty"}}</p>
</div>
{{end}}
<div class="divider"></div>
<h5>{{ctx.Locale.Tr "org.settings.issue_priority_add"}}</h5>
<form class="ui form" method="post" action="{{.OrgLink}}/settings/issue-priorities">
{{.CsrfTokenHtml}}
<div class="three fields">
<div class="required field">
<label>{{ctx.Locale.Tr "org.settings.issue_priority_name"}}</label>
<input name="name" required placeholder="e.g. Critical, High, Medium, Low">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.issue_priority_color"}}</label>
<input name="color" type="color" value="#f59e0b">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.issue_priority_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_priority_description"}}</label>
<input name="description" placeholder="Help text shown to users">
</div>
<div class="field">
<div class="ui checkbox tw-mt-4">
<input name="is_default" type="checkbox">
<label>{{ctx.Locale.Tr "org.settings.issue_priority_default"}}</label>
</div>
<p class="help">{{ctx.Locale.Tr "org.settings.issue_priority_default_help"}}</p>
</div>
</div>
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "org.settings.issue_priority_add"}}</button>
</form>
</div>
{{template "org/settings/layout_footer" .}}
+3
View File
@@ -34,6 +34,9 @@
<a class="{{if .PageIsSettingsIssueStatuses}}active {{end}}item" href="{{.OrgLink}}/settings/issue-statuses">
{{svg "octicon-tasklist"}} {{ctx.Locale.Tr "org.settings.issue_statuses"}}
</a>
<a class="{{if .PageIsSettingsIssuePriorities}}active {{end}}item" href="{{.OrgLink}}/settings/issue-priorities">
{{svg "octicon-flame"}} {{ctx.Locale.Tr "org.settings.issue_priorities"}}
</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>
+9
View File
@@ -122,6 +122,15 @@
</a>
{{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}}
<a href="{{.RepoLink}}/packages" class="{{if .IsPackagesPage}}active {{end}}item">
{{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}}
@@ -0,0 +1,33 @@
{{if .IssuePriorityDefs}}
<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.priority"}}</span>
{{$canModify := .HasIssuesOrPullsWritePermission}}
{{if $canModify}}
<form method="post" action="{{.RepoLink}}/issues/{{.Issue.ID}}/custom-priority" class="tw-inline">
{{$.CsrfTokenHtml}}
<select name="priority_id" class="ui compact mini dropdown tw-max-w-48" onchange="this.form.submit()">
<option value="0">-</option>
{{range .IssuePriorityDefs}}
<option value="{{.ID}}" {{if eq .ID $.Issue.PriorityID}}selected{{end}}
{{if .Color}}style="border-left: 3px solid {{.Color}}"{{end}}>
{{.Name}}
</option>
{{end}}
</select>
</form>
{{else}}
{{$found := false}}
{{range .IssuePriorityDefs}}
{{if eq .ID $.Issue.PriorityID}}
{{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}}
@@ -0,0 +1,33 @@
{{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}}
+45 -16
View File
@@ -85,24 +85,53 @@
<div class="field footer">
<div class="flex-text-block tw-justify-end">
{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .DisableStatusChange)}}
{{$btnIconColor := ""}}{{$btnIcon := ""}}{{$btnTextNoComment := ""}}{{$btnTextWithComment := ""}}{{$btnValue := ""}}
{{if .Issue.IsClosed}}
{{$btnValue = "reopen"}}
{{$btnIconColor = "tw-text-green"}}
{{$btnIcon = Iif .Issue.IsPull "octicon-git-pull-request" "octicon-issue-reopened"}}
{{$btnTextNoComment = ctx.Locale.Tr (Iif .Issue.IsPull "repo.pulls.reopen" "repo.issues.reopen_issue")}}
{{$btnTextWithComment = ctx.Locale.Tr "repo.issues.reopen_comment_issue"}}{{/* general: Reopen with Comment */}}
{{if and .IssueStatusDefs (not .Issue.IsPull)}}
{{if and .Issue.IsClosed (not .HasIssuesOrPullsWritePermission)}}
<button id="status-button" class="ui button" name="status_id" value="reopen">
<span class="status-button-icon tw-text-green">{{svg "octicon-issue-reopened"}}</span>
<span class="status-button-text">{{ctx.Locale.Tr "repo.issues.reopen_issue"}}</span>
</button>
{{else}}
<select name="status_id" class="ui compact dropdown" style="min-width:140px;padding:7px 10px;border:1px solid var(--color-secondary);border-radius:4px;background:var(--color-body);">
<option value="">-- {{ctx.Locale.Tr "repo.issues.status"}} --</option>
{{range .IssueStatusDefs}}
{{if not .ClosesIssue}}
<option value="{{.ID}}" {{if eq .ID $.Issue.StatusID}}selected{{end}}>{{.Name}}</option>
{{end}}
{{end}}
<option disabled>---</option>
{{range .IssueStatusDefs}}
{{if .ClosesIssue}}
<option value="{{.ID}}" {{if eq .ID $.Issue.StatusID}}selected{{end}}>{{.Name}} (close)</option>
{{end}}
{{end}}
{{if not $.Issue.IsClosed}}
<option value="close">{{ctx.Locale.Tr "repo.issues.close"}}</option>
{{else}}
<option value="reopen">{{ctx.Locale.Tr "repo.issues.reopen_issue"}}</option>
{{end}}
</select>
{{end}}
{{else}}
{{$btnValue = "close"}}
{{$btnIconColor = "tw-text-red"}}
{{$btnIcon = Iif .Issue.IsPull "octicon-git-pull-request-closed" "octicon-issue-closed"}}
{{$btnTextNoComment = ctx.Locale.Tr (Iif .Issue.IsPull "repo.pulls.close" "repo.issues.close")}}
{{$btnTextWithComment = ctx.Locale.Tr "repo.issues.close_comment_issue"}}{{/* general: Close with Comment */}}
{{$btnIconColor := ""}}{{$btnIcon := ""}}{{$btnTextNoComment := ""}}{{$btnTextWithComment := ""}}{{$btnValue := ""}}
{{if .Issue.IsClosed}}
{{$btnValue = "reopen"}}
{{$btnIconColor = "tw-text-green"}}
{{$btnIcon = Iif .Issue.IsPull "octicon-git-pull-request" "octicon-issue-reopened"}}
{{$btnTextNoComment = ctx.Locale.Tr (Iif .Issue.IsPull "repo.pulls.reopen" "repo.issues.reopen_issue")}}
{{$btnTextWithComment = ctx.Locale.Tr "repo.issues.reopen_comment_issue"}}
{{else}}
{{$btnValue = "close"}}
{{$btnIconColor = "tw-text-red"}}
{{$btnIcon = Iif .Issue.IsPull "octicon-git-pull-request-closed" "octicon-issue-closed"}}
{{$btnTextNoComment = ctx.Locale.Tr (Iif .Issue.IsPull "repo.pulls.close" "repo.issues.close")}}
{{$btnTextWithComment = ctx.Locale.Tr "repo.issues.close_comment_issue"}}
{{end}}
<button id="status-button" class="ui button" data-status="{{$btnTextNoComment}}" data-status-and-comment="{{$btnTextWithComment}}" name="status" value="{{$btnValue}}">
<span class="status-button-icon {{$btnIconColor}}">{{svg $btnIcon}}</span>
<span class="status-button-text">{{$btnTextNoComment}}</span>
</button>
{{end}}
<button id="status-button" class="ui button" data-status="{{$btnTextNoComment}}" data-status-and-comment="{{$btnTextWithComment}}" name="status" value="{{$btnValue}}">
<span class="status-button-icon {{$btnIconColor}}">{{svg $btnIcon}}</span>
<span class="status-button-text">{{$btnTextNoComment}}</span>
</button>
{{end}}
<button id="comment-button" class="ui primary button">
{{ctx.Locale.Tr "repo.issues.create_comment"}}
@@ -7,7 +7,9 @@
{{template "repo/issue/sidebar/label_list" $.IssuePageMetaData}}
{{template "repo/issue/sidebar/issue_status" $}}
{{template "repo/issue/sidebar/issue_priority" $}}
{{template "repo/issue/sidebar/issue_type" $}}
{{template "repo/issue/sidebar/custom_fields" $}}
+99
View File
@@ -0,0 +1,99 @@
{{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" .}}
+3
View File
@@ -18,6 +18,9 @@
<a class="{{if .PageIsSettingsMetadata}}active {{end}}item" href="{{.RepoLink}}/settings/metadata">
{{svg "octicon-list-unordered"}} {{ctx.Locale.Tr "repo.settings.metadata"}}
</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}}
<a class="{{if .PageIsSettingsPublicAccess}}active {{end}}item" href="{{.RepoLink}}/settings/public_access">
{{svg "octicon-eye"}} {{ctx.Locale.Tr "repo.settings.public_access"}}
+140
View File
@@ -0,0 +1,140 @@
{{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" .}}
+74 -1
View File
@@ -55,12 +55,51 @@
</div>
</div>
</div>
{{if .WikiBreadcrumbs}}
{{if gt (len .WikiBreadcrumbs) 1}}
<div class="tw-mb-2">
<span class="breadcrumb">
<a class="section" href="{{.RepoLink}}/wiki/">{{svg "octicon-book" 14}} Wiki</a>
{{range .WikiBreadcrumbs}}
<span class="breadcrumb-divider">/</span>
<a class="section" href="{{$.RepoLink}}/wiki/{{.SubURL}}">{{.Name}}</a>
{{end}}
</span>
</div>
{{end}}
{{end}}
{{if .FormatWarning}}
<div class="ui negative message">
<p>{{.FormatWarning}}</p>
</div>
{{end}}
{{if .IsWikiFolder}}
<h4 class="ui top attached header">
{{svg "octicon-file-directory" 16 "tw-mr-2"}}{{.WikiFolderPath}}
</h4>
<div class="ui attached segment">
{{if .WikiFolderEntries}}
<div class="wiki-folder-listing">
{{range .WikiFolderEntries}}
<div class="tw-py-1">
{{if (StringUtils.HasSuffix .Name "/")}}
{{svg "octicon-file-directory" 16 "tw-mr-1"}}
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}"><strong>{{.Name}}</strong></a>
{{else}}
{{svg "octicon-file" 16 "tw-mr-1"}}
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}">{{.Name}}</a>
{{end}}
</div>
{{end}}
</div>
{{else}}
<p class="text grey">This folder is empty.</p>
{{end}}
</div>
{{end}}
<div class="wiki-content-parts">
{{if .WikiSidebarTocHTML}}
<div class="render-content markup wiki-content-sidebar wiki-content-toc">
@@ -68,11 +107,45 @@
</div>
{{end}}
<div class="render-content markup wiki-content-main {{if or .WikiSidebarTocHTML .WikiSidebarHTML}}with-sidebar{{end}}">
<div class="render-content markup wiki-content-main {{if or .WikiSidebarTocHTML .WikiSidebarHTML .WikiTree}}with-sidebar{{end}}">
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus}}
{{.WikiContentHTML}}
</div>
{{if .WikiTree}}
<div class="render-content markup wiki-content-sidebar wiki-content-tree">
<strong>{{svg "octicon-list-unordered" 14}} Pages</strong>
<ul class="wiki-tree-list">
{{range .WikiTree}}
<li>
{{if .IsDir}}
{{svg "octicon-file-directory" 14}}
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}"><strong>{{.Name}}</strong></a>
{{if .Children}}
<ul>
{{range .Children}}
<li>
{{if .IsDir}}
{{svg "octicon-file-directory" 14}}
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}"><strong>{{.Name}}</strong></a>
{{else}}
{{svg "octicon-file" 14}}
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}" {{if eq $.PageURL .SubURL}}class="active"{{end}}>{{.Name}}</a>
{{end}}
</li>
{{end}}
</ul>
{{end}}
{{else}}
{{svg "octicon-file" 14}}
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}" {{if eq $.PageURL .SubURL}}class="active"{{end}}>{{.Name}}</a>
{{end}}
</li>
{{end}}
</ul>
</div>
{{end}}
{{if .WikiSidebarHTML}}
<div class="render-content markup wiki-content-sidebar">
{{if and .CanWriteWiki (not .Repository.IsMirror)}}
+3
View File
@@ -26,6 +26,9 @@
{{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>
{{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>
</div>
{{if .TotalTrackedTime}}
+6 -6
View File
@@ -9,29 +9,29 @@
<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"}}">
{{ctx.Locale.Tr "home.issues.in_your_repos"}}
<strong>{{CountFmt .IssueStats.YourRepositoriesCount}}</strong>
<span class="ui small label">{{CountFmt .IssueStats.YourRepositoriesCount}}</span>
</a>
<a class="{{if eq .ViewType "assigned"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "assigned"}}">
{{ctx.Locale.Tr "repo.issues.filter_type.assigned_to_you"}}
<strong>{{CountFmt .IssueStats.AssignCount}}</strong>
<span class="ui small label">{{CountFmt .IssueStats.AssignCount}}</span>
</a>
<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"}}
<strong>{{CountFmt .IssueStats.CreateCount}}</strong>
<span class="ui small label">{{CountFmt .IssueStats.CreateCount}}</span>
</a>
{{if .PageIsPulls}}
<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"}}
<strong>{{CountFmt .IssueStats.ReviewRequestedCount}}</strong>
<span class="ui small label">{{CountFmt .IssueStats.ReviewRequestedCount}}</span>
</a>
<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"}}
<strong>{{CountFmt .IssueStats.ReviewedCount}}</strong>
<span class="ui small label">{{CountFmt .IssueStats.ReviewedCount}}</span>
</a>
{{end}}
<a class="{{if eq .ViewType "mentioned"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "mentioned"}}">
{{ctx.Locale.Tr "repo.issues.filter_type.mentioning_you"}}
<strong>{{CountFmt .IssueStats.MentionCount}}</strong>
<span class="ui small label">{{CountFmt .IssueStats.MentionCount}}</span>
</a>
</div>
</div>
+27
View File
@@ -50,6 +50,33 @@
border-left-style: dashed;
}
.repository.wiki .wiki-tree-list {
list-style: none;
padding: 0;
margin: 0.5em 0 0 0;
font-size: 0.9em;
}
.repository.wiki .wiki-tree-list ul {
list-style: none;
padding: 0 0 0 1.2em;
margin: 0;
border-left: 1px dashed var(--color-secondary);
}
.repository.wiki .wiki-tree-list li {
padding: 2px 0;
}
.repository.wiki .wiki-tree-list a.active {
font-weight: bold;
color: var(--color-primary);
}
.repository.wiki .wiki-folder-listing {
font-size: 0.95em;
}
@media (max-width: 767.98px) {
.repository.wiki .wiki-content-main.with-sidebar,
.repository.wiki .wiki-content-sidebar {
+32
View File
@@ -0,0 +1,32 @@
# Custom Branding
## Logo & Favicon
Located in the container at `/var/lib/gitea/custom/public/assets/img/`:
- `logo.svg` — Navbar logo
- `logo.png` — Fallback logo
- `favicon.png` — Browser tab icon
- `favicon.svg` — SVG favicon
Source: Moko Consulting CRM favicon
## Landing Page
- `LANDING_PAGE = organizations` in `app.ini`
- Custom JS redirects home/logo clicks to `/explore/organizations`
- After login, users see the organizations list
## Themes
Custom CSS themes at `/var/lib/gitea/custom/public/assets/css/`:
- `theme-moko-dark.css`
- `theme-moko-light.css`
- `theme-moko-auto.css`
---
*Repo: [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
| Revision | Date | Author | Description |
|---|---|---|---|
| 1.0 | 2026-05-09 | Moko Consulting | Initial version |
+29 -10
View File
@@ -28,23 +28,42 @@ Each status has:
| Sort Order | Controls display order in dropdowns (ascending) |
| Is Active | Inactive statuses are hidden from dropdowns but preserved on existing issues |
### Example Statuses
### Default Statuses (auto-seeded)
| Status | Color | Closes Issue | Use Case |
|--------|-------|:------------:|----------|
| In Progress | Blue | No | Work is actively being done |
| Needs Info | Yellow | No | Waiting for more information from reporter |
| Blocked | Red | No | Cannot proceed due to external dependency |
| Won't Fix | Gray | Yes | Decided not to address this issue |
| Duplicate | Purple | Yes | Already tracked in another issue |
| Resolved | Green | Yes | Fix has been implemented and verified |
| Needs Info | Yellow | No | Waiting for more information |
| Blocked | Red | No | Cannot proceed due to dependency |
| Resolved | Green | Yes | Fix implemented and verified |
| Won't Fix | Gray | Yes | Decided not to address |
| Duplicate | Purple | Yes | Already tracked elsewhere |
| Pending: Design | Lavender | No | Waiting on design work |
| Pending: Testing | Yellow | No | Waiting for testing |
| Pending: Review | Green | No | Waiting for code review |
| Pending: Feedback | Pink | No | Waiting for feedback |
| Pending: Documentation | Purple | No | Waiting for docs |
| Pending: Deployment | Blue | No | Ready to deploy |
| Pending: Dependency | Light Blue | No | Blocked by external dependency |
Statuses are auto-seeded when an org first accesses them. Admins can add, edit, reorder, or deactivate statuses.
## Comment Form Integration
The status dropdown **replaces the close/reopen button** in the comment form footer for issues with org statuses:
- Open issues show all statuses plus a "Close" option
- Selecting a status with `closes_issue = true` auto-closes the issue
- Closed issues show only "Reopen" for non-admin users
- Admins see the full dropdown on closed issues including "Reopen"
- PRs still use the standard close/reopen button
## Issue List Badges
Status shows as a colored badge on each issue in the issue list view, alongside Type and Priority badges.
## Issue Sidebar
When an organization has custom statuses defined, a **Status** dropdown appears in the issue sidebar between labels and custom fields. The dropdown:
- Shows all active status definitions for the repo's organization
- Auto-submits on change (no save button needed)
Status also appears as a read-only display in the sidebar (the editable control is in the comment form). The dropdown:
- Displays a colored left border on each option
- Shows a power symbol on statuses that close the issue
- Selecting "—" (empty) clears the status
+48
View File
@@ -0,0 +1,48 @@
# Deployment
## Docker Image
MokoGitea runs as a custom Docker image built from the `moko/1.25.5-project-api` branch.
### Build
```bash
cd /opt/MokoGitea
git pull
docker build -t mokogitea:1.25.5-project-api -f Dockerfile.rootless .
```
### Deploy
The docker-compose at `/opt/gitea/docker-compose.yml` references the image:
```yaml
services:
gitea:
image: mokogitea:1.25.5-project-api
```
### Update Process
1. Pull latest from `moko/1.25.5-project-api`
2. Rebuild Docker image
3. `docker compose down gitea && docker compose up -d gitea`
## Volumes
| Path | Purpose |
|------|---------|
| `./gitea/data` | Repository data, LFS, avatars |
| `./gitea/conf` | `app.ini` configuration |
## Custom Files
Located at `/var/lib/gitea/custom/`:
- `templates/custom/header.tmpl` — Branding, logo redirect
- `public/assets/img/logo.svg` — Moko logo
- `public/assets/img/favicon.png` — Moko favicon
- `public/assets/css/theme-moko-*.css` — Custom themes
---
*Repo: [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
| Revision | Date | Author | Description |
|---|---|---|---|
| 1.0 | 2026-05-09 | Moko Consulting | Initial version |
+7 -3
View File
@@ -7,7 +7,7 @@ Moko Consulting's custom fork of [Gitea](https://gitea.com), extending the self-
| **Language** | Go |
| **License** | MIT |
| **Upstream** | Gitea 1.26.1 |
| **Version** | v1.26.1-moko.06.04.00 |
| **Version** | v1.26.1-moko.06.10.00 |
| **Platform** | [Gitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) |
---
@@ -16,10 +16,13 @@ Moko Consulting's custom fork of [Gitea](https://gitea.com), extending the self-
- **Commercial License System** — Package-based license keys with download gating, domain restriction, key expiry, and payment webhook API
- **Update Server** — Built-in update feeds for Joomla, WordPress, Dolibarr, Composer, Drupal, PrestaShop, and WHMCS
- **Custom Issue Statuses** — Org-defined workflow states (In Progress, Blocked, Won't Fix) with auto close/reopen
- **First-Class Issue Fields** — Type (12 types), Status (13 statuses), Priority (4 levels) as built-in fields with auto-seed defaults, colored badges in issue list, and sidebar dropdowns
- **Security Scanning** — Built-in security scanner with secret detection (15 patterns), push-time scanning, alert management, and Security tab in repo navigation
- **Custom Fields** — Org-level field definitions for issues (sidebar) and repos (metadata) with dropdown, text, number, date, checkbox, and URL types
- **Manifest Settings** — Per-repo identity/governance/build metadata with REST API for CI/CD integration
- **Manifest Settings** — Per-repo identity/governance/build metadata with REST API and auto-sync on push
- **Wiki Folders** — Hierarchical folder navigation with sidebar tree, breadcrumbs, and index page fallback
- **Well-Known File Tabs** — README/LICENSE/CONTRIBUTING/SECURITY/CHANGELOG tabs on repo home page
- **MCP Server** — 120+ tool MCP server published to npm (@mokoconsulting/mokogitea-mcp) with SSE transport
- **Org-Level Branch Protection** — Organization-scoped rulesets that cascade to all repos. Supports glob patterns. Full CRUD API
- **Enterprise Sub-Orgs** — Parent-child organization hierarchy
- **Three-Level Visibility** — Public (200), Private (403), Hidden (404) for repositories
@@ -39,6 +42,7 @@ Moko Consulting's custom fork of [Gitea](https://gitea.com), extending the self-
| [Org Branch Protection API](Org-Branch-Protection-API) | Org-level branch protection rulesets and API reference |
| [Project API](Project-API) | Custom API endpoint reference for project boards |
| [Roadmap](Roadmap) | Development roadmap and planned features |
| [features/](features) | Feature documentation folder |
---
+115
View File
@@ -0,0 +1,115 @@
# Org-Level Branch Protection API
## Overview
MokoGitea v1261.0.0 introduces **organization-level branch protection rulesets** that cascade automatically to all repositories within an organization. This eliminates the need to configure identical branch protection rules on each repo individually.
## How Inheritance Works
1. **Repo rules take precedence** — If a repo has its own protection rule for a branch pattern (e.g., `main`), the org rule is ignored for that repo.
2. **Org rules are the fallback** — If no repo-level rule matches a branch, the system checks org-level rules.
3. **Team-based only** — Org rules reference teams, not individual users (use repo-level rules for per-user whitelists).
## API Endpoints
All endpoints require authentication (`token`) and org ownership permissions.
### List Rules
```
GET /api/v1/orgs/{org}/branch_protections
```
### Create Rule
```
POST /api/v1/orgs/{org}/branch_protections
```
**Body:**
```json
{
"rule_name": "main",
"enable_push": true,
"enable_push_whitelist": true,
"push_whitelist_teams": ["developers"],
"enable_merge_whitelist": true,
"merge_whitelist_teams": ["maintainers"],
"required_approvals": 2,
"block_on_rejected_reviews": true,
"block_on_outdated_branch": true,
"dismiss_stale_approvals": true,
"require_signed_commits": false
}
```
### Get Rule
```
GET /api/v1/orgs/{org}/branch_protections/{name}
```
### Update Rule
```
PATCH /api/v1/orgs/{org}/branch_protections/{name}
```
Only fields included in the request body are updated.
### Delete Rule
```
DELETE /api/v1/orgs/{org}/branch_protections/{name}
```
## Glob Patterns
Rule names support glob patterns for matching multiple branches:
| Pattern | Matches |
|---------|---------|
| `main` | Exactly `main` |
| `dev` | Exactly `dev` |
| `rc/*` | `rc/1.0`, `rc/2.0-beta`, etc. |
| `beta/*` | `beta/feature-x`, etc. |
| `release/**` | `release/v1`, `release/v1/hotfix`, etc. |
## Example: Protect All Standard Branches
```bash
TOKEN="your-token"
ORG="MokoConsulting"
API="https://git.mokoconsulting.tech/api/v1"
for BRANCH in main dev "rc/*" "beta/*" "alpha/*"; do
curl -X POST "$API/orgs/$ORG/branch_protections" \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"rule_name\": \"$BRANCH\",
\"enable_push\": true,
\"enable_push_whitelist\": true,
\"push_whitelist_teams\": [\"developers\"],
\"required_approvals\": 1,
\"block_on_rejected_reviews\": true,
\"block_on_outdated_branch\": true
}"
done
```
## Configuration: Help & Support URLs
Also new in v1261.0.0 — configurable help/support links in `app.ini`:
```ini
[DEFAULT]
HELP_URL = https://docs.mokoconsulting.tech
SUPPORT_URL = https://mokoconsulting.tech/support
```
These replace the hardcoded `docs.gitea.com` links in the navigation bar and are visible in **Site Admin > Configuration**.
## Version Convention
MokoGitea uses `1261.xx.xx` versioning where `1261` represents the fork starting point from upstream Gitea. Minor and patch numbers track MokoGitea-specific releases.
+202
View File
@@ -0,0 +1,202 @@
# Project Board API Reference
Complete REST API for managing Gitea project boards, columns, and issue cards. This API was added by MokoGitea and is not available in upstream Gitea.
## Authentication
All write endpoints require a token with `issue` scope:
```
Authorization: token YOUR_TOKEN
```
## Projects
### List Projects
```
GET /api/v1/repos/{owner}/{repo}/projects
```
Query parameters:
- `state``open` (default), `closed`, or `all`
- `page` — page number (1-based)
- `limit` — results per page
Response: Array of Project objects
### Create Project
```
POST /api/v1/repos/{owner}/{repo}/projects
```
Body:
```json
{
"title": "Sprint Q2 2026",
"description": "Second quarter sprint",
"board_type": 1,
"card_type": 0
}
```
- `board_type`: 0=none, 1=basic kanban, 2=bug triage
- `card_type`: 0=text only, 1=images and text
### Get Project
```
GET /api/v1/repos/{owner}/{repo}/projects/{id}
```
### Update Project
```
PATCH /api/v1/repos/{owner}/{repo}/projects/{id}
```
Body:
```json
{
"title": "Updated Title",
"description": "Updated description"
}
```
### Delete Project
```
DELETE /api/v1/repos/{owner}/{repo}/projects/{id}
```
### Close/Reopen Project
```
POST /api/v1/repos/{owner}/{repo}/projects/{id}/close
POST /api/v1/repos/{owner}/{repo}/projects/{id}/reopen
```
## Columns
### List Columns
```
GET /api/v1/repos/{owner}/{repo}/projects/{id}/columns
```
### Create Column
```
POST /api/v1/repos/{owner}/{repo}/projects/{id}/columns
```
Body:
```json
{
"title": "Backlog",
"color": "#0075ca"
}
```
### Delete Column
```
DELETE /api/v1/repos/{owner}/{repo}/projects/{id}/columns/{columnId}
```
## Issue Cards
### List Issues in Column
```
GET /api/v1/repos/{owner}/{repo}/projects/{id}/columns/{columnId}/issues
```
Response: Array of ProjectColumnIssue objects with `issue_id`, `project_id`, `column_id`, `sorting`
### Add Issue to Column
```
POST /api/v1/repos/{owner}/{repo}/projects/{id}/columns/{columnId}/issues
```
Body:
```json
{
"issue_id": 42
}
```
### Move Issue Between Columns
```
PATCH /api/v1/repos/{owner}/{repo}/projects/{id}/issues/{issueId}/move
```
Body:
```json
{
"column_id": 5,
"sorting": 0
}
```
### Remove Issue from Project
```
DELETE /api/v1/repos/{owner}/{repo}/projects/{id}/issues/{issueId}
```
## Data Types
### Project
```json
{
"id": 1,
"title": "Roadmap",
"description": "Development roadmap",
"owner_id": 2,
"repo_id": 68,
"creator_id": 1,
"is_closed": false,
"created_at": "2026-05-08T00:06:45Z",
"updated_at": "2026-05-08T00:06:45Z",
"closed_at": null
}
```
### ProjectColumn
```json
{
"id": 7,
"title": "Backlog",
"sorting": 0,
"color": "#0075ca",
"project_id": 1,
"default": false,
"created_at": "2026-05-08T00:06:58Z",
"updated_at": "2026-05-08T00:06:58Z"
}
```
### ProjectColumnIssue
```json
{
"id": 1,
"issue_id": 42,
"project_id": 1,
"column_id": 7,
"sorting": 0
}
```
## MCP Integration
The `project-mcp` server wraps this API. Key tool: `project_setup_roadmap` creates a full project board with columns and loads all open issues in one call.
## Quick Start
```bash
# Create a project
curl -X POST -H "Authorization: token TOKEN" \
https://git.mokoconsulting.tech/api/v1/repos/MokoConsulting/MokoCRM/projects \
-d '{"title":"Roadmap","board_type":1}'
# Add columns
curl -X POST -H "Authorization: token TOKEN" \
https://git.mokoconsulting.tech/api/v1/repos/MokoConsulting/MokoCRM/projects/1/columns \
-d '{"title":"Backlog"}'
# Add an issue
curl -X POST -H "Authorization: token TOKEN" \
https://git.mokoconsulting.tech/api/v1/repos/MokoConsulting/MokoCRM/projects/1/columns/1/issues \
-d '{"issue_id":42}'
```
---
*Repo: [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
| Revision | Date | Author | Description |
|---|---|---|---|
| 1.0 | 2026-05-09 | Moko Consulting | Initial version |
+42
View File
@@ -0,0 +1,42 @@
# MokoGitea Roadmap
## Recently Completed (v1.26.1-moko.06.10)
- 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
## In Progress
- Granular role-based permissions for all features (#9)
- Wire moko-platform CLI to manifest API (#505)
## Planned
- Standard status presets and cross-org migration (#507)
- Auto-create default teams on org creation (#513)
- Update server reads from repo_manifest (#512)
- 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 |