Compare commits

...

54 Commits

Author SHA1 Message Date
jmiller b545e7414c chore: sync workflow-sync-trigger.yml from Template-Generic [skip ci] 2026-06-24 11:50:59 +00:00
jmiller 024b00a38e chore: sync version-set.yml from Template-Generic [skip ci] 2026-06-24 11:50:50 +00:00
jmiller 8f9a43f32c chore: sync notify.yml from Template-Generic [skip ci] 2026-06-24 11:50:35 +00:00
jmiller 225ea65881 chore: sync gitleaks.yml from Template-Generic [skip ci] 2026-06-24 11:50:22 +00:00
jmiller 4c018fac62 chore: sync deploy-manual.yml from Template-Generic [skip ci] 2026-06-24 11:50:18 +00:00
jmiller 41e48945fd chore: sync auto-release.yml from Template-Generic [skip ci] 2026-06-23 23:12:05 +00:00
jmiller 7d28fe522d Merge pull request 'fix: correct dev health URL to git.dev.mokoconsulting.tech' (#695) from fix/dev-deploy-hostname into main
Deploy MokoGitea / Verify dev environment is healthy (push) Successful in 2s
Deploy MokoGitea / deploy (push) Has been cancelled
fix: correct dev health URL to git.dev.mokoconsulting.tech (#695)
2026-06-23 23:08:13 +00:00
Jonathan Miller 8780ec7c5f fix: correct dev health URL to git.dev.mokoconsulting.tech
Universal: PR Check / Branch Policy (pull_request) Failing after 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 9s
PR RC Release / Build RC Release (pull_request) Failing after 59s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m22s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Failing after 1s
Universal: PR Check / Secret Scan (pull_request) Successful in 1m20s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
Was dev.git.mokoconsulting.tech which doesn't exist. The dev instance
runs at git.dev.mokoconsulting.tech (port 3090 behind nginx).
2026-06-23 18:06:49 -05:00
jmiller 9106cfe254 Merge pull request 'feat: wiki search, metadata deploy fields, workflow cleanup' (#694) from dev into main
Deploy MokoGitea / Verify dev environment is healthy (push) Failing after 2s
Deploy MokoGitea / deploy (push) Has been cancelled
Merge PR #694: wiki search, metadata deploy fields, workflow cleanup
2026-06-23 22:34:39 +00:00
Jonathan Miller f7d70ae95a docs: update changelog with wiki search, metadata deploy fields, fixes
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 14s
Universal: Auto Version Bump / Version Bump (push) Successful in 17s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Failing after 1m30s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m35s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: PR Check / Secret Scan (pull_request) Successful in 2m30s
PR RC Release / Build RC Release (pull_request) Failing after 2m28s
Universal: Build & Release / Build & Release Pipeline (pull_request) Failing after 1m16s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
2026-06-23 17:33:35 -05:00
Jonathan Miller 5c43cf1f02 fix(metadata): support partial updates — only sent fields are changed
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
Universal: PR Check / Validate PR (pull_request) Failing after 10s
Universal: Auto Version Bump / Version Bump (push) Successful in 16s
Universal: PR Check / Secret Scan (pull_request) Successful in 1m30s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m32s
PR RC Release / Build RC Release (pull_request) Failing after 2m28s
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Failing after 2m32s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
Previously PUT /metadata replaced all fields, wiping any not included
in the request. Now loads existing metadata first and merges only the
fields present in the JSON body.
2026-06-23 17:28:40 -05:00
Jonathan Miller c8c74c7afe fix: handle DB errors in licensing API, fix wiki API URL-decode fallback
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 10s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Successful in 16s
Universal: PR Check / Secret Scan (pull_request) Successful in 1m11s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m12s
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Failing after 1m46s
PR RC Release / Build RC Release (pull_request) Failing after 1m38s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
- licensing/manage.go: capture Update/Delete errors instead of silently
  discarding them (UpdateLicense, UpdateTier, DeleteTier)
- wiki.go API: fix findEntryForFile to allow URL-decode fallback for
  non-ASCII page names (was returning on ErrNotExist instead of falling through)
2026-06-23 17:25:47 -05:00
Jonathan Miller 7b68963b67 feat(metadata): add deploy fields to repo metadata API (#692)
Universal: Auto Version Bump / Version Bump (push) Successful in 18s
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Failing after 1m2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m11s
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 2s
Universal: PR Check / Validate PR (pull_request) Failing after 10s
Universal: Build & Release / Promote to RC (pull_request) Failing after 18s
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 1m16s
Universal: PR Check / Secret Scan (pull_request) Successful in 1m23s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
- Migration v360: adds deploy_host, deploy_port, deploy_user, deploy_path,
  docker_image, docker_registry, container_name, health_url to repo_manifest
- API: GET/PUT /metadata now includes deploy fields
- Settings: preserve deploy fields on web UI save
- Remove 4 unneeded workflows (gitleaks, npm-publish, notify, workflow-sync)
  - gitleaks will become built-in (#692)
  - npm-publish/notify not applicable to Go repo
  - workflow-sync moving to MokoCLI
2026-06-23 14:32:31 -05:00
jmiller 22c2a99f8b chore: remove security-audit.yml -- handled by MokoGitea
Deploy MokoGitea / Verify dev environment is healthy (push) Failing after 2s
Deploy MokoGitea / deploy (push) Has been cancelled
2026-06-23 18:26:46 +00:00
jmiller c974970118 chore: remove security-audit.yml -- handled by MokoGitea
Universal: Auto Version Bump / Version Bump (push) Successful in 19s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m36s
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Failing after 1m6s
2026-06-23 18:04:41 +00:00
jmiller c9a8deee0e chore: remove deploy-manual.yml -- no longer needed
Universal: Auto Version Bump / Version Bump (push) Successful in 25s
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Failing after 3m2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m33s
2026-06-23 17:59:24 +00:00
jmiller ea05851d0a chore: remove composer-publish.yml -- no longer needed
Universal: Auto Version Bump / Version Bump (push) Successful in 23s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m44s
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Failing after 3m12s
2026-06-23 17:59:09 +00:00
Jonathan Miller 1178975be3 feat(wiki): full-text search across wiki pages (#550)
Universal: Auto Version Bump / Version Bump (push) Successful in 20s
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Failing after 1m25s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m16s
- Web: WikiSearch handler with case-insensitive search of titles and content
- Web: search.tmpl with search form and results display
- Web: "Search wiki" link added to wiki dropdown menu
- API: GET /wiki/search?q=term endpoint with pagination
- Recursive traversal handles nested folder wikis
2026-06-23 12:05:48 -05:00
Jonathan Miller b283fad8bd docs: update README and changelog with wiki features documentation
Universal: Auto Version Bump / Version Bump (push) Successful in 20s
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Failing after 1m1s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m3s
- README: rewrite with current feature set, fix stale code.mokoconsulting.tech URLs
- CHANGELOG: add missing wiki feature entries (#667, #668, #671, #673, #674, #675)
- Wiki Features page published to org wiki (standards/Wiki-Features)
2026-06-23 09:08:58 -05:00
Jonathan Miller a792772397 feat: dev environment deploy gate for production safety
Universal: Auto Version Bump / Version Bump (push) Successful in 20s
Deploy MokoGitea / Verify dev environment is healthy (push) Failing after 2s
Deploy MokoGitea (Dev) / Build & Deploy to Dev (push) Failing after 1m30s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m23s
Deploy MokoGitea / deploy (push) Has been cancelled
- New deploy-dev.yml: builds and deploys to dev.git.mokoconsulting.tech
  on push to dev branch
- Production deploy (deploy-mokogitea.yml): now checks dev health before
  building. If dev.git.mokoconsulting.tech/api/healthz fails, production
  deploy is blocked.
- Flow: push to dev → dev builds/deploys → verify healthy → merge to
  main → production checks dev health → builds/deploys production
2026-06-23 08:12:22 -05:00
Jonathan Miller 4d1be56bad fix: move CategoryPage type to package level to fix compile error
Universal: Auto Version Bump / Version Bump (push) Successful in 14s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 48s
Deploy MokoGitea / deploy (push) Successful in 4m51s
CategoryPage was defined inside WikiCategory() but referenced by
scanCategoryEntries() which is a top-level function. Renamed to
wikiCategoryPage and moved to package scope.
2026-06-23 07:40:19 -05:00
Jonathan Miller 2dc745c5fa feat(wiki): print view, ZIP export, and folder access control (#674, #675)
Universal: Auto Version Bump / Version Bump (push) Successful in 18s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 34s
Deploy MokoGitea / deploy (push) Failing after 4m10s
Print view: clean rendering without navigation chrome for printing.
ZIP export: download entire wiki as ZIP archive of markdown files.
Folder ACL: _access.yml per-folder write protection with role checks.
Resolve merge conflicts between #674 and #675 implementations.
2026-06-22 08:51:58 -05:00
Jonathan Miller 9dda78da7c feat(wiki): print view, ZIP export, and folder access control (#674, #675)
Universal: Auto Version Bump / Version Bump (push) Successful in 12s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 27s
Print view: clean page rendering without navigation chrome for printing.
ZIP export: download entire wiki as ZIP archive of markdown files.
Folder ACL: _access.yml files control per-folder write permissions.
Both accessible from wiki pages dropdown menu.
2026-06-22 08:47:36 -05:00
jmiller 6ceef765eb feat(wiki): per-folder access control via _access.yml (#674)
Deploy MokoGitea / deploy (push) Failing after 4m8s
2026-06-22 01:12:47 +00:00
jmiller 23d3528676 feat(wiki): per-folder access control via _access.yml (#674)
Deploy MokoGitea / deploy (push) Failing after 5m28s
2026-06-22 01:08:35 +00:00
jmiller 249b639c70 feat(wiki): per-folder access control via _access.yml (#674)
Universal: Auto Version Bump / Version Bump (push) Successful in 15s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 49s
2026-06-22 01:05:43 +00:00
jmiller 5c9db551dc feat(wiki): per-folder access control via _access.yml (#674) — template
Universal: Auto Version Bump / Version Bump (push) Successful in 11s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 41s
2026-06-22 00:54:07 +00:00
jmiller 408f2329b3 chore: sync auto-release.yml from Template-Generic [skip ci] 2026-06-22 00:35:06 +00:00
Jonathan Miller 827025bd17 feat(wiki): enhanced ToC — collapsible, inline, sticky, frontmatter control (#673)
Universal: Auto Version Bump / Version Bump (push) Successful in 13s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 56s
Deploy MokoGitea / deploy (push) Failing after 4m4s
ToC can be controlled via frontmatter: toc=false disables, toc=inline
shows at top of content instead of sidebar. Sidebar ToC is now
collapsible via <details> and sticky on scroll. Inline ToC also
uses collapsible <details> with "Contents" header.
2026-06-21 19:33:57 -05:00
Jonathan Miller 98da1644be feat(wiki): template transclusion — reusable content blocks (#671)
Universal: Auto Version Bump / Version Bump (push) Successful in 14s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 53s
Deploy MokoGitea / deploy (push) Failing after 3m57s
Add {{template:Name|key=val}} syntax for embedding reusable content.
Templates stored as _Template/Name.md with {{{key}}} parameter
substitution. Recursive with depth limit of 5. _Template folder
hidden from sidebar tree.
2026-06-21 19:05:12 -05:00
Jonathan Miller db596575a0 feat(wiki): page categories via YAML frontmatter (#668)
Universal: Auto Version Bump / Version Bump (push) Successful in 12s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 54s
Deploy MokoGitea / deploy (push) Failing after 5m44s
Add categories support using frontmatter: categories: [arch, api, ref]
Categories render as clickable tags at the bottom of wiki pages.
Category index page lists all pages tagged with a given category.
Frontmatter is stripped before markdown rendering.
2026-06-21 18:32:07 -05:00
Jonathan Miller 3c56dc8814 feat(wiki): revision diff — view changes for any wiki commit (#667)
Universal: Auto Version Bump / Version Bump (push) Successful in 14s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m9s
Deploy MokoGitea / deploy (push) Failing after 5m31s
Compare a wiki commit against its parent showing added/removed lines
with color-coded diff view. Accessible via diff icon button in wiki
page header and from revision history. Shows commit metadata
(author, message, timestamp) alongside the diff.
2026-06-21 18:23:09 -05:00
Jonathan Miller dce712fabd Merge remote-tracking branch 'origin/main' into dev
Deploy MokoGitea / deploy (push) Failing after 6m21s
2026-06-21 18:17:44 -05:00
Jonathan Miller 78b0ce9650 fix: org metadata API respects visibility for unauthenticated requests (#690)
Universal: Auto Version Bump / Version Bump (push) Successful in 18s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m28s
Add checkOrgVisibility() guard to issue-statuses, issue-priorities,
and issue-types endpoints. Public orgs remain accessible to everyone.
Private/limited orgs return 404 for unauthenticated callers.
2026-06-21 18:17:00 -05:00
jmiller 500a5be6d7 Merge pull request 'Release: Wiki redirects, code review fixes, bug fixes' (#689) from dev into main
Deploy MokoGitea / deploy (push) Failing after 7m34s
2026-06-21 23:14:07 +00:00
Jonathan Miller 95a747b1d5 docs: update changelog with post-#682 fixes and #672
Universal: Auto Version Bump / Version Bump (push) Successful in 14s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m1s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Failing after 1m27s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 12s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Secret Scan (pull_request) Successful in 2m43s
PR RC Release / Build RC Release (pull_request) Failing after 2m5s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 7s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
2026-06-21 18:13:28 -05:00
Jonathan Miller bb7e99ad40 fix: backlinks template pluralization
Universal: Auto Version Bump / Version Bump (push) Successful in 16s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m6s
Deploy MokoGitea / deploy (push) Failing after 6m34s
2026-06-21 18:10:16 -05:00
Jonathan Miller 6c6b7c888e feat(wiki): page rename with automatic redirects (#672)
Universal: Auto Version Bump / Version Bump (push) Successful in 16s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m25s
Deploy MokoGitea / deploy (push) Successful in 5m57s
When a wiki page is renamed, automatically create a redirect file at
the old path with YAML frontmatter (redirect: new/path). The redirect
is detected in renderViewPage() before markdown rendering, issuing an
HTTP redirect with a flash message "Redirected from OldPage".
2026-06-21 18:00:56 -05:00
Jonathan Miller 2a1692d599 fix: resolve code review issues in wiki and status features
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 36s
Deploy MokoGitea / deploy (push) Successful in 7m31s
- collectWikiPages: store hyphen-normalized names for flexible lookup
- Backlinks: use wiki_service.GitPathToWebPath for subdirectory URLs
- issue_statuses.tmpl: fix garbled em-dash bytes (0x97 → hyphen)
- backlinks.tmpl: fix pluralization text
2026-06-21 17:57:46 -05:00
Jonathan Miller 6984ac108f Merge remote-tracking branch 'origin/main' into dev
Deploy MokoGitea / deploy (push) Successful in 4m47s
2026-06-21 17:39:36 -05:00
Jonathan Miller 3fdbe94830 fix: use commit.MessageTitle() instead of commit.Message() in wiki recent changes
Universal: Auto Version Bump / Version Bump (push) Successful in 11s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 35s
The Commit type embeds CommitMessage which provides MessageTitle(),
MessageUTF8(), and MessageBody() — not Message().
2026-06-21 17:38:43 -05:00
jmiller e937dd8d8b Merge pull request 'Release: Wiki system, licensing, issue statuses, metadata API' (#682) from dev into main
Deploy MokoGitea / deploy (push) Failing after 4m39s
2026-06-21 22:26:33 +00:00
Jonathan Miller e7b70f54ed docs: add #670 to changelog
Universal: PR Check / Branch Policy (pull_request) Successful in 4s
Universal: PR Check / Validate PR (pull_request) Failing after 21s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: PR Check / Secret Scan (pull_request) Successful in 1m50s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 6s
PR RC Release / Build RC Release (pull_request) Failing after 1m52s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Failing after 1m11s
Universal: Auto Version Bump / Version Bump (push) Successful in 11s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 32s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
2026-06-21 17:24:17 -05:00
Jonathan Miller b161561571 Merge remote-tracking branch 'origin/main' into dev
Deploy MokoGitea / deploy (push) Failing after 5m8s
2026-06-21 17:21:01 -05:00
Jonathan Miller b981cf72e3 feat(wiki): recent changes page — cross-page edit activity (#670)
Universal: Auto Version Bump / Version Bump (push) Successful in 13s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m4s
Shows all recent wiki edits with page name, author, edit summary,
and timestamp. Paginated. Accessible via "Recent changes" in the
wiki pages dropdown menu.
2026-06-21 17:20:12 -05:00
jmiller 9964c7e16c chore: sync auto-release.yml from Template-Generic [skip ci] 2026-06-21 22:02:50 +00:00
Jonathan Miller ff27e77c37 docs: update changelog with session 2 changes
Universal: Auto Version Bump / Version Bump (push) Successful in 12s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m6s
2026-06-21 16:54:20 -05:00
Jonathan Miller 04ce7dc896 feat(wiki): internal wikilinks with [[Page Name]] syntax (#666)
Deploy MokoGitea / deploy (push) Successful in 4m51s
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 40s
Add Wikipedia-style [[Page Name]] and [[Page|Display Text]] syntax.
Existing pages render as normal links; non-existent pages render as
red "new page" links. Supports [[folder/Page]], [[Page#Section]],
and [[#Anchor]] patterns.
2026-06-21 15:42:50 -05:00
Jonathan Miller f87f904a21 fix: remove Version field from metadata settings template
Version is no longer stored in RepoMetadata struct — it comes from
the metadata API. The template reference caused a 500 error.
2026-06-21 15:42:46 -05:00
Jonathan Miller fc72d8e90a feat(wiki): add backlinks — "What links here" for wiki pages (#669)
Deploy MokoGitea / deploy (push) Successful in 3m27s
Scan all wiki pages for references to the current page and display
them on a dedicated backlinks page. Uses content search across
markdown files (no database needed). Adds cross-reference button
to wiki page header.
2026-06-21 11:45:20 -05:00
Jonathan Miller 71d52e432e feat: enforce required baseline issue statuses (#681)
Deploy MokoGitea / deploy (push) Successful in 5m8s
Add IsRequired field to IssueStatusDef. Open and Closed statuses are
seeded as required and cannot be deleted. Delete attempts return an
error flash in the web UI and ErrStatusRequired in the model layer.
API response now includes is_required field.
2026-06-21 11:29:49 -05:00
jmiller 172303b61f chore: sync pre-release.yml from Template-Generic [skip ci] 2026-06-21 16:05:39 +00:00
Jonathan Miller bfb4b53da3 feat: add folder-based tree sidebar to org wiki (#680)
Deploy MokoGitea / deploy (push) Successful in 3m31s
Replace flat page list with hierarchical folder tree in org wiki sidebar.
_Sidebar.md takes precedence when present; otherwise auto-generates
collapsible folder menus up to 2 levels deep.
2026-06-21 11:00:18 -05:00
Jonathan Miller 9149fa100c feat: make metadata/manifest GET endpoint publicly accessible (#676)
Deploy MokoGitea / deploy (push) Successful in 3m30s
Remove reqRepoReader auth requirement from GET /repos/{owner}/{repo}/metadata
and /manifest endpoints. PUT (update) still requires token + admin.
2026-06-21 10:20:56 -05:00
35 changed files with 2707 additions and 802 deletions
+467 -421
View File
@@ -1,421 +1,467 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> # Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# #
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Release # INGROUP: mokocli.Release
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
# PATH: /templates/workflows/universal/auto-release.yml.template # PATH: /templates/workflows/universal/auto-release.yml.template
# VERSION: 05.00.00 # VERSION: 05.00.00
# BRIEF: Universal build & release detects platform from manifest.xml # BRIEF: Universal build & release detects platform from manifest.xml
# #
# +========================================================================+ # +=======================================================================+
# | UNIVERSAL BUILD & RELEASE PIPELINE | # | UNIVERSAL BUILD & RELEASE PIPELINE |
# +========================================================================+ # +=======================================================================+
# | | # | |
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | # | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
# | | # | |
# | Platform-specific: | # | Platform-specific: |
# | joomla: XML manifest, type-prefixed packages | # | joomla: XML manifest, type-prefixed packages |
# | dolibarr: mod*.class.php, update.txt, dev version reset | # | dolibarr: mod*.class.php, update.txt, dev version reset |
# | generic: README-only, no update stream | # | generic: README-only, no update stream |
# | | # | |
# +========================================================================+ # +=======================================================================+
name: "Universal: Build & Release" name: "Universal: Build & Release"
on: on:
pull_request: pull_request:
types: [opened, closed] types: [opened, synchronize, closed]
branches: branches:
- main - main
workflow_dispatch: paths-ignore:
inputs: - '.mokogitea/workflows/**'
action: - '*.md'
description: 'Action to perform' - 'wiki/**'
required: false - '.editorconfig'
type: choice - '.gitignore'
default: release - '.gitattributes'
options: - '.gitmessage'
- release - 'LICENSE'
- promote-rc workflow_dispatch:
inputs:
env: action:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true description: 'Action to perform'
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} required: false
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} type: choice
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} default: release
options:
permissions: - release
contents: write - promote-rc
jobs: env:
# ── PR Opened → Rename branch to RC and build RC release ───────────────────── FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
promote-rc: GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
name: Promote to RC GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
runs-on: release GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
if: >-
(github.event.action == 'opened' && github.event.pull_request.merged != true) || permissions:
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') contents: write
steps: jobs:
- name: Checkout repository # ── PR Opened → Rename branch to RC and build RC release ─────────────────────────
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 promote-rc:
with: name: Promote to RC
token: ${{ secrets.MOKOGITEA_TOKEN }} runs-on: release
fetch-depth: 1 if: >-
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
- name: Setup mokocli tools (github.event.action == 'synchronize' && github.event.pull_request.merged != true) ||
env: (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting steps:
run: | - name: Checkout repository
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
echo Using pre-installed /opt/mokocli with:
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV token: ${{ secrets.MOKOGITEA_TOKEN }}
else fetch-depth: 1
echo Falling back to fresh clone
if ! command -v composer > /dev/null 2>&1; then - name: Setup mokocli tools
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 env:
fi MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
rm -rf /tmp/mokocli MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git run: |
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
cd /tmp/mokocli echo Using pre-installed /opt/mokocli
composer install --no-dev --no-interaction --quiet echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV else
fi echo Falling back to fresh clone
if ! command -v composer > /dev/null 2>&1; then
- name: Rename branch to rc 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
run: | fi
php ${MOKO_CLI}/branch_rename.php \ rm -rf /tmp/mokocli
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
--token "${{ secrets.MOKOGITEA_TOKEN }}" \ git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ cd /tmp/mokocli
--pr "${{ github.event.pull_request.number }}" composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
- name: Checkout rc and configure git fi
run: |
git fetch origin rc - name: Rename branch to rc
git checkout rc run: |
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" php ${MOKO_CLI}/branch_rename.php \
git config --local user.name "gitea-actions[bot]" --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" --token "${{ secrets.MOKOGITEA_TOKEN }}" \
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
- name: Publish RC release --pr "${{ github.event.pull_request.number }}"
run: |
php ${MOKO_CLI}/release_publish.php \ - name: Checkout rc and configure git
--path . --stability rc --bump minor --branch rc \ run: |
--token "${{ secrets.MOKOGITEA_TOKEN }}" git fetch origin rc
git checkout rc
- name: Update RC release notes from CHANGELOG.md git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
run: | git config --local user.name "gitea-actions[bot]"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
- name: Publish RC release
# Extract [Unreleased] section from changelog run: |
NOTES="" php ${MOKO_CLI}/release_publish.php \
if [ -f "CHANGELOG.md" ]; then --path . --stability rc --bump minor --branch rc \
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) --token "${{ secrets.MOKOGITEA_TOKEN }}"
fi
[ -z "$NOTES" ] && NOTES="Release candidate" - name: Update RC release notes from CHANGELOG.md
run: |
# Find the RC release and update its body API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
RELEASE_ID=$(curl -sf -H "Authorization: token ${TOKEN}" \ TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
"${API_BASE}/releases/tags/release-candidate" \
| python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) # Extract [Unreleased] section from changelog
NOTES=""
if [ -n "$RELEASE_ID" ]; then if [ -f "CHANGELOG.md" ]; then
python3 -c " NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
import json, urllib.request fi
body = open('/dev/stdin').read() [ -z "$NOTES" ] && NOTES="Release candidate"
payload = json.dumps({'body': body}).encode()
req = urllib.request.Request( # Find the RC release and update its body
'${API_BASE}/releases/${RELEASE_ID}', RELEASE_ID=$(curl -sf -H "Authorization: token ${TOKEN}" \
data=payload, method='PATCH', "${API_BASE}/releases/tags/release-candidate" \
headers={ | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
'Authorization': 'token ${TOKEN}',
'Content-Type': 'application/json' if [ -n "$RELEASE_ID" ]; then
}) python3 -c "
urllib.request.urlopen(req) import json, urllib.request
" <<< "$NOTES" body = open('/dev/stdin').read()
echo "RC release notes updated from CHANGELOG.md" payload = json.dumps({'body': body}).encode()
fi req = urllib.request.Request(
'${API_BASE}/releases/${RELEASE_ID}',
- name: Summary data=payload, method='PATCH',
if: always() headers={
run: | 'Authorization': 'token ${TOKEN}',
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY 'Content-Type': 'application/json'
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY })
urllib.request.urlopen(req)
# ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── " <<< "$NOTES"
release: echo "RC release notes updated from CHANGELOG.md"
name: Build & Release Pipeline fi
runs-on: release
if: >- - name: Summary
github.event.pull_request.merged == true || if: always()
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') run: |
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
steps: echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 # ── Merged PR → Build & Release (or promote RC to stable) ─────────────────────────
with: release:
token: ${{ secrets.MOKOGITEA_TOKEN }} name: Build & Release Pipeline
fetch-depth: 0 runs-on: release
if: >-
- name: Configure git for bot pushes github.event.pull_request.merged == true ||
run: | (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]" steps:
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Check for merge conflict markers with:
run: | token: ${{ secrets.MOKOGITEA_TOKEN }}
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) fetch-depth: 0
if [ -n "$CONFLICTS" ]; then
echo "::error::Merge conflict markers found — aborting release" - name: Configure git for bot pushes
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY run: |
echo '```' >> $GITHUB_STEP_SUMMARY git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY git config --local user.name "gitea-actions[bot]"
echo '```' >> $GITHUB_STEP_SUMMARY git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
exit 1
fi - name: Check for merge conflict markers
echo "No conflict markers found" run: |
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
- name: Setup mokocli tools if [ -n "$CONFLICTS" ]; then
env: echo "::error::Merge conflict markers found — aborting release"
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting echo '```' >> $GITHUB_STEP_SUMMARY
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
run: | echo '```' >> $GITHUB_STEP_SUMMARY
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then exit 1
echo Using pre-installed /opt/mokocli fi
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV echo "No conflict markers found"
else
echo Falling back to fresh clone - name: Setup mokocli tools
if ! command -v composer > /dev/null 2>&1; then env:
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1 MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
fi MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
rm -rf /tmp/mokocli COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git run: |
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
cd /tmp/mokocli echo Using pre-installed /opt/mokocli
composer install --no-dev --no-interaction --quiet echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV else
fi echo Falling back to fresh clone
if ! command -v composer > /dev/null 2>&1; then
- name: "Detect platform" sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
id: platform fi
run: | rm -rf /tmp/mokocli
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
php ${MOKO_CLI}/manifest_read.php --path . --github-output 2>/dev/null || true git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
cd /tmp/mokocli
- name: "Determine version bump level" composer install --no-dev --no-interaction --quiet
id: bump echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
run: | fi
# Fix/patch branches: version was already bumped by pre-release, just strip suffix
# Feature/dev branches: bump minor for the new stable release - name: "Detect platform"
HEAD_REF="${{ github.event.pull_request.head.ref || 'dev' }}" id: platform
case "$HEAD_REF" in run: |
fix/*|patch/*|hotfix/*|bugfix/*) BUMP="none" ;; php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
*) BUMP="minor" ;; php ${MOKO_CLI}/manifest_read.php --path . --github-output 2>/dev/null || true
esac
echo "level=${BUMP}" >> "$GITHUB_OUTPUT" - name: "Determine version bump level"
echo "Bump level: ${BUMP} (from branch: ${HEAD_REF})" id: bump
run: |
- name: "Publish stable release" # Fix/patch branches: version was already bumped by pre-release, just strip suffix
run: | # Feature/dev branches: bump minor for the new stable release
BUMP_FLAG="" HEAD_REF="${{ github.event.pull_request.head.ref || 'dev' }}"
if [ "${{ steps.bump.outputs.level }}" != "none" ]; then case "$HEAD_REF" in
BUMP_FLAG="--bump ${{ steps.bump.outputs.level }}" fix/*|patch/*|hotfix/*|bugfix/*) BUMP="none" ;;
fi *) BUMP="minor" ;;
php ${MOKO_CLI}/release_publish.php \ esac
--path . --stability stable ${BUMP_FLAG} --branch main \ echo "level=${BUMP}" >> "$GITHUB_OUTPUT"
--token "${{ secrets.MOKOGITEA_TOKEN }}" echo "Bump level: ${BUMP} (from branch: ${HEAD_REF})"
- name: "Read published version" - name: "Publish stable release"
id: version run: |
run: | BUMP_FLAG=""
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "") if [ "${{ steps.bump.outputs.level }}" != "none" ]; then
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') BUMP_FLAG="--bump ${{ steps.bump.outputs.level }}"
[ -z "$VERSION" ] && VERSION="00.00.00" && echo "skip=true" >> "$GITHUB_OUTPUT" fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT" php ${MOKO_CLI}/release_publish.php \
echo "tag=stable" >> "$GITHUB_OUTPUT" --path . --stability stable ${BUMP_FLAG} --branch main \
echo "release_tag=stable" >> "$GITHUB_OUTPUT" --token "${{ secrets.MOKOGITEA_TOKEN }}"
echo "branch=main" >> "$GITHUB_OUTPUT"
echo "Published version: ${VERSION}" - name: "Read published version"
id: version
- name: Update release notes and promote changelog run: |
run: | VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "")
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" [ -z "$VERSION" ] && VERSION="00.00.00" && echo "skip=true" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
# Get the stable release info (version and ID) PLATFORM="${{ steps.platform.outputs.platform }}"
RELEASE_JSON=$(curl -sf -H "Authorization: token ${TOKEN}" \ if [[ "$PLATFORM" == joomla* ]]; then
"${API_BASE}/releases/tags/stable" 2>/dev/null || echo '{}') echo "tag=stable" >> "$GITHUB_OUTPUT"
RELEASE_ID=$(python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" <<< "$RELEASE_JSON" 2>/dev/null || true) echo "release_tag=stable" >> "$GITHUB_OUTPUT"
# Extract version from release name (e.g. "06.17.00" or "v06.17.00") else
VERSION=$(python3 -c " echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
import json, sys, re echo "release_tag=v${VERSION}" >> "$GITHUB_OUTPUT"
r = json.load(sys.stdin) fi
name = r.get('name', '') echo "branch=main" >> "$GITHUB_OUTPUT"
m = re.search(r'(\d+\.\d+\.\d+)', name) echo "Published version: ${VERSION}"
print(m.group(1) if m else '')
" <<< "$RELEASE_JSON" 2>/dev/null || true) - name: "Create semver tag for non-Joomla repos"
id: semver
# Extract [Unreleased] section from changelog if: |
NOTES="" steps.version.outputs.skip != 'true' &&
if [ -f "CHANGELOG.md" ]; then !startsWith(steps.platform.outputs.platform, 'joomla')
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) run: |
fi VERSION="${{ steps.version.outputs.version }}"
[ -z "$NOTES" ] && NOTES="Stable release" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Update release body via API SEMVER_TAG="v${VERSION}"
if [ -n "$RELEASE_ID" ]; then
python3 -c " echo "Creating semver tag: ${SEMVER_TAG}"
import json, urllib.request
body = open('/dev/stdin').read() # Create the git tag via API
payload = json.dumps({'body': body}).encode() HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" \
req = urllib.request.Request( -X POST -H "Authorization: token ${TOKEN}" \
'${API_BASE}/releases/${RELEASE_ID}', -H "Content-Type: application/json" \
data=payload, method='PATCH', "${API_BASE}/tags" \
headers={ -d "{\"tag_name\":\"${SEMVER_TAG}\",\"target\":\"main\",\"message\":\"Release ${VERSION}\"}" 2>/dev/null || echo "000")
'Authorization': 'token ${TOKEN}',
'Content-Type': 'application/json' if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then
}) echo "Created semver tag: ${SEMVER_TAG}"
urllib.request.urlopen(req) elif [ "$HTTP_CODE" = "409" ]; then
" <<< "$NOTES" echo "Semver tag ${SEMVER_TAG} already exists (skipped)"
echo "Release notes updated from CHANGELOG.md" else
fi echo "::warning::Failed to create semver tag ${SEMVER_TAG} (HTTP ${HTTP_CODE})"
fi
# Promote [Unreleased] → [version] in CHANGELOG.md and reset
if [ -n "$VERSION" ] && [ -f "CHANGELOG.md" ]; then echo "semver_tag=${SEMVER_TAG}" >> "$GITHUB_OUTPUT"
DATE=$(date +%Y-%m-%d)
python3 -c " - name: Update release notes and promote changelog
import sys run: |
version, date = sys.argv[1], sys.argv[2] API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
content = open('CHANGELOG.md').read() TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
old = '## [Unreleased]'
new = f'## [Unreleased]\n\n## [{version}] --- {date}' # Get the stable release info (version and ID)
content = content.replace(old, new, 1) RELEASE_JSON=$(curl -sf -H "Authorization: token ${TOKEN}" \
open('CHANGELOG.md', 'w').write(content) "${API_BASE}/releases/tags/stable" 2>/dev/null || echo '{}')
" "$VERSION" "$DATE" RELEASE_ID=$(python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" <<< "$RELEASE_JSON" 2>/dev/null || true)
git add CHANGELOG.md # Extract version from release name (e.g. "06.17.00" or "v06.17.00")
git commit -m "chore: promote changelog [Unreleased] → [${VERSION}]" || true VERSION=$(python3 -c "
git push origin main || true import json, sys, re
echo "Changelog promoted: [Unreleased] → [${VERSION}]" r = json.load(sys.stdin)
fi name = r.get('name', '')
m = re.search(r'(\d+\.\d+\.\d+)', name)
# -- STEP 9: Mirror to GitHub (stable only) -------------------------------- print(m.group(1) if m else '')
- name: "Step 9: Mirror release to GitHub" " <<< "$RELEASE_JSON" 2>/dev/null || true)
if: >-
steps.version.outputs.skip != 'true' && # Extract [Unreleased] section from changelog
secrets.GH_MIRROR_TOKEN != '' NOTES=""
continue-on-error: true if [ -f "CHANGELOG.md" ]; then
run: | NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" fi
RELEASE_TAG="${{ steps.version.outputs.release_tag }}" [ -z "$NOTES" ] && NOTES="Stable release"
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" # Update release body via API
php ${MOKO_CLI}/release_mirror.php \ if [ -n "$RELEASE_ID" ]; then
--version "$VERSION" --tag "$RELEASE_TAG" \ python3 -c "
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ import json, urllib.request
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \ body = open('/dev/stdin').read()
--branch main 2>&1 || true payload = json.dumps({'body': body}).encode()
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY req = urllib.request.Request(
'${API_BASE}/releases/${RELEASE_ID}',
# -- STEP 10: Sync main branch to GitHub mirror ---------------------------- data=payload, method='PATCH',
- name: "Step 10: Push main to GitHub mirror" headers={
if: >- 'Authorization': 'token ${TOKEN}',
steps.version.outputs.skip != 'true' && 'Content-Type': 'application/json'
secrets.GH_MIRROR_TOKEN != '' })
continue-on-error: true urllib.request.urlopen(req)
run: | " <<< "$NOTES"
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" echo "Release notes updated from CHANGELOG.md"
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) fi
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ # Promote [Unreleased] → [version] in CHANGELOG.md and reset
git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" if [ -n "$VERSION" ] && [ -f "CHANGELOG.md" ]; then
git fetch origin main --depth=1 DATE=$(date +%Y-%m-%d)
git push github origin/main:refs/heads/main --force 2>/dev/null \ python3 -c "
&& echo "main branch pushed to GitHub mirror" \ import sys
|| echo "WARNING: GitHub mirror push failed" version, date = sys.argv[1], sys.argv[2]
content = open('CHANGELOG.md').read()
- name: "Step 11: Delete rc branch and recreate dev from main" old = '## [Unreleased]'
if: steps.version.outputs.skip != 'true' new = f'## [Unreleased]\n\n## [{version}] --- {date}'
continue-on-error: true content = content.replace(old, new, 1)
run: | open('CHANGELOG.md', 'w').write(content)
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" " "$VERSION" "$DATE"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" git add CHANGELOG.md
git commit -m "chore: promote changelog [Unreleased] → [${VERSION}]" || true
# Delete rc branch (ephemeral — created by promote-rc) git push origin main || true
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ echo "Changelog promoted: [Unreleased] → [${VERSION}]"
"${API_BASE}/branches/rc" 2>/dev/null \ fi
&& echo "Deleted rc branch" || echo "rc branch not found"
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
# Delete dev branch - name: "Step 9: Mirror release to GitHub"
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ if: >-
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch" steps.version.outputs.skip != 'true' &&
secrets.GH_MIRROR_TOKEN != ''
# Recreate dev from main (now includes version bump + changelog promotion) continue-on-error: true
curl -sf -X POST -H "Authorization: token ${TOKEN}" \ run: |
-H "Content-Type: application/json" \ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
"${API_BASE}/branches" \ RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY php ${MOKO_CLI}/release_mirror.php \
--version "$VERSION" --tag "$RELEASE_TAG" \
- name: "Step 12: Create version branch from main" --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
if: steps.version.outputs.skip != 'true' --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
continue-on-error: true --branch main 2>&1 || true
run: | echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" # -- STEP 10: Sync main branch to GitHub mirror ----------------------------
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - name: "Step 10: Push main to GitHub mirror"
BRANCH_NAME="version/${VERSION}" if: >-
MAIN_SHA=$(git rev-parse HEAD) steps.version.outputs.skip != 'true' &&
secrets.GH_MIRROR_TOKEN != ''
# Delete old version branch if it exists (same version re-release) continue-on-error: true
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}" run: |
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
# Create version/XX.YY.ZZ from main GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed" GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
git fetch origin main --depth=1
git push github origin/main:refs/heads/main --force 2>/dev/null \
&& echo "main branch pushed to GitHub mirror" \
# -- Dolibarr post-release: Reset dev version ----------------------------- || echo "WARNING: GitHub mirror push failed"
- name: "Post-release: Reset dev version"
if: steps.version.outputs.skip != 'true' - name: "Step 11: Delete rc branch and recreate dev from main"
continue-on-error: true if: steps.version.outputs.skip != 'true'
run: | continue-on-error: true
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" run: |
php ${MOKO_CLI}/version_reset_dev.php \ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \ TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
--branch dev --path . 2>&1 || true
# Delete rc branch (ephemeral — created by promote-rc)
# -- Summary -------------------------------------------------------------- curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
- name: Pipeline Summary "${API_BASE}/branches/rc" 2>/dev/null \
if: always() && echo "Deleted rc branch" || echo "rc branch not found"
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" # Delete dev branch
PLATFORM="${{ steps.platform.outputs.platform }}" curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY # Recreate dev from main (now includes version bump + changelog promotion)
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then curl -sf -X POST -H "Authorization: token ${TOKEN}" \
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY -H "Content-Type: application/json" \
else "${API_BASE}/branches" \
echo "" >> $GITHUB_STEP_SUMMARY -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY - name: "Step 12: Create version branch from main"
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY if: steps.version.outputs.skip != 'true'
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY continue-on-error: true
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY run: |
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
fi VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
BRANCH_NAME="version/${VERSION}"
MAIN_SHA=$(git rev-parse HEAD)
# Delete old version branch if it exists (same version re-release)
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
# Create version/XX.YY.ZZ from main
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
# -- Dolibarr post-release: Reset dev version -----------------------------
- name: "Post-release: Reset dev version"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/version_reset_dev.php \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
--branch dev --path . 2>&1 || true
# -- Summary --------------------------------------------------------------
- name: Pipeline Summary
if: always()
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
PLATFORM="${{ steps.platform.outputs.platform }}"
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
fi
-76
View File
@@ -1,76 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
name: "Publish to Composer"
on:
push:
tags:
- 'v*'
- '[0-9]*.[0-9]*.[0-9]*'
release:
types: [published]
workflow_dispatch:
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
jobs:
publish:
name: Publish Package
runs-on: ubuntu-latest
if: >-
!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[skip publish]')
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup PHP
run: |
if ! command -v php &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
- name: Install dependencies
run: composer install --no-dev --no-interaction --prefer-dist --quiet
- name: Determine version
id: version
run: |
VERSION=$(php -r "echo json_decode(file_get_contents('composer.json'))->version;")
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "Package version: ${VERSION}"
# Gitea Composer Registry — auto-publishes from tags
# The tag push itself registers the package at:
# https://git.mokoconsulting.tech/api/packages/MokoConsulting/composer
- name: Verify Gitea registry
run: |
echo "Gitea Composer registry auto-publishes from tags."
echo "Package available at: ${GITEA_URL}/api/packages/MokoConsulting/composer"
echo "Install: composer require mokoconsulting/mokocli"
# Packagist — notify of new version
- name: Notify Packagist
if: secrets.PACKAGIST_TOKEN != ''
run: |
VERSION="${{ steps.version.outputs.version }}"
echo "Notifying Packagist of version ${VERSION}..."
curl -sf -X POST \
-H "Content-Type: application/json" \
-d '{"repository":{"url":"https://git.mokoconsulting.tech/MokoConsulting/mokocli"}}' \
"https://packagist.org/api/update-package?username=mokoconsulting&apiToken=${{ secrets.PACKAGIST_TOKEN }}" \
&& echo "Packagist notified" \
|| echo "::warning::Packagist notification failed (package may not be registered yet)"
- name: Summary
run: |
VERSION="${{ steps.version.outputs.version }}"
echo "## Composer Package Published" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Registry | Status |" >> $GITHUB_STEP_SUMMARY
echo "|----------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Gitea | \`composer require mokoconsulting/mokocli:${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Packagist | \`composer require mokoconsulting/mokocli\` |" >> $GITHUB_STEP_SUMMARY
+109
View File
@@ -0,0 +1,109 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
# BRIEF: Build and deploy MokoGitea to dev environment on push to dev branch.
# Production deploy (deploy-mokogitea.yml) only succeeds if dev is healthy.
name: Deploy MokoGitea (Dev)
on:
push:
branches:
- dev
concurrency:
group: deploy-mokogitea-dev
cancel-in-progress: true
env:
REGISTRY: git.mokoconsulting.tech
IMAGE: mokoconsulting/mokogitea
DEPLOY_HOST: git.mokoconsulting.tech
DEPLOY_PORT: 2918
DEPLOY_USER: mokoconsulting
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
deploy-dev:
name: "Build & Deploy to Dev"
runs-on: ubuntu-latest
steps:
- name: Checkout source
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Determine version
id: config
run: |
VERSION=$(git describe --tags --always 2>/dev/null || echo "dev-$(git rev-parse --short HEAD)")
echo "tag=${VERSION}-dev" >> $GITHUB_OUTPUT
echo "Version: ${VERSION}-dev"
- name: Write deploy key
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
run: |
mkdir -p ~/.ssh
echo "$DEPLOY_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
- name: Build and deploy to dev via SSH
env:
REGISTRY_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
TAG: ${{ steps.config.outputs.tag }}
run: |
HEALTH_FMT='${{ '{{' }}.State.Health.Status${{ '}}' }}'
ssh -i ~/.ssh/deploy_key -p ${{ env.DEPLOY_PORT }} \
-o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o ServerAliveInterval=30 -o ServerAliveCountMax=10 \
${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} bash -s <<DEPLOY_EOF
set -e
echo 'SSH connected to dev environment'
echo 'Cleaning Docker build cache...'
docker builder prune -af 2>/dev/null || true
docker image prune -af 2>/dev/null || true
echo 'Pulling source...'
SOURCE_DIR=/opt/gitea-dev/source
if [ ! -d \$SOURCE_DIR/.git ]; then
git clone -b dev https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-Fork.git \$SOURCE_DIR
fi
cd \$SOURCE_DIR
git remote set-url origin https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-Fork.git 2>/dev/null || true
git fetch origin dev
git reset --hard origin/dev
echo 'Building Docker image...'
docker build --no-cache --build-arg GOFLAGS='-p 1' \
--tag ${{ env.REGISTRY }}/${{ env.IMAGE }}:\$TAG \
-f Dockerfile .
echo 'Pushing to registry...'
echo '\$REGISTRY_TOKEN' | docker login ${{ env.REGISTRY }} -u ${{ env.DEPLOY_USER }} --password-stdin
docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:\$TAG
echo 'Restarting dev container...'
cd /opt/gitea-dev
sed -i "s|${{ env.IMAGE }}:[^ ]*|${{ env.IMAGE }}:\$TAG|" docker-compose.yml
docker compose up -d mokogitea-dev
echo 'Health check...'
for i in 1 2 3 4 5 6 7 8; do
sleep 15
if docker inspect --format='\$HEALTH_FMT' mokogitea-dev 2>/dev/null | grep -q healthy; then
echo 'Dev container healthy!'
exit 0
fi
echo "Waiting... (attempt \$i/8)"
done
echo 'Health check failed'
docker logs mokogitea-dev --tail 20
exit 1
DEPLOY_EOF
- name: Verify dev instance
run: |
sleep 5
curl -sf https://git.dev.mokoconsulting.tech/api/healthz && echo " Dev API healthy"
+16
View File
@@ -36,7 +36,23 @@ env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs: jobs:
check-dev:
name: "Verify dev environment is healthy"
runs-on: ubuntu-latest
steps:
- name: Check dev health
run: |
echo "Checking git.dev.mokoconsulting.tech health..."
if curl -sf --max-time 10 https://git.dev.mokoconsulting.tech/api/healthz; then
echo " Dev environment is healthy — proceeding with production deploy"
else
echo "::error::Dev environment is NOT healthy — blocking production deploy"
echo "Deploy to dev first (push to dev branch) and verify it passes before merging to main."
exit 1
fi
deploy: deploy:
needs: check-dev
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout source (for version detection) - name: Checkout source (for version detection)
-51
View File
@@ -1,51 +0,0 @@
name: Publish MCP to npm
on:
push:
branches: [main]
paths:
- '.mokogitea/mcp/**'
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- name: Install and build
working-directory: .mokogitea/mcp
run: |
npm ci
npx tsc
- name: Check version change
id: version
working-directory: .mokogitea/mcp
run: |
LOCAL_VERSION=$(node -p "require('./package.json').version")
NPM_VERSION=$(npm view @mokoconsulting/mokogitea-mcp version 2>/dev/null || echo "0.0.0")
if [ "$LOCAL_VERSION" != "$NPM_VERSION" ]; then
echo "changed=true" >> $GITHUB_OUTPUT
echo "Version changed: $NPM_VERSION -> $LOCAL_VERSION"
else
echo "changed=false" >> $GITHUB_OUTPUT
echo "Version unchanged: $LOCAL_VERSION"
fi
- name: Publish to npm
if: steps.version.outputs.changed == 'true'
working-directory: .mokogitea/mcp
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish to Gitea registry
if: steps.version.outputs.changed == 'true'
working-directory: .mokogitea/mcp
run: |
npm publish --registry ${{ github.server_url }}/api/packages/${{ github.repository_owner }}/npm/ \
--//$(echo "${{ github.server_url }}" | sed 's|https://||')/api/packages/${{ github.repository_owner }}/npm/:_authToken=${{ secrets.MOKOGITEA_TOKEN }}
+16
View File
@@ -88,8 +88,20 @@ jobs:
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
php ${MOKO_CLI}/manifest_read.php --path . --github-output php ${MOKO_CLI}/manifest_read.php --path . --github-output
- name: Check platform eligibility (Joomla only)
id: eligibility
run: |
PLATFORM="${{ steps.platform.outputs.platform }}"
if [[ "$PLATFORM" == joomla* ]] || [[ "$PLATFORM" == "joomla" ]]; then
echo "proceed=true" >> "$GITHUB_OUTPUT"
else
echo "proceed=false" >> "$GITHUB_OUTPUT"
echo "::notice::Platform '$PLATFORM' — non-Joomla, skipping pre-release auto-bump"
fi
- name: Resolve metadata and bump version - name: Resolve metadata and bump version
id: meta id: meta
if: steps.eligibility.outputs.proceed == 'true'
run: | run: |
# Auto-detect stability from branch name on push, or use input on dispatch # Auto-detect stability from branch name on push, or use input on dispatch
if [ "${{ github.event_name }}" = "push" ]; then if [ "${{ github.event_name }}" = "push" ]; then
@@ -166,6 +178,7 @@ jobs:
- name: Create release - name: Create release
id: release id: release
if: steps.eligibility.outputs.proceed == 'true'
run: | run: |
TAG="${{ steps.meta.outputs.tag }}" TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}" VERSION="${{ steps.meta.outputs.version }}"
@@ -176,6 +189,7 @@ jobs:
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease --repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
- name: Update release notes from CHANGELOG.md - name: Update release notes from CHANGELOG.md
if: steps.eligibility.outputs.proceed == 'true'
run: | run: |
TAG="${{ steps.meta.outputs.tag }}" TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}" VERSION="${{ steps.meta.outputs.version }}"
@@ -212,6 +226,7 @@ jobs:
- name: Build package and upload - name: Build package and upload
id: package id: package
if: steps.eligibility.outputs.proceed == 'true'
run: | run: |
VERSION="${{ steps.meta.outputs.version }}" VERSION="${{ steps.meta.outputs.version }}"
TAG="${{ steps.meta.outputs.tag }}" TAG="${{ steps.meta.outputs.tag }}"
@@ -225,6 +240,7 @@ jobs:
# No need to build, commit, or sync updates.xml from workflows # No need to build, commit, or sync updates.xml from workflows
- name: "Delete lesser pre-release channels (cascade)" - name: "Delete lesser pre-release channels (cascade)"
if: steps.eligibility.outputs.proceed == 'true'
continue-on-error: true continue-on-error: true
run: | run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
-82
View File
@@ -1,82 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Security
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/security-audit.yml
# VERSION: 01.00.00
# BRIEF: Dependency vulnerability scanning for composer and npm packages
name: "Universal: Security Audit"
on:
schedule:
- cron: '0 6 * * 1' # Weekly on Monday at 06:00 UTC
pull_request:
branches:
- main
paths:
- 'composer.json'
- 'composer.lock'
- 'package.json'
- 'package-lock.json'
workflow_dispatch:
permissions:
contents: read
env:
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
jobs:
audit:
name: Dependency Audit
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Composer audit
if: hashFiles('composer.lock') != ''
run: |
echo "=== Composer Security Audit ==="
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli composer >/dev/null 2>&1
fi
composer audit --format=plain 2>&1 | tee /tmp/composer-audit.txt
RESULT=$?
if [ $RESULT -ne 0 ]; then
echo "::warning::Composer vulnerabilities found"
echo "composer_vulnerable=true" >> "$GITHUB_ENV"
else
echo "No known vulnerabilities in composer dependencies"
fi
- name: NPM audit
if: hashFiles('package-lock.json') != ''
run: |
echo "=== NPM Security Audit ==="
npm audit --production 2>&1 | tee /tmp/npm-audit.txt || true
if npm audit --production 2>&1 | grep -q "found 0 vulnerabilities"; then
echo "No known vulnerabilities in npm dependencies"
else
echo "::warning::NPM vulnerabilities found"
echo "npm_vulnerable=true" >> "$GITHUB_ENV"
fi
- name: Notify on vulnerabilities
if: env.composer_vulnerable == 'true' || env.npm_vulnerable == 'true'
run: |
REPO="${{ github.event.repository.name }}"
curl -sS \
-H "Title: ${REPO} has vulnerable dependencies" \
-H "Tags: lock,warning" \
-H "Priority: high" \
-d "Security audit found vulnerabilities. Review dependency updates." \
"${NTFY_URL}/${NTFY_TOPIC}" || true
+130
View File
@@ -0,0 +1,130 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow.Template
# INGROUP: MokoStandards.CI
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
# PATH: /.mokogitea/workflows/version-set.yml
# VERSION: 01.00.00
# BRIEF: Set or reset the extension version across all version-bearing files
name: "Joomla: Set Version"
on:
workflow_dispatch:
inputs:
version:
description: "Version number (e.g. 01.00.00)"
required: true
type: string
branch:
description: "Branch to update (default: current)"
required: false
type: string
permissions:
contents: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
set-version:
name: Set Version to ${{ inputs.version }}
runs-on: ubuntu-latest
steps:
- name: Validate version format
run: |
VERSION="${{ inputs.version }}"
if ! echo "$VERSION" | grep -qP '^\d{2}\.\d{2}\.\d{2}$'; then
echo "::error::Invalid version format '${VERSION}' — expected XX.YY.ZZ (e.g. 01.00.00)"
exit 1
fi
echo "VERSION=${VERSION}" >> "$GITHUB_ENV"
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.MOKOGITEA_TOKEN || secrets.GA_TOKEN || github.token }}
ref: ${{ inputs.branch || github.ref }}
fetch-depth: 1
- name: Update manifest version
run: |
MANIFEST=""
for XML_FILE in $(find . -maxdepth 3 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
MANIFEST="$XML_FILE"
break
fi
done
if [ -z "$MANIFEST" ]; then
echo "::warning::No Joomla extension manifest found — skipping manifest update"
else
OLD_VER=$(grep -oP '<version>\K[^<]+' "$MANIFEST" | head -1)
sed -i "s|<version>${OLD_VER}</version>|<version>${VERSION}</version>|" "$MANIFEST"
echo "Manifest: ${OLD_VER} → ${VERSION} (${MANIFEST})"
fi
- name: Update README.md version
run: |
if [ -f "README.md" ]; then
if grep -qP '^\s*VERSION:\s*\d' README.md; then
sed -i -E "s/(VERSION:\s*)[0-9]{2}\.[0-9]{2}\.[0-9]{2}/\1${VERSION}/" README.md
echo "README.md version updated to ${VERSION}"
else
echo "::warning::No VERSION line found in README.md — skipping"
fi
fi
- name: Update CHANGELOG.md
run: |
if [ -f "CHANGELOG.md" ]; then
DATE=$(date +%Y-%m-%d)
# Check if this version already has an entry
if grep -q "^\#\# \[${VERSION}\]" CHANGELOG.md; then
echo "CHANGELOG.md already has entry for ${VERSION} — skipping"
else
# Insert new version entry after [Unreleased] or at the top after header
if grep -q '^\#\# \[Unreleased\]' CHANGELOG.md; then
sed -i "/^\#\# \[Unreleased\]/a\\\\n## [${VERSION}] --- ${DATE}" CHANGELOG.md
else
sed -i "/^\# Changelog/a\\\\n## [Unreleased]\n\n## [${VERSION}] --- ${DATE}" CHANGELOG.md
fi
echo "CHANGELOG.md: added entry for ${VERSION}"
fi
else
echo "::warning::No CHANGELOG.md found — skipping"
fi
- name: Update FILE INFORMATION blocks
run: |
# Update VERSION in file header blocks (# VERSION: XX.YY.ZZ)
find . -maxdepth 1 -type f \( -name "*.yml" -o -name "*.yaml" -o -name "*.php" -o -name "*.md" \) \
-not -path "./.git/*" -not -path "./vendor/*" -print0 2>/dev/null | \
while IFS= read -r -d '' FILE; do
if head -20 "$FILE" | grep -qP '^\s*#?\s*VERSION:\s*\d{2}\.\d{2}\.\d{2}'; then
sed -i -E "s/(#?\s*VERSION:\s*)[0-9]{2}\.[0-9]{2}\.[0-9]{2}/\1${VERSION}/" "$FILE"
echo "Updated FILE INFORMATION VERSION in ${FILE}"
fi
done
- name: Commit and push
run: |
git config user.name "Moko Consulting [bot]"
git config user.email "hello@mokoconsulting.tech"
git add -A
if git diff --cached --quiet; then
echo "No version changes detected — nothing to commit"
else
git commit -m "chore: set version to ${VERSION} [skip bump]
Authored-by: Moko Consulting"
git push
echo "### Version Set" >> $GITHUB_STEP_SUMMARY
echo "Version updated to \`${VERSION}\` on branch \`${GITHUB_REF_NAME}\`" >> $GITHUB_STEP_SUMMARY
fi
+35
View File
@@ -3,6 +3,17 @@
## [Unreleased] ## [Unreleased]
### Added ### Added
- Wiki full-text search: case-insensitive search across all wiki page titles and content (#550)
- Wiki search API: GET /wiki/search?q=term with paginated JSON results (#550)
- Metadata deploy fields: deploy_host, deploy_port, deploy_user, deploy_path, docker_image, docker_registry, container_name, health_url (#692)
- Metadata API partial updates: PUT /metadata now merges only sent fields instead of replacing all
- Wiki revision diff: line-by-line diff view per commit in wiki page history (#667)
- Wiki categories: YAML frontmatter `categories:` with category index page (#668)
- Wiki template transclusion: `{{template:Name|key=val}}` with `_Template/` folder (#671)
- Wiki enhanced ToC: collapsible, inline via frontmatter, sticky sidebar (#673)
- Wiki folder ACL: `_access.yml` per-folder write protection (#674)
- Wiki print view and ZIP export of all wiki pages (#675)
- Wiki features documentation page in org wiki (standards/Wiki-Features)
- DLID licensing system: license, entitlement, activation, product_tier, audit_log tables (v359 migration) - DLID licensing system: license, entitlement, activation, product_tier, audit_log tables (v359 migration)
- License CRUD with CRC32-checksummed DLID generation and format validation - License CRUD with CRC32-checksummed DLID generation and format validation
- Entitlement model with tier-based rebuild and custom entitlement preservation - Entitlement model with tier-based rebuild and custom entitlement preservation
@@ -10,6 +21,30 @@
- 13 seeded product tiers from base to enterprise - 13 seeded product tiers from base to enterprise
- DLID-gated update XML endpoint: GET /api/v1/licensing/updates/{product}.xml - DLID-gated update XML endpoint: GET /api/v1/licensing/updates/{product}.xml
- Profile repo fallback chain: .mokogitea > .profile > .github - Profile repo fallback chain: .mokogitea > .profile > .github
- Metadata/manifest GET endpoint publicly accessible without auth (#676)
- Org wiki: folder-based collapsible tree sidebar, _Sidebar.md overrides (#680)
- Wiki backlinks: "What links here" page showing all pages referencing current page (#669)
- Wiki wikilinks: [[Page Name]] and [[Page|Display Text]] syntax with red links for missing pages (#666)
- Required baseline issue statuses: Open and Closed are indestructible (is_required flag) (#681)
- Issue status API response includes is_required field
- Wiki recent changes page: cross-page edit activity with pagination (#670)
- Wiki page rename with automatic redirects via YAML frontmatter (#672)
### Fixed
- Licensing API: handle DB write errors in UpdateLicense, UpdateTier, DeleteTier instead of silently discarding
- Wiki API: fix findEntryForFile URL-decode fallback for non-ASCII page names
- Metadata settings template 500 error: removed reference to deleted Version field
- Wiki recent changes: use commit.MessageTitle() instead of commit.Message()
- Wiki backlinks: proper URL encoding for subdirectory pages
- Wiki wikilinks: page existence lookup normalizes spaces and hyphens
- Issue statuses template: garbled em-dash character replaced
### Changed
- Issue status seed defaults: Open, In Progress, Waiting, In Review, Closed, Won't Fix
- Pre-release workflow: auto-bump skipped for non-Joomla repos (platform check)
### Removed
- Workflows: gitleaks.yml, npm-publish.yml, notify.yml, workflow-sync-trigger.yml, composer-publish.yml, deploy-manual.yml, security-audit.yml (not applicable to Go repo)
## [06.19.00] --- 2026-06-20 ## [06.19.00] --- 2026-06-20
+14 -23
View File
@@ -1,38 +1,29 @@
# MokoGitea # MokoGitea
Moko fork of Gitea — adding project board REST API endpoints and custom enhancements Custom Gitea fork with enhanced wiki system, DLID licensing, issue statuses, org metadata, and project board API.
![Language](https://img.shields.io/badge/Go-00ADD8?style=flat-square&logo=go&logoColor=white) ![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green?style=flat-square) ![Wiki](https://img.shields.io/badge/wiki-MokoGitea-blue?style=flat-square) ![Language](https://img.shields.io/badge/Go-00ADD8?style=flat-square&logo=go&logoColor=white) ![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green?style=flat-square)
Custom Gitea fork with Project Board API
--- ---
## Pages ## Key Features
- [Branding](https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/wiki/Branding) - **Wiki System** -- wikilinks, categories, backlinks, template transclusion, revision diffs, rename redirects, folder ACL, enhanced ToC, print view, ZIP export ([details](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki/standards/Wiki-Features))
- [Deployment](https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/wiki/Deployment) - **DLID Licensing** -- license management, entitlements, domain activations, ed25519-signed downloads
- [Project API](Project API) - **Issue Statuses** -- custom workflow statuses per org with required baseline protection
- [roadmap](https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/wiki/roadmap) - **Org Metadata** -- per-repo metadata API (public GET, admin PUT), platform detection for versioning
- **Project Board API** -- REST endpoints for project columns and cards
--- - **Dev Deploy Gate** -- builds deploy to dev environment first, production checks dev health
**Category:** Infrastructure | **Platform:** [MokoPlatform wiki](https://code.mokoconsulting.tech/MokoConsulting/MokoPlatform/wiki)
---
---
## Documentation ## Documentation
Full documentation is available on the [Wiki](https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/wiki). - [Org Wiki](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki/) -- standards, CLI reference, API docs
- [Wiki Features](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki/standards/Wiki-Features) -- all 10 wiki enhancements
- [Licensing API](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki/api/Licensing-API)
## Contributing ## Contributing
See the wiki for development guidelines and contribution instructions. See the [org wiki](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki/) for development guidelines, coding standards, and contribution instructions.
## License ## License
@@ -40,4 +31,4 @@ This project is licensed under the GNU General Public License v3.0 or later -- s
--- ---
*[Moko Consulting](https://mokoconsulting.tech) -- [MokoStandards](https://code.mokoconsulting.tech/MokoConsulting/MokoPlatform/wiki/Home)* *[Moko Consulting](https://mokoconsulting.tech)*
+32 -6
View File
@@ -22,6 +22,7 @@ type IssueStatusDef struct {
Color string `xorm:"VARCHAR(7)"` // hex color, e.g. "#e11d48" Color string `xorm:"VARCHAR(7)"` // hex color, e.g. "#e11d48"
Description string `xorm:"TEXT"` Description string `xorm:"TEXT"`
ClosesIssue bool `xorm:"NOT NULL DEFAULT false 'closes_issue'"` ClosesIssue bool `xorm:"NOT NULL DEFAULT false 'closes_issue'"`
IsRequired bool `xorm:"NOT NULL DEFAULT false 'is_required'"` // cannot be deleted
SortOrder int `xorm:"NOT NULL DEFAULT 0 'sort_order'"` SortOrder int `xorm:"NOT NULL DEFAULT 0 'sort_order'"`
IsActive bool `xorm:"NOT NULL DEFAULT true 'is_active'"` IsActive bool `xorm:"NOT NULL DEFAULT true 'is_active'"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
@@ -56,14 +57,15 @@ func GetIssueStatusDefsByOrg(ctx context.Context, orgID int64) ([]*IssueStatusDe
} }
// seedDefaultIssueStatuses creates the standard status presets for an org. // seedDefaultIssueStatuses creates the standard status presets for an org.
// Open and Closed are required (is_required=true) and cannot be deleted.
func seedDefaultIssueStatuses(ctx context.Context, orgID int64) error { func seedDefaultIssueStatuses(ctx context.Context, orgID int64) error {
defaults := []*IssueStatusDef{ defaults := []*IssueStatusDef{
{OrgID: orgID, Name: "In Progress", Color: "#2563eb", Description: "Work is actively being done", SortOrder: 1, IsActive: true}, {OrgID: orgID, Name: "Open", Color: "#2563eb", Description: "New or active issue", ClosesIssue: false, IsRequired: true, SortOrder: 0, IsActive: true},
{OrgID: orgID, Name: "Needs Info", Color: "#f59e0b", Description: "Waiting for more information", SortOrder: 2, IsActive: true}, {OrgID: orgID, Name: "In Progress", Color: "#7c3aed", Description: "Work is actively being done", SortOrder: 1, IsActive: true},
{OrgID: orgID, Name: "Blocked", Color: "#dc2626", Description: "Cannot proceed due to dependency", SortOrder: 3, IsActive: true}, {OrgID: orgID, Name: "Waiting", Color: "#f59e0b", Description: "Blocked or waiting for input", SortOrder: 2, IsActive: true},
{OrgID: orgID, Name: "Resolved", Color: "#16a34a", Description: "Fix implemented and verified", ClosesIssue: true, SortOrder: 4, IsActive: true}, {OrgID: orgID, Name: "In Review", Color: "#0891b2", Description: "PR submitted, awaiting review", SortOrder: 3, IsActive: true},
{OrgID: orgID, Name: "Closed", Color: "#16a34a", Description: "Completed or resolved", ClosesIssue: true, IsRequired: 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: "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 { for _, d := range defaults {
if _, err := db.GetEngine(ctx).Insert(d); err != nil { if _, err := db.GetEngine(ctx).Insert(d); err != nil {
@@ -111,13 +113,37 @@ func UpdateIssueStatusDef(ctx context.Context, def *IssueStatusDef) error {
return err return err
} }
// ErrStatusRequired is returned when trying to delete a required status.
type ErrStatusRequired struct {
ID int64
Name string
}
func (e ErrStatusRequired) Error() string {
return "status is required and cannot be deleted"
}
// IsErrStatusRequired checks if an error is ErrStatusRequired.
func IsErrStatusRequired(err error) bool {
_, ok := err.(ErrStatusRequired)
return ok
}
// DeleteIssueStatusDef deletes a status definition and clears references on issues. // DeleteIssueStatusDef deletes a status definition and clears references on issues.
// Returns ErrStatusRequired if the status is marked as required.
func DeleteIssueStatusDef(ctx context.Context, id int64) error { func DeleteIssueStatusDef(ctx context.Context, id int64) error {
def, err := GetIssueStatusDefByID(ctx, id)
if err != nil {
return err
}
if def.IsRequired {
return ErrStatusRequired{ID: def.ID, Name: def.Name}
}
// Clear status_id on all issues that reference this definition // Clear status_id on all issues that reference this definition
if _, err := db.GetEngine(ctx).Exec("UPDATE issue SET status_id = 0 WHERE status_id = ?", id); err != nil { if _, err := db.GetEngine(ctx).Exec("UPDATE issue SET status_id = 0 WHERE status_id = ?", id); err != nil {
return err return err
} }
_, err := db.GetEngine(ctx).ID(id).Delete(new(IssueStatusDef)) _, err = db.GetEngine(ctx).ID(id).Delete(new(IssueStatusDef))
return err return err
} }
+1
View File
@@ -436,6 +436,7 @@ func prepareMigrationTasks() []*migration {
newMigration(356, "Rename package_type to extension_type in repo manifest", v1_27.RenamePackageTypeToExtensionType), newMigration(356, "Rename package_type to extension_type in repo manifest", v1_27.RenamePackageTypeToExtensionType),
newMigration(357, "Drop display_name from repo manifest and update stream config", v1_27.DropDisplayNameColumns), newMigration(357, "Drop display_name from repo manifest and update stream config", v1_27.DropDisplayNameColumns),
newMigration(358, "Add licensing tables (license, entitlement, activation, product_tier)", v1_27.AddLicensingTables), newMigration(358, "Add licensing tables (license, entitlement, activation, product_tier)", v1_27.AddLicensingTables),
newMigration(359, "Add deploy fields to repo manifest", v1_27.AddDeployFieldsToRepoManifest),
} }
return preparedMigrations return preparedMigrations
} }
+23
View File
@@ -0,0 +1,23 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import (
"xorm.io/xorm"
)
// AddDeployFieldsToRepoManifest adds deploy configuration columns to repo_manifest.
func AddDeployFieldsToRepoManifest(x *xorm.Engine) error {
type RepoManifest struct {
DeployHost string `xorm:"VARCHAR(255) 'deploy_host'"`
DeployPort string `xorm:"VARCHAR(10) 'deploy_port'"`
DeployUser string `xorm:"VARCHAR(100) 'deploy_user'"`
DeployPath string `xorm:"TEXT 'deploy_path'"`
DockerImage string `xorm:"VARCHAR(255) 'docker_image'"`
DockerRegistry string `xorm:"VARCHAR(255) 'docker_registry'"`
ContainerName string `xorm:"VARCHAR(100) 'container_name'"`
HealthURL string `xorm:"TEXT 'health_url'"`
}
return x.Sync(new(RepoManifest))
}
+10
View File
@@ -50,6 +50,16 @@ type RepoMetadata struct {
ExtensionType string `xorm:"VARCHAR(50) 'extension_type'"` // component, module, plugin, package, template, library, file ExtensionType string `xorm:"VARCHAR(50) 'extension_type'"` // component, module, plugin, package, template, library, file
EntryPoint string `xorm:"TEXT 'entry_point'"` // build entry point path EntryPoint string `xorm:"TEXT 'entry_point'"` // build entry point path
// deploy section
DeployHost string `xorm:"VARCHAR(255) 'deploy_host'"` // SSH host for deploy
DeployPort string `xorm:"VARCHAR(10) 'deploy_port'"` // SSH port (default 2918)
DeployUser string `xorm:"VARCHAR(100) 'deploy_user'"` // SSH user
DeployPath string `xorm:"TEXT 'deploy_path'"` // remote path for source/compose
DockerImage string `xorm:"VARCHAR(255) 'docker_image'"` // e.g. mokoconsulting/mokogitea
DockerRegistry string `xorm:"VARCHAR(255) 'docker_registry'"` // e.g. git.mokoconsulting.tech
ContainerName string `xorm:"VARCHAR(100) 'container_name'"` // Docker container name
HealthURL string `xorm:"TEXT 'health_url'"` // health check URL after deploy
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"` UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"`
} }
+1
View File
@@ -165,6 +165,7 @@ type IssueStatusDef struct {
Color string `json:"color"` Color string `json:"color"`
Description string `json:"description"` Description string `json:"description"`
ClosesIssue bool `json:"closes_issue"` ClosesIssue bool `json:"closes_issue"`
IsRequired bool `json:"is_required"`
SortOrder int `json:"sort_order"` SortOrder int `json:"sort_order"`
} }
+5 -6
View File
@@ -1314,6 +1314,7 @@ func Routes() *web.Router {
m.Get("/revisions/*", repo.ListPageRevisions) m.Get("/revisions/*", repo.ListPageRevisions)
m.Post("/new", reqToken(), mustNotBeArchived, reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.NewWikiPage) m.Post("/new", reqToken(), mustNotBeArchived, reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.NewWikiPage)
m.Get("/pages", repo.ListWikiPages) m.Get("/pages", repo.ListWikiPages)
m.Get("/search", repo.SearchWikiPages)
}, mustEnableWiki) }, mustEnableWiki)
m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup) m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup)
m.Post("/markdown", reqToken(), bind(api.MarkdownOption{}), misc.Markdown) m.Post("/markdown", reqToken(), bind(api.MarkdownOption{}), misc.Markdown)
@@ -1480,12 +1481,10 @@ func Routes() *web.Router {
Delete(reqToken(), repo.DeleteTopic) Delete(reqToken(), repo.DeleteTopic)
}, reqAdmin()) }, reqAdmin())
}, reqAnyRepoReader()) }, reqAnyRepoReader())
m.Combo("/metadata", reqRepoReader(unit.TypeCode)). m.Get("/metadata", repo.GetRepoMetadata)
Get(repo.GetRepoMetadata). m.Put("/metadata", reqToken(), reqAdmin(), repo.UpdateRepoMetadata)
Put(reqToken(), reqAdmin(), repo.UpdateRepoMetadata) m.Get("/manifest", repo.GetRepoMetadata) // backward compat
m.Combo("/manifest", reqRepoReader(unit.TypeCode)). // backward compat m.Put("/manifest", reqToken(), reqAdmin(), repo.UpdateRepoMetadata)
Get(repo.GetRepoMetadata).
Put(reqToken(), reqAdmin(), repo.UpdateRepoMetadata)
// MokoGitea badge engine // MokoGitea badge engine
m.Get("/badge/{type}.svg", repo.GetRepoBadge) m.Get("/badge/{type}.svg", repo.GetRepoBadge)
m.Get("/issue_templates", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(), repo.GetIssueTemplates) m.Get("/issue_templates", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(), repo.GetIssueTemplates)
+12 -3
View File
@@ -207,7 +207,10 @@ func UpdateLicense(ctx *context.APIContext) {
} }
if len(cols) > 0 { if len(cols) > 0 {
cols = append(cols, "updated_at") cols = append(cols, "updated_at")
db.GetEngine(ctx).ID(id).Cols(cols...).Update(license) if _, err := db.GetEngine(ctx).ID(id).Cols(cols...).Update(license); err != nil {
ctx.APIErrorInternal(err)
return
}
} }
ctx.JSON(http.StatusOK, licenseToJSON(ctx, license)) ctx.JSON(http.StatusOK, licenseToJSON(ctx, license))
@@ -399,7 +402,10 @@ func UpdateTier(ctx *context.APIContext) {
} }
if len(cols) > 0 { if len(cols) > 0 {
db.GetEngine(ctx).ID(id).Cols(cols...).Update(tier) if _, err := db.GetEngine(ctx).ID(id).Cols(cols...).Update(tier); err != nil {
ctx.APIErrorInternal(err)
return
}
} }
ctx.JSON(http.StatusOK, tierToJSON(tier)) ctx.JSON(http.StatusOK, tierToJSON(tier))
@@ -427,7 +433,10 @@ func DeleteTier(ctx *context.APIContext) {
return return
} }
db.GetEngine(ctx).ID(id).Delete(new(licensing_model.ProductTier)) if _, err := db.GetEngine(ctx).ID(id).Delete(new(licensing_model.ProductTier)); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent) ctx.Status(http.StatusNoContent)
} }
+26
View File
@@ -11,6 +11,19 @@ import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
) )
// checkOrgVisibility returns true if the current user can view org metadata.
// Public orgs are visible to everyone. Private/limited orgs require authentication.
func checkOrgVisibility(ctx *context.APIContext) bool {
if ctx.Org.Organization.Visibility == api.VisibleTypePublic {
return true
}
if ctx.Doer == nil {
ctx.APIErrorNotFound()
return false
}
return true
}
// ListIssueStatuses returns active issue status definitions for an org. // ListIssueStatuses returns active issue status definitions for an org.
func ListIssueStatuses(ctx *context.APIContext) { func ListIssueStatuses(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/issue-statuses organization orgListIssueStatuses // swagger:operation GET /orgs/{org}/issue-statuses organization orgListIssueStatuses
@@ -34,6 +47,10 @@ func ListIssueStatuses(ctx *context.APIContext) {
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
if !checkOrgVisibility(ctx) {
return
}
defs, err := issues_model.GetIssueStatusDefsByOrg(ctx, ctx.Org.Organization.ID) defs, err := issues_model.GetIssueStatusDefsByOrg(ctx, ctx.Org.Organization.ID)
if err != nil { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
@@ -47,6 +64,7 @@ func ListIssueStatuses(ctx *context.APIContext) {
Color: d.Color, Color: d.Color,
Description: d.Description, Description: d.Description,
ClosesIssue: d.ClosesIssue, ClosesIssue: d.ClosesIssue,
IsRequired: d.IsRequired,
SortOrder: d.SortOrder, SortOrder: d.SortOrder,
}) })
} }
@@ -76,6 +94,10 @@ func ListIssuePriorities(ctx *context.APIContext) {
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
if !checkOrgVisibility(ctx) {
return
}
defs, err := issues_model.GetIssuePriorityDefsByOrg(ctx, ctx.Org.Organization.ID) defs, err := issues_model.GetIssuePriorityDefsByOrg(ctx, ctx.Org.Organization.ID)
if err != nil { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
@@ -118,6 +140,10 @@ func ListIssueTypes(ctx *context.APIContext) {
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
if !checkOrgVisibility(ctx) {
return
}
defs, err := issues_model.GetIssueTypeDefsByOrg(ctx, ctx.Org.Organization.ID) defs, err := issues_model.GetIssueTypeDefsByOrg(ctx, ctx.Org.Organization.ID)
if err != nil { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
+73 -23
View File
@@ -32,6 +32,16 @@ type apiMetadata struct {
Language string `json:"language"` Language string `json:"language"`
ExtensionType string `json:"extension_type"` ExtensionType string `json:"extension_type"`
EntryPoint string `json:"entry_point"` EntryPoint string `json:"entry_point"`
// deploy
DeployHost string `json:"deploy_host,omitempty"`
DeployPort string `json:"deploy_port,omitempty"`
DeployUser string `json:"deploy_user,omitempty"`
DeployPath string `json:"deploy_path,omitempty"`
DockerImage string `json:"docker_image,omitempty"`
DockerRegistry string `json:"docker_registry,omitempty"`
ContainerName string `json:"container_name,omitempty"`
HealthURL string `json:"health_url,omitempty"`
} }
// GetRepoMetadata returns the manifest settings for a repository. // GetRepoMetadata returns the manifest settings for a repository.
@@ -81,6 +91,14 @@ func GetRepoMetadata(ctx *context.APIContext) {
Language: m.Language, Language: m.Language,
ExtensionType: m.ExtensionType, ExtensionType: m.ExtensionType,
EntryPoint: m.EntryPoint, EntryPoint: m.EntryPoint,
DeployHost: m.DeployHost,
DeployPort: m.DeployPort,
DeployUser: m.DeployUser,
DeployPath: m.DeployPath,
DockerImage: m.DockerImage,
DockerRegistry: m.DockerRegistry,
ContainerName: m.ContainerName,
HealthURL: m.HealthURL,
}) })
} }
@@ -96,35 +114,59 @@ func UpdateRepoMetadata(ctx *context.APIContext) {
// responses: // responses:
// "200": // "200":
// "$ref": "#/responses/Manifest" // "$ref": "#/responses/Manifest"
var req apiMetadata // Decode into a map to detect which fields were actually sent.
if err := json.NewDecoder(ctx.Req.Body).Decode(&req); err != nil { var raw map[string]any
if err := json.NewDecoder(ctx.Req.Body).Decode(&raw); err != nil {
ctx.APIError(http.StatusBadRequest, err) ctx.APIError(http.StatusBadRequest, err)
return return
} }
m := &repo_model.RepoMetadata{ // Load existing metadata (or create defaults).
RepoID: ctx.Repo.Repository.ID, m, _ := repo_model.GetRepoMetadata(ctx, ctx.Repo.Repository.ID)
Name: req.Name, if m == nil {
Org: req.Org, m = &repo_model.RepoMetadata{
Description: req.Description, RepoID: ctx.Repo.Repository.ID,
Name: ctx.Repo.Repository.Name,
LicenseSPDX: req.LicenseSPDX, Org: ctx.Repo.Repository.OwnerName,
LicenseName: req.LicenseName, Description: ctx.Repo.Repository.Description,
VersionPrefix: req.VersionPrefix, }
ElementName: req.ElementName,
Platform: req.Platform,
StandardsVersion: req.StandardsVersion,
StandardsSource: req.StandardsSource,
Maintainer: req.Maintainer,
MaintainerURL: req.MaintainerURL,
InfoURL: req.InfoURL,
TargetVersion: req.TargetVersion,
PHPMinimum: req.PHPMinimum,
Language: req.Language,
ExtensionType: req.ExtensionType,
EntryPoint: req.EntryPoint,
} }
// Apply only the fields present in the request.
setStr := func(key string, target *string) {
if v, ok := raw[key]; ok {
if s, ok := v.(string); ok {
*target = s
}
}
}
setStr("name", &m.Name)
setStr("org", &m.Org)
setStr("description", &m.Description)
setStr("license_spdx", &m.LicenseSPDX)
setStr("license_name", &m.LicenseName)
setStr("version_prefix", &m.VersionPrefix)
setStr("element_name", &m.ElementName)
setStr("platform", &m.Platform)
setStr("standards_version", &m.StandardsVersion)
setStr("standards_source", &m.StandardsSource)
setStr("maintainer", &m.Maintainer)
setStr("maintainer_url", &m.MaintainerURL)
setStr("info_url", &m.InfoURL)
setStr("target_version", &m.TargetVersion)
setStr("php_minimum", &m.PHPMinimum)
setStr("language", &m.Language)
setStr("extension_type", &m.ExtensionType)
setStr("entry_point", &m.EntryPoint)
setStr("deploy_host", &m.DeployHost)
setStr("deploy_port", &m.DeployPort)
setStr("deploy_user", &m.DeployUser)
setStr("deploy_path", &m.DeployPath)
setStr("docker_image", &m.DockerImage)
setStr("docker_registry", &m.DockerRegistry)
setStr("container_name", &m.ContainerName)
setStr("health_url", &m.HealthURL)
if err := repo_model.CreateOrUpdateRepoMetadata(ctx, m); err != nil { if err := repo_model.CreateOrUpdateRepoMetadata(ctx, m); err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
@@ -151,5 +193,13 @@ func UpdateRepoMetadata(ctx *context.APIContext) {
Language: m.Language, Language: m.Language,
ExtensionType: m.ExtensionType, ExtensionType: m.ExtensionType,
EntryPoint: m.EntryPoint, EntryPoint: m.EntryPoint,
DeployHost: m.DeployHost,
DeployPort: m.DeployPort,
DeployUser: m.DeployUser,
DeployPath: m.DeployPath,
DockerImage: m.DockerImage,
DockerRegistry: m.DockerRegistry,
ContainerName: m.ContainerName,
HealthURL: m.HealthURL,
}) })
} }
+140 -1
View File
@@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"strings"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
@@ -461,10 +462,148 @@ func ListPageRevisions(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, convert.ToWikiCommitList(commitsHistory, commitsCount)) ctx.JSON(http.StatusOK, convert.ToWikiCommitList(commitsHistory, commitsCount))
} }
// SearchWikiPages searches wiki page titles and content.
func SearchWikiPages(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/wiki/search repository repoSearchWikiPages
// ---
// summary: Search wiki pages
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: q
// in: query
// description: search query
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// description: "SearchResults"
// "404":
// "$ref": "#/responses/notFound"
query := strings.TrimSpace(ctx.FormString("q"))
if query == "" {
ctx.JSON(http.StatusOK, []interface{}{})
return
}
wikiRepo, commit := findWikiRepoCommit(ctx)
if wikiRepo != nil {
defer wikiRepo.Close()
}
if ctx.Written() {
return
}
queryLower := strings.ToLower(query)
type WikiSearchResult struct {
PageName string `json:"page_name"`
PageURL string `json:"page_url"`
Context string `json:"context,omitempty"`
}
entries, err := commit.ListEntriesRecursiveFast()
if err != nil {
ctx.APIErrorInternal(err)
return
}
var results []WikiSearchResult
for _, entry := range entries {
if !entry.IsRegular() || !strings.HasSuffix(entry.Name(), ".md") {
continue
}
baseName := strings.TrimSuffix(entry.Name(), ".md")
// Extract just the filename without path for special file check
parts := strings.Split(baseName, "/")
shortName := parts[len(parts)-1]
if shortName == "_Sidebar" || shortName == "_Footer" {
continue
}
blob := entry.Blob()
content, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize)
if err != nil {
continue
}
titleMatch := strings.Contains(strings.ToLower(baseName), queryLower)
contentMatch := strings.Contains(strings.ToLower(content), queryLower)
if !titleMatch && !contentMatch {
continue
}
contextLine := ""
if contentMatch {
for _, line := range strings.Split(content, "\n") {
if strings.Contains(strings.ToLower(line), queryLower) {
contextLine = strings.TrimSpace(line)
if len(contextLine) > 200 {
contextLine = contextLine[:200] + "..."
}
break
}
}
}
wikiName, err := wiki_service.GitPathToWebPath(entry.Name())
if err != nil {
continue
}
_, displayName := wiki_service.WebPathToUserTitle(wikiName)
results = append(results, WikiSearchResult{
PageName: displayName,
PageURL: string(wikiName),
Context: contextLine,
})
}
// Pagination
page := max(ctx.FormInt("page"), 1)
limit := ctx.FormInt("limit")
if limit <= 0 {
limit = setting.API.DefaultPagingNum
}
total := len(results)
start := (page - 1) * limit
end := start + limit
if start > total {
start = total
}
if end > total {
end = total
}
ctx.SetLinkHeader(int64(total), limit)
ctx.SetTotalCountHeader(int64(total))
ctx.JSON(http.StatusOK, results[start:end])
}
// findEntryForFile finds the tree entry for a target filepath. // findEntryForFile finds the tree entry for a target filepath.
func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) { func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) {
entry, err := commit.GetTreeEntryByPath(target) entry, err := commit.GetTreeEntryByPath(target)
if err != nil { if err != nil && !git.IsErrNotExist(err) {
return nil, err return nil, err
} }
if entry != nil { if entry != nil {
+5
View File
@@ -103,6 +103,11 @@ func SettingsIssueStatusesDeletePost(ctx *context.Context) {
} }
if err := issues_model.DeleteIssueStatusDef(ctx, id); err != nil { if err := issues_model.DeleteIssueStatusDef(ctx, id); err != nil {
if issues_model.IsErrStatusRequired(err) {
ctx.Flash.Error("Cannot delete required status: " + def.Name)
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-statuses")
return
}
ctx.ServerError("DeleteIssueStatusDef", err) ctx.ServerError("DeleteIssueStatusDef", err)
return return
} }
+73 -25
View File
@@ -29,6 +29,14 @@ type OrgWikiPage struct {
SubURL string SubURL string
} }
// OrgWikiTreeNode represents a node in the org wiki folder tree for sidebar navigation.
type OrgWikiTreeNode struct {
Name string
SubURL string
IsDir bool
Children []*OrgWikiTreeNode
}
// Wiki renders the org wiki tab. // Wiki renders the org wiki tab.
func Wiki(ctx *context.Context) { func Wiki(ctx *context.Context) {
org := ctx.Org.Organization org := ctx.Org.Organization
@@ -71,31 +79,9 @@ func Wiki(ctx *context.Context) {
} }
ctx.Data["WikiRepoLink"] = wikiRepo.Link() ctx.Data["WikiRepoLink"] = wikiRepo.Link()
// Build page list from repo root. // Build folder tree for sidebar navigation.
entries, err := commit.ListEntries() wikiTree := buildOrgWikiTree(commit)
if err != nil { ctx.Data["WikiTree"] = wikiTree
ctx.ServerError("ListEntries", err)
return
}
pages := make([]OrgWikiPage, 0, len(entries))
for _, entry := range entries {
if !entry.IsRegular() {
continue
}
name := entry.Name()
if !isMarkdownFile(name) {
continue
}
displayName := strings.TrimSuffix(name, path.Ext(name))
if strings.EqualFold(displayName, "_sidebar") || strings.EqualFold(displayName, "_footer") {
continue
}
pages = append(pages, OrgWikiPage{
Name: displayName,
SubURL: displayName,
})
}
ctx.Data["Pages"] = pages
// Determine which page to render. // Determine which page to render.
pageName := ctx.PathParamRaw("*") pageName := ctx.PathParamRaw("*")
@@ -157,6 +143,68 @@ func Wiki(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplOrgWiki) ctx.HTML(http.StatusOK, tplOrgWiki)
} }
// buildOrgWikiTree builds a hierarchical folder tree from the org wiki git repo.
// Shows up to 2 levels deep (folders and their immediate children).
func buildOrgWikiTree(commit *git.Commit) []*OrgWikiTreeNode {
if commit == nil {
return nil
}
entries, err := commit.ListEntries()
if err != nil {
return nil
}
var topLevel []*OrgWikiTreeNode
for _, entry := range entries {
name := entry.Name()
if entry.IsDir() {
node := &OrgWikiTreeNode{
Name: name,
SubURL: name,
IsDir: true,
}
// List children of this directory (1 level deep).
subTree := entry.Tree()
if subTree != nil {
children, _ := subTree.ListEntries()
for _, child := range children {
childName := child.Name()
if child.IsDir() {
node.Children = append(node.Children, &OrgWikiTreeNode{
Name: childName,
SubURL: name + "/" + childName,
IsDir: true,
})
} else if isMarkdownFile(childName) {
displayName := strings.TrimSuffix(childName, path.Ext(childName))
if strings.EqualFold(displayName, "_sidebar") || strings.EqualFold(displayName, "_footer") {
continue
}
node.Children = append(node.Children, &OrgWikiTreeNode{
Name: displayName,
SubURL: name + "/" + displayName,
IsDir: false,
})
}
}
}
topLevel = append(topLevel, node)
} else if isMarkdownFile(name) {
displayName := strings.TrimSuffix(name, path.Ext(name))
if strings.EqualFold(displayName, "_sidebar") || strings.EqualFold(displayName, "_footer") {
continue
}
topLevel = append(topLevel, &OrgWikiTreeNode{
Name: displayName,
SubURL: displayName,
IsDir: false,
})
}
}
return topLevel
}
// findOrgWikiCommit locates the profile repo's wiki and returns its HEAD commit. // findOrgWikiCommit locates the profile repo's wiki and returns its HEAD commit.
// The org wiki lives in the .wiki.git sidecar of the profile repo (e.g. .mokogitea.wiki.git). // The org wiki lives in the .wiki.git sidecar of the profile repo (e.g. .mokogitea.wiki.git).
// Tries fallback repo names (.profile, .github) if the primary doesn't exist. // Tries fallback repo names (.profile, .github) if the primary doesn't exist.
+8
View File
@@ -123,6 +123,14 @@ func saveMetadata(ctx *context.Context) {
manifest.Maintainer = existing.Maintainer manifest.Maintainer = existing.Maintainer
manifest.MaintainerURL = existing.MaintainerURL manifest.MaintainerURL = existing.MaintainerURL
manifest.Language = existing.Language manifest.Language = existing.Language
manifest.DeployHost = existing.DeployHost
manifest.DeployPort = existing.DeployPort
manifest.DeployUser = existing.DeployUser
manifest.DeployPath = existing.DeployPath
manifest.DockerImage = existing.DockerImage
manifest.DockerRegistry = existing.DockerRegistry
manifest.ContainerName = existing.ContainerName
manifest.HealthURL = existing.HealthURL
} }
if err := repo_model.CreateOrUpdateRepoMetadata(ctx, manifest); err != nil { if err := repo_model.CreateOrUpdateRepoMetadata(ctx, manifest); err != nil {
+1111 -66
View File
File diff suppressed because it is too large Load Diff
@@ -28,6 +28,7 @@
</td> </td>
<td> <td>
<strong>{{.Name}}</strong> <strong>{{.Name}}</strong>
{{if .IsRequired}}<span class="ui mini blue label" title="Required status - cannot be deleted">{{svg "octicon-lock" 10}} required</span>{{end}}
{{if not .IsActive}}<span class="ui mini grey label">{{ctx.Locale.Tr "org.settings.issue_status_inactive"}}</span>{{end}} {{if not .IsActive}}<span class="ui mini grey label">{{ctx.Locale.Tr "org.settings.issue_status_inactive"}}</span>{{end}}
{{if .Description}}<br><small class="text grey">{{.Description}}</small>{{end}} {{if .Description}}<br><small class="text grey">{{.Description}}</small>{{end}}
</td> </td>
@@ -40,10 +41,14 @@
</td> </td>
<td>{{.SortOrder}}</td> <td>{{.SortOrder}}</td>
<td class="tw-text-right"> <td class="tw-text-right">
{{if .IsRequired}}
<span class="ui tiny icon button disabled" title="Required - cannot be deleted">{{svg "octicon-lock" 14}}</span>
{{else}}
<form method="post" action="{{$.OrgLink}}/settings/issue-statuses/{{.ID}}/delete" class="tw-inline"> <form method="post" action="{{$.OrgLink}}/settings/issue-statuses/{{.ID}}/delete" class="tw-inline">
{{$.CsrfTokenHtml}} {{$.CsrfTokenHtml}}
<button class="ui tiny red icon button" type="submit" title="{{ctx.Locale.Tr "remove"}}">{{svg "octicon-trash" 14}}</button> <button class="ui tiny red icon button" type="submit" title="{{ctx.Locale.Tr "remove"}}">{{svg "octicon-trash" 14}}</button>
</form> </form>
{{end}}
</td> </td>
</tr> </tr>
{{end}} {{end}}
+37 -12
View File
@@ -11,7 +11,7 @@
This organization doesn't have a wiki yet. This organization doesn't have a wiki yet.
</div> </div>
<p class="tw-text-center"> <p class="tw-text-center">
Enable the wiki on the <code>.profile</code> (public) or <code>.profile-private</code> (members-only) Enable the wiki on the <code>.mokogitea</code> (public) or <code>.mokogitea-private</code> (members-only)
repository to get started. repository to get started.
</p> </p>
</div> </div>
@@ -47,34 +47,59 @@
<p>The page "{{.CurrentPage}}" does not exist in this wiki.</p> <p>The page "{{.CurrentPage}}" does not exist in this wiki.</p>
</div> </div>
</div> </div>
{{if .Pages}} {{if .WikiTree}}
<h4>Available pages:</h4> <h4>Available pages:</h4>
<ul> <ul>
{{range .Pages}} {{range .WikiTree}}
<li><a href="{{$.Org.HomeLink}}/-/wiki/{{.SubURL}}">{{.Name}}</a></li> {{if .IsDir}}
{{range .Children}}
<li><a href="{{$.Org.HomeLink}}/-/wiki/{{.SubURL}}">{{.Name}}</a></li>
{{end}}
{{else}}
<li><a href="{{$.Org.HomeLink}}/-/wiki/{{.SubURL}}">{{.Name}}</a></li>
{{end}}
{{end}} {{end}}
</ul> </ul>
{{end}} {{end}}
</div> </div>
{{else}} {{else}}
<div class="wiki-content-parts"> <div class="wiki-content-parts">
<div class="render-content markup wiki-content-main {{if or .WikiSidebarHTML .Pages}}with-sidebar{{end}}"> <div class="render-content markup wiki-content-main {{if or .WikiSidebarHTML .WikiTree}}with-sidebar{{end}}">
{{.WikiContent}} {{.WikiContent}}
</div> </div>
{{if or .WikiSidebarHTML .Pages}} {{if or .WikiSidebarHTML .WikiTree}}
<div class="render-content markup wiki-content-sidebar"> <div class="render-content markup wiki-content-sidebar">
{{if .WikiSidebarHTML}} {{if .WikiSidebarHTML}}
{{.WikiSidebarHTML}} {{.WikiSidebarHTML}}
<div class="ui divider"></div> {{else if .WikiTree}}
{{end}}
{{if .Pages}}
<strong>{{svg "octicon-list-unordered" 14}} Pages</strong> <strong>{{svg "octicon-list-unordered" 14}} Pages</strong>
<ul class="wiki-tree-list"> <ul class="wiki-tree-list">
{{range .Pages}} {{range .WikiTree}}
<li> <li>
{{svg "octicon-file" 14}} {{if .IsDir}}
<a href="{{$.Org.HomeLink}}/-/wiki/{{.SubURL}}" {{if eq $.CurrentPage .Name}}class="active"{{end}}>{{.Name}}</a> <details open>
<summary>{{svg "octicon-file-directory" 14}} <strong>{{.Name}}</strong></summary>
{{if .Children}}
<ul>
{{range .Children}}
<li>
{{if .IsDir}}
{{svg "octicon-file-directory" 14}}
<a href="{{$.Org.HomeLink}}/-/wiki/{{.SubURL}}"><strong>{{.Name}}</strong></a>
{{else}}
{{svg "octicon-file" 14}}
<a href="{{$.Org.HomeLink}}/-/wiki/{{.SubURL}}" {{if eq $.CurrentPage .Name}}class="active"{{end}}>{{.Name}}</a>
{{end}}
</li>
{{end}}
</ul>
{{end}}
</details>
{{else}}
{{svg "octicon-file" 14}}
<a href="{{$.Org.HomeLink}}/-/wiki/{{.SubURL}}" {{if eq $.CurrentPage .Name}}class="active"{{end}}>{{.Name}}</a>
{{end}}
</li> </li>
{{end}} {{end}}
</ul> </ul>
+1 -5
View File
@@ -21,11 +21,7 @@
<input name="org" value="{{.Manifest.Org}}" placeholder="Organization"> <input name="org" value="{{.Manifest.Org}}" placeholder="Organization">
</div> </div>
</div> </div>
<div class="four fields"> <div class="three fields">
<div class="field">
<label>Version</label>
<input name="version" value="{{.Manifest.Version}}" placeholder="e.g. 06.00.00">
</div>
<div class="field"> <div class="field">
<label>Version Prefix</label> <label>Version Prefix</label>
<input name="version_prefix" value="{{.Manifest.VersionPrefix}}" placeholder="e.g. v1.26.1-moko."> <input name="version_prefix" value="{{.Manifest.VersionPrefix}}" placeholder="e.g. v1.26.1-moko.">
+42
View File
@@ -0,0 +1,42 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content repository wiki">
{{template "repo/header" .}}
<div class="ui container">
<div class="repo-button-row">
<div class="tw-flex tw-items-center tw-gap-2">
<a class="ui small button" href="{{.RepoLink}}/wiki/{{.PageURL}}">
{{svg "octicon-arrow-left" 14}} Back to {{.title}}
</a>
</div>
</div>
<h2>{{svg "octicon-cross-reference" 20}} What links here: {{.title}}</h2>
{{if .Backlinks}}
<div class="ui relaxed divided list">
{{range .Backlinks}}
<div class="item">
<div class="content">
<a class="header" href="{{$.RepoLink}}/wiki/{{.PageURL}}">{{.PageName}}</a>
{{if .Context}}
<div class="description">
<code class="tw-text-sm">{{.Context}}</code>
</div>
{{end}}
</div>
</div>
{{end}}
</div>
<p class="tw-mt-4 text grey">{{.BacklinkCount}} {{if eq .BacklinkCount 1}}page{{else}}pages{{end}} linking here.</p>
{{else}}
<div class="ui placeholder segment">
<div class="ui icon header">
{{svg "octicon-unlink" 48}}
<br>
No pages link to "{{.title}}"
</div>
</div>
{{end}}
</div>
</div>
{{template "base/footer" .}}
+36
View File
@@ -0,0 +1,36 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content repository wiki">
{{template "repo/header" .}}
<div class="ui container">
<div class="repo-button-row tw-flex tw-items-center tw-gap-2 tw-mb-4">
<div class="tw-flex-1">
<a class="ui small button" href="{{.RepoLink}}/wiki/">
{{svg "octicon-arrow-left" 14}} Back to wiki
</a>
</div>
</div>
<h2>{{svg "octicon-tag" 20}} Category: {{.CategoryName}}</h2>
{{if .CategoryPages}}
<div class="ui relaxed divided list">
{{range .CategoryPages}}
<div class="item">
{{svg "octicon-file" 14}}
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}">{{.Name}}</a>
</div>
{{end}}
</div>
<p class="tw-mt-4 text grey">{{.CategoryCount}} {{if eq .CategoryCount 1}}page{{else}}pages{{end}} in this category.</p>
{{else}}
<div class="ui placeholder segment">
<div class="ui icon header">
{{svg "octicon-tag" 48}}
<br>
No pages in category "{{.CategoryName}}"
</div>
</div>
{{end}}
</div>
</div>
{{template "base/footer" .}}
+51
View File
@@ -0,0 +1,51 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content repository wiki">
{{template "repo/header" .}}
<div class="ui container">
<div class="repo-button-row tw-flex tw-items-center tw-gap-2 tw-mb-4">
<div class="tw-flex-1">
<a href="{{.RepoLink}}/wiki/{{.PageURL}}">{{svg "octicon-arrow-left" 14}} {{.title}}</a>
&middot;
<a href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_revision">Revision history</a>
</div>
</div>
<div class="ui segment">
<h3>{{svg "octicon-diff" 20}} Changes in <code>{{.CommitID}}</code></h3>
<p>
<strong>{{.CommitAuthor}}</strong> &mdash; {{.CommitMessage}}
<br>
<small class="text grey">{{DateUtils.TimeSince .CommitWhen}}</small>
</p>
{{if .IsNewPage}}
<div class="ui info message">New page created</div>
{{else if .IsDeletedPage}}
<div class="ui warning message">Page deleted</div>
{{else if not .HasDiff}}
<div class="ui info message">No content changes in this revision</div>
{{end}}
{{if .HasDiff}}
<div class="diff-file-box" style="overflow-x: auto;">
<table class="chroma" style="width: 100%; border-collapse: collapse; font-family: monospace; font-size: 13px;">
{{range .DiffLines}}
<tr class="{{if eq .Type "add"}}diff-line-add{{else if eq .Type "del"}}diff-line-del{{else}}diff-line-context{{end}}">
<td style="width: 40px; text-align: right; padding: 0 8px; color: #999; user-select: none; {{if eq .Type "add"}}background: #e6ffec;{{else if eq .Type "del"}}background: #ffebe9;{{else}}background: #f6f8fa;{{end}}">
{{if .OldNum}}{{.OldNum}}{{end}}
</td>
<td style="width: 40px; text-align: right; padding: 0 8px; color: #999; user-select: none; {{if eq .Type "add"}}background: #e6ffec;{{else if eq .Type "del"}}background: #ffebe9;{{else}}background: #f6f8fa;{{end}}">
{{if .NewNum}}{{.NewNum}}{{end}}
</td>
<td style="padding: 0 8px; white-space: pre-wrap; word-break: break-all; {{if eq .Type "add"}}background: #e6ffec;{{else if eq .Type "del"}}background: #ffebe9;{{else}}background: #fff;{{end}}">
{{if eq .Type "add"}}+{{else if eq .Type "del"}}-{{else}} {{end}} {{.Content}}
</td>
</tr>
{{end}}
</table>
</div>
{{end}}
</div>
</div>
</div>
{{template "base/footer" .}}
+40
View File
@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{.Title}}</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 40px 20px;
color: #333;
line-height: 1.6;
}
h1 { font-size: 2em; border-bottom: 1px solid #eee; padding-bottom: 0.3em; }
h2 { font-size: 1.5em; border-bottom: 1px solid #eee; padding-bottom: 0.3em; }
code { background: #f6f8fa; padding: 2px 6px; border-radius: 3px; font-size: 85%; }
pre { background: #f6f8fa; padding: 16px; border-radius: 6px; overflow-x: auto; }
pre code { background: none; padding: 0; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
th { background: #f6f8fa; }
img { max-width: 100%; }
blockquote { border-left: 4px solid #ddd; margin: 0; padding: 0 16px; color: #666; }
a { color: #0366d6; }
@media print {
body { padding: 0; }
a { color: inherit; text-decoration: none; }
}
</style>
</head>
<body>
<h1>{{.Title}}</h1>
{{.WikiContentHTML}}
<hr>
<p style="font-size: 12px; color: #999;">
Printed from wiki &middot; {{.Title}}
</p>
</body>
</html>
+72
View File
@@ -0,0 +1,72 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content repository wiki">
{{template "repo/header" .}}
<div class="ui container">
<div class="repo-button-row tw-flex tw-items-center tw-gap-2 tw-mb-4">
<div class="tw-flex-1">
<h2>{{svg "octicon-history" 20}} Recent changes</h2>
</div>
<a class="ui small button" href="{{.RepoLink}}/wiki/">
{{svg "octicon-arrow-left" 14}} Back to wiki
</a>
</div>
{{if .RecentChanges}}
<table class="ui compact table">
<thead>
<tr>
<th>Page</th>
<th>Author</th>
<th>Edit summary</th>
<th>When</th>
<th>Commit</th>
</tr>
</thead>
<tbody>
{{range .RecentChanges}}
<tr>
<td>
{{if .PageURL}}
{{svg "octicon-file" 14}}
<a href="{{$.RepoLink}}/wiki/{{.PageURL}}">{{.PageName}}</a>
{{else if .PageName}}
{{svg "octicon-file" 14}} {{.PageName}}
{{else}}
<span class="text grey">—</span>
{{end}}
</td>
<td>{{.Author}}</td>
<td class="gt-ellipsis" style="max-width: 400px;">{{.Message}}</td>
<td>{{DateUtils.TimeSince .When}}</td>
<td><code class="tw-text-xs">{{.SHA}}</code></td>
</tr>
{{end}}
</tbody>
</table>
<div class="tw-flex tw-justify-between tw-mt-4">
{{if .HasPrevPage}}
<a class="ui small button" href="{{.RepoLink}}/wiki/?action=_recent&page={{Eval .CurrentPage "-" 1}}">
{{svg "octicon-chevron-left" 14}} Newer
</a>
{{else}}
<span></span>
{{end}}
{{if .HasNextPage}}
<a class="ui small button" href="{{.RepoLink}}/wiki/?action=_recent&page={{Eval .CurrentPage "+" 1}}">
Older {{svg "octicon-chevron-right" 14}}
</a>
{{end}}
</div>
{{else}}
<div class="ui placeholder segment">
<div class="ui icon header">
{{svg "octicon-history" 48}}
<br>
No recent changes
</div>
</div>
{{end}}
</div>
</div>
{{template "base/footer" .}}
+52
View File
@@ -0,0 +1,52 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content repository wiki">
{{template "repo/header" .}}
<div class="ui container">
<div class="repo-button-row">
<div class="tw-flex tw-items-center tw-gap-2">
<a class="ui small button" href="{{.RepoLink}}/wiki/">
{{svg "octicon-arrow-left" 14}} Back to wiki
</a>
</div>
</div>
<h2>{{svg "octicon-search" 20}} Wiki search</h2>
<form class="ui form tw-mb-4" action="{{.RepoLink}}/wiki/" method="get">
<input type="hidden" name="action" value="_search">
<div class="ui action input tw-w-full">
<input type="text" name="q" value="{{.Query}}" placeholder="Search wiki pages..." autofocus>
<button class="ui primary button" type="submit">{{svg "octicon-search" 14}} Search</button>
</div>
</form>
{{if .Query}}
{{if .Results}}
<div class="ui relaxed divided list">
{{range .Results}}
<div class="item">
<div class="content">
<a class="header" href="{{$.RepoLink}}/wiki/{{.PageURL}}">{{.PageName}}</a>
{{if .Context}}
<div class="description">
<code class="tw-text-sm">{{.Context}}</code>
</div>
{{end}}
</div>
</div>
{{end}}
</div>
<p class="tw-mt-4 text grey">{{.ResultCount}} {{if eq .ResultCount 1}}result{{else}}results{{end}} for "{{.Query}}"</p>
{{else}}
<div class="ui placeholder segment">
<div class="ui icon header">
{{svg "octicon-search" 48}}
<br>
No results for "{{.Query}}"
</div>
</div>
{{end}}
{{end}}
</div>
</div>
{{template "base/footer" .}}
+33 -2
View File
@@ -20,6 +20,10 @@
</div> </div>
<div class="scrolling menu"> <div class="scrolling menu">
<a class="item muted" href="{{.RepoLink}}/wiki/?action=_pages">{{ctx.Locale.Tr "repo.wiki.pages"}}</a> <a class="item muted" href="{{.RepoLink}}/wiki/?action=_pages">{{ctx.Locale.Tr "repo.wiki.pages"}}</a>
<a class="item muted" href="{{.RepoLink}}/wiki/?action=_search">{{svg "octicon-search" 14}} Search wiki</a>
<a class="item muted" href="{{.RepoLink}}/wiki/?action=_recent">{{svg "octicon-history" 14}} Recent changes</a>
t <a class="item muted" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_print" target="_blank">{{svg "octicon-browser" 14}} Print view</a>
<a class="item muted" href="{{.RepoLink}}/wiki/?action=_export&format=zip">{{svg "octicon-download" 14}} Export wiki (ZIP)</a>
<div class="divider"></div> <div class="divider"></div>
{{range .Pages}} {{range .Pages}}
<a class="item {{if eq $.Title .Name}}selected{{end}}" href="{{$.RepoLink}}/wiki/{{.SubURL}}">{{.Name}}</a> <a class="item {{if eq $.Title .Name}}selected{{end}}" href="{{$.RepoLink}}/wiki/{{.SubURL}}">{{.Name}}</a>
@@ -34,6 +38,8 @@
<div class="flex-text-block tw-flex-wrap tw-justify-end"> <div class="flex-text-block tw-flex-wrap tw-justify-end">
<div class="flex-text-block tw-flex-1 tw-min-w-[300px]"> <div class="flex-text-block tw-flex-1 tw-min-w-[300px]">
<a class="ui basic button tw-px-3 tw-gap-3" title="{{ctx.Locale.Tr "repo.wiki.file_revision"}}" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_revision" >{{if .CommitCount}}<span>{{.CommitCount}}</span> {{end}}{{svg "octicon-history"}}</a> <a class="ui basic button tw-px-3 tw-gap-3" title="{{ctx.Locale.Tr "repo.wiki.file_revision"}}" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_revision" >{{if .CommitCount}}<span>{{.CommitCount}}</span> {{end}}{{svg "octicon-history"}}</a>
<a class="ui basic button tw-px-3" title="What links here" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_backlinks">{{svg "octicon-cross-reference"}}</a>
{{if .LastCommitID}}<a class="ui basic button tw-px-3" title="View last change" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_diff&commit={{.LastCommitID}}">{{svg "octicon-diff"}}</a>{{end}}
<div class="tw-flex-1 gt-ellipsis"> <div class="tw-flex-1 gt-ellipsis">
{{$title}} {{$title}}
<div class="ui sub header gt-ellipsis"> <div class="ui sub header gt-ellipsis">
@@ -47,7 +53,7 @@
<a class="ui small button unescape-button tw-hidden" data-unicode-content-selector=".wiki-content-parts">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</a> <a class="ui small button unescape-button tw-hidden" data-unicode-content-selector=".wiki-content-parts">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</a>
<a class="ui small button escape-button" data-unicode-content-selector=".wiki-content-parts">{{ctx.Locale.Tr "repo.escape_control_characters"}}</a> <a class="ui small button escape-button" data-unicode-content-selector=".wiki-content-parts">{{ctx.Locale.Tr "repo.escape_control_characters"}}</a>
{{end}} {{end}}
{{if and .CanWriteWiki (not .Repository.IsMirror)}} {{if and .CanWriteWiki (not .Repository.IsMirror) (not .WikiFolderProtected)}}
<a class="ui small button" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_edit">{{ctx.Locale.Tr "repo.wiki.edit_page_button"}}</a> <a class="ui small button" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_edit">{{ctx.Locale.Tr "repo.wiki.edit_page_button"}}</a>
<a class="ui small primary button" href="{{.RepoLink}}/wiki?action=_new">{{ctx.Locale.Tr "repo.wiki.new_page_button"}}</a> <a class="ui small primary button" href="{{.RepoLink}}/wiki?action=_new">{{ctx.Locale.Tr "repo.wiki.new_page_button"}}</a>
<a class="ui small red button link-action" href data-modal-confirm="#repo-wiki-delete-page-modal" data-url="{{.RepoLink}}/wiki/{{.PageURL}}?action=_delete">{{ctx.Locale.Tr "repo.wiki.delete_page_button"}}</a> <a class="ui small red button link-action" href data-modal-confirm="#repo-wiki-delete-page-modal" data-url="{{.RepoLink}}/wiki/{{.PageURL}}?action=_delete">{{ctx.Locale.Tr "repo.wiki.delete_page_button"}}</a>
@@ -69,6 +75,12 @@
{{end}} {{end}}
{{end}} {{end}}
{{if .WikiFolderProtected}}
<div class="ui warning message">
<p>{{svg "octicon-lock" 14}} This page is in a protected folder. Only users with the required role can edit it.</p>
</div>
{{end}}
{{if .FormatWarning}} {{if .FormatWarning}}
<div class="ui negative message"> <div class="ui negative message">
<p>{{.FormatWarning}}</p> <p>{{.FormatWarning}}</p>
@@ -103,13 +115,30 @@
<div class="wiki-content-parts"> <div class="wiki-content-parts">
{{if .WikiSidebarTocHTML}} {{if .WikiSidebarTocHTML}}
<div class="render-content markup wiki-content-sidebar wiki-content-toc"> <div class="render-content markup wiki-content-sidebar wiki-content-toc">
{{.WikiSidebarTocHTML}} <details open>
<summary><strong>{{svg "octicon-list-unordered" 14}} Contents</strong></summary>
{{.WikiSidebarTocHTML}}
</details>
</div> </div>
{{end}} {{end}}
<div class="render-content markup wiki-content-main {{if or .WikiSidebarTocHTML .WikiSidebarHTML .WikiTree}}with-sidebar{{end}}"> <div class="render-content markup wiki-content-main {{if or .WikiSidebarTocHTML .WikiSidebarHTML .WikiTree}}with-sidebar{{end}}">
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus}} {{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus}}
{{if .WikiInlineTocHTML}}
<details open class="wiki-toc-inline tw-mb-4">
<summary><strong>{{svg "octicon-list-unordered" 14}} Contents</strong></summary>
{{.WikiInlineTocHTML}}
</details>
{{end}}
{{.WikiContentHTML}} {{.WikiContentHTML}}
{{if .WikiCategories}}
<div class="tw-mt-4 tw-pt-2" style="border-top: 1px solid var(--color-secondary);">
{{svg "octicon-tag" 14}} Categories:
{{range .WikiCategories}}
<a class="ui small label" href="{{$.RepoLink}}/wiki/?action=_category&name={{.}}">{{.}}</a>
{{end}}
</div>
{{end}}
</div> </div>
{{if .WikiTree}} {{if .WikiTree}}
@@ -121,6 +150,7 @@
{{if .IsDir}} {{if .IsDir}}
{{svg "octicon-file-directory" 14}} {{svg "octicon-file-directory" 14}}
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}"><strong>{{.Name}}</strong></a> <a href="{{$.RepoLink}}/wiki/{{.SubURL}}"><strong>{{.Name}}</strong></a>
{{if .Protected}}{{svg "octicon-lock" 12}}{{end}}
{{if .Children}} {{if .Children}}
<ul> <ul>
{{range .Children}} {{range .Children}}
@@ -128,6 +158,7 @@
{{if .IsDir}} {{if .IsDir}}
{{svg "octicon-file-directory" 14}} {{svg "octicon-file-directory" 14}}
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}"><strong>{{.Name}}</strong></a> <a href="{{$.RepoLink}}/wiki/{{.SubURL}}"><strong>{{.Name}}</strong></a>
{{if .Protected}}{{svg "octicon-lock" 12}}{{end}}
{{else}} {{else}}
{{svg "octicon-file" 14}} {{svg "octicon-file" 14}}
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}" {{if eq $.PageURL .SubURL}}class="active"{{end}}>{{.Name}}</a> <a href="{{$.RepoLink}}/wiki/{{.SubURL}}" {{if eq $.PageURL .SubURL}}class="active"{{end}}>{{.Name}}</a>
+31
View File
@@ -86,3 +86,34 @@
max-width: unset; max-width: unset;
} }
} }
/* Wikilinks: red links for non-existent pages */
.wiki .wiki-link-new {
color: var(--color-red);
}
.wiki .wiki-link-new:hover {
color: var(--color-red);
text-decoration: underline;
}
/* Wiki inline ToC */
.wiki .wiki-toc-inline {
border: 1px solid var(--color-secondary);
border-radius: 4px;
padding: 8px 16px;
background: var(--color-box-body);
}
.wiki .wiki-toc-inline summary {
cursor: pointer;
user-select: none;
}
/* Sticky sidebar ToC */
.wiki .wiki-content-toc {
position: sticky;
top: 16px;
max-height: calc(100vh - 100px);
overflow-y: auto;
}