Compare commits

...

69 Commits

Author SHA1 Message Date
Jonathan Miller 7ed335e6c3 feat(api): add /metadata API route alongside /manifest for backward compat
Generic: Project CI / Tests (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Project CI / Lint & Validate (push) Successful in 24s
Deploy MokoGitea / deploy (push) Successful in 4m8s
2026-06-10 04:33:58 -05:00
Jonathan Miller 4ed6e0175d feat(settings): SPDX license dropdown with auto-name, clean up metadata
Generic: Project CI / Tests (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Project CI / Lint & Validate (push) Successful in 42s
Deploy MokoGitea / deploy (push) Successful in 5m50s
- License field is now a dropdown with common SPDX identifiers
- License name auto-derived from SPDX (spdxToName map)
- Preserve hidden fields (element_name, display_name, maintainer, etc.)
  when saving from the simplified UI
- Remove unused updateserver_model import from metadata handler
2026-06-10 04:08:12 -05:00
Jonathan Miller a86350eebb fix(settings): remove auto-derived fields from metadata UI
Generic: Project CI / Tests (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Project CI / Lint & Validate (push) Successful in 35s
Deploy MokoGitea / deploy (push) Successful in 4m22s
Remove fields that are auto-derived at runtime:
- Standards source/version (internal)
- Maintainer/maintainer URL (from org profile)
- Display name (auto: ExtensionType - RepoName)
- Element name full (auto: prefix_reponame_lowercase)
- Language (auto-detected by Gitea)

Rename "Package Type" to "Extension Type".
Build section only shown for Joomla platform.
2026-06-10 00:48:13 -05:00
Jonathan Miller 97ea4fc4d0 feat(migration): migrate update server metadata fields to repo manifest
Generic: Project CI / Tests (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 3s
Generic: Project CI / Lint & Validate (push) Successful in 27s
Deploy MokoGitea / deploy (push) Successful in 3m27s
Migration 355: copies extension metadata from update_stream_config to
repo_manifest where manifest fields are empty. Fields migrated:
display_name, extension_name→element_name, extension_type→package_type,
target_version, maintainer, maintainer_url, info_url, php_minimum, platform.
Only fills empty manifest fields (won't overwrite existing data).
2026-06-09 23:38:14 -05:00
Jonathan Miller faef50ec4d fix(settings): update server page shows only visibility and gating
Generic: Project CI / Tests (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 4s
Generic: Project CI / Lint & Validate (push) Successful in 38s
Deploy MokoGitea / deploy (push) Successful in 3m56s
2026-06-09 23:36:21 -05:00
Jonathan Miller 9b9e5ae964 feat(settings): separate update server page from metadata
Generic: Project CI / Tests (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Project CI / Lint & Validate (push) Successful in 30s
Deploy MokoGitea / deploy (push) Successful in 3m26s
- /settings/metadata: project identity + custom fields
- /settings/updateserver: enable, platform, visibility, gating, keys
- Update server nav link shown when LicensingEnabled
- Old /settings/licensing and /settings/manifest redirect
2026-06-09 23:34:26 -05:00
Jonathan Miller 82a48d69cc chore: remove old manifest files (consolidated into metadata)
Generic: Project CI / Tests (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 22s
Generic: Project CI / Lint & Validate (push) Successful in 31s
Deploy MokoGitea / deploy (push) Failing after 13m42s
2026-06-09 23:26:19 -05:00
Jonathan Miller 4e5b7c7e65 chore: remove .gitea directory — workflows live in .mokogitea
The .gitea/workflows/deploy.yml was added by mistake. All workflows
and issue templates live in .mokogitea/ per MokoGitea convention.
2026-06-09 23:26:00 -05:00
Jonathan Miller e1ca5cdfc4 feat(settings): consolidate manifest + custom fields into metadata page
Generic: Project CI / Tests (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Project CI / Lint & Validate (push) Successful in 33s
Deploy MokoGitea / deploy (push) Failing after 3m31s
- Merge project identity (manifest), update server config, and custom
  fields into single /settings/metadata page
- Three sections: Project Identity, Update Server, Custom Fields
- Old /settings/manifest and /settings/licensing redirect to /metadata
- Single nav link replaces two separate entries
2026-06-09 23:24:35 -05:00
Jonathan Miller 704d9d10be feat(issues): status/priority/type dropdowns on new issue form (#598)
Generic: Project CI / Tests (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Project CI / Lint & Validate (push) Successful in 28s
Deploy MokoGitea / deploy (push) Successful in 4m0s
- Add type, priority, status dropdowns to new issue sidebar
- Pre-select org defaults (is_default for type/priority, first open status)
- Auto-assign defaults on API create when not provided
- Required indicators (*) on dropdowns and custom fields
- Validate required custom fields on submit (#597)
- Add feed_visibility to update server settings on manifest page
2026-06-09 23:19:19 -05:00
Jonathan Miller 549e890cd0 refactor: rename models/licenses to models/updateserver
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Project CI / Lint & Validate (push) Successful in 40s
Deploy MokoGitea / deploy (push) Successful in 5m29s
Generic: Project CI / Tests (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Rename the licensing/update server model package to better reflect
its purpose. All imports updated from licenses_model to updateserver_model.
License key management UI files kept (only imports updated).
2026-06-09 20:29:03 -05:00
Jonathan Miller 857c51e030 feat(settings): consolidate update server settings into manifest page
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Project CI / Lint & Validate (push) Successful in 33s
Deploy MokoGitea / deploy (push) Successful in 4m32s
Generic: Project CI / Tests (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
- Move licensing/update server form into manifest settings page
- Remove separate licensing nav link (redirect old URL for compat)
- Extension metadata fields already in manifest section (no duplication)
- Closes #582
2026-06-09 19:35:47 -05:00
Jonathan Miller d8c4f1efaf fix(ci): use git.mokoconsulting.tech instead of code.mokoconsulting.tech
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Project CI / Lint & Validate (push) Successful in 31s
Deploy MokoGitea / deploy (push) Successful in 4m27s
Generic: Project CI / Tests (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-09 19:15:10 -05:00
Jonathan Miller d9cdaacb77 fix(ci): rename all GA_TOKEN/GITEA_TOKEN refs to MOKOGITEA_TOKEN
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Project CI / Lint & Validate (push) Successful in 49s
Deploy MokoGitea / deploy (push) Failing after 4m26s
Generic: Project CI / Tests (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-09 18:38:48 -05:00
Jonathan Miller 4a4873c733 fix(ci): use MOKOGITEA_TOKEN instead of deleted GITEA_TOKEN
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Project CI / Lint & Validate (push) Successful in 32s
Deploy MokoGitea / deploy (push) Failing after 4m35s
Generic: Project CI / Tests (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-09 18:28:38 -05:00
Jonathan Miller 633980bd05 ci: add auto-deploy workflow on push to main
Builds Docker image from Dockerfile.rootless, deploys to git server,
runs health check with automatic rollback on failure. Replaces manual
docker build/restart process.

Runs on the release runner with Docker-in-Docker access.
2026-06-09 18:17:37 -05:00
Jonathan Miller 4bd6be7935 fix(ci): use single SSH session for deploy, add wiki debug logging
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Project CI / Lint & Validate (push) Successful in 32s
Deploy MokoGitea / deploy (push) Failing after 5m0s
Generic: Project CI / Tests (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Consolidate all SSH commands into one session via heredoc to avoid
connection refused errors from SSH rate limiting on repeated connects.
Add error logging to OrgWikiRepoExists to debug missing wiki tab.
2026-06-09 16:35:04 -05:00
Jonathan Miller cefff4878c chore: remove upstream Gitea CI workflows
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Project CI / Lint & Validate (push) Successful in 42s
Deploy MokoGitea / deploy (push) Failing after 58s
Generic: Project CI / Tests (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
These workflows target upstream Gitea infrastructure (Docker Hub,
GHCR, AWS S3, namespace runners) that MokoGitea doesn't use.
MokoGitea deploy will use its own workflow.
2026-06-09 16:23:36 -05:00
Jonathan Miller 47ac97d284 debug: add error logging to OrgWikiRepoExists
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 3s
Generic: Project CI / Lint & Validate (push) Successful in 43s
Deploy MokoGitea / deploy (push) Failing after 45s
Generic: Project CI / Tests (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-09 16:16:51 -05:00
Jonathan Miller be6f3091af fix: switch org wiki tab to .profile wiki sidecars (#595)
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Project CI / Lint & Validate (push) Successful in 35s
Deploy MokoGitea / deploy (push) Failing after 1m16s
Generic: Project CI / Tests (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Fixes #594
2026-06-09 15:05:05 -05:00
Jonathan Miller aeb36e4312 feat: use manifest API as source of truth for update feed metadata (#592)
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 3s
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Replace custom field + config table cascade with manifest-only read
for extension identity fields (element_name, package_type, display_name,
target_version, php_minimum, description). Config table retained only
for licensing fields (download_gating, key_prefix, support_url fallback).

Fix client field: package/component/library/file → administrator.

Remove issues_model import (custom field lookups removed).
2026-06-09 14:32:39 -05:00
Jonathan Miller 2135f4c37c fix(ci): use echo instead of printf for deploy key write
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Project CI / Lint & Validate (push) Successful in 38s
Deploy MokoGitea / deploy (push) Failing after 42s
Generic: Project CI / Tests (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-09 14:20:20 -05:00
Jonathan Miller d7bc3c3879 fix(ci): rewrite deploy workflow to fix YAML expression parsing
Generic: Project CI / Lint & Validate (push) Waiting to run
Generic: Project CI / Tests (push) Blocked by required conditions
Deploy MokoGitea / deploy (push) Waiting to run
Generic: Repo Health / Access control (push) Waiting to run
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Waiting to run
Generic: Repo Health / Report Issues (push) Blocked by required conditions
- Escape Go template {{ }} in docker inspect with Gitea expression syntax
- Remove Python heredoc updates.xml step (had unescapable { in f-strings)
- Remove maintenance mode steps
- Use $VAR instead of ${VAR} to avoid brace conflicts
2026-06-09 12:43:27 -05:00
Jonathan Miller e7e2c5f7a2 fix(ci): remove maintenance mode steps blocking deploy
Generic: Project CI / Tests (push) Blocked by required conditions
deploy-mokogitea.yml / deploy (push) Waiting to run
Generic: Repo Health / Access control (push) Waiting to run
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Waiting to run
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Project CI / Lint & Validate (push) Successful in 28s
2026-06-09 12:35:06 -05:00
Jonathan Miller 45a0338fc3 fix(ci): URL-encode JSON braces in maintenance mode data
Generic: Project CI / Tests (push) Blocked by required conditions
deploy-mokogitea.yml / deploy (push) Waiting to run
Generic: Repo Health / Access control (push) Waiting to run
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Waiting to run
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Project CI / Lint & Validate (push) Successful in 28s
2026-06-09 12:31:59 -05:00
Jonathan Miller ce5f3570fb fix(ci): move JSON braces to env vars to avoid YAML parse error
Generic: Project CI / Tests (push) Blocked by required conditions
deploy-mokogitea.yml / deploy (push) Waiting to run
Generic: Repo Health / Access control (push) Waiting to run
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Waiting to run
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Project CI / Lint & Validate (push) Successful in 27s
2026-06-09 12:28:57 -05:00
Jonathan Miller 5acf10f766 fix(ci): escape braces in maintenance mode curl data
Generic: Project CI / Lint & Validate (push) Waiting to run
Generic: Project CI / Tests (push) Blocked by required conditions
deploy-mokogitea.yml / deploy (push) Waiting to run
Generic: Repo Health / Access control (push) Waiting to run
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Waiting to run
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Gitea Actions expression parser chokes on literal { in run blocks.
Use double-quoted strings with escaped quotes instead.
2026-06-09 12:22:12 -05:00
Jonathan Miller e7fd70e0f2 fix(ci): fix YAML parse error in deploy key step
Generic: Project CI / Tests (push) Blocked by required conditions
deploy-mokogitea.yml / deploy (push) Waiting to run
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Project CI / Lint & Validate (push) Successful in 35s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Replace heredoc (KEYEOF) with printf - heredoc terminator
was indented which broke YAML parsing and blocked all runners.
2026-06-09 11:54:42 -05:00
Jonathan Miller 34b1ef6638 fix(ci): prevent deploy key leak in logs
Generic: Project CI / Tests (push) Blocked by required conditions
deploy-mokogitea.yml / deploy (push) Waiting to run
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Project CI / Lint & Validate (push) Successful in 29s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Move SSH key write to separate step to avoid env: logging
the private key in CI output. Key has been rotated.
2026-06-09 11:07:03 -05:00
Jonathan Miller ce05f9f3c6 fix(ci): add docker login before registry push, remove MCP submodule
Generic: Project CI / Tests (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Project CI / Lint & Validate (push) Successful in 40s
Deploy MokoGitea / deploy (push) Failing after 45s
- Add docker login with GITEA_TOKEN before pushing to container registry
  (fixes unauthorized: reqPackageAccess deploy failure)
- Remove mcp-mokogitea-api submodule reference (now standalone repo)
2026-06-09 10:50:35 -05:00
gitea-actions[bot] 6f9d7ca03a chore(release): build 06.14.00 [skip ci] 2026-06-09 15:25:21 +00:00
jmiller 1c7d43df38 Merge pull request 'feat: issue metadata API + org wiki tab' (#590) from chore/mcp-cleanup into main
Generic: Project CI / Tests (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Project CI / Lint & Validate (push) Successful in 30s
Deploy MokoGitea / deploy (push) Failing after 1m12s
2026-06-09 15:24:17 +00:00
Jonathan Miller b2b31f6c7b fix(settings): validate wiki mode and URL scheme
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Generic: Project CI / Lint & Validate (pull_request) Successful in 27s
PR RC Release / Build RC Release (pull_request) Failing after 47s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 48s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 2m58s
- Reject wiki_mode values other than "" or "external"
- Validate wiki_url is http/https to prevent javascript: URI XSS
- Resolve CHANGELOG merge conflict
2026-06-09 10:22:59 -05:00
jmiller 5ba1d0b2e5 Merge pull request 'feat: issue metadata API — first-class status, priority, type fields' (#591) from dev into main
Generic: Project CI / Tests (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Deploy MokoGitea / deploy (push) Failing after 25s
Generic: Project CI / Lint & Validate (push) Successful in 26s
2026-06-09 15:22:13 +00:00
Jonathan Miller 1caf26453f feat: issue metadata API + org wiki tab with internal/external mode
Generic: Project CI / Tests (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 12s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 41s
Generic: Project CI / Lint & Validate (pull_request) Successful in 43s
Generic: Project CI / Lint & Validate (push) Successful in 45s
PR RC Release / Build RC Release (pull_request) Failing after 39s
Universal: Build & Release / Build & Release Pipeline (pull_request) Failing after 31s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Issue Status/Priority/Type API:
- Expose status_id, priority_id, type_id (with resolved names) on Issue API struct
- New endpoints: GET /orgs/{org}/issue-statuses, /issue-priorities, /issue-types
- CreateIssue and EditIssue handlers accept status_id, priority_id, type_id
- MCP tools: 5 new tools + updated create/update with metadata params

Org Wiki Tab:
- Convention repos: wiki (public) and wiki-private (members-only)
- Inline wiki rendering with markdown pipeline, sidebar, footer, page list
- Public/private view dropdown (same UX as org profile README selector)
- External wiki mode: link to outside URL from wiki tab
- Wiki mode setting in org settings (internal vs external with URL field)
- Migration 354: add wiki_mode and wiki_url to user table
2026-06-09 10:20:54 -05:00
Jonathan Miller 1e90900f69 chore: update mcp-mokogitea-api submodule with CI fixes 2026-06-08 05:53:13 -05:00
Jonathan Miller b762c94a25 feat: issue metadata API + org wiki tab with internal/external mode
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: Build & Release / Promote to RC (pull_request) Failing after 6s
Generic: Project CI / Lint & Validate (pull_request) Successful in 27s
PR RC Release / Build RC Release (pull_request) Failing after 36s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 37s
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Project CI / Tests (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
Issue Status/Priority/Type API:
- Expose status_id, priority_id, type_id (with resolved names) on Issue API struct
- New endpoints: GET /orgs/{org}/issue-statuses, /issue-priorities, /issue-types
- CreateIssue and EditIssue handlers accept status_id, priority_id, type_id
- MCP tools: 5 new tools + updated create/update with metadata params

Org Wiki Tab:
- Convention repos: wiki (public) and wiki-private (members-only)
- Inline wiki rendering with markdown pipeline, sidebar, footer, page list
- Public/private view dropdown (same UX as org profile README selector)
- External wiki mode: link to outside URL from wiki tab
- Wiki mode setting in org settings (internal vs external with URL field)
- Migration 354: add wiki_mode and wiki_url to user table
2026-06-08 05:21:45 -05:00
Jonathan Miller 6070f7dbd4 chore: update mcp-mokogitea-api submodule with CI fixes
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 3s
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-07 14:47:16 -05:00
jmiller 082c550bc4 Merge pull request 'release: remove duplicate MCP, update submodule with manifest tools' (#588) from dev into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Publish MCP to npm / publish (push) Failing after 10s
Generic: Project CI / Lint & Validate (push) Successful in 25s
Deploy MokoGitea / deploy (push) Failing after 4m13s
Generic: Project CI / Tests (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-07 19:34:01 +00:00
jmiller b1da31420f Merge pull request 'chore: remove duplicate .mokogitea/mcp, update mcp submodule with manifest tools' (#587) from chore/mcp-cleanup into dev
Generic: Project CI / Lint & Validate (push) Successful in 29s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 3s
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Generic: Repo Health / Access control (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Generic: Project CI / Lint & Validate (pull_request) Successful in 28s
Universal: Build & Release / Promote to RC (pull_request) Successful in 35s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 34s
PR RC Release / Build RC Release (pull_request) Failing after 34s
Universal: Build & Release / Build & Release Pipeline (pull_request) Failing after 32s
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Project CI / Tests (push) Has been cancelled
Generic: Project CI / Tests (pull_request) Has been cancelled
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
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-07 19:31:27 +00:00
Jonathan Miller 30d7ddc375 chore: remove duplicate .mokogitea/mcp, update mcp-mokogitea-api submodule
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Generic: Project CI / Lint & Validate (pull_request) Successful in 30s
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Project CI / Tests (pull_request) Has been cancelled
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
- Remove .mokogitea/mcp/ which was a duplicate of the mcp-mokogitea-api submodule
- Update submodule to include manifest get/update tools with all distribution fields
2026-06-07 14:30:35 -05:00
jmiller 8fa56271de Merge pull request 'release: template duplicate fix' (#586) from dev into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 3s
Generic: Project CI / Lint & Validate (push) Successful in 26s
Deploy MokoGitea / deploy (push) Failing after 3m21s
Generic: Project CI / Tests (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-07 18:55:43 +00:00
jmiller 8535928a04 Merge pull request 'fix(manifest): remove duplicate element name block in template' (#585) from fix/manifest-template-duplicate into dev
Generic: Project CI / Lint & Validate (push) Successful in 26s
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Generic: Repo Health / Site Health (push) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Generic: Repo Health / Access control (push) 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
Universal: Build & Release / Promote to RC (pull_request) Successful in 32s
Generic: Project CI / Lint & Validate (pull_request) Successful in 35s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 43s
Universal: Build & Release / Build & Release Pipeline (pull_request) Failing after 32s
PR RC Release / Build RC Release (pull_request) Failing after 53s
Generic: Project CI / Tests (push) Has been cancelled
Generic: Project CI / Tests (pull_request) Has been cancelled
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
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-07 18:53:26 +00:00
Jonathan Miller d0853b874f fix(manifest): remove duplicate element name block in template
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Generic: Project CI / Lint & Validate (pull_request) Successful in 28s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Failing after 1s
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Project CI / Tests (pull_request) Has been cancelled
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-07 13:47:07 -05:00
gitea-actions[bot] 0fe1d769ea chore(release): build 06.13.00 [skip ci] 2026-06-07 18:39:00 +00:00
jmiller 18372c84a7 Merge pull request 'release: manifest distribution fields + update server fix' (#584) from dev into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Project CI / Lint & Validate (push) Successful in 27s
Deploy MokoGitea / deploy (push) Failing after 3m42s
Generic: Project CI / Tests (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-07 18:38:10 +00:00
Jonathan Miller c26ad626bd feat(manifest): add distribution metadata fields (phase 1 consolidation)
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 11s
Universal: PR Check / Validate PR (pull_request) Failing after 11s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 30s
PR RC Release / Build RC Release (pull_request) Failing after 29s
Generic: Project CI / Lint & Validate (pull_request) Successful in 37s
Generic: Project CI / Lint & Validate (push) Successful in 1m11s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 2m14s
Generic: Project CI / Tests (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Project CI / Tests (pull_request) Has been cancelled
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
Add DisplayName, Maintainer, MaintainerURL, InfoURL, TargetVersion, and
PHPMinimum to RepoManifest. These fields were previously only in
UpdateStreamConfig and are now in the manifest as the single source of
truth. Distribution section shown conditionally for joomla/wordpress/
dolibarr platforms. Closes #582 phase 1.
2026-06-07 13:36:58 -05:00
gitea-actions[bot] 7a5c2d146f chore(version): pre-release bump to 06.12.03-dev [skip ci] 2026-06-07 13:36:57 -05:00
jmiller d85ee80a4e Merge pull request 'release: fix update server disable bug' (#579) from dev into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Deploy MokoGitea / deploy (push) Failing after 35s
Generic: Project CI / Lint & Validate (push) Successful in 39s
Generic: Project CI / Tests (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-07 18:14:57 +00:00
jmiller 3ae1265fb6 chore: sync security-audit.yml from Template-Go [skip ci] 2026-06-07 17:58:04 +00:00
jmiller 6a23c3e90d chore: sync repo-health.yml from Template-Go [skip ci] 2026-06-07 17:57:57 +00:00
jmiller 5f327f9da7 chore: sync pr-check.yml from Template-Go [skip ci] 2026-06-07 17:57:50 +00:00
jmiller 2ba1a6795f chore: sync notify.yml from Template-Go [skip ci] 2026-06-07 17:57:44 +00:00
jmiller 15e1149eb5 chore: sync issue-branch.yml from Template-Go [skip ci] 2026-06-07 17:57:37 +00:00
jmiller ed91aa3392 chore: sync gitleaks.yml from Template-Go [skip ci] 2026-06-07 17:57:30 +00:00
jmiller 8b4ea10e02 chore: sync deploy-manual.yml from Template-Go [skip ci] 2026-06-07 17:57:23 +00:00
jmiller ad4bac1162 chore: sync cleanup.yml from Template-Go [skip ci] 2026-06-07 17:57:16 +00:00
jmiller 983ce46278 chore: sync ci-generic.yml from Template-Go [skip ci] 2026-06-07 17:57:09 +00:00
jmiller 7130c79317 chore: sync cascade-dev.yml from Template-Go [skip ci] 2026-06-07 17:57:02 +00:00
jmiller c3f1a5ab40 chore: sync branch-cleanup.yml from Template-Go [skip ci] 2026-06-07 17:56:56 +00:00
jmiller 0b2646880e chore: sync auto-release.yml from Template-Go [skip ci] 2026-06-07 17:56:49 +00:00
jmiller 49274afa40 chore: sync auto-bump.yml from Template-Go [skip ci] 2026-06-07 17:56:42 +00:00
jmiller f6578969e2 Merge pull request 'release: template fix for manifest settings' (#576) from dev into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Deploy MokoGitea / deploy (push) Failing after 22s
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-07 17:52:35 +00:00
jmiller 51a10782e6 ci: add platform_detect.php to pre-release workflow [skip ci] 2026-06-07 17:40:04 +00:00
jmiller 9a51bf23d4 Merge pull request 'release: manifest sync + element name + workflow rename' (#574) from dev into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Deploy MokoGitea / deploy (push) Failing after 31s
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-07 17:37:37 +00:00
jmiller ff9e7183d6 Merge pull request 'release: manifest version prefix + platform/language dropdowns' (#571) from dev into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Deploy MokoGitea / deploy (push) Failing after 37s
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-07 17:13:43 +00:00
jmiller 3361ce9f90 fix: correct auto-release.yml with patch detection and --bump none for fix branches [skip ci] 2026-06-07 17:01:10 +00:00
jmiller c44766106c Merge pull request 'release: v1.26.1-moko.06.12 - rename + changelog + Joomla fix' (#569) from dev into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Deploy MokoGitea / deploy (push) Failing after 4m9s
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-07 16:57:58 +00:00
jmiller 48c9759639 ci: trigger pre-release builds on fix/patch/hotfix/bugfix branches [skip ci] 2026-06-07 16:56:17 +00:00
107 changed files with 4005 additions and 7753 deletions
-73
View File
@@ -1,73 +0,0 @@
# Populates the go module, build, and golangci-lint caches under the default
# branch's cache scope so that PR runs have a warm fallback to restore from.
#
# GitHub Actions caches are scoped per ref: a PR run can only write to its own
# branch's scope, but can read from the base branch's scope as a fallback.
# PRs therefore cannot seed main's scope themselves. Running the same cache
# steps on push-to-main is the only opportunity to populate that fallback
# scope so fresh PR branches start with a useful cache on first run.
# A PR job's exact key lives in its own PR-scope (empty on first run, filled
# by later runs of the same PR); on miss, actions/cache's restore-keys fall
# back to prefix matches against entries this seeder saves in main's scope.
name: cache-seeder
on:
push:
branches:
- main
paths:
- "go.sum"
- ".golangci.yml"
- ".github/actions/go-cache/action.yml"
- ".github/workflows/cache-seeder.yml"
concurrency:
group: cache-seeder
cancel-in-progress: true
permissions:
contents: read
jobs:
gobuild:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: seed
- run: make deps-backend
- run: TAGS="bindata" make backend
- run: TAGS="bindata gogit" GOEXPERIMENT="" make backend
lint:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- { job: lint-backend, tags: "bindata", target: "lint-backend" }
- { job: lint-go-windows, tags: "bindata", target: "lint-go-windows" }
- { job: lint-go-gogit, tags: "bindata gogit", target: "lint-go" }
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: ${{ matrix.job }}
lint-cache: "true"
- run: make deps-backend deps-tools
- run: make ${{ matrix.target }}
env:
TAGS: ${{ matrix.tags }}
-31
View File
@@ -1,31 +0,0 @@
name: cron-licenses
on:
# schedule:
# - cron: "7 0 * * 1" # every Monday at 00:07 UTC
workflow_dispatch:
jobs:
cron-licenses:
runs-on: ubuntu-latest
if: github.repository == 'go-gitea/gitea'
permissions:
contents: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
- run: make generate-gitignore
timeout-minutes: 40
- name: push translations to repo
uses: appleboy/git-push-action@3b2c8661652360dbf1afe1b319a49dbb739c39f1 # v1.2.0
with:
author_email: "teabot@gitea.io"
author_name: GiteaBot
branch: main
commit: true
commit_message: "[skip ci] Updated licenses and gitignores"
remote: "git@github.com:go-gitea/gitea.git"
ssh_key: ${{ secrets.DEPLOY_KEY }}
-32
View File
@@ -1,32 +0,0 @@
name: cron-renovate
on:
schedule:
- cron: "23 * * * *" # hourly at :23
workflow_dispatch:
concurrency:
group: cron-renovate
env:
RENOVATE_VERSION: 43.141.5 # renovate: datasource=docker depName=ghcr.io/renovatebot/renovate
permissions:
contents: read
jobs:
cron-renovate:
runs-on: ubuntu-latest
if: github.repository == 'go-gitea/gitea' # prevent running on forks
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: renovatebot/github-action@f66d8679fcfcfa051abde6e7a623007173bf5164 # v46.1.12
with:
renovate-version: ${{ env.RENOVATE_VERSION }}
configurationFile: renovate.json5
token: ${{ secrets.RENOVATE_TOKEN }}
env:
RENOVATE_BINARY_SOURCE: install # auto-install go/node toolchains needed by post-upgrade tasks.
RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '["^make (tidy|svg nolyfill)$"]'
RENOVATE_REPOSITORIES: '["go-gitea/gitea"]'
-40
View File
@@ -1,40 +0,0 @@
name: cron-translations
on:
schedule:
- cron: "7 0 * * *" # every day at 00:07 UTC
workflow_dispatch:
jobs:
crowdin-pull:
runs-on: ubuntu-latest
if: github.repository == 'go-gitea/gitea'
permissions:
contents: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2
with:
upload_sources: true
upload_translations: false
download_sources: false
download_translations: true
push_translations: false
push_sources: false
create_pull_request: false
config: crowdin.yml
env:
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_KEY: ${{ secrets.CROWDIN_KEY }}
- name: update locales
run: ./build/update-locales.sh
- name: push translations to repo
uses: appleboy/git-push-action@3b2c8661652360dbf1afe1b319a49dbb739c39f1 # v1.2.0
with:
author_email: "teabot@gitea.io"
author_name: GiteaBot
branch: main
commit: true
commit_message: "[skip ci] Updated translations via Crowdin"
remote: "git@github.com:go-gitea/gitea.git"
ssh_key: ${{ secrets.DEPLOY_KEY }}
-125
View File
@@ -1,125 +0,0 @@
name: files-changed
on:
workflow_call:
outputs:
backend:
value: ${{ jobs.detect.outputs.backend }}
frontend:
value: ${{ jobs.detect.outputs.frontend }}
docs:
value: ${{ jobs.detect.outputs.docs }}
actions:
value: ${{ jobs.detect.outputs.actions }}
templates:
value: ${{ jobs.detect.outputs.templates }}
docker:
value: ${{ jobs.detect.outputs.docker }}
dockerfile:
value: ${{ jobs.detect.outputs.dockerfile }}
swagger:
value: ${{ jobs.detect.outputs.swagger }}
yaml:
value: ${{ jobs.detect.outputs.yaml }}
json:
value: ${{ jobs.detect.outputs.json }}
e2e:
value: ${{ jobs.detect.outputs.e2e }}
permissions:
contents: read
jobs:
detect:
runs-on: ubuntu-latest
timeout-minutes: 3
outputs:
backend: ${{ steps.changes.outputs.backend }}
frontend: ${{ steps.changes.outputs.frontend }}
docs: ${{ steps.changes.outputs.docs }}
actions: ${{ steps.changes.outputs.actions }}
templates: ${{ steps.changes.outputs.templates }}
docker: ${{ steps.changes.outputs.docker }}
dockerfile: ${{ steps.changes.outputs.dockerfile }}
swagger: ${{ steps.changes.outputs.swagger }}
yaml: ${{ steps.changes.outputs.yaml }}
json: ${{ steps.changes.outputs.json }}
e2e: ${{ steps.changes.outputs.e2e }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: changes
with:
filters: |
backend:
- "**/*.go"
- "templates/**/*.tmpl"
- "assets/emoji.json"
- "go.mod"
- "go.sum"
- "Makefile"
- ".golangci.yml"
- ".editorconfig"
- "options/locale/locale_en-US.json"
frontend:
- "*.js"
- "*.ts"
- "web_src/**"
- "tools/*.js"
- "tools/*.ts"
- "assets/emoji.json"
- "package.json"
- "pnpm-lock.yaml"
- "Makefile"
- ".eslintrc.cjs"
- ".npmrc"
docs:
- "**/*.md"
- ".markdownlint.yaml"
- "package.json"
- "pnpm-lock.yaml"
actions:
- ".github/workflows/*"
- "Makefile"
templates:
- "tools/lint-templates-*.js"
- "templates/**/*.tmpl"
- "pyproject.toml"
- "uv.lock"
docker:
- ".github/workflows/pull-docker-dryrun.yml"
- "Dockerfile"
- "Dockerfile.rootless"
- "docker/**"
- "Makefile"
dockerfile:
- "Dockerfile"
- "Dockerfile.rootless"
swagger:
- "templates/swagger/v1_json.tmpl"
- "templates/swagger/v1_input.json"
- "Makefile"
- "package.json"
- "pnpm-lock.yaml"
- ".spectral.yaml"
yaml:
- "**/*.yml"
- "**/*.yaml"
- ".yamllint.yaml"
- "pyproject.toml"
json:
- "**/*.json"
e2e:
- "tests/e2e/**"
- "tools/test-e2e.sh"
- "playwright.config.ts"
-178
View File
@@ -1,178 +0,0 @@
name: compliance
on:
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
files-changed:
uses: ./.github/workflows/files-changed.yml
lint-backend:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: lint-backend
lint-cache: "true"
- run: make deps-backend deps-tools
- run: make lint-backend
env:
TAGS: bindata
lint-on-demand:
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- run: make lint-spell
- if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true'
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
- if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true'
run: uv python install 3.14 && make deps-py lint-templates lint-yaml
- if: needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.swagger == 'true' || needs.files-changed.outputs.json == 'true'
run: make deps-frontend lint-md lint-swagger lint-json
- if: needs.files-changed.outputs.actions == 'true'
run: make lint-actions
lint-go-windows:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: lint-go-windows
lint-cache: "true"
- run: make deps-backend deps-tools
- run: make lint-go-windows
env:
TAGS: bindata
GOOS: windows
GOARCH: amd64
lint-go-gogit:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: lint-go-gogit
lint-cache: "true"
- run: make deps-backend deps-tools
- run: make lint-go
env:
TAGS: bindata gogit
checks-backend:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: checks-backend
build-cache: "false"
- run: make deps-backend deps-tools
- run: make --always-make checks-backend # ensure the "go-licenses" make target runs
frontend:
if: needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- run: make deps-frontend
- run: make lint-frontend
- run: make checks-frontend
- run: make test-frontend
- run: make frontend
backend:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: compliance-backend
- run: make deps-backend generate-go
# no frontend build here as backend should be able to build, even without any frontend files
# CGO is not used when cross-compile, so these steps also test if the code is compatible with CGO disabled
- name: build-backend-arm64
run: go build -o gitea_linux_arm64
env:
GOOS: linux
GOARCH: arm64
TAGS: bindata gogit
- name: build-backend-windows
run: go build -o gitea_windows
env:
GOOS: windows
GOARCH: amd64
TAGS: bindata gogit
- name: build-backend-386
run: go build -o gitea_linux_386
env:
GOOS: linux
GOARCH: 386
-262
View File
@@ -1,262 +0,0 @@
name: db-tests
on:
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
files-changed:
uses: ./.github/workflows/files-changed.yml
test-pgsql:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
services:
pgsql:
image: postgres:14
env:
POSTGRES_DB: test
POSTGRES_PASSWORD: postgres
ports:
- "5432:5432"
ldap:
image: gitea/test-openldap:latest
ports:
- "389:389"
- "636:636"
minio:
# as github actions doesn't support "entrypoint", we need to use a non-official image
# that has a custom entrypoint set to "minio server /data"
image: bitnamilegacy/minio:2023.12.23
env:
MINIO_ROOT_USER: 123456
MINIO_ROOT_PASSWORD: 12345678
ports:
- "9000:9000"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: pgsql
- name: Add hosts to /etc/hosts
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 pgsql ldap minio" | sudo tee -a /etc/hosts'
- run: make deps-backend
- run: make backend
env:
TAGS: bindata
- name: run migration tests
run: GITEA_TEST_DATABASE=pgsql make test-migration
- name: run tests
run: GITEA_TEST_DATABASE=pgsql make test-integration
timeout-minutes: 50
env:
# pgsql is chosen to be the unlucky one to run with the slow "race detector", it is about 60% slower.
GOTEST_FLAGS: -race -timeout=40m
TAGS: bindata gogit
TEST_LDAP: 1
test-sqlite:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: sqlite
- run: make deps-backend
- run: make backend
env:
TAGS: bindata gogit
GOEXPERIMENT:
- name: run migration tests
run: GITEA_TEST_DATABASE=sqlite make test-migration
env:
TAGS: bindata gogit
- name: run tests
run: GITEA_TEST_DATABASE=sqlite make test-integration
timeout-minutes: 50
env:
# sqlite driver can contain large amount of Golang code, so don't use race detector for it, otherwise, extremely slow
GOTEST_FLAGS: -timeout=40m
TAGS: bindata gogit
GOEXPERIMENT:
test-unit:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.19.14
env:
discovery.type: single-node
xpack.security.enabled: false
ports:
- "9200:9200"
meilisearch:
image: getmeili/meilisearch:v1
env:
MEILI_ENV: development # disable auth
ports:
- "7700:7700"
redis:
image: redis
options: >- # wait until redis has started
--health-cmd "redis-cli ping"
--health-interval 5s
--health-timeout 3s
--health-retries 10
ports:
- 6379:6379
minio:
image: bitnamilegacy/minio:2021.12.29
env:
MINIO_ACCESS_KEY: 123456
MINIO_SECRET_KEY: 12345678
ports:
- "9000:9000"
devstoreaccount1.azurite.local: # https://github.com/Azure/Azurite/issues/1583
image: mcr.microsoft.com/azure-storage/azurite:latest
ports:
- 10000:10000
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: unit
build-cache-rotate: "true"
- name: Add hosts to /etc/hosts
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 minio devstoreaccount1.azurite.local mysql elasticsearch meilisearch smtpimap" | sudo tee -a /etc/hosts'
- run: make deps-backend
- run: make backend
env:
TAGS: bindata
- name: unit-tests
run: make test-backend test-check
env:
GOTEST_FLAGS: -race -timeout=20m
TAGS: bindata
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
- name: unit-tests-gogit
run: make test-backend test-check
env:
GOTEST_FLAGS: -race -timeout=20m
TAGS: bindata gogit
GOEXPERIMENT:
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
test-mysql:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
services:
mysql:
# the bitnami mysql image has more options than the official one, it's easier to customize
image: bitnamilegacy/mysql:8.4
env:
ALLOW_EMPTY_PASSWORD: true
MYSQL_DATABASE: testgitea
ports:
- "3306:3306"
options: >-
--mount type=tmpfs,destination=/bitnami/mysql/data
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.19.14
env:
discovery.type: single-node
xpack.security.enabled: false
ports:
- "9200:9200"
smtpimap:
image: tabascoterrier/docker-imap-devel:latest
ports:
- "25:25"
- "143:143"
- "587:587"
- "993:993"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: mysql
- name: Add hosts to /etc/hosts
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mysql elasticsearch smtpimap" | sudo tee -a /etc/hosts'
- run: make deps-backend
- run: make backend
env:
TAGS: bindata
- name: run migration tests
run: GITEA_TEST_DATABASE=mysql make test-migration
- name: run tests
run: GITEA_TEST_DATABASE=mysql make test-integration
env:
TAGS: bindata
TEST_INDEXER_CODE_ES_URL: "http://elastic:changeme@elasticsearch:9200"
test-mssql:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
services:
mssql:
image: mcr.microsoft.com/mssql/server:2019-latest
env:
ACCEPT_EULA: Y
MSSQL_PID: Standard
SA_PASSWORD: MwantsaSecurePassword1
ports:
- "1433:1433"
devstoreaccount1.azurite.local: # https://github.com/Azure/Azurite/issues/1583
image: mcr.microsoft.com/azure-storage/azurite:latest
ports:
- 10000:10000
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: mssql
- name: Add hosts to /etc/hosts
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mssql devstoreaccount1.azurite.local" | sudo tee -a /etc/hosts'
- run: make deps-backend
- run: make backend
env:
TAGS: bindata
- run: GITEA_TEST_DATABASE=mssql make test-migration
- name: run tests
run: GITEA_TEST_DATABASE=mssql make test-integration
timeout-minutes: 50
env:
TAGS: bindata
-47
View File
@@ -1,47 +0,0 @@
name: docker-dryrun
on:
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
files-changed:
uses: ./.github/workflows/files-changed.yml
# QEMU-based build is slow (40-50 minutes), so run arm64 and riscv64 when dockerfile changes.
# Run amd64 when any docker-related files change, which is fast (4 minutes).
container-amd64:
if: needs.files-changed.outputs.docker == 'true'
needs: [files-changed]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/docker-dryrun
with:
platform: linux/amd64
container-arm64:
if: needs.files-changed.outputs.dockerfile == 'true'
needs: [files-changed]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/docker-dryrun
with:
platform: linux/arm64
container-riscv64:
if: needs.files-changed.outputs.dockerfile == 'true'
needs: [files-changed]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/docker-dryrun
with:
platform: linux/riscv64
-50
View File
@@ -1,50 +0,0 @@
name: e2e-tests
on:
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
files-changed:
uses: ./.github/workflows/files-changed.yml
test-e2e:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.e2e == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: e2e
build-cache: "false"
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- run: make deps-frontend
- run: make frontend
- run: make deps-backend
- run: make backend
env:
TAGS: bindata
- run: make playwright
- run: make test-e2e
timeout-minutes: 10
env:
TAGS: bindata
FORCE_COLOR: 1
GITEA_TEST_E2E_DEBUG: 1
-20
View File
@@ -1,20 +0,0 @@
name: labeler
on:
pull_request_target:
types: [opened, synchronize, reopened]
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
labeler:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
with:
sync-labels: true
-28
View File
@@ -1,28 +0,0 @@
name: pr-title
on:
pull_request:
types:
- opened
- edited
- reopened
- synchronize
- ready_for_review
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
lint-pr-title:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- run: make lint-pr-title
env:
PR_TITLE: ${{ github.event.pull_request.title }}
-135
View File
@@ -1,135 +0,0 @@
name: release-nightly
on:
push:
branches: [main, release/v*]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
nightly-binary:
runs-on: namespace-profile-gitea-release-binary
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- run: make deps-frontend deps-backend
# xgo build
- run: make release
env:
TAGS: bindata
- name: import gpg key
id: import_gpg
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
with:
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
- name: sign binaries
run: |
for f in dist/release/*; do
echo '${{ secrets.GPGSIGN_PASSPHRASE }}' | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u ${{ steps.import_gpg.outputs.fingerprint }} --output "$f.asc" "$f"
done
# clean branch name to get the folder name in S3
- name: Get cleaned branch name
id: clean_name
run: |
REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
echo "Cleaned name is ${REF_NAME}"
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
- name: configure aws
uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
with:
aws-region: ${{ secrets.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: upload binaries to s3
run: |
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
nightly-container:
runs-on: namespace-profile-gitea-release-docker
permissions:
contents: read
packages: write # to publish to ghcr.io
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Get cleaned branch name
id: clean_name
run: |
REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
- uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
id: meta
with:
images: |-
gitea/gitea
ghcr.io/go-gitea/gitea
tags: |
type=raw,value=${{ steps.clean_name.outputs.branch }}
annotations: |
org.opencontainers.image.authors="maintainers@gitea.io"
- uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
id: meta_rootless
with:
images: |-
gitea/gitea
ghcr.io/go-gitea/gitea
# each tag below will have the suffix of -rootless
flavor: |
suffix=-rootless
tags: |
type=raw,value=${{ steps.clean_name.outputs.branch }}
annotations: |
org.opencontainers.image.authors="maintainers@gitea.io"
- name: Login to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: build regular docker image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
push: true
tags: ${{ steps.meta.outputs.tags }}
annotations: ${{ steps.meta.outputs.annotations }}
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
cache-to: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful,mode=max
- name: build rootless docker image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
push: true
file: Dockerfile.rootless
tags: ${{ steps.meta_rootless.outputs.tags }}
annotations: ${{ steps.meta_rootless.outputs.annotations }}
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootless
cache-to: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootless,mode=max
-141
View File
@@ -1,141 +0,0 @@
name: release-tag-rc
on:
push:
tags:
- "v1*-rc*"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
binary:
runs-on: namespace-profile-gitea-release-binary
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- run: make deps-frontend deps-backend
# xgo build
- run: make release
env:
TAGS: bindata
- name: import gpg key
id: import_gpg
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
with:
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
- name: sign binaries
run: |
for f in dist/release/*; do
echo '${{ secrets.GPGSIGN_PASSPHRASE }}' | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u ${{ steps.import_gpg.outputs.fingerprint }} --output "$f.asc" "$f"
done
# clean branch name to get the folder name in S3
- name: Get cleaned branch name
id: clean_name
run: |
REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\/v//' -e 's/release\/v//')
echo "Cleaned name is ${REF_NAME}"
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
- name: configure aws
uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
with:
aws-region: ${{ secrets.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: upload binaries to s3
run: |
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
- name: Install GH CLI
uses: dev-hanz-ops/install-gh-cli-action@af38ce09b1ec248aeb08eea2b16bbecea9e059f8 # v0.2.1
with:
gh-cli-version: 2.39.1
- name: create github release
run: |
gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --draft --notes-from-tag dist/release/*
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
container:
runs-on: namespace-profile-gitea-release-docker
permissions:
contents: read
packages: write # to publish to ghcr.io
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
id: meta
with:
images: |-
gitea/gitea
ghcr.io/go-gitea/gitea
flavor: |
latest=false
# 1.2.3-rc0
tags: |
type=semver,pattern={{version}}
annotations: |
org.opencontainers.image.authors="maintainers@gitea.io"
- uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
id: meta_rootless
with:
images: |-
gitea/gitea
ghcr.io/go-gitea/gitea
# each tag below will have the suffix of -rootless
flavor: |
latest=false
suffix=-rootless
# 1.2.3-rc0
tags: |
type=semver,pattern={{version}}
annotations: |
org.opencontainers.image.authors="maintainers@gitea.io"
- name: Login to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: build regular container image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
push: true
tags: ${{ steps.meta.outputs.tags }}
annotations: ${{ steps.meta.outputs.annotations }}
- name: build rootless container image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
push: true
file: Dockerfile.rootless
tags: ${{ steps.meta_rootless.outputs.tags }}
annotations: ${{ steps.meta_rootless.outputs.annotations }}
-153
View File
@@ -1,153 +0,0 @@
name: release-tag-version
on:
push:
tags:
- "v1.*"
- "!v1*-rc*"
- "!v1*-dev"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
binary:
runs-on: namespace-profile-gitea-release-binary
permissions:
contents: read
packages: write # to publish to ghcr.io
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- run: make deps-frontend deps-backend
# xgo build
- run: make release
env:
TAGS: bindata
- name: import gpg key
id: import_gpg
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
with:
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
- name: sign binaries
run: |
for f in dist/release/*; do
echo '${{ secrets.GPGSIGN_PASSPHRASE }}' | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u ${{ steps.import_gpg.outputs.fingerprint }} --output "$f.asc" "$f"
done
# clean branch name to get the folder name in S3
- name: Get cleaned branch name
id: clean_name
run: |
REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\/v//' -e 's/release\/v//')
echo "Cleaned name is ${REF_NAME}"
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
- name: configure aws
uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
with:
aws-region: ${{ secrets.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: upload binaries to s3
run: |
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
- name: Install GH CLI
uses: dev-hanz-ops/install-gh-cli-action@af38ce09b1ec248aeb08eea2b16bbecea9e059f8 # v0.2.1
with:
gh-cli-version: 2.39.1
- name: create github release
run: |
gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --notes-from-tag dist/release/*
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
container:
runs-on: namespace-profile-gitea-release-docker
permissions:
contents: read
packages: write # to publish to ghcr.io
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
id: meta
with:
images: |-
gitea/gitea
ghcr.io/go-gitea/gitea
# this will generate tags in the following format:
# latest
# 1
# 1.2
# 1.2.3
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}}
annotations: |
org.opencontainers.image.authors="maintainers@gitea.io"
- uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
id: meta_rootless
with:
images: |-
gitea/gitea
ghcr.io/go-gitea/gitea
# each tag below will have the suffix of -rootless
flavor: |
suffix=-rootless,onlatest=true
# this will generate tags in the following format (with -rootless suffix added):
# latest
# 1
# 1.2
# 1.2.3
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}}
annotations: |
org.opencontainers.image.authors="maintainers@gitea.io"
- name: Login to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: build regular container image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
push: true
tags: ${{ steps.meta.outputs.tags }}
annotations: ${{ steps.meta.outputs.annotations }}
- name: build rootless container image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
push: true
file: Dockerfile.rootless
tags: ${{ steps.meta_rootless.outputs.tags }}
annotations: ${{ steps.meta_rootless.outputs.annotations }}
+1 -1
View File
@@ -4,7 +4,7 @@
<name>MokoGitea</name> <name>MokoGitea</name>
<org>MokoConsulting</org> <org>MokoConsulting</org>
<description>Moko fork of Gitea - adding project board REST API endpoints and custom enhancements</description> <description>Moko fork of Gitea - adding project board REST API endpoints and custom enhancements</description>
<version>06.12.02</version> <version>06.14.00</version>
<version-prefix>v1.26.1+MOKO</version-prefix> <version-prefix>v1.26.1+MOKO</version-prefix>
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license> <license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
</identity> </identity>
-129
View File
@@ -1,129 +0,0 @@
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later
DEFGROUP: gitea-api-mcp.Documentation
REPO: https://git.mokoconsulting.tech/MokoConsulting/gitea-api-mcp
-->
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Changed
- **Renamed** package from `@mokoconsulting/gitea-api-mcp` to `@mokoconsulting/mokogitea-api-mcp` to distinguish Moko's forked Gitea MCP from upstream
- **Renamed** McpServer name and bin entry to `mokogitea-api-mcp`
## [0.0] - 2026-05-07
### Added
#### User / Auth (3 tools)
- `gitea_me` -- Get the authenticated user info
- `gitea_user_orgs` -- List organizations the authenticated user belongs to
- `gitea_user_repos` -- List repositories owned by the authenticated user
#### Repositories (8 tools)
- `gitea_repo_get` -- Get repository details
- `gitea_repo_create` -- Create a new repository
- `gitea_repo_delete` -- Delete a repository
- `gitea_repo_edit` -- Edit repository settings
- `gitea_repo_fork` -- Fork a repository
- `gitea_repo_search` -- Search repositories
- `gitea_org_repos` -- List repositories in an organization
- `gitea_list_connections` -- List configured Gitea connections
#### File Contents (5 tools)
- `gitea_file_get` -- Get file contents from a repository
- `gitea_dir_get` -- Get directory contents (file listing) from a repository
- `gitea_file_create_or_update` -- Create or update a file in a repository
- `gitea_file_delete` -- Delete a file from a repository
- `gitea_tree_get` -- Get the git tree for a repository (recursive file listing)
#### Branches (4 tools)
- `gitea_branches_list` -- List branches in a repository
- `gitea_branch_get` -- Get a specific branch
- `gitea_branch_create` -- Create a new branch
- `gitea_branch_delete` -- Delete a branch
#### Commits (2 tools)
- `gitea_commits_list` -- List commits in a repository
- `gitea_commit_get` -- Get a specific commit
#### Issues (7 tools)
- `gitea_issues_list` -- List issues in a repository
- `gitea_issue_get` -- Get a single issue by number
- `gitea_issue_create` -- Create a new issue
- `gitea_issue_update` -- Update an issue
- `gitea_issue_comments_list` -- List comments on an issue
- `gitea_issue_comment_create` -- Add a comment to an issue
- `gitea_issue_search` -- Search issues across all repositories
#### Labels (2 tools)
- `gitea_labels_list` -- List labels in a repository
- `gitea_label_create` -- Create a label
#### Milestones (2 tools)
- `gitea_milestones_list` -- List milestones in a repository
- `gitea_milestone_create` -- Create a milestone
#### Pull Requests (6 tools)
- `gitea_pulls_list` -- List pull requests
- `gitea_pull_get` -- Get a single pull request
- `gitea_pull_create` -- Create a pull request
- `gitea_pull_merge` -- Merge a pull request
- `gitea_pull_files` -- List files changed in a pull request
- `gitea_pull_review_create` -- Create a pull request review
#### Releases (5 tools)
- `gitea_releases_list` -- List releases
- `gitea_release_get` -- Get a single release by ID
- `gitea_release_latest` -- Get the latest release
- `gitea_release_create` -- Create a new release
- `gitea_release_delete` -- Delete a release
#### Tags (3 tools)
- `gitea_tags_list` -- List tags
- `gitea_tag_create` -- Create a tag
- `gitea_tag_delete` -- Delete a tag
#### Actions (2 tools)
- `gitea_actions_runs_list` -- List workflow runs for a repository
- `gitea_actions_run_get` -- Get a specific workflow run
#### Organizations (3 tools)
- `gitea_org_get` -- Get organization details
- `gitea_org_teams_list` -- List teams in an organization
- `gitea_org_members_list` -- List members of an organization
#### Users (2 tools)
- `gitea_user_get` -- Get a user profile
- `gitea_users_search` -- Search users
#### Webhooks (2 tools)
- `gitea_webhooks_list` -- List webhooks for a repository
- `gitea_webhook_create` -- Create a webhook
#### Wiki (2 tools)
- `gitea_wiki_pages_list` -- List wiki pages
- `gitea_wiki_page_get` -- Get a wiki page
#### Notifications (2 tools)
- `gitea_notifications_list` -- List notifications for the authenticated user
- `gitea_notifications_read` -- Mark all notifications as read
#### Generic (2 tools)
- `gitea_api_request` -- Make a raw API request to any Gitea v1 endpoint
- `gitea_list_connections` -- List configured Gitea connections
### Infrastructure
- Multi-connection config support via `~/.gitea-api-mcp.json`
- Token-based authentication (Gitea native `Authorization: token` header)
- Built on `node:https` / `node:http` (zero HTTP dependencies)
- MCP SDK v1.12.x with stdio transport
[0.0.1]: https://git.mokoconsulting.tech/MokoConsulting/gitea-api-mcp/releases/tag/v0.0.1
-18
View File
@@ -1,18 +0,0 @@
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --production=false
COPY tsconfig.json ./
COPY src/ ./src/
RUN npx tsc && npm prune --production
EXPOSE 3100
ENV PORT=3100
ENV NODE_ENV=production
# SSE mode by default for Docker deployments
CMD ["node", "dist/sse.js"]
-116
View File
@@ -1,116 +0,0 @@
# MokoGitea MCP Server
A comprehensive [Model Context Protocol](https://modelcontextprotocol.io) server for [Gitea](https://gitea.com) and [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea). 120+ tools for repos, issues, PRs, projects, releases, custom fields, statuses, priorities, and manifests.
Works with any Gitea instance. MokoGitea-specific features degrade gracefully on vanilla Gitea.
## Quick Start
### npx (no install)
```bash
GITEA_URL=https://gitea.example.com GITEA_TOKEN=your_token npx @mokoconsulting/mokogitea-mcp
```
### Claude Code
Add to `.claude.json`:
```json
{
"mcpServers": {
"mokogitea": {
"command": "npx",
"args": ["@mokoconsulting/mokogitea-mcp"],
"env": {
"GITEA_URL": "https://gitea.example.com",
"GITEA_TOKEN": "your_token"
}
}
}
}
```
### Docker (SSE mode)
```bash
docker run -p 3100:3100 \
-e GITEA_URL=https://gitea.example.com \
-e GITEA_TOKEN=your_token \
mokoconsulting/mokogitea-mcp
```
Connect MCP client to `http://localhost:3100/sse`.
### Multi-instance config
Create `~/.mcp_mokogitea.json`:
```json
{
"defaultConnection": "production",
"connections": {
"production": { "baseUrl": "https://gitea.example.com", "token": "your_token" },
"dev": { "baseUrl": "https://dev.gitea.example.com", "token": "dev_token" }
}
}
```
## Configuration
| Method | Use Case |
|--------|----------|
| `GITEA_URL` + `GITEA_TOKEN` env vars | Single instance, quick setup |
| `~/.mcp_mokogitea.json` config file | Multiple instances |
| `GITEA_API_MCP_CONFIG` env var | Custom config path |
| `GITEA_INSECURE=true` | Skip TLS verification |
## Tools (120+)
### Repositories
`gitea_repo_create` `gitea_repo_get` `gitea_repo_edit` `gitea_repo_delete` `gitea_repo_search` `gitea_repo_fork` `gitea_repo_generate` `gitea_repo_languages` `gitea_repo_contributors` `gitea_repo_topics` `gitea_repo_topics_set`
### Issues
`gitea_issue_create` (dedup by title) `gitea_issue_get` `gitea_issue_update` `gitea_issues_list` `gitea_issue_search` `gitea_issue_comment_create` `gitea_issue_comments_list` `gitea_issue_labels_set` `gitea_issue_bulk_set_status`
### Pull Requests
`gitea_pull_create` `gitea_pull_get` `gitea_pulls_list` `gitea_pull_merge` `gitea_pull_files` `gitea_pull_review_create`
### Branches and Tags
`gitea_branches_list` `gitea_branch_create` `gitea_branch_delete` `gitea_branch_get` `gitea_tags_list` `gitea_tag_create` `gitea_tag_delete`
### Releases
`gitea_releases_list` `gitea_release_create` `gitea_release_get` `gitea_release_latest` `gitea_release_delete` `gitea_release_asset_upload` `gitea_release_asset_delete`
### Files and Trees
`gitea_file_get` `gitea_file_create_or_update` `gitea_file_delete` `gitea_dir_get` `gitea_tree_get` `gitea_bulk_file_push`
### Projects
`gitea_project_list` `gitea_project_create` `gitea_project_get` `gitea_project_update` `gitea_project_delete` `gitea_project_overview` `gitea_project_columns_list` `gitea_project_column_create` `gitea_project_column_delete` `gitea_project_cards_list` `gitea_project_card_add` `gitea_project_card_move` `gitea_project_card_remove`
### Organizations
`gitea_org_get` `gitea_org_repos` `gitea_org_members_list` `gitea_org_teams_list` `gitea_org_labels_list` `gitea_org_label_create`
### Wiki
`gitea_wiki_pages_list` `gitea_wiki_page_get`
### MokoGitea Extensions
`gitea_manifest_get` `gitea_manifest_update` `gitea_org_custom_fields_list` `gitea_org_custom_field_create` `gitea_org_custom_field_delete` `gitea_issue_custom_fields_get` `gitea_issue_custom_fields_set` `gitea_org_issue_statuses_list` `gitea_issue_set_status` `gitea_org_issue_priorities_list` `gitea_issue_set_priority`
### Admin and Other
`gitea_me` `gitea_users_search` `gitea_user_get` `gitea_notifications_list` `gitea_notifications_read` `gitea_commits_list` `gitea_commit_get` `gitea_compare` `gitea_webhooks_list` `gitea_webhook_create` `gitea_admin_users_list` `gitea_admin_orgs_list` `gitea_admin_cron_list` `gitea_admin_cron_run` `gitea_list_connections`
## SSE Server
For hosted deployments:
```
GET / Server info
GET /sse SSE connection endpoint
POST /message Tool call messages
GET /health Health check
```
## License
GPL-3.0-or-later - [Moko Consulting](https://mokoconsulting.tech)
-13
View File
@@ -1,13 +0,0 @@
{
"defaultConnection": "moko",
"connections": {
"moko": {
"baseUrl": "https://git.mokoconsulting.tech",
"token": "your-gitea-access-token"
},
"github-mirror": {
"baseUrl": "https://gitea.example.com",
"token": "your-other-token"
}
}
}
-1198
View File
File diff suppressed because it is too large Load Diff
-58
View File
@@ -1,58 +0,0 @@
{
"name": "@mokoconsulting/mokogitea-mcp",
"version": "1.1.0",
"description": "MCP server for Gitea and MokoGitea - 120+ tools for repos, issues, PRs, projects, releases, custom fields, statuses, priorities, and manifests",
"type": "module",
"main": "dist/index.js",
"bin": {
"mokogitea-mcp": "dist/index.js",
"mokogitea-mcp-sse": "dist/sse.js"
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"start": "node dist/index.js",
"start:sse": "node dist/sse.js",
"setup": "node scripts/setup.mjs",
"clean": "rm -rf dist/"
},
"keywords": [
"mcp",
"gitea",
"mokogitea",
"model-context-protocol",
"claude",
"ai",
"git",
"self-hosted",
"api",
"devops"
],
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.1",
"zod": "^3.24.4"
},
"devDependencies": {
"@types/node": "^22.15.3",
"typescript": "^5.8.3"
},
"engines": {
"node": ">=20.0.0"
},
"license": "GPL-3.0-or-later",
"author": "Moko Consulting <hello@mokoconsulting.tech>",
"homepage": "https://git.mokoconsulting.tech/MokoConsulting/mcp_mokogitea_api",
"repository": {
"type": "git",
"url": "https://git.mokoconsulting.tech/MokoConsulting/mcp_mokogitea_api.git"
},
"files": [
"dist/",
"config.example.json",
"README.md",
"LICENSE"
],
"publishConfig": {
"access": "public"
}
}
-15
View File
@@ -1,15 +0,0 @@
# mcp_mokogitea_api PowerShell Profile
# Source this with: . ./profile.ps1
$env:MCP_ROOT = $PSScriptRoot
$env:TEMP = 'A:\temp'
$env:TMP = 'A:\temp'
function mcp { Set-Location $PSScriptRoot }
function mcp-src { Set-Location (Join-Path $PSScriptRoot 'src') }
function mcp-build { Set-Location $PSScriptRoot; npm run build }
function mcp-dev { Set-Location $PSScriptRoot; npm run dev }
Write-Host "mcp_mokogitea_api profile loaded" -ForegroundColor Cyan
Write-Host " Commands: mcp-build, mcp-dev" -ForegroundColor DarkGray
Write-Host " Navigate: mcp, mcp-src" -ForegroundColor DarkGray
-40
View File
@@ -1,40 +0,0 @@
#!/usr/bin/env node
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
* BRIEF: Interactive setup — prompts for Gitea connection details
*/
import { createInterface } from 'node:readline/promises';
import { readFile, writeFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { homedir } from 'node:os';
const CONFIG_PATH = resolve(homedir(), '.gitea-api-mcp.json');
const rl = createInterface({ input: process.stdin, output: process.stdout });
async function prompt(q, d) { const a = await rl.question(`${q}${d ? ` [${d}]` : ''}: `); return a.trim() || d || ''; }
async function promptRequired(q) { let a = ''; while (!a) { a = (await rl.question(`${q}: `)).trim(); if (!a) console.log(' Required.'); } return a; }
async function main() {
console.log('\n=== gitea-api-mcp Setup ===\n');
let existing = null;
try { existing = JSON.parse(await readFile(CONFIG_PATH, 'utf-8')); console.log(`Existing: ${Object.keys(existing.connections).join(', ')}\n`); } catch {}
const name = await prompt('Connection name', 'moko');
const baseUrl = await promptRequired('Gitea URL (e.g. https://git.mokoconsulting.tech)');
const token = await promptRequired('Access token (Settings > Applications > Generate Token)');
const insecure = (await prompt('Skip TLS verification? (y/N)', 'N')).toLowerCase() === 'y';
const conn = { baseUrl: baseUrl.replace(/\/+$/, ''), token };
if (insecure) conn.insecure = true;
const config = existing ?? { defaultConnection: name, connections: {} };
config.connections[name] = conn;
if (!existing) config.defaultConnection = name;
else if ((await prompt(`Set "${name}" as default? (y/N)`, 'N')).toLowerCase() === 'y') config.defaultConnection = name;
await writeFile(CONFIG_PATH, JSON.stringify(config, null, '\t') + '\n', 'utf-8');
console.log(`\nConfig written to ${CONFIG_PATH}\n`);
rl.close();
}
main().catch(e => { console.error(e.message); rl.close(); process.exit(1); });
-120
View File
@@ -1,120 +0,0 @@
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: gitea-api-mcp.Client
* INGROUP: gitea-api-mcp
* REPO: https://git.mokoconsulting.tech/MokoConsulting/gitea-api-mcp
* PATH: /src/client.ts
* VERSION: 01.00.00
* BRIEF: HTTP client for Gitea REST API v1
*/
import * as https from 'node:https';
import * as http from 'node:http';
import type { GiteaConnection, ApiResponse } from './types.js';
const API_PREFIX = '/api/v1';
const TIMEOUT_MS = 30_000;
export class GiteaClient {
private readonly base_url: string;
private readonly headers: Record<string, string>;
private readonly insecure: boolean;
constructor(conn: GiteaConnection) {
this.base_url = conn.baseUrl.replace(/\/+$/, '') + API_PREFIX;
this.headers = {
'Authorization': `token ${conn.token}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
};
this.insecure = conn.insecure ?? false;
}
async get(endpoint: string, params?: Record<string, string>): Promise<ApiResponse> {
return this.request(this.buildUrl(endpoint, params), 'GET');
}
async post(endpoint: string, body?: unknown): Promise<ApiResponse> {
return this.request(this.buildUrl(endpoint), 'POST', body);
}
async patch(endpoint: string, body: unknown): Promise<ApiResponse> {
return this.request(this.buildUrl(endpoint), 'PATCH', body);
}
async put(endpoint: string, body: unknown): Promise<ApiResponse> {
return this.request(this.buildUrl(endpoint), 'PUT', body);
}
async delete(endpoint: string): Promise<ApiResponse> {
return this.request(this.buildUrl(endpoint), 'DELETE');
}
private buildUrl(endpoint: string, params?: Record<string, string>): string {
const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
const url = new URL(`${this.base_url}${path}`);
if (params) {
for (const [key, value] of Object.entries(params)) {
url.searchParams.set(key, value);
}
}
return url.toString();
}
private request(url: string, method: string, body?: unknown): Promise<ApiResponse> {
return new Promise((resolve, reject) => {
const parsed = new URL(url);
const is_https = parsed.protocol === 'https:';
const transport = is_https ? https : http;
const options: https.RequestOptions = {
hostname: parsed.hostname,
port: parsed.port || (is_https ? 443 : 80),
path: parsed.pathname + parsed.search,
method,
headers: { ...this.headers },
timeout: TIMEOUT_MS,
};
if (this.insecure && is_https) {
options.rejectUnauthorized = false;
}
const payload = body !== undefined ? JSON.stringify(body) : undefined;
if (payload) {
(options.headers as Record<string, string>)['Content-Length'] = Buffer.byteLength(payload).toString();
}
const req = transport.request(options, (res) => {
const chunks: Buffer[] = [];
res.on('data', (chunk: Buffer) => chunks.push(chunk));
res.on('end', () => {
const raw = Buffer.concat(chunks).toString('utf-8');
let data: unknown;
try {
data = JSON.parse(raw);
} catch {
data = raw;
}
resolve({ status: res.statusCode ?? 0, data });
});
});
req.on('error', (err) => reject(err));
req.on('timeout', () => {
req.destroy();
reject(new Error('Request timed out'));
});
if (payload) {
req.write(payload);
}
req.end();
});
}
}
-61
View File
@@ -1,61 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { homedir } from 'node:os';
import type { GiteaConfig, GiteaConnection } from './types.js';
const CONFIG_FILENAME = '.mcp_mokogitea.json';
export async function loadConfig(): Promise<GiteaConfig> {
// Priority 1: Environment variables (zero-config single instance)
if (process.env.GITEA_URL && process.env.GITEA_TOKEN) {
const conn: GiteaConnection = {
baseUrl: process.env.GITEA_URL,
token: process.env.GITEA_TOKEN,
insecure: process.env.GITEA_INSECURE === 'true',
};
return {
connections: { default: conn },
defaultConnection: 'default',
};
}
// Priority 2: Config file
const config_path = process.env.GITEA_API_MCP_CONFIG
? resolve(process.env.GITEA_API_MCP_CONFIG)
: resolve(homedir(), CONFIG_FILENAME);
try {
const raw = await readFile(config_path, 'utf-8');
const parsed = JSON.parse(raw) as Partial<GiteaConfig>;
if (!parsed.connections || Object.keys(parsed.connections).length === 0) {
throw new Error('No connections defined in config');
}
return {
connections: parsed.connections,
defaultConnection: parsed.defaultConnection ?? Object.keys(parsed.connections)[0],
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw new Error(
`Failed to load config from ${config_path}: ${message}\n` +
`Option 1: Set GITEA_URL and GITEA_TOKEN environment variables\n` +
`Option 2: Create ${config_path} - see config.example.json for format`,
);
}
}
export function getConnection(config: GiteaConfig, name?: string): GiteaConnection {
const key = name ?? config.defaultConnection;
const conn = config.connections[key];
if (!conn) {
throw new Error(
`Connection "${key}" not found. Available: ${Object.keys(config.connections).join(', ')}`,
);
}
return conn;
}
File diff suppressed because it is too large Load Diff
-16
View File
@@ -1,16 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
//
// Creates a configured MCP server instance for use by both stdio and SSE transports.
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type { GiteaConfig } from './types.js';
// Import index.ts to register all tools on its exported `server` singleton,
// then re-export a factory that initializes config and returns the server.
import { server, initConfig } from './index.js';
export function createMcpServer(cfg: GiteaConfig): McpServer {
initConfig(cfg);
return server;
}
-100
View File
@@ -1,100 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
//
// SSE transport entry point for MokoGitea MCP server.
// Run with: node dist/sse.js
// Or: GITEA_URL=https://gitea.example.com GITEA_TOKEN=xxx node dist/sse.js
//
// Listens on PORT (default 3100) and serves SSE at /sse with POST at /message.
import { createServer } from 'node:http';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { createMcpServer } from './server.js';
import { loadConfig } from './config.js';
const PORT = parseInt(process.env.PORT ?? '3100', 10);
async function main(): Promise<void> {
const config = await loadConfig();
const transports = new Map<string, SSEServerTransport>();
const httpServer = createServer(async (req, res) => {
// CORS headers for browser clients
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') {
res.writeHead(204);
res.end();
return;
}
// Health check
if (req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok', tools: 120 }));
return;
}
// SSE endpoint - client connects here
if (req.url === '/sse' && req.method === 'GET') {
const transport = new SSEServerTransport('/message', res);
const sessionId = transport.sessionId;
transports.set(sessionId, transport);
const server = createMcpServer(config);
await server.connect(transport);
req.on('close', () => {
transports.delete(sessionId);
});
return;
}
// Message endpoint - client sends tool calls here
if (req.url?.startsWith('/message') && req.method === 'POST') {
const url = new URL(req.url, `http://${req.headers.host}`);
const sessionId = url.searchParams.get('sessionId');
if (!sessionId || !transports.has(sessionId)) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid or missing sessionId' }));
return;
}
const transport = transports.get(sessionId)!;
await transport.handlePostMessage(req, res);
return;
}
// Root - info page
if (req.url === '/' || req.url === '') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
name: '@mokoconsulting/mokogitea-mcp',
version: '1.1.0',
description: 'MCP server for Gitea and MokoGitea - 120+ tools',
endpoints: {
sse: '/sse',
message: '/message',
health: '/health',
},
docs: 'https://git.mokoconsulting.tech/MokoConsulting/mcp_mokogitea_api',
}));
return;
}
res.writeHead(404);
res.end('Not found');
});
httpServer.listen(PORT, () => {
process.stderr.write(`MokoGitea MCP SSE server listening on port ${PORT}\n`);
process.stderr.write(` SSE: http://localhost:${PORT}/sse\n`);
process.stderr.write(` Health: http://localhost:${PORT}/health\n`);
});
}
main().catch((err) => {
process.stderr.write(`Fatal: ${err}\n`);
process.exit(1);
});
-37
View File
@@ -1,37 +0,0 @@
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: gitea-api-mcp.Types
* INGROUP: gitea-api-mcp
* REPO: https://git.mokoconsulting.tech/MokoConsulting/gitea-api-mcp
* PATH: /src/types.ts
* VERSION: 01.00.00
* BRIEF: TypeScript type definitions for Gitea API MCP server
*/
export interface GiteaConnection {
baseUrl: string;
token: string;
/** Skip TLS certificate verification (self-signed certs) */
insecure?: boolean;
}
export interface GitHubBackupConfig {
token: string;
org: string;
}
export interface GiteaConfig {
connections: Record<string, GiteaConnection>;
defaultConnection: string;
github?: GitHubBackupConfig;
}
export interface ApiResponse {
status: number;
data: unknown;
}
-19
View File
@@ -1,19 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
+66
View File
@@ -0,0 +1,66 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.mokogitea/workflows/auto-bump.yml
# VERSION: 09.02.00
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
name: "Universal: Auto Version Bump"
on:
push:
branches:
- dev
- rc
- 'feature/**'
- 'patch/**'
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
permissions:
contents: write
jobs:
bump:
name: Version Bump
runs-on: release
if: >-
!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[skip bump]') &&
!startsWith(github.event.head_commit.message, 'Merge pull request')
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1
- name: Setup moko-platform tools
run: |
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
if [ -d "/opt/moko-platform/cli" ]; then
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
else
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
fi
- name: Bump version
run: |
php ${MOKO_CLI}/version_auto_bump.php \
--path . --branch "${GITHUB_REF_NAME}" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
+341 -1
View File
@@ -1 +1,341 @@
placeholder # Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/universal/auto-release.yml.template
# VERSION: 05.00.00
# BRIEF: Universal build & release detects platform from manifest.xml
#
# +========================================================================+
# | UNIVERSAL BUILD & RELEASE PIPELINE |
# +========================================================================+
# | |
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
# | |
# | Platform-specific: |
# | joomla: XML manifest, type-prefixed packages |
# | dolibarr: mod*.class.php, update.txt, dev version reset |
# | generic: README-only, no update stream |
# | |
# +========================================================================+
name: "Universal: Build & Release"
on:
pull_request:
types: [opened, closed]
branches:
- main
workflow_dispatch:
inputs:
action:
description: 'Action to perform'
required: false
type: choice
default: release
options:
- release
- promote-rc
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
permissions:
contents: write
jobs:
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────
promote-rc:
name: Promote to RC
runs-on: release
if: >-
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
echo Using pre-installed /opt/moko-platform
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
else
echo Falling back to fresh clone
if ! command -v composer > /dev/null 2>&1; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
fi
rm -rf /tmp/moko-platform-api
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
cd /tmp/moko-platform-api
composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
fi
- name: Rename branch to rc
run: |
php ${MOKO_CLI}/branch_rename.php \
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
--pr "${{ github.event.pull_request.number }}"
- name: Checkout rc and configure git
run: |
git fetch origin rc
git checkout rc
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- name: Publish RC release
run: |
php ${MOKO_CLI}/release_publish.php \
--path . --stability rc --bump minor --branch rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}"
- name: Summary
if: always()
run: |
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
release:
name: Build & Release Pipeline
runs-on: release
if: >-
github.event.pull_request.merged == true ||
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 0
- name: Configure git for bot pushes
run: |
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- name: Check for merge conflict markers
run: |
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
if [ -n "$CONFLICTS" ]; then
echo "::error::Merge conflict markers found — aborting release"
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "No conflict markers found"
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
run: |
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
echo Using pre-installed /opt/moko-platform
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
else
echo Falling back to fresh clone
if ! command -v composer > /dev/null 2>&1; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
fi
rm -rf /tmp/moko-platform-api
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
cd /tmp/moko-platform-api
composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
fi
- name: "Determine version bump level"
id: bump
run: |
# Fix/patch branches: version was already bumped by pre-release, just strip suffix
# Feature/dev branches: bump minor for the new stable release
HEAD_REF="${{ github.event.pull_request.head.ref || 'dev' }}"
case "$HEAD_REF" in
fix/*|patch/*|hotfix/*|bugfix/*) BUMP="none" ;;
*) BUMP="minor" ;;
esac
echo "level=${BUMP}" >> "$GITHUB_OUTPUT"
echo "Bump level: ${BUMP} (from branch: ${HEAD_REF})"
- name: "Publish stable release"
run: |
BUMP_FLAG=""
if [ "${{ steps.bump.outputs.level }}" != "none" ]; then
BUMP_FLAG="--bump ${{ steps.bump.outputs.level }}"
fi
php ${MOKO_CLI}/release_publish.php \
--path . --stability stable ${BUMP_FLAG} --branch main \
--token "${{ secrets.MOKOGITEA_TOKEN }}"
- name: Update release notes from CHANGELOG.md
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Extract [Unreleased] section from changelog
if [ -f "CHANGELOG.md" ]; then
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
[ -z "$NOTES" ] && NOTES="Stable release"
else
NOTES="Stable release"
fi
# Update release body via API
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
"${API_BASE}/releases/tags/stable" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -n "$RELEASE_ID" ]; then
python3 -c "
import json, urllib.request
body = open('/dev/stdin').read()
payload = json.dumps({'body': body}).encode()
req = urllib.request.Request(
'${API_BASE}/releases/${RELEASE_ID}',
data=payload, method='PATCH',
headers={
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
'Content-Type': 'application/json'
})
urllib.request.urlopen(req)
" <<< "$NOTES"
echo "Release notes updated from CHANGELOG.md"
fi
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
- name: "Step 9: Mirror release to GitHub"
if: >-
steps.version.outputs.skip != 'true' &&
secrets.GH_MIRROR_TOKEN != ''
continue-on-error: true
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_mirror.php \
--version "$VERSION" --tag "$RELEASE_TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
--branch main 2>&1 || true
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
- name: "Step 10: Push main to GitHub mirror"
if: >-
steps.version.outputs.skip != 'true' &&
secrets.GH_MIRROR_TOKEN != ''
continue-on-error: true
run: |
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
git fetch origin main --depth=1
git push github origin/main:refs/heads/main --force 2>/dev/null \
&& echo "main branch pushed to GitHub mirror" \
|| echo "WARNING: GitHub mirror push failed"
- name: "Step 11: Delete rc branch and recreate dev from main"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Delete rc branch (ephemeral — created by promote-rc)
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/rc" 2>/dev/null \
&& echo "Deleted rc branch" || echo "rc branch not found"
# Delete dev branch
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
# Recreate dev from main (now includes version bump + changelog promotion)
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API_BASE}/branches" \
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
- name: "Step 12: Create version branch from main"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
BRANCH_NAME="version/${VERSION}"
MAIN_SHA=$(git rev-parse HEAD)
# Delete old version branch if it exists (same version re-release)
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
# Create version/XX.YY.ZZ from main
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
# -- Dolibarr post-release: Reset dev version -----------------------------
- name: "Post-release: Reset dev version"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/version_reset_dev.php \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
--branch dev --path . 2>&1 || true
# -- Summary --------------------------------------------------------------
- name: Pipeline Summary
if: always()
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
PLATFORM="${{ steps.platform.outputs.platform }}"
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
fi
+2 -2
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Universal # INGROUP: MokoStandards.Universal
# REPO: https://code.mokoconsulting.tech/MokoConsulting/mokoplatform # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.mokogitea/workflows/branch-cleanup.yml # PATH: /.mokogitea/workflows/branch-cleanup.yml
# VERSION: 01.00.00 # VERSION: 01.00.00
# BRIEF: Delete feature branches after PR merge # BRIEF: Delete feature branches after PR merge
@@ -32,7 +32,7 @@ jobs:
- name: Delete source branch - name: Delete source branch
run: | run: |
BRANCH="${{ github.event.pull_request.head.ref }}" BRANCH="${{ github.event.pull_request.head.ref }}"
API="${{ vars.GITEA_URL || 'https://code.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches" API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');") ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \ STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
+10
View File
@@ -0,0 +1,10 @@
# DISABLED — auto-release Step 11 recreates dev from main after every release.
# Cascade-dev is redundant and causes version conflicts when both main and dev
# have different version numbers in templateDetails.xml / manifest.xml.
name: "Cascade Main → Dev (DISABLED)"
on: workflow_dispatch
jobs:
noop:
runs-on: ubuntu-latest
steps:
- run: echo "Cascade disabled — auto-release handles dev recreation"
+204
View File
@@ -0,0 +1,204 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.CI
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.gitea/workflows/ci-generic.yml
# VERSION: 01.00.00
# BRIEF: CI pipeline — lint, validate, and test for generic projects (PHP + Node.js)
name: "Generic: Project CI"
on:
push:
branches:
- main
- dev
- dev/**
- rc/**
- version/**
pull_request:
branches:
- main
- dev
- dev/**
- rc/**
workflow_dispatch:
permissions:
contents: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
# ── Lint & Validate ───────────────────────────────────────────────────
lint:
name: Lint & Validate
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Detect toolchain
id: detect
run: |
HAS_PHP=false
HAS_NODE=false
[ -f "composer.json" ] && HAS_PHP=true
[ -f "package.json" ] && HAS_NODE=true
echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT"
echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT"
echo "Toolchain: PHP=$HAS_PHP Node=$HAS_NODE"
- name: Setup PHP
if: steps.detect.outputs.has_php == 'true'
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 >/dev/null 2>&1
fi
php -v
- name: Setup Node.js
if: steps.detect.outputs.has_node == 'true'
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install PHP dependencies
if: steps.detect.outputs.has_php == 'true'
run: |
if [ -f "composer.json" ]; then
composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true
fi
- name: Install Node.js dependencies
if: steps.detect.outputs.has_node == 'true'
run: |
if [ -f "package.json" ]; then
npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true
fi
- name: PHP syntax check
if: steps.detect.outputs.has_php == 'true'
run: |
ERRORS=0
while IFS= read -r -d '' file; do
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
echo "::error file=${file}::PHP syntax error"
ERRORS=$((ERRORS + 1))
fi
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./node_modules/*" -print0)
echo "## PHP Lint" >> $GITHUB_STEP_SUMMARY
if [ "$ERRORS" -eq 0 ]; then
echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY
else
echo "${ERRORS} file(s) with syntax errors." >> $GITHUB_STEP_SUMMARY
exit 1
fi
- name: TypeScript/JavaScript lint
if: steps.detect.outputs.has_node == 'true'
run: |
if [ -f "node_modules/.bin/eslint" ]; then
npx eslint src/ --quiet 2>&1 || { echo "::error::ESLint errors found"; exit 1; }
echo "## ESLint" >> $GITHUB_STEP_SUMMARY
echo "All files passed ESLint." >> $GITHUB_STEP_SUMMARY
elif [ -f ".eslintrc.json" ] || [ -f ".eslintrc.js" ] || [ -f "eslint.config.js" ]; then
echo "::warning::ESLint config found but eslint not installed"
else
echo "No ESLint configured — skipping"
fi
- name: TypeScript compile check
if: steps.detect.outputs.has_node == 'true'
run: |
if [ -f "tsconfig.json" ] && [ -f "node_modules/.bin/tsc" ]; then
npx tsc --noEmit 2>&1 || { echo "::error::TypeScript compilation errors"; exit 1; }
echo "## TypeScript" >> $GITHUB_STEP_SUMMARY
echo "TypeScript compilation passed." >> $GITHUB_STEP_SUMMARY
fi
- name: PHPStan static analysis
if: steps.detect.outputs.has_php == 'true'
run: |
if [ -f "phpstan.neon" ] && [ -f "vendor/bin/phpstan" ]; then
vendor/bin/phpstan analyse --no-progress 2>&1 || { echo "::warning::PHPStan found issues"; }
fi
# ── Tests ─────────────────────────────────────────────────────────────
test:
name: Tests
runs-on: ubuntu-latest
needs: lint
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Detect toolchain
id: detect
run: |
HAS_PHP=false
HAS_NODE=false
[ -f "composer.json" ] && HAS_PHP=true
[ -f "package.json" ] && HAS_NODE=true
echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT"
echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT"
- name: Setup PHP
if: steps.detect.outputs.has_php == 'true'
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 >/dev/null 2>&1
fi
- name: Setup Node.js
if: steps.detect.outputs.has_node == 'true'
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: |
[ -f "composer.json" ] && composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true
[ -f "package.json" ] && { npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true; }
- name: Run PHP tests
if: steps.detect.outputs.has_php == 'true'
run: |
if [ -f "vendor/bin/phpunit" ]; then
vendor/bin/phpunit --testdox 2>&1
echo "## PHPUnit" >> $GITHUB_STEP_SUMMARY
echo "Tests passed." >> $GITHUB_STEP_SUMMARY
elif [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then
echo "::warning::PHPUnit config found but phpunit not installed"
else
echo "No PHPUnit configured — skipping"
fi
- name: Run Node.js tests
if: steps.detect.outputs.has_node == 'true'
run: |
if jq -e '.scripts.test' package.json > /dev/null 2>&1; then
npm test 2>&1
echo "## Node.js Tests" >> $GITHUB_STEP_SUMMARY
echo "Tests passed." >> $GITHUB_STEP_SUMMARY
else
echo "No test script in package.json — skipping"
fi
- name: Build check
run: |
if [ -f "Makefile" ]; then
make build 2>&1 || echo "::warning::Build failed or not configured"
elif [ -f "package.json" ] && jq -e '.scripts.build' package.json > /dev/null 2>&1; then
npm run build 2>&1 || echo "::warning::Build failed"
fi
+87
View File
@@ -0,0 +1,87 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Maintenance
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/cleanup.yml
# VERSION: 01.00.00
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
name: "Universal: Repository Cleanup"
on:
schedule:
- cron: '0 3 * * 0' # Weekly on Sunday at 03:00 UTC
workflow_dispatch:
permissions:
contents: write
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
jobs:
cleanup:
name: Clean Merged Branches
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }}
- name: Delete merged branches
env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: |
echo "=== Merged Branch Cleanup ==="
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
# List branches via API
BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
"${API}/branches?limit=50" | jq -r '.[].name')
DELETED=0
for BRANCH in $BRANCHES; do
# Skip protected branches
case "$BRANCH" in
main|master|develop|release/*|hotfix/*) continue ;;
esac
# Check if branch is merged into main
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
echo " Deleting merged branch: ${BRANCH}"
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
"${API}/branches/${BRANCH}" 2>/dev/null || true
DELETED=$((DELETED + 1))
fi
done
echo "Deleted ${DELETED} merged branch(es)"
- name: Clean old workflow runs
env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: |
echo "=== Workflow Run Cleanup ==="
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
# Get old completed runs
RUNS=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
"${API}/actions/runs?status=completed&limit=50" | \
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
DELETED=0
for RUN_ID in $RUNS; do
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
DELETED=$((DELETED + 1))
done
echo "Deleted ${DELETED} old workflow run(s)"
+126
View File
@@ -0,0 +1,126 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Deploy
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
# PATH: /templates/workflows/joomla/deploy-manual.yml.template
# VERSION: 04.07.00
# BRIEF: Manual SFTP deploy to dev server for Joomla repos
name: "Universal: Deploy to Dev (Manual)"
on:
workflow_dispatch:
inputs:
clear_remote:
description: 'Delete all remote files before uploading'
required: false
default: 'false'
type: boolean
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: read
jobs:
deploy:
name: SFTP Deploy to Dev
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP
run: |
php -v && composer --version
- name: Setup MokoStandards tools
env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
run: |
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
/tmp/mokostandards-api 2>/dev/null || true
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi
- name: Check FTP configuration
id: check
env:
HOST: ${{ vars.DEV_FTP_HOST }}
PATH_VAR: ${{ vars.DEV_FTP_PATH }}
PORT: ${{ vars.DEV_FTP_PORT }}
run: |
if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then
echo "DEV_FTP_HOST or DEV_FTP_PATH not configured -- cannot deploy"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "host=$HOST" >> "$GITHUB_OUTPUT"
REMOTE="${PATH_VAR%/}"
echo "remote=$REMOTE" >> "$GITHUB_OUTPUT"
[ -z "$PORT" ] && PORT="22"
echo "port=$PORT" >> "$GITHUB_OUTPUT"
- name: Deploy via SFTP
if: steps.check.outputs.skip != 'true'
env:
SFTP_KEY: ${{ secrets.DEV_FTP_KEY }}
SFTP_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
SFTP_USER: ${{ vars.DEV_FTP_USERNAME }}
run: |
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ -- nothing to deploy"; exit 0; }
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
"${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \
> /tmp/sftp-config.json
if [ -n "$SFTP_KEY" ]; then
echo "$SFTP_KEY" > /tmp/deploy_key
chmod 600 /tmp/deploy_key
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
else
printf ',"password":"%s"}' "$SFTP_PASS" >> /tmp/sftp-config.json
fi
DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
[ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote)
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
else
php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
fi
rm -f /tmp/deploy_key /tmp/sftp-config.json
- name: Summary
if: always()
run: |
if [ "${{ steps.check.outputs.skip }}" = "true" ]; then
echo "### Deploy Skipped -- FTP not configured" >> $GITHUB_STEP_SUMMARY
else
echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Host | \`${{ steps.check.outputs.host }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Remote | \`${{ steps.check.outputs.remote }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Clear | ${{ inputs.clear_remote }} |" >> $GITHUB_STEP_SUMMARY
fi
+61 -184
View File
@@ -11,7 +11,7 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
version: version:
description: 'Version tag (e.g. v1.26.1+MOKO06.12.00)' description: 'Version tag'
required: true required: true
default: 'latest' default: 'latest'
environment: environment:
@@ -28,9 +28,9 @@ concurrency:
cancel-in-progress: false cancel-in-progress: false
env: env:
REGISTRY: code.mokoconsulting.tech REGISTRY: git.mokoconsulting.tech
IMAGE: mokoconsulting/mokogitea IMAGE: mokoconsulting/mokogitea
DEPLOY_HOST: code.mokoconsulting.tech DEPLOY_HOST: git.mokoconsulting.tech
DEPLOY_PORT: 2918 DEPLOY_PORT: 2918
DEPLOY_USER: mokoconsulting DEPLOY_USER: mokoconsulting
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
@@ -47,8 +47,6 @@ jobs:
- name: Determine settings - name: Determine settings
id: config id: config
run: | run: |
# On push to main, auto-deploy to production with git-derived version.
# On workflow_dispatch, use the provided inputs.
if [ "${{ github.event_name }}" = "push" ]; then if [ "${{ github.event_name }}" = "push" ]; then
VERSION=$(git describe --tags --always 2>/dev/null || echo "dev-$(git rev-parse --short HEAD)") VERSION=$(git describe --tags --always 2>/dev/null || echo "dev-$(git rev-parse --short HEAD)")
ENV="production" ENV="production"
@@ -56,217 +54,96 @@ jobs:
VERSION="${{ github.event.inputs.version }}" VERSION="${{ github.event.inputs.version }}"
ENV="${{ github.event.inputs.environment }}" ENV="${{ github.event.inputs.environment }}"
fi fi
if [ "$ENV" = "production" ]; then if [ "$ENV" = "production" ]; then
echo "compose_dir=/opt/gitea" >> $GITHUB_OUTPUT echo "compose_dir=/opt/gitea" >> $GITHUB_OUTPUT
echo "container=mokogitea" >> $GITHUB_OUTPUT echo "container=mokogitea" >> $GITHUB_OUTPUT
echo "source_dir=/opt/gitea/source" >> $GITHUB_OUTPUT echo "source_dir=/opt/gitea/source" >> $GITHUB_OUTPUT
echo "branch=main" >> $GITHUB_OUTPUT echo "branch=main" >> $GITHUB_OUTPUT
echo "tag=${VERSION}" >> $GITHUB_OUTPUT echo "tag=$VERSION" >> $GITHUB_OUTPUT
echo "instance_url=https://code.mokoconsulting.tech" >> $GITHUB_OUTPUT
else else
echo "compose_dir=/opt/gitea-dev" >> $GITHUB_OUTPUT echo "compose_dir=/opt/gitea-dev" >> $GITHUB_OUTPUT
echo "container=mokogitea-dev" >> $GITHUB_OUTPUT echo "container=mokogitea-dev" >> $GITHUB_OUTPUT
echo "source_dir=/opt/gitea-dev/source" >> $GITHUB_OUTPUT echo "source_dir=/opt/gitea-dev/source" >> $GITHUB_OUTPUT
echo "branch=dev" >> $GITHUB_OUTPUT echo "branch=dev" >> $GITHUB_OUTPUT
echo "tag=${VERSION}-dev" >> $GITHUB_OUTPUT echo "tag=$VERSION-dev" >> $GITHUB_OUTPUT
echo "instance_url=https://git.dev.mokoconsulting.tech" >> $GITHUB_OUTPUT
fi fi
- name: Enable maintenance mode - name: Write deploy key
env: env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
INSTANCE_URL: ${{ steps.config.outputs.instance_url }}
run: | run: |
echo "Enabling maintenance mode on ${INSTANCE_URL}..." mkdir -p ~/.ssh
curl -sf -X POST \ echo "$DEPLOY_KEY" > ~/.ssh/deploy_key
-H "Authorization: token ${GITEA_TOKEN}" \ chmod 600 ~/.ssh/deploy_key
-H "Content-Type: application/x-www-form-urlencoded" \
"${INSTANCE_URL}/-/admin/config" \
-d 'key=instance.maintenance_mode&value={"AdminWebAccessOnly":true}' \
|| echo "WARNING: Could not enable maintenance mode (instance may be down)"
- name: Build and deploy via SSH - name: Build and deploy via SSH
env: env:
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }} REGISTRY_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
TAG: ${{ steps.config.outputs.tag }} TAG: ${{ steps.config.outputs.tag }}
BRANCH: ${{ steps.config.outputs.branch }} BRANCH: ${{ steps.config.outputs.branch }}
SOURCE_DIR: ${{ steps.config.outputs.source_dir }} SOURCE_DIR: ${{ steps.config.outputs.source_dir }}
COMPOSE_DIR: ${{ steps.config.outputs.compose_dir }} COMPOSE_DIR: ${{ steps.config.outputs.compose_dir }}
CONTAINER: ${{ steps.config.outputs.container }} CONTAINER: ${{ steps.config.outputs.container }}
run: | run: |
mkdir -p ~/.ssh HEALTH_FMT='${{ '{{' }}.State.Health.Status${{ '}}' }}'
echo "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key IMAGE_FMT='Image: ${{ '{{' }}.Config.Image${{ '}}' }}'
chmod 600 ~/.ssh/deploy_key
SSH_CMD="ssh -i ~/.ssh/deploy_key -p ${{ env.DEPLOY_PORT }} -o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }}" 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'
$SSH_CMD "echo 'SSH connected'" echo 'Cleaning Docker build cache...'
docker builder prune -af 2>/dev/null || true
docker image prune -af 2>/dev/null || true
sudo swapoff -a && sudo swapon -a 2>/dev/null || true
free -m | head -3
# Pre-deploy cleanup: free disk and memory for the build echo 'Pulling source...'
$SSH_CMD " if [ ! -d $SOURCE_DIR/.git ]; then
echo 'Cleaning Docker build cache and unused images...' git clone -b $BRANCH https://git.mokoconsulting.tech/MokoConsulting/MokoGitea.git $SOURCE_DIR
docker builder prune -af 2>/dev/null || true fi
docker image prune -af 2>/dev/null || true cd $SOURCE_DIR
echo 'Clearing swap...' git fetch origin $BRANCH
sudo swapoff -a && sudo swapon -a 2>/dev/null || true git reset --hard origin/$BRANCH
echo 'Cleanup complete'
free -m | head -3
"
# Pull latest source echo 'Building Docker image...'
$SSH_CMD " docker build --no-cache --build-arg GOFLAGS='-p 1' \
set -e --tag ${{ env.REGISTRY }}/${{ env.IMAGE }}:$TAG \
if [ ! -d ${SOURCE_DIR}/.git ]; then --tag ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest \
git clone -b ${BRANCH} https://code.mokoconsulting.tech/MokoConsulting/MokoGitea.git ${SOURCE_DIR} -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
docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest
echo 'Restarting container...'
cd $COMPOSE_DIR
sed -i 's|${{ env.IMAGE }}:[^ ]*|${{ env.IMAGE }}:$TAG|' docker-compose.yml
docker compose up -d $CONTAINER
echo 'Health check...'
for i in 1 2 3 4 5 6 7 8; do
sleep 15
if docker inspect --format='$HEALTH_FMT' $CONTAINER 2>/dev/null | grep -q healthy; then
echo 'Container healthy!'
docker inspect --format='$IMAGE_FMT' $CONTAINER
exit 0
fi fi
cd ${SOURCE_DIR} echo "Waiting... (attempt \$i/8)"
git fetch origin ${BRANCH} done
git reset --hard origin/${BRANCH} echo 'Health check failed'
" docker logs $CONTAINER --tail 20
exit 1
# Build Docker image DEPLOY_EOF
$SSH_CMD "
set -e
cd ${SOURCE_DIR}
docker build --no-cache --build-arg GOFLAGS='-p 1' \
--tag ${{ env.REGISTRY }}/${{ env.IMAGE }}:${TAG} \
--tag ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest \
-f Dockerfile .
"
# Push to container registry
$SSH_CMD "
set -e
docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:${TAG}
docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest
"
# Update compose and restart
$SSH_CMD "
set -e
cd ${COMPOSE_DIR}
sed -i 's|${{ env.IMAGE }}:[^ ]*|${{ env.IMAGE }}:${TAG}|' docker-compose.yml
docker compose up -d ${CONTAINER}
"
# Health check
$SSH_CMD "
for i in 1 2 3 4 5 6 7 8; do
sleep 15
if docker inspect --format='{{.State.Health.Status}}' ${CONTAINER} 2>/dev/null | grep -q healthy; then
echo 'Container healthy!'
docker inspect --format='Image: {{.Config.Image}}' ${CONTAINER}
exit 0
fi
echo \"Waiting... (attempt \$i/8)\"
done
echo 'Health check failed'
docker logs ${CONTAINER} --tail 20
exit 1
"
- name: Update updates.xml
if: success()
env:
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
TAG: ${{ steps.config.outputs.tag }}
INSTANCE_URL: ${{ steps.config.outputs.instance_url }}
DEPLOY_ENV: ${{ github.event.inputs.environment || 'production' }}
run: |
# Only update updates.xml for production stable releases
if [ "$DEPLOY_ENV" != "production" ]; then
echo "Skipping updates.xml — dev deployments don't update stable channel"
exit 0
fi
# Extract project version by stripping the version prefix from the tag.
# Reads prefix from manifest API (e.g. "v1.26.1+MOKO"), falls back to legacy pattern.
API_BASE="https://${REGISTRY}/api/v1/repos/MokoConsulting/MokoGitea"
PREFIX=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
"${API_BASE}/manifest" | python3 -c "import json,sys; print(json.load(sys.stdin).get('version_prefix',''))" 2>/dev/null || true)
if [ -n "$PREFIX" ]; then
MOKO_VER="${TAG#$PREFIX}"
else
# Legacy fallback: strip everything up to and including "-moko."
MOKO_VER=$(echo "$TAG" | sed -n 's/.*-moko\.\(.*\)/\1/p')
fi
if [ -z "$MOKO_VER" ]; then
echo "Could not extract version from tag: $TAG (prefix: ${PREFIX:-none})"
exit 0
fi
RELEASE_URL="https://${REGISTRY}/MokoConsulting/MokoGitea/releases/tag/${TAG}"
DOCKER_IMG="${REGISTRY}/${IMAGE}:${TAG}"
python3 << PYEOF
import json, os, re, base64, urllib.request
token = os.environ["GITEA_TOKEN"]
registry = os.environ["REGISTRY"]
tag = os.environ["TAG"]
moko_ver = os.environ["MOKO_VER"]
release_url = os.environ["RELEASE_URL"]
docker_img = os.environ["DOCKER_IMG"]
api = f"https://{registry}/api/v1/repos/MokoConsulting/MokoGitea"
# Fetch current updates.xml
req = urllib.request.Request(f"{api}/contents/updates.xml?ref=main",
headers={"Authorization": f"token {token}"})
with urllib.request.urlopen(req) as resp:
data = json.loads(resp.read())
sha = data["sha"]
content = base64.b64decode(data["content"]).decode("utf-8")
# Update stable channel — match the <update> block containing <tag>stable</tag>
def replace_channel(xml, channel, ver, url, docker):
pattern = rf"(<update>\s*<name>MokoGitea</name>[\s\S]*?<tags><tag>{channel}</tag></tags>[\s\S]*?</update>)"
def replacer(m):
block = m.group(1)
block = re.sub(r"<version>[^<]*</version>", f"<version>{ver}</version>", block)
block = re.sub(r"(<infourl[^>]*>)[^<]*(</infourl>)", rf"\1{url}\2", block)
block = re.sub(r"(<downloadurl[^>]*>)[^<]*(</downloadurl>)", rf"\1{docker}\2", block)
return block
return re.sub(pattern, replacer, xml)
content = replace_channel(content, "stable", moko_ver, release_url, docker_img)
content = re.sub(r"VERSION: [^\n]*", f"VERSION: {moko_ver}", content)
# Push updated file
encoded = base64.b64encode(content.encode()).decode()
payload = json.dumps({
"message": f"chore(ci): update updates.xml to {moko_ver}",
"content": encoded,
"sha": sha,
"branch": "main",
}).encode()
req = urllib.request.Request(f"{api}/contents/updates.xml",
data=payload, method="PUT",
headers={"Authorization": f"token {token}", "Content-Type": "application/json"})
with urllib.request.urlopen(req) as resp:
print(f"updates.xml updated to {moko_ver}")
PYEOF
- name: Disable maintenance mode
if: always()
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
INSTANCE_URL: ${{ steps.config.outputs.instance_url }}
run: |
echo "Disabling maintenance mode on ${INSTANCE_URL}..."
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/x-www-form-urlencoded" \
"${INSTANCE_URL}/-/admin/config" \
-d 'key=instance.maintenance_mode&value={"AdminWebAccessOnly":false}' \
|| echo "WARNING: Could not disable maintenance mode"
- name: Verify - name: Verify
run: | run: |
sleep 5 sleep 5
curl -sf https://${{ env.DEPLOY_HOST }}/api/healthz && echo " API healthy" curl -sf https://${{ env.DEPLOY_HOST }}/api/healthz && echo " API healthy"
- name: Notify on failure - name: Notify on failure
if: failure() if: failure()
+96
View File
@@ -0,0 +1,96 @@
# 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-tech/MokoStandards-API
# PATH: /templates/workflows/gitleaks.yml.template
# VERSION: 01.00.00
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
#
# +========================================================================+
# | SECRET SCANNING |
# +========================================================================+
# | |
# | Scans commits for leaked secrets using Gitleaks. |
# | |
# | - PR scan: only new commits in the PR |
# | - Scheduled: full repo scan weekly |
# | - Alerts via ntfy on findings |
# | |
# +========================================================================+
name: "Universal: Secret Scanning"
on:
pull_request:
branches:
- main
- 'dev/**'
schedule:
- cron: '0 5 * * 1' # Weekly Monday 05:00 UTC
workflow_dispatch:
permissions:
contents: read
env:
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
jobs:
gitleaks:
name: Gitleaks Secret Scan
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Gitleaks
run: |
GITLEAKS_VERSION="8.21.2"
curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
| tar -xz -C /usr/local/bin gitleaks
gitleaks version
- name: Scan for secrets
id: scan
run: |
echo "### Secret Scanning" >> $GITHUB_STEP_SUMMARY
ARGS="--source . --verbose --report-format json --report-path /tmp/gitleaks-report.json"
if [ "${{ github.event_name }}" = "pull_request" ]; then
# Scan only PR commits
ARGS="$ARGS --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}"
echo "Scanning PR commits only" >> $GITHUB_STEP_SUMMARY
else
echo "Full repository scan" >> $GITHUB_STEP_SUMMARY
fi
if gitleaks detect $ARGS 2>&1; then
echo "result=clean" >> "$GITHUB_OUTPUT"
echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY
else
echo "result=found" >> "$GITHUB_OUTPUT"
FINDINGS=$(jq length /tmp/gitleaks-report.json 2>/dev/null || echo "unknown")
echo "**${FINDINGS} potential secret(s) detected.**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Review the findings and rotate any exposed credentials immediately." >> $GITHUB_STEP_SUMMARY
exit 1
fi
- name: Notify on findings
if: failure() && steps.scan.outputs.result == 'found'
run: |
REPO="${{ github.event.repository.name }}"
curl -sS \
-H "Title: ${REPO} — secrets detected in code" \
-H "Tags: rotating_light,key" \
-H "Priority: urgent" \
-d "Gitleaks found potential secrets. Review and rotate credentials immediately." \
"${NTFY_URL}/${NTFY_TOPIC}" || true
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokoplatform.Automation # INGROUP: mokoplatform.Automation
# VERSION: 06.12.02 # VERSION: 06.14.00
# BRIEF: Auto-create feature branch when an issue is opened # BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch" name: "Universal: Issue Branch"
+70
View File
@@ -0,0 +1,70 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Notifications
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/notify.yml
# VERSION: 01.00.00
# BRIEF: Push notifications via ntfy on release success or workflow failure
name: "Universal: Notifications"
on:
workflow_run:
workflows:
- "Joomla Build & Release"
- "Joomla Extension CI"
- "Deploy"
types:
- completed
permissions:
contents: read
env:
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-releases' }}
jobs:
notify:
name: Send Notification
runs-on: ubuntu-latest
if: >-
github.event.workflow_run.conclusion == 'success' ||
github.event.workflow_run.conclusion == 'failure'
steps:
- name: Notify on success (releases only)
if: >-
github.event.workflow_run.conclusion == 'success' &&
contains(github.event.workflow_run.name, 'Release')
run: |
REPO="${{ github.event.repository.name }}"
WORKFLOW="${{ github.event.workflow_run.name }}"
URL="${{ github.event.workflow_run.html_url }}"
curl -sS \
-H "Title: ${REPO} released" \
-H "Tags: white_check_mark,package" \
-H "Priority: default" \
-H "Click: ${URL}" \
-d "${WORKFLOW} completed successfully." \
"${NTFY_URL}/${NTFY_TOPIC}"
- name: Notify on failure
if: github.event.workflow_run.conclusion == 'failure'
run: |
REPO="${{ github.event.repository.name }}"
WORKFLOW="${{ github.event.workflow_run.name }}"
URL="${{ github.event.workflow_run.html_url }}"
curl -sS \
-H "Title: ${REPO} workflow failed" \
-H "Tags: x,warning" \
-H "Priority: high" \
-H "Click: ${URL}" \
-d "${WORKFLOW} failed. Check the run for details." \
"${NTFY_URL}/${NTFY_TOPIC}"
+1 -1
View File
@@ -48,4 +48,4 @@ jobs:
working-directory: .mokogitea/mcp working-directory: .mokogitea/mcp
run: | run: |
npm publish --registry ${{ github.server_url }}/api/packages/${{ github.repository_owner }}/npm/ \ npm publish --registry ${{ github.server_url }}/api/packages/${{ github.repository_owner }}/npm/ \
--//$(echo "${{ github.server_url }}" | sed 's|https://||')/api/packages/${{ github.repository_owner }}/npm/:_authToken=${{ secrets.GITEA_TOKEN }} --//$(echo "${{ github.server_url }}" | sed 's|https://||')/api/packages/${{ github.repository_owner }}/npm/:_authToken=${{ secrets.MOKOGITEA_TOKEN }}
File diff suppressed because it is too large Load Diff
+3 -237
View File
@@ -4,242 +4,8 @@
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokoplatform.Release # INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /templates/workflows/universal/pre-release.yml.template # PATH: /templates/workflows/universal/pre-release.yml.template
# VERSION: 05.01.00 # VERSION: 05.01.00
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches # BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
name: "Universal: Pre-Release"
on:
push:
branches:
- dev
- alpha
- beta
- rc
workflow_dispatch:
inputs:
stability:
description: 'Pre-release channel'
required: true
type: choice
options:
- development
- alpha
- beta
- release-candidate
permissions:
contents: write
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
jobs:
build:
name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})"
runs-on: release
if: >-
github.event_name == 'workflow_dispatch' ||
github.event_name == 'push'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }}
ref: ${{ github.ref_name }}
- name: Setup mokoplatform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
# Use pre-installed /opt/mokoplatform if available (updated by cron every 6h)
if [ -f /opt/mokoplatform/cli/version_bump.php ] && [ -f /opt/mokoplatform/cli/manifest_element.php ] && [ -f /opt/mokoplatform/vendor/autoload.php ]; then
echo Using pre-installed /opt/mokoplatform
echo MOKO_CLI=/opt/mokoplatform/cli >> $GITHUB_ENV
else
echo Falling back to fresh clone
if ! command -v composer > /dev/null 2>&1; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
fi
rm -rf /tmp/mokoplatform-api
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokoplatform.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokoplatform-api
cd /tmp/mokoplatform-api && composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/mokoplatform-api/cli >> $GITHUB_ENV
fi
- name: Detect platform
id: platform
run: |
php ${MOKO_CLI}/manifest_read.php --path . --github-output
- name: Resolve metadata and bump version
id: meta
run: |
# Auto-detect stability from branch name on push, or use input on dispatch
if [ "${{ github.event_name }}" = "push" ]; then
case "${{ github.ref_name }}" in
rc) STABILITY="release-candidate" ;;
alpha) STABILITY="alpha" ;;
beta) STABILITY="beta" ;;
*) STABILITY="development" ;;
esac
else
STABILITY="${{ inputs.stability || 'development' }}"
fi
case "$STABILITY" in
development) SUFFIX="-dev"; TAG="development" ;;
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
beta) SUFFIX="-beta"; TAG="beta" ;;
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
esac
# Bump version via CLI: patch for dev/alpha/beta, minor for RC
case "$STABILITY" in
release-candidate) BUMP="minor" ;;
*) BUMP="patch" ;;
esac
php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true
# Set stability suffix and verify consistency
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01")
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
php ${MOKO_CLI}/version_set_platform.php \
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
# Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml
php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true
# Append suffix for output
if [ -n "$SUFFIX" ]; then
VERSION="${VERSION}${SUFFIX}"
fi
# Commit version bump
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git add -A
git diff --cached --quiet || {
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
git push origin HEAD 2>&1
}
# Auto-detect element via manifest_element.php
php ${MOKO_CLI}/manifest_element.php \
--path . --version "$VERSION" --stability "$STABILITY" \
--repo "${GITEA_REPO}" --github-output
# Read back element outputs
EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
[ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
- name: Create release
id: release
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_create.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
- name: Update release notes from CHANGELOG.md
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
if [ -f "CHANGELOG.md" ]; then
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
else
NOTES="Release ${VERSION}"
fi
# Update release body via API
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
"${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -n "$RELEASE_ID" ]; then
python3 -c "
import json, urllib.request
body = open('/dev/stdin').read()
payload = json.dumps({'body': body}).encode()
req = urllib.request.Request(
'${API_BASE}/releases/${RELEASE_ID}',
data=payload, method='PATCH',
headers={
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
'Content-Type': 'application/json'
})
urllib.request.urlopen(req)
" <<< "$NOTES"
echo "Release notes updated from CHANGELOG.md"
fi
- name: Build package and upload
id: package
run: |
VERSION="${{ steps.meta.outputs.version }}"
TAG="${{ steps.meta.outputs.tag }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_package.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --output /tmp || true
# updates.xml is generated dynamically by MokoGitea license server
# No need to build, commit, or sync updates.xml from workflows
- name: "Delete lesser pre-release channels (cascade)"
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
php ${MOKO_CLI}/release_cascade.php \
--stability "${{ steps.meta.outputs.stability }}" \
--token "${TOKEN}" \
--api-base "${API_BASE}"
- name: Summary
if: always()
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
SHA256="${{ steps.package.outputs.sha256_zip }}"
echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
File diff suppressed because it is too large Load Diff
+82
View File
@@ -0,0 +1,82 @@
# 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
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
- name: Sync upstream bugs - name: Sync upstream bugs
env: env:
GH_TOKEN: ${{ secrets.GH_MIRROR_TOKEN }} GH_TOKEN: ${{ secrets.GH_MIRROR_TOKEN }}
MOKOGITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKOGITEA_URL: https://code.mokoconsulting.tech MOKOGITEA_URL: https://code.mokoconsulting.tech
MOKOGITEA_REPO: MokoConsulting/MokoGitea MOKOGITEA_REPO: MokoConsulting/MokoGitea
UPSTREAM_BRANCH: release/v1.26 UPSTREAM_BRANCH: release/v1.26
+17 -44
View File
@@ -1,8 +1,25 @@
# Changelog # Changelog
## [Unreleased]
All notable changes to MokoGitea are documented here. Versions follow the format All notable changes to MokoGitea are documented here. Versions follow the format
`v{upstream}-moko.{major}.{minor}` (e.g. `v1.26.1-moko.06.03`). `v{upstream}-moko.{major}.{minor}` (e.g. `v1.26.1-moko.06.03`).
## [06.14.00] --- 2026-06-09
* FEATURES
* feat(api): issue status/priority/type exposed in REST API - GET/PATCH on issues now includes status_id, priority_id, type_id with resolved names
* feat(api): org-level issue metadata endpoints - GET /orgs/{org}/issue-statuses, /issue-priorities, /issue-types
* feat(wiki): org wiki tab - inline wiki rendering from convention repos (wiki / wiki-private)
* feat(wiki): public/private wiki toggle dropdown (same UX as org profile README selector)
* feat(wiki): external wiki support - link to an outside URL from the org wiki tab
* feat(settings): wiki mode setting in org settings (internal repos vs external URL)
* feat(mcp): 5 new MCP tools - gitea_org_issue_statuses_list, gitea_org_issue_priorities_list, gitea_org_issue_types_list, gitea_issue_set_status, gitea_issue_set_priority
* feat(mcp): gitea_issue_create and gitea_issue_update now accept status_id, priority_id, type_id
* MIGRATIONS
* migration 354: add wiki_mode and wiki_url columns to user table for org wiki settings
## [v1.26.1-moko.06.12] - 2026-06-07 ## [v1.26.1-moko.06.12] - 2026-06-07
* FEATURES * FEATURES
@@ -179,47 +196,3 @@ All notable changes to MokoGitea are documented here. Versions follow the format
* fix(updates): correct dlid prefix and align XML with Joomla standard * fix(updates): correct dlid prefix and align XML with Joomla standard
* INFRASTRUCTURE * INFRASTRUCTURE
* fix(ci): auto-deploy to production on merge to main (#235) * fix(ci): auto-deploy to production on merge to main (#235)
## [v1.26.1-moko.04] - 2026-05-24
* SECURITY
* Backport 12 upstream v1.26.2 security fixes:
* golang.org/x/net v0.55.0 security update (#140)
* Token scope enforcement on raw/media/attachment downloads (#141)
* OAuth PKCE hardening and refresh token replay protection (#142)
* Wiki git write and LFS token access enforcement (#143)
* Public-only token filtering in API queries (#144)
* Artifact signature payload hardening (#146)
* AWS credentials encryption (#161)
* Mermaid v11.15.0 security update (#162)
* Composer package permission check (#164)
* BUGFIXES
* fix(actions): nil pointer dereference in concurrency during PR creation (#136)
* fix(ui): actions runs list broken row layout (#138)
* fix: scheduled action panic with null event payload
* fix: treat email addresses case-insensitively
* fix: .mod lexer panic — removed invalid AMPL mapping
* FEATURES
* Joomla-style updates.xml with channel selection
* Update checker with configurable CHANNEL setting
* Admin dashboard update banner with docker pull command
* Upstream bug sync workflow — daily automated issue creation
* PR RC release workflow — auto-build RC on PR to main
* INFRASTRUCTURE
* New 3-part versioning: v{upstream}-moko.{major}.{minor}.{patch}
* Branding updates: error pages, home page, settings link
* Deploy workflow updated for new version format
* PROCESS
* Created `type: bug` and `upstream` labels for automated issue tracking
* Closed 24 upstream bug/security issues after backporting
## [v1.26.1-moko.03] - 2026-05-15
* FEATURES
* feat(api): Bulk issue operations — add/remove/replace labels, close/reopen, set milestone, assignees (#21)
* INFRASTRUCTURE
* Grafana: Standardized kiosk header across all 14 playlist dashboards
* PROCESS
* Reopened 9 closed issues lacking documented testing proof
* Created `pending: testing` label for features awaiting verification
* Established policy: issues must not be closed without documented testing proof
+3
View File
@@ -430,6 +430,9 @@ func prepareMigrationTasks() []*migration {
newMigration(350, "Add issue type definitions table", v1_27.AddIssueTypeDefTable), newMigration(350, "Add issue type definitions table", v1_27.AddIssueTypeDefTable),
newMigration(351, "Add CDN public flag to attachments", v1_27.AddAttachmentCDNPublic), newMigration(351, "Add CDN public flag to attachments", v1_27.AddAttachmentCDNPublic),
newMigration(352, "Add version prefix and element name to repo manifest", v1_27.AddManifestVersionPrefixAndElement), newMigration(352, "Add version prefix and element name to repo manifest", v1_27.AddManifestVersionPrefixAndElement),
newMigration(353, "Add distribution metadata fields to repo manifest", v1_27.AddManifestDistributionFields),
newMigration(354, "Add org wiki settings to user table", v1_27.AddOrgWikiSettings),
newMigration(355, "Migrate update server metadata to repo manifest", v1_27.MigrateUpdateServerFieldsToManifest),
} }
return preparedMigrations return preparedMigrations
} }
+20
View File
@@ -0,0 +1,20 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import "xorm.io/xorm"
// AddManifestDistributionFields adds distribution metadata fields to repo_manifest
// for update server feed generation (consolidating from UpdateStreamConfig).
func AddManifestDistributionFields(x *xorm.Engine) error {
type RepoManifest struct {
DisplayName string `xorm:"TEXT 'display_name'"`
Maintainer string `xorm:"TEXT 'maintainer'"`
MaintainerURL string `xorm:"TEXT 'maintainer_url'"`
InfoURL string `xorm:"TEXT 'info_url'"`
TargetVersion string `xorm:"TEXT 'target_version'"`
PHPMinimum string `xorm:"VARCHAR(20) 'php_minimum'"`
}
return x.Sync(new(RepoManifest))
}
+16
View File
@@ -0,0 +1,16 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import "xorm.io/xorm"
// AddOrgWikiSettings adds wiki_mode and wiki_url columns to the user table
// for configuring org-level wiki behavior (internal convention repos vs external link).
func AddOrgWikiSettings(x *xorm.Engine) error {
type User struct {
WikiMode string `xorm:"VARCHAR(20) NOT NULL DEFAULT '' 'wiki_mode'"`
WikiURL string `xorm:"TEXT 'wiki_url'"`
}
return x.Sync(new(User))
}
+108
View File
@@ -0,0 +1,108 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import "xorm.io/xorm"
// MigrateUpdateServerFieldsToManifest copies extension metadata from
// update_stream_config into repo_manifest where the manifest fields are empty.
// This consolidates the source of truth into repo_manifest.
func MigrateUpdateServerFieldsToManifest(x *xorm.Engine) error {
// Copy display_name from config to manifest where manifest is empty
_, err := x.Exec(`
UPDATE repo_manifest m
INNER JOIN update_stream_config c ON m.repo_id = c.repo_id
SET m.display_name = c.display_name
WHERE (m.display_name IS NULL OR m.display_name = '') AND c.display_name != ''
`)
if err != nil {
return err
}
// Copy extension_name → element_name
_, err = x.Exec(`
UPDATE repo_manifest m
INNER JOIN update_stream_config c ON m.repo_id = c.repo_id
SET m.element_name = c.extension_name
WHERE (m.element_name IS NULL OR m.element_name = '') AND c.extension_name != ''
`)
if err != nil {
return err
}
// Copy extension_type → package_type
_, err = x.Exec(`
UPDATE repo_manifest m
INNER JOIN update_stream_config c ON m.repo_id = c.repo_id
SET m.package_type = c.extension_type
WHERE (m.package_type IS NULL OR m.package_type = '') AND c.extension_type != ''
`)
if err != nil {
return err
}
// Copy target_version
_, err = x.Exec(`
UPDATE repo_manifest m
INNER JOIN update_stream_config c ON m.repo_id = c.repo_id
SET m.target_version = c.target_version
WHERE (m.target_version IS NULL OR m.target_version = '') AND c.target_version != ''
`)
if err != nil {
return err
}
// Copy maintainer
_, err = x.Exec(`
UPDATE repo_manifest m
INNER JOIN update_stream_config c ON m.repo_id = c.repo_id
SET m.maintainer = c.maintainer
WHERE (m.maintainer IS NULL OR m.maintainer = '') AND c.maintainer != ''
`)
if err != nil {
return err
}
// Copy maintainer_url
_, err = x.Exec(`
UPDATE repo_manifest m
INNER JOIN update_stream_config c ON m.repo_id = c.repo_id
SET m.maintainer_url = c.maintainer_url
WHERE (m.maintainer_url IS NULL OR m.maintainer_url = '') AND c.maintainer_url != ''
`)
if err != nil {
return err
}
// Copy info_url
_, err = x.Exec(`
UPDATE repo_manifest m
INNER JOIN update_stream_config c ON m.repo_id = c.repo_id
SET m.info_url = c.info_url
WHERE (m.info_url IS NULL OR m.info_url = '') AND c.info_url != ''
`)
if err != nil {
return err
}
// Copy php_minimum
_, err = x.Exec(`
UPDATE repo_manifest m
INNER JOIN update_stream_config c ON m.repo_id = c.repo_id
SET m.php_minimum = c.php_minimum
WHERE (m.php_minimum IS NULL OR m.php_minimum = '') AND c.php_minimum != ''
`)
if err != nil {
return err
}
// Copy platform from config to manifest where manifest platform is empty
_, err = x.Exec(`
UPDATE repo_manifest m
INNER JOIN update_stream_config c ON m.repo_id = c.repo_id
SET m.platform = c.platform
WHERE (m.platform IS NULL OR m.platform = '') AND c.platform != ''
`)
return err
}
+8
View File
@@ -38,6 +38,14 @@ type RepoManifest struct {
VersionPrefix string `xorm:"TEXT 'version_prefix'"` // tag prefix stripped for version display, e.g. "v1.26.1-moko." VersionPrefix string `xorm:"TEXT 'version_prefix'"` // tag prefix stripped for version display, e.g. "v1.26.1-moko."
ElementName string `xorm:"TEXT 'element_name'"` // full element name override, e.g. "pkg_mokowaas" (auto-constructed if empty) ElementName string `xorm:"TEXT 'element_name'"` // full element name override, e.g. "pkg_mokowaas" (auto-constructed if empty)
// distribution metadata (used by update server feed generation)
DisplayName string `xorm:"TEXT 'display_name'"` // human-readable name for update feeds, e.g. "Package - MokoWaaS"
Maintainer string `xorm:"TEXT 'maintainer'"` // maintainer/author name
MaintainerURL string `xorm:"TEXT 'maintainer_url'"` // maintainer website
InfoURL string `xorm:"TEXT 'info_url'"` // extension info/product page URL
TargetVersion string `xorm:"TEXT 'target_version'"` // target platform version regex, e.g. "(5|6)\..*"
PHPMinimum string `xorm:"VARCHAR(20) 'php_minimum'"` // minimum PHP version, e.g. "8.1"
// build section // build section
Language string `xorm:"VARCHAR(50) 'language'"` // Go, PHP, TypeScript, etc. Language string `xorm:"VARCHAR(50) 'language'"` // Go, PHP, TypeScript, etc.
PackageType string `xorm:"VARCHAR(50) 'package_type'"` // application, library, plugin, module, component, package PackageType string `xorm:"VARCHAR(50) 'package_type'"` // application, library, plugin, module, component, package
@@ -1,7 +1,7 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech> // Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package licenses package updateserver
import ( import (
"context" "context"
@@ -1,7 +1,7 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech> // Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package licenses package updateserver
import ( import (
"context" "context"
@@ -1,7 +1,7 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech> // Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package licenses package updateserver
import ( import (
"context" "context"
@@ -1,7 +1,7 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech> // Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package licenses package updateserver
import ( import (
"context" "context"
@@ -1,7 +1,7 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech> // Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package licenses package updateserver
import ( import (
"context" "context"
@@ -1,7 +1,7 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech> // Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package licenses package updateserver
import ( import (
"context" "context"
+2
View File
@@ -153,6 +153,8 @@ type User struct {
Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 0"` Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 0"`
RepoAdminChangeTeamAccess bool `xorm:"NOT NULL DEFAULT false"` RepoAdminChangeTeamAccess bool `xorm:"NOT NULL DEFAULT false"`
ParentOrgID int64 `xorm:"INDEX DEFAULT 0"` // 0 = no parent (top-level org) ParentOrgID int64 `xorm:"INDEX DEFAULT 0"` // 0 = no parent (top-level org)
WikiMode string `xorm:"VARCHAR(20) NOT NULL DEFAULT '' 'wiki_mode'"` // "" = internal (convention repos), "external" = link to WikiURL
WikiURL string `xorm:"TEXT 'wiki_url'"` // external wiki URL (used when WikiMode == "external")
// Preferences // Preferences
DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"` DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"`
+49
View File
@@ -84,6 +84,14 @@ type Issue struct {
PinOrder int `json:"pin_order"` PinOrder int `json:"pin_order"`
// The version of the issue content for optimistic locking // The version of the issue content for optimistic locking
ContentVersion int `json:"content_version"` ContentVersion int `json:"content_version"`
// Issue metadata (org-level definitions)
StatusID int64 `json:"status_id"`
StatusName string `json:"status_name"`
PriorityID int64 `json:"priority_id"`
PriorityName string `json:"priority_name"`
TypeID int64 `json:"type_id"`
TypeName string `json:"type_name"`
} }
// CreateIssueOption options to create one issue // CreateIssueOption options to create one issue
@@ -106,6 +114,10 @@ type CreateIssueOption struct {
Closed bool `json:"closed"` Closed bool `json:"closed"`
// custom field values keyed by field name // custom field values keyed by field name
CustomFields map[string]string `json:"custom_fields,omitempty"` CustomFields map[string]string `json:"custom_fields,omitempty"`
// org-level issue metadata IDs
StatusID *int64 `json:"status_id,omitempty"`
PriorityID *int64 `json:"priority_id,omitempty"`
TypeID *int64 `json:"type_id,omitempty"`
} }
// EditIssueOption options for editing an issue // EditIssueOption options for editing an issue
@@ -125,6 +137,10 @@ type EditIssueOption struct {
RemoveDeadline *bool `json:"unset_due_date"` RemoveDeadline *bool `json:"unset_due_date"`
// The current version of the issue content to detect conflicts during editing // The current version of the issue content to detect conflicts during editing
ContentVersion *int `json:"content_version"` ContentVersion *int `json:"content_version"`
// org-level issue metadata IDs
StatusID *int64 `json:"status_id,omitempty"`
PriorityID *int64 `json:"priority_id,omitempty"`
TypeID *int64 `json:"type_id,omitempty"`
} }
// EditDeadlineOption options for creating a deadline // EditDeadlineOption options for creating a deadline
@@ -141,6 +157,39 @@ type IssueDeadline struct {
Deadline *time.Time `json:"due_date"` Deadline *time.Time `json:"due_date"`
} }
// IssueStatusDef represents an org-level issue status definition
// swagger:model
type IssueStatusDef struct {
ID int64 `json:"id"`
Name string `json:"name"`
Color string `json:"color"`
Description string `json:"description"`
ClosesIssue bool `json:"closes_issue"`
SortOrder int `json:"sort_order"`
}
// IssuePriorityDef represents an org-level issue priority definition
// swagger:model
type IssuePriorityDef struct {
ID int64 `json:"id"`
Name string `json:"name"`
Color string `json:"color"`
Description string `json:"description"`
SortOrder int `json:"sort_order"`
IsDefault bool `json:"is_default"`
}
// IssueTypeDef represents an org-level issue type definition
// swagger:model
type IssueTypeDef struct {
ID int64 `json:"id"`
Name string `json:"name"`
Color string `json:"color"`
Description string `json:"description"`
SortOrder int `json:"sort_order"`
IsDefault bool `json:"is_default"`
}
// IssueFormFieldType defines issue form field type, can be "markdown", "textarea", "input", "dropdown" or "checkboxes" // IssueFormFieldType defines issue form field type, can be "markdown", "textarea", "input", "dropdown" or "checkboxes"
// //
// swagger:enum IssueFormFieldType // swagger:enum IssueFormFieldType
+7
View File
@@ -2756,6 +2756,13 @@
"repo.settings.manifest_build": "Build", "repo.settings.manifest_build": "Build",
"repo.settings.manifest_language": "Language", "repo.settings.manifest_language": "Language",
"repo.settings.manifest_package_type": "Package Type", "repo.settings.manifest_package_type": "Package Type",
"repo.settings.manifest_distribution": "Distribution",
"repo.settings.manifest_display_name": "Display Name",
"repo.settings.manifest_maintainer": "Maintainer",
"repo.settings.manifest_maintainer_url": "Maintainer URL",
"repo.settings.manifest_info_url": "Info / Product URL",
"repo.settings.manifest_target_version": "Target Platform Version",
"repo.settings.manifest_php_minimum": "Minimum PHP Version",
"repo.settings.manifest_entry_point": "Entry Point", "repo.settings.manifest_entry_point": "Entry Point",
"repo.settings.manifest_save": "Save Manifest", "repo.settings.manifest_save": "Save Manifest",
"repo.settings.manifest_saved": "Manifest settings saved.", "repo.settings.manifest_saved": "Manifest settings saved.",
+7 -1
View File
@@ -1479,7 +1479,10 @@ func Routes() *web.Router {
Delete(reqToken(), repo.DeleteTopic) Delete(reqToken(), repo.DeleteTopic)
}, reqAdmin()) }, reqAdmin())
}, reqAnyRepoReader()) }, reqAnyRepoReader())
m.Combo("/manifest", reqRepoReader(unit.TypeCode)). m.Combo("/metadata", reqRepoReader(unit.TypeCode)).
Get(repo.GetRepoManifest).
Put(reqToken(), reqAdmin(), repo.UpdateRepoManifest)
m.Combo("/manifest", reqRepoReader(unit.TypeCode)). // backward compat
Get(repo.GetRepoManifest). Get(repo.GetRepoManifest).
Put(reqToken(), reqAdmin(), repo.UpdateRepoManifest) Put(reqToken(), reqAdmin(), repo.UpdateRepoManifest)
// MokoGitea badge engine // MokoGitea badge engine
@@ -1773,6 +1776,9 @@ func Routes() *web.Router {
m.Post("", reqToken(), reqOrgOwnership(), org.CreateOrgCustomField) m.Post("", reqToken(), reqOrgOwnership(), org.CreateOrgCustomField)
m.Delete("/{id}", reqToken(), reqOrgOwnership(), org.DeleteOrgCustomField) m.Delete("/{id}", reqToken(), reqOrgOwnership(), org.DeleteOrgCustomField)
}) })
m.Get("/issue-statuses", org.ListIssueStatuses)
m.Get("/issue-priorities", org.ListIssuePriorities)
m.Get("/issue-types", org.ListIssueTypes)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true), checkTokenPublicOnly()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true), checkTokenPublicOnly())
m.Group("/teams/{teamid}", func() { m.Group("/teams/{teamid}", func() {
m.Combo("").Get(reqToken(), org.GetTeam). m.Combo("").Get(reqToken(), org.GetTeam).
+138
View File
@@ -0,0 +1,138 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package org
import (
"net/http"
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
api "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
// ListIssueStatuses returns active issue status definitions for an org.
func ListIssueStatuses(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/issue-statuses organization orgListIssueStatuses
// ---
// summary: List an organization's issue status definitions
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "200":
// description: "IssueStatusDefList"
// schema:
// type: array
// items:
// "$ref": "#/definitions/IssueStatusDef"
// "404":
// "$ref": "#/responses/notFound"
defs, err := issues_model.GetIssueStatusDefsByOrg(ctx, ctx.Org.Organization.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
result := make([]*api.IssueStatusDef, 0, len(defs))
for _, d := range defs {
result = append(result, &api.IssueStatusDef{
ID: d.ID,
Name: d.Name,
Color: d.Color,
Description: d.Description,
ClosesIssue: d.ClosesIssue,
SortOrder: d.SortOrder,
})
}
ctx.JSON(http.StatusOK, result)
}
// ListIssuePriorities returns active issue priority definitions for an org.
func ListIssuePriorities(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/issue-priorities organization orgListIssuePriorities
// ---
// summary: List an organization's issue priority definitions
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "200":
// description: "IssuePriorityDefList"
// schema:
// type: array
// items:
// "$ref": "#/definitions/IssuePriorityDef"
// "404":
// "$ref": "#/responses/notFound"
defs, err := issues_model.GetIssuePriorityDefsByOrg(ctx, ctx.Org.Organization.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
result := make([]*api.IssuePriorityDef, 0, len(defs))
for _, d := range defs {
result = append(result, &api.IssuePriorityDef{
ID: d.ID,
Name: d.Name,
Color: d.Color,
Description: d.Description,
SortOrder: d.SortOrder,
IsDefault: d.IsDefault,
})
}
ctx.JSON(http.StatusOK, result)
}
// ListIssueTypes returns active issue type definitions for an org.
func ListIssueTypes(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/issue-types organization orgListIssueTypes
// ---
// summary: List an organization's issue type definitions
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "200":
// description: "IssueTypeDefList"
// schema:
// type: array
// items:
// "$ref": "#/definitions/IssueTypeDef"
// "404":
// "$ref": "#/responses/notFound"
defs, err := issues_model.GetIssueTypeDefsByOrg(ctx, ctx.Org.Organization.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
result := make([]*api.IssueTypeDef, 0, len(defs))
for _, d := range defs {
result = append(result, &api.IssueTypeDef{
ID: d.ID,
Name: d.Name,
Color: d.Color,
Description: d.Description,
SortOrder: d.SortOrder,
IsDefault: d.IsDefault,
})
}
ctx.JSON(http.StatusOK, result)
}
+60
View File
@@ -756,6 +756,46 @@ func CreateIssue(ctx *context.APIContext) {
} }
} }
// Set org-level issue metadata (status/priority/type).
// If not provided, auto-assign the org default.
if form.StatusID != nil && *form.StatusID > 0 {
_ = issues_model.SetIssueStatusID(ctx, issue.ID, *form.StatusID)
} else {
// Auto-assign first non-closing status.
if defs, err := issues_model.GetIssueStatusDefsByOrg(ctx, ctx.Repo.Repository.OwnerID); err == nil {
for _, d := range defs {
if !d.ClosesIssue {
_ = issues_model.SetIssueStatusID(ctx, issue.ID, d.ID)
break
}
}
}
}
if form.PriorityID != nil && *form.PriorityID > 0 {
_ = issues_model.SetIssuePriorityID(ctx, issue.ID, *form.PriorityID)
} else {
if defs, err := issues_model.GetIssuePriorityDefsByOrg(ctx, ctx.Repo.Repository.OwnerID); err == nil {
for _, d := range defs {
if d.IsDefault {
_ = issues_model.SetIssuePriorityID(ctx, issue.ID, d.ID)
break
}
}
}
}
if form.TypeID != nil && *form.TypeID > 0 {
_ = issues_model.SetIssueTypeID(ctx, issue.ID, *form.TypeID)
} else {
if defs, err := issues_model.GetIssueTypeDefsByOrg(ctx, ctx.Repo.Repository.OwnerID); err == nil {
for _, d := range defs {
if d.IsDefault {
_ = issues_model.SetIssueTypeID(ctx, issue.ID, d.ID)
break
}
}
}
}
if form.Closed { if form.Closed {
if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil { if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil {
if issues_model.IsErrDependenciesLeft(err) { if issues_model.IsErrDependenciesLeft(err) {
@@ -980,6 +1020,26 @@ func EditIssue(ctx *context.APIContext) {
} }
} }
// Update org-level issue metadata (status/priority/type)
if canWrite && form.StatusID != nil {
if err := issues_model.SetIssueStatusID(ctx, issue.ID, *form.StatusID); err != nil {
ctx.APIErrorInternal(err)
return
}
}
if canWrite && form.PriorityID != nil {
if err := issues_model.SetIssuePriorityID(ctx, issue.ID, *form.PriorityID); err != nil {
ctx.APIErrorInternal(err)
return
}
}
if canWrite && form.TypeID != nil {
if err := issues_model.SetIssueTypeID(ctx, issue.ID, *form.TypeID); err != nil {
ctx.APIErrorInternal(err)
return
}
}
// Refetch from database to assign some automatic values // Refetch from database to assign some automatic values
issue, err = issues_model.GetIssueByID(ctx, issue.ID) issue, err = issues_model.GetIssueByID(ctx, issue.ID)
if err != nil { if err != nil {
+44 -44
View File
@@ -7,7 +7,7 @@ import (
"net/http" "net/http"
"time" "time"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses" updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/web" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/web"
@@ -16,7 +16,7 @@ import (
// GetLicenseSettings returns the licensing/update stream settings for a repo. // GetLicenseSettings returns the licensing/update stream settings for a repo.
func GetLicenseSettings(ctx *context.APIContext) { func GetLicenseSettings(ctx *context.APIContext) {
cfg := licenses.GetEffectiveConfig(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID) cfg := updateserver_model.GetEffectiveConfig(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID)
if cfg == nil { if cfg == nil {
ctx.JSON(http.StatusOK, &structs.LicenseSettings{}) ctx.JSON(http.StatusOK, &structs.LicenseSettings{})
return return
@@ -42,7 +42,7 @@ func GetLicenseSettings(ctx *context.APIContext) {
func UpdateLicenseSettings(ctx *context.APIContext) { func UpdateLicenseSettings(ctx *context.APIContext) {
form := web.GetForm(ctx).(*structs.LicenseSettings) form := web.GetForm(ctx).(*structs.LicenseSettings)
cfg := &licenses.UpdateStreamConfig{ cfg := &updateserver_model.UpdateStreamConfig{
OwnerID: ctx.Repo.Repository.OwnerID, OwnerID: ctx.Repo.Repository.OwnerID,
RepoID: ctx.Repo.Repository.ID, RepoID: ctx.Repo.Repository.ID,
LicensingEnabled: form.LicensingEnabled, LicensingEnabled: form.LicensingEnabled,
@@ -61,7 +61,7 @@ func UpdateLicenseSettings(ctx *context.APIContext) {
StreamMode: "joomla", StreamMode: "joomla",
} }
if err := licenses.SaveConfig(ctx, cfg); err != nil { if err := updateserver_model.SaveConfig(ctx, cfg); err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
} }
@@ -70,7 +70,7 @@ func UpdateLicenseSettings(ctx *context.APIContext) {
} }
// verifyPackageOwnership checks that a package belongs to the current repo's owner. // verifyPackageOwnership checks that a package belongs to the current repo's owner.
func verifyPackageOwnership(ctx *context.APIContext, pkg *licenses.LicensePackage) bool { func verifyPackageOwnership(ctx *context.APIContext, pkg *updateserver_model.LicensePackage) bool {
if pkg.OwnerID != ctx.Repo.Repository.OwnerID { if pkg.OwnerID != ctx.Repo.Repository.OwnerID {
ctx.APIErrorNotFound(nil) ctx.APIErrorNotFound(nil)
return false return false
@@ -79,7 +79,7 @@ func verifyPackageOwnership(ctx *context.APIContext, pkg *licenses.LicensePackag
} }
// verifyKeyOwnership checks that a key belongs to the current repo's owner. // verifyKeyOwnership checks that a key belongs to the current repo's owner.
func verifyKeyOwnership(ctx *context.APIContext, key *licenses.LicenseKey) bool { func verifyKeyOwnership(ctx *context.APIContext, key *updateserver_model.LicenseKey) bool {
if key.OwnerID != ctx.Repo.Repository.OwnerID { if key.OwnerID != ctx.Repo.Repository.OwnerID {
ctx.APIErrorNotFound(nil) ctx.APIErrorNotFound(nil)
return false return false
@@ -87,7 +87,7 @@ func verifyKeyOwnership(ctx *context.APIContext, key *licenses.LicenseKey) bool
return true return true
} }
func toLicensePackageAPI(pkg *licenses.LicensePackage) *structs.LicensePackage { func toLicensePackageAPI(pkg *updateserver_model.LicensePackage) *structs.LicensePackage {
return &structs.LicensePackage{ return &structs.LicensePackage{
ID: pkg.ID, ID: pkg.ID,
OwnerID: pkg.OwnerID, OwnerID: pkg.OwnerID,
@@ -103,7 +103,7 @@ func toLicensePackageAPI(pkg *licenses.LicensePackage) *structs.LicensePackage {
} }
} }
func toLicenseKeyAPI(key *licenses.LicenseKey) *structs.LicenseKey { func toLicenseKeyAPI(key *updateserver_model.LicenseKey) *structs.LicenseKey {
lk := &structs.LicenseKey{ lk := &structs.LicenseKey{
ID: key.ID, ID: key.ID,
PackageID: key.PackageID, PackageID: key.PackageID,
@@ -134,7 +134,7 @@ func toLicenseKeyAPI(key *licenses.LicenseKey) *structs.LicenseKey {
// ListLicensePackages lists license packages for the repo owner. // ListLicensePackages lists license packages for the repo owner.
func ListLicensePackages(ctx *context.APIContext) { func ListLicensePackages(ctx *context.APIContext) {
pkgs, err := licenses.ListLicensePackages(ctx, ctx.Repo.Repository.OwnerID) pkgs, err := updateserver_model.ListLicensePackages(ctx, ctx.Repo.Repository.OwnerID)
if err != nil { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
@@ -151,7 +151,7 @@ func ListLicensePackages(ctx *context.APIContext) {
func CreateLicensePackage(ctx *context.APIContext) { func CreateLicensePackage(ctx *context.APIContext) {
form := web.GetForm(ctx).(*structs.CreateLicensePackageOption) form := web.GetForm(ctx).(*structs.CreateLicensePackageOption)
pkg := &licenses.LicensePackage{ pkg := &updateserver_model.LicensePackage{
OwnerID: ctx.Repo.Repository.OwnerID, OwnerID: ctx.Repo.Repository.OwnerID,
Name: form.Name, Name: form.Name,
Description: form.Description, Description: form.Description,
@@ -165,7 +165,7 @@ func CreateLicensePackage(ctx *context.APIContext) {
pkg.RepoScope = "all" pkg.RepoScope = "all"
} }
if err := licenses.CreateLicensePackage(ctx, pkg); err != nil { if err := updateserver_model.CreateLicensePackage(ctx, pkg); err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
} }
@@ -178,7 +178,7 @@ func EditLicensePackage(ctx *context.APIContext) {
form := web.GetForm(ctx).(*structs.EditLicensePackageOption) form := web.GetForm(ctx).(*structs.EditLicensePackageOption)
pkgID := ctx.PathParamInt64("id") pkgID := ctx.PathParamInt64("id")
pkg, err := licenses.GetLicensePackageByID(ctx, pkgID) pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID)
if err != nil { if err != nil {
ctx.APIErrorNotFound(err) ctx.APIErrorNotFound(err)
return return
@@ -187,7 +187,7 @@ func EditLicensePackage(ctx *context.APIContext) {
return return
} }
if pkg.Name == licenses.MasterPackageName { if pkg.Name == updateserver_model.MasterPackageName {
ctx.APIError(http.StatusForbidden, "master package cannot be edited") ctx.APIError(http.StatusForbidden, "master package cannot be edited")
return return
} }
@@ -214,7 +214,7 @@ func EditLicensePackage(ctx *context.APIContext) {
pkg.IsActive = *form.IsActive pkg.IsActive = *form.IsActive
} }
if err := licenses.UpdateLicensePackage(ctx, pkg); err != nil { if err := updateserver_model.UpdateLicensePackage(ctx, pkg); err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
} }
@@ -226,7 +226,7 @@ func EditLicensePackage(ctx *context.APIContext) {
func DeleteLicensePackage(ctx *context.APIContext) { func DeleteLicensePackage(ctx *context.APIContext) {
pkgID := ctx.PathParamInt64("id") pkgID := ctx.PathParamInt64("id")
pkg, err := licenses.GetLicensePackageByID(ctx, pkgID) pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID)
if err != nil { if err != nil {
ctx.APIErrorNotFound(err) ctx.APIErrorNotFound(err)
return return
@@ -235,12 +235,12 @@ func DeleteLicensePackage(ctx *context.APIContext) {
return return
} }
if pkg.Name == licenses.MasterPackageName { if pkg.Name == updateserver_model.MasterPackageName {
ctx.APIError(http.StatusForbidden, "master package cannot be deleted") ctx.APIError(http.StatusForbidden, "master package cannot be deleted")
return return
} }
if err := licenses.DeleteLicensePackage(ctx, pkgID); err != nil { if err := updateserver_model.DeleteLicensePackage(ctx, pkgID); err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
} }
@@ -252,7 +252,7 @@ func DeleteLicensePackage(ctx *context.APIContext) {
func ArchiveLicensePackage(ctx *context.APIContext) { func ArchiveLicensePackage(ctx *context.APIContext) {
pkgID := ctx.PathParamInt64("id") pkgID := ctx.PathParamInt64("id")
pkg, err := licenses.GetLicensePackageByID(ctx, pkgID) pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID)
if err != nil { if err != nil {
ctx.APIErrorNotFound(err) ctx.APIErrorNotFound(err)
return return
@@ -261,12 +261,12 @@ func ArchiveLicensePackage(ctx *context.APIContext) {
return return
} }
if pkg.Name == licenses.MasterPackageName { if pkg.Name == updateserver_model.MasterPackageName {
ctx.APIError(http.StatusForbidden, "master package cannot be archived") ctx.APIError(http.StatusForbidden, "master package cannot be archived")
return return
} }
if err := licenses.ArchiveLicensePackage(ctx, pkgID); err != nil { if err := updateserver_model.ArchiveLicensePackage(ctx, pkgID); err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
} }
@@ -277,7 +277,7 @@ func ArchiveLicensePackage(ctx *context.APIContext) {
// UnarchiveLicensePackage restores an archived license package via API. // UnarchiveLicensePackage restores an archived license package via API.
func UnarchiveLicensePackage(ctx *context.APIContext) { func UnarchiveLicensePackage(ctx *context.APIContext) {
pkgID := ctx.PathParamInt64("id") pkgID := ctx.PathParamInt64("id")
pkg, err := licenses.GetLicensePackageByID(ctx, pkgID) pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID)
if err != nil { if err != nil {
ctx.APIErrorNotFound(err) ctx.APIErrorNotFound(err)
return return
@@ -285,7 +285,7 @@ func UnarchiveLicensePackage(ctx *context.APIContext) {
if !verifyPackageOwnership(ctx, pkg) { if !verifyPackageOwnership(ctx, pkg) {
return return
} }
if err := licenses.UnarchiveLicensePackage(ctx, pkgID); err != nil { if err := updateserver_model.UnarchiveLicensePackage(ctx, pkgID); err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
} }
@@ -294,7 +294,7 @@ func UnarchiveLicensePackage(ctx *context.APIContext) {
// ListLicenseKeys lists license keys for the repo owner. // ListLicenseKeys lists license keys for the repo owner.
func ListLicenseKeys(ctx *context.APIContext) { func ListLicenseKeys(ctx *context.APIContext) {
keys, err := licenses.ListLicenseKeys(ctx, ctx.Repo.Repository.OwnerID) keys, err := updateserver_model.ListLicenseKeys(ctx, ctx.Repo.Repository.OwnerID)
if err != nil { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
@@ -311,7 +311,7 @@ func ListLicenseKeys(ctx *context.APIContext) {
func CreateLicenseKey(ctx *context.APIContext) { func CreateLicenseKey(ctx *context.APIContext) {
form := web.GetForm(ctx).(*structs.CreateLicenseKeyOption) form := web.GetForm(ctx).(*structs.CreateLicenseKeyOption)
key := &licenses.LicenseKey{ key := &updateserver_model.LicenseKey{
PackageID: form.PackageID, PackageID: form.PackageID,
OwnerID: ctx.Repo.Repository.OwnerID, OwnerID: ctx.Repo.Repository.OwnerID,
LicenseeName: form.LicenseeName, LicenseeName: form.LicenseeName,
@@ -329,7 +329,7 @@ func CreateLicenseKey(ctx *context.APIContext) {
key.ExpiresUnix = timeutil.TimeStamp(form.ExpiresAt.Unix()) key.ExpiresUnix = timeutil.TimeStamp(form.ExpiresAt.Unix())
} else { } else {
// Auto-calculate from package duration. // Auto-calculate from package duration.
pkg, err := licenses.GetLicensePackageByID(ctx, form.PackageID) pkg, err := updateserver_model.GetLicensePackageByID(ctx, form.PackageID)
if err != nil { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
@@ -344,7 +344,7 @@ func CreateLicenseKey(ctx *context.APIContext) {
} }
} }
rawKey, err := licenses.CreateLicenseKey(ctx, key) rawKey, err := updateserver_model.CreateLicenseKey(ctx, key)
if err != nil { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
@@ -362,7 +362,7 @@ func EditLicenseKey(ctx *context.APIContext) {
form := web.GetForm(ctx).(*structs.EditLicenseKeyOption) form := web.GetForm(ctx).(*structs.EditLicenseKeyOption)
keyID := ctx.PathParamInt64("id") keyID := ctx.PathParamInt64("id")
key, err := licenses.GetLicenseKeyByID(ctx, keyID) key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID)
if err != nil { if err != nil {
ctx.APIErrorNotFound(err) ctx.APIErrorNotFound(err)
return return
@@ -395,7 +395,7 @@ func EditLicenseKey(ctx *context.APIContext) {
key.ExpiresUnix = timeutil.TimeStamp(form.ExpiresAt.Unix()) key.ExpiresUnix = timeutil.TimeStamp(form.ExpiresAt.Unix())
} }
if err := licenses.UpdateLicenseKey(ctx, key); err != nil { if err := updateserver_model.UpdateLicenseKey(ctx, key); err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
} }
@@ -409,7 +409,7 @@ func PurchaseLicenseKey(ctx *context.APIContext) {
// Idempotency check: if payment_ref already exists, return existing key. // Idempotency check: if payment_ref already exists, return existing key.
if form.PaymentRef != "" { if form.PaymentRef != "" {
existing, err := licenses.GetLicenseKeyByPaymentRef(ctx, form.PaymentRef) existing, err := updateserver_model.GetLicenseKeyByPaymentRef(ctx, form.PaymentRef)
if err == nil { if err == nil {
resp := &structs.LicenseKeyCreated{ resp := &structs.LicenseKeyCreated{
LicenseKey: *toLicenseKeyAPI(existing), LicenseKey: *toLicenseKeyAPI(existing),
@@ -420,7 +420,7 @@ func PurchaseLicenseKey(ctx *context.APIContext) {
} }
} }
pkg, err := licenses.GetLicensePackageByID(ctx, form.PackageID) pkg, err := updateserver_model.GetLicensePackageByID(ctx, form.PackageID)
if err != nil { if err != nil {
ctx.APIErrorNotFound(err) ctx.APIErrorNotFound(err)
return return
@@ -429,7 +429,7 @@ func PurchaseLicenseKey(ctx *context.APIContext) {
return return
} }
key := &licenses.LicenseKey{ key := &updateserver_model.LicenseKey{
PackageID: form.PackageID, PackageID: form.PackageID,
OwnerID: ctx.Repo.Repository.OwnerID, OwnerID: ctx.Repo.Repository.OwnerID,
LicenseeName: form.LicenseeName, LicenseeName: form.LicenseeName,
@@ -444,7 +444,7 @@ func PurchaseLicenseKey(ctx *context.APIContext) {
key.ExpiresUnix = timeutil.TimeStamp(expires.Unix()) key.ExpiresUnix = timeutil.TimeStamp(expires.Unix())
} }
rawKey, err := licenses.CreateLicenseKey(ctx, key) rawKey, err := updateserver_model.CreateLicenseKey(ctx, key)
if err != nil { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
@@ -460,7 +460,7 @@ func PurchaseLicenseKey(ctx *context.APIContext) {
// RenewLicenseKey extends a key's expiration by its package duration. // RenewLicenseKey extends a key's expiration by its package duration.
func RenewLicenseKey(ctx *context.APIContext) { func RenewLicenseKey(ctx *context.APIContext) {
keyID := ctx.PathParamInt64("id") keyID := ctx.PathParamInt64("id")
key, err := licenses.GetLicenseKeyByID(ctx, keyID) key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID)
if err != nil { if err != nil {
ctx.APIErrorNotFound(err) ctx.APIErrorNotFound(err)
return return
@@ -469,7 +469,7 @@ func RenewLicenseKey(ctx *context.APIContext) {
return return
} }
pkg, err := licenses.GetLicensePackageByID(ctx, key.PackageID) pkg, err := updateserver_model.GetLicensePackageByID(ctx, key.PackageID)
if err != nil { if err != nil {
ctx.APIErrorNotFound(err) ctx.APIErrorNotFound(err)
return return
@@ -480,20 +480,20 @@ func RenewLicenseKey(ctx *context.APIContext) {
days = 365 days = 365
} }
if err := licenses.RenewLicenseKey(ctx, keyID, days); err != nil { if err := updateserver_model.RenewLicenseKey(ctx, keyID, days); err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
} }
// Reload key to get updated fields. // Reload key to get updated fields.
key, _ = licenses.GetLicenseKeyByID(ctx, keyID) key, _ = updateserver_model.GetLicenseKeyByID(ctx, keyID)
ctx.JSON(http.StatusOK, toLicenseKeyAPI(key)) ctx.JSON(http.StatusOK, toLicenseKeyAPI(key))
} }
// RevokeLicenseKey deactivates a license key via API. // RevokeLicenseKey deactivates a license key via API.
func RevokeLicenseKey(ctx *context.APIContext) { func RevokeLicenseKey(ctx *context.APIContext) {
keyID := ctx.PathParamInt64("id") keyID := ctx.PathParamInt64("id")
key, err := licenses.GetLicenseKeyByID(ctx, keyID) key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID)
if err != nil { if err != nil {
ctx.APIErrorNotFound(err) ctx.APIErrorNotFound(err)
return return
@@ -503,7 +503,7 @@ func RevokeLicenseKey(ctx *context.APIContext) {
} }
key.IsActive = false key.IsActive = false
if err := licenses.UpdateLicenseKey(ctx, key); err != nil { if err := updateserver_model.UpdateLicenseKey(ctx, key); err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
} }
@@ -514,7 +514,7 @@ func RevokeLicenseKey(ctx *context.APIContext) {
// DeleteLicenseKey deletes a license key. // DeleteLicenseKey deletes a license key.
func DeleteLicenseKey(ctx *context.APIContext) { func DeleteLicenseKey(ctx *context.APIContext) {
keyID := ctx.PathParamInt64("id") keyID := ctx.PathParamInt64("id")
key, err := licenses.GetLicenseKeyByID(ctx, keyID) key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID)
if err != nil { if err != nil {
ctx.APIErrorNotFound(err) ctx.APIErrorNotFound(err)
return return
@@ -522,7 +522,7 @@ func DeleteLicenseKey(ctx *context.APIContext) {
if !verifyKeyOwnership(ctx, key) { if !verifyKeyOwnership(ctx, key) {
return return
} }
if err := licenses.DeleteLicenseKey(ctx, keyID); err != nil { if err := updateserver_model.DeleteLicenseKey(ctx, keyID); err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
} }
@@ -533,7 +533,7 @@ func DeleteLicenseKey(ctx *context.APIContext) {
func ValidateLicenseKey(ctx *context.APIContext) { func ValidateLicenseKey(ctx *context.APIContext) {
form := web.GetForm(ctx).(*structs.ValidateLicenseKeyOption) form := web.GetForm(ctx).(*structs.ValidateLicenseKeyOption)
key, pkg, err := licenses.ValidateLicenseKey(ctx, form.Key, form.Domain) key, pkg, err := updateserver_model.ValidateLicenseKey(ctx, form.Key, form.Domain)
if err != nil { if err != nil {
ctx.JSON(http.StatusOK, &structs.ValidateLicenseKeyResponse{ ctx.JSON(http.StatusOK, &structs.ValidateLicenseKeyResponse{
Valid: false, Valid: false,
@@ -542,7 +542,7 @@ func ValidateLicenseKey(ctx *context.APIContext) {
return return
} }
_ = licenses.TouchHeartbeat(ctx, key.ID) _ = updateserver_model.TouchHeartbeat(ctx, key.ID)
var expiresAt *time.Time var expiresAt *time.Time
if key.ExpiresUnix > 0 { if key.ExpiresUnix > 0 {
@@ -555,7 +555,7 @@ func ValidateLicenseKey(ctx *context.APIContext) {
maxSites = pkg.MaxSites maxSites = pkg.MaxSites
} }
sitesUsed, _ := licenses.CountUniqueDomainsByKey(ctx, key.ID) sitesUsed, _ := updateserver_model.CountUniqueDomainsByKey(ctx, key.ID)
ctx.JSON(http.StatusOK, &structs.ValidateLicenseKeyResponse{ ctx.JSON(http.StatusOK, &structs.ValidateLicenseKeyResponse{
Valid: true, Valid: true,
@@ -569,7 +569,7 @@ func ValidateLicenseKey(ctx *context.APIContext) {
// GetLicenseKeyUsage returns usage logs for a license key. // GetLicenseKeyUsage returns usage logs for a license key.
func GetLicenseKeyUsage(ctx *context.APIContext) { func GetLicenseKeyUsage(ctx *context.APIContext) {
usages, err := licenses.GetRecentUsage(ctx, ctx.PathParamInt64("id"), 100) usages, err := updateserver_model.GetRecentUsage(ctx, ctx.PathParamInt64("id"), 100)
if err != nil { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
+24
View File
@@ -24,6 +24,12 @@ type apiManifest struct {
Platform string `json:"platform"` Platform string `json:"platform"`
StandardsVersion string `json:"standards_version"` StandardsVersion string `json:"standards_version"`
StandardsSource string `json:"standards_source"` StandardsSource string `json:"standards_source"`
DisplayName string `json:"display_name"`
Maintainer string `json:"maintainer"`
MaintainerURL string `json:"maintainer_url"`
InfoURL string `json:"info_url"`
TargetVersion string `json:"target_version"`
PHPMinimum string `json:"php_minimum"`
Language string `json:"language"` Language string `json:"language"`
PackageType string `json:"package_type"` PackageType string `json:"package_type"`
EntryPoint string `json:"entry_point"` EntryPoint string `json:"entry_point"`
@@ -67,6 +73,12 @@ func GetRepoManifest(ctx *context.APIContext) {
Platform: m.Platform, Platform: m.Platform,
StandardsVersion: m.StandardsVersion, StandardsVersion: m.StandardsVersion,
StandardsSource: m.StandardsSource, StandardsSource: m.StandardsSource,
DisplayName: m.DisplayName,
Maintainer: m.Maintainer,
MaintainerURL: m.MaintainerURL,
InfoURL: m.InfoURL,
TargetVersion: m.TargetVersion,
PHPMinimum: m.PHPMinimum,
Language: m.Language, Language: m.Language,
PackageType: m.PackageType, PackageType: m.PackageType,
EntryPoint: m.EntryPoint, EntryPoint: m.EntryPoint,
@@ -104,6 +116,12 @@ func UpdateRepoManifest(ctx *context.APIContext) {
Platform: req.Platform, Platform: req.Platform,
StandardsVersion: req.StandardsVersion, StandardsVersion: req.StandardsVersion,
StandardsSource: req.StandardsSource, StandardsSource: req.StandardsSource,
DisplayName: req.DisplayName,
Maintainer: req.Maintainer,
MaintainerURL: req.MaintainerURL,
InfoURL: req.InfoURL,
TargetVersion: req.TargetVersion,
PHPMinimum: req.PHPMinimum,
Language: req.Language, Language: req.Language,
PackageType: req.PackageType, PackageType: req.PackageType,
EntryPoint: req.EntryPoint, EntryPoint: req.EntryPoint,
@@ -126,6 +144,12 @@ func UpdateRepoManifest(ctx *context.APIContext) {
Platform: m.Platform, Platform: m.Platform,
StandardsVersion: m.StandardsVersion, StandardsVersion: m.StandardsVersion,
StandardsSource: m.StandardsSource, StandardsSource: m.StandardsSource,
DisplayName: m.DisplayName,
Maintainer: m.Maintainer,
MaintainerURL: m.MaintainerURL,
InfoURL: m.InfoURL,
TargetVersion: m.TargetVersion,
PHPMinimum: m.PHPMinimum,
Language: m.Language, Language: m.Language,
PackageType: m.PackageType, PackageType: m.PackageType,
EntryPoint: m.EntryPoint, EntryPoint: m.EntryPoint,
+3 -3
View File
@@ -9,7 +9,7 @@ import (
"strings" "strings"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
licenses_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses" updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/organization" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/organization"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/renderhelper" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/renderhelper"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
@@ -109,10 +109,10 @@ func home(ctx *context.Context, viewRepositories bool) {
ctx.Data["IsOrganizationMember"] = ctx.Org.IsMember ctx.Data["IsOrganizationMember"] = ctx.Org.IsMember
ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner
orgCfg, _ := licenses_model.GetOrgConfig(ctx, ctx.Org.Organization.ID) orgCfg, _ := updateserver_model.GetOrgConfig(ctx, ctx.Org.Organization.ID)
ctx.Data["OrgLicensingEnabled"] = orgCfg != nil && orgCfg.LicensingEnabled ctx.Data["OrgLicensingEnabled"] = orgCfg != nil && orgCfg.LicensingEnabled
if orgCfg != nil && orgCfg.LicensingEnabled { if orgCfg != nil && orgCfg.LicensingEnabled {
numPkgs, _ := licenses_model.CountOrgPackages(ctx, ctx.Org.Organization.ID) numPkgs, _ := updateserver_model.CountOrgPackages(ctx, ctx.Org.Organization.ID)
ctx.Data["NumOrgLicensePackages"] = numPkgs ctx.Data["NumOrgLicensePackages"] = numPkgs
} }
ctx.Data["IsPublicMember"] = func(uid int64) bool { ctx.Data["IsPublicMember"] = func(uid int64) bool {
+49 -49
View File
@@ -9,7 +9,7 @@ import (
"strings" "strings"
"time" "time"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses" updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/perm" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/perm"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
unit_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit" unit_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit"
@@ -27,7 +27,7 @@ type OrgChannelItem struct {
Label string Label string
} }
func buildOrgChannelItems(streams []licenses.StreamDef) []OrgChannelItem { func buildOrgChannelItems(streams []updateserver_model.StreamDef) []OrgChannelItem {
items := make([]OrgChannelItem, 0, len(streams)) items := make([]OrgChannelItem, 0, len(streams))
for _, s := range streams { for _, s := range streams {
label := s.Name label := s.Name
@@ -62,7 +62,7 @@ func parseOrgAllowedChannels(s string) []string {
// LicensePackageDisplay is used in templates. // LicensePackageDisplay is used in templates.
type LicensePackageDisplay struct { type LicensePackageDisplay struct {
*licenses.LicensePackage *updateserver_model.LicensePackage
KeyCount int64 KeyCount int64
Created time.Time Created time.Time
} }
@@ -79,7 +79,7 @@ func Licenses(ctx *context.Context) {
// Auto-create master key if has write access. // Auto-create master key if has write access.
if canWriteLicenses { if canWriteLicenses {
newMasterKey, err := licenses.EnsureMasterKey(ctx, ownerID) newMasterKey, err := updateserver_model.EnsureMasterKey(ctx, ownerID)
if err != nil { if err != nil {
ctx.ServerError("EnsureMasterKey", err) ctx.ServerError("EnsureMasterKey", err)
return return
@@ -89,7 +89,7 @@ func Licenses(ctx *context.Context) {
} }
} }
pkgs, err := licenses.ListLicensePackagesWithAncestors(ctx, ownerID) pkgs, err := updateserver_model.ListLicensePackagesWithAncestors(ctx, ownerID)
if err != nil { if err != nil {
ctx.ServerError("ListLicensePackages", err) ctx.ServerError("ListLicensePackages", err)
return return
@@ -97,7 +97,7 @@ func Licenses(ctx *context.Context) {
var display []LicensePackageDisplay var display []LicensePackageDisplay
for _, pkg := range pkgs { for _, pkg := range pkgs {
count, _ := licenses.CountKeysByPackage(ctx, pkg.ID) count, _ := updateserver_model.CountKeysByPackage(ctx, pkg.ID)
display = append(display, LicensePackageDisplay{ display = append(display, LicensePackageDisplay{
LicensePackage: pkg, LicensePackage: pkg,
KeyCount: count, KeyCount: count,
@@ -110,11 +110,11 @@ func Licenses(ctx *context.Context) {
searchQuery := strings.TrimSpace(ctx.FormString("q")) searchQuery := strings.TrimSpace(ctx.FormString("q"))
ctx.Data["SearchQuery"] = searchQuery ctx.Data["SearchQuery"] = searchQuery
var keys []*licenses.LicenseKey var keys []*updateserver_model.LicenseKey
if searchQuery != "" { if searchQuery != "" {
keys, err = licenses.SearchLicenseKeysWithAncestors(ctx, ownerID, searchQuery) keys, err = updateserver_model.SearchLicenseKeysWithAncestors(ctx, ownerID, searchQuery)
} else { } else {
keys, err = licenses.ListLicenseKeysWithAncestors(ctx, ownerID) keys, err = updateserver_model.ListLicenseKeysWithAncestors(ctx, ownerID)
} }
if err != nil { if err != nil {
ctx.ServerError("ListLicenseKeys", err) ctx.ServerError("ListLicenseKeys", err)
@@ -128,10 +128,10 @@ func Licenses(ctx *context.Context) {
ctx.Data["OrgLicensingEnabled"] = true ctx.Data["OrgLicensingEnabled"] = true
// Load archived packages. // Load archived packages.
archivedPkgs, _ := licenses.ListArchivedLicensePackages(ctx, ownerID) archivedPkgs, _ := updateserver_model.ListArchivedLicensePackages(ctx, ownerID)
var archivedDisplay []LicensePackageDisplay var archivedDisplay []LicensePackageDisplay
for _, pkg := range archivedPkgs { for _, pkg := range archivedPkgs {
count, _ := licenses.CountKeysByPackage(ctx, pkg.ID) count, _ := updateserver_model.CountKeysByPackage(ctx, pkg.ID)
archivedDisplay = append(archivedDisplay, LicensePackageDisplay{ archivedDisplay = append(archivedDisplay, LicensePackageDisplay{
LicensePackage: pkg, LicensePackage: pkg,
KeyCount: count, KeyCount: count,
@@ -140,12 +140,12 @@ func Licenses(ctx *context.Context) {
} }
ctx.Data["ArchivedPackages"] = archivedDisplay ctx.Data["ArchivedPackages"] = archivedDisplay
orgCfg, _ := licenses.GetOrgConfig(ctx, ownerID) orgCfg, _ := updateserver_model.GetOrgConfig(ctx, ownerID)
var orgStreams []licenses.StreamDef var orgStreams []updateserver_model.StreamDef
if orgCfg != nil { if orgCfg != nil {
orgStreams = orgCfg.GetActiveStreams() orgStreams = orgCfg.GetActiveStreams()
} else { } else {
orgStreams = licenses.DefaultJoomlaStreams() orgStreams = updateserver_model.DefaultJoomlaStreams()
} }
ctx.Data["AvailableStreams"] = orgStreams ctx.Data["AvailableStreams"] = orgStreams
ctx.Data["ChannelItems"] = buildOrgChannelItems(orgStreams) ctx.Data["ChannelItems"] = buildOrgChannelItems(orgStreams)
@@ -182,7 +182,7 @@ func LicensesCreatePackage(ctx *context.Context) {
repoScope = "all" repoScope = "all"
} }
pkg := &licenses.LicensePackage{ pkg := &updateserver_model.LicensePackage{
OwnerID: ctx.Org.Organization.ID, OwnerID: ctx.Org.Organization.ID,
Name: name, Name: name,
Description: ctx.FormString("description"), Description: ctx.FormString("description"),
@@ -194,7 +194,7 @@ func LicensesCreatePackage(ctx *context.Context) {
IsActive: true, IsActive: true,
} }
if err := licenses.CreateLicensePackage(ctx, pkg); err != nil { if err := updateserver_model.CreateLicensePackage(ctx, pkg); err != nil {
ctx.ServerError("CreateLicensePackage", err) ctx.ServerError("CreateLicensePackage", err)
return return
} }
@@ -212,13 +212,13 @@ func LicensesGenerateKey(ctx *context.Context) {
return return
} }
pkg, err := licenses.GetLicensePackageByID(ctx, packageID) pkg, err := updateserver_model.GetLicensePackageByID(ctx, packageID)
if err != nil { if err != nil {
ctx.ServerError("GetLicensePackageByID", err) ctx.ServerError("GetLicensePackageByID", err)
return return
} }
key := &licenses.LicenseKey{ key := &updateserver_model.LicenseKey{
PackageID: packageID, PackageID: packageID,
OwnerID: ctx.Org.Organization.ID, OwnerID: ctx.Org.Organization.ID,
IsActive: true, IsActive: true,
@@ -233,13 +233,13 @@ func LicensesGenerateKey(ctx *context.Context) {
var rawKey string var rawKey string
customKey := strings.TrimSpace(ctx.FormString("custom_key")) customKey := strings.TrimSpace(ctx.FormString("custom_key"))
if customKey != "" && (ctx.IsUserSiteAdmin() || ctx.Org.IsOwner) { if customKey != "" && (ctx.IsUserSiteAdmin() || ctx.Org.IsOwner) {
if err := licenses.CreateLicenseKeyCustom(ctx, key, customKey); err != nil { if err := updateserver_model.CreateLicenseKeyCustom(ctx, key, customKey); err != nil {
ctx.ServerError("CreateLicenseKeyCustom", err) ctx.ServerError("CreateLicenseKeyCustom", err)
return return
} }
rawKey = customKey rawKey = customKey
} else { } else {
rawKey, err = licenses.CreateLicenseKey(ctx, key) rawKey, err = updateserver_model.CreateLicenseKey(ctx, key)
if err != nil { if err != nil {
ctx.ServerError("CreateLicenseKey", err) ctx.ServerError("CreateLicenseKey", err)
return return
@@ -253,10 +253,10 @@ func LicensesGenerateKey(ctx *context.Context) {
ctx.Data["NewKeyCreated"] = rawKey ctx.Data["NewKeyCreated"] = rawKey
ownerID := ctx.Org.Organization.ID ownerID := ctx.Org.Organization.ID
pkgs, _ := licenses.ListLicensePackages(ctx, ownerID) pkgs, _ := updateserver_model.ListLicensePackages(ctx, ownerID)
var display []LicensePackageDisplay var display []LicensePackageDisplay
for _, p := range pkgs { for _, p := range pkgs {
count, _ := licenses.CountKeysByPackage(ctx, p.ID) count, _ := updateserver_model.CountKeysByPackage(ctx, p.ID)
display = append(display, LicensePackageDisplay{ display = append(display, LicensePackageDisplay{
LicensePackage: p, LicensePackage: p,
KeyCount: count, KeyCount: count,
@@ -264,14 +264,14 @@ func LicensesGenerateKey(ctx *context.Context) {
}) })
} }
ctx.Data["LicensePackages"] = display ctx.Data["LicensePackages"] = display
keys, _ := licenses.ListLicenseKeys(ctx, ownerID) keys, _ := updateserver_model.ListLicenseKeys(ctx, ownerID)
ctx.Data["LicenseKeys"] = keys ctx.Data["LicenseKeys"] = keys
orgCfg, _ := licenses.GetOrgConfig(ctx, ownerID) orgCfg, _ := updateserver_model.GetOrgConfig(ctx, ownerID)
if orgCfg != nil { if orgCfg != nil {
ctx.Data["AvailableStreams"] = orgCfg.GetActiveStreams() ctx.Data["AvailableStreams"] = orgCfg.GetActiveStreams()
} else { } else {
ctx.Data["AvailableStreams"] = licenses.DefaultJoomlaStreams() ctx.Data["AvailableStreams"] = updateserver_model.DefaultJoomlaStreams()
} }
ctx.HTML(http.StatusOK, tplOrgLicenses) ctx.HTML(http.StatusOK, tplOrgLicenses)
@@ -283,13 +283,13 @@ const tplOrgLicensesEditKey templates.TplName = "repo/licenses_edit_key"
// LicensesEditPackage shows the edit form for an org license package. // LicensesEditPackage shows the edit form for an org license package.
func LicensesEditPackage(ctx *context.Context) { func LicensesEditPackage(ctx *context.Context) {
pkgID := ctx.PathParamInt64("id") pkgID := ctx.PathParamInt64("id")
pkg, err := licenses.GetLicensePackageByID(ctx, pkgID) pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID)
if err != nil { if err != nil {
ctx.ServerError("GetLicensePackageByID", err) ctx.ServerError("GetLicensePackageByID", err)
return return
} }
if pkg.Name == licenses.MasterPackageName { if pkg.Name == updateserver_model.MasterPackageName {
ctx.Flash.Error("Master package cannot be edited") ctx.Flash.Error("Master package cannot be edited")
ctx.Redirect(ctx.Org.OrgLink + "/-/licenses") ctx.Redirect(ctx.Org.OrgLink + "/-/licenses")
return return
@@ -301,12 +301,12 @@ func LicensesEditPackage(ctx *context.Context) {
selectedChannels := parseOrgAllowedChannels(pkg.AllowedChannels) selectedChannels := parseOrgAllowedChannels(pkg.AllowedChannels)
ctx.Data["SelectedChannels"] = selectedChannels ctx.Data["SelectedChannels"] = selectedChannels
orgCfg, _ := licenses.GetOrgConfig(ctx, ctx.Org.Organization.ID) orgCfg, _ := updateserver_model.GetOrgConfig(ctx, ctx.Org.Organization.ID)
var editStreams []licenses.StreamDef var editStreams []updateserver_model.StreamDef
if orgCfg != nil { if orgCfg != nil {
editStreams = orgCfg.GetActiveStreams() editStreams = orgCfg.GetActiveStreams()
} else { } else {
editStreams = licenses.DefaultJoomlaStreams() editStreams = updateserver_model.DefaultJoomlaStreams()
} }
ctx.Data["AvailableStreams"] = editStreams ctx.Data["AvailableStreams"] = editStreams
ctx.Data["ChannelItems"] = buildOrgChannelItems(editStreams) ctx.Data["ChannelItems"] = buildOrgChannelItems(editStreams)
@@ -318,13 +318,13 @@ func LicensesEditPackage(ctx *context.Context) {
// LicensesEditPackagePost saves edits to an org license package. // LicensesEditPackagePost saves edits to an org license package.
func LicensesEditPackagePost(ctx *context.Context) { func LicensesEditPackagePost(ctx *context.Context) {
pkgID := ctx.PathParamInt64("id") pkgID := ctx.PathParamInt64("id")
pkg, err := licenses.GetLicensePackageByID(ctx, pkgID) pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID)
if err != nil { if err != nil {
ctx.ServerError("GetLicensePackageByID", err) ctx.ServerError("GetLicensePackageByID", err)
return return
} }
if pkg.Name == licenses.MasterPackageName { if pkg.Name == updateserver_model.MasterPackageName {
ctx.Flash.Error("Master package cannot be edited") ctx.Flash.Error("Master package cannot be edited")
ctx.Redirect(ctx.Org.OrgLink + "/-/licenses") ctx.Redirect(ctx.Org.OrgLink + "/-/licenses")
return return
@@ -349,7 +349,7 @@ func LicensesEditPackagePost(ctx *context.Context) {
pkg.IsActive = ctx.FormString("is_active") == "on" pkg.IsActive = ctx.FormString("is_active") == "on"
if err := licenses.UpdateLicensePackage(ctx, pkg); err != nil { if err := updateserver_model.UpdateLicensePackage(ctx, pkg); err != nil {
ctx.ServerError("UpdateLicensePackage", err) ctx.ServerError("UpdateLicensePackage", err)
return return
} }
@@ -366,17 +366,17 @@ func canOrgDeleteLicenses(ctx *context.Context) bool {
// LicensesArchivePackage archives an org license package. // LicensesArchivePackage archives an org license package.
func LicensesArchivePackage(ctx *context.Context) { func LicensesArchivePackage(ctx *context.Context) {
pkgID := ctx.PathParamInt64("id") pkgID := ctx.PathParamInt64("id")
pkg, err := licenses.GetLicensePackageByID(ctx, pkgID) pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID)
if err != nil { if err != nil {
ctx.ServerError("GetLicensePackageByID", err) ctx.ServerError("GetLicensePackageByID", err)
return return
} }
if pkg.Name == licenses.MasterPackageName { if pkg.Name == updateserver_model.MasterPackageName {
ctx.Flash.Error("Master package cannot be archived") ctx.Flash.Error("Master package cannot be archived")
ctx.Redirect(ctx.Org.OrgLink + "/-/licenses") ctx.Redirect(ctx.Org.OrgLink + "/-/licenses")
return return
} }
if err := licenses.ArchiveLicensePackage(ctx, pkgID); err != nil { if err := updateserver_model.ArchiveLicensePackage(ctx, pkgID); err != nil {
ctx.ServerError("ArchiveLicensePackage", err) ctx.ServerError("ArchiveLicensePackage", err)
return return
} }
@@ -387,7 +387,7 @@ func LicensesArchivePackage(ctx *context.Context) {
// LicensesUnarchivePackage restores an archived org license package. // LicensesUnarchivePackage restores an archived org license package.
func LicensesUnarchivePackage(ctx *context.Context) { func LicensesUnarchivePackage(ctx *context.Context) {
pkgID := ctx.PathParamInt64("id") pkgID := ctx.PathParamInt64("id")
if err := licenses.UnarchiveLicensePackage(ctx, pkgID); err != nil { if err := updateserver_model.UnarchiveLicensePackage(ctx, pkgID); err != nil {
ctx.ServerError("UnarchiveLicensePackage", err) ctx.ServerError("UnarchiveLicensePackage", err)
return return
} }
@@ -402,17 +402,17 @@ func LicensesDeletePackage(ctx *context.Context) {
return return
} }
pkgID := ctx.PathParamInt64("id") pkgID := ctx.PathParamInt64("id")
pkg, err := licenses.GetLicensePackageByID(ctx, pkgID) pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID)
if err != nil { if err != nil {
ctx.ServerError("GetLicensePackageByID", err) ctx.ServerError("GetLicensePackageByID", err)
return return
} }
if pkg.Name == licenses.MasterPackageName { if pkg.Name == updateserver_model.MasterPackageName {
ctx.Flash.Error("Master package cannot be deleted") ctx.Flash.Error("Master package cannot be deleted")
ctx.Redirect(ctx.Org.OrgLink + "/-/licenses") ctx.Redirect(ctx.Org.OrgLink + "/-/licenses")
return return
} }
if err := licenses.DeleteLicensePackage(ctx, pkgID); err != nil { if err := updateserver_model.DeleteLicensePackage(ctx, pkgID); err != nil {
ctx.ServerError("DeleteLicensePackage", err) ctx.ServerError("DeleteLicensePackage", err)
return return
} }
@@ -424,7 +424,7 @@ func LicensesDeletePackage(ctx *context.Context) {
// LicensesEditKey shows the edit form for an org license key. // LicensesEditKey shows the edit form for an org license key.
func LicensesEditKey(ctx *context.Context) { func LicensesEditKey(ctx *context.Context) {
keyID := ctx.PathParamInt64("id") keyID := ctx.PathParamInt64("id")
key, err := licenses.GetLicenseKeyByID(ctx, keyID) key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID)
if err != nil { if err != nil {
ctx.ServerError("GetLicenseKeyByID", err) ctx.ServerError("GetLicenseKeyByID", err)
return return
@@ -452,7 +452,7 @@ func LicensesEditKey(ctx *context.Context) {
// LicensesEditKeyPost saves edits to an org license key. // LicensesEditKeyPost saves edits to an org license key.
func LicensesEditKeyPost(ctx *context.Context) { func LicensesEditKeyPost(ctx *context.Context) {
keyID := ctx.PathParamInt64("id") keyID := ctx.PathParamInt64("id")
key, err := licenses.GetLicenseKeyByID(ctx, keyID) key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID)
if err != nil { if err != nil {
ctx.ServerError("GetLicenseKeyByID", err) ctx.ServerError("GetLicenseKeyByID", err)
return return
@@ -481,7 +481,7 @@ func LicensesEditKeyPost(ctx *context.Context) {
key.ExpiresUnix = 0 key.ExpiresUnix = 0
} }
if err := licenses.UpdateLicenseKey(ctx, key); err != nil { if err := updateserver_model.UpdateLicenseKey(ctx, key); err != nil {
ctx.ServerError("UpdateLicenseKey", err) ctx.ServerError("UpdateLicenseKey", err)
return return
} }
@@ -493,14 +493,14 @@ func LicensesEditKeyPost(ctx *context.Context) {
// LicensesRevokeKey handles POST to revoke an org license key. // LicensesRevokeKey handles POST to revoke an org license key.
func LicensesRevokeKey(ctx *context.Context) { func LicensesRevokeKey(ctx *context.Context) {
keyID := ctx.PathParamInt64("id") keyID := ctx.PathParamInt64("id")
key, err := licenses.GetLicenseKeyByID(ctx, keyID) key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID)
if err != nil { if err != nil {
ctx.ServerError("GetLicenseKeyByID", err) ctx.ServerError("GetLicenseKeyByID", err)
return return
} }
key.IsActive = false key.IsActive = false
if err := licenses.UpdateLicenseKey(ctx, key); err != nil { if err := updateserver_model.UpdateLicenseKey(ctx, key); err != nil {
ctx.ServerError("UpdateLicenseKey", err) ctx.ServerError("UpdateLicenseKey", err)
return return
} }
@@ -512,13 +512,13 @@ func LicensesRevokeKey(ctx *context.Context) {
// LicensesRenewKey extends a license key's expiration by the package's duration. // LicensesRenewKey extends a license key's expiration by the package's duration.
func LicensesRenewKey(ctx *context.Context) { func LicensesRenewKey(ctx *context.Context) {
keyID := ctx.PathParamInt64("id") keyID := ctx.PathParamInt64("id")
key, err := licenses.GetLicenseKeyByID(ctx, keyID) key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID)
if err != nil { if err != nil {
ctx.ServerError("GetLicenseKeyByID", err) ctx.ServerError("GetLicenseKeyByID", err)
return return
} }
pkg, err := licenses.GetLicensePackageByID(ctx, key.PackageID) pkg, err := updateserver_model.GetLicensePackageByID(ctx, key.PackageID)
if err != nil { if err != nil {
ctx.ServerError("GetLicensePackageByID", err) ctx.ServerError("GetLicensePackageByID", err)
return return
@@ -529,7 +529,7 @@ func LicensesRenewKey(ctx *context.Context) {
days = 365 // default to 1 year for lifetime packages days = 365 // default to 1 year for lifetime packages
} }
if err := licenses.RenewLicenseKey(ctx, keyID, days); err != nil { if err := updateserver_model.RenewLicenseKey(ctx, keyID, days); err != nil {
ctx.ServerError("RenewLicenseKey", err) ctx.ServerError("RenewLicenseKey", err)
return return
} }
@@ -545,7 +545,7 @@ func LicensesDeleteKey(ctx *context.Context) {
return return
} }
keyID := ctx.PathParamInt64("id") keyID := ctx.PathParamInt64("id")
if err := licenses.DeleteLicenseKey(ctx, keyID); err != nil { if err := updateserver_model.DeleteLicenseKey(ctx, keyID); err != nil {
ctx.ServerError("DeleteLicenseKey", err) ctx.ServerError("DeleteLicenseKey", err)
return return
} }
+22
View File
@@ -126,6 +126,28 @@ func SettingsPost(ctx *context.Context) {
} }
} }
// Save wiki mode settings.
wikiMode := ctx.FormString("wiki_mode")
if wikiMode != "" && wikiMode != "external" {
wikiMode = ""
}
wikiURL := ctx.FormString("wiki_url")
if wikiMode == "external" && wikiURL != "" {
u, urlErr := url.Parse(wikiURL)
if urlErr != nil || (u.Scheme != "http" && u.Scheme != "https") {
ctx.Flash.Error("Wiki URL must be a valid http or https URL")
ctx.Redirect(ctx.Org.OrgLink + "/settings")
return
}
}
orgUser := org.AsUser()
orgUser.WikiMode = wikiMode
orgUser.WikiURL = wikiURL
if err := user_model.UpdateUserCols(ctx, orgUser, "wiki_mode", "wiki_url"); err != nil {
ctx.ServerError("UpdateUserCols(wiki)", err)
return
}
log.Trace("Organization setting updated: %s", org.Name) log.Trace("Organization setting updated: %s", org.Name)
ctx.Flash.Success(ctx.Tr("org.settings.update_setting_success")) ctx.Flash.Success(ctx.Tr("org.settings.update_setting_success"))
ctx.Redirect(ctx.Org.OrgLink + "/settings") ctx.Redirect(ctx.Org.OrgLink + "/settings")
+4 -4
View File
@@ -7,7 +7,7 @@ import (
"net/http" "net/http"
"strings" "strings"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses" updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
) )
@@ -22,7 +22,7 @@ func SettingsUpdateStreams(ctx *context.Context) {
orgID := ctx.Org.Organization.ID orgID := ctx.Org.Organization.ID
cfg, err := licenses.GetOrgConfig(ctx, orgID) cfg, err := updateserver_model.GetOrgConfig(ctx, orgID)
if err != nil { if err != nil {
ctx.ServerError("GetOrgConfig", err) ctx.ServerError("GetOrgConfig", err)
return return
@@ -37,7 +37,7 @@ func SettingsUpdateStreams(ctx *context.Context) {
func SettingsUpdateStreamsPost(ctx *context.Context) { func SettingsUpdateStreamsPost(ctx *context.Context) {
orgID := ctx.Org.Organization.ID orgID := ctx.Org.Organization.ID
cfg := &licenses.UpdateStreamConfig{ cfg := &updateserver_model.UpdateStreamConfig{
OwnerID: orgID, OwnerID: orgID,
RepoID: 0, RepoID: 0,
StreamMode: ctx.FormString("stream_mode"), StreamMode: ctx.FormString("stream_mode"),
@@ -64,7 +64,7 @@ func SettingsUpdateStreamsPost(ctx *context.Context) {
cfg.StreamMode = "joomla" cfg.StreamMode = "joomla"
} }
if err := licenses.SaveConfig(ctx, cfg); err != nil { if err := updateserver_model.SaveConfig(ctx, cfg); err != nil {
ctx.ServerError("SaveConfig", err) ctx.ServerError("SaveConfig", err)
return return
} }
+213
View File
@@ -0,0 +1,213 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package org
import (
"net/http"
"path"
"strings"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/renderhelper"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/gitrepo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/markup/markdown"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/util"
shared_user "code.mokoconsulting.tech/MokoConsulting/MokoGitea/routers/web/shared/user"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
const tplOrgWiki templates.TplName = "org/wiki/view"
// OrgWikiPage represents a single page in the org wiki sidebar.
type OrgWikiPage struct {
Name string
SubURL string
}
// Wiki renders the org wiki tab.
func Wiki(ctx *context.Context) {
org := ctx.Org.Organization
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
ctx.ServerError("RenderUserOrgHeader", err)
return
}
ctx.Data["PageIsViewWiki"] = true
ctx.Data["Title"] = org.DisplayName() + " - Wiki"
// Determine which wiki repo to use (public vs member).
viewAs := ctx.FormString("view_as", util.Iif(ctx.Org.IsMember, "member", "public"))
viewAsMember := viewAs == "member"
wikiRepo, commit := findOrgWikiCommit(ctx, org.ID, util.Iif(viewAsMember, shared_user.RepoNameWikiPrivate, shared_user.RepoNameWikiPublic))
if wikiRepo == nil && viewAsMember {
// Fall back to public wiki if member wiki doesn't exist.
wikiRepo, commit = findOrgWikiCommit(ctx, org.ID, shared_user.RepoNameWikiPublic)
viewAsMember = false
}
if wikiRepo == nil && !viewAsMember {
// Fall back to member wiki if public wiki doesn't exist.
wikiRepo, commit = findOrgWikiCommit(ctx, org.ID, shared_user.RepoNameWikiPrivate)
viewAsMember = true
}
ctx.Data["IsViewingWikiAsMember"] = viewAsMember
// Check whether both repos exist (for the dropdown toggle).
publicExists := shared_user.OrgWikiRepoExists(ctx, org.ID, shared_user.RepoNameWikiPublic)
privateExists := shared_user.OrgWikiRepoExists(ctx, org.ID, shared_user.RepoNameWikiPrivate)
ctx.Data["ShowWikiViewSelector"] = publicExists && privateExists && ctx.Org.IsMember
if wikiRepo == nil || commit == nil {
ctx.Data["WikiEmpty"] = true
ctx.HTML(http.StatusOK, tplOrgWiki)
return
}
ctx.Data["WikiRepoLink"] = wikiRepo.Link()
// Build page list from repo root.
entries, err := commit.ListEntries()
if err != nil {
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.
pageName := ctx.PathParamRaw("*")
if pageName == "" {
pageName = "Home"
}
ctx.Data["CurrentPage"] = pageName
// Try to find the file: exact match, then with .md extension.
blob := findWikiBlob(commit, pageName)
if blob == nil {
// Page not found — show empty state with page list.
ctx.Data["WikiPageNotFound"] = true
ctx.HTML(http.StatusOK, tplOrgWiki)
return
}
content, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize)
if err != nil {
ctx.ServerError("GetBlobContent", err)
return
}
rctx := renderhelper.NewRenderContextRepoFile(ctx, wikiRepo, renderhelper.RepoFileOptions{
CurrentRefPath: path.Join("branch", util.PathEscapeSegments(wikiRepo.DefaultBranch)),
})
renderedContent, err := markdown.RenderString(rctx, content)
if err != nil {
log.Error("Failed to render org wiki page %q: %v", pageName, err)
ctx.ServerError("RenderString", err)
return
}
ctx.Data["WikiContent"] = renderedContent
// Render _Sidebar if it exists.
sidebarBlob := findWikiBlob(commit, "_Sidebar")
if sidebarBlob != nil {
sidebarContent, err := sidebarBlob.GetBlobContent(setting.UI.MaxDisplayFileSize)
if err == nil {
rendered, err := markdown.RenderString(rctx, sidebarContent)
if err == nil {
ctx.Data["WikiSidebarHTML"] = rendered
}
}
}
// Render _Footer if it exists.
footerBlob := findWikiBlob(commit, "_Footer")
if footerBlob != nil {
footerContent, err := footerBlob.GetBlobContent(setting.UI.MaxDisplayFileSize)
if err == nil {
rendered, err := markdown.RenderString(rctx, footerContent)
if err == nil {
ctx.Data["WikiFooterHTML"] = rendered
}
}
}
ctx.HTML(http.StatusOK, tplOrgWiki)
}
// 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. .profile.wiki.git).
func findOrgWikiCommit(ctx *context.Context, orgID int64, repoName string) (*repo_model.Repository, *git.Commit) {
dbRepo, err := repo_model.GetRepositoryByName(ctx, orgID, repoName)
if err != nil {
if !repo_model.IsErrRepoNotExist(err) {
log.Error("findOrgWikiCommit: GetRepositoryByName(%d, %s): %v", orgID, repoName, err)
}
return nil, nil
}
// Open the wiki git repo (.wiki.git sidecar), not the main repo.
wikiGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, dbRepo.WikiStorageRepo())
if err != nil {
// Wiki repo doesn't exist yet — not an error, just no wiki.
return nil, nil
}
branch := dbRepo.DefaultWikiBranch
if branch == "" {
branch = "main"
}
commit, err := wikiGitRepo.GetBranchCommit(branch)
if err != nil {
log.Error("findOrgWikiCommit: GetBranchCommit wiki(%s, %s): %v", dbRepo.FullName(), branch, err)
return nil, nil
}
return dbRepo, commit
}
// findWikiBlob looks up a markdown file in the commit by name.
// Tries exact match first, then appends .md.
func findWikiBlob(commit *git.Commit, name string) *git.Blob {
// Try exact match (e.g., "Home.md").
if blob, _ := commit.GetBlobByPath(name); blob != nil {
return blob
}
// Try with .md extension (e.g., "Home" → "Home.md").
if blob, _ := commit.GetBlobByPath(name + ".md"); blob != nil {
return blob
}
// Try with .markdown extension.
if blob, _ := commit.GetBlobByPath(name + ".markdown"); blob != nil {
return blob
}
return nil
}
// isMarkdownFile returns true if the filename looks like a markdown file.
func isMarkdownFile(name string) bool {
ext := strings.ToLower(path.Ext(name))
return ext == ".md" || ext == ".markdown"
}
+2 -2
View File
@@ -9,7 +9,7 @@ import (
"net/http" "net/http"
"strings" "strings"
licenses_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses" updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/httplib" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/httplib"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
@@ -87,7 +87,7 @@ func CDNHandler(w http.ResponseWriter, req *http.Request) {
// If the release is assigned to an update stream, CDN is disabled - // If the release is assigned to an update stream, CDN is disabled -
// the update server handles distribution for streamed releases. // the update server handles distribution for streamed releases.
if stream := licenses_model.GetReleaseStream(req.Context(), release.ID); stream != "" { if stream := updateserver_model.GetReleaseStream(req.Context(), release.ID); stream != "" {
http.Error(w, "Forbidden: release is served via update stream", http.StatusForbidden) http.Error(w, "Forbidden: release is served via update stream", http.StatusForbidden)
return return
} }
+2 -2
View File
@@ -10,7 +10,7 @@ import (
"strings" "strings"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses" updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
) )
@@ -58,7 +58,7 @@ func ServeChangelogXML(ctx *context.Context) {
} }
// Get extension metadata for element name and type. // Get extension metadata for element name and type.
cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID) cfg := updateserver_model.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
element := strings.ToLower(repo.Name) element := strings.ToLower(repo.Name)
extType := "component" extType := "component"
if cfg != nil { if cfg != nil {
+7 -7
View File
@@ -4,7 +4,7 @@
package repo package repo
import ( import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses" updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
) )
@@ -20,7 +20,7 @@ func CheckDownloadGating(ctx *context.Context, tagName string) bool {
repo := ctx.Repo.Repository repo := ctx.Repo.Repository
// Check effective config (repo override → org default). // Check effective config (repo override → org default).
cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID) cfg := updateserver_model.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
if cfg == nil || !cfg.LicensingEnabled { if cfg == nil || !cfg.LicensingEnabled {
return true // licensing not enabled — allow all downloads return true // licensing not enabled — allow all downloads
} }
@@ -38,8 +38,8 @@ func CheckDownloadGating(ctx *context.Context, tagName string) bool {
// For prerelease-only gating, check if this is a prerelease tag. // For prerelease-only gating, check if this is a prerelease tag.
if gating == "prerelease" && tagName != "" { if gating == "prerelease" && tagName != "" {
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID) streams := updateserver_model.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
matched := licenses.MatchStreamFromTag(tagName, false, streams) matched := updateserver_model.MatchStreamFromTag(tagName, false, streams)
if matched == "stable" { if matched == "stable" {
return true // stable releases are public return true // stable releases are public
} }
@@ -60,14 +60,14 @@ func CheckDownloadGating(ctx *context.Context, tagName string) bool {
} }
domain := ctx.FormString("domain") domain := ctx.FormString("domain")
key, _, err := licenses.ValidateLicenseKeyForRepo(ctx, rawKey, domain, repo.ID) key, _, err := updateserver_model.ValidateLicenseKeyForRepo(ctx, rawKey, domain, repo.ID)
if err != nil { if err != nil {
log.Debug("Download gating: key validation failed: %v", err) log.Debug("Download gating: key validation failed: %v", err)
return false return false
} }
// Record heartbeat on successful download validation. // Record heartbeat on successful download validation.
_ = licenses.TouchHeartbeat(ctx, key.ID) _ = updateserver_model.TouchHeartbeat(ctx, key.ID)
return true return true
} }
@@ -76,7 +76,7 @@ func GetSupportURL(ctx *context.Context) string {
if ctx.Repo == nil || ctx.Repo.Repository == nil { if ctx.Repo == nil || ctx.Repo.Repository == nil {
return "" return ""
} }
cfg := licenses.GetEffectiveConfig(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID) cfg := updateserver_model.GetEffectiveConfig(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID)
if cfg != nil && cfg.SupportURL != "" { if cfg != nil && cfg.SupportURL != "" {
return cfg.SupportURL return cfg.SupportURL
} }
+22
View File
@@ -142,6 +142,14 @@ func NewIssue(ctx *context.Context) {
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.Permission.CanWrite(unit.TypeIssues) ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.Permission.CanWrite(unit.TypeIssues)
// Load org-level status/priority/type definitions for the new issue sidebar.
issueStatusDefs, _ := issues_model.GetIssueStatusDefsByOrg(ctx, ctx.Repo.Repository.OwnerID)
ctx.Data["IssueStatusDefs"] = issueStatusDefs
issuePriorityDefs, _ := issues_model.GetIssuePriorityDefsByOrg(ctx, ctx.Repo.Repository.OwnerID)
ctx.Data["IssuePriorityDefs"] = issuePriorityDefs
issueTypeDefs, _ := issues_model.GetIssueTypeDefsByOrg(ctx, ctx.Repo.Repository.OwnerID)
ctx.Data["IssueTypeDefs"] = issueTypeDefs
// Load org-level issue-scoped custom fields for the new issue sidebar. // Load org-level issue-scoped custom fields for the new issue sidebar.
customFieldDefs, cfErr := issues_model.GetCustomFieldsByOwner(ctx, ctx.Repo.Repository.OwnerID, issues_model.CustomFieldScopeIssue) customFieldDefs, cfErr := issues_model.GetCustomFieldsByOwner(ctx, ctx.Repo.Repository.OwnerID, issues_model.CustomFieldScopeIssue)
if cfErr != nil { if cfErr != nil {
@@ -408,6 +416,17 @@ func NewIssuePost(ctx *context.Context) {
return return
} }
// Save status/priority/type from sidebar dropdowns.
if statusID := ctx.FormInt64("status_id"); statusID > 0 {
_ = issues_model.SetIssueStatusID(ctx, issue.ID, statusID)
}
if priorityID := ctx.FormInt64("priority_id"); priorityID > 0 {
_ = issues_model.SetIssuePriorityID(ctx, issue.ID, priorityID)
}
if typeID := ctx.FormInt64("type_id"); typeID > 0 {
_ = issues_model.SetIssueTypeID(ctx, issue.ID, typeID)
}
// Save custom field values submitted from the new issue form. // Save custom field values submitted from the new issue form.
saveCustomFieldsFromForm(ctx, repo.OwnerID, issue.ID) saveCustomFieldsFromForm(ctx, repo.OwnerID, issue.ID)
@@ -429,6 +448,7 @@ func NewIssuePost(ctx *context.Context) {
// saveCustomFieldsFromForm reads custom field values from the form // saveCustomFieldsFromForm reads custom field values from the form
// (submitted as "custom-field-{fieldID}") and persists them for the issue. // (submitted as "custom-field-{fieldID}") and persists them for the issue.
// Returns true if all required fields are satisfied.
func saveCustomFieldsFromForm(ctx *context.Context, ownerID, issueID int64) { func saveCustomFieldsFromForm(ctx *context.Context, ownerID, issueID int64) {
defs, err := issues_model.GetCustomFieldsByOwner(ctx, ownerID, issues_model.CustomFieldScopeIssue) defs, err := issues_model.GetCustomFieldsByOwner(ctx, ownerID, issues_model.CustomFieldScopeIssue)
if err != nil { if err != nil {
@@ -443,6 +463,8 @@ func saveCustomFieldsFromForm(ctx *context.Context, ownerID, issueID int64) {
v := ctx.Req.FormValue(fmt.Sprintf("custom-field-%d", def.ID)) v := ctx.Req.FormValue(fmt.Sprintf("custom-field-%d", def.ID))
if v != "" { if v != "" {
vals[def.ID] = v vals[def.ID] = v
} else if def.Required {
ctx.Flash.Error(fmt.Sprintf("Custom field %q is required", def.Name))
} }
} }
if len(vals) > 0 { if len(vals) > 0 {
+58 -58
View File
@@ -9,7 +9,7 @@ import (
"strings" "strings"
"time" "time"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses" updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
unit_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit" unit_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/json" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/json"
@@ -48,7 +48,7 @@ type ChannelItem struct {
} }
// buildChannelItems converts stream definitions into combolist items. // buildChannelItems converts stream definitions into combolist items.
func buildChannelItems(streams []licenses.StreamDef) []ChannelItem { func buildChannelItems(streams []updateserver_model.StreamDef) []ChannelItem {
items := make([]ChannelItem, 0, len(streams)) items := make([]ChannelItem, 0, len(streams))
for _, s := range streams { for _, s := range streams {
label := s.Name label := s.Name
@@ -62,7 +62,7 @@ func buildChannelItems(streams []licenses.StreamDef) []ChannelItem {
// LicensePackageDisplay is used in templates. // LicensePackageDisplay is used in templates.
type LicensePackageDisplay struct { type LicensePackageDisplay struct {
*licenses.LicensePackage *updateserver_model.LicensePackage
KeyCount int64 KeyCount int64
Created time.Time Created time.Time
} }
@@ -80,7 +80,7 @@ func Licenses(ctx *context.Context) {
// Auto-create master package + key if admin and none exist. // Auto-create master package + key if admin and none exist.
if canWriteLicenses { if canWriteLicenses {
newMasterKey, err := licenses.EnsureMasterKey(ctx, ownerID) newMasterKey, err := updateserver_model.EnsureMasterKey(ctx, ownerID)
if err != nil { if err != nil {
ctx.ServerError("EnsureMasterKey", err) ctx.ServerError("EnsureMasterKey", err)
return return
@@ -91,10 +91,10 @@ func Licenses(ctx *context.Context) {
} }
// Always load the master key for display (prefix + status). // Always load the master key for display (prefix + status).
masterKey, _ := licenses.GetMasterKey(ctx, ownerID) masterKey, _ := updateserver_model.GetMasterKey(ctx, ownerID)
ctx.Data["MasterKey"] = masterKey ctx.Data["MasterKey"] = masterKey
pkgs, err := licenses.ListLicensePackages(ctx, ownerID) pkgs, err := updateserver_model.ListLicensePackages(ctx, ownerID)
if err != nil { if err != nil {
ctx.ServerError("ListLicensePackages", err) ctx.ServerError("ListLicensePackages", err)
return return
@@ -102,7 +102,7 @@ func Licenses(ctx *context.Context) {
var display []LicensePackageDisplay var display []LicensePackageDisplay
for _, pkg := range pkgs { for _, pkg := range pkgs {
count, _ := licenses.CountKeysByPackage(ctx, pkg.ID) count, _ := updateserver_model.CountKeysByPackage(ctx, pkg.ID)
display = append(display, LicensePackageDisplay{ display = append(display, LicensePackageDisplay{
LicensePackage: pkg, LicensePackage: pkg,
KeyCount: count, KeyCount: count,
@@ -115,11 +115,11 @@ func Licenses(ctx *context.Context) {
searchQuery := strings.TrimSpace(ctx.FormString("q")) searchQuery := strings.TrimSpace(ctx.FormString("q"))
ctx.Data["SearchQuery"] = searchQuery ctx.Data["SearchQuery"] = searchQuery
var keys []*licenses.LicenseKey var keys []*updateserver_model.LicenseKey
if searchQuery != "" { if searchQuery != "" {
keys, err = licenses.SearchLicenseKeys(ctx, ownerID, searchQuery) keys, err = updateserver_model.SearchLicenseKeys(ctx, ownerID, searchQuery)
} else { } else {
keys, err = licenses.ListLicenseKeys(ctx, ownerID) keys, err = updateserver_model.ListLicenseKeys(ctx, ownerID)
} }
if err != nil { if err != nil {
ctx.ServerError("ListLicenseKeys", err) ctx.ServerError("ListLicenseKeys", err)
@@ -129,10 +129,10 @@ func Licenses(ctx *context.Context) {
ctx.Data["CanDelete"] = canDeleteLicenses(ctx) ctx.Data["CanDelete"] = canDeleteLicenses(ctx)
// Load archived packages. // Load archived packages.
archivedPkgs, _ := licenses.ListArchivedLicensePackages(ctx, ownerID) archivedPkgs, _ := updateserver_model.ListArchivedLicensePackages(ctx, ownerID)
var archivedDisplay []LicensePackageDisplay var archivedDisplay []LicensePackageDisplay
for _, pkg := range archivedPkgs { for _, pkg := range archivedPkgs {
count, _ := licenses.CountKeysByPackage(ctx, pkg.ID) count, _ := updateserver_model.CountKeysByPackage(ctx, pkg.ID)
archivedDisplay = append(archivedDisplay, LicensePackageDisplay{ archivedDisplay = append(archivedDisplay, LicensePackageDisplay{
LicensePackage: pkg, LicensePackage: pkg,
KeyCount: count, KeyCount: count,
@@ -142,12 +142,12 @@ func Licenses(ctx *context.Context) {
ctx.Data["ArchivedPackages"] = archivedDisplay ctx.Data["ArchivedPackages"] = archivedDisplay
// Load available streams for the channels combolist. // Load available streams for the channels combolist.
orgCfg, _ := licenses.GetOrgConfig(ctx, ownerID) orgCfg, _ := updateserver_model.GetOrgConfig(ctx, ownerID)
var streams []licenses.StreamDef var streams []updateserver_model.StreamDef
if orgCfg != nil { if orgCfg != nil {
streams = orgCfg.GetActiveStreams() streams = orgCfg.GetActiveStreams()
} else { } else {
streams = licenses.DefaultJoomlaStreams() streams = updateserver_model.DefaultJoomlaStreams()
} }
ctx.Data["AvailableStreams"] = streams ctx.Data["AvailableStreams"] = streams
ctx.Data["ChannelItems"] = buildChannelItems(streams) ctx.Data["ChannelItems"] = buildChannelItems(streams)
@@ -186,7 +186,7 @@ func LicensesCreatePackage(ctx *context.Context) {
repoScope = "all" repoScope = "all"
} }
pkg := &licenses.LicensePackage{ pkg := &updateserver_model.LicensePackage{
OwnerID: ctx.Repo.Repository.OwnerID, OwnerID: ctx.Repo.Repository.OwnerID,
Name: name, Name: name,
Description: ctx.FormString("description"), Description: ctx.FormString("description"),
@@ -199,7 +199,7 @@ func LicensesCreatePackage(ctx *context.Context) {
IsActive: true, IsActive: true,
} }
if err := licenses.CreateLicensePackage(ctx, pkg); err != nil { if err := updateserver_model.CreateLicensePackage(ctx, pkg); err != nil {
ctx.ServerError("CreateLicensePackage", err) ctx.ServerError("CreateLicensePackage", err)
return return
} }
@@ -213,21 +213,21 @@ func LicensesRegenerateMasterKey(ctx *context.Context) {
ownerID := ctx.Repo.Repository.OwnerID ownerID := ctx.Repo.Repository.OwnerID
// Deactivate the old master key. // Deactivate the old master key.
oldKey, _ := licenses.GetMasterKey(ctx, ownerID) oldKey, _ := updateserver_model.GetMasterKey(ctx, ownerID)
if oldKey != nil { if oldKey != nil {
oldKey.IsActive = false oldKey.IsActive = false
_ = licenses.UpdateLicenseKey(ctx, oldKey) _ = updateserver_model.UpdateLicenseKey(ctx, oldKey)
} }
// Find the master package. // Find the master package.
pkgs, err := licenses.ListLicensePackages(ctx, ownerID) pkgs, err := updateserver_model.ListLicensePackages(ctx, ownerID)
if err != nil { if err != nil {
ctx.ServerError("ListLicensePackages", err) ctx.ServerError("ListLicensePackages", err)
return return
} }
var masterPkg *licenses.LicensePackage var masterPkg *updateserver_model.LicensePackage
for _, pkg := range pkgs { for _, pkg := range pkgs {
if pkg.Name == licenses.MasterPackageName { if pkg.Name == updateserver_model.MasterPackageName {
masterPkg = pkg masterPkg = pkg
break break
} }
@@ -239,13 +239,13 @@ func LicensesRegenerateMasterKey(ctx *context.Context) {
} }
// Create a new master key. // Create a new master key.
newKey := &licenses.LicenseKey{ newKey := &updateserver_model.LicenseKey{
PackageID: masterPkg.ID, PackageID: masterPkg.ID,
OwnerID: ownerID, OwnerID: ownerID,
IsInternal: true, IsInternal: true,
IsActive: true, IsActive: true,
} }
rawKey, err := licenses.CreateLicenseKey(ctx, newKey) rawKey, err := updateserver_model.CreateLicenseKey(ctx, newKey)
if err != nil { if err != nil {
ctx.ServerError("CreateLicenseKey", err) ctx.ServerError("CreateLicenseKey", err)
return return
@@ -270,7 +270,7 @@ func LicensesGenerateKey(ctx *context.Context) {
return return
} }
pkg, err := licenses.GetLicensePackageByID(ctx, packageID) pkg, err := updateserver_model.GetLicensePackageByID(ctx, packageID)
if err != nil { if err != nil {
ctx.ServerError("GetLicensePackageByID", err) ctx.ServerError("GetLicensePackageByID", err)
return return
@@ -282,7 +282,7 @@ func LicensesGenerateKey(ctx *context.Context) {
domainRestriction = pkg.DomainRestriction domainRestriction = pkg.DomainRestriction
} }
key := &licenses.LicenseKey{ key := &updateserver_model.LicenseKey{
PackageID: packageID, PackageID: packageID,
OwnerID: ctx.Repo.Repository.OwnerID, OwnerID: ctx.Repo.Repository.OwnerID,
IsActive: true, IsActive: true,
@@ -301,13 +301,13 @@ func LicensesGenerateKey(ctx *context.Context) {
var rawKey string var rawKey string
customKey := strings.TrimSpace(ctx.FormString("custom_key")) customKey := strings.TrimSpace(ctx.FormString("custom_key"))
if customKey != "" && (ctx.IsUserSiteAdmin() || ctx.Repo.Permission.IsOwner()) { if customKey != "" && (ctx.IsUserSiteAdmin() || ctx.Repo.Permission.IsOwner()) {
if err := licenses.CreateLicenseKeyCustom(ctx, key, customKey); err != nil { if err := updateserver_model.CreateLicenseKeyCustom(ctx, key, customKey); err != nil {
ctx.ServerError("CreateLicenseKeyCustom", err) ctx.ServerError("CreateLicenseKeyCustom", err)
return return
} }
rawKey = customKey rawKey = customKey
} else { } else {
rawKey, err = licenses.CreateLicenseKey(ctx, key) rawKey, err = updateserver_model.CreateLicenseKey(ctx, key)
if err != nil { if err != nil {
ctx.ServerError("CreateLicenseKey", err) ctx.ServerError("CreateLicenseKey", err)
return return
@@ -323,10 +323,10 @@ func LicensesGenerateKey(ctx *context.Context) {
// Re-render the page with the new key displayed. // Re-render the page with the new key displayed.
ownerID := ctx.Repo.Repository.OwnerID ownerID := ctx.Repo.Repository.OwnerID
pkgs, _ := licenses.ListLicensePackages(ctx, ownerID) pkgs, _ := updateserver_model.ListLicensePackages(ctx, ownerID)
var display []LicensePackageDisplay var display []LicensePackageDisplay
for _, p := range pkgs { for _, p := range pkgs {
count, _ := licenses.CountKeysByPackage(ctx, p.ID) count, _ := updateserver_model.CountKeysByPackage(ctx, p.ID)
display = append(display, LicensePackageDisplay{ display = append(display, LicensePackageDisplay{
LicensePackage: p, LicensePackage: p,
KeyCount: count, KeyCount: count,
@@ -334,15 +334,15 @@ func LicensesGenerateKey(ctx *context.Context) {
}) })
} }
ctx.Data["LicensePackages"] = display ctx.Data["LicensePackages"] = display
keys, _ := licenses.ListLicenseKeys(ctx, ownerID) keys, _ := updateserver_model.ListLicenseKeys(ctx, ownerID)
ctx.Data["LicenseKeys"] = keys ctx.Data["LicenseKeys"] = keys
orgCfg, _ := licenses.GetOrgConfig(ctx, ownerID) orgCfg, _ := updateserver_model.GetOrgConfig(ctx, ownerID)
var genStreams []licenses.StreamDef var genStreams []updateserver_model.StreamDef
if orgCfg != nil { if orgCfg != nil {
genStreams = orgCfg.GetActiveStreams() genStreams = orgCfg.GetActiveStreams()
} else { } else {
genStreams = licenses.DefaultJoomlaStreams() genStreams = updateserver_model.DefaultJoomlaStreams()
} }
ctx.Data["AvailableStreams"] = genStreams ctx.Data["AvailableStreams"] = genStreams
ctx.Data["ChannelItems"] = buildChannelItems(genStreams) ctx.Data["ChannelItems"] = buildChannelItems(genStreams)
@@ -353,14 +353,14 @@ func LicensesGenerateKey(ctx *context.Context) {
// LicensesRevokeKey handles POST to revoke a license key. // LicensesRevokeKey handles POST to revoke a license key.
func LicensesRevokeKey(ctx *context.Context) { func LicensesRevokeKey(ctx *context.Context) {
keyID := ctx.PathParamInt64("id") keyID := ctx.PathParamInt64("id")
key, err := licenses.GetLicenseKeyByID(ctx, keyID) key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID)
if err != nil { if err != nil {
ctx.ServerError("GetLicenseKeyByID", err) ctx.ServerError("GetLicenseKeyByID", err)
return return
} }
key.IsActive = false key.IsActive = false
if err := licenses.UpdateLicenseKey(ctx, key); err != nil { if err := updateserver_model.UpdateLicenseKey(ctx, key); err != nil {
ctx.ServerError("UpdateLicenseKey", err) ctx.ServerError("UpdateLicenseKey", err)
return return
} }
@@ -375,7 +375,7 @@ const tplLicensesEditKey templates.TplName = "repo/licenses_edit_key"
// LicensesEditKey shows the edit form for a license key. // LicensesEditKey shows the edit form for a license key.
func LicensesEditKey(ctx *context.Context) { func LicensesEditKey(ctx *context.Context) {
keyID := ctx.PathParamInt64("id") keyID := ctx.PathParamInt64("id")
key, err := licenses.GetLicenseKeyByID(ctx, keyID) key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID)
if err != nil { if err != nil {
ctx.ServerError("GetLicenseKeyByID", err) ctx.ServerError("GetLicenseKeyByID", err)
return return
@@ -404,7 +404,7 @@ func LicensesEditKey(ctx *context.Context) {
// LicensesEditKeyPost saves edits to a license key. // LicensesEditKeyPost saves edits to a license key.
func LicensesEditKeyPost(ctx *context.Context) { func LicensesEditKeyPost(ctx *context.Context) {
keyID := ctx.PathParamInt64("id") keyID := ctx.PathParamInt64("id")
key, err := licenses.GetLicenseKeyByID(ctx, keyID) key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID)
if err != nil { if err != nil {
ctx.ServerError("GetLicenseKeyByID", err) ctx.ServerError("GetLicenseKeyByID", err)
return return
@@ -433,7 +433,7 @@ func LicensesEditKeyPost(ctx *context.Context) {
key.ExpiresUnix = 0 key.ExpiresUnix = 0
} }
if err := licenses.UpdateLicenseKey(ctx, key); err != nil { if err := updateserver_model.UpdateLicenseKey(ctx, key); err != nil {
ctx.ServerError("UpdateLicenseKey", err) ctx.ServerError("UpdateLicenseKey", err)
return return
} }
@@ -445,13 +445,13 @@ func LicensesEditKeyPost(ctx *context.Context) {
// LicensesEditPackage shows the edit form for a license package. // LicensesEditPackage shows the edit form for a license package.
func LicensesEditPackage(ctx *context.Context) { func LicensesEditPackage(ctx *context.Context) {
pkgID := ctx.PathParamInt64("id") pkgID := ctx.PathParamInt64("id")
pkg, err := licenses.GetLicensePackageByID(ctx, pkgID) pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID)
if err != nil { if err != nil {
ctx.ServerError("GetLicensePackageByID", err) ctx.ServerError("GetLicensePackageByID", err)
return return
} }
if pkg.Name == licenses.MasterPackageName { if pkg.Name == updateserver_model.MasterPackageName {
ctx.Flash.Error("Master package cannot be edited") ctx.Flash.Error("Master package cannot be edited")
ctx.Redirect(ctx.Repo.RepoLink + "/licenses") ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
return return
@@ -465,12 +465,12 @@ func LicensesEditPackage(ctx *context.Context) {
ctx.Data["SelectedChannels"] = selectedChannels ctx.Data["SelectedChannels"] = selectedChannels
ownerID := ctx.Repo.Repository.OwnerID ownerID := ctx.Repo.Repository.OwnerID
orgCfg, _ := licenses.GetOrgConfig(ctx, ownerID) orgCfg, _ := updateserver_model.GetOrgConfig(ctx, ownerID)
var editStreams []licenses.StreamDef var editStreams []updateserver_model.StreamDef
if orgCfg != nil { if orgCfg != nil {
editStreams = orgCfg.GetActiveStreams() editStreams = orgCfg.GetActiveStreams()
} else { } else {
editStreams = licenses.DefaultJoomlaStreams() editStreams = updateserver_model.DefaultJoomlaStreams()
} }
ctx.Data["AvailableStreams"] = editStreams ctx.Data["AvailableStreams"] = editStreams
ctx.Data["ChannelItems"] = buildChannelItems(editStreams) ctx.Data["ChannelItems"] = buildChannelItems(editStreams)
@@ -482,13 +482,13 @@ func LicensesEditPackage(ctx *context.Context) {
// LicensesEditPackagePost saves edits to a license package. // LicensesEditPackagePost saves edits to a license package.
func LicensesEditPackagePost(ctx *context.Context) { func LicensesEditPackagePost(ctx *context.Context) {
pkgID := ctx.PathParamInt64("id") pkgID := ctx.PathParamInt64("id")
pkg, err := licenses.GetLicensePackageByID(ctx, pkgID) pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID)
if err != nil { if err != nil {
ctx.ServerError("GetLicensePackageByID", err) ctx.ServerError("GetLicensePackageByID", err)
return return
} }
if pkg.Name == licenses.MasterPackageName { if pkg.Name == updateserver_model.MasterPackageName {
ctx.Flash.Error("Master package cannot be edited") ctx.Flash.Error("Master package cannot be edited")
ctx.Redirect(ctx.Repo.RepoLink + "/licenses") ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
return return
@@ -518,7 +518,7 @@ func LicensesEditPackagePost(ctx *context.Context) {
pkg.DomainRestriction = strings.TrimSpace(ctx.FormString("domain_restriction")) pkg.DomainRestriction = strings.TrimSpace(ctx.FormString("domain_restriction"))
pkg.IsActive = ctx.FormString("is_active") == "on" pkg.IsActive = ctx.FormString("is_active") == "on"
if err := licenses.UpdateLicensePackage(ctx, pkg); err != nil { if err := updateserver_model.UpdateLicensePackage(ctx, pkg); err != nil {
ctx.ServerError("UpdateLicensePackage", err) ctx.ServerError("UpdateLicensePackage", err)
return return
} }
@@ -535,17 +535,17 @@ func canDeleteLicenses(ctx *context.Context) bool {
// LicensesArchivePackage archives a license package. // LicensesArchivePackage archives a license package.
func LicensesArchivePackage(ctx *context.Context) { func LicensesArchivePackage(ctx *context.Context) {
pkgID := ctx.PathParamInt64("id") pkgID := ctx.PathParamInt64("id")
pkg, err := licenses.GetLicensePackageByID(ctx, pkgID) pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID)
if err != nil { if err != nil {
ctx.ServerError("GetLicensePackageByID", err) ctx.ServerError("GetLicensePackageByID", err)
return return
} }
if pkg.Name == licenses.MasterPackageName { if pkg.Name == updateserver_model.MasterPackageName {
ctx.Flash.Error("Master package cannot be archived") ctx.Flash.Error("Master package cannot be archived")
ctx.Redirect(ctx.Repo.RepoLink + "/licenses") ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
return return
} }
if err := licenses.ArchiveLicensePackage(ctx, pkgID); err != nil { if err := updateserver_model.ArchiveLicensePackage(ctx, pkgID); err != nil {
ctx.ServerError("ArchiveLicensePackage", err) ctx.ServerError("ArchiveLicensePackage", err)
return return
} }
@@ -556,7 +556,7 @@ func LicensesArchivePackage(ctx *context.Context) {
// LicensesUnarchivePackage removes archive status from a package. // LicensesUnarchivePackage removes archive status from a package.
func LicensesUnarchivePackage(ctx *context.Context) { func LicensesUnarchivePackage(ctx *context.Context) {
pkgID := ctx.PathParamInt64("id") pkgID := ctx.PathParamInt64("id")
if err := licenses.UnarchiveLicensePackage(ctx, pkgID); err != nil { if err := updateserver_model.UnarchiveLicensePackage(ctx, pkgID); err != nil {
ctx.ServerError("UnarchiveLicensePackage", err) ctx.ServerError("UnarchiveLicensePackage", err)
return return
} }
@@ -571,17 +571,17 @@ func LicensesDeletePackage(ctx *context.Context) {
return return
} }
pkgID := ctx.PathParamInt64("id") pkgID := ctx.PathParamInt64("id")
pkg, err := licenses.GetLicensePackageByID(ctx, pkgID) pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID)
if err != nil { if err != nil {
ctx.ServerError("GetLicensePackageByID", err) ctx.ServerError("GetLicensePackageByID", err)
return return
} }
if pkg.Name == licenses.MasterPackageName { if pkg.Name == updateserver_model.MasterPackageName {
ctx.Flash.Error("Master package cannot be deleted") ctx.Flash.Error("Master package cannot be deleted")
ctx.Redirect(ctx.Repo.RepoLink + "/licenses") ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
return return
} }
if err := licenses.DeleteLicensePackage(ctx, pkgID); err != nil { if err := updateserver_model.DeleteLicensePackage(ctx, pkgID); err != nil {
ctx.ServerError("DeleteLicensePackage", err) ctx.ServerError("DeleteLicensePackage", err)
return return
} }
@@ -593,13 +593,13 @@ func LicensesDeletePackage(ctx *context.Context) {
// LicensesRenewKey extends a license key's expiration by the package's duration. // LicensesRenewKey extends a license key's expiration by the package's duration.
func LicensesRenewKey(ctx *context.Context) { func LicensesRenewKey(ctx *context.Context) {
keyID := ctx.PathParamInt64("id") keyID := ctx.PathParamInt64("id")
key, err := licenses.GetLicenseKeyByID(ctx, keyID) key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID)
if err != nil { if err != nil {
ctx.ServerError("GetLicenseKeyByID", err) ctx.ServerError("GetLicenseKeyByID", err)
return return
} }
pkg, err := licenses.GetLicensePackageByID(ctx, key.PackageID) pkg, err := updateserver_model.GetLicensePackageByID(ctx, key.PackageID)
if err != nil { if err != nil {
ctx.ServerError("GetLicensePackageByID", err) ctx.ServerError("GetLicensePackageByID", err)
return return
@@ -610,7 +610,7 @@ func LicensesRenewKey(ctx *context.Context) {
days = 365 // default to 1 year for lifetime packages days = 365 // default to 1 year for lifetime packages
} }
if err := licenses.RenewLicenseKey(ctx, keyID, days); err != nil { if err := updateserver_model.RenewLicenseKey(ctx, keyID, days); err != nil {
ctx.ServerError("RenewLicenseKey", err) ctx.ServerError("RenewLicenseKey", err)
return return
} }
@@ -626,7 +626,7 @@ func LicensesDeleteKey(ctx *context.Context) {
return return
} }
keyID := ctx.PathParamInt64("id") keyID := ctx.PathParamInt64("id")
if err := licenses.DeleteLicenseKey(ctx, keyID); err != nil { if err := updateserver_model.DeleteLicenseKey(ctx, keyID); err != nil {
ctx.ServerError("DeleteLicenseKey", err) ctx.ServerError("DeleteLicenseKey", err)
return return
} }
+7 -7
View File
@@ -14,7 +14,7 @@ import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git" git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
licenses_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses" updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/renderhelper" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/renderhelper"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit"
@@ -359,11 +359,11 @@ func newReleaseCommon(ctx *context.Context) {
// Load available streams for the stream selector (when licensing enabled). // Load available streams for the stream selector (when licensing enabled).
if ctx.Data["LicensingEnabled"] == true { if ctx.Data["LicensingEnabled"] == true {
ownerID := ctx.Repo.Repository.OwnerID ownerID := ctx.Repo.Repository.OwnerID
orgCfg, _ := licenses_model.GetOrgConfig(ctx, ownerID) orgCfg, _ := updateserver_model.GetOrgConfig(ctx, ownerID)
if orgCfg != nil { if orgCfg != nil {
ctx.Data["AvailableStreams"] = orgCfg.GetActiveStreams() ctx.Data["AvailableStreams"] = orgCfg.GetActiveStreams()
} else { } else {
ctx.Data["AvailableStreams"] = licenses_model.DefaultJoomlaStreams() ctx.Data["AvailableStreams"] = updateserver_model.DefaultJoomlaStreams()
} }
} }
@@ -534,7 +534,7 @@ func NewReleasePost(ctx *context.Context) {
} }
// Save manual stream assignment if specified. // Save manual stream assignment if specified.
if streamName := form.UpdateStream; streamName != "" { if streamName := form.UpdateStream; streamName != "" {
_ = licenses_model.SetReleaseStream(ctx, rel.ID, rel.RepoID, streamName) _ = updateserver_model.SetReleaseStream(ctx, rel.ID, rel.RepoID, streamName)
} }
ctx.Redirect(ctx.Repo.RepoLink + "/releases") ctx.Redirect(ctx.Repo.RepoLink + "/releases")
return return
@@ -596,7 +596,7 @@ func EditRelease(ctx *context.Context) {
ctx.Data["content"] = rel.Note ctx.Data["content"] = rel.Note
ctx.Data["prerelease"] = rel.IsPrerelease ctx.Data["prerelease"] = rel.IsPrerelease
ctx.Data["IsDraft"] = rel.IsDraft ctx.Data["IsDraft"] = rel.IsDraft
releaseStream := licenses_model.GetReleaseStream(ctx, rel.ID) releaseStream := updateserver_model.GetReleaseStream(ctx, rel.ID)
ctx.Data["ReleaseStream"] = releaseStream ctx.Data["ReleaseStream"] = releaseStream
ctx.Data["ReleaseHasStream"] = releaseStream != "" ctx.Data["ReleaseHasStream"] = releaseStream != ""
ctx.Data["CDNEnabled"] = setting.CDN.Enabled ctx.Data["CDNEnabled"] = setting.CDN.Enabled
@@ -682,9 +682,9 @@ func EditReleasePost(ctx *context.Context) {
} }
// Save manual stream assignment. // Save manual stream assignment.
if streamName := form.UpdateStream; streamName != "" { if streamName := form.UpdateStream; streamName != "" {
_ = licenses_model.SetReleaseStream(ctx, rel.ID, rel.RepoID, streamName) _ = updateserver_model.SetReleaseStream(ctx, rel.ID, rel.RepoID, streamName)
} else { } else {
_ = licenses_model.DeleteReleaseStream(ctx, rel.ID) _ = updateserver_model.DeleteReleaseStream(ctx, rel.ID)
} }
// Update per-asset CDN visibility flags. // Update per-asset CDN visibility flags.
+4 -57
View File
@@ -4,68 +4,15 @@
package setting package setting
import ( import (
"net/http"
licenses_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
) )
const tplSettingsLicensing templates.TplName = "repo/settings/licensing" // LicensingSettings redirects to the manifest page where licensing is now consolidated.
// LicensingSettings displays the licensing settings page.
func LicensingSettings(ctx *context.Context) { func LicensingSettings(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.licensing_section") ctx.Redirect(ctx.Repo.RepoLink + "/settings/updateserver")
ctx.Data["PageIsSettingsLicensing"] = true
repoCfg, _ := licenses_model.GetRepoConfig(ctx, ctx.Repo.Repository.ID)
ctx.Data["RepoUpdateConfig"] = repoCfg
ctx.HTML(http.StatusOK, tplSettingsLicensing)
} }
// LicensingSettingsPost saves the licensing settings. // LicensingSettingsPost redirects POST to the manifest page.
func LicensingSettingsPost(ctx *context.Context) { func LicensingSettingsPost(ctx *context.Context) {
repo := ctx.Repo.Repository ctx.Redirect(ctx.Repo.RepoLink + "/settings/updateserver")
updatePlatform := ctx.FormString("update_platform")
if updatePlatform == "" {
updatePlatform = "joomla"
}
enabled := ctx.FormString("enable_licensing") == "on"
if !enabled {
// Remove repo-level override so org config takes effect
if err := licenses_model.DeleteRepoConfig(ctx, repo.ID); err != nil {
log.Error("DeleteRepoConfig: %v", err)
}
} else {
updateCfg := &licenses_model.UpdateStreamConfig{
OwnerID: repo.OwnerID,
RepoID: repo.ID,
Platform: updatePlatform,
LicensingEnabled: true,
RequireKey: ctx.FormString("require_update_key") == "on",
DownloadGating: ctx.FormString("download_gating"),
SupportURL: ctx.FormString("support_url"),
ExtensionName: ctx.FormString("extension_name"),
DisplayName: ctx.FormString("display_name"),
ExtensionType: ctx.FormString("extension_type"),
TargetVersion: ctx.FormString("target_version"),
Maintainer: ctx.FormString("maintainer"),
PHPMinimum: ctx.FormString("php_minimum"),
StreamMode: "joomla",
}
if err := licenses_model.SaveConfig(ctx, updateCfg); err != nil {
log.Error("SaveConfig: %v", err)
ctx.ServerError("SaveConfig", err)
return
}
}
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/licensing")
} }
-167
View File
@@ -1,167 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package setting
import (
"encoding/xml"
"fmt"
"net/http"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
const tplSettingsManifest templates.TplName = "repo/settings/manifest"
// manifestXML mirrors the .mokogitea/manifest.xml schema for XML parsing.
type manifestXML struct {
XMLName xml.Name `xml:"mokoplatform"`
Identity manifestIdentity `xml:"identity"`
Governance manifestGovernance `xml:"governance"`
Build manifestBuild `xml:"build"`
}
type manifestIdentity struct {
Name string `xml:"name"`
Org string `xml:"org"`
Description string `xml:"description"`
Version string `xml:"version"`
VersionPrefix string `xml:"version-prefix"`
License manifestLicense `xml:"license"`
}
type manifestLicense struct {
SPDX string `xml:"spdx,attr"`
Name string `xml:",chardata"`
}
type manifestGovernance struct {
Platform string `xml:"platform"`
StandardsVersion string `xml:"standards-version"`
StandardsSource string `xml:"standards-source"`
}
type manifestBuild struct {
Language string `xml:"language"`
PackageType string `xml:"package-type"`
EntryPoint string `xml:"entry-point"`
}
// ManifestSettings displays the repo manifest settings page.
// On first visit, if no manifest exists in DB but .mokogitea/manifest.xml
// exists in the repo, it auto-migrates the XML values into the database.
func ManifestSettings(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.manifest")
ctx.Data["PageIsSettingsManifest"] = true
repoID := ctx.Repo.Repository.ID
manifest, err := repo_model.GetRepoManifest(ctx, repoID)
if err != nil {
ctx.ServerError("GetRepoManifest", err)
return
}
// Auto-detect and migrate .mokogitea/manifest.xml if no DB record exists.
if manifest == nil {
manifest = tryMigrateManifestXML(ctx)
}
if manifest == nil {
// No manifest found — provide empty defaults from repo metadata.
manifest = &repo_model.RepoManifest{
RepoID: repoID,
Name: ctx.Repo.Repository.Name,
Org: ctx.Repo.Repository.OwnerName,
Description: ctx.Repo.Repository.Description,
}
}
ctx.Data["Manifest"] = manifest
ctx.HTML(http.StatusOK, tplSettingsManifest)
}
// ManifestSettingsPost saves manifest settings from the form.
func ManifestSettingsPost(ctx *context.Context) {
manifest := &repo_model.RepoManifest{
RepoID: ctx.Repo.Repository.ID,
Name: ctx.FormString("name"),
Org: ctx.FormString("org"),
Description: ctx.Repo.Repository.Description,
Version: ctx.FormString("version"),
LicenseSPDX: ctx.FormString("license_spdx"),
LicenseName: ctx.FormString("license_name"),
VersionPrefix: ctx.FormString("version_prefix"),
ElementName: ctx.FormString("element_name"),
Platform: ctx.FormString("platform"),
StandardsVersion: ctx.FormString("standards_version"),
StandardsSource: ctx.FormString("standards_source"),
Language: ctx.FormString("language"),
PackageType: ctx.FormString("package_type"),
EntryPoint: ctx.FormString("entry_point"),
}
if err := repo_model.CreateOrUpdateRepoManifest(ctx, manifest); err != nil {
ctx.ServerError("CreateOrUpdateRepoManifest", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.manifest_saved"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/manifest")
}
// tryMigrateManifestXML reads .mokogitea/manifest.xml from the repo,
// parses it, and stores the values in the DB. Returns nil if no file found.
func tryMigrateManifestXML(ctx *context.Context) *repo_model.RepoManifest {
if ctx.Repo.GitRepo == nil || ctx.Repo.Commit == nil {
return nil
}
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(".mokogitea/manifest.xml")
if err != nil || entry == nil {
return nil // no manifest.xml found — not an error
}
reader, err := entry.Blob().DataAsync()
if err != nil {
log.Error("ManifestMigrate: read blob: %v", err)
return nil
}
defer reader.Close()
var mxml manifestXML
if err := xml.NewDecoder(reader).Decode(&mxml); err != nil {
log.Error("ManifestMigrate: parse XML: %v", err)
return nil
}
manifest := &repo_model.RepoManifest{
RepoID: ctx.Repo.Repository.ID,
Name: mxml.Identity.Name,
Org: mxml.Identity.Org,
Description: mxml.Identity.Description,
Version: mxml.Identity.Version,
LicenseSPDX: mxml.Identity.License.SPDX,
LicenseName: mxml.Identity.License.Name,
VersionPrefix: mxml.Identity.VersionPrefix,
Platform: mxml.Governance.Platform,
StandardsVersion: mxml.Governance.StandardsVersion,
StandardsSource: mxml.Governance.StandardsSource,
Language: mxml.Build.Language,
PackageType: mxml.Build.PackageType,
EntryPoint: mxml.Build.EntryPoint,
}
if err := repo_model.CreateOrUpdateRepoManifest(ctx, manifest); err != nil {
log.Error("ManifestMigrate: save to DB: %v", err)
return nil
}
log.Info("ManifestMigrate: migrated .mokogitea/manifest.xml for repo %s/%s",
ctx.Repo.Repository.OwnerName, ctx.Repo.Repository.Name)
ctx.Flash.Info(fmt.Sprintf("Manifest settings imported from .mokogitea/manifest.xml. You can now delete the file from the repository."))
return manifest
}
+96 -5
View File
@@ -9,20 +9,39 @@ import (
"net/http" "net/http"
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues" issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
) )
const tplSettingsMetadata templates.TplName = "repo/settings/metadata" const tplSettingsMetadata templates.TplName = "repo/settings/metadata"
// Metadata displays the repo metadata page (repo-scoped custom field values). // Metadata displays the consolidated metadata page:
// project identity (manifest), update server config, and repo-scoped custom fields.
func Metadata(ctx *context.Context) { func Metadata(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.metadata") ctx.Data["Title"] = "Metadata"
ctx.Data["PageIsSettingsMetadata"] = true ctx.Data["PageIsSettingsMetadata"] = true
ownerID := ctx.Repo.Repository.OwnerID
repoID := ctx.Repo.Repository.ID repoID := ctx.Repo.Repository.ID
ownerID := ctx.Repo.Repository.OwnerID
// Load manifest (project identity).
manifest, err := repo_model.GetRepoManifest(ctx, repoID)
if err != nil {
ctx.ServerError("GetRepoManifest", err)
return
}
if manifest == nil {
manifest = &repo_model.RepoManifest{
RepoID: repoID,
Name: ctx.Repo.Repository.Name,
Org: ctx.Repo.Repository.OwnerName,
Description: ctx.Repo.Repository.Description,
}
}
ctx.Data["Manifest"] = manifest
// Load repo-scoped custom fields.
fields, _ := issues_model.GetCustomFieldsByOwner(ctx, ownerID, issues_model.CustomFieldScopeRepo) fields, _ := issues_model.GetCustomFieldsByOwner(ctx, ownerID, issues_model.CustomFieldScopeRepo)
ctx.Data["CustomFieldDefs"] = fields ctx.Data["CustomFieldDefs"] = fields
@@ -45,8 +64,79 @@ func Metadata(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplSettingsMetadata) ctx.HTML(http.StatusOK, tplSettingsMetadata)
} }
// MetadataPost saves repo-scoped custom field values. // MetadataPost routes to the correct sub-handler based on the action param.
func MetadataPost(ctx *context.Context) { func MetadataPost(ctx *context.Context) {
switch ctx.FormString("action") {
case "manifest":
saveManifest(ctx)
case "customfields":
saveCustomFields(ctx)
default:
saveManifest(ctx)
}
}
// spdxToName maps SPDX identifiers to human-readable license names.
var spdxToName = map[string]string{
"GPL-3.0-or-later": "GNU General Public License v3 or later",
"GPL-2.0-or-later": "GNU General Public License v2 or later",
"MIT": "MIT License",
"Apache-2.0": "Apache License 2.0",
"BSD-3-Clause": "BSD 3-Clause License",
"BSD-2-Clause": "BSD 2-Clause License",
"LGPL-3.0-or-later": "GNU Lesser General Public License v3 or later",
"MPL-2.0": "Mozilla Public License 2.0",
"ISC": "ISC License",
"AGPL-3.0-or-later": "GNU Affero General Public License v3 or later",
"Unlicense": "The Unlicense",
"proprietary": "Proprietary",
}
func saveManifest(ctx *context.Context) {
spdx := ctx.FormString("license_spdx")
licenseName := spdxToName[spdx]
// Preserve existing values for fields removed from the UI.
existing, _ := repo_model.GetRepoManifest(ctx, ctx.Repo.Repository.ID)
manifest := &repo_model.RepoManifest{
RepoID: ctx.Repo.Repository.ID,
Name: ctx.FormString("name"),
Org: ctx.FormString("org"),
Description: ctx.Repo.Repository.Description,
Version: ctx.FormString("version"),
LicenseSPDX: spdx,
LicenseName: licenseName,
VersionPrefix: ctx.FormString("version_prefix"),
Platform: ctx.FormString("platform"),
InfoURL: ctx.FormString("info_url"),
TargetVersion: ctx.FormString("target_version"),
PHPMinimum: ctx.FormString("php_minimum"),
PackageType: ctx.FormString("package_type"),
EntryPoint: ctx.FormString("entry_point"),
}
// Preserve fields not in the UI but still in the model.
if existing != nil {
manifest.ElementName = existing.ElementName
manifest.StandardsVersion = existing.StandardsVersion
manifest.StandardsSource = existing.StandardsSource
manifest.DisplayName = existing.DisplayName
manifest.Maintainer = existing.Maintainer
manifest.MaintainerURL = existing.MaintainerURL
manifest.Language = existing.Language
}
if err := repo_model.CreateOrUpdateRepoManifest(ctx, manifest); err != nil {
ctx.ServerError("CreateOrUpdateRepoManifest", err)
return
}
ctx.Flash.Success("Project identity saved")
ctx.Redirect(ctx.Repo.RepoLink + "/settings/metadata")
}
func saveCustomFields(ctx *context.Context) {
repoID := ctx.Repo.Repository.ID repoID := ctx.Repo.Repository.ID
ownerID := ctx.Repo.Repository.OwnerID ownerID := ctx.Repo.Repository.OwnerID
@@ -59,6 +149,7 @@ func MetadataPost(ctx *context.Context) {
} }
} }
ctx.Flash.Success(ctx.Tr("repo.settings.metadata_saved")) ctx.Flash.Success("Custom fields saved")
ctx.Redirect(ctx.Repo.RepoLink + "/settings/metadata") ctx.Redirect(ctx.Repo.RepoLink + "/settings/metadata")
} }
+5 -5
View File
@@ -12,7 +12,7 @@ import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/organization" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/organization"
licenses_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses" updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/perm" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/perm"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
unit_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit" unit_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit"
@@ -101,7 +101,7 @@ func SettingsCtxData(ctx *context.Context) {
// Settings show a repository's settings page // Settings show a repository's settings page
func Settings(ctx *context.Context) { func Settings(ctx *context.Context) {
repoCfg, _ := licenses_model.GetRepoConfig(ctx, ctx.Repo.Repository.ID) repoCfg, _ := updateserver_model.GetRepoConfig(ctx, ctx.Repo.Repository.ID)
ctx.Data["RepoUpdateConfig"] = repoCfg ctx.Data["RepoUpdateConfig"] = repoCfg
ctx.HTML(http.StatusOK, tplSettingsOptions) ctx.HTML(http.StatusOK, tplSettingsOptions)
} }
@@ -679,7 +679,7 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
// so it falls through to org defaults cleanly. // so it falls through to org defaults cleanly.
if !form.EnableLicensing { if !form.EnableLicensing {
// Remove repo-level override so org config takes effect // Remove repo-level override so org config takes effect
if err := licenses_model.DeleteRepoConfig(ctx, repo.ID); err != nil { if err := updateserver_model.DeleteRepoConfig(ctx, repo.ID); err != nil {
log.Error("DeleteRepoConfig: %v", err) log.Error("DeleteRepoConfig: %v", err)
} }
} else { } else {
@@ -687,7 +687,7 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
if updatePlatform == "" { if updatePlatform == "" {
updatePlatform = "joomla" updatePlatform = "joomla"
} }
updateCfg := &licenses_model.UpdateStreamConfig{ updateCfg := &updateserver_model.UpdateStreamConfig{
OwnerID: repo.OwnerID, OwnerID: repo.OwnerID,
RepoID: repo.ID, RepoID: repo.ID,
Platform: updatePlatform, Platform: updatePlatform,
@@ -703,7 +703,7 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
PHPMinimum: form.PHPMinimum, PHPMinimum: form.PHPMinimum,
StreamMode: "joomla", StreamMode: "joomla",
} }
if err := licenses_model.SaveConfig(ctx, updateCfg); err != nil { if err := updateserver_model.SaveConfig(ctx, updateCfg); err != nil {
log.Error("SaveConfig: %v", err) log.Error("SaveConfig: %v", err)
} }
} }
+71
View File
@@ -0,0 +1,71 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package setting
import (
"net/http"
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
const tplSettingsUpdateServer templates.TplName = "repo/settings/updateserver"
// UpdateServerSettings displays the update server settings page.
func UpdateServerSettings(ctx *context.Context) {
ctx.Data["Title"] = "Update Server"
ctx.Data["PageIsSettingsUpdateServer"] = true
repoCfg, _ := updateserver_model.GetRepoConfig(ctx, ctx.Repo.Repository.ID)
ctx.Data["RepoUpdateConfig"] = repoCfg
ctx.HTML(http.StatusOK, tplSettingsUpdateServer)
}
// UpdateServerSettingsPost saves update server visibility and gating settings.
func UpdateServerSettingsPost(ctx *context.Context) {
repo := ctx.Repo.Repository
enabled := ctx.FormString("enable_licensing") == "on"
if !enabled {
if err := updateserver_model.DeleteRepoConfig(ctx, repo.ID); err != nil {
log.Error("DeleteRepoConfig: %v", err)
}
} else {
// Load existing config to preserve platform and other fields.
existing, _ := updateserver_model.GetRepoConfig(ctx, repo.ID)
platform := "joomla"
streamMode := "joomla"
supportURL := ""
if existing != nil {
platform = existing.Platform
streamMode = existing.StreamMode
supportURL = existing.SupportURL
}
updateCfg := &updateserver_model.UpdateStreamConfig{
OwnerID: repo.OwnerID,
RepoID: repo.ID,
Platform: platform,
LicensingEnabled: true,
RequireKey: ctx.FormString("require_update_key") == "on",
DownloadGating: ctx.FormString("download_gating"),
FeedVisibility: ctx.FormString("feed_visibility"),
SupportURL: supportURL,
StreamMode: streamMode,
}
if err := updateserver_model.SaveConfig(ctx, updateCfg); err != nil {
log.Error("SaveConfig: %v", err)
ctx.ServerError("SaveConfig", err)
return
}
}
ctx.Flash.Success("Update server settings saved")
ctx.Redirect(ctx.Repo.RepoLink + "/settings/updateserver")
}
+5 -5
View File
@@ -7,7 +7,7 @@ import (
"net/http" "net/http"
"strings" "strings"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses" updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/json" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/json"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
@@ -33,15 +33,15 @@ func validateUpdateKey(ctx *context.Context) (allowedChannels []string, ok bool,
} }
domain := ctx.FormString("domain") domain := ctx.FormString("domain")
key, pkg, err := licenses.ValidateLicenseKeyForRepo(ctx, rawKey, domain, ctx.Repo.Repository.ID) key, pkg, err := updateserver_model.ValidateLicenseKeyForRepo(ctx, rawKey, domain, ctx.Repo.Repository.ID)
if err != nil { if err != nil {
log.Debug("License key validation failed: %v", err) log.Debug("License key validation failed: %v", err)
return nil, false, false return nil, false, false
} }
// Update heartbeat and record usage. // Update heartbeat and record usage.
_ = licenses.TouchHeartbeat(ctx, key.ID) _ = updateserver_model.TouchHeartbeat(ctx, key.ID)
_ = licenses.RecordUsage(ctx, &licenses.LicenseKeyUsage{ _ = updateserver_model.RecordUsage(ctx, &updateserver_model.LicenseKeyUsage{
KeyID: key.ID, KeyID: key.ID,
RepoID: ctx.Repo.Repository.ID, RepoID: ctx.Repo.Repository.ID,
Domain: domain, Domain: domain,
@@ -93,7 +93,7 @@ func ServeUpdatesXML(ctx *context.Context) {
return return
} }
repoCfg, _ := licenses.GetRepoConfig(ctx, ctx.Repo.Repository.ID) repoCfg, _ := updateserver_model.GetRepoConfig(ctx, ctx.Repo.Repository.ID)
// Show <downloadkey> only when downloads are gated (prerelease or all). // Show <downloadkey> only when downloads are gated (prerelease or all).
// No gating = no license keys needed = no downloadkey element. // No gating = no license keys needed = no downloadkey element.
requireKey := repoCfg != nil && repoCfg.DownloadGating != "" && repoCfg.DownloadGating != "none" requireKey := repoCfg != nil && repoCfg.DownloadGating != "" && repoCfg.DownloadGating != "none"
+31
View File
@@ -137,6 +137,8 @@ type PrepareOwnerHeaderResult struct {
const ( const (
RepoNameProfilePrivate = ".profile-private" RepoNameProfilePrivate = ".profile-private"
RepoNameProfile = ".profile" RepoNameProfile = ".profile"
RepoNameWikiPublic = ".profile"
RepoNameWikiPrivate = ".profile-private"
) )
func RenderUserOrgHeader(ctx *context.Context) (result *PrepareOwnerHeaderResult, err error) { func RenderUserOrgHeader(ctx *context.Context) (result *PrepareOwnerHeaderResult, err error) {
@@ -155,6 +157,18 @@ func RenderUserOrgHeader(ctx *context.Context) (result *PrepareOwnerHeaderResult
result.ProfilePrivateRepo, result.ProfilePrivateReadmeBlob = FindOwnerProfileReadme(ctx, ctx.Doer, RepoNameProfilePrivate) result.ProfilePrivateRepo, result.ProfilePrivateReadmeBlob = FindOwnerProfileReadme(ctx, ctx.Doer, RepoNameProfilePrivate)
result.HasOrgProfileReadme = result.ProfilePublicReadmeBlob != nil || result.ProfilePrivateReadmeBlob != nil result.HasOrgProfileReadme = result.ProfilePublicReadmeBlob != nil || result.ProfilePrivateReadmeBlob != nil
ctx.Data["HasOrgProfileReadme"] = result.HasOrgProfileReadme // many pages need it to show the "overview" tab ctx.Data["HasOrgProfileReadme"] = result.HasOrgProfileReadme // many pages need it to show the "overview" tab
// Check if org has a wiki (internal convention repos or external URL).
orgUser := ctx.ContextUser
if orgUser.WikiMode == "external" && orgUser.WikiURL != "" {
ctx.Data["HasOrgWiki"] = true
ctx.Data["OrgWikiIsExternal"] = true
ctx.Data["OrgWikiExternalURL"] = orgUser.WikiURL
} else {
hasWiki := OrgWikiRepoExists(ctx, ctx.ContextUser.ID, RepoNameWikiPublic) ||
OrgWikiRepoExists(ctx, ctx.ContextUser.ID, RepoNameWikiPrivate)
ctx.Data["HasOrgWiki"] = hasWiki
}
} else { } else {
_, profileReadmeBlob := FindOwnerProfileReadme(ctx, ctx.Doer) _, profileReadmeBlob := FindOwnerProfileReadme(ctx, ctx.Doer)
ctx.Data["HasUserProfileReadme"] = profileReadmeBlob != nil ctx.Data["HasUserProfileReadme"] = profileReadmeBlob != nil
@@ -194,3 +208,20 @@ func loadHeaderCount(ctx *context.Context) error {
return nil return nil
} }
// OrgWikiRepoExists checks whether a profile repo's wiki exists and has content.
func OrgWikiRepoExists(ctx *context.Context, ownerID int64, repoName string) bool {
dbRepo, err := repo_model.GetRepositoryByName(ctx, ownerID, repoName)
if err != nil {
log.Trace("OrgWikiRepoExists: repo %s not found for owner %d: %v", repoName, ownerID, err)
return false
}
wikiRepo := dbRepo.WikiStorageRepo()
_, err = gitrepo.GetDefaultBranch(ctx, wikiRepo)
if err != nil {
log.Error("OrgWikiRepoExists: GetDefaultBranch for wiki of %s failed: %v (path: %s)", dbRepo.FullName(), err, wikiRepo.RelativePath())
return false
}
log.Trace("OrgWikiRepoExists: wiki found for %s", dbRepo.FullName())
return true
}
+8 -2
View File
@@ -1162,6 +1162,11 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Get("/repositories", org.Repositories) m.Get("/repositories", org.Repositories)
m.Get("/heatmap", user.DashboardHeatmap) m.Get("/heatmap", user.DashboardHeatmap)
m.Group("/wiki", func() {
m.Get("", org.Wiki)
m.Get("/*", org.Wiki)
})
m.Group("/projects", func() { m.Group("/projects", func() {
m.Group("", func() { m.Group("", func() {
m.Get("", org.Projects) m.Get("", org.Projects)
@@ -1224,9 +1229,10 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Group("", func() { m.Group("", func() {
m.Combo("/advanced").Get(repo_setting.AdvancedSettings).Post(web.Bind(forms.RepoSettingForm{}), repo_setting.SettingsPost) m.Combo("/advanced").Get(repo_setting.AdvancedSettings).Post(web.Bind(forms.RepoSettingForm{}), repo_setting.SettingsPost)
}, repo_setting.SettingsCtxData) }, repo_setting.SettingsCtxData)
m.Combo("/licensing").Get(repo_setting.LicensingSettings).Post(repo_setting.LicensingSettingsPost)
m.Combo("/manifest").Get(repo_setting.ManifestSettings).Post(repo_setting.ManifestSettingsPost)
m.Combo("/metadata").Get(repo_setting.Metadata).Post(repo_setting.MetadataPost) m.Combo("/metadata").Get(repo_setting.Metadata).Post(repo_setting.MetadataPost)
m.Combo("/updateserver").Get(repo_setting.UpdateServerSettings).Post(repo_setting.UpdateServerSettingsPost)
m.Combo("/manifest").Get(repo_setting.LicensingSettings).Post(repo_setting.LicensingSettingsPost) // redirect
m.Combo("/licensing").Get(repo_setting.LicensingSettings).Post(repo_setting.LicensingSettingsPost) // redirect
m.Group("/security", func() { m.Group("/security", func() {
m.Combo("").Get(repo_setting.SecuritySettings).Post(repo_setting.SecuritySettingsPost) m.Combo("").Get(repo_setting.SecuritySettings).Post(repo_setting.SecuritySettingsPost)
m.Post("/scan", repo_setting.SecurityScanNow) m.Post("/scan", repo_setting.SecurityScanNow)
+6 -6
View File
@@ -18,7 +18,7 @@ import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git" git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues" issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
licenses_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses" updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
access_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/perm/access" access_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/perm/access"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
unit_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit" unit_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit"
@@ -438,8 +438,8 @@ func repoAssignmentLegacy(ctx *Context, data *repoAssignmentPrepareDataStruct) {
// Check if licensing is enabled — licensed repos allow access to // Check if licensing is enabled — licensed repos allow access to
// releases and downloads via license key, even without membership. // releases and downloads via license key, even without membership.
orgCfg, _ := licenses_model.GetOrgConfig(ctx, repo.OwnerID) orgCfg, _ := updateserver_model.GetOrgConfig(ctx, repo.OwnerID)
repoCfg, _ := licenses_model.GetRepoConfig(ctx, repo.ID) repoCfg, _ := updateserver_model.GetRepoConfig(ctx, repo.ID)
licensingEnabled := (orgCfg != nil && orgCfg.LicensingEnabled) || licensingEnabled := (orgCfg != nil && orgCfg.LicensingEnabled) ||
(repoCfg != nil && repoCfg.LicensingEnabled) (repoCfg != nil && repoCfg.LicensingEnabled)
@@ -652,12 +652,12 @@ func repoAssignmentPrepareTemplateData(ctx *Context, data *repoAssignmentPrepare
} }
// Check if licensing is enabled for this repo/org. // Check if licensing is enabled for this repo/org.
orgCfg, _ := licenses_model.GetOrgConfig(ctx, repo.OwnerID) orgCfg, _ := updateserver_model.GetOrgConfig(ctx, repo.OwnerID)
repoUpdateCfg, _ := licenses_model.GetRepoConfig(ctx, repo.ID) repoUpdateCfg, _ := updateserver_model.GetRepoConfig(ctx, repo.ID)
licensingEnabled := (orgCfg != nil && orgCfg.LicensingEnabled) || licensingEnabled := (orgCfg != nil && orgCfg.LicensingEnabled) ||
(repoUpdateCfg != nil && repoUpdateCfg.LicensingEnabled) (repoUpdateCfg != nil && repoUpdateCfg.LicensingEnabled)
numLicensePackages, _ := db.Count[licenses_model.LicensePackage](ctx, licenses_model.FindLicensePackageOptions{ numLicensePackages, _ := db.Count[updateserver_model.LicensePackage](ctx, updateserver_model.FindLicensePackageOptions{
OwnerID: repo.OwnerID, OwnerID: repo.OwnerID,
}) })
ctx.Data["NumLicensePackages"] = numLicensePackages ctx.Data["NumLicensePackages"] = numLicensePackages
+2 -2
View File
@@ -4,7 +4,7 @@
package context package context
import ( import (
licenses_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses" updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user" user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
@@ -43,7 +43,7 @@ func RepoAssignmentPublicFeed() func(ctx *Context) {
ctx.Repo.Repository = repo ctx.Repo.Repository = repo
// Load update config for platform-aware routing. // Load update config for platform-aware routing.
repoUpdateCfg, _ := licenses_model.GetRepoConfig(ctx, repo.ID) repoUpdateCfg, _ := updateserver_model.GetRepoConfig(ctx, repo.ID)
if repoUpdateCfg != nil { if repoUpdateCfg != nil {
ctx.Data["RepoUpdatePlatform"] = repoUpdateCfg.Platform ctx.Data["RepoUpdatePlatform"] = repoUpdateCfg.Platform
} else { } else {
+20
View File
@@ -131,6 +131,26 @@ func toIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Iss
apiIssue.Deadline = issue.DeadlineUnix.AsTimePtr() apiIssue.Deadline = issue.DeadlineUnix.AsTimePtr()
} }
// Populate org-level issue metadata (status/priority/type)
apiIssue.StatusID = issue.StatusID
apiIssue.PriorityID = issue.PriorityID
apiIssue.TypeID = issue.TypeID
if issue.StatusID > 0 {
if def, err := issues_model.GetIssueStatusDefByID(ctx, issue.StatusID); err == nil {
apiIssue.StatusName = def.Name
}
}
if issue.PriorityID > 0 {
if def, err := issues_model.GetIssuePriorityDefByID(ctx, issue.PriorityID); err == nil {
apiIssue.PriorityName = def.Name
}
}
if issue.TypeID > 0 {
if def, err := issues_model.GetIssueTypeDefByID(ctx, issue.TypeID); err == nil {
apiIssue.TypeName = def.Name
}
}
return apiIssue return apiIssue
} }
+2 -2
View File
@@ -9,7 +9,7 @@ import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git" git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
licenses_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses" updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user" user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/webhook" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/webhook"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git/gitcmd" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git/gitcmd"
@@ -167,7 +167,7 @@ func registerCleanupExpiredLicenseKeys() {
Schedule: "@weekly", Schedule: "@weekly",
}, func(ctx context.Context, _ *user_model.User, config Config) error { }, func(ctx context.Context, _ *user_model.User, config Config) error {
// Delete non-internal keys that expired more than 365 days ago. // Delete non-internal keys that expired more than 365 days ago.
deleted, err := licenses_model.DeleteExpiredKeys(ctx, 365) deleted, err := updateserver_model.DeleteExpiredKeys(ctx, 365)
if err != nil { if err != nil {
return err return err
} }
+4 -4
View File
@@ -11,7 +11,7 @@ import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git" git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
licenses_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses" updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user" user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/container" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/container"
@@ -192,8 +192,8 @@ func validateTagAgainstStreams(ctx context.Context, rel *repo_model.Release) err
} }
// Check if licensing is enabled at org or repo level. // Check if licensing is enabled at org or repo level.
orgCfg, _ := licenses_model.GetOrgConfig(ctx, repo.OwnerID) orgCfg, _ := updateserver_model.GetOrgConfig(ctx, repo.OwnerID)
repoCfg, _ := licenses_model.GetRepoConfig(ctx, repo.ID) repoCfg, _ := updateserver_model.GetRepoConfig(ctx, repo.ID)
licensingEnabled := (orgCfg != nil && orgCfg.LicensingEnabled) || licensingEnabled := (orgCfg != nil && orgCfg.LicensingEnabled) ||
(repoCfg != nil && repoCfg.LicensingEnabled) (repoCfg != nil && repoCfg.LicensingEnabled)
@@ -203,7 +203,7 @@ func validateTagAgainstStreams(ctx context.Context, rel *repo_model.Release) err
// Check that the tag contains a stream-compatible suffix. // Check that the tag contains a stream-compatible suffix.
// Any prerelease suffix in the tag must match a configured stream suffix. // Any prerelease suffix in the tag must match a configured stream suffix.
streams := licenses_model.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID) streams := updateserver_model.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
lower := strings.ToLower(rel.TagName) lower := strings.ToLower(rel.TagName)
for _, s := range streams { for _, s := range streams {
if s.Suffix == "" { if s.Suffix == "" {
+20 -4
View File
@@ -14,10 +14,20 @@ import (
// manifestXML mirrors the .mokogitea/manifest.xml schema for XML parsing. // manifestXML mirrors the .mokogitea/manifest.xml schema for XML parsing.
type manifestXML struct { type manifestXML struct {
XMLName xml.Name `xml:"mokoplatform"` XMLName xml.Name `xml:"mokoplatform"`
Identity manifestIdentity `xml:"identity"` Identity manifestIdentity `xml:"identity"`
Governance manifestGovernance `xml:"governance"` Governance manifestGovernance `xml:"governance"`
Build manifestBuild `xml:"build"` Distribution manifestDistribution `xml:"distribution"`
Build manifestBuild `xml:"build"`
}
type manifestDistribution struct {
DisplayName string `xml:"display-name"`
Maintainer string `xml:"maintainer"`
MaintainerURL string `xml:"maintainer-url"`
InfoURL string `xml:"info-url"`
TargetVersion string `xml:"target-version"`
PHPMinimum string `xml:"php-minimum"`
} }
type manifestIdentity struct { type manifestIdentity struct {
@@ -88,6 +98,12 @@ func SyncManifestFromCommit(ctx context.Context, repo *repo_model.Repository, co
Platform: mxml.Governance.Platform, Platform: mxml.Governance.Platform,
StandardsVersion: mxml.Governance.StandardsVersion, StandardsVersion: mxml.Governance.StandardsVersion,
StandardsSource: mxml.Governance.StandardsSource, StandardsSource: mxml.Governance.StandardsSource,
DisplayName: mxml.Distribution.DisplayName,
Maintainer: mxml.Distribution.Maintainer,
MaintainerURL: mxml.Distribution.MaintainerURL,
InfoURL: mxml.Distribution.InfoURL,
TargetVersion: mxml.Distribution.TargetVersion,
PHPMinimum: mxml.Distribution.PHPMinimum,
Language: mxml.Build.Language, Language: mxml.Build.Language,
PackageType: mxml.Build.PackageType, PackageType: mxml.Build.PackageType,
EntryPoint: mxml.Build.EntryPoint, EntryPoint: mxml.Build.EntryPoint,
+4 -4
View File
@@ -10,7 +10,7 @@ import (
"time" "time"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses" updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
) )
@@ -67,7 +67,7 @@ func GenerateComposerJSON(ctx context.Context, repo *repo_model.Repository, lice
baseURL := strings.TrimSuffix(setting.AppURL, "/") baseURL := strings.TrimSuffix(setting.AppURL, "/")
repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name) repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name)
cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID) cfg := updateserver_model.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
meta := resolveExtensionMetadata(ctx, repo, cfg) meta := resolveExtensionMetadata(ctx, repo, cfg)
// Composer package name: vendor/package (override with resolved extension name if set) // Composer package name: vendor/package (override with resolved extension name if set)
@@ -91,7 +91,7 @@ func GenerateComposerJSON(ctx context.Context, repo *repo_model.Repository, lice
phpMin = ">=" + meta.PHPMinimum phpMin = ">=" + meta.PHPMinimum
} }
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID) streams := updateserver_model.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
versions := make(map[string]ComposerVersion) versions := make(map[string]ComposerVersion)
for _, rel := range releases { for _, rel := range releases {
@@ -99,7 +99,7 @@ func GenerateComposerJSON(ctx context.Context, repo *repo_model.Repository, lice
continue continue
} }
ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams) ch := updateserver_model.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
if ch != "stable" { if ch != "stable" {
continue // Composer only serves stable versions continue // Composer only serves stable versions
} }
+3 -3
View File
@@ -10,7 +10,7 @@ import (
"time" "time"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses" updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
) )
@@ -59,7 +59,7 @@ func GenerateDolibarrJSON(ctx context.Context, repo *repo_model.Repository, allo
} }
// Resolve effective streams. // Resolve effective streams.
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID) streams := updateserver_model.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
// Track best release per channel. // Track best release per channel.
bestByChannel := make(map[string]*repo_model.Release) bestByChannel := make(map[string]*repo_model.Release)
@@ -67,7 +67,7 @@ func GenerateDolibarrJSON(ctx context.Context, repo *repo_model.Repository, allo
if rel.IsDraft || rel.IsTag { if rel.IsDraft || rel.IsTag {
continue continue
} }
ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams) ch := updateserver_model.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
existing, ok := bestByChannel[ch] existing, ok := bestByChannel[ch]
if !ok || rel.CreatedUnix > existing.CreatedUnix { if !ok || rel.CreatedUnix > existing.CreatedUnix {
bestByChannel[ch] = rel bestByChannel[ch] = rel
+4 -4
View File
@@ -10,7 +10,7 @@ import (
"strings" "strings"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses" updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
) )
@@ -65,12 +65,12 @@ func GenerateDrupalXML(ctx context.Context, repo *repo_model.Repository, allowed
baseURL := strings.TrimSuffix(setting.AppURL, "/") baseURL := strings.TrimSuffix(setting.AppURL, "/")
repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name) repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name)
cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID) cfg := updateserver_model.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
meta := resolveExtensionMetadata(ctx, repo, cfg) meta := resolveExtensionMetadata(ctx, repo, cfg)
shortName := meta.Element shortName := meta.Element
title := meta.DisplayName title := meta.DisplayName
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID) streams := updateserver_model.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
channelAllowed := make(map[string]bool) channelAllowed := make(map[string]bool)
if len(allowedChannels) > 0 { if len(allowedChannels) > 0 {
@@ -97,7 +97,7 @@ func GenerateDrupalXML(ctx context.Context, repo *repo_model.Repository, allowed
if rel.IsDraft || rel.IsTag { if rel.IsDraft || rel.IsTag {
continue continue
} }
ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams) ch := updateserver_model.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
if len(channelAllowed) > 0 && !channelAllowed[ch] { if len(channelAllowed) > 0 && !channelAllowed[ch] {
continue continue
} }
+49 -91
View File
@@ -13,8 +13,7 @@ import (
"time" "time"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues" updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/storage" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/storage"
@@ -112,7 +111,7 @@ func channelFromTag(tagName string, isPrerelease bool) string {
// isStreamName checks if a string matches any stream name (indicating the tag // isStreamName checks if a string matches any stream name (indicating the tag
// is a stream name, not a version number). // is a stream name, not a version number).
func isStreamName(s string, streams []licenses.StreamDef) bool { func isStreamName(s string, streams []updateserver_model.StreamDef) bool {
for _, st := range streams { for _, st := range streams {
if strings.EqualFold(st.Name, s) { if strings.EqualFold(st.Name, s) {
return true return true
@@ -163,7 +162,7 @@ func NormalizeChannel(ch string) string {
} }
// extensionMetadata holds resolved metadata for feed generation. // extensionMetadata holds resolved metadata for feed generation.
// Fields are resolved with priority: custom field → config table → default. // Fields are resolved with priority: manifest → config table (gating only) → default.
type extensionMetadata struct { type extensionMetadata struct {
Element string Element string
DisplayName string DisplayName string
@@ -176,9 +175,10 @@ type extensionMetadata struct {
KeyPrefix string KeyPrefix string
} }
// resolveExtensionMetadata loads extension metadata with cascading fallback: // resolveExtensionMetadata loads extension metadata from the repo manifest API.
// org-level repo-scoped custom fields → update_stream_config → repo-derived defaults. // The manifest is the single source of truth for extension identity fields.
func resolveExtensionMetadata(ctx context.Context, repo *repo_model.Repository, cfg *licenses.UpdateStreamConfig) extensionMetadata { // The config table is only used for licensing/gating fields not in the manifest.
func resolveExtensionMetadata(ctx context.Context, repo *repo_model.Repository, cfg *updateserver_model.UpdateStreamConfig) extensionMetadata {
m := extensionMetadata{ m := extensionMetadata{
Element: strings.ToLower(repo.Name), Element: strings.ToLower(repo.Name),
DisplayName: fmt.Sprintf("%s - %s", repo.Owner.Name, repo.Name), DisplayName: fmt.Sprintf("%s - %s", repo.Owner.Name, repo.Name),
@@ -186,91 +186,49 @@ func resolveExtensionMetadata(ctx context.Context, repo *repo_model.Repository,
TargetVersion: "(5|6)\\..*", TargetVersion: "(5|6)\\..*",
} }
// Apply config table values. // Manifest is the source of truth for extension metadata.
manifest, err := repo_model.GetRepoManifest(ctx, repo.ID)
if err != nil {
log.Error("resolveExtensionMetadata: GetRepoManifest for repo %d: %v", repo.ID, err)
}
if manifest != nil {
if manifest.ElementName != "" {
m.Element = manifest.ElementName
}
if manifest.PackageType != "" {
m.ExtType = manifest.PackageType
}
if manifest.DisplayName != "" {
m.DisplayName = manifest.DisplayName
}
if manifest.TargetVersion != "" {
m.TargetVersion = manifest.TargetVersion
}
if manifest.PHPMinimum != "" {
m.PHPMinimum = manifest.PHPMinimum
}
if manifest.Description != "" {
m.Description = manifest.Description
}
if manifest.InfoURL != "" {
m.SupportURL = manifest.InfoURL
}
}
// Config table: only licensing/gating fields (not in manifest).
if cfg != nil { if cfg != nil {
if cfg.ExtensionName != "" {
m.Element = cfg.ExtensionName
}
if cfg.DisplayName != "" {
m.DisplayName = cfg.DisplayName
}
if cfg.ExtensionType != "" {
m.ExtType = cfg.ExtensionType
}
if cfg.TargetVersion != "" {
m.TargetVersion = cfg.TargetVersion
}
if cfg.PHPMinimum != "" {
m.PHPMinimum = cfg.PHPMinimum
}
if cfg.Description != "" {
m.Description = cfg.Description
}
if cfg.SupportURL != "" {
m.SupportURL = cfg.SupportURL
}
if cfg.DownloadGating != "" { if cfg.DownloadGating != "" {
m.DownloadGating = cfg.DownloadGating m.DownloadGating = cfg.DownloadGating
} }
if cfg.KeyPrefix != "" { if cfg.KeyPrefix != "" {
m.KeyPrefix = cfg.KeyPrefix m.KeyPrefix = cfg.KeyPrefix
} }
} // SupportURL from config as fallback if manifest.InfoURL is empty
if m.SupportURL == "" && cfg.SupportURL != "" {
// Override with custom field values (highest priority). m.SupportURL = cfg.SupportURL
fields, err := issues_model.GetCustomFieldsByOwner(ctx, repo.OwnerID, issues_model.CustomFieldScopeRepo)
if err != nil {
log.Error("resolveExtensionMetadata: GetCustomFieldsByOwner for repo %d: %v", repo.ID, err)
return m
}
if len(fields) == 0 {
return m
}
values, err := issues_model.GetCustomFieldValuesMap(ctx, repo.ID)
if err != nil {
log.Error("resolveExtensionMetadata: GetCustomFieldValuesMap for repo %d: %v", repo.ID, err)
return m
}
if len(values) == 0 {
return m
}
// Build name → value map from field definitions + values.
named := make(map[string]string, len(fields))
for _, f := range fields {
if v, ok := values[f.ID]; ok && v != "" {
named[f.Name] = v
} }
} }
if v := named["Extension Name"]; v != "" {
m.Element = v
}
if v := named["Display Name"]; v != "" {
m.DisplayName = v
}
if v := named["Extension Type"]; v != "" {
m.ExtType = v
}
if v := named["Target Version"]; v != "" {
m.TargetVersion = v
}
if v := named["PHP Minimum"]; v != "" {
m.PHPMinimum = v
}
if v := named["Support URL"]; v != "" {
m.SupportURL = v
}
if v := named["Description"]; v != "" {
m.Description = v
}
if v := named["Download Gating"]; v != "" {
m.DownloadGating = v
}
if v := named["Key Prefix"]; v != "" {
m.KeyPrefix = v
}
return m return m
} }
@@ -301,7 +259,7 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require
// Load extension metadata with cascading fallback: // Load extension metadata with cascading fallback:
// custom fields → config table → repo-derived defaults. // custom fields → config table → repo-derived defaults.
cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID) cfg := updateserver_model.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
meta := resolveExtensionMetadata(ctx, repo, cfg) meta := resolveExtensionMetadata(ctx, repo, cfg)
element := meta.Element element := meta.Element
@@ -322,7 +280,7 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require
} }
// Resolve effective streams (repo override → org default → Joomla default). // Resolve effective streams (repo override → org default → Joomla default).
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID) streams := updateserver_model.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
// Track best (latest) release per channel to emit one entry per channel. // Track best (latest) release per channel to emit one entry per channel.
bestByChannel := make(map[string]*repo_model.Release) bestByChannel := make(map[string]*repo_model.Release)
@@ -330,7 +288,7 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require
if rel.IsDraft || rel.IsTag { if rel.IsDraft || rel.IsTag {
continue continue
} }
ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams) ch := updateserver_model.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
existing, ok := bestByChannel[ch] existing, ok := bestByChannel[ch]
if !ok || rel.CreatedUnix > existing.CreatedUnix { if !ok || rel.CreatedUnix > existing.CreatedUnix {
bestByChannel[ch] = rel bestByChannel[ch] = rel
@@ -422,13 +380,13 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require
infoURL = meta.SupportURL infoURL = meta.SupportURL
} }
// Joomla <client> element: packages use client_id=0 in #__extensions, // Joomla <client> element: admin-side extensions use "administrator",
// so we must output <client>0</client> for Joomla to match the update // site-side extensions use "site". Packages, components, libraries,
// to the installed extension. Other types default to "site" (client_id=0) // and files are admin-side by default.
// or "administrator" (client_id=1).
client := "site" client := "site"
if extType == "package" { switch extType {
client = "0" case "package", "component", "library", "file":
client = "administrator"
} }
u := xmlUpdate{ u := xmlUpdate{
+4 -4
View File
@@ -11,7 +11,7 @@ import (
"time" "time"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses" updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
) )
@@ -54,7 +54,7 @@ func GeneratePrestaShopXML(ctx context.Context, repo *repo_model.Repository, all
baseURL := strings.TrimSuffix(setting.AppURL, "/") baseURL := strings.TrimSuffix(setting.AppURL, "/")
repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name) repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name)
cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID) cfg := updateserver_model.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
meta := resolveExtensionMetadata(ctx, repo, cfg) meta := resolveExtensionMetadata(ctx, repo, cfg)
moduleName := meta.Element moduleName := meta.Element
displayName := meta.DisplayName displayName := meta.DisplayName
@@ -64,7 +64,7 @@ func GeneratePrestaShopXML(ctx context.Context, repo *repo_model.Repository, all
maintainer = cfg.Maintainer maintainer = cfg.Maintainer
} }
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID) streams := updateserver_model.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
// Channel filtering. // Channel filtering.
channelAllowed := make(map[string]bool) channelAllowed := make(map[string]bool)
@@ -80,7 +80,7 @@ func GeneratePrestaShopXML(ctx context.Context, repo *repo_model.Repository, all
if rel.IsDraft || rel.IsTag { if rel.IsDraft || rel.IsTag {
continue continue
} }
ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams) ch := updateserver_model.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
existing, ok := bestByChannel[ch] existing, ok := bestByChannel[ch]
if !ok || rel.CreatedUnix > existing.CreatedUnix { if !ok || rel.CreatedUnix > existing.CreatedUnix {
bestByChannel[ch] = rel bestByChannel[ch] = rel
+4 -4
View File
@@ -10,7 +10,7 @@ import (
"time" "time"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses" updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
) )
@@ -49,7 +49,7 @@ func GenerateWHMCSJSON(ctx context.Context, repo *repo_model.Repository, license
baseURL := strings.TrimSuffix(setting.AppURL, "/") baseURL := strings.TrimSuffix(setting.AppURL, "/")
repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name) repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name)
cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID) cfg := updateserver_model.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
meta := resolveExtensionMetadata(ctx, repo, cfg) meta := resolveExtensionMetadata(ctx, repo, cfg)
displayName := meta.DisplayName displayName := meta.DisplayName
description := meta.Description description := meta.Description
@@ -64,7 +64,7 @@ func GenerateWHMCSJSON(ctx context.Context, repo *repo_model.Repository, license
} }
} }
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID) streams := updateserver_model.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
// Find latest stable release. // Find latest stable release.
var latestStable *repo_model.Release var latestStable *repo_model.Release
@@ -72,7 +72,7 @@ func GenerateWHMCSJSON(ctx context.Context, repo *repo_model.Repository, license
if rel.IsDraft || rel.IsTag { if rel.IsDraft || rel.IsTag {
continue continue
} }
ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams) ch := updateserver_model.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
if ch == "stable" { if ch == "stable" {
if latestStable == nil || rel.CreatedUnix > latestStable.CreatedUnix { if latestStable == nil || rel.CreatedUnix > latestStable.CreatedUnix {
latestStable = rel latestStable = rel
+6 -6
View File
@@ -11,7 +11,7 @@ import (
"time" "time"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses" updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
) )
@@ -59,7 +59,7 @@ func GenerateWordPressJSON(ctx context.Context, repo *repo_model.Repository, lic
// Load extension metadata with cascading fallback: // Load extension metadata with cascading fallback:
// custom fields → config table → repo-derived defaults. // custom fields → config table → repo-derived defaults.
cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID) cfg := updateserver_model.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
meta := resolveExtensionMetadata(ctx, repo, cfg) meta := resolveExtensionMetadata(ctx, repo, cfg)
slug := meta.Element slug := meta.Element
@@ -84,13 +84,13 @@ func GenerateWordPressJSON(ctx context.Context, repo *repo_model.Repository, lic
} }
// Resolve streams and find the latest stable release. // Resolve streams and find the latest stable release.
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID) streams := updateserver_model.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
var latestStable *repo_model.Release var latestStable *repo_model.Release
for _, rel := range releases { for _, rel := range releases {
if rel.IsDraft || rel.IsTag { if rel.IsDraft || rel.IsTag {
continue continue
} }
ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams) ch := updateserver_model.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
if ch == "stable" { if ch == "stable" {
if latestStable == nil || rel.CreatedUnix > latestStable.CreatedUnix { if latestStable == nil || rel.CreatedUnix > latestStable.CreatedUnix {
latestStable = rel latestStable = rel
@@ -171,14 +171,14 @@ func GenerateWordPressJSON(ctx context.Context, repo *repo_model.Repository, lic
} }
// buildWordPressChangelog builds an HTML changelog from multiple releases. // buildWordPressChangelog builds an HTML changelog from multiple releases.
func buildWordPressChangelog(ctx context.Context, releases []*repo_model.Release, streams []licenses.StreamDef) string { func buildWordPressChangelog(ctx context.Context, releases []*repo_model.Release, streams []updateserver_model.StreamDef) string {
var b strings.Builder var b strings.Builder
count := 0 count := 0
for _, rel := range releases { for _, rel := range releases {
if rel.IsDraft || rel.IsTag || rel.Note == "" { if rel.IsDraft || rel.IsTag || rel.Note == "" {
continue continue
} }
ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams) ch := updateserver_model.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
if ch != "stable" { if ch != "stable" {
continue continue
} }
+11
View File
@@ -38,6 +38,17 @@
{{svg "octicon-code"}} {{ctx.Locale.Tr "org.code"}} {{svg "octicon-code"}} {{ctx.Locale.Tr "org.code"}}
</a> </a>
{{end}} {{end}}
{{if .HasOrgWiki}}
{{if .OrgWikiIsExternal}}
<a class="item" href="{{.OrgWikiExternalURL}}" target="_blank" rel="noopener noreferrer">
{{svg "octicon-book"}} Wiki {{svg "octicon-link-external" 12}}
</a>
{{else}}
<a class="{{if .PageIsViewWiki}}active {{end}}item" href="{{$.Org.HomeLink}}/-/wiki/">
{{svg "octicon-book"}} Wiki
</a>
{{end}}
{{end}}
{{if .NumMembers}} {{if .NumMembers}}
<a class="{{if $.PageIsOrgMembers}}active {{end}}item" href="{{$.OrgLink}}/members"> <a class="{{if $.PageIsOrgMembers}}active {{end}}item" href="{{$.OrgLink}}/members">
{{svg "octicon-person"}} {{ctx.Locale.Tr "org.members"}} {{svg "octicon-person"}} {{ctx.Locale.Tr "org.members"}}

Some files were not shown because too many files have changed in this diff Show More