Compare commits

...

36 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: 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) Successful in 5m29s
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: 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) Successful in 4m32s
- 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: 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 31s
Deploy MokoGitea / deploy (push) Successful in 4m27s
2026-06-09 19:15:10 -05:00
Jonathan Miller d9cdaacb77 fix(ci): rename all GA_TOKEN/GITEA_TOKEN refs to MOKOGITEA_TOKEN
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 49s
Deploy MokoGitea / deploy (push) Failing after 4m26s
2026-06-09 18:38:48 -05:00
Jonathan Miller 4a4873c733 fix(ci): use MOKOGITEA_TOKEN instead of deleted GITEA_TOKEN
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 32s
Deploy MokoGitea / deploy (push) Failing after 4m35s
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: 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 32s
Deploy MokoGitea / deploy (push) Failing after 5m0s
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: 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) Failing after 58s
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: 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 43s
Deploy MokoGitea / deploy (push) Failing after 45s
2026-06-09 16:16:51 -05:00
Jonathan Miller be6f3091af fix: switch org wiki tab to .profile wiki sidecars (#595)
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 35s
Deploy MokoGitea / deploy (push) Failing after 1m16s
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 / 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
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: 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 38s
Deploy MokoGitea / deploy (push) Failing after 42s
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 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
70 changed files with 976 additions and 2353 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.13.00</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>
+3 -3
View File
@@ -33,11 +33,11 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.GA_TOKEN }} token: ${{ secrets.MOKOGITEA_TOKEN }}
- name: Delete merged branches - name: Delete merged branches
env: env:
GA_TOKEN: ${{ secrets.GA_TOKEN }} GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: | run: |
echo "=== Merged Branch Cleanup ===" echo "=== Merged Branch Cleanup ==="
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
@@ -66,7 +66,7 @@ jobs:
- name: Clean old workflow runs - name: Clean old workflow runs
env: env:
GA_TOKEN: ${{ secrets.GA_TOKEN }} GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: | run: |
echo "=== Workflow Run Cleanup ===" echo "=== Workflow Run Cleanup ==="
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
+4 -4
View File
@@ -42,10 +42,10 @@ jobs:
- name: Setup MokoStandards tools - name: Setup MokoStandards tools
env: env:
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }} MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}' COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
run: | run: |
git clone --depth 1 --branch main --quiet \ git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \ "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
+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()
+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.13.00 # 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"
+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 }}
+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
+3 -34
View File
@@ -1,9 +1,11 @@
# 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`).
## [Unreleased] ## [06.14.00] --- 2026-06-09
* FEATURES * 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): issue status/priority/type exposed in REST API - GET/PATCH on issues now includes status_id, priority_id, type_id with resolved names
@@ -194,36 +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
Submodule mcp-mokogitea-api deleted from c9eb6cfc89
+1
View File
@@ -432,6 +432,7 @@ func prepareMigrationTasks() []*migration {
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(353, "Add distribution metadata fields to repo manifest", v1_27.AddManifestDistributionFields),
newMigration(354, "Add org wiki settings to user table", v1_27.AddOrgWikiSettings), 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
} }
+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
}
@@ -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"
+4 -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
+30 -10
View File
@@ -756,23 +756,43 @@ func CreateIssue(ctx *context.APIContext) {
} }
} }
// Set org-level issue metadata (status/priority/type) if provided // Set org-level issue metadata (status/priority/type).
// If not provided, auto-assign the org default.
if form.StatusID != nil && *form.StatusID > 0 { if form.StatusID != nil && *form.StatusID > 0 {
if err := issues_model.SetIssueStatusID(ctx, issue.ID, *form.StatusID); err != nil { _ = issues_model.SetIssueStatusID(ctx, issue.ID, *form.StatusID)
ctx.APIErrorInternal(err) } else {
return // 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 { if form.PriorityID != nil && *form.PriorityID > 0 {
if err := issues_model.SetIssuePriorityID(ctx, issue.ID, *form.PriorityID); err != nil { _ = issues_model.SetIssuePriorityID(ctx, issue.ID, *form.PriorityID)
ctx.APIErrorInternal(err) } else {
return 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 { if form.TypeID != nil && *form.TypeID > 0 {
if err := issues_model.SetIssueTypeID(ctx, issue.ID, *form.TypeID); err != nil { _ = issues_model.SetIssueTypeID(ctx, issue.ID, *form.TypeID)
ctx.APIErrorInternal(err) } else {
return 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
}
}
} }
} }
+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
+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
} }
+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
} }
+11 -9
View File
@@ -157,7 +157,8 @@ func Wiki(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplOrgWiki) ctx.HTML(http.StatusOK, tplOrgWiki)
} }
// findOrgWikiCommit locates the convention wiki repo and returns its HEAD commit. // findOrgWikiCommit locates the profile repo's wiki and returns its HEAD commit.
// The org wiki lives in the .wiki.git sidecar of the profile repo (e.g. .profile.wiki.git).
func findOrgWikiCommit(ctx *context.Context, orgID int64, repoName string) (*repo_model.Repository, *git.Commit) { func findOrgWikiCommit(ctx *context.Context, orgID int64, repoName string) (*repo_model.Repository, *git.Commit) {
dbRepo, err := repo_model.GetRepositoryByName(ctx, orgID, repoName) dbRepo, err := repo_model.GetRepositoryByName(ctx, orgID, repoName)
if err != nil { if err != nil {
@@ -167,19 +168,20 @@ func findOrgWikiCommit(ctx *context.Context, orgID int64, repoName string) (*rep
return nil, nil return nil, nil
} }
if dbRepo.IsEmpty { // 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 return nil, nil
} }
gitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, dbRepo) branch := dbRepo.DefaultWikiBranch
if err != nil { if branch == "" {
log.Error("findOrgWikiCommit: OpenRepository(%s): %v", dbRepo.FullName(), err) branch = "main"
return nil, nil
} }
commit, err := wikiGitRepo.GetBranchCommit(branch)
commit, err := gitRepo.GetBranchCommit(dbRepo.DefaultBranch)
if err != nil { if err != nil {
log.Error("findOrgWikiCommit: GetBranchCommit(%s, %s): %v", dbRepo.FullName(), dbRepo.DefaultBranch, err) log.Error("findOrgWikiCommit: GetBranchCommit wiki(%s, %s): %v", dbRepo.FullName(), branch, err)
return nil, nil return nil, nil
} }
+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")
} }
-189
View File
@@ -1,189 +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"`
Distribution manifestDistribution `xml:"distribution"`
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 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 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"),
DisplayName: ctx.FormString("display_name"),
Maintainer: ctx.FormString("maintainer"),
MaintainerURL: ctx.FormString("maintainer_url"),
InfoURL: ctx.FormString("info_url"),
TargetVersion: ctx.FormString("target_version"),
PHPMinimum: ctx.FormString("php_minimum"),
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,
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,
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"
+12 -4
View File
@@ -137,8 +137,8 @@ type PrepareOwnerHeaderResult struct {
const ( const (
RepoNameProfilePrivate = ".profile-private" RepoNameProfilePrivate = ".profile-private"
RepoNameProfile = ".profile" RepoNameProfile = ".profile"
RepoNameWikiPublic = "wiki" RepoNameWikiPublic = ".profile"
RepoNameWikiPrivate = "wiki-private" RepoNameWikiPrivate = ".profile-private"
) )
func RenderUserOrgHeader(ctx *context.Context) (result *PrepareOwnerHeaderResult, err error) { func RenderUserOrgHeader(ctx *context.Context) (result *PrepareOwnerHeaderResult, err error) {
@@ -209,11 +209,19 @@ func loadHeaderCount(ctx *context.Context) error {
return nil return nil
} }
// OrgWikiRepoExists checks whether a convention wiki repo exists and is non-empty. // OrgWikiRepoExists checks whether a profile repo's wiki exists and has content.
func OrgWikiRepoExists(ctx *context.Context, ownerID int64, repoName string) bool { func OrgWikiRepoExists(ctx *context.Context, ownerID int64, repoName string) bool {
dbRepo, err := repo_model.GetRepositoryByName(ctx, ownerID, repoName) dbRepo, err := repo_model.GetRepositoryByName(ctx, ownerID, repoName)
if err != nil || dbRepo.IsEmpty { if err != nil {
log.Trace("OrgWikiRepoExists: repo %s not found for owner %d: %v", repoName, ownerID, err)
return false 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 return true
} }
+3 -2
View File
@@ -1229,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 {
+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 == "" {
+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
} }
+2 -2
View File
@@ -68,11 +68,11 @@
<div class="field"> <div class="field">
<div class="ui radio checkbox"> <div class="ui radio checkbox">
<input class="enable-system-radio" name="wiki_mode" type="radio" value="" data-context="#external_wiki_box" data-target="#internal_wiki_box" {{if eq .Org.WikiMode ""}}checked{{end}}> <input class="enable-system-radio" name="wiki_mode" type="radio" value="" data-context="#external_wiki_box" data-target="#internal_wiki_box" {{if eq .Org.WikiMode ""}}checked{{end}}>
<label>Internal wiki (uses <code>wiki</code> / <code>wiki-private</code> repos)</label> <label>Internal wiki (uses <code>.profile</code> / <code>.profile-private</code> repo wikis)</label>
</div> </div>
</div> </div>
<div id="internal_wiki_box" class="field tw-pl-4 {{if ne .Org.WikiMode ""}}disabled{{end}}"> <div id="internal_wiki_box" class="field tw-pl-4 {{if ne .Org.WikiMode ""}}disabled{{end}}">
<p class="help">Create repos named <code>wiki</code> (public) and/or <code>wiki-private</code> (members-only) under this organization.</p> <p class="help">Enable the wiki on <code>.profile</code> (public) and/or <code>.profile-private</code> (members-only) repos.</p>
</div> </div>
<div class="field"> <div class="field">
<div class="ui radio checkbox"> <div class="ui radio checkbox">
+2 -2
View File
@@ -11,8 +11,8 @@
This organization doesn't have a wiki yet. This organization doesn't have a wiki yet.
</div> </div>
<p class="tw-text-center"> <p class="tw-text-center">
Create a repository named <code>wiki</code> (public) or <code>wiki-private</code> (members-only) Enable the wiki on the <code>.profile</code> (public) or <code>.profile-private</code> (members-only)
with markdown files to get started. repository to get started.
</p> </p>
</div> </div>
{{else}} {{else}}
+49 -3
View File
@@ -59,6 +59,52 @@
{{end}} {{end}}
{{template "repo/issue/sidebar/assignee_list" $.IssuePageMetaData}} {{template "repo/issue/sidebar/assignee_list" $.IssuePageMetaData}}
{{if .IssueTypeDefs}}
<div class="divider"></div>
<div class="tw-flex tw-items-center tw-justify-between tw-gap-2">
<span class="text grey tw-text-sm">{{ctx.Locale.Tr "repo.issues.type"}} <span class="tw-text-red">*</span></span>
<select name="type_id" class="ui compact mini dropdown tw-max-w-48" required>
<option value="">—</option>
{{range .IssueTypeDefs}}
<option value="{{.ID}}" {{if .IsDefault}}selected{{end}}
{{if .Color}}style="border-left: 3px solid {{.Color}}"{{end}}>
{{.Name}}
</option>
{{end}}
</select>
</div>
{{end}}
{{if .IssuePriorityDefs}}
<div class="tw-flex tw-items-center tw-justify-between tw-gap-2 tw-mt-2">
<span class="text grey tw-text-sm">{{ctx.Locale.Tr "repo.issues.priority"}} <span class="tw-text-red">*</span></span>
<select name="priority_id" class="ui compact mini dropdown tw-max-w-48" required>
<option value="">—</option>
{{range .IssuePriorityDefs}}
<option value="{{.ID}}" {{if .IsDefault}}selected{{end}}
{{if .Color}}style="border-left: 3px solid {{.Color}}"{{end}}>
{{.Name}}
</option>
{{end}}
</select>
</div>
{{end}}
{{if .IssueStatusDefs}}
<div class="tw-flex tw-items-center tw-justify-between tw-gap-2 tw-mt-2">
<span class="text grey tw-text-sm">{{ctx.Locale.Tr "repo.issues.status"}} <span class="tw-text-red">*</span></span>
<select name="status_id" class="ui compact mini dropdown tw-max-w-48" required>
<option value="">—</option>
{{range .IssueStatusDefs}}
<option value="{{.ID}}" {{if and (eq .SortOrder 1) (not .ClosesIssue)}}selected{{end}}
{{if .Color}}style="border-left: 3px solid {{.Color}}"{{end}}>
{{.Name}}{{if .ClosesIssue}}{{end}}
</option>
{{end}}
</select>
</div>
{{end}}
{{if .CustomFieldDefs}} {{if .CustomFieldDefs}}
<div class="divider"></div> <div class="divider"></div>
<div class="tw-flex tw-flex-col tw-gap-2"> <div class="tw-flex tw-flex-col tw-gap-2">
@@ -67,17 +113,17 @@
{{range .CustomFieldDefs}} {{range .CustomFieldDefs}}
{{$currentVal := index $values .ID}} {{$currentVal := index $values .ID}}
<div class="tw-flex tw-items-center tw-justify-between tw-gap-2"> <div class="tw-flex tw-items-center tw-justify-between tw-gap-2">
<span class="text grey tw-text-sm" {{if .Description}}title="{{.Description}}"{{end}}>{{.Name}}</span> <span class="text grey tw-text-sm" {{if .Description}}title="{{.Description}}"{{end}}>{{.Name}}{{if .Required}} <span class="tw-text-red">*</span>{{end}}</span>
{{if ne .Options ""}} {{if ne .Options ""}}
{{$opts := index $fieldOptions .ID}} {{$opts := index $fieldOptions .ID}}
<select name="custom-field-{{.ID}}" class="ui compact mini dropdown tw-max-w-48"> <select name="custom-field-{{.ID}}" class="ui compact mini dropdown tw-max-w-48" {{if .Required}}required{{end}}>
<option value="">—</option> <option value="">—</option>
{{range $opts}} {{range $opts}}
<option value="{{.}}" {{if eq . $currentVal}}selected{{end}}>{{.}}</option> <option value="{{.}}" {{if eq . $currentVal}}selected{{end}}>{{.}}</option>
{{end}} {{end}}
</select> </select>
{{else}} {{else}}
<input name="custom-field-{{.ID}}" type="text" class="tw-max-w-48 tw-text-sm" value="{{$currentVal}}" placeholder="—"> <input name="custom-field-{{.ID}}" type="text" class="tw-max-w-48 tw-text-sm" value="{{$currentVal}}" placeholder="—" {{if .Required}}required{{end}}>
{{end}} {{end}}
</div> </div>
{{end}} {{end}}
-148
View File
@@ -1,148 +0,0 @@
{{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings manifest")}}
<h4 class="ui top attached header">
{{ctx.Locale.Tr "repo.settings.manifest"}}
</h4>
<div class="ui attached segment">
<p class="text grey">{{ctx.Locale.Tr "repo.settings.manifest_desc"}}</p>
<form class="ui form" method="post" action="{{.RepoLink}}/settings/manifest">
{{.CsrfTokenHtml}}
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.manifest_identity"}}</h5>
<div class="two fields">
<div class="field">
{{if eq .Manifest.Platform "joomla"}}
<label>{{ctx.Locale.Tr "repo.settings.manifest_element_name"}}</label>
<input name="name" value="{{.Manifest.Name}}" placeholder="e.g. mokowaas">
<p class="help">{{ctx.Locale.Tr "repo.settings.manifest_element_name_help"}}</p>
{{else}}
<label>{{ctx.Locale.Tr "repo.settings.manifest_name"}}</label>
<input name="name" value="{{.Manifest.Name}}" placeholder="Project name">
{{end}}
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_org"}}</label>
<input name="org" value="{{.Manifest.Org}}" placeholder="Organization">
</div>
</div>
<div class="four fields">
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_version"}}</label>
<input name="version" value="{{.Manifest.Version}}" placeholder="e.g. 06.00.00">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_version_prefix"}}</label>
<input name="version_prefix" value="{{.Manifest.VersionPrefix}}" placeholder="e.g. v1.26.1-moko.">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_license_spdx"}}</label>
<input name="license_spdx" value="{{.Manifest.LicenseSPDX}}" placeholder="e.g. GPL-3.0-or-later">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_license_name"}}</label>
<input name="license_name" value="{{.Manifest.LicenseName}}" placeholder="e.g. GNU General Public License v3">
</div>
</div>
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.manifest_governance"}}</h5>
<div class="three fields">
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_platform"}}</label>
<select name="platform" class="ui dropdown">
<option value="">—</option>
{{$platform := .Manifest.Platform}}
{{range $val := StringUtils.Split "joomla,wordpress,dolibarr,go,mcp,platform,generic" ","}}
<option value="{{$val}}" {{if eq $val $platform}}selected{{end}}>{{$val}}</option>
{{end}}
</select>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_standards_version"}}</label>
<input name="standards_version" value="{{.Manifest.StandardsVersion}}" placeholder="e.g. 05.00.00">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_standards_source"}}</label>
<input name="standards_source" value="{{.Manifest.StandardsSource}}" placeholder="URL to standards repo">
</div>
</div>
{{if or (eq .Manifest.Platform "joomla") (eq .Manifest.Platform "wordpress") (eq .Manifest.Platform "dolibarr")}}
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.manifest_distribution"}}</h5>
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_display_name"}}</label>
<input name="display_name" value="{{.Manifest.DisplayName}}" placeholder="e.g. Package - MokoWaaS">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_info_url"}}</label>
<input name="info_url" value="{{.Manifest.InfoURL}}" placeholder="https://mokoconsulting.tech/product/...">
</div>
</div>
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_maintainer"}}</label>
<input name="maintainer" value="{{.Manifest.Maintainer}}" placeholder="Moko Consulting">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_maintainer_url"}}</label>
<input name="maintainer_url" value="{{.Manifest.MaintainerURL}}" placeholder="https://mokoconsulting.tech">
</div>
</div>
{{if or (eq .Manifest.Platform "joomla") (eq .Manifest.Platform "wordpress")}}
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_target_version"}}</label>
<input name="target_version" value="{{.Manifest.TargetVersion}}" placeholder="e.g. (5|6)\..*">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_php_minimum"}}</label>
<input name="php_minimum" value="{{.Manifest.PHPMinimum}}" placeholder="e.g. 8.1">
</div>
</div>
{{end}}
{{end}}
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.manifest_build"}}</h5>
<div class="three fields">
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_language"}}</label>
<select name="language" class="ui dropdown">
<option value="">—</option>
{{$lang := .Manifest.Language}}
{{range $val := StringUtils.Split "Go,PHP,TypeScript,JavaScript,Python,Ruby,Java,C#,Rust,Shell,SQL,CSS,HTML" ","}}
<option value="{{$val}}" {{if eq $val $lang}}selected{{end}}>{{$val}}</option>
{{end}}
</select>
</div>
{{if eq .Manifest.Platform "joomla"}}
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_package_type"}}</label>
<select name="package_type" class="ui dropdown">
<option value="">—</option>
{{$pkgType := .Manifest.PackageType}}
{{range $val := StringUtils.Split "component,module,plugin,package,template,library,file" ","}}
<option value="{{$val}}" {{if eq $val $pkgType}}selected{{end}}>{{$val}}</option>
{{end}}
</select>
<p class="help">{{ctx.Locale.Tr "repo.settings.manifest_package_type_help"}}</p>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_element_full"}}</label>
<input name="element_name" value="{{.Manifest.ElementName}}" placeholder="{{.Manifest.AutoElementName}}">
{{if .Manifest.ElementNameMismatch}}
<p class="help tw-text-yellow-600">{{ctx.Locale.Tr "repo.settings.manifest_element_mismatch" .Manifest.AutoElementName}}</p>
{{else}}
<p class="help">{{ctx.Locale.Tr "repo.settings.manifest_element_full_help"}}</p>
{{end}}
</div>
{{end}}
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_entry_point"}}</label>
<input name="entry_point" value="{{.Manifest.EntryPoint}}" placeholder="e.g. ./ or src/index.ts">
</div>
</div>
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "repo.settings.manifest_save"}}</button>
</form>
</div>
{{template "repo/settings/layout_footer" .}}
+150 -46
View File
@@ -1,49 +1,153 @@
{{template "repo/settings/layout_head" (dict "pageClass" "repository settings metadata")}} {{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings metadata")}}
<div class="user-main-content twelve wide column"> <h4 class="ui top attached header">
<h4 class="ui top attached header"> {{svg "octicon-file-code" 16}} Project Identity
{{svg "octicon-list-unordered" 16}} {{ctx.Locale.Tr "repo.settings.metadata"}} </h4>
</h4> <div class="ui attached segment">
<div class="ui attached segment"> <form class="ui form" method="post" action="{{.RepoLink}}/settings/metadata?action=manifest">
{{if .CustomFieldDefs}} {{.CsrfTokenHtml}}
<form class="ui form" method="post" action="{{.RepoLink}}/settings/metadata">
{{.CsrfTokenHtml}} <div class="two fields">
{{$values := .CustomFieldValues}} <div class="field">
{{$options := .CustomFieldOptions}} {{if eq .Manifest.Platform "joomla"}}
{{range .CustomFieldDefs}} <label>Element Name</label>
{{$currentVal := index $values .ID}} <input name="name" value="{{.Manifest.Name}}" placeholder="e.g. mokowaas">
<div class="field"> {{else}}
<label>{{.Name}}{{if .Description}} <small class="text grey">({{.Description}})</small>{{end}}</label> <label>Project Name</label>
{{if .Options}} <input name="name" value="{{.Manifest.Name}}" placeholder="Project name">
{{$opts := index $options .ID}}
<select name="field_{{.ID}}" class="ui dropdown">
<option value="">—</option>
{{range $opts}}
<option value="{{.}}" {{if eq . $currentVal}}selected{{end}}>{{.}}</option>
{{end}}
</select>
{{else if eq (printf "%s" .FieldType) "checkbox"}}
<div class="ui checkbox">
<input type="checkbox" name="field_{{.ID}}" value="true" {{if eq $currentVal "true"}}checked{{end}}>
<label></label>
</div>
{{else if eq (printf "%s" .FieldType) "number"}}
<input type="number" name="field_{{.ID}}" value="{{$currentVal}}">
{{else if eq (printf "%s" .FieldType) "url"}}
<input type="url" name="field_{{.ID}}" value="{{$currentVal}}" placeholder="https://...">
{{else if eq (printf "%s" .FieldType) "date"}}
<input type="date" name="field_{{.ID}}" value="{{$currentVal}}">
{{else}}
<input type="text" name="field_{{.ID}}" value="{{$currentVal}}">
{{end}}
</div>
{{end}} {{end}}
<div class="field tw-mt-4"> </div>
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "save"}}</button> <div class="field">
</div> <label>Organization</label>
</form> <input name="org" value="{{.Manifest.Org}}" placeholder="Organization">
{{else}} </div>
<p class="text grey">{{ctx.Locale.Tr "repo.settings.metadata_empty"}}</p>
{{end}}
</div> </div>
</div> <div class="four fields">
<div class="field">
<label>Version</label>
<input name="version" value="{{.Manifest.Version}}" placeholder="e.g. 06.00.00">
</div>
<div class="field">
<label>Version Prefix</label>
<input name="version_prefix" value="{{.Manifest.VersionPrefix}}" placeholder="e.g. v1.26.1-moko.">
</div>
<div class="field">
<label>License</label>
<select name="license_spdx" class="ui dropdown">
<option value="">—</option>
{{$lic := .Manifest.LicenseSPDX}}
<option value="GPL-3.0-or-later" {{if eq $lic "GPL-3.0-or-later"}}selected{{end}}>GPL-3.0-or-later</option>
<option value="GPL-2.0-or-later" {{if eq $lic "GPL-2.0-or-later"}}selected{{end}}>GPL-2.0-or-later</option>
<option value="MIT" {{if eq $lic "MIT"}}selected{{end}}>MIT</option>
<option value="Apache-2.0" {{if eq $lic "Apache-2.0"}}selected{{end}}>Apache-2.0</option>
<option value="BSD-3-Clause" {{if eq $lic "BSD-3-Clause"}}selected{{end}}>BSD-3-Clause</option>
<option value="BSD-2-Clause" {{if eq $lic "BSD-2-Clause"}}selected{{end}}>BSD-2-Clause</option>
<option value="LGPL-3.0-or-later" {{if eq $lic "LGPL-3.0-or-later"}}selected{{end}}>LGPL-3.0-or-later</option>
<option value="MPL-2.0" {{if eq $lic "MPL-2.0"}}selected{{end}}>MPL-2.0</option>
<option value="ISC" {{if eq $lic "ISC"}}selected{{end}}>ISC</option>
<option value="AGPL-3.0-or-later" {{if eq $lic "AGPL-3.0-or-later"}}selected{{end}}>AGPL-3.0-or-later</option>
<option value="Unlicense" {{if eq $lic "Unlicense"}}selected{{end}}>Unlicense</option>
<option value="proprietary" {{if eq $lic "proprietary"}}selected{{end}}>Proprietary</option>
</select>
</div>
</div>
<h5 class="ui dividing header">Governance</h5>
<div class="two fields">
<div class="field">
<label>Platform</label>
<select name="platform" class="ui dropdown">
<option value="">—</option>
{{$platform := .Manifest.Platform}}
{{range $val := StringUtils.Split "joomla,wordpress,dolibarr,go,mcp,platform,generic" ","}}
<option value="{{$val}}" {{if eq $val $platform}}selected{{end}}>{{$val}}</option>
{{end}}
</select>
</div>
<div class="field">
<label>Info URL</label>
<input name="info_url" value="{{.Manifest.InfoURL}}" placeholder="https://mokoconsulting.tech/product/...">
</div>
</div>
{{if or (eq .Manifest.Platform "joomla") (eq .Manifest.Platform "wordpress")}}
<div class="two fields">
<div class="field">
<label>Target Platform Version</label>
<input name="target_version" value="{{.Manifest.TargetVersion}}" placeholder="e.g. (5|6)\..*">
</div>
<div class="field">
<label>PHP Minimum</label>
<input name="php_minimum" value="{{.Manifest.PHPMinimum}}" placeholder="e.g. 8.1">
</div>
</div>
{{end}}
{{if eq .Manifest.Platform "joomla"}}
<h5 class="ui dividing header">Build</h5>
<div class="two fields">
<div class="field">
<label>Extension Type</label>
<select name="package_type" class="ui dropdown">
<option value="">—</option>
{{$pkgType := .Manifest.PackageType}}
{{range $val := StringUtils.Split "component,module,plugin,package,template,library,file" ","}}
<option value="{{$val}}" {{if eq $val $pkgType}}selected{{end}}>{{$val}}</option>
{{end}}
</select>
</div>
<div class="field">
<label>Entry Point</label>
<input name="entry_point" value="{{.Manifest.EntryPoint}}" placeholder="e.g. ./ or src/index.ts">
</div>
</div>
{{end}}
<button class="ui primary button" type="submit">Save Project Identity</button>
</form>
</div>
{{if .CustomFieldDefs}}
<h4 class="ui top attached header">
{{svg "octicon-list-unordered" 16}} Custom Fields
</h4>
<div class="ui attached segment">
<form class="ui form" method="post" action="{{.RepoLink}}/settings/metadata?action=customfields">
{{.CsrfTokenHtml}}
{{$values := .CustomFieldValues}}
{{$options := .CustomFieldOptions}}
{{range .CustomFieldDefs}}
{{$currentVal := index $values .ID}}
<div class="field">
<label>{{.Name}}{{if .Required}} <span class="tw-text-red">*</span>{{end}}{{if .Description}} <small class="text grey">({{.Description}})</small>{{end}}</label>
{{if .Options}}
{{$opts := index $options .ID}}
<select name="field_{{.ID}}" class="ui dropdown" {{if .Required}}required{{end}}>
<option value="">—</option>
{{range $opts}}
<option value="{{.}}" {{if eq . $currentVal}}selected{{end}}>{{.}}</option>
{{end}}
</select>
{{else if eq (printf "%s" .FieldType) "checkbox"}}
<div class="ui checkbox">
<input type="checkbox" name="field_{{.ID}}" value="true" {{if eq $currentVal "true"}}checked{{end}}>
<label></label>
</div>
{{else if eq (printf "%s" .FieldType) "number"}}
<input type="number" name="field_{{.ID}}" value="{{$currentVal}}" {{if .Required}}required{{end}}>
{{else if eq (printf "%s" .FieldType) "url"}}
<input type="url" name="field_{{.ID}}" value="{{$currentVal}}" placeholder="https://..." {{if .Required}}required{{end}}>
{{else if eq (printf "%s" .FieldType) "date"}}
<input type="date" name="field_{{.ID}}" value="{{$currentVal}}" {{if .Required}}required{{end}}>
{{else}}
<input type="text" name="field_{{.ID}}" value="{{$currentVal}}" {{if .Required}}required{{end}}>
{{end}}
</div>
{{end}}
<div class="field tw-mt-4">
<button class="ui primary button" type="submit">Save Custom Fields</button>
</div>
</form>
</div>
{{end}}
{{template "repo/settings/layout_footer" .}} {{template "repo/settings/layout_footer" .}}
+5 -8
View File
@@ -7,17 +7,14 @@
<a class="{{if .PageIsSettingsAdvanced}}active {{end}}item" href="{{.RepoLink}}/settings/advanced"> <a class="{{if .PageIsSettingsAdvanced}}active {{end}}item" href="{{.RepoLink}}/settings/advanced">
{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings.advanced_settings"}} {{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings.advanced_settings"}}
</a> </a>
<a class="{{if .PageIsSettingsMetadata}}active {{end}}item" href="{{.RepoLink}}/settings/metadata">
{{svg "octicon-file-code"}} Metadata
</a>
{{if .LicensingEnabled}} {{if .LicensingEnabled}}
<a class="{{if .PageIsSettingsLicensing}}active {{end}}item" href="{{.RepoLink}}/settings/licensing"> <a class="{{if .PageIsSettingsUpdateServer}}active {{end}}item" href="{{.RepoLink}}/settings/updateserver">
{{svg "octicon-broadcast"}} {{ctx.Locale.Tr "repo.settings.licensing_section"}} {{svg "octicon-broadcast"}} Update Server
</a> </a>
{{end}} {{end}}
<a class="{{if .PageIsSettingsManifest}}active {{end}}item" href="{{.RepoLink}}/settings/manifest">
{{svg "octicon-file-code"}} {{ctx.Locale.Tr "repo.settings.manifest"}}
</a>
<a class="{{if .PageIsSettingsMetadata}}active {{end}}item" href="{{.RepoLink}}/settings/metadata">
{{svg "octicon-list-unordered"}} {{ctx.Locale.Tr "repo.settings.metadata"}}
</a>
<a class="{{if .PageIsSettingsSecurity}}active {{end}}item" href="{{.RepoLink}}/settings/security"> <a class="{{if .PageIsSettingsSecurity}}active {{end}}item" href="{{.RepoLink}}/settings/security">
{{svg "octicon-shield"}} {{ctx.Locale.Tr "repo.settings.security"}} {{svg "octicon-shield"}} {{ctx.Locale.Tr "repo.settings.security"}}
</a> </a>
+51
View File
@@ -0,0 +1,51 @@
{{template "repo/settings/layout_head" (dict "pageClass" "repository settings updateserver")}}
<div class="user-main-content twelve wide column">
<h4 class="ui top attached header">
{{svg "octicon-broadcast" 16}} Update Server
</h4>
<div class="ui attached segment">
<form class="ui form" method="post" action="{{.RepoLink}}/settings/updateserver">
{{.CsrfTokenHtml}}
<div class="inline field">
<div class="ui checkbox">
<input name="enable_licensing" type="checkbox" {{if and .RepoUpdateConfig .RepoUpdateConfig.LicensingEnabled}}checked{{end}}>
<label><strong>Enable Update Server</strong></label>
</div>
<p class="help">Serve update feeds from releases and show the Licenses tab for optional key management.</p>
</div>
<div class="ui divider"></div>
<div class="inline field">
<label>Feed Visibility</label>
<select name="feed_visibility" class="ui dropdown">
<option value="public" {{if or (not .RepoUpdateConfig) (eq .RepoUpdateConfig.FeedVisibility "") (eq .RepoUpdateConfig.FeedVisibility "public")}}selected{{end}}>Public (show versions and downloads)</option>
<option value="no-download" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.FeedVisibility "no-download")}}selected{{end}}>No downloads (show versions only)</option>
<option value="hidden" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.FeedVisibility "hidden")}}selected{{end}}>Hidden (require license key)</option>
</select>
</div>
<div class="inline field">
<label>Download Gating</label>
<select name="download_gating" class="ui dropdown">
<option value="none" {{if or (not .RepoUpdateConfig) (eq .RepoUpdateConfig.DownloadGating "") (eq .RepoUpdateConfig.DownloadGating "none")}}selected{{end}}>None</option>
<option value="prerelease" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.DownloadGating "prerelease")}}selected{{end}}>Pre-release only</option>
<option value="all" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.DownloadGating "all")}}selected{{end}}>All releases</option>
</select>
</div>
<div class="inline field">
<div class="ui checkbox">
<input name="require_update_key" type="checkbox" {{if and .RepoUpdateConfig .RepoUpdateConfig.RequireKey}}checked{{end}}>
<label>Require license key for update feed</label>
</div>
</div>
<div class="field">
<button class="ui primary button">Save</button>
</div>
</form>
</div>
</div>
{{template "repo/settings/layout_footer" .}}