Compare commits

...

224 Commits

Author SHA1 Message Date
Jonathan Miller fe6ca172f6 chore: merge main back into dev after v1.26.1-moko.06.03 release
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
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-05 00:07:28 -05:00
jmiller 5ca1c888c0 Merge pull request 'feat(custom-fields): template pre-fill + feed generator migration' (#494) from feat/issue-template-custom-fields into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
PR RC Release / Build RC Release (pull_request) Failing after 23s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 1m22s
2026-06-05 03:15:16 +00:00
Jonathan Miller 8e0388c9d8 fix(custom-fields): log errors instead of silently discarding them
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
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 / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
- saveCustomFieldsFromForm: log GetCustomFieldsByOwner errors
- resolveExtensionMetadata: log DB errors on custom field lookup
- NewIssue/ViewIssue: log errors from GetCustomFieldsByOwner and
  GetCustomFieldValuesMap instead of blank-assigning
- Composer: fix misleading comment about override source

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 22:14:10 -05:00
Jonathan Miller cd4c701cb6 fix(custom-fields): address code review findings
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
- API: return 500 on GetCustomFieldsByOwner failure instead of silently
  swallowing the error
- resolveExtensionMetadata: add DownloadGating/KeyPrefix to metadata
  struct instead of mutating the caller's cfg pointer (side effect)
- resolveExtensionMetadata: add Description custom field mapping
- Composer: use meta.PHPMinimum instead of bypassing the cascade
- Web form: flash error on custom field save failure instead of silent log

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 21:59:50 -05:00
Jonathan Miller b72f88e78b docs(changelog): add #492 and #493 entries
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Generic: Repo Health / Access control (pull_request) Successful in 37s
PR RC Release / Build RC Release (pull_request) Successful in 43s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 21:53:04 -05:00
Jonathan Miller 1935889f6b feat(updateserver): resolve extension metadata from custom fields with config fallback
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Add resolveExtensionMetadata() with cascading priority: org-level
repo-scoped custom fields → update_stream_config table → repo-derived
defaults. All six feed generators (Joomla, WordPress, Composer, Drupal,
PrestaShop, WHMCS) now use this unified resolver. Repos can be migrated
to custom fields gradually since the config table remains as fallback.

Ref #492

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 21:48:14 -05:00
Jonathan Miller 9ebe1b26b1 feat(custom-fields): pre-fill custom fields from issue template YAML frontmatter
Add `custom_fields` map to IssueTemplate struct so templates can specify
default values (e.g. `Priority: Medium`). On new issue form, org-level
issue-scoped fields appear in the sidebar with template defaults pre-selected.
NewIssuePost saves the values after issue creation. The API create issue
endpoint also accepts `custom_fields` by name.

Closes #493

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 21:47:15 -05:00
Jonathan Miller 3aec6c2cae fix(migration): set issue_id default to 0 for new custom_field_value inserts
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 24s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 58s
The old issue_id column has NOT NULL without a default, causing inserts
via the new entity_id-based API to fail. Migration now ALTERs the
column to DEFAULT 0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 19:51:56 -05:00
Jonathan Miller 539619be2f feat(api): custom fields API for org definitions, repo metadata, and issue values
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 24s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 55s
API endpoints:
- GET/POST/DELETE /api/v1/orgs/{org}/custom-fields — org field definitions
- GET/PUT /api/v1/repos/{owner}/{repo}/metadata — repo-scoped field values
- GET/PUT /api/v1/repos/{owner}/{repo}/issues/{index}/custom-fields — issue values

All endpoints use field names as keys (not IDs) for ergonomic access.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 19:20:21 -05:00
Jonathan Miller 6bd9548b2a feat(custom-fields): move to org-level definitions with issue and repo scopes
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 23s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 1m4s
- CustomFieldDef now has owner_id (org) and scope (issue/repo)
- Issue sidebar loads fields by org owner_id, not repo_id
- Org Settings > Custom Fields page for managing field definitions
- Repo Settings > Metadata page for filling in repo-scoped values
- Migration v345 adds owner_id, scope, entity_id, entity_type columns
- Per-repo custom field management replaced by org-level
- Replaces .mokogitea/manifest.xml with database-backed metadata

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 19:11:22 -05:00
Jonathan Miller 5665bc545e fix(updateserver): use client=0 for packages to fix Joomla extension matching (#482)
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 25s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 1m5s
Joomla matches updates to installed extensions via element+type+client_id.
Packages in #__extensions have client_id=0. Omitting <client> caused
Joomla to default to client_id=1, resulting in extension_id=0 in
#__updates and updates not appearing.

Fix: output <client>0</client> for package types.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 18:44:46 -05:00
Jonathan Miller df58aacc30 fix(downloads): signed-in users with repo access bypass download gating
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 23s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 1m29s
Download gating only applies to anonymous/external clients (Joomla
update checker). Users who are signed in and have permission to
access the repo can always download releases regardless of gating.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 18:35:57 -05:00
Jonathan Miller 6575d3fce2 fix(ui): remove package count badge from Licenses tab
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 24s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 1m19s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 18:24:20 -05:00
Jonathan Miller d553c87a9d fix(updateserver): derive maintainer from org profile, infourl from support_url
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 24s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 1m2s
- Maintainer name: org FullName (from org profile)
- Maintainer URL: org Website (from org profile)
- Info URL: support_url (product page), falls back to releases page
- Removes dependency on separate maintainer/maintainer_url/info_url
  fields in update_stream_config

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 18:17:51 -05:00
Jonathan Miller 178e8fffe2 fix(licenses): fix key generation modal not passing package_id
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 22s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 1m3s
The generate key modal's hidden package_id input was always empty
because show-modal doesn't wire data attributes to hidden inputs.
Fix: pass package_id via the form action URL query string instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 18:13:10 -05:00
Jonathan Miller 2f767e91cb chore: update changelog with today's fixes
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
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 17:50:58 -05:00
Jonathan Miller 5c22bb04b5 fix(licenses): remove master key banner, sort master keys first in list
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 24s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 1m0s
Remove the dedicated master key segment at the top of the Licenses
page. Master keys now appear first in the keys table via ORDER BY
is_internal DESC.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 17:42:05 -05:00
Jonathan Miller d6d0d5a11f feat(ui): hide license sections on Update Server page when no gating
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 23s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 1m4s
When download_gating is off, the Update Server page only shows feed
URLs. License packages, keys, master key, and modals are hidden since
they're not relevant without download gating.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 16:43:41 -05:00
Jonathan Miller c948696488 feat(ui): show Update Server tab when no gating, Licenses tab when gated
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
- No gating: tab shows as "Update Server" with broadcast icon
- Gated (prerelease/all): tab shows as "Licenses" with key icon and
  package count badge
- Licensing disabled: tab hidden entirely

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 16:38:49 -05:00
Jonathan Miller 635a13d277 fix(updateserver): only show downloadkey when downloads are gated
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 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 1s
Universal: PR Check / Validate PR (pull_request) Failing after 10s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 26s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 1m0s
Show <downloadkey> only when download_gating is prerelease or all.
No gating means no license keys are needed, so don't prompt for one.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 16:33:04 -05:00
Jonathan Miller 6be3e5c879 fix(updateserver): always show downloadkey when licensing enabled
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 22s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 1m3s
Include <downloadkey prefix="dlid="> in Joomla update XML whenever
Update Server is enabled for the repo, not just when require_key is
set. This ensures Joomla shows the Download Key field in Update Sites
even when downloads are currently public.

Also corrected joomlaTagName comment with Joomla source reference.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 16:28:14 -05:00
Jonathan Miller a86a9afb1a Revert "chore: remove build/ directory from tracking"
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 25s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 1m20s
This reverts commit 10e76cf033.
2026-06-04 16:18:13 -05:00
Jonathan Miller 5a80b8da33 docs(updateserver): correct joomlaTagName comment with Joomla source reference
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
PR RC Release / Build RC Release (pull_request) Failing after 21s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 58s
Joomla's Update.php maps tags via STABILITY_ + strtoupper(tag).
Valid values: dev, alpha, beta, rc, stable. Full names like
"development" silently fall back to STABILITY_STABLE.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 14:29:26 -05:00
jmiller 0b21fe859e Merge pull request 'feat(issues): custom fields in issue sidebar' (#473) from feat/custom-fields-sidebar into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
2026-06-04 18:49:41 +00:00
Jonathan Miller 4ec0db8658 feat(issues): show custom fields in issue sidebar with inline editing
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
- Load custom field definitions and values in ViewIssue handler
- New sidebar template displays dropdown fields with onchange submit
- POST handler at /issues/{id}/custom-fields/{field_id} saves values
- Dropdown options parsed from JSON and passed to template
- Non-dropdown fields display as read-only text
- Section appears between Labels and Milestone in sidebar

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 13:48:22 -05:00
Jonathan Miller 10e76cf033 chore: remove build/ directory from tracking
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
Build artifacts are created by CI, not tracked in source.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 13:33:35 -05:00
jmiller a5bda0f9a6 Merge pull request 'chore: consolidate changelog to minor versions' (#471) from chore/changelog-consolidation into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
PR RC Release / Build RC Release (pull_request) Failing after 20s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
2026-06-04 17:51:05 +00:00
Jonathan Miller a053126bd9 chore: consolidate changelog to minor version numbers only
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Merge patch-level entries into their minor version, drop .00 suffixes,
and add today's Update Server and license UI fixes to moko.06.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 12:50:01 -05:00
gitea-actions[bot] 9fff67ab57 chore(release): build 05.31.00 [skip ci] 2026-06-04 17:25:06 +00:00
jmiller 3eb649a1a6 Merge pull request 'fix(updateserver): merge version fix to main' (#470) from dev into main
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Deploy MokoGitea / deploy (push) Failing after 28s
2026-06-04 17:24:24 +00:00
jmiller f091f4cab3 Merge pull request 'fix(updateserver): version from asset filename takes priority' (#469) from fix/update-xml-version-priority into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 23s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 1m5s
2026-06-04 17:24:12 +00:00
Jonathan Miller 0de02fdce5 fix(updateserver): prevent stream name tag from overriding asset-derived version
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
When the tag is a stream name (e.g. "release-candidate"), the version
extracted from the asset filename was being overwritten by the release
title version. Remove the isStreamName check since the priority chain
(filename -> tag -> title) already handles this correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 12:22:40 -05:00
gitea-actions[bot] 06f8ab3d1a chore(release): build 05.30.00 [skip ci] 2026-06-04 17:16:24 +00:00
jmiller 3918e8ef9a Merge pull request 'fix(updateserver): merge XML fixes to main' (#468) from dev into main
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Deploy MokoGitea / deploy (push) Failing after 1m16s
2026-06-04 17:15:48 +00:00
jmiller 480aaa088a Merge pull request 'fix(updateserver): extract version from asset filename, omit client for packages' (#467) from fix/joomla-update-xml into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
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 / Access control (push) Successful in 2s
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 22s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 1m3s
2026-06-04 17:15:29 +00:00
Jonathan Miller f0aa2c3034 fix(updateserver): extract version from asset filename, omit client for packages
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 0s
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
- Version now extracted from the zip asset filename first (most
  accurate), falling back to tag name then release title. Fixes
  mismatch where title version was updated but asset was stale.
- Omit <client> element for package extension types (packages manage
  their own sub-extension clients per Joomla spec).
- Make Client field omitempty so empty string doesn't render empty tag.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 12:14:29 -05:00
jmiller 01d38e13f9 chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:58:28 +00:00
jmiller 04338fe159 chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:56:32 +00:00
jmiller 0389410efc chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:40:57 +00:00
jmiller ddababa6fa chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:38:59 +00:00
jmiller 3e156e8307 chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:32:19 +00:00
jmiller 546245c9bb chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:29:32 +00:00
jmiller 26bb906a96 chore: remove updates.xml [skip ci] 2026-06-04 15:27:32 +00:00
jmiller 6509bd1eb7 chore: remove updates.xml [skip ci] 2026-06-04 15:27:25 +00:00
jmiller 6ac7c0c774 chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:19:07 +00:00
jmiller 53c86c9b17 chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:14:04 +00:00
jmiller 4a687a9438 chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-06-04 14:23:31 +00:00
jmiller 0187f9814f chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-06-04 14:20:43 +00:00
gitea-actions[bot] 92ca601aa6 chore: update channels for 05.29.00 [skip ci] 2026-06-04 14:19:43 +00:00
gitea-actions[bot] e866d16ee6 chore(release): build 05.29.00 [skip ci] 2026-06-04 14:19:07 +00:00
jmiller 5a1772b026 Merge pull request 'fix(ui): merge Update Server rename to main' (#466) from dev into main
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 3s
Deploy MokoGitea / deploy (push) Failing after 28s
2026-06-04 14:18:24 +00:00
jmiller 969015a87a Merge pull request 'fix(ui): rename Licensing to Update Server across settings' (#465) from fix/rename-licensing-to-update-server into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 27s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 1m11s
2026-06-04 14:17:56 +00:00
Jonathan Miller ee20006b15 fix(ui): rename "Licensing" to "Update Server" across settings UI
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 / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Access control (push) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
The update server system works without license keys — the primary
feature is serving update feeds. Rename all UI labels from "Licensing"
to "Update Server" to reflect this. License key management remains
under the Licenses tab.

- Repo settings nav: "Licensing & Updates" -> "Update Server"
- Advanced settings toggle: "Enable licensing" -> "Enable Update Server"
- Org settings nav: "Licensing & Update Streams" -> "Update Server"
- Icon changed from octicon-key to octicon-broadcast
- Updated help text to emphasize update feeds over licensing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 09:16:49 -05:00
jmiller ce38bab2cf chore: sync updates.xml 05.28.00 from main [skip ci] 2026-06-04 14:08:45 +00:00
gitea-actions[bot] aba8021344 chore: update channels for 05.28.00 [skip ci] 2026-06-04 14:08:44 +00:00
gitea-actions[bot] 7cbbfb7505 chore(release): build 05.28.00 [skip ci] 2026-06-04 14:08:10 +00:00
jmiller 7f45e98630 Merge pull request 'feat(licenses): merge domain restriction to main' (#464) from dev into main
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Deploy MokoGitea / deploy (push) Failing after 23s
2026-06-04 14:07:38 +00:00
jmiller 74194f4283 Merge pull request 'feat(licenses): add domain restriction to packages and key generation' (#463) from feat/license-domain-restriction into dev
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / 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: 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
PR RC Release / Build RC Release (pull_request) Successful in 27s
Generic: Repo Health / Access control (push) Successful in 2s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 1m35s
2026-06-04 14:06:57 +00:00
Jonathan Miller e4ea1303ea feat(licenses): add domain restriction to packages and key generation
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Generic: Repo Health / Access control (push) 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 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
- Add DomainRestriction field to LicensePackage model with migration
- Packages can define default allowed domains (comma-separated)
- Key generation form now includes licensee name, email, and domain
  fields in a proper modal instead of a tiny inline form
- Keys inherit domain restriction from their package if not overridden
- Package create/edit forms include domain restriction input
- Domain enforcement already exists in heartbeat validation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 09:05:44 -05:00
jmiller 7d6cc3152d chore: sync updates.xml 05.27.00 from main [skip ci] 2026-06-04 13:50:31 +00:00
gitea-actions[bot] 42b0ff182c chore: update channels for 05.27.00 [skip ci] 2026-06-04 13:50:26 +00:00
gitea-actions[bot] 32d5a292c7 chore(release): build 05.27.00 [skip ci] 2026-06-04 13:49:44 +00:00
jmiller 33ba1159c3 Merge pull request 'fix(licenses): merge license UI fixes to main' (#462) from dev into main
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Deploy MokoGitea / deploy (push) Failing after 58s
2026-06-04 13:49:05 +00:00
jmiller cd45824a0d Merge pull request 'fix(licenses): fix master key visibility and package creation at repo level' (#461) from fix/licenses-ui into dev
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
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
PR RC Release / Build RC Release (pull_request) Successful in 24s
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 2m13s
2026-06-04 13:48:50 +00:00
Jonathan Miller bab1acdfe3 fix(licenses): fix master key visibility, package creation, and template structure
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
- Always show master key prefix and status in a dedicated segment with
  a Regenerate button that deactivates the old key and creates a new one
- Fix broken <details> structure where </details> was inside an {{if}}
  block, causing malformed HTML
- Move create package form into a proper modal instead of a broken
  details/summary toggle
- Add copy button for all key prefixes (not just full keys)
- Add POST /licenses/master-key/regenerate route and handler
- Add locale keys for regenerate master key feature

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 08:47:11 -05:00
gitea-actions[bot] e59837b250 chore: update channels for 05.26.00 [skip ci] 2026-06-04 13:11:23 +00:00
gitea-actions[bot] 5c1e1cc8cc chore(release): build 05.26.00 [skip ci] 2026-06-04 13:10:51 +00:00
jmiller 6ea5dd37aa Merge pull request 'fix(settings): merge advanced settings UI to main' (#459) from dev into main
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Deploy MokoGitea / deploy (push) Failing after 24s
2026-06-04 13:10:14 +00:00
jmiller 099f30d05b Merge pull request 'fix(settings): add licensing toggle to Advanced Settings, clean up UI' (#458) from fix/advanced-settings-ui into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 25s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 1m5s
2026-06-04 13:09:59 +00:00
Jonathan Miller cbaf289657 fix(settings): add licensing toggle to Advanced Settings, clean up UI
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
- Add licensing enable/disable checkbox to Advanced Settings page so
  users can toggle licensing without needing the separate Licensing
  settings page
- Restore LicensingEnabled gate on Licensing nav item (shows only when
  licensing is enabled, as intended)
- Replace inline dividers with Fomantic UI dividing headers for cleaner
  visual section separation
- Fix redirect after saving advanced settings to go back to /advanced
  instead of /settings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 08:09:23 -05:00
gitea-actions[bot] 619295f469 chore: update channels for 05.25.00 [skip ci] 2026-06-04 13:03:07 +00:00
gitea-actions[bot] eced91be74 chore(release): build 05.25.00 [skip ci] 2026-06-04 13:02:37 +00:00
jmiller 902e3b5edd Merge pull request 'fix(settings): merge licensing nav fix to main' (#457) from dev into main
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Deploy MokoGitea / deploy (push) Failing after 26s
2026-06-04 13:01:56 +00:00
jmiller 95d4259a26 Merge pull request 'fix(settings): always show Licensing nav item in repo settings' (#456) from fix/licensing-nav-always-visible into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
PR RC Release / Build RC Release (pull_request) Failing after 21s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 1m2s
2026-06-04 13:01:10 +00:00
Jonathan Miller 64dc6f28fa fix(settings): always show Licensing nav item in repo settings
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
The Licensing nav item was gated behind LicensingEnabled, creating a
chicken-and-egg problem: users couldn't reach the settings page to
enable licensing because the nav link was hidden until licensing was
already enabled.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 07:58:17 -05:00
jmiller cea3418894 chore: sync updates.xml 05.24.00 from main [skip ci] 2026-06-04 12:54:29 +00:00
gitea-actions[bot] b6671ee1f9 chore: update channels for 05.24.00 [skip ci] 2026-06-04 12:54:28 +00:00
gitea-actions[bot] a52835b8ee chore(release): build 05.24.00 [skip ci] 2026-06-04 12:53:56 +00:00
jmiller c64bafbe80 Merge pull request 'fix(settings): merge nav highlight fix to main' (#455) from dev into main
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Deploy MokoGitea / deploy (push) Failing after 1m0s
2026-06-04 12:53:22 +00:00
jmiller e088589af7 Merge pull request 'fix(settings): prevent double-highlight on Advanced Settings nav item' (#454) from fix/advanced-settings-nav-highlight into dev
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Failing after 9s
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
PR RC Release / Build RC Release (pull_request) Successful in 26s
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 2m11s
2026-06-04 12:52:51 +00:00
Jonathan Miller c90edc3efc fix(settings): prevent double-highlight on Advanced Settings nav item
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
SettingsCtxData middleware sets PageIsSettingsOptions=true for all
settings routes including /advanced. The advanced handler now explicitly
clears it so only the Advanced Settings nav item is highlighted.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 07:50:41 -05:00
jmiller dda0f6d4ed chore: sync updates.xml 05.23.00 from main [skip ci] 2026-06-04 12:40:18 +00:00
gitea-actions[bot] 21fb789d3c chore: update channels for 05.23.00 [skip ci] 2026-06-04 12:40:16 +00:00
gitea-actions[bot] 558bf37fce chore(release): build 05.23.00 [skip ci] 2026-06-04 12:39:40 +00:00
jmiller 746f1a5a50 Merge pull request 'fix(build): merge UTF-8 fix to main' (#453) from dev into main
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Deploy MokoGitea / deploy (push) Failing after 3m26s
2026-06-04 12:39:06 +00:00
jmiller 9cceb5da0b Merge pull request 'fix(build): replace invalid UTF-8 character in API comment' (#452) from fix/utf8-comment into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 22s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 2m33s
2026-06-04 12:38:19 +00:00
Jonathan Miller fb5002d317 fix(build): replace invalid UTF-8 character in API comment
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
The em dash in the TODO comment was encoded as invalid UTF-8, causing
a Go compiler error. Replace with ASCII hyphen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 07:37:42 -05:00
jmiller f2482a712c chore: sync updates.xml 05.22.00 from main [skip ci] 2026-06-04 12:31:29 +00:00
gitea-actions[bot] 349a326881 chore: update channels for 05.22.00 [skip ci] 2026-06-04 12:31:28 +00:00
gitea-actions[bot] 7dc598104b chore(release): build 05.22.00 [skip ci] 2026-06-04 12:31:01 +00:00
jmiller 402166589b Merge pull request 'fix(build): merge custom fields build fix to main' (#451) from dev into main
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Deploy MokoGitea / deploy (push) Failing after 22s
2026-06-04 12:30:27 +00:00
jmiller 6c9a26ebd3 Merge pull request 'fix(build): remove stale custom field API routes and dead code' (#450) from fix/custom-fields-build into dev
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
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
PR RC Release / Build RC Release (pull_request) Successful in 21s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 5m27s
2026-06-04 12:28:18 +00:00
Jonathan Miller c15582aa64 fix(build): remove stale custom field API routes, structs, and migration
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 4s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Comment out custom-fields API routes in api.go that referenced handler
functions from the deleted routers/api/v1/repo/custom_field.go. Remove
the unreferenced modules/structs/custom_field.go and the duplicate
v1_25/v323 migration (superseded by v1_27/v343).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 07:26:16 -05:00
jmiller 03dee5af39 chore: sync updates.xml 05.21.00 from main [skip ci] 2026-06-04 12:05:07 +00:00
gitea-actions[bot] 2827fa0a4c chore: update channels for 05.21.00 [skip ci] 2026-06-04 12:05:05 +00:00
gitea-actions[bot] 979d6f5964 chore(release): build 05.21.00 [skip ci] 2026-06-04 12:04:33 +00:00
jmiller 396220368f Merge pull request 'fix(build): remove stale custom field API' (#449) from dev into main
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Deploy MokoGitea / deploy (push) Failing after 37s
2026-06-04 12:04:01 +00:00
Jonathan Miller 877f39d4f4 fix(build): remove stale custom field API file from prior session
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
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
PR RC Release / Build RC Release (pull_request) Successful in 21s
Generic: Repo Health / Site Health (push) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 6m5s
The old API file references non-existent struct names and fields.
Will be rebuilt properly after the web UI is complete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 07:02:40 -05:00
jmiller 0492ea399e chore: sync updates.xml 05.20.00 from main [skip ci] 2026-06-04 11:57:57 +00:00
gitea-actions[bot] 53b2d5b754 chore: update channels for 05.20.00 [skip ci] 2026-06-04 11:55:24 +00:00
gitea-actions[bot] 5db84e3932 chore(release): build 05.20.00 [skip ci] 2026-06-04 11:54:44 +00:00
jmiller 02cb4ae1a1 Merge pull request 'fix(build): custom field API function names' (#448) from dev into main
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Deploy MokoGitea / deploy (push) Failing after 1m13s
2026-06-04 11:54:05 +00:00
Jonathan Miller 75f05e1c80 fix(build): update custom field API to match renamed model functions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
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
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 27s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 6m17s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 06:52:56 -05:00
jmiller 23eb52cafb chore: sync updates.xml 05.19.00 from main [skip ci] 2026-06-04 11:50:10 +00:00
gitea-actions[bot] 7f2aaa84bd chore: update channels for 05.19.00 [skip ci] 2026-06-04 11:50:07 +00:00
gitea-actions[bot] 6f16459e13 chore(release): build 05.19.00 [skip ci] 2026-06-04 11:49:23 +00:00
jmiller 5cf91a12bc Merge pull request 'feat(issues): custom fields foundation' (#447) from dev into main
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Deploy MokoGitea / deploy (push) Failing after 25s
2026-06-04 11:48:39 +00:00
Jonathan Miller c7d8f6066f feat(issues): custom fields foundation — model, migration, settings UI (#8)
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
PR RC Release / Build RC Release (pull_request) Failing after 19s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 6m35s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 06:46:33 -05:00
jmiller 6b0ec5196a chore: sync .mokogitea/workflows/repo-health.yml from moko-platform [skip ci] 2026-06-03 09:37:15 +00:00
jmiller 8741096fb4 chore: sync .mokogitea/workflows/repo-health.yml from moko-platform [skip ci] 2026-06-03 03:11:01 +00:00
gitea-actions[bot] 0ab3b7dbd7 chore: update channels for 05.18.00 [skip ci] 2026-06-03 03:00:25 +00:00
gitea-actions[bot] 02495327ee chore(release): build 05.18.00 [skip ci] 2026-06-03 02:59:47 +00:00
jmiller 6f5c40716d Merge pull request 'fix(updates): default Joomla target to 5/6, correct URL mapping' (#446) from dev into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Deploy MokoGitea / deploy (push) Failing after 5m19s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-03 02:59:09 +00:00
Jonathan Miller e5aa0c343d fix(updates): default Joomla target version to 5/6
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 25s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 4m31s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Release configuration (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 21:57:03 -05:00
jmiller ab3a65abdf chore: sync updates.xml 05.17.00 from main [skip ci] 2026-06-03 02:56:36 +00:00
gitea-actions[bot] 1eff03ab21 chore: update channels for 05.17.00 [skip ci] 2026-06-03 02:56:34 +00:00
gitea-actions[bot] e3e2cb4543 chore(release): build 05.17.00 [skip ci] 2026-06-03 02:56:05 +00:00
jmiller a15139f70b Merge pull request 'fix(updates): correct infourl/maintainerurl mapping' (#445) from dev into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Deploy MokoGitea / deploy (push) Failing after 22s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-03 02:55:31 +00:00
Jonathan Miller ba0d180e39 fix(updates): correct infourl/maintainerurl mapping
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
PR RC Release / Build RC Release (pull_request) Failing after 19s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 6m52s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Release configuration (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
<infourl> = InfoURL field (product/release info page), fallback /releases
<maintainerurl> = SupportURL field (support site), fallback MaintainerURL, fallback org profile

Previously SupportURL was mapped to <infourl> which was wrong.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 21:54:01 -05:00
gitea-actions[bot] abf961dd1e chore: update channels for 05.16.00 [skip ci] 2026-06-03 01:33:13 +00:00
gitea-actions[bot] b34381e8da chore(release): build 05.16.00 [skip ci] 2026-06-03 01:32:42 +00:00
jmiller f9653411a7 Merge pull request 'docs: CHANGELOG and wiki update for v1.26.1-moko.06.02.00 final' (#444) from dev into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Deploy MokoGitea / deploy (push) Failing after 23s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-03 01:32:08 +00:00
Jonathan Miller 44107d6485 docs: update CHANGELOG and wiki for v1.26.1-moko.06.02.00 final
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
PR RC Release / Build RC Release (pull_request) Failing after 21s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 1m6s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Release configuration (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
Changelog: comprehensive entry covering all features, security
fixes, platform feeds, UI changes, and settings restructure.

Wiki: all 7 platform feeds now listed as Production. Revision 1.5
added covering sub-orgs, visibility modes, settings pages, and
security hardening.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 20:31:06 -05:00
jmiller 82c3c11053 chore: sync updates.xml 05.15.00 from main [skip ci] 2026-06-03 00:15:06 +00:00
gitea-actions[bot] cd2e8b4d34 chore: update channels for 05.15.00 [skip ci] 2026-06-03 00:15:04 +00:00
gitea-actions[bot] acf9b4a4da chore(release): build 05.15.00 [skip ci] 2026-06-03 00:14:33 +00:00
jmiller 23af404ae4 Merge pull request 'fix(licenses): explicit xorm column names for UpdateStreamConfig' (#443) from dev into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Deploy MokoGitea / deploy (push) Failing after 21s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-03 00:14:01 +00:00
Jonathan Miller ff6d1bf3c9 fix(licenses): add explicit xorm column names for all UpdateStreamConfig fields
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
PR RC Release / Build RC Release (pull_request) Failing after 19s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 1m23s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Release configuration (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
xorm auto-maps CamelCase to snake_case by splitting on each
uppercase letter. MaintainerURL became maintainer_u_r_l instead
of maintainer_url, causing DB reads to return empty values.

Added explicit column name tags to all multi-word fields:
SupportURL, KeyPrefix, ExtensionName, DisplayName, ExtensionType,
MaintainerURL, InfoURL, TargetVersion, PHPMinimum, LicensingEnabled,
RequireKey, FeedVisibility, DownloadGating, StreamMode, CustomStreams.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 19:12:54 -05:00
jmiller a5b4f24b48 Merge pull request 'feat(licenses): ancestor-aware org license handler' (#442) from dev into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Deploy MokoGitea / deploy (push) Failing after 3m3s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 23:47:38 +00:00
jmiller 9832f8a7bb chore: add .mokogitea/workflows/auto-release.yml from moko-platform [skip ci]
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
2026-06-02 23:47:37 +00:00
Jonathan Miller 9506a19ab8 feat(licenses): use ancestor-aware functions in org license handler
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Generic: Repo Health / Site Health (push) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 20s
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Release configuration (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Org licenses handler now uses ListLicensePackagesWithAncestors,
ListLicenseKeysWithAncestors, and SearchLicenseKeysWithAncestors
to show packages and keys from parent orgs in the hierarchy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 18:46:33 -05:00
jmiller f485f14615 Merge pull request 'fix(ui): icons on user settings navbar' (#441) from dev into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Deploy MokoGitea / deploy (push) Failing after 1m1s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 22:32:18 +00:00
Jonathan Miller d0e3b3dfd8 fix(ui): add octicon icons to user settings navbar
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
PR RC Release / Build RC Release (pull_request) Successful in 22s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Release configuration (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Profile (person), Account (shield-lock), Notifications (bell),
Appearance (paintbrush), Security (lock), Blocked Users (blocked),
Applications (apps), SSH/GPG Keys (key), Actions (play),
Packages (package), Webhooks (webhook), Organizations (organization),
Repositories (repo).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 17:10:45 -05:00
jmiller 3231ac2707 chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-02 21:51:52 +00:00
jmiller b2d2a3b622 Merge pull request 'fix(licenses): allow anonymous download paths on licensed repos' (#440) from dev into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Deploy MokoGitea / deploy (push) Failing after 1m11s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 20:51:12 +00:00
Jonathan Miller 963fa6d384 fix(licenses): always allow anonymous download path access on licensed repos
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 23s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Release configuration (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
RepoAssignment now always grants LicensedReadOnly for download
paths (/releases/, /archive/) on licensed repos. The actual
download gating (none/prerelease/all) is enforced by
CheckDownloadGating in the handler.

Previously only download_gating=none allowed anonymous access.
Now prerelease gating also allows through (CheckDownloadGating
blocks non-stable downloads without a key).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 15:50:23 -05:00
jmiller ba9907ba41 Merge pull request 'fix(updates): feed always public, downloads gated separately' (#439) from dev into main
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Site Health (push) Has been skipped
Deploy MokoGitea / deploy (push) Failing after 25s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 20:46:14 +00:00
Jonathan Miller 48ff05d4b3 fix(updates): feed always public, downloads gated separately
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 24s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Release configuration (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
The updates.xml/JSON feed is now always fully public with versions
and download URLs visible. The actual file downloads are what get
blocked by CheckDownloadGating (none/prerelease/all).

Previously require_key and feed_visibility controlled the XML
feed visibility. Now the feed is informational only — Joomla can
always see what versions are available. The download gating
setting controls whether the files can actually be downloaded.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 15:45:00 -05:00
Moko Consulting 70699b4f2a chore(ci): add CI issue reporter for auto-filing gate failures
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 20:38:03 +00:00
Moko Consulting 99f5833c25 chore(ci): add CI issue reporter for auto-filing gate failures
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 20:37:54 +00:00
Moko Consulting 241596361e chore(ci): add CI issue reporter for auto-filing gate failures
Generic: Repo Health / Access control (push) Successful in 3s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 20:37:45 +00:00
Moko Consulting b1b64a3b4e chore(ci): add CI issue reporter for auto-filing gate failures
Generic: Repo Health / Access control (push) Successful in 3s
Generic: Repo Health / Site Health (push) Has been skipped
Deploy MokoGitea / deploy (push) Failing after 27s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 20:37:36 +00:00
Moko Consulting 3cddb46053 chore(ci): add CI issue reporter for auto-filing gate failures
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Site Health (push) Has been skipped
Deploy MokoGitea / deploy (push) Failing after 29s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 20:37:28 +00:00
Moko Consulting 0a0cc16528 chore(ci): add CI issue reporter for auto-filing gate failures
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Site Health (push) Has been skipped
Deploy MokoGitea / deploy (push) Failing after 29s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 20:37:17 +00:00
jmiller da41d7072f Merge pull request 'fix(licenses): restrict downloadsPublic to release paths only' (#438) from dev into main
Deploy MokoGitea / deploy (push) Failing after 17s
2026-06-02 20:31:27 +00:00
Jonathan Miller 6405163e60 fix(licenses): restrict downloadsPublic to release/download paths only
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
PR RC Release / Build RC Release (pull_request) Successful in 21s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
The downloadsPublic flag was granting LicensedReadOnly to all
routes including the main repo page, causing 404 on private repos.
Now only applies to paths containing /releases/ or /archive/.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 15:30:23 -05:00
jmiller cb3817f5bc Merge pull request 'fix(licenses): allow anonymous downloads when download_gating=none' (#437) from dev into main
Deploy MokoGitea / deploy (push) Failing after 47s
2026-06-02 20:26:46 +00:00
Jonathan Miller 01011f6115 fix(licenses): allow anonymous downloads when download_gating=none on private repos
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
PR RC Release / Build RC Release (pull_request) Successful in 21s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
RepoAssignment now checks the download_gating setting. When set to
"none" (all downloads public), anonymous users can access release
downloads on licensed private repos without a key.

Previously, anonymous users always got 403 on private repos even
when download gating was set to public.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 15:25:28 -05:00
jmiller f657f58fbb Merge pull request 'fix(ui): octicon-settings to octicon-gear' (#436) from dev into main
Deploy MokoGitea / deploy (push) Failing after 41s
2026-06-02 19:47:07 +00:00
Jonathan Miller ea10e8500c fix(ui): replace invalid octicon-settings with octicon-gear
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
PR RC Release / Build RC Release (pull_request) Successful in 23s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 14:45:37 -05:00
jmiller 1709566fa6 Merge pull request 'fix(ui): section headers with dividers, icons on all settings navbar items' (#435) from dev into main
Deploy MokoGitea / deploy (push) Failing after 1m23s
2026-06-02 19:34:36 +00:00
Jonathan Miller 92bd3f7dc0 fix(ui): clean section headers with dividers instead of accordions, icons on all navbar items
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 22s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Advanced settings: replace collapsible accordions with clean h5
section headers (icon + bold text) separated by dividers. All
sections always visible — no clicking to expand.

Navbar: add octicon icons to every menu item — Options (settings),
Advanced (tools), Licensing (key), Public Access (eye),
Collaboration (people), Webhooks (webhook), Branches (git-branch),
Tags (tag), Git Hooks (terminal), Deploy Keys (key-asterisk),
LFS (file-binary), Actions (play).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 14:33:23 -05:00
jmiller 95c136d838 Merge pull request 'feat(settings): dedicated advanced settings page at /settings/advanced' (#434) from dev into main
Deploy MokoGitea / deploy (push) Failing after 17s
2026-06-02 19:25:37 +00:00
Jonathan Miller 89fcbda623 feat(settings): move advanced settings to dedicated /settings/advanced page
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
PR RC Release / Build RC Release (pull_request) Failing after 18s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Extract all feature unit settings (Code, Wiki, Issues, Projects,
Releases, Packages, Pull Requests) from options.tmpl into a
separate advanced.tmpl with its own route at /settings/advanced.

Options page now only contains: basic repo settings, avatar,
mirror config, signing settings, and danger zone.

Navbar updated: Advanced Settings link points to /settings/advanced.
Form posts still go through the existing SettingsPost handler.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 14:23:49 -05:00
Jonathan Miller dd6ee750f0 fix(build): remove extra {{end}} in settings accordion template
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 14:17:47 -05:00
jmiller 48f32ae961 Merge pull request 'feat(settings): accordion layout for advanced settings' (#433) from dev into main
Deploy MokoGitea / deploy (push) Failing after 4m1s
2026-06-02 19:14:08 +00:00
Jonathan Miller ffb9363e3e feat(settings): accordion layout for advanced settings sections
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 21s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Each feature section (Code, Wiki, Issues, Projects, Releases,
Packages, Pull Requests) is now wrapped in a collapsible <details>
accordion with an icon and bold title.

Code section is open by default; others are collapsed. This
reduces visual clutter and lets admins focus on the section
they need.

Licensing section removed from advanced settings (now on its
own /settings/licensing page).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 14:13:32 -05:00
jmiller dce87fcb5d Merge pull request 'feat(settings): licensing settings page + navbar restructure' (#432) from dev into main
Deploy MokoGitea / deploy (push) Failing after 3m40s
2026-06-02 19:03:52 +00:00
Jonathan Miller a1ceac6396 feat(settings): separate licensing settings page with navbar entry
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 23s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Extract licensing/update feed settings to its own page at
/settings/licensing with dedicated template and handler.

Navbar additions:
- Advanced Settings link (points to existing options page)
- Licensing link with key icon (when licensing enabled)

New handler: LicensingSettings/LicensingSettingsPost serves the
standalone licensing form with all fields (platform, gating,
metadata, extensions).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 14:02:53 -05:00
jmiller 7004170d64 Merge pull request 'fix(ui): login form on 403 page + visibility badge right-aligned' (#431) from dev into main
Deploy MokoGitea / deploy (push) Failing after 19s
2026-06-02 18:56:17 +00:00
Jonathan Miller a22fa57ab1 fix(ui): embed login form on 403 Access Denied page
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 19s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Anonymous users seeing the 403 page now get an inline login form
with username, password, and submit button. After login, redirects
back to the page they were trying to access.

Compact form centered with tw-max-w-sm, includes CSRF token and
redirect_to hidden field pointing to CurrentURL.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 13:55:52 -05:00
jmiller c045c6abfc Merge pull request 'fix(ui): visibility badge floated right of title' (#430) from dev into main
Deploy MokoGitea / deploy (push) Failing after 22s
2026-06-02 18:51:55 +00:00
Jonathan Miller 68736c78a1 fix(ui): float visibility badge to right of title label
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 13:51:31 -05:00
jmiller ce35e3a603 Merge pull request 'fix(build): UpdateRepositoryColsWithAutoTime' (#429) from dev into main
Deploy MokoGitea / deploy (push) Failing after 22s
2026-06-02 18:46:56 +00:00
Jonathan Miller cfea80d3ca fix(build): use UpdateRepositoryColsWithAutoTime for is_hidden
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 21s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 13:46:26 -05:00
jmiller 3e4cb4d2e5 Merge pull request 'feat(repos): three-level visibility Public/Private/Hidden' (#428) from dev into main
Deploy MokoGitea / deploy (push) Failing after 23s
2026-06-02 18:44:15 +00:00
Jonathan Miller e2c738a8d8 feat(repos): three-level visibility — Public, Private, Hidden
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 22s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Add IsHidden field to Repository model. Three visibility modes:

- Public: visible to everyone (green label)
- Private: members only, non-members see 403 Access Denied (orange)
- Hidden: members only, non-members see 404 Not Found (red)

Private mode is for commercial repos — customers know the repo
exists and see a styled 403 page with sign-in button. Licensed
update feeds and key-gated downloads still work.

Hidden mode is for internal/secret repos — complete stealth, as
if the repo doesn't exist.

Settings UI: radio button selector in danger zone replaces the
old binary toggle. Each option shows a colored label with
description.

Migration v342: adds is_hidden column to repository table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 13:42:25 -05:00
jmiller ba361c609f Merge pull request 'fix(licenses): RequireUnitReader allows LicensedReadOnly' (#427) from dev into main
Deploy MokoGitea / deploy (push) Failing after 21s
2026-06-02 15:45:48 +00:00
Jonathan Miller 6c7a6e4061 fix(licenses): RequireUnitReader allows LicensedReadOnly access
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 21s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
RequireUnitReader now checks for LicensedReadOnly context flag
before checking standard permissions. This lets the releases
download route pass through for licensed private repos where
RepoAssignment granted read-only access via license key.

Fixes the 404 on /releases/download/ with valid dlid= param.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 10:23:38 -05:00
jmiller 7aaf8dcbb7 Merge pull request 'fix(licenses): bypass attachment perm check for licensed downloads' (#426) from dev into main
Deploy MokoGitea / deploy (push) Failing after 26s
2026-06-02 15:17:40 +00:00
Jonathan Miller 95d93da2bc fix(licenses): bypass attachment permission check for licensed downloads
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 20s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
ServeAttachment checks perm.CanRead(unitType) which fails for
licensed read-only access on private repos. Now skips the check
when LicensedReadOnly is set in context (from RepoAssignment).

This allows Joomla/WordPress clients with valid dlid= params to
download release files from private licensed repos.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 10:17:06 -05:00
jmiller 128b120ad9 Merge pull request 'fix(licenses): allow downloads on private repos with license key' (#425) from dev into main
Deploy MokoGitea / deploy (push) Failing after 3m3s
2026-06-02 15:13:47 +00:00
Jonathan Miller 02424c3f75 fix(licenses): allow download access on private licensed repos with license key
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 4s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 20s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
RepoAssignment now checks for dlid/key/download_key query params
when licensing is enabled. Anonymous Joomla/WordPress clients with
valid license keys can access release download routes on private
repos without being signed in.

Access flow for licensed private repos:
- Anonymous + no key → 403 (styled page)
- Anonymous + valid dlid → access granted (CheckDownloadGating validates)
- Signed in + no membership → access granted (releases visible, downloads hidden)
- Org member → full access

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 10:12:45 -05:00
jmiller 3f817babd3 Merge pull request 'fix(ui): styled 403 Access Denied page matching 404 layout' (#424) from dev into main
Deploy MokoGitea / deploy (push) Failing after 20s
2026-06-02 15:06:02 +00:00
Jonathan Miller 449af83e2b fix(ui): styled 403 Access Denied page matching the 404 page layout
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 21s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Add templates/status/403.tmpl mirroring the 404 page design with
a centered error message and sign-in button for anonymous users.

New Forbidden() method on Context renders the styled 403 template
for HTML requests, falls back to plain text for API/non-HTML.

RepoAssignment now calls ctx.Forbidden() instead of raw HTTPError
for both anonymous and signed-in users without access.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 10:05:24 -05:00
jmiller 6290ff07e4 Merge pull request 'fix(security): 403 for all users on private repos' (#423) from dev into main
Deploy MokoGitea / deploy (push) Failing after 1m1s
2026-06-02 14:57:50 +00:00
Jonathan Miller 3ad37e48e1 fix(security): return 403 for all users on private repos, not 404
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 21s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Both anonymous and signed-in users now get 403 Access Denied when
accessing a private repo they lack permission for. Previously
anonymous users got 404 which hid the repo's existence.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 09:57:29 -05:00
jmiller c4e51ff55c Merge pull request 'fix(licenses): licensed private repos allow release viewing for signed-in users' (#422) from dev into main
Deploy MokoGitea / deploy (push) Failing after 3m10s
2026-06-02 14:52:29 +00:00
Jonathan Miller 021a054348 fix(licenses): licensed private repos allow signed-in users to view releases
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 4s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 19s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
When licensing is enabled on a private repo, signed-in users who
are not repo members can now view the releases page (with downloads
hidden). The RepoAssignment permission check detects licensing and
grants read-only access instead of returning 403.

This enables the commercial pattern: private source code, but
release notes visible to any authenticated user. Download files
are gated by license key via HideReleaseDownloads.

Anonymous users still get 404 (no information leak).
Non-licensed private repos still return 403 for non-members.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 09:52:05 -05:00
jmiller b707c5aff9 Merge pull request 'fix(updates): allow update feeds on private repos' (#421) from dev into main
Deploy MokoGitea / deploy (push) Failing after 2m48s
2026-06-02 14:37:42 +00:00
Jonathan Miller ead620daf9 fix(updates): allow update feeds on private repos via lightweight repo loader
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 20s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Update feed endpoints (updates.xml, dolibarr.json, wordpress.json,
packages.json, prestashop.xml, drupal.xml, whmcs.json, changelog.xml)
now use RepoAssignmentPublicFeed instead of the full RepoAssignment.

The lightweight loader fetches the repo by owner/name without checking
user permissions. Feed handlers gate access via license keys, not repo
membership. This allows private repos to serve update feeds to
anonymous Joomla/WordPress/Composer clients with valid license keys.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 09:36:06 -05:00
jmiller 2db1f4eaf6 Merge pull request 'fix(security): 403 Access Denied for signed-in users on private repos' (#420) from dev into main
Deploy MokoGitea / deploy (push) Failing after 1m10s
2026-06-02 14:27:19 +00:00
Jonathan Miller 0add8bda72 fix(security): show 403 Access Denied instead of 404 for signed-in users on private repos
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 19s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Signed-in users who lack permission to a private repo now see a
403 "You do not have permission" instead of a misleading 404.
Anonymous users still get 404 to prevent repo enumeration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 09:26:21 -05:00
jmiller 25499fb183 Merge pull request 'fix(build): unused import in drupal.go' (#419) from dev into main
Deploy MokoGitea / deploy (push) Failing after 21s
2026-06-02 14:10:45 +00:00
Jonathan Miller bd81616432 fix(build): remove unused time import in drupal.go
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 20s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 09:10:11 -05:00
jmiller 7c15301228 Merge pull request 'feat(updates): PrestaShop, Drupal, WHMCS update feeds (#352, #353, #355)' (#418) from dev into main
Deploy MokoGitea / deploy (push) Failing after 21s
2026-06-02 14:08:33 +00:00
Jonathan Miller 02f3ed88f1 feat(updates): PrestaShop (#352), Drupal (#353), WHMCS (#355) update feeds
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 18s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
PrestaShop: GET /updates/prestashop.xml — module update XML with
name, version, download URL, author, SHA256. Serves stable only.

Drupal: GET /updates/drupal.xml — update status XML per Drupal API
spec. Includes project metadata, all releases with status, download
links, SHA256. Uses TargetVersion config for api_version field.

WHMCS: GET /updates/whmcs.json — simple JSON with latest stable
version, download URL (with dlid), changelog, author. License key
embedded in download URL when provided.

All three use ResolveReleaseStream for manual/auto stream mapping,
readSHA256FromSidecar for integrity hashes, and extractVersion with
stream-name tag fallback.

Routes registered under the update server group alongside Joomla,
Dolibarr, WordPress, and Composer feeds.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 09:08:03 -05:00
jmiller e4718f5036 Merge pull request 'feat(updates): Composer feed (#354), hide Actions/Licenses tabs for guests' (#417) from dev into main
Deploy MokoGitea / deploy (push) Failing after 2m59s
2026-06-02 14:02:23 +00:00
Jonathan Miller 0fb0aea719 feat(updates): Composer packages.json feed (#354), hide menu items for guests
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 20s
Composer feed: new endpoint GET /updates/packages.json serving
Composer/Packagist-compatible packages.json. Includes version,
dist URL with SHA256, authors, PHP requirement. License key
embedded in download URL when provided.

Menu visibility: Actions and Licenses tabs in repo header now
require .IsSigned — anonymous users no longer see tabs they
can't access. Previously the tabs were visible but clicking
redirected to login (confusing UX).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 09:02:00 -05:00
jmiller 581bfa5f31 Merge pull request 'feat(licenses): key prefix (#406), header button (#408), open feed (#409)' (#416) from dev into main
Deploy MokoGitea / deploy (push) Failing after 18s
2026-06-02 13:52:27 +00:00
Jonathan Miller eca929f680 feat(licenses): configurable key prefix (#406), header button (#408), open feed button (#409)
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
PR RC Release / Build RC Release (pull_request) Failing after 18s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
#406: Add KeyPrefix field to UpdateStreamConfig. GenerateKeyString
now accepts a prefix parameter, looked up from org config. Default
remains MOKO if not set. Auto-uppercased, max 20 chars.

#408: Move "New Package" button into the packages header bar,
right-aligned. Uses details/summary pattern — clicking the button
expands the create form below. Cleaner layout on both repo and org.

#409: Add open-in-new-tab button (external link icon) next to every
copy button on feed URLs. All four feeds: Joomla XML, Dolibarr JSON,
WordPress JSON, Changelog XML.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 08:51:55 -05:00
jmiller 8ae663e15e Merge pull request 'SECURITY: fix release download gating and require login for actions' (#415) from dev into main
Deploy MokoGitea / deploy (push) Failing after 1m11s
2026-06-02 13:41:10 +00:00
Jonathan Miller b65b155446 SECURITY: fix release download gating and require login for actions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 21s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Release gating: HideReleaseDownloads now checks download_gating
setting in addition to feed_visibility. When licensing is enabled
and download_gating != "none", anonymous users see "Sign in to
download" instead of download links on the release page.

Actions: changed from optSignIn to reqSignIn on the repo actions
route group. Anonymous users can no longer view CI/CD runs, logs,
or artifacts. This is a MokoGitea policy override — upstream Gitea
allows public actions on public repos.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 08:40:40 -05:00
jmiller 4bc962adbf Merge pull request 'fix(build): permanent fixes for recurring build errors' (#414) from dev into main
Deploy MokoGitea / deploy (push) Failing after 20s
2026-06-02 13:35:03 +00:00
Jonathan Miller de52ad0fbc fix(build): permanent fixes for recurring build errors
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 11s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 26s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
- AI migration 339: replaced with noopMigration placeholder
- feed/file.go: add missing comma in struct literal
- license_key.go: remove unused org_model import

These were being applied as server-side hotfixes on every deploy.
Now committed to dev so they persist through merges.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 08:34:14 -05:00
jmiller ca841716db Merge pull request 'SECURITY: require login for licenses page' (#413) from dev into main
Deploy MokoGitea / deploy (push) Failing after 25s
2026-06-02 13:26:17 +00:00
Jonathan Miller 1dfa5d8079 SECURITY: require login for licenses page — was accessible anonymously
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 21s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
The repo licenses route used optSignIn (login optional), allowing
anonymous users to view license packages and keys. Changed to
reqSignIn to require authentication.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 08:25:31 -05:00
jmiller 117daf51c3 Merge pull request 'fix(build): org list API and unused import' (#412) from dev into main
Deploy MokoGitea / deploy (push) Failing after 23s
2026-06-02 13:22:10 +00:00
Jonathan Miller 70793075fc fix(build): use db.Find for org list, remove unused org_model import
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 22s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 08:21:39 -05:00
jmiller a2e0735a26 Merge pull request 'feat(orgs): enterprise sub-org hierarchy (#410)' (#411) from dev into main
Deploy MokoGitea / deploy (push) Failing after 1m18s
2026-06-02 13:15:30 +00:00
Jonathan Miller 2799558040 feat(orgs): enterprise sub-org hierarchy with parent-child relationships (#410)
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 26s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Add ParentOrgID field to User model for org hierarchy. Parent orgs
can have child orgs, enabling enterprise structures like
MokoConsulting → client orgs.

Model changes:
- ParentOrgID int64 on User (INDEX, DEFAULT 0)
- GetChildOrgs, GetAncestorOrgIDs, GetParentOrg helpers
- Max 10 hierarchy levels with cycle detection

License integration:
- ListLicensePackagesWithAncestors — shows packages from parent orgs
- ListLicenseKeysWithAncestors — shows keys from parent orgs
- SearchLicenseKeysWithAncestors — searches across hierarchy
- Master keys from parent orgs validate for child org repos

UI:
- Parent org dropdown in org settings (owners/admins only)
- Shows all orgs user owns except self

Migration v341: adds parent_org_id column to user table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 08:14:08 -05:00
jmiller 1a46a8f14f Merge pull request 'fix(build): EditReleaseForm UpdateStream field' (#405) from dev into main
Deploy MokoGitea / deploy (push) Failing after 19s
2026-06-02 12:56:36 +00:00
Jonathan Miller d85ae6aa21 fix(build): add UpdateStream to EditReleaseForm
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 19s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 07:56:11 -05:00
jmiller b18519e8b9 Merge pull request 'fix(build): pass ctx to WordPress changelog builder' (#404) from dev into main
Deploy MokoGitea / deploy (push) Failing after 1m32s
2026-06-02 12:48:26 +00:00
jmiller 94649efed0 Merge pull request 'feat(updates): manual stream mapping, version extraction fixes, feed visibility' (#403) from dev into main
Deploy MokoGitea / deploy (push) Failing after 22s
2026-06-02 12:43:29 +00:00
jmiller a52ac1bf61 Merge pull request 'feat(licenses): full commercial license management system v1.26.1-moko.06.02.00' (#402) from dev into main
Deploy MokoGitea / deploy (push) Failing after 19s
feat(licenses): full commercial license management system v1.26.1-moko.06.02.00 (#402)
2026-06-02 12:00:19 +00:00
jmiller 5da4b3b314 Merge pull request 'fix(build): remove unused imports' (#377) from dev into main
Deploy MokoGitea / deploy (push) Failing after 21s
2026-05-31 18:51:42 +00:00
jmiller 75e2a21b89 Merge pull request 'chore: merge dev into main — Issue.Ref deprecation, stale TODO cleanup' (#376) from dev into main
Deploy MokoGitea / deploy (push) Failing after 3m45s
2026-05-31 18:39:57 +00:00
jmiller e82fe7d021 Merge pull request 'fix(cron): add missing translation for cleanup_expired_license_keys' (#375) from dev into main
Deploy MokoGitea / deploy (push) Failing after 21s
2026-05-31 18:34:46 +00:00
jmiller 24a9bfb30d Merge pull request 'fix(docker): disable openssh s6 service in Dockerfile' (#374) from dev into main
Deploy MokoGitea / deploy (push) Failing after 4m30s
2026-05-31 17:14:36 +00:00
jmiller 257908e083 Merge pull request 'chore: merge dev into main — tech-debt, namespace migration, combo-multiselect' (#373) from dev into main
Deploy MokoGitea / deploy (push) Failing after 6m39s
2026-05-31 17:12:39 +00:00
jmiller 2c3aad51af Merge pull request 'fix(build): Go 1.23 maps.Values slices.Collect' (#371) from dev into main
Deploy MokoGitea / deploy (push) Failing after 25s
2026-05-31 16:38:42 +00:00
jmiller 66a6a2afc1 Merge pull request 'fix(build): Go 1.23 maps.Values compatibility' (#370) from dev into main
Deploy MokoGitea / deploy (push) Failing after 22s
2026-05-31 16:31:39 +00:00
jmiller 74935e3bed Merge pull request 'fix(licenses): remove duplicate DeleteLicenseKey (build fix)' (#358) from dev into main
Deploy MokoGitea / deploy (push) Failing after 3m39s
2026-05-31 16:07:13 +00:00
jmiller bc95ecf4d5 Merge pull request 'feat(updates): extension metadata settings, tab visibility, platform support' (#356) from dev into main
Deploy MokoGitea / deploy (push) Failing after 28s
2026-05-31 16:01:49 +00:00
jmiller a35fb4695c Merge pull request 'chore: sync dev to main (namespace rename + all fixes)' (#348) from dev into main
Deploy MokoGitea / deploy (push) Failing after 3m18s
2026-05-31 15:40:34 +00:00
jmiller 70c31a4953 Merge pull request 'fix(updates): correct dlid prefix and Joomla standard alignment' (#345) from dev into main
Deploy MokoGitea / deploy (push) Failing after 2m32s
2026-05-31 15:31:09 +00:00
jmiller 6c913abbda Merge pull request 'feat(licenses): plaintext key storage with copy buttons' (#342) from dev into main
Deploy MokoGitea / deploy (push) Failing after 3m35s
2026-05-31 15:08:02 +00:00
jmiller 878671ebc9 Merge pull request 'feat(licenses): platform enforcement, key deletion, expired key cleanup' (#340) from dev into main
Deploy MokoGitea / deploy (push) Failing after 3m4s
2026-05-31 15:03:46 +00:00
jmiller c7cfcf894b Merge pull request 'fix(licenses): remove repo unit requirement causing 404s' (#339) from dev into main
Deploy MokoGitea / deploy (push) Failing after 3m35s
2026-05-31 14:48:30 +00:00
jmiller bbe3e570fe Merge pull request 'chore: migrate namespace from git. to code.mokoconsulting.tech' (#337) from chore/namespace-migration into main
Deploy MokoGitea / deploy (push) Failing after 4m27s
2026-05-31 14:46:21 +00:00
Jonathan Miller 26bbe690fd chore: migrate namespace from git. to code.mokoconsulting.tech (#336)
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 28s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Update all URLs in manifest.xml and updates.xml to use the new
code.mokoconsulting.tech domain.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 09:45:58 -05:00
jmiller bfa9043bc8 Merge pull request 'feat(licenses): UI/UX cleanup, permissions system, and key management improvements' (#306) from dev into main
Deploy MokoGitea / deploy (push) Failing after 3m35s
2026-05-31 14:22:04 +00:00
jmiller b1a9b09f5b Merge pull request 'chore: merge dev into main — toggle fix' (#295) from dev into main
Deploy MokoGitea / deploy (push) Failing after 3m59s
2026-05-31 04:22:47 +00:00
81 changed files with 5499 additions and 2018 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
<name>MokoGitea</name>
<org>MokoConsulting</org>
<description>Moko fork of Gitea — adding project board REST API endpoints and custom enhancements</description>
<version>05.14.00</version>
<version>05.31.00</version>
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
</identity>
<governance>
+285
View File
@@ -0,0 +1,285 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/universal/auto-release.yml.template
# VERSION: 05.00.00
# BRIEF: Universal build & release detects platform from manifest.xml
#
# +========================================================================+
# | UNIVERSAL BUILD & RELEASE PIPELINE |
# +========================================================================+
# | |
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
# | |
# | Platform-specific: |
# | joomla: XML manifest, updates.xml, type-prefixed packages |
# | dolibarr: mod*.class.php, update.txt, dev version reset |
# | generic: README-only, no update stream |
# | |
# +========================================================================+
name: "Universal: Build & Release"
on:
pull_request:
types: [opened, closed]
branches:
- main
workflow_dispatch:
inputs:
action:
description: 'Action to perform'
required: false
type: choice
default: release
options:
- release
- promote-rc
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
permissions:
contents: write
jobs:
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────
promote-rc:
name: Promote to RC
runs-on: release
if: >-
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
# Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api
composer install --no-dev --no-interaction --quiet
- name: Rename branch to rc
run: |
php /tmp/moko-platform-api/cli/branch_rename.php \
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
--pr "${{ github.event.pull_request.number }}"
- name: Checkout rc and configure git
run: |
git fetch origin rc
git checkout rc
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- name: Publish RC release
run: |
php /tmp/moko-platform-api/cli/release_publish.php \
--path . --stability rc --bump minor --branch rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--skip-update-stream
- name: Summary
if: always()
run: |
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
echo "Branch renamed to rc, minor bump, RC release built (updates.xml managed by Gitea Pages)" >> $GITHUB_STEP_SUMMARY
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
release:
name: Build & Release Pipeline
runs-on: release
if: >-
github.event.pull_request.merged == true ||
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 0
- name: Configure git for bot pushes
run: |
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- name: Check for merge conflict markers
run: |
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
if [ -n "$CONFLICTS" ]; then
echo "::error::Merge conflict markers found — aborting release"
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "No conflict markers found"
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
run: |
# Ensure PHP + Composer are available
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
# Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api
composer install --no-dev --no-interaction --quiet
- name: "Publish stable release"
run: |
php /tmp/moko-platform-api/cli/release_publish.php \
--path . --stability stable --bump minor --branch main \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--skip-update-stream
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
- name: "Step 9: Mirror release to GitHub"
if: >-
steps.version.outputs.skip != 'true' &&
secrets.GH_MIRROR_TOKEN != ''
continue-on-error: true
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php /tmp/moko-platform-api/cli/release_mirror.php \
--version "$VERSION" --tag "$RELEASE_TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
--branch main 2>&1 || true
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
- name: "Step 10: Push main to GitHub mirror"
if: >-
steps.version.outputs.skip != 'true' &&
secrets.GH_MIRROR_TOKEN != ''
continue-on-error: true
run: |
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
git fetch origin main --depth=1
git push github origin/main:refs/heads/main --force 2>/dev/null \
&& echo "main branch pushed to GitHub mirror" \
|| echo "WARNING: GitHub mirror push failed"
- name: "Step 11: Delete rc branch and recreate dev from main"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Delete rc branch (ephemeral — created by promote-rc)
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/rc" 2>/dev/null \
&& echo "Deleted rc branch" || echo "rc branch not found"
# Delete dev branch
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
# Recreate dev from main (now includes version bump + changelog promotion)
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API_BASE}/branches" \
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
- name: "Step 12: Create version branch from main"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
BRANCH_NAME="version/${VERSION}"
MAIN_SHA=$(git rev-parse HEAD)
# Delete old version branch if it exists (same version re-release)
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
# Create version/XX.YY.ZZ from main
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
# -- Dolibarr post-release: Reset dev version -----------------------------
- name: "Post-release: Reset dev version"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php /tmp/moko-platform-api/cli/version_reset_dev.php \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
--branch dev --path . 2>&1 || true
# -- Summary --------------------------------------------------------------
- name: Pipeline Summary
if: always()
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
PLATFORM="${{ steps.platform.outputs.platform }}"
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
fi
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Automation
# VERSION: 05.14.00
# VERSION: 05.31.00
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
+508 -236
View File
@@ -1,236 +1,508 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.CI
# REPO: https://code.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/universal/pr-check.yml.template
# VERSION: 05.00.00
# BRIEF: PR gate — branch policy + code validation before merge
name: "Universal: PR Check"
on:
pull_request:
types: [opened, synchronize, reopened, edited]
permissions:
contents: read
pull-requests: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
# ── Branch Policy ──────────────────────────────────────────────────────
branch-policy:
name: Branch Policy
runs-on: ubuntu-latest
steps:
- name: Check branch merge target
run: |
HEAD="${{ github.head_ref }}"
BASE="${{ github.base_ref }}"
echo "PR: ${HEAD} → ${BASE}"
ALLOWED=true
REASON=""
case "$HEAD" in
feature/*|feat/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Feature branches must target 'dev', not '${BASE}'"
fi
;;
fix/*|bugfix/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Fix branches must target 'dev', not '${BASE}'"
fi
;;
patch/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
ALLOWED=false
REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
fi
;;
hotfix/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
fi
;;
rc)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="RC branch can only merge into 'main', not '${BASE}'"
fi
;;
dev)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Dev branch can only merge into 'main', not '${BASE}'"
fi
;;
esac
if [ "$ALLOWED" = false ]; then
echo "::error::${REASON}"
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "Branch policy: OK (${HEAD} → ${BASE})"
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
# ── Code Validation ────────────────────────────────────────────────────
validate:
name: Validate PR
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Detect platform
id: platform
run: |
# Read platform from XML manifest (<platform> tag) or plain text fallback
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
[ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
[ -z "$PLATFORM" ] && PLATFORM="generic"
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
- name: Setup PHP
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
run: |
if ! command -v php &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
fi
- name: PHP syntax check
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
run: |
ERRORS=0
while IFS= read -r -d '' file; do
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
ERRORS=$((ERRORS + 1))
fi
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
echo "PHP lint: ${ERRORS} error(s)"
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
- name: Validate platform manifest
run: |
PLATFORM="${{ steps.platform.outputs.platform }}"
case "$PLATFORM" in
joomla)
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -z "$MANIFEST" ]; then
echo "::warning::No Joomla manifest found (WaaS site)"
exit 0
fi
echo "Manifest: ${MANIFEST}"
if command -v php &> /dev/null; then
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
fi
for ELEMENT in name version description; do
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
done
echo "Joomla manifest valid"
;;
dolibarr)
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
if [ -z "$MOD_FILE" ]; then
echo "::error::No mod*.class.php found"
exit 1
fi
echo "Dolibarr module: ${MOD_FILE}"
;;
*)
echo "Generic platform — no manifest validation"
;;
esac
- name: Check update stream format
run: |
PLATFORM="${{ steps.platform.outputs.platform }}"
case "$PLATFORM" in
joomla)
if [ -f "updates.xml" ]; then
if command -v php &> /dev/null; then
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
fi
echo "updates.xml valid"
fi
;;
dolibarr)
[ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
;;
esac
- name: Check changelog has unreleased entry
run: |
if [ ! -f "CHANGELOG.md" ]; then
echo "::warning::No CHANGELOG.md found"
exit 0
fi
# Check for content under [Unreleased] section
if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
echo "::error::CHANGELOG.md missing [Unreleased] section"
exit 1
fi
# Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
- name: Verify package source
run: |
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ ! -d "$SOURCE_DIR" ]; then
echo "::warning::No src/ or htdocs/ directory"
exit 0
fi
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
echo "Source: ${FILE_COUNT} files"
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
# ── Pre-Release RC Build ─────────────────────────────────────────────────
pre-release:
name: Build RC Package
runs-on: ubuntu-latest
needs: [branch-policy, validate]
steps:
- name: Trigger RC pre-release
env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
REPO: ${{ github.repository }}
BRANCH: ${{ github.head_ref }}
GITEA_URL: ${{ vars.GITEA_URL || 'https://code.mokoconsulting.tech' }}
run: |
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.CI
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/universal/pr-check.yml.template
# VERSION: 09.23.00
# BRIEF: PR gate — branch policy + code validation before merge
name: "Universal: PR Check"
on:
pull_request:
types: [opened, synchronize, reopened, edited]
permissions:
contents: read
pull-requests: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
# ── Branch Policy ──────────────────────────────────────────────────────
branch-policy:
name: Branch Policy
runs-on: ubuntu-latest
steps:
- name: Check branch merge target
run: |
HEAD="${{ github.head_ref }}"
BASE="${{ github.base_ref }}"
echo "PR: ${HEAD} → ${BASE}"
ALLOWED=true
REASON=""
case "$HEAD" in
feature/*|feat/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Feature branches must target 'dev', not '${BASE}'"
fi
;;
fix/*|bugfix/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Fix branches must target 'dev', not '${BASE}'"
fi
;;
patch/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
ALLOWED=false
REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
fi
;;
hotfix/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
fi
;;
rc)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="RC branch can only merge into 'main', not '${BASE}'"
fi
;;
dev)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Dev branch can only merge into 'main', not '${BASE}'"
fi
;;
esac
if [ "$ALLOWED" = false ]; then
echo "::error::${REASON}"
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "Branch policy: OK (${HEAD} → ${BASE})"
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
# ── Code Validation ────────────────────────────────────────────────────
validate:
name: Validate PR
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check for merge conflict markers
run: |
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
if [ -n "$CONFLICTS" ]; then
echo "::error::Merge conflict markers found in source files"
echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "No conflict markers found"
- name: Detect platform
id: platform
run: |
# Read platform from XML manifest (<platform> tag) or plain text fallback
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
[ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
[ -z "$PLATFORM" ] && PLATFORM="generic"
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
- name: Setup PHP
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
run: |
if ! command -v php &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
fi
- name: PHP syntax check
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
run: |
ERRORS=0
while IFS= read -r -d '' file; do
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
ERRORS=$((ERRORS + 1))
fi
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
echo "PHP lint: ${ERRORS} error(s)"
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
- name: Joomla JEXEC guard check
if: steps.platform.outputs.platform == 'joomla'
run: |
ERRORS=0
while IFS= read -r -d '' file; do
# Skip vendor, node_modules, and index.html stub files
case "$file" in ./vendor/*|./node_modules/*) continue ;; esac
# Check first 10 lines for JEXEC or JPATH guard
if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then
echo "::error file=${file}::Missing JEXEC guard: ${file}"
ERRORS=$((ERRORS + 1))
fi
done < <(find . -name "*.php" -path "*/src/*" -not -path "./.git/*" -not -path "./vendor/*" -print0)
if [ "$ERRORS" -gt 0 ]; then
echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard"
echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY
echo "${ERRORS} file(s) in src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "JEXEC guard: OK"
- name: Joomla directory listing protection
if: steps.platform.outputs.platform == 'joomla'
run: |
MISSING=0
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && exit 0
while IFS= read -r dir; do
if [ ! -f "${dir}/index.html" ]; then
echo "::warning::Missing index.html in ${dir} (directory listing protection)"
MISSING=$((MISSING + 1))
fi
done < <(find "$SOURCE_DIR" -type d -not -path "./.git/*" -not -path "*/vendor/*" -not -path "*/node_modules/*")
if [ "$MISSING" -gt 0 ]; then
echo "## Directory Protection" >> $GITHUB_STEP_SUMMARY
echo "${MISSING} director(ies) missing index.html" >> $GITHUB_STEP_SUMMARY
fi
echo "Directory protection: ${MISSING} missing (advisory)"
- name: Joomla script file and asset checks
if: steps.platform.outputs.platform == 'joomla'
run: |
ERRORS=0
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
[ -z "$MANIFEST" ] && exit 0
MANIFEST_DIR=$(dirname "$MANIFEST")
# Check scriptfile exists if declared
SCRIPTFILE=$(sed -n 's/.*<scriptfile>\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null)
if [ -n "$SCRIPTFILE" ]; then
if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then
echo "::error::Manifest declares <scriptfile>${SCRIPTFILE}</scriptfile> but file not found at ${MANIFEST_DIR}/${SCRIPTFILE}"
ERRORS=$((ERRORS + 1))
else
echo "Script file: ${MANIFEST_DIR}/${SCRIPTFILE} (OK)"
fi
fi
# Require joomla.asset.json and validate it
ASSET_JSON=$(find "$MANIFEST_DIR" -name "joomla.asset.json" -not -path "./.git/*" 2>/dev/null | head -1)
if [ -z "$ASSET_JSON" ]; then
echo "::error::joomla.asset.json not found — Joomla asset system is required"
ERRORS=$((ERRORS + 1))
else
if command -v php &> /dev/null; then
php -r "json_decode(file_get_contents('$ASSET_JSON')); if(json_last_error()!==JSON_ERROR_NONE){echo json_last_error_msg();exit(1);}" 2>&1 || {
echo "::error::joomla.asset.json is not valid JSON"
ERRORS=$((ERRORS + 1))
}
fi
echo "joomla.asset.json: valid"
fi
# Validate all XML files in src/ are well-formed
XML_ERRORS=0
if command -v php &> /dev/null; then
while IFS= read -r -d '' xmlfile; do
if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$xmlfile'); if(!\$x){foreach(libxml_get_errors() as \$e) echo trim(\$e->message) . ' in $xmlfile'; exit(1);}" 2>&1; then
XML_ERRORS=$((XML_ERRORS + 1))
fi
done < <(find "$MANIFEST_DIR" -name "*.xml" -not -path "./.git/*" -print0)
fi
if [ "$XML_ERRORS" -gt 0 ]; then
echo "::error::${XML_ERRORS} XML file(s) are malformed"
ERRORS=$((ERRORS + 1))
else
echo "XML well-formedness: OK"
fi
[ "$ERRORS" -gt 0 ] && exit 1
echo "Joomla asset checks: OK"
- name: Validate platform manifest
run: |
PLATFORM="${{ steps.platform.outputs.platform }}"
case "$PLATFORM" in
joomla)
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -z "$MANIFEST" ]; then
echo "::warning::No Joomla manifest found (WaaS site)"
exit 0
fi
echo "Manifest: ${MANIFEST}"
if command -v php &> /dev/null; then
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
fi
for ELEMENT in name version description; do
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
done
# Block legacy raw/branch update server URLs on MokoGitea
RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true)
if [ -n "$RAW_URLS" ]; then
echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the Gitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)"
echo "$RAW_URLS"
exit 1
fi
echo "Joomla manifest valid"
;;
dolibarr)
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
if [ -z "$MOD_FILE" ]; then
echo "::error::No mod*.class.php found"
exit 1
fi
echo "Dolibarr module: ${MOD_FILE}"
;;
*)
echo "Generic platform — no manifest validation"
;;
esac
- name: Check update stream format
run: |
PLATFORM="${{ steps.platform.outputs.platform }}"
case "$PLATFORM" in
joomla)
if [ -f "updates.xml" ]; then
if command -v php &> /dev/null; then
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
fi
echo "updates.xml valid"
fi
;;
dolibarr)
[ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
;;
esac
- name: Validate Joomla language files
if: steps.platform.outputs.platform == 'joomla'
run: |
ERRORS=0
WARNINGS=0
# Require both en-GB and en-US language directories
LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1)
if [ -z "$LANG_ROOT" ]; then
echo "No language/ directory found — skipping"
exit 0
fi
if [ ! -d "$LANG_ROOT/en-GB" ]; then
echo "::error::Missing en-GB language directory (${LANG_ROOT}/en-GB)"
ERRORS=$((ERRORS + 1))
fi
if [ ! -d "$LANG_ROOT/en-US" ]; then
echo "::error::Missing en-US language directory (${LANG_ROOT}/en-US)"
ERRORS=$((ERRORS + 1))
fi
# Check that en-GB and en-US have matching .ini files
if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then
for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do
[ ! -f "$GB_INI" ] && continue
US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")"
if [ ! -f "$US_INI" ]; then
echo "::error::$(basename "$GB_INI") exists in en-GB but missing from en-US"
ERRORS=$((ERRORS + 1))
fi
done
for US_INI in "$LANG_ROOT/en-US"/*.ini; do
[ ! -f "$US_INI" ] && continue
GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")"
if [ ! -f "$GB_INI" ]; then
echo "::error::$(basename "$US_INI") exists in en-US but missing from en-GB"
ERRORS=$((ERRORS + 1))
fi
done
fi
# Find all .ini language files
INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null)
if [ -z "$INI_FILES" ]; then
echo "No .ini language files found"
[ "$ERRORS" -gt 0 ] && exit 1
exit 0
fi
echo "Found $(echo "$INI_FILES" | wc -l) language file(s)"
for FILE in $INI_FILES; do
FNAME=$(basename "$FILE")
LINENUM=0
SEEN_KEYS=""
while IFS= read -r line || [ -n "$line" ]; do
LINENUM=$((LINENUM + 1))
# Skip empty lines and comments
[ -z "$line" ] && continue
echo "$line" | grep -qE '^\s*;' && continue
echo "$line" | grep -qE '^\s*$' && continue
# Must match KEY="VALUE" format
if ! echo "$line" | grep -qE '^[A-Z_][A-Z0-9_]*=".*"$'; then
echo "::error file=${FILE},line=${LINENUM}::Malformed line: ${line}"
ERRORS=$((ERRORS + 1))
continue
fi
# Extract key and check for duplicates
KEY=$(echo "$line" | sed 's/=.*//')
if echo "$SEEN_KEYS" | grep -qx "$KEY"; then
echo "::error file=${FILE},line=${LINENUM}::Duplicate key: ${KEY}"
ERRORS=$((ERRORS + 1))
fi
SEEN_KEYS="${SEEN_KEYS}
${KEY}"
done < "$FILE"
echo " ${FILE}: checked ${LINENUM} lines"
done
# Cross-check en-GB vs en-US key consistency
GB_DIR=$(find . -path "*/language/en-GB" -type d -not -path "./.git/*" 2>/dev/null | head -1)
US_DIR=$(find . -path "*/language/en-US" -type d -not -path "./.git/*" 2>/dev/null | head -1)
if [ -n "$GB_DIR" ] && [ -n "$US_DIR" ]; then
for GB_FILE in "$GB_DIR"/*.ini; do
[ ! -f "$GB_FILE" ] && continue
FNAME=$(basename "$GB_FILE")
US_FILE="$US_DIR/$FNAME"
[ ! -f "$US_FILE" ] && continue
GB_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$GB_FILE" 2>/dev/null | sort)
US_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$US_FILE" 2>/dev/null | sort)
# Keys in en-GB but not en-US
MISSING_US=$(comm -23 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
if [ -n "$MISSING_US" ]; then
echo "::warning::Keys in en-GB/$FNAME but missing from en-US/$FNAME:"
echo "$MISSING_US" | while read -r k; do echo " - $k"; done
WARNINGS=$((WARNINGS + 1))
fi
# Keys in en-US but not en-GB
MISSING_GB=$(comm -13 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
if [ -n "$MISSING_GB" ]; then
echo "::warning::Keys in en-US/$FNAME but missing from en-GB/$FNAME:"
echo "$MISSING_GB" | while read -r k; do echo " - $k"; done
WARNINGS=$((WARNINGS + 1))
fi
done
fi
{
echo "### Language File Validation"
echo "| Metric | Count |"
echo "|---|---|"
echo "| Files checked | $(echo "$INI_FILES" | wc -l) |"
echo "| Errors | ${ERRORS} |"
echo "| Warnings | ${WARNINGS} |"
} >> $GITHUB_STEP_SUMMARY
if [ "$ERRORS" -gt 0 ]; then
echo "::error::Language validation failed with ${ERRORS} error(s)"
exit 1
fi
echo "Language files: OK (${WARNINGS} warning(s))"
- name: Check changelog has unreleased entry
run: |
if [ ! -f "CHANGELOG.md" ]; then
echo "::warning::No CHANGELOG.md found"
exit 0
fi
# Check for content under [Unreleased] section
if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
echo "::error::CHANGELOG.md missing [Unreleased] section"
exit 1
fi
# Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
- name: Verify package source
run: |
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ ! -d "$SOURCE_DIR" ]; then
echo "::warning::No src/ or htdocs/ directory"
exit 0
fi
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
echo "Source: ${FILE_COUNT} files"
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
# ── Pre-Release RC Build ─────────────────────────────────────────────────
pre-release:
name: Build RC Package
runs-on: ubuntu-latest
needs: [branch-policy, validate]
steps:
- name: Trigger RC pre-release
env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
REPO: ${{ github.repository }}
BRANCH: ${{ github.head_ref }}
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: |
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
# ── Issue Reporter ──────────────────────────────────────────────────────
report-issues:
name: Report Issues
runs-on: ubuntu-latest
needs: [branch-policy, validate]
if: >-
always() &&
needs.validate.result == 'failure'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
sparse-checkout: automation/ci-issue-reporter.sh
sparse-checkout-cone-mode: false
- name: "File issue for PR validation failure"
env:
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: |
chmod +x automation/ci-issue-reporter.sh
./automation/ci-issue-reporter.sh \
--gate "PR Validation" \
--workflow "PR Check" \
--severity error \
--details "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed."
+711
View File
@@ -0,0 +1,711 @@
# ============================================================================
# Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
#
# This file is part of a Moko Consulting project.
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Validation
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/joomla/repo_health.yml.template
# VERSION: 09.23.00
# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts.
# ============================================================================
name: "Generic: Repo Health"
defaults:
run:
shell: bash
on:
workflow_dispatch:
inputs:
profile:
description: 'Validation profile: all, scripts, or repo'
required: true
default: all
type: choice
options:
- all
- scripts
- repo
pull_request:
push:
permissions:
contents: read
env:
# Scripts governance policy
SCRIPTS_REQUIRED_DIRS:
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
# Repo health policy
REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.mokogitea/workflows/
REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/
REPO_DISALLOWED_DIRS:
REPO_DISALLOWED_FILES: TODO.md,todo.md
# Extended checks toggles
EXTENDED_CHECKS: "true"
# File / directory variables
DOCS_INDEX: docs/docs-index.md
SCRIPT_DIR: scripts
WORKFLOWS_DIR: .mokogitea/workflows
SHELLCHECK_PATTERN: '*.sh'
SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml'
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
access_check:
name: Access control
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
outputs:
allowed: ${{ steps.perm.outputs.allowed }}
permission: ${{ steps.perm.outputs.permission }}
steps:
- name: Check actor permission (admin only)
id: perm
env:
TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
REPO: ${{ github.repository }}
ACTOR: ${{ github.actor }}
run: |
set -euo pipefail
ALLOWED=false
PERMISSION=unknown
METHOD=""
# Hardcoded authorized users — always allowed
case "$ACTOR" in
jmiller|gitea-actions[bot])
ALLOWED=true
PERMISSION=admin
METHOD="hardcoded allowlist"
;;
*)
# Detect platform and check permissions via API
API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}"
RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \
"${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}')
PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown")
if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "maintain" ] || [ "$PERMISSION" = "owner" ]; then
ALLOWED=true
fi
METHOD="collaborator API"
;;
esac
echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT"
echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT"
{
echo "## Access Authorization"
echo ""
echo "| Field | Value |"
echo "|-------|-------|"
echo "| **Actor** | \`${ACTOR}\` |"
echo "| **Repository** | \`${REPO}\` |"
echo "| **Permission** | \`${PERMISSION}\` |"
echo "| **Method** | ${METHOD} |"
echo "| **Authorized** | ${ALLOWED} |"
echo ""
if [ "$ALLOWED" = "true" ]; then
echo "${ACTOR} authorized (${METHOD})"
else
echo "${ACTOR} is NOT authorized. Requires admin or maintain role."
fi
} >> "${GITHUB_STEP_SUMMARY}"
- name: Deny execution when not permitted
if: ${{ steps.perm.outputs.allowed != 'true' }}
run: |
set -euo pipefail
printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}"
exit 1
scripts_governance:
name: Scripts governance
needs: access_check
if: ${{ needs.access_check.outputs.allowed == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Scripts folder checks
env:
PROFILE_RAW: ${{ github.event.inputs.profile }}
run: |
set -euo pipefail
profile="${PROFILE_RAW:-all}"
case "${profile}" in
all|scripts|repo) ;;
*)
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
exit 1
;;
esac
if [ "${profile}" = 'repo' ]; then
{
printf '%s\n' '### Scripts governance'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' 'Status: SKIPPED'
printf '%s\n' 'Reason: profile excludes scripts governance'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
exit 0
fi
if [ ! -d "${SCRIPT_DIR}" ]; then
{
printf '%s\n' '### Scripts governance'
printf '%s\n' 'Status: OK (advisory)'
printf '%s\n' 'scripts/ directory not present. No scripts governance enforced.'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
exit 0
fi
if [ -n "${SCRIPTS_REQUIRED_DIRS:-}" ]; then IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"; else required_dirs=(); fi
IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}"
missing_dirs=()
unapproved_dirs=()
for d in "${required_dirs[@]}"; do
req="${d%/}"
[ ! -d "${req}" ] && missing_dirs+=("${req}/")
done
while IFS= read -r d; do
allowed=false
for a in "${allowed_dirs[@]}"; do
a_norm="${a%/}"
[ "${d%/}" = "${a_norm}" ] && allowed=true
done
[ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/")
done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##')
{
printf '%s\n' '### Scripts governance'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' '| Area | Status | Notes |'
printf '%s\n' '|---|---|---|'
if [ "${#missing_dirs[@]}" -gt 0 ]; then
printf '%s\n' '| Required directories | Warning | Missing required subfolders |'
else
printf '%s\n' '| Required directories | OK | All required subfolders present |'
fi
if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |'
else
printf '%s\n' '| Directory policy | OK | No unapproved directories |'
fi
printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |'
printf '\n'
if [ "${#missing_dirs[@]}" -gt 0 ]; then
printf '%s\n' 'Missing required script directories:'
for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done
printf '\n'
else
printf '%s\n' 'Missing required script directories: none.'
printf '\n'
fi
if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
printf '%s\n' 'Unapproved script directories detected:'
for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done
printf '\n'
else
printf '%s\n' 'Unapproved script directories detected: none.'
printf '\n'
fi
printf '%s\n' 'Scripts governance completed in advisory mode.'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
repo_health:
name: Repository health
needs: access_check
if: ${{ needs.access_check.outputs.allowed == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Repository health checks
env:
PROFILE_RAW: ${{ github.event.inputs.profile }}
run: |
set -euo pipefail
profile="${PROFILE_RAW:-all}"
case "${profile}" in
all|scripts|repo) ;;
*)
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
exit 1
;;
esac
if [ "${profile}" = 'scripts' ]; then
{
printf '%s\n' '### Repository health'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' 'Status: SKIPPED'
printf '%s\n' 'Reason: profile excludes repository health'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
exit 0
fi
IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}"
IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}"
if [ -n "${REPO_DISALLOWED_DIRS:-}" ]; then IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"; else disallowed_dirs=(); fi
IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES:-}"
missing_required=()
missing_optional=()
# Source directory: src/ or htdocs/ (either is valid for extension repos)
SOURCE_DIR=""
if [ -d "src" ]; then
SOURCE_DIR="src"
elif [ -d "htdocs" ]; then
SOURCE_DIR="htdocs"
elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then
# Platform/tooling repos don't need src/
SOURCE_DIR=""
else
missing_required+=("src/ or htdocs/ (source directory required)")
fi
for item in "${required_artifacts[@]}"; do
if printf '%s' "${item}" | grep -q '/$'; then
d="${item%/}"
[ ! -d "${d}" ] && missing_required+=("${item}")
else
[ ! -f "${item}" ] && missing_required+=("${item}")
fi
done
for f in "${optional_files[@]}"; do
if printf '%s' "${f}" | grep -q '/$'; then
d="${f%/}"
[ ! -d "${d}" ] && missing_optional+=("${f}")
else
[ ! -f "${f}" ] && missing_optional+=("${f}")
fi
done
for d in "${disallowed_dirs[@]}"; do
d_norm="${d%/}"
[ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)")
done
for f in "${disallowed_files[@]}"; do
[ -f "${f}" ] && missing_required+=("${f} (disallowed)")
done
git fetch origin --prune
dev_paths=()
dev_branches=()
while IFS= read -r b; do
name="${b#origin/}"
if [ "${name}" = 'dev' ]; then
dev_branches+=("${name}")
else
dev_paths+=("${name}")
fi
done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//')
if [ "${#dev_paths[@]}" -eq 0 ] && [ "${#dev_branches[@]}" -eq 0 ]; then
missing_required+=("dev or dev/* branch")
fi
content_warnings=()
if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then
content_warnings+=("CHANGELOG.md missing '# Changelog' header")
fi
if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then
content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)")
fi
if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then
content_warnings+=("LICENSE does not look like a GPL text")
fi
if [ -f 'README.md' ] && ! grep -qiE 'moko|Moko' README.md; then
content_warnings+=("README.md missing expected brand keyword")
fi
export PROFILE_RAW="${profile}"
export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")"
export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")"
export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")"
report_json=$(printf '{"profile":"%s","missing_required":%d,"missing_optional":%d,"content_warnings":%d}' "$profile" "${#missing_required[@]}" "${#missing_optional[@]}" "${#content_warnings[@]}")
{
printf '%s\n' '### Repository health'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' '| Metric | Value |'
printf '%s\n' '|---|---|'
printf '%s\n' "| Missing required | ${#missing_required[@]} |"
printf '%s\n' "| Missing optional | ${#missing_optional[@]} |"
printf '%s\n' "| Content warnings | ${#content_warnings[@]} |"
printf '\n'
printf '%s\n' '### Guardrails report (JSON)'
printf '%s\n' '```json'
printf '%s\n' "${report_json}"
printf '%s\n' '```'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
if [ "${#missing_required[@]}" -gt 0 ]; then
{
printf '%s\n' '### Missing required repo artifacts'
for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done
printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
exit 1
fi
if [ "${#missing_optional[@]}" -gt 0 ]; then
{
printf '%s\n' '### Missing optional repo artifacts'
for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
if [ "${#content_warnings[@]}" -gt 0 ]; then
{
printf '%s\n' '### Repo content warnings'
for m in "${content_warnings[@]}"; do printf '%s\n' "- ${m}"; done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
# -- Joomla-specific checks --
joomla_findings=()
MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)"
if [ -z "${MANIFEST}" ]; then
joomla_findings+=("Joomla XML manifest not found (no *.xml with <extension> tag)")
else
if ! grep -qP '<version>' "${MANIFEST}"; then
joomla_findings+=("XML manifest: <version> tag missing")
fi
if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then
joomla_findings+=("XML manifest: type attribute missing or invalid")
fi
if ! grep -qP '<name>' "${MANIFEST}"; then
joomla_findings+=("XML manifest: <name> tag missing")
fi
if ! grep -qP '<author>' "${MANIFEST}"; then
joomla_findings+=("XML manifest: <author> tag missing")
fi
if ! grep -qP '<namespace' "${MANIFEST}"; then
joomla_findings+=("XML manifest: <namespace> missing (required for Joomla 5+)")
fi
fi
INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)"
if [ "${INI_COUNT}" -eq 0 ]; then
joomla_findings+=("No .ini language files found")
fi
if [ ! -f 'updates.xml' ]; then
joomla_findings+=("updates.xml missing in root (required for Joomla update server)")
fi
if [ -n "${SOURCE_DIR}" ]; then
INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site")
for dir in "${INDEX_DIRS[@]}"; do
if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then
joomla_findings+=("${dir}/index.html missing (directory listing protection)")
fi
done
fi
if [ "${#joomla_findings[@]}" -gt 0 ]; then
{
printf '%s\n' '### Joomla extension checks'
printf '%s\n' '| Check | Status |'
printf '%s\n' '|---|---|'
for f in "${joomla_findings[@]}"; do
printf '%s\n' "| ${f} | Warning |"
done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
else
{
printf '%s\n' '### Joomla extension checks'
printf '%s\n' 'All Joomla-specific checks passed.'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
extended_enabled="${EXTENDED_CHECKS:-true}"
extended_findings=()
if [ "${extended_enabled}" = 'true' ]; then
if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then
:
else
extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)")
fi
if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then
bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)"
if [ -n "${bad_refs}" ]; then
extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt")
{
printf '%s\n' '### Workflow pinning advisory'
printf '%s\n' 'Found uses: entries pinned to main/master:'
printf '%s\n' '```'
printf '%s\n' "${bad_refs}"
printf '%s\n' '```'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
fi
if [ -f "${DOCS_INDEX}" ]; then
missing_links=""
while IFS= read -r docline; do
for link in $(echo "$docline" | grep -oE '\]\([^)]+\)' | sed 's/\](//' | sed 's/)$//' || true); do
case "$link" in http://*|https://*|"#"*|mailto:*) continue ;; esac
linkpath="${link%%#*}"
linkpath="${linkpath%%\?*}"
[ -z "$linkpath" ] && continue
if [ "${linkpath:0:1}" = "/" ]; then
testpath="${linkpath#/}"
else
testpath="$(dirname "${DOCS_INDEX}")/${linkpath}"
fi
[ ! -e "$testpath" ] && missing_links="${missing_links}${testpath} "
done
done < "${DOCS_INDEX}"
if [ -n "${missing_links}" ]; then
extended_findings+=("docs/docs-index.md contains broken relative links")
{
printf '%s\n' '### Docs index link integrity'
printf '%s\n' 'Broken relative links:'
for bl in ${missing_links}; do
printf '%s\n' "- ${bl}"
done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
fi
if [ -d "${SCRIPT_DIR}" ]; then
if ! command -v shellcheck >/dev/null 2>&1; then
sudo apt-get update -qq
sudo apt-get install -y shellcheck >/dev/null
fi
sc_out=''
while IFS= read -r shf; do
[ -z "${shf}" ] && continue
out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)"
if [ -n "${out_one}" ]; then
sc_out="${sc_out}${out_one}\n"
fi
done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort)
if [ -n "${sc_out}" ]; then
extended_findings+=("ShellCheck warnings detected (advisory)")
sc_head="$(printf '%s' "${sc_out}" | head -n 200)"
{
printf '%s\n' '### ShellCheck (advisory)'
printf '%s\n' '```'
printf '%s\n' "${sc_head}"
printf '%s\n' '```'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
fi
spdx_missing=()
IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}"
spdx_args=()
for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done
while IFS= read -r f; do
[ -z "${f}" ] && continue
if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then
spdx_missing+=("${f}")
fi
done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true)
if [ "${#spdx_missing[@]}" -gt 0 ]; then
extended_findings+=("SPDX header missing in some tracked files (advisory)")
{
printf '%s\n' '### SPDX header advisory'
printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):'
for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
stale_cutoff_days=180
stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 > days*86400) print $1}' | head -50)"
if [ -n "${stale_branches}" ]; then
extended_findings+=("Stale remote branches detected (advisory)")
{
printf '%s\n' '### Git hygiene advisory'
printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):"
while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}"
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
fi
{
printf '%s\n' '### Guardrails coverage matrix'
printf '%s\n' '| Domain | Status | Notes |'
printf '%s\n' '|---|---|---|'
printf '%s\n' '| Access control | OK | Admin-only execution gate |'
printf '%s\n' '| Release policy | N/A | Releases handled by MokoGitea |'
printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |'
printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |'
printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |'
if [ "${extended_enabled}" = 'true' ]; then
if [ "${#extended_findings[@]}" -gt 0 ]; then
printf '%s\n' '| Extended checks | Warning | See extended findings below |'
else
printf '%s\n' '| Extended checks | OK | No findings |'
fi
else
printf '%s\n' '| Extended checks | SKIPPED | EXTENDED_CHECKS disabled |'
fi
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
if [ "${extended_enabled}" = 'true' ] && [ "${#extended_findings[@]}" -gt 0 ]; then
{
printf '%s\n' '### Extended findings (advisory)'
for f in "${extended_findings[@]}"; do printf '%s\n' "- ${f}"; done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}"
site-health:
name: Site Health
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch'
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- name: Uptime check
if: env.URLS != ''
run: |
echo "$URLS" > /tmp/urls.txt
php monitoring/uptime-probe.php --urls /tmp/urls.txt --timeout 15 || echo "::warning::Some sites are down"
rm -f /tmp/urls.txt
env:
URLS: ${{ vars.MONITORED_URLS }}
- name: SSL certificate check
if: env.DOMAINS != ''
run: |
echo "$DOMAINS" > /tmp/domains.txt
php monitoring/ssl-check.php --domains /tmp/domains.txt --warn-days 30 || echo "::warning::SSL certificates expiring soon"
rm -f /tmp/domains.txt
env:
DOMAINS: ${{ vars.MONITORED_DOMAINS }}
- name: Summary
if: always()
run: |
echo "### Site Health" >> $GITHUB_STEP_SUMMARY
echo "Uptime and SSL checks completed." >> $GITHUB_STEP_SUMMARY
# ═══════════════════════════════════════════════════════════════════════
# Issue Reporter — file issues for failed gates
# ═══════════════════════════════════════════════════════════════════════
report-issues:
name: "Report Issues"
runs-on: ubuntu-latest
needs: [access_check, scripts_governance, repo_health]
if: >-
always() &&
(needs.scripts_governance.result == 'failure' ||
needs.repo_health.result == 'failure')
steps:
- name: Checkout
uses: actions/checkout@v4
with:
sparse-checkout: automation/ci-issue-reporter.sh
sparse-checkout-cone-mode: false
- name: "File issues for failed gates"
env:
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: |
chmod +x automation/ci-issue-reporter.sh
REPORTER="./automation/ci-issue-reporter.sh"
WF="Repo Health"
report_gate() {
local gate="$1" result="$2" details="$3"
if [ "$result" = "failure" ]; then
"$REPORTER" --gate "$gate" --details "$details" --workflow "$WF" --severity error
fi
}
report_gate "Scripts Governance" \
"${{ needs.scripts_governance.result }}" \
"Scripts directory policy violations detected. Review required and allowed directories."
report_gate "Repository Health" \
"${{ needs.repo_health.result }}" \
"Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary."
+95 -496
View File
@@ -1,72 +1,117 @@
# Changelog
This changelog goes through the changes that have been made in each release
without substantial changes to our git log; to see the highlights of what has
been added to each release, please refer to the [blog](https://blog.gitea.com).
## [v1.26.1-moko.06.02.00] - 2026-06-02
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`).
## [v1.26.1-moko.06.03] - 2026-06-04
* FEATURES
* feat(licenses): full commercial license management system
* Package archiving with soft-delete and collapsible archived section
* Search keys by customer, domain, key number, email, or payment ref
* Download gating (none/prerelease/all modes)
* Feed visibility (public/no-download/hidden modes)
* Domain lock grace period (DomainLockHours)
* Domain restriction on packages and keys (comma-separated allowed domains)
* RepoScope enforcement — packages scoped to specific repos
* Joomla changelog XML endpoint (/changelog.xml)
* WordPress PUC-compatible update feed (/updates/wordpress.json)
* SHA256 checksums from sidecar files in Joomla updates.xml
* Joomla-standard tag values (dev/alpha/beta/rc/stable)
* Configurable license key prefix per organization
* Master key auto-generates, sorts first in key list
* License package creation at repo level via modal
* Key generation modal with licensee name, email, and domain fields
* Manual release-to-stream mapping with UI selector
* Double confirmation modals for permanent deletion
* Combolist channel picker (replaces checkboxes)
* Extension metadata in repo settings (per-repo override)
* API: package CRUD, key revoke, key renew, settings GET/PUT
* API: purchase webhook with PaymentRef idempotency
* API: public validation endpoint (no auth)
* Migration v340: all new columns synced
* feat(updates): infourl defaults to release listing page
* feat(updates): downloadkey prefix matches Akeeba pattern (dlid=)
* fix(licenses): expanded delete permissions to org owners + site admins
* fix(licenses): no-download mode shows release notes but hides files
* fix(licenses): releases require login in hidden feed visibility mode
* Migration v340-v344: all new columns synced
* feat(updates): Update Server system (renamed from "Licensing")
* Joomla XML with SHA256, changelog URL, version from asset filename
* Dolibarr JSON with channel filtering
* WordPress PUC-compatible JSON (plugin-update-checker)
* Composer packages.json
* PrestaShop module update XML
* Drupal update status XML
* WHMCS module update JSON
* Feed always public — downloads gated separately
* Stream-name tags supported alongside version tags
* Omit `<client>` for package extension types
* `<downloadkey>` only when download_gating is prerelease or all
* Version extracted from asset filename (matches actual download)
* Joomla tag values verified: dev, alpha, beta, rc, stable
* feat(orgs): enterprise sub-org hierarchy with parent-child relationships
* feat(repos): three-level visibility — Public (200), Private (403), Hidden (404)
* feat(settings): Update Server settings page with enable toggle in Advanced Settings
* feat(settings): advanced settings on dedicated page with dividing headers
* feat(settings): icons on all settings navbars (repo, org, user, admin)
* feat(ui): styled 403 Access Denied page with inline login form
* feat(issues): custom fields with inline editing in issue sidebar
* feat(issues): pre-fill custom fields from issue template YAML frontmatter (#493)
* Templates specify `custom_fields:` map (field name → default value)
* New issue sidebar shows org-level fields with template defaults pre-selected
* API create issue accepts `custom_fields` map by name
* feat(updateserver): resolve extension metadata from org-level custom fields (#492)
* Cascading fallback: custom fields → config table → repo-derived defaults
* All six generators updated (Joomla, WordPress, Composer, Drupal, PrestaShop, WHMCS)
* Repos can be migrated to custom fields gradually
* feat(ui): two-in-one Update Server / Licenses tab
* No gating: shows "Update Server" tab with feed URLs only
* Gated: shows "Licenses" tab with full key management
* `<downloadkey>` only appears when downloads are gated
* SECURITY
* fix(security): ownership guards on all API handlers (cross-org prevention)
* fix(security): RepoScope JSON parsing (substring matching bug)
* fix(security): CSRF tokens in delete confirmation modals
* fix(security): XSS escaping in WordPress changelog HTML
* fix(security): require login for licenses and actions pages
* fix(security): 403 for all users on private repos (not 404)
* fix(security): licensed private repos allow release viewing for signed-in users
* fix(security): anonymous download access respects download_gating setting
* FIXES
* fix(licenses): explicit xorm column names for UpdateStreamConfig fields
* fix(licenses): feed always public when licensing enabled
* fix(settings): prevent double-highlight on Advanced Settings nav item
* fix(settings): redirect back to /settings/advanced after save
* fix(build): remove stale custom field API routes and dead code
* fix(build): replace invalid UTF-8 character in API comment
* fix(build): permanent fixes for AI migration, feed/file.go, unused imports
* fix(updateserver): version extracted from asset filename (not release title)
* fix(updateserver): omit `<client>` for package types per Joomla spec
* fix(updateserver): `<downloadkey>` only shown when downloads are gated
* fix(updateserver): prevent stream name tag from overriding asset-derived version
* fix(build): restore build/ directory after accidental deletion
* fix(licenses): master key banner removed, master keys sort first in table
* fix(issues): issue sidebar loads org-level fields instead of legacy repo-level fields
## [v1.26.1-moko.05.15.00] - 2026-05-31
## [v1.26.1-moko.05] - 2026-05-31
* BREAKING CHANGES
* Deprecated Issue.Ref branch selector UI (#307)
* Removed branch/tag selector from issue sidebar and new issue form
* Removed ref badge from issue lists
* Removed POST /ref web route and UpdateIssueRef handler
* DB column and commit-close logic preserved for backward compatibility
* API create/edit still accept `ref` field (no-op) for backward compat
* FEATURES
* feat(ui): add generic combo-multiselect component (#361)
* feat(ui): generic combo-multiselect component (#361)
* Reusable dropdown with search, checkable items, and selected-items display
* Template: `shared/combolist.tmpl` — accepts Items, Name, Title, SelectedValues
* Decoupled from issue sidebar — works in any form context
* Template: `shared/combolist.tmpl`
* feat(updates): extension metadata settings for update feed generation
* feat(licenses): platform enforcement, key deletion, expired key cleanup
* feat(licenses): store keys in plaintext, show full key with copy button
* feat(actions): rebrand actions bot user to mokogitea-actions (#233, #234)
* Backward-compatible: recognizes github-actions[bot], gitea-actions[bot]
* feat(actions): actions bot user in branch protection whitelist (#233, #234)
* WhitelistActionsUser, MergeWhitelistActionsUser, ForcePushAllowlistActionsUser
* TECH DEBT
* chore: full namespace migration from git.mokoconsulting.tech to code.mokoconsulting.tech (#336, #337, #344)
* Go module path, all imports, template URLs, workflow configs (2,276 files)
* chore: full namespace migration to code.mokoconsulting.tech (#336, #337, #344)
* fix(blame): set HasSourceRenderedToggle for renderable files (#344)
* fix(settings): translate team permission strings via data-locale attributes (#344)
* fix(settings): translate team permission strings via data-locale (#344)
* fix(dropzone): use relative path for non-image attachment markdown links (#344)
* fix(templates): add required validation to issue dropdown fields (#350)
* refactor(ts): remove redundant `handled` field from MarkdownHandleIndentionResult (#350)
* refactor(go): rename HasOrgOrUserVisible to IsOwnerVisibleToDoer (#350)
* refactor(go): replace ValuesRepository with maps.Values (Go 1.21+) (#357)
* refactor(go): remove CanEnableEditor wrapper, use CanContentChange directly (#357)
* fix(ts): parseIssueHref now uses URL pathname and trims appSubUrl (#360)
* fix(actions): enforce MaxJobNumPerRun (256) limit when creating jobs (#360)
* refactor(go): remove CanEnableEditor wrapper (#357)
* fix(ts): parseIssueHref uses URL pathname and trims appSubUrl (#360)
* fix(actions): enforce MaxJobNumPerRun (256) limit (#360)
* fix(css): use calc(infinity * 1px) for --border-radius-full (#361)
* fix(css): remove legacy .center class from 2015, replace with tw-text-center (#361)
* chore: remove stale TODO from OAuth2 regenerate secret (already implemented) (#332)
* chore: remove stale pull request test stub TODOs (#328)
* chore: remove stale GetProjectsMode TODO
* chore: remove stale mustNotBeArchived/mustEnableEditor FIXME from API
* fix(routes): remove dead legacy /cherry-pick/{sha} route (replaced by /_cherrypick/)
* fix(css): remove legacy .center class, replace with tw-text-center (#361)
* fix(routes): remove dead legacy /cherry-pick/{sha} route
* fix(feed): use full ref name instead of ShortName for file feed revision
* BUGFIXES
* fix(build): use slices.Collect for maps.Values (Go 1.23+ compat)
@@ -74,25 +119,10 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
* fix(licenses): only show licenses tab when licensing is enabled
* fix(licenses): show feed URLs based on repo update platform setting
* fix(updates): correct dlid prefix and align XML with Joomla standard
## [v1.26.1-moko.05.06.00] - 2026-05-30
* FEATURES
* feat(actions): rebrand actions bot user to mokogitea-actions (#233, #234)
* Name: gitea-actions → mokogitea-actions, FullName: MokoGitea Actions
* Email: mokogitea-actions[bot]@mokoconsulting.tech
* Backward-compatible: recognizes github-actions[bot], gitea-actions[bot], mokogitea-actions[bot]
* feat(actions): add actions bot user to branch protection whitelist (#233, #234)
* New toggles: WhitelistActionsUser, MergeWhitelistActionsUser, ForcePushAllowlistActionsUser
* Allows CI/CD workflows to push/merge/force-push to protected branches when enabled
* DB migration v334 adds the three boolean columns
* Exposed in API (create/edit branch protection) and web UI settings
* INFRASTRUCTURE
* fix(ci): auto-deploy to production on merge to main (#235)
* Deploy workflow now triggers on push to main, not just manual dispatch
* Version derived from git describe for auto-deploys
## [v1.26.1-moko.04.00.00] - 2026-05-24
## [v1.26.1-moko.04] - 2026-05-24
* SECURITY
* Backport 12 upstream v1.26.2 security fixes:
@@ -101,468 +131,37 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
* 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)
* Reading permission fix (#145)
* 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 — CSS class mismatch (#138)
* fix: scheduled action panic with null event payload (upstream #37459)
* fix: treat email addresses case-insensitively (upstream #37600)
* 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
* fix: remove unused setting import in action.go
* fix: restore Permission field access in context middleware
* FEATURES
* Joomla-style updates.xml with channel selection (stable/dev/security/rc)
* Update checker reads from updates.xml with configurable CHANNEL setting
* Admin dashboard shows update banner with channel name and docker pull command
* Upstream bug sync workflow — daily automated issue creation from release/v1.26
* 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 to MokoGitea
* 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
* Deduplicated 19 duplicate feature request issues
* Closed 24 upstream bug/security issues after backporting
## [MokoGitea Unreleased]
## [v1.26.1-moko.03] - 2026-05-15
* FEATURES
* feat(api): Bulk issue operations — add/remove/replace labels, close/reopen, set milestone, and set assignees across multiple issues in a single request (#21)
* `POST /api/v1/repos/{owner}/{repo}/issues/bulk/labels`
* `POST /api/v1/repos/{owner}/{repo}/issues/bulk/state`
* `POST /api/v1/repos/{owner}/{repo}/issues/bulk/milestone`
* `POST /api/v1/repos/{owner}/{repo}/issues/bulk/assignees`
* Partial-failure support: returns per-issue success/failure map
* feat(api): Bulk issue operations — add/remove/replace labels, close/reopen, set milestone, assignees (#21)
* INFRASTRUCTURE
* Grafana: Standardized kiosk header across all 14 playlist dashboards — each now shows dashboard name, kiosk link, terminal/exit/switch instructions
* Grafana: Standardized kiosk header across all 14 playlist dashboards
* PROCESS
* Reopened 9 closed issues lacking documented testing proof (#3, #5, #38, #41, #70, #74, #75, #76, #78)
* Reopened 9 closed issues lacking documented testing proof
* Created `pending: testing` label for features awaiting verification
* Established policy: issues must not be closed without documented testing proof
## [1.26.1](https://github.com/go-gitea/gitea/releases/tag/v1.26.1) - 2026-04-21
* BUGFIXES
* Add event.schedule context for schedule actions task (#37320) (#37348)
* Fix an issue where changing an organization's visibility caused problems when users had forked its repositories. (#37324) (#37344)
* Use modern "git update-index --cacheinfo" syntax to support more file names (#37338) (#37343)
* Fix URL related escaping for oauth2 (#37334) (#37340)
* When the requested arch rpm is missing fall back to noarch (#37236) (#37339)
* Fix actions concurrency groups cross-branch leak (#37311) (#37331)
* Fix bug when accessing user badges (#37321) (#37329)
* Fix AppFullLink (#37325) (#37328)
* Fix container auth for public instance (#37290) (#37294)
* Enhance GetActionWorkflow to support fallback references (#37189) (#37283)
* Fix vite manifest update masking build errors (#37279) (#37310)
* Fix Mermaid diagrams failing when node labels contain line breaks (#37296) (#37299)
* Use TriggerEvent instead of Event in workflow runs API response for scheduled runs (#37288) #37360
* Add URL to Learn more about blocking a user. (#37355) #37367
* Fix button layout shift when collapsing file tree in editor (#37363) #37375
* Fix org team assignee/reviewer lookups for team member permissions (#37365) #37391
* Fix repo init README EOL (#37388) #37399
* Fix: dump with default zip type produces uncompressed zip (#37401)#37402
## [1.26.0](https://github.com/go-gitea/gitea/releases/tag/v1.26.0) - 2026-04-17
* BREAKING
* Correct swagger annotations for enums, status codes, and notification state (#37030)
* Remove GET API registration-token (#36801)
* Support Actions `concurrency` syntax (#32751)
* Make PUBLIC_URL_DETECTION default to "auto" (#36955)
* SECURITY
* Bound PageSize in `ListUnadoptedRepositories` (#36884)
* FEATURES
* Support Actions `concurrency` syntax (#32751)
* Add terraform state registry (#36710)
* Instance-wide (global) info banner and maintenance mode (#36571)
* Support rendering OpenAPI spec (#36449)
* Add keyboard shortcuts for repository file and code search (#36416)
* Add support for archive-upload rpc (#36391)
* Add ability to download subpath archive (#36371)
* Add workflow dependencies visualization (#26062) (#36248) & Restyle Workflow Graph (#36912)
* Automatic generation of release notes (#35977)
* Add "Go to file", "Delete Directory" to repo file list page (#35911)
* Introduce "config edit-ini" sub command to help maintaining INI config file (#35735)
* Add button to re-run failed jobs in Actions (#36924)
* Support actions and reusable workflows from private repos (#32562)
* Add summary to action runs view (#36883)
* Add user badges (#36752)
* Add configurable permissions for Actions automatic tokens (#36173)
* Add per-runner "Disable/Pause" (#36776)
* Feature non-zipped actions artifacts (action v7 / nodejs / npm v6.2.0) (#36786)
* PERFORMANCE
* WorkflowDispatch API optionally return runid (#36706)
* Add render cache for SVG icons (#36863)
* Load `mentionValues` asynchronously (#36739)
* Lazy-load some Vue components, fix heatmap chunk loading on every page (#36719)
* Load heatmap data asynchronously (#36622)
* Use prev/next pagination for user profile activities page to speed up (#36642)
* Refactor cat-file batch operations and support `--batch-command` approach (#35775)
* Use merge tree to detect conflicts when possible (#36400)
* ENHANCEMENTS
* Implement logout redirection for reverse proxy auth setups (#36085) (#37171)
* Adds option to force update new branch in contents routes (#35592)
* Add viewer controller for mermaid (zoom, drag) (#36557)
* Add code editor setting dropdowns (#36534)
* Add `elk` layout support to mermaid (#36486)
* Add resolve/unresolve review comment API endpoints (#36441)
* Allow configuring default PR base branch (fixes #36412) (#36425)
* Add support for RPM Errata (updateinfo.xml) (#37125)
* Require additional user confirmation for making repo private (#36959)
* Add `actions.WORKFLOW_DIRS` setting (#36619)
* Avoid opening new tab when downloading actions logs (#36740)
* Implements OIDC RP-Initiated Logout (#36724)
* Show workflow link (#37070)
* Desaturate dark theme background colors (#37056)
* Refactor "org teams" page and help new users to "add member" to an org (#37051)
* Add webhook name field to improve webhook identification (#37025) (#37040)
* Make task list checkboxes clickable in the preview tab (#37010)
* Improve severity labels in Actions logs and tweak colors (#36993)
* Linkify URLs in Actions workflow logs (#36986)
* Allow text selection on checkbox labels (#36970)
* Support dark/light theme images in markdown (#36922)
* Enable native dark mode for swagger-ui (#36899)
* Rework checkbox styling, remove `input` border hover effect (#36870)
* Refactor storage content-type handling of ServeDirectURL (#36804)
* Use "Enable Gravatar" but not "Disable" (#36771)
* Use case-insensitive matching for Git error "Not a valid object name" (#36728)
* Add "Copy Source" to markup comment menu (#36726)
* Change image transparency grid to CSS (#36711)
* Add "Run" prefix for unnamed action steps (#36624)
* Persist actions log time display settings in `localStorage` (#36623)
* Use first commit title for multi-commit PRs and fix auto-focus title field (#36606)
* Improve BuildCaseInsensitiveLike with lowercase (#36598)
* Improve diff highlighting (#36583)
* Exclude cancelled runs from failure-only email notifications (#36569)
* Use full-file highlighting for diff sections (#36561)
* Color command/error logs in Actions log (#36538)
* Add paging headers (#36521)
* Improve timeline entries for WIP prefix changes in pull requests (#36518)
* Add FOLDER_ICON_THEME configuration option (#36496)
* Normalize guessed languages for code highlighting (#36450)
* Add chunked transfer encoding support for LFS uploads (#36380)
* Indicate when only optional checks failed (#36367)
* Add 'allow_maintainer_edit' API option for creating a pull request (#36283)
* Support closing keywords with URL references (#36221)
* Improve diff file headers (#36215)
* Fix and enhance comment editor monospace toggle (#36181)
* Add git.DIFF_RENAME_SIMILARITY_THRESHOLD option (#36164)
* Add matching pair insertion to markdown textarea (#36121)
* Add sorting/filtering to admin user search API endpoint (#36112)
* Allow action user have read permission in public repo like other user (#36095)
* Disable matchBrackets in monaco (#36089)
* Use GitHub-style commit message for squash merge (#35987)
* Make composer registry support tar.gz and tar.bz2 and fix bugs (#35958)
* Add GITEA_PR_INDEX env variable to githooks (#35938)
* Add proper error message if session provider can not be created (#35520)
* Add button to copy file name in PR files (#35509)
* Move `X_FRAME_OPTIONS` setting from `cors` to `security` section (#30256)
* Add placeholder content for empty content page (#37114)
* Add `DEFAULT_DELETE_BRANCH_AFTER_MERGE` setting (#36917)
* Redirect to the only OAuth2 provider when no other login methods and fix various problems (#36901)
* Add admin badge to navbar avatar (#36790)
* Add `never` option to `PUBLIC_URL_DETECTION` configuration (#36785)
* Add background and run count to actions list page (#36707)
* Add icon to buttons "Close with Comment", "Close Pull Request", "Close Issue" (#36654)
* Add support for in_progress event in workflow_run webhook (#36979)
* Report commit status for pull_request_review events (#36589)
* Render merged pull request title as such in dashboard feed (#36479)
* Feature to be able to filter project boards by milestones (#36321)
* Use user id in noreply emails (#36550)
* Enable pagination on GiteaDownloader.getIssueReactions() (#36549)
* Remove striped tables in UI (#36509)
* Improve control char rendering and escape button styling (#37094)
* Support legacy run/job index-based URLs and refactor migration 326 (#37008)
* Add date to "No Contributions" tooltip (#36190)
* Show edit page confirmation dialog on tree view file change (#36130)
* Mention proc-receive in text for dashboard.resync_all_hooks func (#35991)
* Reuse selectable style for wiki (#35990)
* Support blue yellow colorblind theme (#35910)
* Support selecting theme on the footer (#35741)
* Improve online runner check (#35722)
* Add quick approve button on PR page (#35678)
* Enable commenting on expanded lines in PR diffs (#35662)
* Print PR-Title into tooltip for actions (#35579)
* Use explicit, stronger defaults for newly generated repo signing keys for Debian (#36236)
* Improve the compare page (#36261)
* Unify repo names in system notices (#36491)
* Move package settings to package instead of being tied to version (#37026)
* Add Actions API rerun endpoints for runs and jobs (#36768)
* Add branch_count to repository API (#35351) (#36743)
* Add created_by filter to SearchIssues (#36670)
* Allow admins to rename non-local users (#35970)
* Support updating branch via API (#35951)
* Add an option to automatically verify SSH keys from LDAP (#35927)
* Make "update file" API can create a new file when SHA is not set (#35738)
* Update issue.go with labels documentation (labels content, not ids) (#35522)
* Expose content_version for optimistic locking on issue and PR edits (#37035)
* Pass ServeHeaderOptions by value instead of pointer, fine tune httplib tests (#36982)
* BUGFIXES
* Frontend iframe renderer framework: 3D models, OpenAPI (#37233) (#37273)
* Fix CODEOWNERS absolute path matching. (#37244) (#37264)
* Swift registry metadata: preserve more JSON fields and accept empty metadata (#37254) (#37261)
* Fix user ssh key exporting and tests (#37256) (#37258)
* Fix team member avatar size and add tooltip (#37253)
* Fix commit title rendering in action run and blame (#37243) (#37251)
* Fix corrupted JSON caused by goccy library (#37214) (#37220)
* Add test for "fetch redirect", add CSS value validation for external render (#37207) (#37216)
* Fix incorrect concurrency check (#37205) (#37215)
* Fix handle missing base branch in PR commits API (#37193) (#37203)
* Fix encoding for Matrix Webhooks (#37190) (#37201)
* Fix handle fork-only commits in compare API (#37185) (#37199)
* Indicate form field readonly via background, fix RunUser config (#37175, #37180) (#37178)
* Report structurally invalid workflows to users (#37116) (#37164)
* Fix API not persisting pull request unit config when has_pull_requests is not set (#36718)
* Rename CSS variables and improve colorblind themes (#36353)
* Hide `add-matcher` and `remove-matcher` from actions job logs (#36520)
* Prevent navigation keys from triggering actions during IME composition (#36540)
* Fix vertical alignment of `.commit-sign-badge` children (#36570)
* Fix duplicate startup warnings in admin panel (#36641)
* Fix CODEOWNERS review request attribution using comment metadata (#36348)
* Fix HTML tags appearing in wiki table of contents (#36284)
* Fix various bugs (#37096)
* Fix various legacy problems (#37092)
* Fix RPM Registry 404 when package name contains 'package' (#37087)
* Merge some standalone Vite entries into index.js (#37085)
* Fix various problems (#37077)
* Fix issue label deletion with Actions tokens (#37013)
* Hide delete branch or tag buttons in mirror or archived repositories. (#37006)
* Fix org contact email not clearable once set (#36975)
* Fix a bug when forking a repository in an organization (#36950)
* Preserve sort order of exclusive labels from template repo (#36931)
* Make container registry support Apple Container (basic auth) (#36920)
* Fix the wrong push commits in the pull request when force push (#36914)
* Add class "list-header-filters" to the div for projects (#36889)
* Fix dbfs error handling (#36844)
* Fix incorrect viewed files counter if reverted change was viewed (#36819)
* Refactor avatar package, support default avatar fallback (#36788)
* Fix README symlink resolution in subdirectories like .github (#36775)
* Fix CSS stacking context issue in actions log (#36749)
* Add gpg signing for merge rebase and update by rebase (#36701)
* Delete non-exist branch should return 404 (#36694)
* Fix `TestActionsCollaborativeOwner` (#36657)
* Fix multi-arch Docker build SIGILL by splitting frontend stage (#36646)
* Fix linguist-detectable attribute being ignored for configuration files (#36640)
* Fix state desync in ComboMarkdownEditor (#36625)
* Unify DEFAULT_SHOW_FULL_NAME output in templates and dropdown (#36597)
* Pull Request Pusher should be the author of the merge (#36581)
* Fix various version parsing problems (#36553)
* Fix highlight diff result (#36539)
* Fix mirror sync parser and fix mirror messages (#36504)
* Fix bug when list pull request commits (#36485)
* Fix various bugs (#36446)
* Fix issue filter menu layout (#36426)
* Restrict branch naming when new change matches with protection rules (#36405)
* Fix link/origin referrer and login redirect (#36279)
* Generate IDs for HTML headings without id attribute (#36233)
* Use a migration test instead of a wrong test which populated the meta test repositories and fix a migration bug (#36160)
* Fix issue close timeline icon (#36138)
* Fix diff blob excerpt expansion (#35922)
* Fix external render (#35727)
* Fix review request webhook bug (#35339) (#35723)
* Fix shutdown waitgroup panic (#35676)
* Cleanup ActionRun creation (#35624)
* Fix possible bug when migrating issues/pull requests (#33487)
* Various fixes (#36697)
* Apply notify/register mail flags during install load (#37120)
* Repair duration display for bad stopped timestamps (#37121)
* Fix(upgrade.sh): use HTTPS for GPG key import and restore SELinux context after upgrade (#36930)
* Fix various trivial problems (#36921)
* Fix various trivial problems (#36953)
* Fix NuGet package upload error handling (#37074)
* Fix CodeQL code scanning alerts (#36858)
* Refactor issue sidebar and fix various problems (#37045)
* Fix various problems (#37029)
* Fix relative-time RangeError (#37021)
* Fix chroma lexer mapping (#36629)
* Fix typos and grammar in English locale (#36751)
* Fix milestone/project text overflow in issue sidebar (#36741)
* Fix `no-content` message not rendering after comment edit (#36733)
* Fix theme loading in development (#36605)
* Fix workflow run jobs API returning null steps (#36603)
* Fix timeline event layout overflow with long content (#36595)
* Fix minor UI issues in runner edit page (#36590)
* Fix incorrect vendored detections (#36508)
* Fix editorconfig not respected in PR Conversation view (#36492)
* Don't create self-references in merged PRs (#36490)
* Fix potential incorrect runID in run status update (#36437)
* Fix file-tree ui error when adding files to repo without commits (#36312)
* Improve image captcha contrast for dark mode (#36265)
* Fix panic in blame view when a file has only a single commit (#36230)
* Fix spelling error in migrate-storage cmd utility (#36226)
* Fix code highlighting on blame page (#36157)
* Fix nilnil in onedev downloader (#36154)
* Fix actions lint (#36029)
* Fix oauth2 session gob register (#36017)
* Fix Arch repo pacman.conf snippet (#35825)
* Fix a number of `strictNullChecks`-related issues (#35795)
* Fix URLJoin, markup render link reoslving, sign-in/up/linkaccount page common data (#36861)
* Hide delete directory button for mirror or archive repository and disable the menu item if user have no permission (#36384)
* Update message severity colors, fix navbar double border (#37019)
* Inline and lazy-load EasyMDE CSS, fix border colors (#36714)
* Closed milestones with no issues now show as 100% completed (#36220)
* Add test for ExtendCommentTreePathLength migration and fix bugs (#35791)
* Only turn links to current instance into hash links (#36237)
* Fix typos in code comments: doesnt, dont, wont (#36890)
* REFACTOR
* Clean up and improve non-gitea js error filter (#37148) (#37155)
* Always show owner/repo name in compare page dropdowns (#37172) (#37200)
* Remove dead CSS rules (#37173) (#37177)
* Replace Monaco with CodeMirror (#36764)
* Replace CSRF cookie with `CrossOriginProtection` (#36183)
* Replace index with id in actions routes (#36842)
* Remove unnecessary function parameter (#35765)
* Move jobparser from act repository to Gitea (#36699)
* Refactor compare router param parse (#36105)
* Optimize 'refreshAccesses' to perform update without removing then adding (#35702)
* Clean up checkbox cursor styles (#37016)
* Remove undocumented support of signing key in the repository git configuration file (#36143)
* Switch `cmd/` to use constructor functions. (#36962)
* Use `relative-time` to render absolute dates (#36238)
* Some refactors about GetMergeBase (#36186)
* Some small refactors (#36163)
* Use gitRepo as parameter instead of repopath when invoking sign functions (#36162)
* Move blame to gitrepo (#36161)
* Move some functions to gitrepo package to reduce RepoPath reference directly (#36126)
* Use gitrepo's clone and push when possible (#36093)
* Remove mermaid margin workaround (#35732)
* Move some functions to gitrepo package (#35543)
* Move GetDiverging functions to gitrepo (#35524)
* Use global lock instead of status pool for cron lock (#35507)
* Use explicit mux instead of DefaultServeMux (#36276)
* Use gitrepo's push function (#36245)
* Pass request context to generateAdditionalHeadersForIssue (#36274)
* Move assign project when creating pull request to the same database transaction (#36244)
* Move catfile batch to a sub package of git module (#36232)
* Use gitrepo.Repository instead of wikipath (#35398)
* Use experimental go json v2 library (#35392)
* Refactor template render (#36438)
* Refactor GetRepoRawDiffForFile to avoid unnecessary pipe or goroutine (#36434)
* Refactor text utility classes to Tailwind CSS (#36703)
* Refactor git command stdio pipe (#36422)
* Refactor git command context & pipeline (#36406)
* Refactor git command stdio pipe (#36393)
* Remove unused functions (#36672)
* Refactor Actions Token Access (#35688)
* Move commit related functions to gitrepo package (#35600)
* Move archive function to repo_model and gitrepo (#35514)
* Move some functions to gitrepo package (#35503)
* Use git model to detect whether branch exist instead of gitrepo method (#35459)
* Some refactor for repo path (#36251)
* Extract helper functions from SearchIssues (#36158)
* Refactor merge conan and container auth preserve actions taskID (#36560)
* Refactor Nuget Auth to reuse Basic Auth Token Validation (#36558)
* Refactor ActionsTaskID (#36503)
* Refactor auth middleware (#36848)
* Refactor code render and render control chars (#37078)
* Clean up AppURL, remove legacy origin-url webcomponent (#37090)
* Remove `util.URLJoin` and replace all callers with direct path concatenation (#36867)
* Replace legacy tw-flex utility classes with flex-text-block/inline (#36778)
* Mark unused&immature activitypub as "not implemented" (#36789)
* TESTING
* Add e2e tests for server push events (#36879)
* Rework e2e tests (#36634)
* Add e2e reaction test, improve accessibility, enable parallel testing (#37081)
* Increase e2e test timeouts on CI to fix flaky tests (#37053)
* BUILD
* Upgrade go-git to v5.18.0 (#37269)
* Replace rollup-plugin-license with rolldown-license-plugin (#37130) (#37158)
* Bump min go version to 1.26.2 (#37139) (#37143)
* Convert locale files from ini to json format (#35489)
* Bump golangci-lint to 2.7.2, enable modernize stringsbuilder (#36180)
* Port away from `flake-utils` (#35675)
* Remove nolint (#36252)
* Update the Unlicense copy to latest version (#36636)
* Update to go 1.26.0 and golangci-lint 2.9.0 (#36588)
* Replace `google/go-licenses` with custom generation (#36575)
* Update go dependencies (#36548)
* Bump appleboy/git-push-action from 1.0.0 to 1.2.0 (#36306)
* Remove fomantic form module (#36222)
* Bump setup-node to v6, re-enable cache (#36207)
* Bump crowdin/github-action from 1 to 2 (#36204)
* Revert "Bump alpine to 3.23 (#36185)" (#36202)
* Update chroma to v2.21.1 (#36201)
* Bump astral-sh/setup-uv from 6 to 7 (#36198)
* Bump docker/build-push-action from 5 to 6 (#36197)
* Bump aws-actions/configure-aws-credentials from 4 to 5 (#36196)
* Bump dev-hanz-ops/install-gh-cli-action from 0.1.0 to 0.2.1 (#36195)
* Add JSON linting (#36192)
* Enable dependabot for actions (#36191)
* Bump alpine to 3.23 (#36185)
* Update chroma to v2.21.0 (#36171)
* Update JS deps and eslint enhancements (#36147)
* Update JS deps (#36091)
* update golangci-lint to v2.7.0 (#36079)
* Update JS deps, fix deprecations (#36040)
* Update JS deps (#35978)
* Add toolchain directive to go.mod (#35901)
* Move `gitea-vet` to use `go tool` (#35878)
* Update to go 1.25.4 (#35877)
* Enable TypeScript `strictNullChecks` (#35843)
* Enable `vue/require-typed-ref` eslint rule (#35764)
* Update JS dependencies (#35759)
* Move `codeformat` folder to tools (#35758)
* Update dependencies (#35733)
* Bump happy-dom from 20.0.0 to 20.0.2 (#35677)
* Bump setup-go to v6 (#35660)
* Update JS deps, misc tweaks (#35643)
* Bump happy-dom from 19.0.2 to 20.0.0 (#35625)
* Use bundled version of spectral (#35573)
* Update JS and PY deps (#35565)
* Bump github.com/wneessen/go-mail from 0.6.2 to 0.7.1 (#35557)
* Migrate from webpack to vite (#37002)
* Update JS dependencies and misc tweaks (#37064)
* Update to eslint 10 (#36925)
* Optimize Docker build with dependency layer caching (#36864)
* Update JS deps (#36850)
* Update tool dependencies and fix new lint issues (#36702)
* Remove redundant linter rules (#36658)
* Move Fomantic dropdown CSS to custom module (#36530)
* Remove and forbid `@ts-expect-error` (#36513)
* Refactor git command stderr handling (#36402)
* Enable gocheckcompilerdirectives linter (#36156)
* Replace `lint-go-gopls` with additional `govet` linters (#36028)
* Update golangci-lint to v2.6.0 (#35801)
* Misc tool tweaks (#35734)
* Add cache to container build (#35697)
* Upgrade vite (#37126)
* Update `setup-uv` to v8.0.0 (#37101)
* Upgrade `go-git` to v5.17.2 and related dependencies (#37060)
* Raise minimum Node.js version to 22.18.0 (#37058)
* Upgrade `golang.org/x/image` to v0.38.0 (#37054)
* Update minimum go version to 1.26.1, golangci-lint to 2.11.2, fix test style (#36876)
* Enable eslint concurrency (#36878)
* Vendor relative-time-element as local web component (#36853)
* Update material-icon-theme v5.32.0 (#36832)
* Update Go dependencies (#36781)
* Upgrade minimatch (#36760)
* Remove i18n backport tool at the moment because of translation format changed (#36643)
* Update emoji data for Unicode 16 (#36596)
* Update JS dependencies, adjust webpack config, misc fixes (#36431)
* Update material-icon-theme to v5.31.0 (#36427)
* Update JS and PY deps (#36383)
* Bump alpine to 3.23, add platforms to `docker-dryrun` (#36379)
* Update JS deps (#36354)
* Update goldmark to v1.7.16 (#36343)
* Update chroma to v2.22.0 (#36342)
* DOCS
* Update AI Contribution Policy (#37022)
* Update AGENTS.md with additional guidelines (#37018)
* Add missing cron tasks to example ini (#37012)
* Add AI Contribution Policy to CONTRIBUTING.md (#36651)
* Minor punctuation improvement in CONTRIBUTING.md (#36291)
* Add documentation for markdown anchor post-processing (#36443)
* MISC
* Correct spelling (#36783)
* Update Nix flake (#37110)
* Update Nix flake (#37024)
* Add valid github scopes (#36977)
* Update Nix flake (#36943)
* Update Nix flake (#36902)
* Update Nix flake (#36857)
* Update Nix flake (#36787)
+237
View File
@@ -0,0 +1,237 @@
#!/usr/bin/env bash
# ============================================================================
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Automation.CI
# INGROUP: moko-platform.Automation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /automation/ci-issue-reporter.sh
# VERSION: 09.23.00
# BRIEF: Creates or updates a Gitea issue when a CI gate fails.
# Deduplicates by searching open issues with the "ci-auto" label
# whose title matches the gate. If a matching issue exists, a comment
# is appended instead of opening a duplicate.
# ============================================================================
set -euo pipefail
# ── Defaults ────────────────────────────────────────────────────────────────
GITEA_URL="${GITEA_URL:-https://git.mokoconsulting.tech}"
GITEA_TOKEN="${GITEA_TOKEN:-}"
REPO="${GITHUB_REPOSITORY:-}"
RUN_URL="${GITHUB_SERVER_URL:-${GITEA_URL}}/${REPO}/actions/runs/${GITHUB_RUN_ID:-0}"
LABEL_NAME="ci-auto"
LABEL_COLOR="#e11d48"
GATE=""
DETAILS=""
SEVERITY="error"
WORKFLOW=""
# ── Parse arguments ─────────────────────────────────────────────────────────
usage() {
cat <<EOF
Usage: ci-issue-reporter.sh --gate NAME --details TEXT [OPTIONS]
Required:
--gate CI gate name (e.g. "Code Quality", "Self-Health")
--details Human-readable failure description
Optional:
--severity "error" (default) or "warning"
--workflow Workflow name for the issue title
--repo owner/repo (default: \$GITHUB_REPOSITORY)
--run-url URL to the CI run (auto-detected from env)
--token Gitea API token (default: \$GITEA_TOKEN)
--url Gitea base URL (default: \$GITEA_URL)
EOF
exit 1
}
while [[ $# -gt 0 ]]; do
case "$1" in
--gate) GATE="$2"; shift 2 ;;
--details) DETAILS="$2"; shift 2 ;;
--severity) SEVERITY="$2"; shift 2 ;;
--workflow) WORKFLOW="$2"; shift 2 ;;
--repo) REPO="$2"; shift 2 ;;
--run-url) RUN_URL="$2"; shift 2 ;;
--token) GITEA_TOKEN="$2"; shift 2 ;;
--url) GITEA_URL="$2"; shift 2 ;;
-h|--help) usage ;;
*) echo "Unknown option: $1"; usage ;;
esac
done
[[ -z "$GATE" ]] && { echo "ERROR: --gate is required"; usage; }
[[ -z "$DETAILS" ]] && { echo "ERROR: --details is required"; usage; }
[[ -z "$GITEA_TOKEN" ]] && { echo "ERROR: GITEA_TOKEN not set"; exit 1; }
[[ -z "$REPO" ]] && { echo "ERROR: GITHUB_REPOSITORY not set"; exit 1; }
API="${GITEA_URL}/api/v1/repos/${REPO}"
# ── Build title ─────────────────────────────────────────────────────────────
if [[ -n "$WORKFLOW" ]]; then
TITLE="[CI] ${WORKFLOW}: ${GATE} failed"
else
TITLE="[CI] ${GATE} failed"
fi
# ── Ensure label exists ─────────────────────────────────────────────────────
ensure_label() {
local exists
exists=$(curl -sf -o /dev/null -w '%{http_code}' \
-H "Authorization: token ${GITEA_TOKEN}" \
"${API}/labels" 2>/dev/null || echo "000")
if [[ "$exists" == "200" ]]; then
# Check if label already exists
local found
found=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
"${API}/labels" 2>/dev/null \
| grep -o "\"name\":\"${LABEL_NAME}\"" || true)
if [[ -z "$found" ]]; then
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/labels" \
-d "{\"name\":\"${LABEL_NAME}\",\"color\":\"${LABEL_COLOR}\",\"description\":\"Auto-created by CI issue reporter\"}" \
> /dev/null 2>&1 || true
fi
fi
}
# ── Search for existing open issue ──────────────────────────────────────────
find_existing_issue() {
# URL-encode the gate name for the query
local query
query=$(printf '%s' "[CI] ${GATE}" | sed 's/ /%20/g; s/\[/%5B/g; s/\]/%5D/g')
local response
response=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
"${API}/issues?type=issues&state=open&labels=${LABEL_NAME}&q=${query}&limit=5" \
2>/dev/null || echo "[]")
# Extract the first matching issue number
echo "$response" \
| grep -oP '"number":\s*\K[0-9]+' \
| head -1
}
# ── Build issue body ────────────────────────────────────────────────────────
build_body() {
local severity_badge
if [[ "$SEVERITY" == "error" ]]; then
severity_badge="**Severity:** Error"
else
severity_badge="**Severity:** Warning"
fi
cat <<BODY
## CI Gate Failure: ${GATE}
${severity_badge}
**Workflow:** ${WORKFLOW:-unknown}
**Branch:** ${GITHUB_REF_NAME:-unknown}
**Commit:** \`${GITHUB_SHA:0:8}\`
**Run:** [View CI run](${RUN_URL})
### Details
${DETAILS}
### Resolution
Fix the issue described above and push a new commit. This issue will be closed automatically when the gate passes, or can be closed manually.
---
*Auto-created by [ci-issue-reporter](${GITEA_URL}/${REPO}/src/branch/main/automation/ci-issue-reporter.sh)*
BODY
}
# ── Build comment body (for existing issues) ────────────────────────────────
build_comment() {
cat <<COMMENT
### CI failure recurrence
**Branch:** ${GITHUB_REF_NAME:-unknown}
**Commit:** \`${GITHUB_SHA:0:8}\`
**Run:** [View CI run](${RUN_URL})
${DETAILS}
COMMENT
}
# ── Main ────────────────────────────────────────────────────────────────────
ensure_label
EXISTING=$(find_existing_issue)
if [[ -n "$EXISTING" ]]; then
# Append comment to existing issue
COMMENT_BODY=$(build_comment)
COMMENT_JSON=$(printf '%s' "$COMMENT_BODY" | python3 -c "
import sys, json
print(json.dumps({'body': sys.stdin.read()}))" 2>/dev/null)
HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${EXISTING}/comments" \
-d "${COMMENT_JSON}" 2>/dev/null || echo "000")
if [[ "$HTTP" == "201" ]]; then
echo "Commented on existing issue #${EXISTING}"
else
echo "WARNING: Failed to comment on issue #${EXISTING} (HTTP ${HTTP})"
fi
else
# Create new issue
ISSUE_BODY=$(build_body)
ISSUE_JSON=$(python3 -c "
import sys, json
body = sys.stdin.read()
print(json.dumps({
'title': sys.argv[1],
'body': body,
'labels': []
}))" "$TITLE" <<< "$ISSUE_BODY" 2>/dev/null)
# Create the issue
RESPONSE=$(curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues" \
-d "${ISSUE_JSON}" 2>/dev/null || echo "{}")
ISSUE_NUM=$(echo "$RESPONSE" | grep -oP '"number":\s*\K[0-9]+' | head -1)
if [[ -n "$ISSUE_NUM" ]]; then
# Apply label (separate call — more reliable across Gitea versions)
LABEL_ID=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
"${API}/labels" 2>/dev/null \
| grep -oP "\"id\":\s*\K[0-9]+(?=[^}]*\"name\":\s*\"${LABEL_NAME}\")" \
| head -1 || true)
if [[ -n "$LABEL_ID" ]]; then
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${ISSUE_NUM}/labels" \
-d "{\"labels\":[${LABEL_ID}]}" \
> /dev/null 2>&1 || true
fi
echo "Created issue #${ISSUE_NUM}: ${TITLE}"
else
echo "WARNING: Failed to create issue"
echo "Response: ${RESPONSE}"
fi
fi
+160 -58
View File
@@ -1,5 +1,5 @@
// Copyright 2026 Moko Consulting. All rights reserved.
// SPDX-License-Identifier: MIT
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package issues
@@ -10,87 +10,185 @@ import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
)
// CustomFieldDefinition defines a custom field for a repository's issues
type CustomFieldDefinition struct {
ID int64 `xorm:"pk autoincr" json:"id"`
RepoID int64 `xorm:"INDEX NOT NULL" json:"repo_id"`
Name string `xorm:"NOT NULL" json:"name"`
FieldType string `xorm:"NOT NULL" json:"field_type"` // text, number, date, dropdown, checkbox
Description string `json:"description"`
Required bool `xorm:"NOT NULL DEFAULT false" json:"required"`
Position int `xorm:"NOT NULL DEFAULT 0" json:"position"`
Options string `xorm:"TEXT" json:"options"` // JSON array for dropdown options
DefaultVal string `json:"default_value"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created" json:"created_at"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated" json:"updated_at"`
}
func init() {
db.RegisterModel(new(CustomFieldDefinition))
db.RegisterModel(new(CustomFieldDef))
db.RegisterModel(new(CustomFieldValue))
}
// CustomFieldValue stores the value of a custom field for a specific issue
// CustomFieldType represents the data type of a custom field.
type CustomFieldType string
const (
CustomFieldTypeText CustomFieldType = "text"
CustomFieldTypeNumber CustomFieldType = "number"
CustomFieldTypeDate CustomFieldType = "date"
CustomFieldTypeDropdown CustomFieldType = "dropdown"
CustomFieldTypeCheckbox CustomFieldType = "checkbox"
CustomFieldTypeURL CustomFieldType = "url"
)
// CustomFieldScope determines where the field appears.
type CustomFieldScope string
const (
CustomFieldScopeIssue CustomFieldScope = "issue" // appears in issue sidebar
CustomFieldScopeRepo CustomFieldScope = "repo" // appears in repo settings metadata
)
// CustomFieldDef defines a custom field at the org level.
// owner_id = org ID, scope = issue or repo.
// repo_id is kept for backward compat but 0 for org-level definitions.
type CustomFieldDef struct {
ID int64 `xorm:"pk autoincr"`
OwnerID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'owner_id'"` // org that owns this field
RepoID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'repo_id'"` // 0 = org-level (inherited by all repos)
Scope CustomFieldScope `xorm:"VARCHAR(10) NOT NULL DEFAULT 'issue' 'scope'"`
Name string `xorm:"NOT NULL"`
FieldType CustomFieldType `xorm:"VARCHAR(20) NOT NULL 'field_type'"`
Description string `xorm:"TEXT"`
Options string `xorm:"TEXT"` // JSON array for dropdown options
Required bool `xorm:"NOT NULL DEFAULT false"`
SortOrder int `xorm:"NOT NULL DEFAULT 0 'sort_order'"`
IsActive bool `xorm:"NOT NULL DEFAULT true 'is_active'"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"`
}
func (CustomFieldDef) TableName() string {
return "custom_field_def"
}
// CustomFieldValue stores a custom field value for an entity (issue or repo).
type CustomFieldValue struct {
ID int64 `xorm:"pk autoincr" json:"id"`
IssueID int64 `xorm:"INDEX NOT NULL" json:"issue_id"`
FieldID int64 `xorm:"INDEX NOT NULL" json:"field_id"`
Value string `xorm:"TEXT" json:"value"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created" json:"created_at"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated" json:"updated_at"`
ID int64 `xorm:"pk autoincr"`
EntityID int64 `xorm:"INDEX NOT NULL 'entity_id'"` // issue ID or repo ID
EntityType string `xorm:"VARCHAR(10) NOT NULL DEFAULT 'issue' 'entity_type'"` // "issue" or "repo"
FieldID int64 `xorm:"INDEX NOT NULL 'field_id'"`
Value string `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"`
}
// GetCustomFieldsByRepoID returns all custom field definitions for a repo
func GetCustomFieldsByRepoID(ctx context.Context, repoID int64) ([]*CustomFieldDefinition, error) {
fields := make([]*CustomFieldDefinition, 0)
return fields, db.GetEngine(ctx).Where("repo_id = ?", repoID).OrderBy("position ASC").Find(&fields)
func (CustomFieldValue) TableName() string {
return "custom_field_value"
}
// GetCustomFieldByID returns a custom field definition by ID
func GetCustomFieldByID(ctx context.Context, id int64) (*CustomFieldDefinition, error) {
field := &CustomFieldDefinition{ID: id}
has, err := db.GetEngine(ctx).Get(field)
// ──────────────────────────────────────────────────────────────────────
// Queries for org-level field definitions
// ──────────────────────────────────────────────────────────────────────
// GetCustomFieldsByOwner returns all active field definitions for an org with a given scope.
func GetCustomFieldsByOwner(ctx context.Context, ownerID int64, scope CustomFieldScope) ([]*CustomFieldDef, error) {
fields := make([]*CustomFieldDef, 0, 10)
return fields, db.GetEngine(ctx).
Where("owner_id = ? AND scope = ? AND is_active = ?", ownerID, scope, true).
OrderBy("sort_order ASC, id ASC").
Find(&fields)
}
// GetAllCustomFieldsByOwner returns all field definitions for an org (including inactive).
func GetAllCustomFieldsByOwner(ctx context.Context, ownerID int64) ([]*CustomFieldDef, error) {
fields := make([]*CustomFieldDef, 0, 10)
return fields, db.GetEngine(ctx).
Where("owner_id = ?", ownerID).
OrderBy("scope ASC, sort_order ASC, id ASC").
Find(&fields)
}
// GetCustomFieldsByOwnerAndScope returns all fields for an org filtered by scope.
func GetCustomFieldsByOwnerAndScope(ctx context.Context, ownerID int64, scope CustomFieldScope) ([]*CustomFieldDef, error) {
fields := make([]*CustomFieldDef, 0, 10)
return fields, db.GetEngine(ctx).
Where("owner_id = ? AND scope = ?", ownerID, scope).
OrderBy("sort_order ASC, id ASC").
Find(&fields)
}
// ──────────────────────────────────────────────────────────────────────
// Backward-compatible queries (load by repo's owner)
// ──────────────────────────────────────────────────────────────────────
// GetCustomFieldsByRepo returns active issue-scoped fields for a repo's org.
// This is the main query used by the issue sidebar.
func GetCustomFieldsByRepo(ctx context.Context, repoID int64) ([]*CustomFieldDef, error) {
// First try org-level fields (owner_id != 0, repo_id = 0)
// Fall back to legacy repo-level fields (repo_id = repoID)
fields := make([]*CustomFieldDef, 0, 10)
return fields, db.GetEngine(ctx).
Where("((owner_id != 0 AND repo_id = 0) OR repo_id = ?) AND scope = ? AND is_active = ?",
repoID, CustomFieldScopeIssue, true).
OrderBy("sort_order ASC, id ASC").
Find(&fields)
}
// GetAllCustomFieldsByRepo returns all field definitions for a repo (for settings page).
func GetAllCustomFieldsByRepo(ctx context.Context, repoID int64) ([]*CustomFieldDef, error) {
fields := make([]*CustomFieldDef, 0, 10)
return fields, db.GetEngine(ctx).
Where("repo_id = ?", repoID).
OrderBy("sort_order ASC, id ASC").
Find(&fields)
}
// ──────────────────────────────────────────────────────────────────────
// Field definition CRUD
// ──────────────────────────────────────────────────────────────────────
// GetCustomFieldDefByID returns a single field definition.
func GetCustomFieldDefByID(ctx context.Context, id int64) (*CustomFieldDef, error) {
field := new(CustomFieldDef)
has, err := db.GetEngine(ctx).ID(id).Get(field)
if err != nil {
return nil, err
}
if !has {
return nil, nil
return nil, db.ErrNotExist{Resource: "CustomFieldDef", ID: id}
}
return field, nil
}
// CreateCustomField creates a new custom field definition
func CreateCustomField(ctx context.Context, field *CustomFieldDefinition) error {
// CreateCustomFieldDef creates a new custom field definition.
func CreateCustomFieldDef(ctx context.Context, field *CustomFieldDef) error {
_, err := db.GetEngine(ctx).Insert(field)
return err
}
// UpdateCustomField updates a custom field definition
func UpdateCustomField(ctx context.Context, field *CustomFieldDefinition) error {
// UpdateCustomFieldDef updates a custom field definition.
func UpdateCustomFieldDef(ctx context.Context, field *CustomFieldDef) error {
_, err := db.GetEngine(ctx).ID(field.ID).AllCols().Update(field)
return err
}
// DeleteCustomField deletes a custom field and all its values
func DeleteCustomField(ctx context.Context, id int64) error {
sess := db.GetEngine(ctx)
if _, err := sess.Where("field_id = ?", id).Delete(&CustomFieldValue{}); err != nil {
// DeleteCustomFieldDef deletes a field definition and all its values.
func DeleteCustomFieldDef(ctx context.Context, id int64) error {
if _, err := db.GetEngine(ctx).Where("field_id = ?", id).Delete(new(CustomFieldValue)); err != nil {
return err
}
_, err := sess.ID(id).Delete(&CustomFieldDefinition{})
_, err := db.GetEngine(ctx).ID(id).Delete(new(CustomFieldDef))
return err
}
// GetCustomFieldValues returns all custom field values for an issue
func GetCustomFieldValues(ctx context.Context, issueID int64) ([]*CustomFieldValue, error) {
values := make([]*CustomFieldValue, 0)
return values, db.GetEngine(ctx).Where("issue_id = ?", issueID).Find(&values)
// ──────────────────────────────────────────────────────────────────────
// Field values — generic entity-based (works for issues and repos)
// ──────────────────────────────────────────────────────────────────────
// GetCustomFieldValuesMap returns field_id -> value for an entity.
func GetCustomFieldValuesMap(ctx context.Context, entityID int64) (map[int64]string, error) {
values := make([]*CustomFieldValue, 0, 10)
if err := db.GetEngine(ctx).Where("entity_id = ?", entityID).Find(&values); err != nil {
return nil, err
}
result := make(map[int64]string, len(values))
for _, v := range values {
result[v.FieldID] = v.Value
}
return result, nil
}
// SetCustomFieldValue sets or updates a custom field value for an issue
func SetCustomFieldValue(ctx context.Context, issueID, fieldID int64, value string) error {
existing := &CustomFieldValue{}
has, err := db.GetEngine(ctx).Where("issue_id = ? AND field_id = ?", issueID, fieldID).Get(existing)
// SetCustomFieldValue creates or updates a single custom field value.
func SetCustomFieldValue(ctx context.Context, entityID, fieldID int64, value string) error {
existing := new(CustomFieldValue)
has, err := db.GetEngine(ctx).Where("entity_id = ? AND field_id = ?", entityID, fieldID).Get(existing)
if err != nil {
return err
}
@@ -100,15 +198,19 @@ func SetCustomFieldValue(ctx context.Context, issueID, fieldID int64, value stri
return err
}
_, err = db.GetEngine(ctx).Insert(&CustomFieldValue{
IssueID: issueID,
FieldID: fieldID,
Value: value,
EntityID: entityID,
FieldID: fieldID,
Value: value,
})
return err
}
// DeleteCustomFieldValue deletes a specific custom field value
func DeleteCustomFieldValue(ctx context.Context, issueID, fieldID int64) error {
_, err := db.GetEngine(ctx).Where("issue_id = ? AND field_id = ?", issueID, fieldID).Delete(&CustomFieldValue{})
return err
// SetCustomFieldValues sets multiple custom field values for an entity.
func SetCustomFieldValues(ctx context.Context, entityID int64, values map[int64]string) error {
for fieldID, value := range values {
if err := SetCustomFieldValue(ctx, entityID, fieldID, value); err != nil {
return err
}
}
return nil
}
+18 -7
View File
@@ -48,14 +48,18 @@ func (LicenseKey) TableName() string {
return "license_key"
}
// GenerateKeyString creates a random license key in MOKO-XXXX-XXXX-XXXX-XXXX format.
func GenerateKeyString() (string, error) {
// GenerateKeyString creates a random license key in PREFIX-XXXX-XXXX-XXXX-XXXX format.
// If prefix is empty, defaults to "MOKO".
func GenerateKeyString(prefix string) (string, error) {
if prefix == "" {
prefix = "MOKO"
}
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
return "", err
}
hex := strings.ToUpper(hex.EncodeToString(b))
return fmt.Sprintf("MOKO-%s-%s-%s-%s", hex[0:4], hex[4:8], hex[8:12], hex[12:16]), nil
h := strings.ToUpper(hex.EncodeToString(b))
return fmt.Sprintf("%s-%s-%s-%s-%s", prefix, h[0:4], h[4:8], h[8:12], h[12:16]), nil
}
// HashKey returns the SHA-256 hash of a raw key string.
@@ -65,8 +69,14 @@ func HashKey(rawKey string) string {
}
// CreateLicenseKey generates a new key, stores it in plaintext and hashed, and returns the raw key.
// The prefix is looked up from the org's update stream config.
func CreateLicenseKey(ctx context.Context, key *LicenseKey) (rawKey string, err error) {
rawKey, err = GenerateKeyString()
prefix := ""
cfg := GetEffectiveConfig(ctx, key.OwnerID, 0)
if cfg != nil && cfg.KeyPrefix != "" {
prefix = cfg.KeyPrefix
}
rawKey, err = GenerateKeyString(prefix)
if err != nil {
return "", fmt.Errorf("GenerateKeyString: %w", err)
}
@@ -120,10 +130,11 @@ func GetLicenseKeyByID(ctx context.Context, id int64) (*LicenseKey, error) {
return key, nil
}
// ListLicenseKeys returns all keys for the given owner.
// ListLicenseKeys returns all keys for the given owner, master keys first.
func ListLicenseKeys(ctx context.Context, ownerID int64) ([]*LicenseKey, error) {
keys := make([]*LicenseKey, 0, 20)
return keys, db.GetEngine(ctx).Where("owner_id = ?", ownerID).Find(&keys)
return keys, db.GetEngine(ctx).Where("owner_id = ?", ownerID).
OrderBy("is_internal DESC, created_unix DESC").Find(&keys)
}
// SearchLicenseKeys searches keys for an owner by key prefix/raw, licensee, email, or domain.
+32
View File
@@ -5,8 +5,10 @@ package licenses
import (
"context"
"strings"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
org_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/organization"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
"xorm.io/builder"
@@ -30,6 +32,10 @@ type LicensePackage struct {
// AllowedChannels defines which update streams keys from this package
// can access. JSON array, e.g. ["stable","rc"]. Empty = all channels.
AllowedChannels string `xorm:"TEXT"`
// DomainRestriction is a comma-separated list of allowed domains.
// Keys generated from this package inherit this unless overridden.
// Empty = no restriction.
DomainRestriction string `xorm:"TEXT"`
IsActive bool `xorm:"NOT NULL DEFAULT true"`
IsArchived bool `xorm:"NOT NULL DEFAULT false"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
@@ -79,6 +85,32 @@ func ListLicensePackages(ctx context.Context, ownerID int64) ([]*LicensePackage,
return pkgs, db.GetEngine(ctx).Where("owner_id = ? AND is_archived = ?", ownerID, false).Find(&pkgs)
}
// ListLicensePackagesWithAncestors returns packages from the org and all parent orgs.
func ListLicensePackagesWithAncestors(ctx context.Context, ownerID int64) ([]*LicensePackage, error) {
ancestorIDs := org_model.GetAncestorOrgIDs(ctx, ownerID)
pkgs := make([]*LicensePackage, 0, 10)
return pkgs, db.GetEngine(ctx).In("owner_id", ancestorIDs).Where("is_archived = ?", false).Find(&pkgs)
}
// ListLicenseKeysWithAncestors returns keys from the org and all parent orgs.
func ListLicenseKeysWithAncestors(ctx context.Context, ownerID int64) ([]*LicenseKey, error) {
ancestorIDs := org_model.GetAncestorOrgIDs(ctx, ownerID)
keys := make([]*LicenseKey, 0, 20)
return keys, db.GetEngine(ctx).In("owner_id", ancestorIDs).Find(&keys)
}
// SearchLicenseKeysWithAncestors searches keys across the org and all parent orgs.
func SearchLicenseKeysWithAncestors(ctx context.Context, ownerID int64, query string) ([]*LicenseKey, error) {
ancestorIDs := org_model.GetAncestorOrgIDs(ctx, ownerID)
keys := make([]*LicenseKey, 0, 20)
like := "%" + strings.ToLower(query) + "%"
return keys, db.GetEngine(ctx).
In("owner_id", ancestorIDs).
And("(LOWER(key_prefix) LIKE ? OR LOWER(key_raw) LIKE ? OR LOWER(licensee_name) LIKE ? OR LOWER(licensee_email) LIKE ? OR LOWER(domain_restriction) LIKE ? OR LOWER(payment_ref) LIKE ?)",
like, like, like, like, like, like).
Find(&keys)
}
// ListArchivedLicensePackages returns archived packages for the given owner.
func ListArchivedLicensePackages(ctx context.Context, ownerID int64) ([]*LicensePackage, error) {
pkgs := make([]*LicensePackage, 0, 10)
+15 -14
View File
@@ -23,26 +23,27 @@ type UpdateStreamConfig struct {
ID int64 `xorm:"pk autoincr"`
OwnerID int64 `xorm:"INDEX NOT NULL"` // org or user
RepoID int64 `xorm:"INDEX NOT NULL DEFAULT 0"` // 0 = org-level default
StreamMode string `xorm:"NOT NULL DEFAULT 'joomla'"` // joomla, custom
StreamMode string `xorm:"NOT NULL DEFAULT 'joomla' 'stream_mode'"` // joomla, custom
Platform string `xorm:"NOT NULL DEFAULT 'joomla'"` // joomla, dolibarr, both, wordpress, prestashop, drupal
LicensingEnabled bool `xorm:"NOT NULL DEFAULT false"` // master toggle for licensing system
RequireKey bool `xorm:"NOT NULL DEFAULT false"` // require license key for update feed
FeedVisibility string `xorm:"VARCHAR(20) NOT NULL DEFAULT 'public'"` // public, no-download, hidden
DownloadGating string `xorm:"VARCHAR(20) NOT NULL DEFAULT 'none'"` // none, all, prerelease
SupportURL string `xorm:"TEXT"` // wiki or external support page URL
LicensingEnabled bool `xorm:"NOT NULL DEFAULT false 'licensing_enabled'"` // master toggle
RequireKey bool `xorm:"NOT NULL DEFAULT false 'require_key'"` // require license key for update feed
FeedVisibility string `xorm:"VARCHAR(20) NOT NULL DEFAULT 'public' 'feed_visibility'"` // public, no-download, hidden
DownloadGating string `xorm:"VARCHAR(20) NOT NULL DEFAULT 'none' 'download_gating'"` // none, all, prerelease
SupportURL string `xorm:"TEXT 'support_url'"` // wiki or external support page URL
KeyPrefix string `xorm:"VARCHAR(20) 'key_prefix'"` // org-specific license key prefix (e.g. "ACME")
// Extension metadata — used in update feed generation.
ExtensionName string `xorm:"TEXT"` // element identifier (e.g. pkg_mokowaas, com_mokowaas)
DisplayName string `xorm:"TEXT"` // human-readable name (e.g. "Package - MokoWaaS")
ExtensionName string `xorm:"TEXT 'extension_name'"` // element identifier (e.g. pkg_mokowaas, com_mokowaas)
DisplayName string `xorm:"TEXT 'display_name'"` // human-readable name (e.g. "Package - MokoWaaS")
Description string `xorm:"TEXT"` // short description for update feeds
ExtensionType string `xorm:"VARCHAR(50)"` // component, module, plugin, package, template, library
ExtensionType string `xorm:"VARCHAR(50) 'extension_type'"` // component, module, plugin, package, template, library
Maintainer string `xorm:"TEXT"` // maintainer/author name
MaintainerURL string `xorm:"TEXT"` // maintainer website
InfoURL string `xorm:"TEXT"` // extension info/product page URL
TargetVersion string `xorm:"TEXT"` // target platform version regex (e.g. "(5|6)\..*")
PHPMinimum string `xorm:"VARCHAR(20)"` // minimum PHP version (e.g. "8.1")
MaintainerURL string `xorm:"TEXT 'maintainer_url'"` // maintainer website
InfoURL string `xorm:"TEXT 'info_url'"` // extension info/product page URL
TargetVersion string `xorm:"TEXT 'target_version'"` // target platform version regex (e.g. "(5|6)\..*")
PHPMinimum string `xorm:"VARCHAR(20) 'php_minimum'"` // minimum PHP version (e.g. "8.1")
// CustomStreams is a JSON array of stream definitions.
// Each entry: {"name":"lts","suffix":"-lts","description":"Long-term support"}
CustomStreams string `xorm:"TEXT"`
CustomStreams string `xorm:"TEXT 'custom_streams'"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED"`
}
+6 -1
View File
@@ -416,8 +416,13 @@ func prepareMigrationTasks() []*migration {
newMigration(336, "Add update stream config table", v1_27.AddUpdateStreamConfigTable),
newMigration(337, "Add key_plain column to license_key", v1_27.AddKeyPlainToLicenseKey),
newMigration(338, "Add platform and require_key to update_stream_config", v1_27.AddPlatformAndRequireKeyToStreamConfig),
newMigration(339, "Add AI assistant tables", v1_27.AddAITables),
newMigration(339, "Placeholder for AI tables", noopMigration),
newMigration(340, "Sync license system columns (key_raw, payment_ref, heartbeat, archive, metadata)", v1_27.SyncLicenseSystemColumns),
newMigration(341, "Add parent_org_id to user table for enterprise sub-org hierarchy", v1_27.AddParentOrgIDToUser),
newMigration(342, "Add is_hidden to repository for three-level visibility", v1_27.AddIsHiddenToRepository),
newMigration(343, "Add custom field tables for issue custom fields", v1_27.AddCustomFieldTables),
newMigration(344, "Add domain_restriction to license_package table", v1_27.AddDomainRestrictionToLicensePackage),
newMigration(345, "Migrate custom fields to org-level with scope", v1_27.MigrateCustomFieldsToOrgLevel),
}
return preparedMigrations
}
-33
View File
@@ -1,33 +0,0 @@
// Copyright 2026 Moko Consulting. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_25
import "xorm.io/xorm"
func AddCustomFieldTables(x *xorm.Engine) error {
type CustomFieldDefinition struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX NOT NULL"`
Name string `xorm:"NOT NULL"`
FieldType string `xorm:"NOT NULL"` // text, number, date, dropdown, checkbox
Description string
Required bool `xorm:"NOT NULL DEFAULT false"`
Position int `xorm:"NOT NULL DEFAULT 0"`
Options string `xorm:"TEXT"` // JSON array for dropdown options
DefaultVal string
CreatedUnix int64 `xorm:"INDEX created"`
UpdatedUnix int64 `xorm:"INDEX updated"`
}
type CustomFieldValue struct {
ID int64 `xorm:"pk autoincr"`
IssueID int64 `xorm:"INDEX NOT NULL"`
FieldID int64 `xorm:"INDEX NOT NULL"`
Value string `xorm:"TEXT"`
CreatedUnix int64 `xorm:"INDEX created"`
UpdatedUnix int64 `xorm:"INDEX updated"`
}
return x.Sync(new(CustomFieldDefinition), new(CustomFieldValue))
}
+1
View File
@@ -49,6 +49,7 @@ type updateStreamConfig340 struct {
InfoURL string `xorm:"TEXT"`
TargetVersion string `xorm:"TEXT"`
PHPMinimum string `xorm:"VARCHAR(20)"`
KeyPrefix string `xorm:"VARCHAR(20)"`
}
func (updateStreamConfig340) TableName() string {
+21
View File
@@ -0,0 +1,21 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import "xorm.io/xorm"
type userParentOrg341 struct {
ID int64 `xorm:"pk autoincr"`
ParentOrgID int64 `xorm:"INDEX DEFAULT 0"`
}
func (userParentOrg341) TableName() string {
return "user"
}
// AddParentOrgIDToUser adds the parent_org_id column to the user table
// for enterprise sub-org hierarchy support.
func AddParentOrgIDToUser(x *xorm.Engine) error {
return x.Sync(new(userParentOrg341))
}
+20
View File
@@ -0,0 +1,20 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import "xorm.io/xorm"
type repoHidden342 struct {
ID int64 `xorm:"pk autoincr"`
IsHidden bool `xorm:"INDEX NOT NULL DEFAULT false"`
}
func (repoHidden342) TableName() string {
return "repository"
}
// AddIsHiddenToRepository adds the is_hidden column for three-level repo visibility.
func AddIsHiddenToRepository(x *xorm.Engine) error {
return x.Sync(new(repoHidden342))
}
+46
View File
@@ -0,0 +1,46 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
"xorm.io/xorm"
)
type customFieldDef343 struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX NOT NULL 'repo_id'"`
Name string `xorm:"NOT NULL"`
FieldType string `xorm:"VARCHAR(20) NOT NULL 'field_type'"`
Description string `xorm:"TEXT"`
Options string `xorm:"TEXT"`
Required bool `xorm:"NOT NULL DEFAULT false"`
SortOrder int `xorm:"NOT NULL DEFAULT 0 'sort_order'"`
IsActive bool `xorm:"NOT NULL DEFAULT true 'is_active'"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"`
}
func (customFieldDef343) TableName() string {
return "custom_field_def"
}
type customFieldValue343 struct {
ID int64 `xorm:"pk autoincr"`
IssueID int64 `xorm:"INDEX NOT NULL 'issue_id'"`
FieldID int64 `xorm:"INDEX NOT NULL 'field_id'"`
Value string `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"`
}
func (customFieldValue343) TableName() string {
return "custom_field_value"
}
// AddCustomFieldTables creates the custom_field_def and custom_field_value tables.
func AddCustomFieldTables(x *xorm.Engine) error {
return x.Sync(new(customFieldDef343), new(customFieldValue343))
}
+15
View File
@@ -0,0 +1,15 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import (
"xorm.io/xorm"
)
func AddDomainRestrictionToLicensePackage(x *xorm.Engine) error {
type LicensePackage struct {
DomainRestriction string `xorm:"TEXT"`
}
return x.Sync(new(LicensePackage))
}
+39
View File
@@ -0,0 +1,39 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import (
"xorm.io/xorm"
)
// MigrateCustomFieldsToOrgLevel adds owner_id, scope to custom_field_def
// and renames issue_id to entity_id + adds entity_type in custom_field_value.
func MigrateCustomFieldsToOrgLevel(x *xorm.Engine) error {
// Add new columns to custom_field_def
type CustomFieldDef struct {
OwnerID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'owner_id'"`
Scope string `xorm:"VARCHAR(10) NOT NULL DEFAULT 'issue' 'scope'"`
}
if err := x.Sync(new(CustomFieldDef)); err != nil {
return err
}
// Add entity_type and entity_id to custom_field_value
type CustomFieldValue struct {
EntityID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'entity_id'"`
EntityType string `xorm:"VARCHAR(10) NOT NULL DEFAULT 'issue' 'entity_type'"`
}
if err := x.Sync(new(CustomFieldValue)); err != nil {
return err
}
// Migrate existing data: copy issue_id to entity_id where entity_id is 0
if _, err := x.Exec("UPDATE custom_field_value SET entity_id = issue_id WHERE entity_id = 0 AND issue_id != 0"); err != nil {
return err
}
// Set issue_id default to 0 so new inserts don't require it
_, err := x.Exec("ALTER TABLE custom_field_value MODIFY COLUMN issue_id bigint NOT NULL DEFAULT 0")
return err
}
+42
View File
@@ -97,6 +97,48 @@ func (org *Organization) IsOrgAdmin(ctx context.Context, uid int64) (bool, error
return IsOrganizationAdmin(ctx, org.ID, uid)
}
// HasParentOrg returns true if this org has a parent.
func (org *Organization) HasParentOrg() bool {
return org.ParentOrgID > 0
}
// GetParentOrg returns the parent organization, or nil if none.
func (org *Organization) GetParentOrg(ctx context.Context) (*Organization, error) {
if org.ParentOrgID == 0 {
return nil, nil
}
return GetOrgByID(ctx, org.ParentOrgID)
}
// GetChildOrgs returns all direct child organizations.
func GetChildOrgs(ctx context.Context, parentOrgID int64) ([]*Organization, error) {
var orgs []*Organization
return orgs, db.GetEngine(ctx).
Where("type = ? AND parent_org_id = ?", user_model.UserTypeOrganization, parentOrgID).
Find(&orgs)
}
// GetAncestorOrgIDs returns all org IDs in the parent chain (including self).
// Used for license validation — a key from any ancestor org is valid.
func GetAncestorOrgIDs(ctx context.Context, orgID int64) []int64 {
ids := []int64{orgID}
currentID := orgID
seen := map[int64]bool{orgID: true}
for i := 0; i < 10; i++ { // max 10 levels to prevent infinite loops
org, err := GetOrgByID(ctx, currentID)
if err != nil || org.ParentOrgID == 0 {
break
}
if seen[org.ParentOrgID] {
break // cycle detected
}
seen[org.ParentOrgID] = true
ids = append(ids, org.ParentOrgID)
currentID = org.ParentOrgID
}
return ids
}
// IsOrgMember returns true if given user is member of organization.
func (org *Organization) IsOrgMember(ctx context.Context, uid int64) (bool, error) {
return IsOrganizationMember(ctx, org.ID, uid)
+1
View File
@@ -185,6 +185,7 @@ type Repository struct {
NumOpenActionRuns int `xorm:"-"`
IsPrivate bool `xorm:"INDEX"`
IsHidden bool `xorm:"INDEX NOT NULL DEFAULT false"` // hidden repos return 404, private repos return 403
IsEmpty bool `xorm:"INDEX"`
IsArchived bool `xorm:"INDEX"`
IsMirror bool `xorm:"INDEX"`
+1
View File
@@ -152,6 +152,7 @@ type User struct {
NumMembers int
Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 0"`
RepoAdminChangeTeamAccess bool `xorm:"NOT NULL DEFAULT false"`
ParentOrgID int64 `xorm:"INDEX DEFAULT 0"` // 0 = no parent (top-level org)
// Preferences
DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"`
-65
View File
@@ -1,65 +0,0 @@
// Copyright 2026 Moko Consulting. All rights reserved.
// SPDX-License-Identifier: MIT
package structs
import "time"
// CustomFieldDefinition represents a custom field definition for a repository
// swagger:model
type CustomFieldDefinition struct {
ID int64 `json:"id"`
RepoID int64 `json:"repo_id"`
Name string `json:"name"`
FieldType string `json:"field_type"`
Description string `json:"description"`
Required bool `json:"required"`
Position int `json:"position"`
Options string `json:"options"`
DefaultValue string `json:"default_value"`
// swagger:strfmt date-time
Created time.Time `json:"created_at"`
// swagger:strfmt date-time
Updated time.Time `json:"updated_at"`
}
// CreateCustomFieldOption options for creating a custom field
// swagger:model
type CreateCustomFieldOption struct {
// required: true
Name string `json:"name" binding:"Required"`
// required: true
FieldType string `json:"field_type" binding:"Required"` // text, number, date, dropdown, checkbox
Description string `json:"description"`
Required bool `json:"required"`
Position int `json:"position"`
Options string `json:"options"` // JSON array for dropdown
DefaultValue string `json:"default_value"`
}
// EditCustomFieldOption options for editing a custom field
// swagger:model
type EditCustomFieldOption struct {
Name *string `json:"name"`
Description *string `json:"description"`
Required *bool `json:"required"`
Position *int `json:"position"`
Options *string `json:"options"`
DefaultValue *string `json:"default_value"`
}
// CustomFieldValue represents a custom field value for an issue
// swagger:model
type CustomFieldValue struct {
ID int64 `json:"id"`
IssueID int64 `json:"issue_id"`
FieldID int64 `json:"field_id"`
Value string `json:"value"`
}
// SetCustomFieldValueOption options for setting a custom field value
// swagger:model
type SetCustomFieldValueOption struct {
// required: true
Value string `json:"value" binding:"Required"`
}
+12 -9
View File
@@ -104,6 +104,8 @@ type CreateIssueOption struct {
// list of project ids
Projects []int64 `json:"projects"`
Closed bool `json:"closed"`
// custom field values keyed by field name
CustomFields map[string]string `json:"custom_fields,omitempty"`
}
// EditIssueOption options for editing an issue
@@ -190,15 +192,16 @@ const (
// IssueTemplate represents an issue template for a repository
// swagger:model
type IssueTemplate struct {
Name string `json:"name" yaml:"name"`
Title string `json:"title" yaml:"title"`
About string `json:"about" yaml:"about"` // Using "description" in a template file is compatible
Labels IssueTemplateStringSlice `json:"labels" yaml:"labels"`
Assignees IssueTemplateStringSlice `json:"assignees" yaml:"assignees"`
Ref string `json:"ref" yaml:"ref"`
Content string `json:"content" yaml:"-"`
Fields []*IssueFormField `json:"body" yaml:"body"`
FileName string `json:"file_name" yaml:"-"`
Name string `json:"name" yaml:"name"`
Title string `json:"title" yaml:"title"`
About string `json:"about" yaml:"about"` // Using "description" in a template file is compatible
Labels IssueTemplateStringSlice `json:"labels" yaml:"labels"`
Assignees IssueTemplateStringSlice `json:"assignees" yaml:"assignees"`
Ref string `json:"ref" yaml:"ref"`
Content string `json:"content" yaml:"-"`
Fields []*IssueFormField `json:"body" yaml:"body"`
FileName string `json:"file_name" yaml:"-"`
CustomFields map[string]string `json:"custom_fields,omitempty" yaml:"custom_fields"`
}
type IssueTemplateStringSlice []string
+64 -10
View File
@@ -110,6 +110,7 @@
"loading": "Loading…",
"files": "Files",
"error_title": "Error",
"error403": "You do not have permission to access this resource. If you believe this is an error, contact the repository owner.",
"error404": "The page you are trying to reach either <strong>does not exist</strong> or <strong>you are not authorized</strong> to view it.",
"error503": "The server could not complete your request. Please try again later.",
"go_back": "Go Back",
@@ -1411,6 +1412,7 @@
"repo.issues.new.open_projects": "Open Projects",
"repo.issues.new.closed_projects": "Closed Projects",
"repo.issues.new.no_items": "No items",
"repo.issues.custom_fields": "Custom Fields",
"repo.issues.new.milestone": "Milestone",
"repo.issues.new.no_milestone": "No Milestone",
"repo.issues.new.clear_milestone": "Clear milestone",
@@ -2148,15 +2150,15 @@
"repo.settings.unit_visibility_private": "Private (follow repo visibility)",
"repo.settings.unit_visibility_public": "Public (anyone can read)",
"repo.settings.unit_visibility_releases_help": "Controls whether the releases page is visible to anonymous visitors.",
"repo.settings.licensing_section": "Licensing & Updates",
"repo.settings.licensing_section_desc": "Manage commercial license keys and gated update feeds for this repository. When enabled, the Licenses tab appears and release tags must follow update stream naming.",
"repo.settings.licensing_section": "Update Server",
"repo.settings.licensing_section_desc": "Manage update feeds and optional license key gating for this repository. When enabled, the Licenses tab appears and release tags are served via update feeds.",
"repo.settings.update_platform": "Update Feed Format",
"repo.settings.update_platform_both": "Both (Joomla + Dolibarr)",
"repo.settings.update_platform_help": "Choose which update feed format to generate. All formats support license key validation.",
"repo.settings.require_update_key": "Require license key for update feeds",
"repo.settings.require_update_key_help": "When enabled, update feeds return empty results unless a valid license key is provided. Joomla clients will see a Download Key field in Update Sites.",
"repo.settings.enable_licensing": "Enable licensing for this repository",
"repo.settings.enable_licensing_help": "Show the Licenses tab and enable license key management for this repository.",
"repo.settings.enable_licensing": "Enable Update Server for this repository",
"repo.settings.enable_licensing_help": "Serve update feeds from releases and show the Licenses tab for optional key management.",
"repo.settings.packages_desc": "Enable Repository Packages Registry",
"repo.settings.projects_desc": "Enable Projects",
"repo.settings.projects_mode_desc": "Projects Mode (which kinds of projects to show)",
@@ -2659,12 +2661,16 @@
"repo.licenses.key_revoked": "License key revoked.",
"repo.licenses.master_key_created": "Master License Key Created",
"repo.licenses.master_key_created_copy": "This is your organization master key with unlimited access to all update channels. Copy it now — it will not be shown again.",
"repo.licenses.regenerate_master_key": "Regenerate",
"repo.licenses.regenerate_master_key_help": "Deactivates the current master key and generates a new one. The new key will be shown once.",
"repo.licenses.master_key_regenerated": "Master key regenerated. Copy the new key below — it will not be shown again.",
"repo.licenses.update_feeds": "Update Feed URLs",
"repo.licenses.edit_key": "Edit License Key",
"repo.licenses.licensee_name": "Licensee Name",
"repo.licenses.licensee_email": "Licensee Email",
"repo.licenses.domain_restriction": "Domain Restriction",
"repo.licenses.domain_restriction_help": "Comma-separated list of allowed domains. Leave empty for no restriction.",
"repo.licenses.domain_restriction_help": "Comma-separated list of allowed domains. Leave empty to inherit from the package default.",
"repo.licenses.domain_restriction_package_help": "Default domain restriction for keys generated from this package. Comma-separated. Keys can override this.",
"repo.licenses.use_package_default": "use package default",
"repo.licenses.expires_at": "Expires At",
"repo.licenses.expires_at_help": "Leave empty for no expiry (lifetime).",
@@ -2677,6 +2683,7 @@
"repo.licenses.feed_joomla_updates": "Joomla updates.xml",
"repo.licenses.feed_dolibarr_updates": "Dolibarr JSON",
"repo.licenses.feed_wordpress_updates": "WordPress (PUC JSON)",
"repo.licenses.open_feed": "Open in new tab",
"repo.licenses.feed_changelog_xml": "Changelog XML (Joomla)",
"repo.licenses.master_label": "Master",
"repo.licenses.unlimited": "unlimited",
@@ -2714,6 +2721,35 @@
"repo.settings.download_gating": "Download Gating",
"repo.settings.support_url": "Support / Product Page URL",
"repo.settings.support_url_help": "Shown when downloads are gated. Can point to your wiki, product page, or external support site.",
"repo.settings.custom_fields": "Custom Fields",
"repo.settings.metadata": "Metadata",
"repo.settings.metadata_saved": "Repository metadata saved.",
"repo.settings.metadata_empty": "No metadata fields defined. Org admins can add fields in Organization Settings > Custom Fields.",
"repo.settings.custom_field_new": "New Field",
"repo.settings.custom_field_create": "Create Field",
"repo.settings.custom_field_name": "Field Name",
"repo.settings.custom_field_type": "Type",
"repo.settings.custom_field_description": "Description",
"repo.settings.custom_field_options": "Options (JSON)",
"repo.settings.custom_field_options_help": "JSON array for dropdown fields. e.g. [\"Low\",\"Medium\",\"High\"]",
"repo.settings.custom_field_required": "Required",
"repo.settings.custom_field_sort_order": "Sort Order",
"repo.settings.custom_field_created": "Custom field created.",
"repo.settings.custom_field_updated": "Custom field updated.",
"repo.settings.custom_field_deleted": "Custom field deleted.",
"repo.settings.custom_field_confirm_delete": "Delete this custom field? All values stored for this field will be lost.",
"repo.settings.custom_fields_none": "No Custom Fields",
"repo.settings.custom_fields_none_desc": "Define custom fields to add structured metadata to issues.",
"repo.settings.features": "Features",
"repo.settings.features_units": "Units",
"repo.settings.change_visibility": "Change Visibility",
"repo.settings.visibility.warning": "Changing repository visibility affects who can access code, releases, and update feeds.",
"repo.settings.visibility.public.label": "Public",
"repo.settings.visibility.public.desc": "Visible to everyone. Anyone can clone and view.",
"repo.settings.visibility.private.label": "Private",
"repo.settings.visibility.private.desc": "Members only. Non-members see Access Denied (403). Licensed update feeds still work.",
"repo.settings.visibility.hidden.label": "Hidden",
"repo.settings.visibility.hidden.desc": "Members only. Non-members see Not Found (404). Hides the repo's existence entirely.",
"repo.release.update_stream": "Update Stream",
"repo.release.update_stream_auto": "(auto-detect from tag name)",
"repo.release.update_stream_help": "Assign this release to an update stream. The update feed will serve the latest release per stream.",
@@ -2862,11 +2898,24 @@
"org.form.create_org_not_allowed": "You are not allowed to create an organization.",
"org.settings": "Settings",
"org.settings.options": "Organization",
"org.settings.update_streams": "Licensing & Update Streams",
"org.settings.licensing": "Licensing",
"org.settings.licensing_desc": "Control commercial license key management and gated update feeds across all repositories in this organization.",
"org.settings.enable_licensing": "Enable licensing for this organization",
"org.settings.enable_licensing_help": "Show the Licenses page in the org menu and enable license key management. Individual repos can also enable licensing independently.",
"org.settings.custom_fields": "Custom Fields",
"org.settings.custom_fields_desc": "Define custom fields that appear across all repositories in this organization. Issue fields show in issue sidebars. Repo fields show in repo settings metadata.",
"org.settings.custom_fields_empty": "No custom fields defined yet.",
"org.settings.custom_field_add": "Add Custom Field",
"org.settings.custom_field_name": "Field Name",
"org.settings.custom_field_scope": "Scope",
"org.settings.custom_field_type": "Type",
"org.settings.custom_field_options": "Options (JSON)",
"org.settings.custom_field_options_help": "For dropdown fields, enter options as a JSON array.",
"org.settings.custom_field_description": "Description",
"org.settings.custom_field_created": "Custom field created.",
"org.settings.custom_field_updated": "Custom field updated.",
"org.settings.custom_field_deleted": "Custom field deleted.",
"org.settings.update_streams": "Update Server",
"org.settings.licensing": "Update Server",
"org.settings.licensing_desc": "Manage update feeds and optional license key gating across all repositories in this organization.",
"org.settings.enable_licensing": "Enable Update Server for this organization",
"org.settings.enable_licensing_help": "Show the Licenses page in the org menu and serve update feeds. Individual repos can also enable this independently.",
"org.settings.require_key": "Require license key for all update feeds",
"org.settings.require_key_help": "Update feeds return empty results unless a valid key is provided. Joomla clients will see a Download Key field. Individual repos can override this.",
"org.settings.feed_visibility": "Update Feed Visibility",
@@ -2908,6 +2957,11 @@
"org.settings.streams_tag_help": "When licensing is active, release tags with prerelease suffixes must match one of the streams above (e.g. v1.0.0-rc1 matches the -rc stream).",
"org.settings.custom_streams": "Custom Stream Definitions (JSON)",
"org.settings.custom_streams_help": "JSON array of stream objects. Each needs: name, suffix, description. Example: [{\"name\":\"lts\",\"suffix\":\"-lts\",\"description\":\"Long-term support\"}]",
"org.settings.key_prefix": "License Key Prefix",
"org.settings.key_prefix_help": "Custom prefix for license keys generated in this org (e.g. ACME, CLIENT). Leave empty for default (MOKO). Max 20 chars, auto-uppercased.",
"org.settings.parent_org": "Parent Organization",
"org.settings.parent_org_none": "(none — top-level organization)",
"org.settings.parent_org_help": "Set a parent org for enterprise hierarchy. Child orgs inherit license packages and master keys from parent orgs.",
"org.settings.update_streams_saved": "Settings saved.",
"org.settings.full_name": "Full Name",
"org.settings.email": "Contact Email Address",
+11 -12
View File
@@ -1655,21 +1655,15 @@ func Routes() *web.Router {
// })
// })
// })
m.Group("/custom-fields", func() {
m.Combo("").Get(repo.ListCustomFields).
Post(reqToken(), reqRepoWriter(unit.TypeIssues), bind(api.CreateCustomFieldOption{}), repo.CreateCustomField)
m.Group("/{fieldId}", func() {
m.Combo("").Get(repo.GetCustomField).
Patch(reqToken(), reqRepoWriter(unit.TypeIssues), bind(api.EditCustomFieldOption{}), repo.EditCustomField).
Delete(reqToken(), reqRepoWriter(unit.TypeIssues), repo.DeleteCustomField)
})
// Repo metadata (repo-scoped custom fields)
m.Group("/metadata", func() {
m.Get("", repo.GetRepoMetadata)
m.Put("", reqToken(), reqRepoWriter(unit.TypeCode), repo.SetRepoMetadata)
})
// Issue custom fields
m.Group("/issues/{index}/custom-fields", func() {
m.Get("", repo.GetIssueCustomFields)
m.Group("/{fieldId}", func() {
m.Put("", reqToken(), reqRepoWriter(unit.TypeIssues), bind(api.SetCustomFieldValueOption{}), repo.SetIssueCustomField)
m.Delete("", reqToken(), reqRepoWriter(unit.TypeIssues), repo.DeleteIssueCustomField)
})
m.Put("", reqToken(), reqRepoWriter(unit.TypeIssues), repo.SetIssueCustomFields)
})
}, repoAssignment(), checkTokenPublicOnly())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue))
@@ -1771,6 +1765,11 @@ func Routes() *web.Router {
m.Delete("", org.UnblockUser)
})
}, reqToken(), reqOrgOwnership())
m.Group("/custom-fields", func() {
m.Get("", org.ListOrgCustomFields)
m.Post("", reqToken(), reqOrgOwnership(), org.CreateOrgCustomField)
m.Delete("/{id}", reqToken(), reqOrgOwnership(), org.DeleteOrgCustomField)
})
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true), checkTokenPublicOnly())
m.Group("/teams/{teamid}", func() {
m.Combo("").Get(reqToken(), org.GetTeam).
+139
View File
@@ -0,0 +1,139 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package org
import (
"encoding/json"
"net/http"
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
type apiCustomFieldDef struct {
ID int64 `json:"id"`
OwnerID int64 `json:"owner_id"`
Scope string `json:"scope"`
Name string `json:"name"`
FieldType string `json:"field_type"`
Description string `json:"description"`
Options any `json:"options"`
Required bool `json:"required"`
SortOrder int `json:"sort_order"`
IsActive bool `json:"is_active"`
}
func toAPIFieldDef(f *issues_model.CustomFieldDef) apiCustomFieldDef {
var opts any
if f.Options != "" {
var parsed []string
if json.Unmarshal([]byte(f.Options), &parsed) == nil {
opts = parsed
} else {
opts = f.Options
}
}
return apiCustomFieldDef{
ID: f.ID,
OwnerID: f.OwnerID,
Scope: string(f.Scope),
Name: f.Name,
FieldType: string(f.FieldType),
Description: f.Description,
Options: opts,
Required: f.Required,
SortOrder: f.SortOrder,
IsActive: f.IsActive,
}
}
// ListOrgCustomFields returns all custom field definitions for an org.
func ListOrgCustomFields(ctx *context.APIContext) {
fields, err := issues_model.GetAllCustomFieldsByOwner(ctx, ctx.Org.Organization.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
result := make([]apiCustomFieldDef, 0, len(fields))
for _, f := range fields {
result = append(result, toAPIFieldDef(f))
}
ctx.JSON(http.StatusOK, result)
}
// CreateOrgCustomField creates a new custom field definition.
func CreateOrgCustomField(ctx *context.APIContext) {
var req struct {
Scope string `json:"scope" binding:"Required"`
Name string `json:"name" binding:"Required"`
FieldType string `json:"field_type" binding:"Required"`
Description string `json:"description"`
Options []string `json:"options"`
Required bool `json:"required"`
SortOrder int `json:"sort_order"`
}
if err := ctx.Req.ParseForm(); err != nil {
ctx.APIError(http.StatusBadRequest, err)
return
}
if err := json.NewDecoder(ctx.Req.Body).Decode(&req); err != nil {
ctx.APIError(http.StatusBadRequest, err)
return
}
if req.Name == "" || req.Scope == "" {
ctx.APIError(http.StatusBadRequest, "name and scope are required")
return
}
scope := issues_model.CustomFieldScope(req.Scope)
if scope != issues_model.CustomFieldScopeIssue && scope != issues_model.CustomFieldScopeRepo {
ctx.APIError(http.StatusBadRequest, "scope must be 'issue' or 'repo'")
return
}
var optionsJSON string
if len(req.Options) > 0 {
data, _ := json.Marshal(req.Options)
optionsJSON = string(data)
}
field := &issues_model.CustomFieldDef{
OwnerID: ctx.Org.Organization.ID,
RepoID: 0,
Scope: scope,
Name: req.Name,
FieldType: issues_model.CustomFieldType(req.FieldType),
Description: req.Description,
Options: optionsJSON,
Required: req.Required,
SortOrder: req.SortOrder,
IsActive: true,
}
if err := issues_model.CreateCustomFieldDef(ctx, field); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusCreated, toAPIFieldDef(field))
}
// DeleteOrgCustomField deletes a custom field definition.
func DeleteOrgCustomField(ctx *context.APIContext) {
id := ctx.PathParamInt64("id")
field, err := issues_model.GetCustomFieldDefByID(ctx, id)
if err != nil {
ctx.APIErrorNotFound()
return
}
if field.OwnerID != ctx.Org.Organization.ID {
ctx.APIErrorNotFound()
return
}
if err := issues_model.DeleteCustomFieldDef(ctx, id); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
-196
View File
@@ -1,196 +0,0 @@
// Copyright 2026 Moko Consulting. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
api "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/web"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
func fieldToAPI(f *issues_model.CustomFieldDefinition) *api.CustomFieldDefinition {
return &api.CustomFieldDefinition{
ID: f.ID,
RepoID: f.RepoID,
Name: f.Name,
FieldType: f.FieldType,
Description: f.Description,
Required: f.Required,
Position: f.Position,
Options: f.Options,
DefaultValue: f.DefaultVal,
Created: f.CreatedUnix.AsTime(),
Updated: f.UpdatedUnix.AsTime(),
}
}
// ListCustomFields lists custom field definitions for a repository
func ListCustomFields(ctx *context.APIContext) {
fields, err := issues_model.GetCustomFieldsByRepoID(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
result := make([]*api.CustomFieldDefinition, len(fields))
for i, f := range fields {
result[i] = fieldToAPI(f)
}
ctx.JSON(http.StatusOK, result)
}
// GetCustomField gets a custom field definition by ID
func GetCustomField(ctx *context.APIContext) {
field, err := issues_model.GetCustomFieldByID(ctx, ctx.PathParamInt64("fieldId"))
if err != nil {
ctx.APIErrorInternal(err)
return
}
if field == nil || field.RepoID != ctx.Repo.Repository.ID {
ctx.APIErrorNotFound()
return
}
ctx.JSON(http.StatusOK, fieldToAPI(field))
}
// CreateCustomField creates a new custom field definition
func CreateCustomField(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.CreateCustomFieldOption)
validTypes := map[string]bool{"text": true, "number": true, "date": true, "dropdown": true, "checkbox": true}
if !validTypes[form.FieldType] {
ctx.APIError(http.StatusUnprocessableEntity, "field_type must be: text, number, date, dropdown, or checkbox")
return
}
field := &issues_model.CustomFieldDefinition{
RepoID: ctx.Repo.Repository.ID,
Name: form.Name,
FieldType: form.FieldType,
Description: form.Description,
Required: form.Required,
Position: form.Position,
Options: form.Options,
DefaultVal: form.DefaultValue,
}
if err := issues_model.CreateCustomField(ctx, field); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusCreated, fieldToAPI(field))
}
// EditCustomField updates a custom field definition
func EditCustomField(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.EditCustomFieldOption)
field, err := issues_model.GetCustomFieldByID(ctx, ctx.PathParamInt64("fieldId"))
if err != nil {
ctx.APIErrorInternal(err)
return
}
if field == nil || field.RepoID != ctx.Repo.Repository.ID {
ctx.APIErrorNotFound()
return
}
if form.Name != nil {
field.Name = *form.Name
}
if form.Description != nil {
field.Description = *form.Description
}
if form.Required != nil {
field.Required = *form.Required
}
if form.Position != nil {
field.Position = *form.Position
}
if form.Options != nil {
field.Options = *form.Options
}
if form.DefaultValue != nil {
field.DefaultVal = *form.DefaultValue
}
if err := issues_model.UpdateCustomField(ctx, field); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, fieldToAPI(field))
}
// DeleteCustomField deletes a custom field and all its values
func DeleteCustomField(ctx *context.APIContext) {
field, err := issues_model.GetCustomFieldByID(ctx, ctx.PathParamInt64("fieldId"))
if err != nil {
ctx.APIErrorInternal(err)
return
}
if field == nil || field.RepoID != ctx.Repo.Repository.ID {
ctx.APIErrorNotFound()
return
}
if err := issues_model.DeleteCustomField(ctx, field.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// GetIssueCustomFields gets all custom field values for an issue
func GetIssueCustomFields(ctx *context.APIContext) {
values, err := issues_model.GetCustomFieldValues(ctx, ctx.PathParamInt64("index"))
if err != nil {
ctx.APIErrorInternal(err)
return
}
result := make([]*api.CustomFieldValue, len(values))
for i, v := range values {
result[i] = &api.CustomFieldValue{
ID: v.ID,
IssueID: v.IssueID,
FieldID: v.FieldID,
Value: v.Value,
}
}
ctx.JSON(http.StatusOK, result)
}
// SetIssueCustomField sets a custom field value on an issue
func SetIssueCustomField(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.SetCustomFieldValueOption)
issueID := ctx.PathParamInt64("index")
fieldID := ctx.PathParamInt64("fieldId")
// Verify field belongs to this repo
field, err := issues_model.GetCustomFieldByID(ctx, fieldID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if field == nil || field.RepoID != ctx.Repo.Repository.ID {
ctx.APIErrorNotFound()
return
}
if err := issues_model.SetCustomFieldValue(ctx, issueID, fieldID, form.Value); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// DeleteIssueCustomField removes a custom field value from an issue
func DeleteIssueCustomField(ctx *context.APIContext) {
issueID := ctx.PathParamInt64("index")
fieldID := ctx.PathParamInt64("fieldId")
if err := issues_model.DeleteCustomFieldValue(ctx, issueID, fieldID); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
+137
View File
@@ -0,0 +1,137 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package repo
import (
"encoding/json"
"net/http"
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
// GetRepoMetadata returns all repo-scoped custom field values.
func GetRepoMetadata(ctx *context.APIContext) {
ownerID := ctx.Repo.Repository.OwnerID
repoID := ctx.Repo.Repository.ID
fields, err := issues_model.GetCustomFieldsByOwner(ctx, ownerID, issues_model.CustomFieldScopeRepo)
if err != nil {
ctx.APIErrorInternal(err)
return
}
values, err := issues_model.GetCustomFieldValuesMap(ctx, repoID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
result := make(map[string]string, len(fields))
for _, f := range fields {
result[f.Name] = values[f.ID]
}
ctx.JSON(http.StatusOK, result)
}
// SetRepoMetadata sets repo-scoped custom field values.
func SetRepoMetadata(ctx *context.APIContext) {
ownerID := ctx.Repo.Repository.OwnerID
repoID := ctx.Repo.Repository.ID
var req map[string]string
if err := json.NewDecoder(ctx.Req.Body).Decode(&req); err != nil {
ctx.APIError(http.StatusBadRequest, err)
return
}
fields, err := issues_model.GetCustomFieldsByOwner(ctx, ownerID, issues_model.CustomFieldScopeRepo)
if err != nil {
ctx.APIErrorInternal(err)
return
}
// Build name->ID map
nameToID := make(map[string]int64, len(fields))
for _, f := range fields {
nameToID[f.Name] = f.ID
}
for name, value := range req {
if fieldID, ok := nameToID[name]; ok {
if err := issues_model.SetCustomFieldValue(ctx, repoID, fieldID, value); err != nil {
ctx.APIErrorInternal(err)
return
}
}
}
ctx.Status(http.StatusNoContent)
}
// GetIssueCustomFields returns custom field values for an issue.
func GetIssueCustomFields(ctx *context.APIContext) {
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
ctx.APIErrorNotFound()
return
}
ownerID := ctx.Repo.Repository.OwnerID
fields, err := issues_model.GetCustomFieldsByOwner(ctx, ownerID, issues_model.CustomFieldScopeIssue)
if err != nil {
ctx.APIErrorInternal(err)
return
}
values, err := issues_model.GetCustomFieldValuesMap(ctx, issue.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
result := make(map[string]string, len(fields))
for _, f := range fields {
result[f.Name] = values[f.ID]
}
ctx.JSON(http.StatusOK, result)
}
// SetIssueCustomFields sets custom field values for an issue.
func SetIssueCustomFields(ctx *context.APIContext) {
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
ctx.APIErrorNotFound()
return
}
var req map[string]string
if err := json.NewDecoder(ctx.Req.Body).Decode(&req); err != nil {
ctx.APIError(http.StatusBadRequest, err)
return
}
ownerID := ctx.Repo.Repository.OwnerID
fields, err := issues_model.GetCustomFieldsByOwner(ctx, ownerID, issues_model.CustomFieldScopeIssue)
if err != nil {
ctx.APIErrorInternal(err)
return
}
nameToID := make(map[string]int64, len(fields))
for _, f := range fields {
nameToID[f.Name] = f.ID
}
for name, value := range req {
if fieldID, ok := nameToID[name]; ok {
if err := issues_model.SetCustomFieldValue(ctx, issue.ID, fieldID, value); err != nil {
ctx.APIErrorInternal(err)
return
}
}
}
ctx.Status(http.StatusNoContent)
}
+23
View File
@@ -702,6 +702,29 @@ func CreateIssue(ctx *context.APIContext) {
return
}
// Save custom field values if provided (resolve field names to IDs).
if len(form.CustomFields) > 0 {
defs, defErr := issues_model.GetCustomFieldsByOwner(ctx, ctx.Repo.Repository.OwnerID, issues_model.CustomFieldScopeIssue)
if defErr != nil {
ctx.APIErrorInternal(defErr)
return
}
if len(defs) > 0 {
vals := make(map[int64]string)
for _, def := range defs {
if v, ok := form.CustomFields[def.Name]; ok {
vals[def.ID] = v
}
}
if len(vals) > 0 {
if setErr := issues_model.SetCustomFieldValues(ctx, issue.ID, vals); setErr != nil {
ctx.APIErrorInternal(setErr)
return
}
}
}
}
if form.Closed {
if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil {
if issues_model.IsErrDependenciesLeft(err) {
+1 -1
View File
@@ -22,7 +22,7 @@ func ShowFileFeed(ctx *context.Context, repo *repo.Repository, formatType string
}
commits, err := ctx.Repo.GitRepo.CommitsByFileAndRange(
git.CommitsByFileAndRangeOptions{
Revision: ctx.Repo.RefFullName.String()
Revision: ctx.Repo.RefFullName.String(),
File: fileName,
Page: 1,
})
+120
View File
@@ -0,0 +1,120 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package org
import (
"net/http"
"strconv"
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
const tplOrgCustomFields templates.TplName = "org/settings/custom_fields"
// SettingsCustomFields shows the org-level custom fields management page.
func SettingsCustomFields(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("org.settings.custom_fields")
ctx.Data["PageIsOrgSettings"] = true
ctx.Data["PageIsSettingsCustomFields"] = true
fields, err := issues_model.GetAllCustomFieldsByOwner(ctx, ctx.Org.Organization.ID)
if err != nil {
ctx.ServerError("GetAllCustomFieldsByOwner", err)
return
}
ctx.Data["CustomFields"] = fields
ctx.HTML(http.StatusOK, tplOrgCustomFields)
}
// SettingsCustomFieldsCreatePost creates a new org-level custom field.
func SettingsCustomFieldsCreatePost(ctx *context.Context) {
sortOrder, _ := strconv.Atoi(ctx.FormString("sort_order"))
scope := issues_model.CustomFieldScope(ctx.FormString("scope"))
if scope != issues_model.CustomFieldScopeIssue && scope != issues_model.CustomFieldScopeRepo {
scope = issues_model.CustomFieldScopeIssue
}
field := &issues_model.CustomFieldDef{
OwnerID: ctx.Org.Organization.ID,
RepoID: 0, // org-level
Scope: scope,
Name: ctx.FormString("name"),
FieldType: issues_model.CustomFieldType(ctx.FormString("field_type")),
Description: ctx.FormString("description"),
Options: ctx.FormString("options"),
Required: ctx.FormString("required") == "on",
SortOrder: sortOrder,
IsActive: true,
}
if field.Name == "" {
ctx.Flash.Error("Field name is required")
ctx.Redirect(ctx.Org.OrgLink + "/settings/custom-fields")
return
}
if err := issues_model.CreateCustomFieldDef(ctx, field); err != nil {
ctx.ServerError("CreateCustomFieldDef", err)
return
}
ctx.Flash.Success(ctx.Tr("org.settings.custom_field_created"))
ctx.Redirect(ctx.Org.OrgLink + "/settings/custom-fields")
}
// SettingsCustomFieldsEditPost updates an org-level custom field.
func SettingsCustomFieldsEditPost(ctx *context.Context) {
id := ctx.PathParamInt64("id")
field, err := issues_model.GetCustomFieldDefByID(ctx, id)
if err != nil {
ctx.ServerError("GetCustomFieldDefByID", err)
return
}
if field.OwnerID != ctx.Org.Organization.ID {
ctx.NotFound(nil)
return
}
field.Name = ctx.FormString("name")
field.FieldType = issues_model.CustomFieldType(ctx.FormString("field_type"))
field.Description = ctx.FormString("description")
field.Options = ctx.FormString("options")
field.Required = ctx.FormString("required") == "on"
field.IsActive = ctx.FormString("is_active") == "on"
sortOrder, _ := strconv.Atoi(ctx.FormString("sort_order"))
field.SortOrder = sortOrder
if err := issues_model.UpdateCustomFieldDef(ctx, field); err != nil {
ctx.ServerError("UpdateCustomFieldDef", err)
return
}
ctx.Flash.Success(ctx.Tr("org.settings.custom_field_updated"))
ctx.Redirect(ctx.Org.OrgLink + "/settings/custom-fields")
}
// SettingsCustomFieldsDeletePost deletes an org-level custom field.
func SettingsCustomFieldsDeletePost(ctx *context.Context) {
id := ctx.PathParamInt64("id")
field, err := issues_model.GetCustomFieldDefByID(ctx, id)
if err != nil {
ctx.ServerError("GetCustomFieldDefByID", err)
return
}
if field.OwnerID != ctx.Org.Organization.ID {
ctx.NotFound(nil)
return
}
if err := issues_model.DeleteCustomFieldDef(ctx, id); err != nil {
ctx.ServerError("DeleteCustomFieldDef", err)
return
}
ctx.Flash.Success(ctx.Tr("org.settings.custom_field_deleted"))
ctx.Redirect(ctx.Org.OrgLink + "/settings/custom-fields")
}
+3 -3
View File
@@ -89,7 +89,7 @@ func Licenses(ctx *context.Context) {
}
}
pkgs, err := licenses.ListLicensePackages(ctx, ownerID)
pkgs, err := licenses.ListLicensePackagesWithAncestors(ctx, ownerID)
if err != nil {
ctx.ServerError("ListLicensePackages", err)
return
@@ -112,9 +112,9 @@ func Licenses(ctx *context.Context) {
var keys []*licenses.LicenseKey
if searchQuery != "" {
keys, err = licenses.SearchLicenseKeys(ctx, ownerID, searchQuery)
keys, err = licenses.SearchLicenseKeysWithAncestors(ctx, ownerID, searchQuery)
} else {
keys, err = licenses.ListLicenseKeys(ctx, ownerID)
keys, err = licenses.ListLicenseKeysWithAncestors(ctx, ownerID)
}
if err != nil {
ctx.ServerError("ListLicenseKeys", err)
+28
View File
@@ -10,6 +10,7 @@ import (
"net/url"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
org_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/organization"
packages_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/packages"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
@@ -47,6 +48,22 @@ func Settings(ctx *context.Context) {
ctx.Data["CurrentVisibility"] = ctx.Org.Organization.Visibility
ctx.Data["RepoAdminChangeTeamAccess"] = ctx.Org.Organization.RepoAdminChangeTeamAccess
ctx.Data["ContextUser"] = ctx.ContextUser
ctx.Data["ParentOrgID"] = ctx.Org.Organization.ParentOrgID
// Load available parent orgs (all orgs the current user belongs to, excluding self).
if ctx.Doer.IsAdmin || ctx.Org.IsOwner {
orgs, _ := db.Find[org_model.Organization](ctx, org_model.FindOrgOptions{
UserID: ctx.Doer.ID,
IncludeVisibility: structs.VisibleTypePrivate,
})
var parentCandidates []*org_model.Organization
for _, o := range orgs {
if o.ID != ctx.Org.Organization.ID {
parentCandidates = append(parentCandidates, o)
}
}
ctx.Data["ParentOrgCandidates"] = parentCandidates
}
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
ctx.ServerError("RenderUserOrgHeader", err)
@@ -98,6 +115,17 @@ func SettingsPost(ctx *context.Context) {
return
}
// Save parent org (enterprise hierarchy).
parentOrgID := ctx.FormInt64("parent_org_id")
if parentOrgID != org.ParentOrgID {
user := org.AsUser()
user.ParentOrgID = parentOrgID
if err := user_model.UpdateUserCols(ctx, user, "parent_org_id"); err != nil {
ctx.ServerError("UpdateUserCols", err)
return
}
}
log.Trace("Organization setting updated: %s", org.Name)
ctx.Flash.Success(ctx.Tr("org.settings.update_setting_success"))
ctx.Redirect(ctx.Org.OrgLink + "/settings")
+2
View File
@@ -5,6 +5,7 @@ package org
import (
"net/http"
"strings"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
@@ -47,6 +48,7 @@ func SettingsUpdateStreamsPost(ctx *context.Context) {
FeedVisibility: ctx.FormString("feed_visibility"),
DownloadGating: ctx.FormString("download_gating"),
SupportURL: ctx.FormString("support_url"),
KeyPrefix: strings.ToUpper(strings.TrimSpace(ctx.FormString("key_prefix"))),
ExtensionName: ctx.FormString("extension_name"),
DisplayName: ctx.FormString("display_name"),
Description: ctx.FormString("feed_description"),
+5 -2
View File
@@ -182,8 +182,11 @@ func ServeAttachment(ctx *context.Context, uuid string) {
}
if !perm.CanRead(unitType) {
ctx.HTTPError(http.StatusNotFound)
return
// Allow access for licensed read-only mode (private repo with valid license key).
if ctx.Data["LicensedReadOnly"] != true {
ctx.HTTPError(http.StatusNotFound)
return
}
}
if requiredScope, ok := attachmentReadScope(unitType); ok {
+1 -1
View File
@@ -630,7 +630,7 @@ func (cpi *comparePageInfoType) prepareCreatePullRequestPage(ctx *context.Contex
if ctx.Written() {
return
}
_, templateErrs := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates, pageMetaData)
_, templateErrs, _ := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates, pageMetaData)
if len(templateErrs) > 0 {
ctx.Flash.Warning(renderErrorOfTemplates(ctx, templateErrs), true)
}
+6
View File
@@ -30,6 +30,12 @@ func CheckDownloadGating(ctx *context.Context, tagName string) bool {
return true // no download gating configured
}
// Signed-in users with repo access bypass download gating.
// The gate is for anonymous/external clients (Joomla update checker).
if ctx.IsSigned && ctx.Repo.Permission.HasAnyUnitAccess() {
return true
}
// For prerelease-only gating, check if this is a prerelease tag.
if gating == "prerelease" && tagName != "" {
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
+33
View File
@@ -0,0 +1,33 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package repo
import (
"fmt"
"net/http"
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
// UpdateIssueCustomField handles POST to set a custom field value on an issue.
func UpdateIssueCustomField(ctx *context.Context) {
issueID := ctx.PathParamInt64("id")
fieldID := ctx.PathParamInt64("field_id")
value := ctx.FormString("value")
// Look up issue to get the index for redirect.
issue, err := issues_model.GetIssueByID(ctx, issueID)
if err != nil {
ctx.ServerError("GetIssueByID", err)
return
}
if err := issues_model.SetCustomFieldValue(ctx, issueID, fieldID, value); err != nil {
ctx.ServerError("SetCustomFieldValue", err)
return
}
ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, issue.Index), http.StatusSeeOther)
}
+65 -5
View File
@@ -4,6 +4,7 @@
package repo
import (
"encoding/json"
"errors"
"fmt"
"html/template"
@@ -36,10 +37,11 @@ import (
)
// Tries to load and set an issue template. The first return value indicates if a template was loaded.
func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles []string, metaData *IssuePageMetaData) (bool, map[string]error) {
// The third return value contains the template's custom_fields map (field name → default value).
func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles []string, metaData *IssuePageMetaData) (bool, map[string]error, map[string]string) {
commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
if err != nil {
return false, nil
return false, nil, nil
}
templateCandidates := make([]string, 0, 1+len(possibleFiles))
@@ -84,9 +86,9 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles
}
metaData.AssigneesData.SelectedAssigneeIDs = strings.Join(selectedAssigneeIDStrings, ",")
return true, templateErrs
return true, templateErrs, template.CustomFields
}
return false, templateErrs
return false, templateErrs, nil
}
// NewIssue render creating issue page
@@ -128,7 +130,7 @@ func NewIssue(ctx *context.Context) {
ctx.Data["Tags"] = tags
ret := issue_service.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
templateLoaded, errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates, pageMetaData)
templateLoaded, errs, templateCustomFields := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates, pageMetaData)
maps.Copy(ret.TemplateErrors, errs)
if ctx.Written() {
return
@@ -140,6 +142,35 @@ func NewIssue(ctx *context.Context) {
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.Permission.CanWrite(unit.TypeIssues)
// 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)
if cfErr != nil {
log.Error("NewIssue: GetCustomFieldsByOwner: %v", cfErr)
}
ctx.Data["CustomFieldDefs"] = customFieldDefs
customFieldValues := make(map[int64]string)
fieldOptions := make(map[int64][]string)
if len(customFieldDefs) > 0 {
// Resolve template custom_fields (name → value) to field IDs.
if len(templateCustomFields) > 0 {
for _, def := range customFieldDefs {
if val, ok := templateCustomFields[def.Name]; ok {
customFieldValues[def.ID] = val
}
}
}
for _, f := range customFieldDefs {
if f.Options != "" {
var opts []string
if err := json.Unmarshal([]byte(f.Options), &opts); err == nil {
fieldOptions[f.ID] = opts
}
}
}
}
ctx.Data["CustomFieldValues"] = customFieldValues
ctx.Data["CustomFieldOptions"] = fieldOptions
if !issueConfig.BlankIssuesEnabled && hasTemplates && !templateLoaded {
// The "issues/new" and "issues/new/choose" share the same query parameters "project" and "milestone", if blank issues are disabled, just redirect to the "issues/choose" page with these parameters.
ctx.Redirect(fmt.Sprintf("%s/issues/new/choose?%s", ctx.Repo.Repository.Link(), ctx.Req.URL.RawQuery), http.StatusSeeOther)
@@ -377,6 +408,9 @@ func NewIssuePost(ctx *context.Context) {
return
}
// Save custom field values submitted from the new issue form.
saveCustomFieldsFromForm(ctx, repo.OwnerID, issue.ID)
log.Trace("Issue created: %d/%d", repo.ID, issue.ID)
if ctx.FormString("redirect_after_creation") == "project" && len(projectIDs) > 0 {
// When issue is in multiple projects, redirect to first project from form order.
@@ -392,3 +426,29 @@ func NewIssuePost(ctx *context.Context) {
}
ctx.JSONRedirect(issue.Link())
}
// saveCustomFieldsFromForm reads custom field values from the form
// (submitted as "custom-field-{fieldID}") and persists them for the issue.
func saveCustomFieldsFromForm(ctx *context.Context, ownerID, issueID int64) {
defs, err := issues_model.GetCustomFieldsByOwner(ctx, ownerID, issues_model.CustomFieldScopeIssue)
if err != nil {
log.Error("saveCustomFieldsFromForm: GetCustomFieldsByOwner: %v", err)
return
}
if len(defs) == 0 {
return
}
vals := make(map[int64]string)
for _, def := range defs {
v := ctx.Req.FormValue(fmt.Sprintf("custom-field-%d", def.ID))
if v != "" {
vals[def.ID] = v
}
}
if len(vals) > 0 {
if err := issues_model.SetCustomFieldValues(ctx, issueID, vals); err != nil {
log.Error("saveCustomFieldsFromForm: %v", err)
ctx.Flash.Error("Failed to save custom field values")
}
}
}
+27
View File
@@ -4,6 +4,7 @@
package repo
import (
"encoding/json"
"fmt"
"math/big"
"net/http"
@@ -337,6 +338,32 @@ func ViewIssue(ctx *context.Context) {
ctx.Data["IsProjectsEnabled"] = ctx.Repo.Permission.CanRead(unit.TypeProjects)
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
// Load custom fields for the issue sidebar (org-level issue-scoped fields).
customFieldDefs, cfErr := issues_model.GetCustomFieldsByOwner(ctx, ctx.Repo.Repository.OwnerID, issues_model.CustomFieldScopeIssue)
if cfErr != nil {
log.Error("ViewIssue: GetCustomFieldsByOwner: %v", cfErr)
}
ctx.Data["CustomFieldDefs"] = customFieldDefs
customFieldValues := make(map[int64]string)
fieldOptions := make(map[int64][]string)
if len(customFieldDefs) > 0 {
var cvErr error
customFieldValues, cvErr = issues_model.GetCustomFieldValuesMap(ctx, issue.ID)
if cvErr != nil {
log.Error("ViewIssue: GetCustomFieldValuesMap: %v", cvErr)
}
for _, f := range customFieldDefs {
if f.Options != "" {
var opts []string
if err := json.Unmarshal([]byte(f.Options), &opts); err == nil {
fieldOptions[f.ID] = opts
}
}
}
}
ctx.Data["CustomFieldValues"] = customFieldValues
ctx.Data["CustomFieldOptions"] = fieldOptions
upload.AddUploadContext(ctx, "comment")
if err := issue.LoadAttributes(ctx); err != nil {
+81 -13
View File
@@ -90,6 +90,10 @@ func Licenses(ctx *context.Context) {
}
}
// Always load the master key for display (prefix + status).
masterKey, _ := licenses.GetMasterKey(ctx, ownerID)
ctx.Data["MasterKey"] = masterKey
pkgs, err := licenses.ListLicensePackages(ctx, ownerID)
if err != nil {
ctx.ServerError("ListLicensePackages", err)
@@ -183,15 +187,16 @@ func LicensesCreatePackage(ctx *context.Context) {
}
pkg := &licenses.LicensePackage{
OwnerID: ctx.Repo.Repository.OwnerID,
Name: name,
Description: ctx.FormString("description"),
DurationDays: durationDays,
MaxSites: maxSites,
DomainLockHours: domainLockHours,
AllowedChannels: allowedChannels,
RepoScope: repoScope,
IsActive: true,
OwnerID: ctx.Repo.Repository.OwnerID,
Name: name,
Description: ctx.FormString("description"),
DurationDays: durationDays,
MaxSites: maxSites,
DomainLockHours: domainLockHours,
AllowedChannels: allowedChannels,
DomainRestriction: strings.TrimSpace(ctx.FormString("domain_restriction")),
RepoScope: repoScope,
IsActive: true,
}
if err := licenses.CreateLicensePackage(ctx, pkg); err != nil {
@@ -203,9 +208,62 @@ func LicensesCreatePackage(ctx *context.Context) {
ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
}
// LicensesRegenerateMasterKey handles POST to regenerate the master key.
func LicensesRegenerateMasterKey(ctx *context.Context) {
ownerID := ctx.Repo.Repository.OwnerID
// Deactivate the old master key.
oldKey, _ := licenses.GetMasterKey(ctx, ownerID)
if oldKey != nil {
oldKey.IsActive = false
_ = licenses.UpdateLicenseKey(ctx, oldKey)
}
// Find the master package.
pkgs, err := licenses.ListLicensePackages(ctx, ownerID)
if err != nil {
ctx.ServerError("ListLicensePackages", err)
return
}
var masterPkg *licenses.LicensePackage
for _, pkg := range pkgs {
if pkg.Name == licenses.MasterPackageName {
masterPkg = pkg
break
}
}
if masterPkg == nil {
ctx.Flash.Error("Master package not found")
ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
return
}
// Create a new master key.
newKey := &licenses.LicenseKey{
PackageID: masterPkg.ID,
OwnerID: ownerID,
IsInternal: true,
IsActive: true,
}
rawKey, err := licenses.CreateLicenseKey(ctx, newKey)
if err != nil {
ctx.ServerError("CreateLicenseKey", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.licenses.master_key_regenerated"))
ctx.Data["NewMasterKey"] = rawKey
Licenses(ctx)
}
// LicensesGenerateKey handles POST to generate a new key from a package.
func LicensesGenerateKey(ctx *context.Context) {
packageID, _ := strconv.ParseInt(ctx.FormString("package_id"), 10, 64)
// Accept package_id from form body or query string (modal sets it via form action URL).
pkgIDStr := ctx.FormString("package_id")
if pkgIDStr == "" {
pkgIDStr = ctx.Req.URL.Query().Get("package_id")
}
packageID, _ := strconv.ParseInt(pkgIDStr, 10, 64)
if packageID == 0 {
ctx.Flash.Error("Invalid package")
ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
@@ -218,10 +276,19 @@ func LicensesGenerateKey(ctx *context.Context) {
return
}
// Domain restriction: use form input, fall back to package default.
domainRestriction := strings.TrimSpace(ctx.FormString("domain_restriction"))
if domainRestriction == "" && pkg.DomainRestriction != "" {
domainRestriction = pkg.DomainRestriction
}
key := &licenses.LicenseKey{
PackageID: packageID,
OwnerID: ctx.Repo.Repository.OwnerID,
IsActive: true,
PackageID: packageID,
OwnerID: ctx.Repo.Repository.OwnerID,
IsActive: true,
DomainRestriction: domainRestriction,
LicenseeName: strings.TrimSpace(ctx.FormString("licensee_name")),
LicenseeEmail: strings.TrimSpace(ctx.FormString("licensee_email")),
}
// Auto-calculate expiry from package duration.
@@ -448,6 +515,7 @@ func LicensesEditPackagePost(ctx *context.Context) {
pkg.AllowedChannels = ""
}
pkg.DomainRestriction = strings.TrimSpace(ctx.FormString("domain_restriction"))
pkg.IsActive = ctx.FormString("is_active") == "on"
if err := licenses.UpdateLicensePackage(ctx, pkg); err != nil {
+21
View File
@@ -0,0 +1,21 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package setting
import (
"net/http"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
const tplSettingsAdvanced templates.TplName = "repo/settings/advanced"
// AdvancedSettings displays the advanced (feature units) settings page.
func AdvancedSettings(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.advanced_settings")
ctx.Data["PageIsSettingsOptions"] = false
ctx.Data["PageIsSettingsAdvanced"] = true
ctx.HTML(http.StatusOK, tplSettingsAdvanced)
}
+114
View File
@@ -0,0 +1,114 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package setting
import (
"net/http"
"strconv"
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
const tplSettingsCustomFields templates.TplName = "repo/settings/custom_fields"
// CustomFields displays the custom fields settings page.
func CustomFields(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.custom_fields")
ctx.Data["PageIsSettingsCustomFields"] = true
fields, err := issues_model.GetAllCustomFieldsByRepo(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("GetAllCustomFieldsByRepo", err)
return
}
ctx.Data["CustomFields"] = fields
ctx.HTML(http.StatusOK, tplSettingsCustomFields)
}
// CustomFieldsCreatePost creates a new custom field definition.
func CustomFieldsCreatePost(ctx *context.Context) {
sortOrder, _ := strconv.Atoi(ctx.FormString("sort_order"))
field := &issues_model.CustomFieldDef{
RepoID: ctx.Repo.Repository.ID,
Name: ctx.FormString("name"),
FieldType: issues_model.CustomFieldType(ctx.FormString("field_type")),
Description: ctx.FormString("description"),
Options: ctx.FormString("options"),
Required: ctx.FormString("required") == "on",
SortOrder: sortOrder,
IsActive: true,
}
if field.Name == "" {
ctx.Flash.Error("Field name is required")
ctx.Redirect(ctx.Repo.RepoLink + "/settings/custom-fields")
return
}
if err := issues_model.CreateCustomFieldDef(ctx, field); err != nil {
ctx.ServerError("CreateCustomFieldDef", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.custom_field_created"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/custom-fields")
}
// CustomFieldsEditPost updates a custom field definition.
func CustomFieldsEditPost(ctx *context.Context) {
id := ctx.PathParamInt64("id")
field, err := issues_model.GetCustomFieldDefByID(ctx, id)
if err != nil {
ctx.ServerError("GetCustomFieldDefByID", err)
return
}
if field.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound(nil)
return
}
field.Name = ctx.FormString("name")
field.FieldType = issues_model.CustomFieldType(ctx.FormString("field_type"))
field.Description = ctx.FormString("description")
field.Options = ctx.FormString("options")
field.Required = ctx.FormString("required") == "on"
field.IsActive = ctx.FormString("is_active") == "on"
sortOrder, _ := strconv.Atoi(ctx.FormString("sort_order"))
field.SortOrder = sortOrder
if err := issues_model.UpdateCustomFieldDef(ctx, field); err != nil {
ctx.ServerError("UpdateCustomFieldDef", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.custom_field_updated"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/custom-fields")
}
// CustomFieldsDeletePost deletes a custom field definition.
func CustomFieldsDeletePost(ctx *context.Context) {
id := ctx.PathParamInt64("id")
field, err := issues_model.GetCustomFieldDefByID(ctx, id)
if err != nil {
ctx.ServerError("GetCustomFieldDefByID", err)
return
}
if field.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound(nil)
return
}
if err := issues_model.DeleteCustomFieldDef(ctx, id); err != nil {
ctx.ServerError("DeleteCustomFieldDef", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.custom_field_deleted"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/custom-fields")
}
+62
View File
@@ -0,0 +1,62 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package setting
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"
)
const tplSettingsLicensing templates.TplName = "repo/settings/licensing"
// LicensingSettings displays the licensing settings page.
func LicensingSettings(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.licensing_section")
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.
func LicensingSettingsPost(ctx *context.Context) {
repo := ctx.Repo.Repository
updatePlatform := ctx.FormString("update_platform")
if updatePlatform == "" {
updatePlatform = "joomla"
}
updateCfg := &licenses_model.UpdateStreamConfig{
OwnerID: repo.OwnerID,
RepoID: repo.ID,
Platform: updatePlatform,
LicensingEnabled: ctx.FormString("enable_licensing") == "on",
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")
}
+64
View File
@@ -0,0 +1,64 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package setting
import (
"encoding/json"
"fmt"
"net/http"
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
const tplSettingsMetadata templates.TplName = "repo/settings/metadata"
// Metadata displays the repo metadata page (repo-scoped custom field values).
func Metadata(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.metadata")
ctx.Data["PageIsSettingsMetadata"] = true
ownerID := ctx.Repo.Repository.OwnerID
repoID := ctx.Repo.Repository.ID
fields, _ := issues_model.GetCustomFieldsByOwner(ctx, ownerID, issues_model.CustomFieldScopeRepo)
ctx.Data["CustomFieldDefs"] = fields
values := make(map[int64]string)
fieldOptions := make(map[int64][]string)
if len(fields) > 0 {
values, _ = issues_model.GetCustomFieldValuesMap(ctx, repoID)
for _, f := range fields {
if f.Options != "" {
var opts []string
if err := json.Unmarshal([]byte(f.Options), &opts); err == nil {
fieldOptions[f.ID] = opts
}
}
}
}
ctx.Data["CustomFieldValues"] = values
ctx.Data["CustomFieldOptions"] = fieldOptions
ctx.HTML(http.StatusOK, tplSettingsMetadata)
}
// MetadataPost saves repo-scoped custom field values.
func MetadataPost(ctx *context.Context) {
repoID := ctx.Repo.Repository.ID
ownerID := ctx.Repo.Repository.OwnerID
fields, _ := issues_model.GetCustomFieldsByOwner(ctx, ownerID, issues_model.CustomFieldScopeRepo)
for _, f := range fields {
val := ctx.Req.FormValue(fmt.Sprintf("field_%d", f.ID))
if err := issues_model.SetCustomFieldValue(ctx, repoID, f.ID, val); err != nil {
ctx.ServerError("SetCustomFieldValue", err)
return
}
}
ctx.Flash.Success(ctx.Tr("repo.settings.metadata_saved"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/metadata")
}
+25 -11
View File
@@ -703,7 +703,7 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
log.Trace("Repository advanced settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
ctx.Redirect(ctx.Repo.RepoLink + "/settings/advanced")
}
func handleSettingsPostSigning(ctx *context.Context) {
@@ -1055,26 +1055,40 @@ func handleSettingsPostVisibility(ctx *context.Context) {
return
}
private := ctx.FormOptionalBool("private").ValueOrDefault(true) // default to true for privacy & safety
visibility := ctx.FormString("visibility")
// Backward compat: if old "private" field is sent instead of "visibility"
if visibility == "" {
private := ctx.FormOptionalBool("private").ValueOrDefault(true)
if private {
visibility = "private"
} else {
visibility = "public"
}
}
// System repos (dot-prefixed) cannot be made public, regardless of user role.
if !private && repo.IsSystemRepo() {
isPrivate := visibility == "private" || visibility == "hidden"
isHidden := visibility == "hidden"
if !isPrivate && repo.IsSystemRepo() {
ctx.JSONError(ctx.Tr("repo.settings.visibility.system_repo_private"))
return
}
// when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public
if !private && setting.Repository.ForcePrivate && !ctx.Doer.IsAdmin {
if !isPrivate && setting.Repository.ForcePrivate && !ctx.Doer.IsAdmin {
ctx.JSONError(ctx.Tr("form.repository_force_private"))
return
}
if private && repo.FullName() != ctx.FormString("confirm_repo_name") {
ctx.JSONError(ctx.Tr("form.enterred_invalid_repo_name"))
err := repo_service.MakeRepoPrivate(ctx, repo, isPrivate)
if err != nil {
log.Error("Tried to change the visibility of the repo: %s", err)
ctx.JSONError(ctx.Tr("repo.settings.visibility.error"))
return
}
err := repo_service.MakeRepoPrivate(ctx, repo, private)
if err != nil {
log.Error("Tried to change the visibility of the repo: %s", err)
// Update IsHidden separately.
repo.IsHidden = isHidden
if err := repo_model.UpdateRepositoryColsWithAutoTime(ctx, repo, "is_hidden"); err != nil {
log.Error("Failed to update is_hidden: %s", err)
ctx.JSONError(ctx.Tr("repo.settings.visibility.error"))
return
}
+135 -25
View File
@@ -27,30 +27,8 @@ func validateUpdateKey(ctx *context.Context) (allowedChannels []string, ok bool,
}
if rawKey == "" {
cfg := licenses.GetEffectiveConfig(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID)
feedVis := "public"
requireKey := false
if cfg != nil {
requireKey = cfg.RequireKey
if cfg.FeedVisibility != "" {
feedVis = cfg.FeedVisibility
}
}
if requireKey {
switch feedVis {
case "hidden":
// Fully hidden — return empty feed.
return nil, false, false
case "no-download":
// Show versions but strip download URLs.
return nil, true, true
default:
// "public" with RequireKey — still hide feed (backward compat).
return nil, false, false
}
}
// No key required — allow public access (all channels).
// Feed is always public — shows versions and download URLs.
// Actual file downloads are gated by CheckDownloadGating, not the feed.
return nil, true, false
}
@@ -116,7 +94,9 @@ func ServeUpdatesXML(ctx *context.Context) {
}
repoCfg, _ := licenses.GetRepoConfig(ctx, ctx.Repo.Repository.ID)
requireKey := repoCfg != nil && repoCfg.RequireKey
// Show <downloadkey> only when downloads are gated (prerelease or all).
// No gating = no license keys needed = no downloadkey element.
requireKey := repoCfg != nil && repoCfg.DownloadGating != "" && repoCfg.DownloadGating != "none"
xmlData, err := updateserver.GenerateJoomlaXML(ctx, ctx.Repo.Repository, requireKey, stripDownloads, allowedChannels...)
if err != nil {
@@ -211,3 +191,133 @@ func ServeWordPressJSON(ctx *context.Context) {
ctx.Resp.WriteHeader(http.StatusOK)
_, _ = ctx.Resp.Write(jsonData)
}
// ServeComposerJSON generates and serves a Composer packages.json feed.
func ServeComposerJSON(ctx *context.Context) {
platform := ctx.Data["RepoUpdatePlatform"]
if platform != "composer" {
ctx.NotFound(nil)
return
}
_, ok, _ := validateUpdateKey(ctx)
if !ok {
ctx.Resp.Header().Set("Content-Type", "application/json; charset=utf-8")
ctx.Resp.WriteHeader(http.StatusOK)
_, _ = ctx.Resp.Write([]byte(`{"packages":{}}`))
return
}
licenseKey := ctx.FormString("key")
if licenseKey == "" {
licenseKey = ctx.FormString("dlid")
}
data, err := updateserver.GenerateComposerJSON(ctx, ctx.Repo.Repository, licenseKey)
if err != nil {
ctx.ServerError("GenerateComposerJSON", err)
return
}
jsonData, err := json.MarshalIndent(data, "", " ")
if err != nil {
ctx.ServerError("json.Marshal", err)
return
}
ctx.Resp.Header().Set("Content-Type", "application/json; charset=utf-8")
ctx.Resp.WriteHeader(http.StatusOK)
_, _ = ctx.Resp.Write(jsonData)
}
// ServePrestaShopXML generates and serves a PrestaShop module update XML.
func ServePrestaShopXML(ctx *context.Context) {
platform := ctx.Data["RepoUpdatePlatform"]
if platform != "prestashop" {
ctx.NotFound(nil)
return
}
allowedChannels, ok, _ := validateUpdateKey(ctx)
if !ok {
ctx.Resp.Header().Set("Content-Type", "application/xml; charset=utf-8")
ctx.Resp.WriteHeader(http.StatusOK)
_, _ = ctx.Resp.Write([]byte(`<?xml version="1.0" encoding="UTF-8"?><modules></modules>`))
return
}
xmlData, err := updateserver.GeneratePrestaShopXML(ctx, ctx.Repo.Repository, allowedChannels...)
if err != nil {
ctx.ServerError("GeneratePrestaShopXML", err)
return
}
ctx.Resp.Header().Set("Content-Type", "application/xml; charset=utf-8")
ctx.Resp.WriteHeader(http.StatusOK)
_, _ = ctx.Resp.Write(xmlData)
}
// ServeDrupalXML generates and serves a Drupal update status XML.
func ServeDrupalXML(ctx *context.Context) {
platform := ctx.Data["RepoUpdatePlatform"]
if platform != "drupal" {
ctx.NotFound(nil)
return
}
allowedChannels, ok, _ := validateUpdateKey(ctx)
if !ok {
ctx.Resp.Header().Set("Content-Type", "application/xml; charset=utf-8")
ctx.Resp.WriteHeader(http.StatusOK)
_, _ = ctx.Resp.Write([]byte(`<?xml version="1.0" encoding="UTF-8"?><project></project>`))
return
}
xmlData, err := updateserver.GenerateDrupalXML(ctx, ctx.Repo.Repository, allowedChannels...)
if err != nil {
ctx.ServerError("GenerateDrupalXML", err)
return
}
ctx.Resp.Header().Set("Content-Type", "application/xml; charset=utf-8")
ctx.Resp.WriteHeader(http.StatusOK)
_, _ = ctx.Resp.Write(xmlData)
}
// ServeWHMCSJSON generates and serves a WHMCS module update JSON.
func ServeWHMCSJSON(ctx *context.Context) {
platform := ctx.Data["RepoUpdatePlatform"]
if platform != "whmcs" {
ctx.NotFound(nil)
return
}
_, ok, _ := validateUpdateKey(ctx)
if !ok {
ctx.Resp.Header().Set("Content-Type", "application/json; charset=utf-8")
ctx.Resp.WriteHeader(http.StatusOK)
_, _ = ctx.Resp.Write([]byte(`{"name":"","version":"0.0.0"}`))
return
}
licenseKey := ctx.FormString("key")
if licenseKey == "" {
licenseKey = ctx.FormString("dlid")
}
data, err := updateserver.GenerateWHMCSJSON(ctx, ctx.Repo.Repository, licenseKey)
if err != nil {
ctx.ServerError("GenerateWHMCSJSON", err)
return
}
jsonData, err := json.MarshalIndent(data, "", " ")
if err != nil {
ctx.ServerError("json.Marshal", err)
return
}
ctx.Resp.Header().Set("Content-Type", "application/json; charset=utf-8")
ctx.Resp.WriteHeader(http.StatusOK)
_, _ = ctx.Resp.Write(jsonData)
}
+20 -3
View File
@@ -1061,6 +1061,12 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Get("", org.SettingsUpdateStreams)
m.Post("", org.SettingsUpdateStreamsPost)
})
m.Group("/custom-fields", func() {
m.Get("", org.SettingsCustomFields)
m.Post("", org.SettingsCustomFieldsCreatePost)
m.Post("/{id}/edit", org.SettingsCustomFieldsEditPost)
m.Post("/{id}/delete", org.SettingsCustomFieldsDeletePost)
})
}, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "PageIsOrgSettings", true))
}, context.OrgAssignment(context.OrgAssignmentOptions{RequireOwner: true}))
}, reqSignIn)
@@ -1183,6 +1189,11 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Post("/avatar/delete", repo_setting.SettingsDeleteAvatar)
m.Combo("/public_access").Get(repo_setting.PublicAccess).Post(repo_setting.PublicAccessPost)
m.Group("", func() {
m.Combo("/advanced").Get(repo_setting.AdvancedSettings).Post(web.Bind(forms.RepoSettingForm{}), repo_setting.SettingsPost)
}, repo_setting.SettingsCtxData)
m.Combo("/licensing").Get(repo_setting.LicensingSettings).Post(repo_setting.LicensingSettingsPost)
m.Combo("/metadata").Get(repo_setting.Metadata).Post(repo_setting.MetadataPost)
m.Group("/collaboration", func() {
m.Combo("").Get(repo_setting.Collaboration).Post(repo_setting.CollaborationPost)
@@ -1387,6 +1398,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Post("/projects/column", reqRepoIssuesOrPullsWriter, reqRepoProjectsWriter, repo.UpdateIssueProjectColumn)
m.Post("/assignee", reqRepoIssuesOrPullsWriter, repo.UpdateIssueAssignee)
m.Post("/status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueStatus)
m.Post("/{id}/custom-fields/{field_id}", reqRepoIssuesOrPullsWriter, repo.UpdateIssueCustomField)
m.Post("/delete", reqRepoAdmin, repo.BatchDeleteIssues)
m.Delete("/unpin/{index}", reqRepoAdmin, repo.IssueUnpin)
m.Post("/move_pin", reqRepoAdmin, repo.IssuePinMove)
@@ -1519,8 +1531,12 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Get("/updates.xml", repo.ServeUpdatesXML)
m.Get("/updates/dolibarr.json", repo.ServeDolibarrJSON)
m.Get("/updates/wordpress.json", repo.ServeWordPressJSON)
m.Get("/updates/packages.json", repo.ServeComposerJSON)
m.Get("/updates/prestashop.xml", repo.ServePrestaShopXML)
m.Get("/updates/drupal.xml", repo.ServeDrupalXML)
m.Get("/updates/whmcs.json", repo.ServeWHMCSJSON)
m.Get("/changelog.xml", repo.ServeChangelogXML)
}, optSignIn, context.RepoAssignment)
}, context.RepoAssignmentPublicFeed())
// end "/{username}/{reponame}": update server
// "/{username}/{reponame}": licenses page
@@ -1535,12 +1551,13 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Post("/packages/{id}/archive", repo.LicensesArchivePackage)
m.Post("/packages/{id}/unarchive", repo.LicensesUnarchivePackage)
m.Post("/keys/generate", repo.LicensesGenerateKey)
m.Post("/master-key/regenerate", repo.LicensesRegenerateMasterKey)
m.Get("/keys/{id}/edit", repo.LicensesEditKey)
m.Post("/keys/{id}/edit", repo.LicensesEditKeyPost)
m.Post("/keys/{id}/revoke", repo.LicensesRevokeKey)
m.Post("/keys/{id}/renew", repo.LicensesRenewKey)
m.Post("/keys/{id}/delete", repo.LicensesDeleteKey)
}, optSignIn, context.RepoAssignment)
}, reqSignIn, context.RepoAssignment)
// end "/{username}/{reponame}": licenses
m.Group("/{username}/{reponame}", func() { // to maintain compatibility with old attachments
@@ -1620,7 +1637,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Group("/workflows/{workflow_name}", func() {
m.Get("/badge.svg", webAuth.AllowBasic, webAuth.AllowOAuth2, actions.GetWorkflowBadge)
})
}, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoActionsReader, actions.MustEnableActions)
}, reqSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoActionsReader, actions.MustEnableActions)
// end "/{username}/{reponame}/actions"
m.Group("/{username}/{reponame}/wiki", func() {
+21
View File
@@ -169,6 +169,27 @@ func (ctx *Context) notFoundInternal(logMsg string, logErr error) {
ctx.HTML(http.StatusNotFound, "status/404")
}
// Forbidden displays a styled 403 (Access Denied) page, matching the 404 page layout.
func (ctx *Context) Forbidden() {
showHTML := false
for _, part := range ctx.Req.Header["Accept"] {
if strings.Contains(part, "text/html") {
showHTML = true
break
}
}
if !showHTML {
ctx.plainTextInternal(3, http.StatusForbidden, []byte("Access denied.\n"))
return
}
ctx.Data["IsRepo"] = ctx.Repo.Repository != nil
ctx.Data["Title"] = "Access Denied"
ctx.Data["CurrentURL"] = ctx.Req.URL.RequestURI()
ctx.HTML(http.StatusForbidden, "status/403")
}
// ServerError displays a 500 (Internal Server Error) page and prints the given error, if any.
// If the error is controlled by our error system, a related 404 page can be displayed instead.
func (ctx *Context) ServerError(logMsg string, logErr error) {
+4
View File
@@ -78,6 +78,10 @@ func RequireUnitWriter(unitTypes ...unit.Type) func(ctx *Context) {
// RequireUnitReader returns a middleware for requiring repository write to one of the unit permission
func RequireUnitReader(unitTypes ...unit.Type) func(ctx *Context) {
return func(ctx *Context) {
// Licensed read-only mode grants read access to all units.
if ctx.Data["LicensedReadOnly"] == true {
return
}
for _, unitType := range unitTypes {
if ctx.Repo.Permission.CanRead(unitType) {
return
+71 -5
View File
@@ -435,8 +435,53 @@ func repoAssignmentLegacy(ctx *Context, data *repoAssignmentPrepareDataStruct) {
EarlyResponseForGoGetMeta(ctx)
return
}
ctx.NotFound(nil)
return
// Check if licensing is enabled — licensed repos allow access to
// releases and downloads via license key, even without membership.
orgCfg, _ := licenses_model.GetOrgConfig(ctx, repo.OwnerID)
repoCfg, _ := licenses_model.GetRepoConfig(ctx, repo.ID)
licensingEnabled := (orgCfg != nil && orgCfg.LicensingEnabled) ||
(repoCfg != nil && repoCfg.LicensingEnabled)
if licensingEnabled {
// Check if a license key is provided in query params (for Joomla/WP clients).
hasKey := ctx.FormString("dlid") != "" || ctx.FormString("key") != "" || ctx.FormString("download_key") != ""
// Check if downloads are set to public (download_gating=none means no key needed).
// Only apply to release/download paths, not the main repo page.
downloadsPublic := false
reqPath := ctx.Req.URL.Path
isDownloadPath := strings.Contains(reqPath, "/releases/") || strings.Contains(reqPath, "/archive/")
if isDownloadPath {
// Allow anonymous access to download paths — the actual gating
// is done by CheckDownloadGating in the handler, which checks
// the stream (stable vs prerelease) and validates the key.
// RepoAssignment just needs to let the request through.
downloadsPublic = true
}
if ctx.IsSigned || hasKey || downloadsPublic {
// Grant read-only access — downloads gated by CheckDownloadGating handler.
ctx.Data["LicensingEnabled"] = licensingEnabled
ctx.Data["HideReleaseDownloads"] = !hasKey && !ctx.IsSigned
ctx.Data["LicensedReadOnly"] = true
// Continue — don't block access.
} else if repo.IsHidden {
// Hidden repo: 404 — pretend it doesn't exist.
ctx.NotFound(nil)
return
} else {
// Private repo: 403 — access denied with styled page.
ctx.Forbidden()
return
}
} else if repo.IsHidden {
ctx.NotFound(nil)
return
} else {
ctx.Forbidden()
return
}
}
ctx.Data["Permission"] = &ctx.Repo.Permission
@@ -618,6 +663,8 @@ func repoAssignmentPrepareTemplateData(ctx *Context, data *repoAssignmentPrepare
ctx.Data["NumLicensePackages"] = numLicensePackages
ctx.Data["EnableLicenses"] = licensingEnabled || numLicensePackages > 0
ctx.Data["LicensingEnabled"] = licensingEnabled
downloadGated := repoUpdateCfg != nil && repoUpdateCfg.DownloadGating != "" && repoUpdateCfg.DownloadGating != "none"
ctx.Data["DownloadGated"] = downloadGated
// Determine release page access based on feed visibility mode.
feedVis := "public"
@@ -628,10 +675,29 @@ func repoAssignmentPrepareTemplateData(ctx *Context, data *repoAssignmentPrepare
feedVis = repoUpdateCfg.FeedVisibility
}
ctx.Data["FeedVisibility"] = feedVis
// Only "hidden" mode requires login. "no-download" shows page but hides files.
// Only "hidden" mode requires login for the page itself.
ctx.Data["ReleasesRequireLogin"] = licensingEnabled && feedVis == "hidden"
// Hide download attachments for anonymous users in "no-download" mode.
ctx.Data["HideReleaseDownloads"] = licensingEnabled && feedVis == "no-download" && !ctx.IsSigned
// Determine download gating mode.
downloadGating := "none"
if orgCfg != nil && orgCfg.DownloadGating != "" {
downloadGating = orgCfg.DownloadGating
}
if repoUpdateCfg != nil && repoUpdateCfg.DownloadGating != "" {
downloadGating = repoUpdateCfg.DownloadGating
}
ctx.Data["DownloadGating"] = downloadGating
// Hide download links on release page when:
// - licensing enabled AND feed visibility is "no-download" (anonymous only), OR
// - licensing enabled AND download gating is active AND user not signed in
hideDownloads := false
if licensingEnabled && !ctx.IsSigned {
if feedVis == "no-download" || feedVis == "hidden" || downloadGating != "none" {
hideDownloads = true
}
}
ctx.Data["HideReleaseDownloads"] = hideDownloads
ctx.Data["IsRepoAdmin"] = ctx.Repo.Permission.IsAdmin()
ctx.Data["IsSiteAdmin"] = ctx.IsUserSiteAdmin()
+56
View File
@@ -0,0 +1,56 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package context
import (
licenses_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
)
// RepoAssignmentPublicFeed is a lightweight repo loader for update feed endpoints.
// It loads the repo and owner without checking user permissions — the feed
// handlers gate access via license keys instead of repo membership.
// This allows private repos to serve update feeds to anonymous clients.
func RepoAssignmentPublicFeed() func(ctx *Context) {
return func(ctx *Context) {
ownerName := ctx.PathParam("username")
repoName := ctx.PathParam("reponame")
owner, err := user_model.GetUserByName(ctx, ownerName)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.NotFound(err)
} else {
ctx.ServerError("GetUserByName", err)
}
return
}
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, ownerName, repoName)
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
ctx.NotFound(err)
} else {
ctx.ServerError("GetRepositoryByOwnerAndName", err)
}
return
}
repo.Owner = owner
ctx.Repo.Repository = repo
// Load update config for platform-aware routing.
repoUpdateCfg, _ := licenses_model.GetRepoConfig(ctx, repo.ID)
if repoUpdateCfg != nil {
ctx.Data["RepoUpdatePlatform"] = repoUpdateCfg.Platform
} else {
ctx.Data["RepoUpdatePlatform"] = "joomla"
}
log.Trace("Public feed access: %s/%s", ownerName, repoName)
}
}
+6 -5
View File
@@ -696,11 +696,12 @@ func (f *GenerateReleaseNotesForm) Validate(req *http.Request, errs binding.Erro
// EditReleaseForm form for changing release
type EditReleaseForm struct {
Title string `form:"title" binding:"Required;MaxSize(255)"`
Content string `form:"content"`
Draft string `form:"draft"`
Prerelease bool `form:"prerelease"`
Files []string
Title string `form:"title" binding:"Required;MaxSize(255)"`
Content string `form:"content"`
Draft string `form:"draft"`
Prerelease bool `form:"prerelease"`
Files []string
UpdateStream string
}
// Validate validates the fields
+173
View File
@@ -0,0 +1,173 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package updateserver
import (
"context"
"fmt"
"strings"
"time"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
)
// ComposerPackage represents the packages.json response for Composer/Packagist.
type ComposerPackage struct {
Packages map[string]map[string]ComposerVersion `json:"packages"`
}
// ComposerVersion represents a single version entry.
type ComposerVersion struct {
Name string `json:"name"`
Version string `json:"version"`
Type string `json:"type,omitempty"`
Description string `json:"description,omitempty"`
Homepage string `json:"homepage,omitempty"`
License []string `json:"license,omitempty"`
Authors []ComposerAuthor `json:"authors,omitempty"`
Dist *ComposerDist `json:"dist,omitempty"`
Require map[string]string `json:"require,omitempty"`
Time string `json:"time,omitempty"`
}
// ComposerAuthor represents a package author.
type ComposerAuthor struct {
Name string `json:"name"`
Homepage string `json:"homepage,omitempty"`
}
// ComposerDist represents a distribution archive.
type ComposerDist struct {
URL string `json:"url"`
Type string `json:"type"`
Shasum string `json:"shasum,omitempty"`
Reference string `json:"reference,omitempty"`
}
// GenerateComposerJSON builds a Composer packages.json from repo releases.
func GenerateComposerJSON(ctx context.Context, repo *repo_model.Repository, licenseKey string) (*ComposerPackage, error) {
releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
RepoID: repo.ID,
ListOptions: db.ListOptionsAll,
IncludeDrafts: false,
IncludeTags: false,
})
if err != nil {
return nil, fmt.Errorf("FindReleases: %w", err)
}
if err := repo.LoadOwner(ctx); err != nil {
return nil, fmt.Errorf("LoadOwner: %w", err)
}
baseURL := strings.TrimSuffix(setting.AppURL, "/")
repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name)
cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
meta := resolveExtensionMetadata(ctx, repo, cfg)
// Composer package name: vendor/package (override with resolved extension name if set)
packageName := fmt.Sprintf("%s/%s", strings.ToLower(repo.Owner.Name), strings.ToLower(repo.Name))
if meta.Element != strings.ToLower(repo.Name) {
packageName = meta.Element
}
description := meta.Description
maintainer := repo.Owner.Name
maintainerURL := fmt.Sprintf("%s/%s", baseURL, repo.Owner.Name)
if cfg != nil && cfg.Maintainer != "" {
maintainer = cfg.Maintainer
}
if cfg != nil && cfg.MaintainerURL != "" {
maintainerURL = cfg.MaintainerURL
}
phpMin := ""
if meta.PHPMinimum != "" {
phpMin = ">=" + meta.PHPMinimum
}
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
versions := make(map[string]ComposerVersion)
for _, rel := range releases {
if rel.IsDraft || rel.IsTag {
continue
}
ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
if ch != "stable" {
continue // Composer only serves stable versions
}
version := extractVersion(rel.TagName)
if isStreamName(version, streams) {
version = extractVersion(rel.Title)
}
if version == "" {
continue
}
if err := rel.LoadAttributes(ctx); err != nil {
continue
}
var downloadURL string
var sha256Hash string
for _, att := range rel.Attachments {
if strings.HasSuffix(strings.ToLower(att.Name), ".zip") && !strings.HasSuffix(att.Name, ".sha256") {
downloadURL = fmt.Sprintf("%s/releases/download/%s/%s", repoLink, rel.TagName, att.Name)
break
}
}
if downloadURL == "" {
downloadURL = fmt.Sprintf("%s/archive/%s.zip", repoLink, rel.TagName)
}
if licenseKey != "" {
downloadURL += "?dlid=" + licenseKey
}
// Look for SHA256 sidecar
for _, att := range rel.Attachments {
if strings.HasSuffix(att.Name, ".sha256") {
sha256Hash = readSHA256FromSidecar(ctx, att)
break
}
}
require := make(map[string]string)
if phpMin != "" {
require["php"] = phpMin
}
v := ComposerVersion{
Name: packageName,
Version: version,
Description: description,
Homepage: repoLink,
Authors: []ComposerAuthor{
{Name: maintainer, Homepage: maintainerURL},
},
Dist: &ComposerDist{
URL: downloadURL,
Type: "zip",
Shasum: sha256Hash,
Reference: rel.TagName,
},
Require: require,
Time: time.Unix(int64(rel.CreatedUnix), 0).Format(time.RFC3339),
}
versions[version] = v
}
return &ComposerPackage{
Packages: map[string]map[string]ComposerVersion{
packageName: versions,
},
}, nil
}
+157
View File
@@ -0,0 +1,157 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package updateserver
import (
"context"
"encoding/xml"
"fmt"
"strings"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
)
// Drupal update status XML structures.
// See: https://www.drupal.org/docs/drupal-apis/update-status-module/update-status-xml-format
type drupalProject struct {
XMLName xml.Name `xml:"project"`
Title string `xml:"title"`
ShortName string `xml:"short_name"`
APIVersion string `xml:"api_version"`
RecommendedMaj string `xml:"recommended_major"`
DefaultMajor string `xml:"default_major"`
ProjectStatus string `xml:"project_status"`
Link string `xml:"link"`
Releases drupalReleases `xml:"releases"`
}
type drupalReleases struct {
Releases []drupalRelease `xml:"release"`
}
type drupalRelease struct {
Name string `xml:"name"`
Version string `xml:"version"`
Tag string `xml:"tag"`
Status string `xml:"status"`
ReleaseLink string `xml:"release_link"`
DownloadURL string `xml:"download_link"`
Date string `xml:"date"`
FileHash string `xml:"mdhash,omitempty"`
SHA256 string `xml:"sha256,omitempty"`
}
// GenerateDrupalXML builds a Drupal update status-compatible XML feed.
func GenerateDrupalXML(ctx context.Context, repo *repo_model.Repository, allowedChannels ...string) ([]byte, error) {
releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
RepoID: repo.ID,
ListOptions: db.ListOptionsAll,
IncludeDrafts: false,
IncludeTags: false,
})
if err != nil {
return nil, fmt.Errorf("FindReleases: %w", err)
}
if err := repo.LoadOwner(ctx); err != nil {
return nil, fmt.Errorf("LoadOwner: %w", err)
}
baseURL := strings.TrimSuffix(setting.AppURL, "/")
repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name)
cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
meta := resolveExtensionMetadata(ctx, repo, cfg)
shortName := meta.Element
title := meta.DisplayName
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
channelAllowed := make(map[string]bool)
if len(allowedChannels) > 0 {
for _, c := range allowedChannels {
channelAllowed[NormalizeChannel(c)] = true
}
}
project := drupalProject{
Title: title,
ShortName: shortName,
APIVersion: "7.x", // default API version
RecommendedMaj: "1",
DefaultMajor: "1",
ProjectStatus: "published",
Link: repoLink,
}
if cfg != nil && cfg.TargetVersion != "" {
project.APIVersion = cfg.TargetVersion
}
for _, rel := range releases {
if rel.IsDraft || rel.IsTag {
continue
}
ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
if len(channelAllowed) > 0 && !channelAllowed[ch] {
continue
}
if err := rel.LoadAttributes(ctx); err != nil {
continue
}
var downloadURL, sha256Hash string
for _, att := range rel.Attachments {
if strings.HasSuffix(strings.ToLower(att.Name), ".zip") && !strings.HasSuffix(att.Name, ".sha256") {
downloadURL = fmt.Sprintf("%s/releases/download/%s/%s", repoLink, rel.TagName, att.Name)
break
}
}
if downloadURL == "" {
downloadURL = fmt.Sprintf("%s/archive/%s.zip", repoLink, rel.TagName)
}
for _, att := range rel.Attachments {
if strings.HasSuffix(att.Name, ".sha256") {
sha256Hash = readSHA256FromSidecar(ctx, att)
break
}
}
version := extractVersion(rel.TagName)
if isStreamName(version, streams) || version == "" {
version = extractVersion(rel.Title)
}
if version == "" {
version = rel.TagName
}
status := "published"
if rel.IsPrerelease {
status = "insecure" // Drupal uses this for non-stable
}
project.Releases.Releases = append(project.Releases.Releases, drupalRelease{
Name: fmt.Sprintf("%s %s", shortName, version),
Version: version,
Tag: rel.TagName,
Status: status,
ReleaseLink: fmt.Sprintf("%s/releases/tag/%s", repoLink, rel.TagName),
DownloadURL: downloadURL,
Date: fmt.Sprintf("%d", rel.CreatedUnix),
SHA256: sha256Hash,
})
}
output, err := xml.MarshalIndent(project, "", " ")
if err != nil {
return nil, fmt.Errorf("xml.MarshalIndent: %w", err)
}
return append([]byte(xml.Header), output...), nil
}
+161 -43
View File
@@ -13,8 +13,10 @@ import (
"time"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/storage"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
)
@@ -37,7 +39,7 @@ type xmlUpdate struct {
TargetPlatform xmlTargetPlat `xml:"targetplatform"`
SHA256 string `xml:"sha256,omitempty"`
SHA512 string `xml:"sha512,omitempty"`
Client string `xml:"client"`
Client string `xml:"client,omitempty"`
PHPMinimum string `xml:"php_minimum,omitempty"`
Description string `xml:"description,omitempty"`
CreationDate string `xml:"creationDate,omitempty"`
@@ -120,7 +122,10 @@ func isStreamName(s string, streams []licenses.StreamDef) bool {
}
// joomlaTagName maps internal stream names to Joomla-standard tag values.
// Joomla recognizes: dev, alpha, beta, rc, stable.
// Joomla's Update.php maps tags via STABILITY_ + strtoupper(tag) constants.
// Valid values: dev (0), alpha (1), beta (2), rc (3), stable (4).
// Using full names like "development" or "release-candidate" would silently
// fall back to STABILITY_STABLE, breaking pre-release channel filtering.
func joomlaTagName(channel string) string {
switch channel {
case ChannelDevelopment:
@@ -157,9 +162,121 @@ func NormalizeChannel(ch string) string {
}
}
// extensionMetadata holds resolved metadata for feed generation.
// Fields are resolved with priority: custom field → config table → default.
type extensionMetadata struct {
Element string
DisplayName string
ExtType string
TargetVersion string
PHPMinimum string
Description string
SupportURL string
DownloadGating string
KeyPrefix string
}
// resolveExtensionMetadata loads extension metadata with cascading fallback:
// org-level repo-scoped custom fields → update_stream_config → repo-derived defaults.
func resolveExtensionMetadata(ctx context.Context, repo *repo_model.Repository, cfg *licenses.UpdateStreamConfig) extensionMetadata {
m := extensionMetadata{
Element: strings.ToLower(repo.Name),
DisplayName: fmt.Sprintf("%s - %s", repo.Owner.Name, repo.Name),
ExtType: "component",
TargetVersion: "(5|6)\\..*",
}
// Apply config table values.
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 != "" {
m.DownloadGating = cfg.DownloadGating
}
if cfg.KeyPrefix != "" {
m.KeyPrefix = cfg.KeyPrefix
}
}
// Override with custom field values (highest priority).
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
}
// GenerateJoomlaXML builds a Joomla-compatible updates.xml from repository releases.
// It returns the raw XML bytes. Extension metadata is read from the update stream config;
// falls back to repo name/owner when not configured.
// It returns the raw XML bytes. Extension metadata is resolved from custom fields first,
// then the update stream config, then repo-derived defaults.
// allowedChannels optionally restricts output to specific channels (nil = all).
func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, requireKey, stripDownloads bool, allowedChannels ...string) ([]byte, error) {
releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
@@ -182,40 +299,26 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require
}
repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name)
// Load extension metadata from config (falls back to repo-derived values).
// Load extension metadata with cascading fallback:
// custom fields → config table → repo-derived defaults.
cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
meta := resolveExtensionMetadata(ctx, repo, cfg)
element := strings.ToLower(repo.Name)
if cfg != nil && cfg.ExtensionName != "" {
element = cfg.ExtensionName
element := meta.Element
displayName := meta.DisplayName
extType := meta.ExtType
targetVersion := meta.TargetVersion
phpMinimum := meta.PHPMinimum
feedDescription := meta.Description
// Maintainer and URL always come from the org profile.
maintainer := repo.Owner.FullName
if maintainer == "" {
maintainer = repo.Owner.Name
}
displayName := fmt.Sprintf("%s - %s", repo.Owner.Name, repo.Name)
if cfg != nil && cfg.DisplayName != "" {
displayName = cfg.DisplayName
}
extType := "component"
if cfg != nil && cfg.ExtensionType != "" {
extType = cfg.ExtensionType
}
maintainer := repo.Owner.Name
if cfg != nil && cfg.Maintainer != "" {
maintainer = cfg.Maintainer
}
maintainerURL := fmt.Sprintf("%s/%s", baseURL, repo.Owner.Name)
if cfg != nil && cfg.MaintainerURL != "" {
maintainerURL = cfg.MaintainerURL
}
targetVersion := ".*"
if cfg != nil && cfg.TargetVersion != "" {
targetVersion = cfg.TargetVersion
}
phpMinimum := ""
if cfg != nil && cfg.PHPMinimum != "" {
phpMinimum = cfg.PHPMinimum
}
feedDescription := ""
if cfg != nil && cfg.Description != "" {
feedDescription = cfg.Description
maintainerURL := repo.Owner.Website
if maintainerURL == "" {
maintainerURL = fmt.Sprintf("%s/%s", baseURL, repo.Owner.Name)
}
// Resolve effective streams (repo override → org default → Joomla default).
@@ -284,9 +387,16 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require
downloadURL = fmt.Sprintf("%s/archive/%s.zip", repoLink, rel.TagName)
}
version := extractVersion(rel.TagName)
// If the tag is a stream name (not a version), try the release title instead.
if version == "" || isStreamName(rel.TagName, streams) {
// Extract version: prefer asset filename (matches actual download),
// then tag name, then release title. Only fall through when empty.
version := ""
if zipName != "" {
version = extractVersion(zipName)
}
if version == "" {
version = extractVersion(rel.TagName)
}
if version == "" {
version = extractVersion(rel.Title)
}
// Last resort: use the tag name as-is.
@@ -306,11 +416,19 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require
desc = fmt.Sprintf("%s %s build.", displayName, ch)
}
// Info URL: use support_url (product page), fall back to releases page.
infoURL := fmt.Sprintf("%s/releases", repoLink)
if cfg != nil && cfg.SupportURL != "" {
infoURL = cfg.SupportURL
} else if cfg != nil && cfg.InfoURL != "" {
infoURL = cfg.InfoURL
if meta.SupportURL != "" {
infoURL = meta.SupportURL
}
// Joomla <client> element: packages use client_id=0 in #__extensions,
// so we must output <client>0</client> for Joomla to match the update
// to the installed extension. Other types default to "site" (client_id=0)
// or "administrator" (client_id=1).
client := "site"
if extType == "package" {
client = "0"
}
u := xmlUpdate{
@@ -318,7 +436,7 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require
Description: desc,
Element: element,
Type: extType,
Client: "site",
Client: client,
Version: version,
CreationDate: time.Unix(int64(rel.CreatedUnix), 0).Format("2006-01-02"),
InfoURL: xmlInfoURL{
+148
View File
@@ -0,0 +1,148 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package updateserver
import (
"context"
"encoding/xml"
"fmt"
"strings"
"time"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
)
// PrestaShop module update XML structures.
type psUpdates struct {
XMLName xml.Name `xml:"modules"`
Modules []psModule `xml:"module"`
}
type psModule struct {
Name string `xml:"name,attr"`
Version string `xml:"version,attr"`
DisplayName string `xml:"displayName"`
Description string `xml:"description,omitempty"`
Author string `xml:"author"`
Tab string `xml:"tab,omitempty"`
Download string `xml:"download"`
Date string `xml:"date"`
SHA256 string `xml:"sha256,omitempty"`
}
// GeneratePrestaShopXML builds a PrestaShop-compatible module update XML.
func GeneratePrestaShopXML(ctx context.Context, repo *repo_model.Repository, allowedChannels ...string) ([]byte, error) {
releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
RepoID: repo.ID,
ListOptions: db.ListOptionsAll,
IncludeDrafts: false,
IncludeTags: false,
})
if err != nil {
return nil, fmt.Errorf("FindReleases: %w", err)
}
if err := repo.LoadOwner(ctx); err != nil {
return nil, fmt.Errorf("LoadOwner: %w", err)
}
baseURL := strings.TrimSuffix(setting.AppURL, "/")
repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name)
cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
meta := resolveExtensionMetadata(ctx, repo, cfg)
moduleName := meta.Element
displayName := meta.DisplayName
description := meta.Description
maintainer := repo.Owner.Name
if cfg != nil && cfg.Maintainer != "" {
maintainer = cfg.Maintainer
}
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
// Channel filtering.
channelAllowed := make(map[string]bool)
if len(allowedChannels) > 0 {
for _, c := range allowedChannels {
channelAllowed[NormalizeChannel(c)] = true
}
}
// Track best release per channel.
bestByChannel := make(map[string]*repo_model.Release)
for _, rel := range releases {
if rel.IsDraft || rel.IsTag {
continue
}
ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
existing, ok := bestByChannel[ch]
if !ok || rel.CreatedUnix > existing.CreatedUnix {
bestByChannel[ch] = rel
}
}
var result psUpdates
for _, stream := range streams {
ch := stream.Name
if len(channelAllowed) > 0 && !channelAllowed[ch] {
continue
}
rel, ok := bestByChannel[ch]
if !ok || ch != "stable" {
continue // PrestaShop typically only serves stable
}
if err := rel.LoadAttributes(ctx); err != nil {
continue
}
var downloadURL, sha256Hash string
for _, att := range rel.Attachments {
if strings.HasSuffix(strings.ToLower(att.Name), ".zip") && !strings.HasSuffix(att.Name, ".sha256") {
downloadURL = fmt.Sprintf("%s/releases/download/%s/%s", repoLink, rel.TagName, att.Name)
break
}
}
if downloadURL == "" {
downloadURL = fmt.Sprintf("%s/archive/%s.zip", repoLink, rel.TagName)
}
for _, att := range rel.Attachments {
if strings.HasSuffix(att.Name, ".sha256") {
sha256Hash = readSHA256FromSidecar(ctx, att)
break
}
}
version := extractVersion(rel.TagName)
if isStreamName(version, streams) || version == "" {
version = extractVersion(rel.Title)
}
if version == "" {
version = rel.TagName
}
result.Modules = append(result.Modules, psModule{
Name: moduleName,
Version: version,
DisplayName: displayName,
Description: description,
Author: maintainer,
Download: downloadURL,
Date: time.Unix(int64(rel.CreatedUnix), 0).Format("2006-01-02"),
SHA256: sha256Hash,
})
}
output, err := xml.MarshalIndent(result, "", " ")
if err != nil {
return nil, fmt.Errorf("xml.MarshalIndent: %w", err)
}
return append([]byte(xml.Header), output...), nil
}
+138
View File
@@ -0,0 +1,138 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package updateserver
import (
"context"
"fmt"
"strings"
"time"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
)
// WHMCS module update JSON structures.
// WHMCS marketplace modules check for updates via a simple JSON endpoint.
type WHMCSUpdate struct {
Name string `json:"name"`
Version string `json:"version"`
Description string `json:"description,omitempty"`
DownloadURL string `json:"download_url,omitempty"`
Changelog string `json:"changelog,omitempty"`
ReleaseDate string `json:"release_date"`
Author string `json:"author,omitempty"`
AuthorURL string `json:"author_url,omitempty"`
SHA256 string `json:"sha256,omitempty"`
}
// GenerateWHMCSJSON builds a WHMCS-compatible module update response.
func GenerateWHMCSJSON(ctx context.Context, repo *repo_model.Repository, licenseKey string) (*WHMCSUpdate, error) {
releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
RepoID: repo.ID,
ListOptions: db.ListOptionsAll,
IncludeDrafts: false,
IncludeTags: false,
})
if err != nil {
return nil, fmt.Errorf("FindReleases: %w", err)
}
if err := repo.LoadOwner(ctx); err != nil {
return nil, fmt.Errorf("LoadOwner: %w", err)
}
baseURL := strings.TrimSuffix(setting.AppURL, "/")
repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name)
cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
meta := resolveExtensionMetadata(ctx, repo, cfg)
displayName := meta.DisplayName
description := meta.Description
maintainer := repo.Owner.Name
maintainerURL := fmt.Sprintf("%s/%s", baseURL, repo.Owner.Name)
if cfg != nil {
if cfg.Maintainer != "" {
maintainer = cfg.Maintainer
}
if cfg.MaintainerURL != "" {
maintainerURL = cfg.MaintainerURL
}
}
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
// Find latest stable release.
var latestStable *repo_model.Release
for _, rel := range releases {
if rel.IsDraft || rel.IsTag {
continue
}
ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
if ch == "stable" {
if latestStable == nil || rel.CreatedUnix > latestStable.CreatedUnix {
latestStable = rel
}
}
}
if latestStable == nil {
return &WHMCSUpdate{
Name: displayName,
Version: "0.0.0",
}, nil
}
if err := latestStable.LoadAttributes(ctx); err != nil {
return nil, fmt.Errorf("LoadAttributes: %w", err)
}
var downloadURL, sha256Hash string
for _, att := range latestStable.Attachments {
if strings.HasSuffix(strings.ToLower(att.Name), ".zip") && !strings.HasSuffix(att.Name, ".sha256") {
downloadURL = fmt.Sprintf("%s/releases/download/%s/%s", repoLink, latestStable.TagName, att.Name)
break
}
}
if downloadURL == "" {
downloadURL = fmt.Sprintf("%s/archive/%s.zip", repoLink, latestStable.TagName)
}
if licenseKey != "" {
downloadURL += "?dlid=" + licenseKey
}
for _, att := range latestStable.Attachments {
if strings.HasSuffix(att.Name, ".sha256") {
sha256Hash = readSHA256FromSidecar(ctx, att)
break
}
}
version := extractVersion(latestStable.TagName)
if isStreamName(version, streams) || version == "" {
version = extractVersion(latestStable.Title)
}
if version == "" {
version = latestStable.TagName
}
changelog := ""
if latestStable.Note != "" {
changelog = latestStable.Note
}
return &WHMCSUpdate{
Name: displayName,
Version: version,
Description: description,
DownloadURL: downloadURL,
Changelog: changelog,
ReleaseDate: time.Unix(int64(latestStable.CreatedUnix), 0).Format("2006-01-02"),
Author: maintainer,
AuthorURL: maintainerURL,
SHA256: sha256Hash,
}, nil
}
+11 -17
View File
@@ -57,36 +57,30 @@ func GenerateWordPressJSON(ctx context.Context, repo *repo_model.Repository, lic
baseURL := strings.TrimSuffix(setting.AppURL, "/")
repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name)
// Load extension metadata.
// Load extension metadata with cascading fallback:
// custom fields → config table → repo-derived defaults.
cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
meta := resolveExtensionMetadata(ctx, repo, cfg)
slug := strings.ToLower(repo.Name)
displayName := fmt.Sprintf("%s - %s", repo.Owner.Name, repo.Name)
slug := meta.Element
displayName := meta.DisplayName
requiresPHP := meta.PHPMinimum
homepage := repoLink
if meta.SupportURL != "" {
homepage = meta.SupportURL
}
maintainer := repo.Owner.Name
maintainerURL := fmt.Sprintf("%s/%s", baseURL, repo.Owner.Name)
homepage := repoLink
requiresPHP := ""
if cfg != nil {
if cfg.ExtensionName != "" {
slug = cfg.ExtensionName
}
if cfg.DisplayName != "" {
displayName = cfg.DisplayName
}
if cfg.Maintainer != "" {
maintainer = cfg.Maintainer
}
if cfg.MaintainerURL != "" {
maintainerURL = cfg.MaintainerURL
}
if cfg.SupportURL != "" {
homepage = cfg.SupportURL
} else if cfg.InfoURL != "" {
if homepage == repoLink && cfg.InfoURL != "" {
homepage = cfg.InfoURL
}
if cfg.PHPMinimum != "" {
requiresPHP = cfg.PHPMinimum
}
}
// Resolve streams and find the latest stable release.
+9 -7
View File
@@ -25,14 +25,16 @@
</div>
{{end}}
<h4 class="ui top attached header">
{{svg "octicon-key" 16}} {{ctx.Locale.Tr "repo.licenses.packages"}}
<details id="new-package-details">
<h4 class="ui top attached header tw-flex tw-items-center tw-justify-between">
<span>{{svg "octicon-key" 16}} {{ctx.Locale.Tr "repo.licenses.packages"}}</span>
{{if .IsRepoAdmin}}
<summary class="ui primary small button">{{svg "octicon-plus" 14}} {{ctx.Locale.Tr "repo.licenses.new_package"}}</summary>
{{end}}
</h4>
<div class="ui attached segment">
{{if .IsRepoAdmin}}
<details class="tw-mb-4">
<summary class="ui primary button">{{svg "octicon-plus" 14}} {{ctx.Locale.Tr "repo.licenses.new_package"}}</summary>
<div class="tw-mt-4">
<div class="tw-mb-4">
<form class="ui form" method="post" action="{{$.Org.HomeLink}}/-/licenses/packages">
{{.CsrfTokenHtml}}
<div class="two fields">
@@ -80,9 +82,9 @@
</div>
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "repo.licenses.create_package"}}</button>
</form>
</div>
</details>
</div>
{{end}}
</details>
{{if .LicensePackages}}
<table class="ui compact table">
<thead>
+85
View File
@@ -0,0 +1,85 @@
{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings custom-fields")}}
<h4 class="ui top attached header">
{{ctx.Locale.Tr "org.settings.custom_fields"}}
</h4>
<div class="ui attached segment">
<p class="text grey">{{ctx.Locale.Tr "org.settings.custom_fields_desc"}}</p>
{{if .CustomFields}}
<table class="ui compact table">
<thead>
<tr>
<th>{{ctx.Locale.Tr "org.settings.custom_field_name"}}</th>
<th>{{ctx.Locale.Tr "org.settings.custom_field_scope"}}</th>
<th>{{ctx.Locale.Tr "org.settings.custom_field_type"}}</th>
<th>{{ctx.Locale.Tr "org.settings.custom_field_options"}}</th>
<th></th>
</tr>
</thead>
<tbody>
{{range .CustomFields}}
<tr>
<td><strong>{{.Name}}</strong>{{if .Description}}<br><small class="text grey">{{.Description}}</small>{{end}}</td>
<td>{{if eq .Scope "issue"}}{{svg "octicon-issue-opened" 14}} Issue{{else}}{{svg "octicon-repo" 14}} Repo{{end}}</td>
<td><code>{{.FieldType}}</code></td>
<td>{{if .Options}}<code class="tw-text-xs">{{.Options}}</code>{{else}}<span class="text grey">-</span>{{end}}</td>
<td class="tw-text-right">
<form method="post" action="{{$.OrgLink}}/settings/custom-fields/{{.ID}}/delete" class="tw-inline">
{{$.CsrfTokenHtml}}
<button class="ui tiny red icon button" type="submit" title="{{ctx.Locale.Tr "remove"}}">{{svg "octicon-trash" 14}}</button>
</form>
</td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
<div class="empty-placeholder">
<p>{{ctx.Locale.Tr "org.settings.custom_fields_empty"}}</p>
</div>
{{end}}
<div class="divider"></div>
<h5>{{ctx.Locale.Tr "org.settings.custom_field_add"}}</h5>
<form class="ui form" method="post" action="{{.OrgLink}}/settings/custom-fields">
{{.CsrfTokenHtml}}
<div class="three fields">
<div class="required field">
<label>{{ctx.Locale.Tr "org.settings.custom_field_name"}}</label>
<input name="name" required placeholder="e.g. Status, Platform, Priority">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.custom_field_scope"}}</label>
<select name="scope" class="ui dropdown">
<option value="issue">{{svg "octicon-issue-opened" 14}} Issue (sidebar)</option>
<option value="repo">{{svg "octicon-repo" 14}} Repo (metadata)</option>
</select>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.custom_field_type"}}</label>
<select name="field_type" class="ui dropdown">
<option value="dropdown">Dropdown</option>
<option value="text">Text</option>
<option value="number">Number</option>
<option value="date">Date</option>
<option value="checkbox">Checkbox</option>
<option value="url">URL</option>
</select>
</div>
</div>
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.custom_field_options"}}</label>
<input name="options" placeholder='["Option 1","Option 2","Option 3"]'>
<p class="help">{{ctx.Locale.Tr "org.settings.custom_field_options_help"}}</p>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.custom_field_description"}}</label>
<input name="description" placeholder="Help text shown to users">
</div>
</div>
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "org.settings.custom_field_add"}}</button>
</form>
</div>
{{template "org/settings/layout_footer" .}}
+12 -9
View File
@@ -1,36 +1,39 @@
<div class="flex-container-nav">
<div class="ui fluid vertical menu">
<div class="header item">{{ctx.Locale.Tr "org.settings"}}</div>
<div class="header item">{{svg "octicon-gear"}} {{ctx.Locale.Tr "org.settings"}}</div>
<a class="{{if .PageIsSettingsOptions}}active {{end}}item" href="{{.OrgLink}}/settings">
{{ctx.Locale.Tr "org.settings.options"}}
{{svg "octicon-gear"}} {{ctx.Locale.Tr "org.settings.options"}}
</a>
{{if not DisableWebhooks}}
<a class="{{if .PageIsSettingsHooks}}active {{end}}item" href="{{.OrgLink}}/settings/hooks">
{{ctx.Locale.Tr "repo.settings.hooks"}}
{{svg "octicon-webhook"}} {{ctx.Locale.Tr "repo.settings.hooks"}}
</a>
{{end}}
<a class="{{if .PageIsOrgSettingsLabels}}active {{end}}item" href="{{.OrgLink}}/settings/labels">
{{ctx.Locale.Tr "repo.labels"}}
{{svg "octicon-tag"}} {{ctx.Locale.Tr "repo.labels"}}
</a>
{{if .EnableOAuth2}}
<a class="{{if .PageIsSettingsApplications}}active {{end}}item" href="{{.OrgLink}}/settings/applications">
{{ctx.Locale.Tr "settings.applications"}}
{{svg "octicon-apps"}} {{ctx.Locale.Tr "settings.applications"}}
</a>
{{end}}
<a class="{{if .PageIsSettingsBlockedUsers}}active {{end}}item" href="{{.OrgLink}}/settings/blocked_users">
{{ctx.Locale.Tr "user.block.list"}}
{{svg "octicon-blocked"}} {{ctx.Locale.Tr "user.block.list"}}
</a>
{{if .EnablePackages}}
<a class="{{if .PageIsSettingsPackages}}active {{end}}item" href="{{.OrgLink}}/settings/packages">
{{ctx.Locale.Tr "packages.title"}}
{{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}}
</a>
{{end}}
<a class="{{if .PageIsSettingsUpdateStreams}}active {{end}}item" href="{{.OrgLink}}/settings/update-streams">
{{svg "octicon-key"}} {{ctx.Locale.Tr "org.settings.update_streams"}}
{{svg "octicon-broadcast"}} {{ctx.Locale.Tr "org.settings.update_streams"}}
</a>
<a class="{{if .PageIsSettingsCustomFields}}active {{end}}item" href="{{.OrgLink}}/settings/custom-fields">
{{svg "octicon-list-unordered"}} {{ctx.Locale.Tr "org.settings.custom_fields"}}
</a>
{{if .EnableActions}}
<details class="item toggleable-item" {{if or .PageIsOrgSettingsActionsGeneral .PageIsSharedSettingsRunners .PageIsSharedSettingsSecrets .PageIsSharedSettingsVariables}}open{{end}}>
<summary>{{ctx.Locale.Tr "actions.actions"}}</summary>
<summary>{{svg "octicon-play"}} {{ctx.Locale.Tr "actions.actions"}}</summary>
<div class="menu">
<a class="{{if .PageIsOrgSettingsActionsGeneral}}active {{end}}item" href="{{.OrgLink}}/settings/actions">
{{ctx.Locale.Tr "settings.general"}}
+13
View File
@@ -28,6 +28,19 @@
<input id="location" name="location" value="{{.Org.Location}}" maxlength="50">
</div>
{{if .ParentOrgCandidates}}
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.parent_org"}}</label>
<select name="parent_org_id" class="ui dropdown">
<option value="0">{{ctx.Locale.Tr "org.settings.parent_org_none"}}</option>
{{range .ParentOrgCandidates}}
<option value="{{.ID}}" {{if eq $.ParentOrgID .ID}}selected{{end}}>{{.Name}}{{if .FullName}} ({{.FullName}}){{end}}</option>
{{end}}
</select>
<p class="help">{{ctx.Locale.Tr "org.settings.parent_org_help"}}</p>
</div>
{{end}}
<div class="field" id="permission_box">
<label>{{ctx.Locale.Tr "org.settings.permission"}}</label>
<div class="field">
@@ -37,6 +37,12 @@
<p class="help">{{ctx.Locale.Tr "org.settings.feed_visibility_help"}}</p>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.key_prefix"}}</label>
<input name="key_prefix" value="{{.StreamConfig.KeyPrefix}}" placeholder="MOKO" maxlength="20">
<p class="help">{{ctx.Locale.Tr "org.settings.key_prefix_help"}}</p>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.download_gating"}}</label>
<select name="download_gating" class="ui dropdown">
+6 -5
View File
@@ -113,7 +113,7 @@
</a>
{{end}}
{{if and .EnableActions (.Permission.CanRead ctx.Consts.RepoUnitTypeActions) (not .IsEmptyRepo)}}
{{if and .EnableActions (.Permission.CanRead ctx.Consts.RepoUnitTypeActions) (not .IsEmptyRepo) .IsSigned}}
<a class="{{if .PageIsActions}}active {{end}}item" href="{{.RepoLink}}/actions">
{{svg "octicon-play"}} {{ctx.Locale.Tr "actions.actions"}}
{{if .Repository.NumOpenActionRuns}}
@@ -128,11 +128,12 @@
</a>
{{end}}
{{if .LicensingEnabled}}
{{if and .LicensingEnabled .IsSigned}}
<a href="{{.RepoLink}}/licenses" class="{{if .IsLicensesPage}}active {{end}}item">
{{svg "octicon-key"}} {{ctx.Locale.Tr "repo.licenses"}}
{{if .NumLicensePackages}}
<span class="ui small label">{{CountFmt .NumLicensePackages}}</span>
{{if .DownloadGated}}
{{svg "octicon-key"}} {{ctx.Locale.Tr "repo.licenses"}}
{{else}}
{{svg "octicon-broadcast"}} {{ctx.Locale.Tr "repo.settings.licensing_section"}}
{{end}}
</a>
{{end}}
+25
View File
@@ -59,6 +59,31 @@
{{end}}
{{template "repo/issue/sidebar/assignee_list" $.IssuePageMetaData}}
{{if .CustomFieldDefs}}
<div class="divider"></div>
<div class="tw-flex tw-flex-col tw-gap-2">
{{$values := .CustomFieldValues}}
{{$fieldOptions := .CustomFieldOptions}}
{{range .CustomFieldDefs}}
{{$currentVal := index $values .ID}}
<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>
{{if ne .Options ""}}
{{$opts := index $fieldOptions .ID}}
<select name="custom-field-{{.ID}}" class="ui compact mini dropdown tw-max-w-48">
<option value="">—</option>
{{range $opts}}
<option value="{{.}}" {{if eq . $currentVal}}selected{{end}}>{{.}}</option>
{{end}}
</select>
{{else}}
<input name="custom-field-{{.ID}}" type="text" class="tw-max-w-48 tw-text-sm" value="{{$currentVal}}" placeholder="—">
{{end}}
</div>
{{end}}
</div>
{{end}}
{{if and .PageIsComparePull (not (eq .HeadRepo.FullName .BaseCompareRepo.FullName)) .CanWriteToHeadRepo}}
<div class="divider"></div>
<div class="ui checkbox">
@@ -0,0 +1,33 @@
{{if .CustomFieldDefs}}
<div class="divider"></div>
<div class="tw-flex tw-flex-col tw-gap-2">
{{$issueID := .Issue.ID}}
{{$repoLink := .RepoLink}}
{{$canModify := .HasIssuesOrPullsWritePermission}}
{{$values := .CustomFieldValues}}
{{$fieldOptions := .CustomFieldOptions}}
{{range .CustomFieldDefs}}
{{$currentVal := index $values .ID}}
<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>
{{if and $canModify (eq .Options "")}}
{{/* Non-dropdown: just display the value */}}
<span class="tw-text-sm">{{if $currentVal}}{{$currentVal}}{{else}}<span class="text grey">—</span>{{end}}</span>
{{else if $canModify}}
<form method="post" action="{{$repoLink}}/issues/{{$issueID}}/custom-fields/{{.ID}}" class="tw-inline">
{{$.CsrfTokenHtml}}
<select name="value" class="ui compact mini dropdown tw-max-w-48" onchange="this.form.submit()">
<option value="">—</option>
{{$opts := index $fieldOptions .ID}}
{{range $opts}}
<option value="{{.}}" {{if eq . $currentVal}}selected{{end}}>{{.}}</option>
{{end}}
</select>
</form>
{{else}}
<span class="tw-text-sm">{{if $currentVal}}{{$currentVal}}{{else}}<span class="text grey">—</span>{{end}}</span>
{{end}}
</div>
{{end}}
</div>
{{end}}
@@ -7,6 +7,8 @@
{{template "repo/issue/sidebar/label_list" $.IssuePageMetaData}}
{{template "repo/issue/sidebar/custom_fields" $}}
{{template "repo/issue/sidebar/milestone_list" $.IssuePageMetaData}}
{{if .IsProjectsEnabled}}
{{template "repo/issue/sidebar/project_list" $.IssuePageMetaData}}
+113 -71
View File
@@ -3,6 +3,7 @@
{{template "repo/header" .}}
<div class="ui container">
{{if .DownloadGated}}
{{if .NewMasterKey}}
<div class="ui info message">
<div class="header">{{ctx.Locale.Tr "repo.licenses.master_key_created"}}</div>
@@ -26,8 +27,11 @@
{{end}}
{{/* License Packages */}}
<h4 class="ui top attached header">
{{svg "octicon-key" 16}} {{ctx.Locale.Tr "repo.licenses.packages"}}
<h4 class="ui top attached header tw-flex tw-items-center tw-justify-between">
<span>{{svg "octicon-key" 16}} {{ctx.Locale.Tr "repo.licenses.packages"}}</span>
{{if .IsRepoAdmin}}
<button class="ui primary small button show-modal" data-modal="#new-package-modal">{{svg "octicon-plus" 14}} {{ctx.Locale.Tr "repo.licenses.new_package"}}</button>
{{end}}
</h4>
<div class="ui attached segment">
{{if .LicensePackages}}
@@ -52,16 +56,12 @@
<td>{{if .IsActive}}<span class="ui green label">{{ctx.Locale.Tr "repo.licenses.active"}}</span>{{else}}<span class="ui grey label">{{ctx.Locale.Tr "repo.licenses.inactive"}}</span>{{end}}</td>
{{if $.IsRepoAdmin}}
<td class="tw-text-right tw-flex tw-gap-1 tw-justify-end">
<form method="post" action="{{$.RepoLink}}/licenses/keys/generate" class="tw-inline tw-flex tw-gap-1 tw-items-center">
{{$.CsrfTokenHtml}}
<input type="hidden" name="package_id" value="{{.ID}}">
{{if $.IsSiteAdmin}}
<input type="text" name="custom_key" placeholder="{{ctx.Locale.Tr "repo.licenses.custom_key_placeholder"}}" class="tw-w-32 tw-text-xs" title="{{ctx.Locale.Tr "repo.licenses.custom_key_help"}}">
{{end}}
<button class="ui tiny primary button" type="submit" title="{{ctx.Locale.Tr "repo.licenses.generate_key"}}">
{{svg "octicon-plus" 14}}
</button>
</form>
<button class="ui tiny primary button show-modal"
data-modal="#generate-key-modal"
data-modal-form.action="{{$.RepoLink}}/licenses/keys/generate?package_id={{.ID}}"
title="{{ctx.Locale.Tr "repo.licenses.generate_key"}}">
{{svg "octicon-plus" 14}}
</button>
{{if ne .Name "Master (Internal)"}}
<a class="ui tiny button" href="{{$.RepoLink}}/licenses/packages/{{.ID}}/edit" title="{{ctx.Locale.Tr "repo.licenses.edit_package"}}">
{{svg "octicon-pencil" 14}}
@@ -90,64 +90,6 @@
{{end}}
</div>
{{/* Create New License Package */}}
{{if .IsRepoAdmin}}
<div class="tw-mt-4">
<details>
<summary class="ui primary button">{{svg "octicon-plus" 14}} {{ctx.Locale.Tr "repo.licenses.new_package"}}</summary>
<div class="ui segment tw-mt-2">
<form class="ui form" method="post" action="{{.RepoLink}}/licenses/packages">
{{.CsrfTokenHtml}}
<div class="two fields">
<div class="required field">
<label>{{ctx.Locale.Tr "repo.licenses.package_name"}}</label>
<input name="name" required placeholder="e.g. Pro Annual, Basic Monthly">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.description"}}</label>
<input name="description" placeholder="e.g. Annual pro subscription with all channels">
</div>
</div>
<div class="fields">
<div class="four wide field">
<label>{{ctx.Locale.Tr "repo.licenses.duration"}} ({{ctx.Locale.Tr "repo.licenses.days"}})</label>
<input name="duration_days" type="number" value="0" min="0">
<p class="help">0 = {{ctx.Locale.Tr "repo.licenses.lifetime"}}</p>
</div>
<div class="four wide field">
<label>{{ctx.Locale.Tr "repo.licenses.max_sites"}}</label>
<input name="max_sites" type="number" value="0" min="0">
<p class="help">0 = {{ctx.Locale.Tr "repo.licenses.unlimited"}}</p>
</div>
<div class="four wide field">
<label>{{ctx.Locale.Tr "repo.licenses.domain_lock_hours"}}</label>
<input name="domain_lock_hours" type="number" value="0" min="0">
<p class="help">{{ctx.Locale.Tr "repo.licenses.domain_lock_hours_help"}}</p>
</div>
<div class="field">
{{template "shared/combolist" dict "Name" "allowed_channels" "Title" (ctx.Locale.Tr "repo.licenses.channels") "Items" $.ChannelItems "SelectedValues" "" "EmptyText" (ctx.Locale.Tr "repo.licenses.all_channels") "Icon" "octicon-tag"}}
<p class="help">{{ctx.Locale.Tr "repo.licenses.channels_help"}}</p>
</div>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.repo_scope"}}</label>
<select name="repo_scope" class="ui dropdown">
<option value="all">{{ctx.Locale.Tr "repo.licenses.repo_scope_all"}}</option>
{{if .OrgRepos}}
{{range .OrgRepos}}
<option value="{{.ID}}">{{.Name}}</option>
{{end}}
{{end}}
</select>
<p class="help">{{ctx.Locale.Tr "repo.licenses.repo_scope_help"}}</p>
</div>
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "repo.licenses.create_package"}}</button>
</form>
</div>
</details>
</div>
{{end}}
{{/* Issued Keys */}}
{{if or .LicenseKeys .SearchQuery}}
<h4 class="ui top attached header tw-mt-4">
@@ -183,7 +125,7 @@
<td>
<div class="tw-flex tw-items-center tw-gap-1">
<code class="js-license-key-{{.ID}}">{{if .KeyRaw}}{{.KeyRaw}}{{else}}{{.KeyPrefix}}{{end}}</code>
{{if .KeyRaw}}<button class="ui tiny icon button" data-clipboard-target=".js-license-key-{{.ID}}" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}">{{svg "octicon-copy" 12}}</button>{{end}}
<button class="ui tiny icon button" data-clipboard-text="{{if .KeyRaw}}{{.KeyRaw}}{{else}}{{.KeyPrefix}}{{end}}" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}">{{svg "octicon-copy" 12}}</button>
{{if .IsInternal}} <span class="ui tiny orange label">{{ctx.Locale.Tr "repo.licenses.master_label"}}</span>{{end}}
</div>
</td>
@@ -260,6 +202,8 @@
</details>
{{end}}
{{end}}{{/* end DownloadGated */}}
{{/* Update Feed URLs */}}
{{if .LicensingEnabled}}
<h4 class="ui top attached header tw-mt-4">
@@ -272,6 +216,7 @@
<div class="ui action input tw-w-full">
<input class="js-feed-url-joomla" type="text" readonly value="{{.Repository.HTMLURL ctx}}/updates.xml" onclick="this.select()">
<button class="ui button" data-clipboard-target=".js-feed-url-joomla" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}">{{svg "octicon-copy" 14}}</button>
<a class="ui button" href="{{.Repository.HTMLURL ctx}}/updates.xml" target="_blank" data-tooltip-content="{{ctx.Locale.Tr "repo.licenses.open_feed"}}">{{svg "octicon-link-external" 14}}</a>
</div>
</div>
{{end}}
@@ -281,6 +226,7 @@
<div class="ui action input tw-w-full">
<input class="js-feed-url-dolibarr" type="text" readonly value="{{.Repository.HTMLURL ctx}}/updates/dolibarr.json" onclick="this.select()">
<button class="ui button" data-clipboard-target=".js-feed-url-dolibarr" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}">{{svg "octicon-copy" 14}}</button>
<a class="ui button" href="{{.Repository.HTMLURL ctx}}/updates/dolibarr.json" target="_blank" data-tooltip-content="{{ctx.Locale.Tr "repo.licenses.open_feed"}}">{{svg "octicon-link-external" 14}}</a>
</div>
</div>
{{end}}
@@ -290,6 +236,7 @@
<div class="ui action input tw-w-full">
<input class="js-feed-url-wordpress" type="text" readonly value="{{.Repository.HTMLURL ctx}}/updates/wordpress.json" onclick="this.select()">
<button class="ui button" data-clipboard-target=".js-feed-url-wordpress" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}">{{svg "octicon-copy" 14}}</button>
<a class="ui button" href="{{.Repository.HTMLURL ctx}}/updates/wordpress.json" target="_blank" data-tooltip-content="{{ctx.Locale.Tr "repo.licenses.open_feed"}}">{{svg "octicon-link-external" 14}}</a>
</div>
</div>
{{end}}
@@ -298,6 +245,7 @@
<div class="ui action input tw-w-full">
<input class="js-feed-url-changelog" type="text" readonly value="{{.Repository.HTMLURL ctx}}/changelog.xml" onclick="this.select()">
<button class="ui button" data-clipboard-target=".js-feed-url-changelog" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}">{{svg "octicon-copy" 14}}</button>
<a class="ui button" href="{{.Repository.HTMLURL ctx}}/changelog.xml" target="_blank" data-tooltip-content="{{ctx.Locale.Tr "repo.licenses.open_feed"}}">{{svg "octicon-link-external" 14}}</a>
</div>
</div>
</div>
@@ -305,7 +253,100 @@
</div>
</div>
{{/* Create New License Package Modal */}}
{{if .IsRepoAdmin}}
<div class="ui small modal" id="new-package-modal">
<div class="header">{{svg "octicon-plus"}} {{ctx.Locale.Tr "repo.licenses.new_package"}}</div>
<div class="content">
<form class="ui form" method="post" action="{{.RepoLink}}/licenses/packages">
{{.CsrfTokenHtml}}
<div class="required field">
<label>{{ctx.Locale.Tr "repo.licenses.package_name"}}</label>
<input name="name" required placeholder="e.g. Pro Annual, Basic Monthly">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.description"}}</label>
<input name="description" placeholder="e.g. Annual pro subscription with all channels">
</div>
<div class="three fields">
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.duration"}} ({{ctx.Locale.Tr "repo.licenses.days"}})</label>
<input name="duration_days" type="number" value="0" min="0">
<p class="help">0 = {{ctx.Locale.Tr "repo.licenses.lifetime"}}</p>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.max_sites"}}</label>
<input name="max_sites" type="number" value="0" min="0">
<p class="help">0 = {{ctx.Locale.Tr "repo.licenses.unlimited"}}</p>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.domain_lock_hours"}}</label>
<input name="domain_lock_hours" type="number" value="0" min="0">
<p class="help">{{ctx.Locale.Tr "repo.licenses.domain_lock_hours_help"}}</p>
</div>
</div>
<div class="field">
{{template "shared/combolist" dict "Name" "allowed_channels" "Title" (ctx.Locale.Tr "repo.licenses.channels") "Items" $.ChannelItems "SelectedValues" "" "EmptyText" (ctx.Locale.Tr "repo.licenses.all_channels") "Icon" "octicon-tag"}}
<p class="help">{{ctx.Locale.Tr "repo.licenses.channels_help"}}</p>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.domain_restriction"}}</label>
<input name="domain_restriction" placeholder="example.com, example.org">
<p class="help">{{ctx.Locale.Tr "repo.licenses.domain_restriction_package_help"}}</p>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.repo_scope"}}</label>
<select name="repo_scope" class="ui dropdown">
<option value="all">{{ctx.Locale.Tr "repo.licenses.repo_scope_all"}}</option>
{{if .OrgRepos}}
{{range .OrgRepos}}
<option value="{{.ID}}">{{.Name}}</option>
{{end}}
{{end}}
</select>
<p class="help">{{ctx.Locale.Tr "repo.licenses.repo_scope_help"}}</p>
</div>
{{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.licenses.create_package"))}}
</form>
</div>
</div>
{{end}}
{{/* Generate Key Modal */}}
{{if and .DownloadGated .IsRepoAdmin}}
<div class="ui small modal" id="generate-key-modal">
<div class="header">{{svg "octicon-plus"}} {{ctx.Locale.Tr "repo.licenses.generate_key"}}</div>
<div class="content">
<form class="ui form" method="post" action="{{.RepoLink}}/licenses/keys/generate">
{{.CsrfTokenHtml}}
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.licensee_name"}}</label>
<input name="licensee_name" placeholder="Customer name">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.licensee_email"}}</label>
<input name="licensee_email" type="email" placeholder="customer@example.com">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.domain_restriction"}}</label>
<input name="domain_restriction" placeholder="example.com, example.org">
<p class="help">{{ctx.Locale.Tr "repo.licenses.domain_restriction_help"}}</p>
</div>
{{if .IsSiteAdmin}}
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.custom_key_placeholder"}}</label>
<input name="custom_key" placeholder="MOKO-XXXX-XXXX-XXXX">
<p class="help">{{ctx.Locale.Tr "repo.licenses.custom_key_help"}}</p>
</div>
{{end}}
{{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.licenses.generate_key"))}}
</form>
</div>
</div>
{{end}}
{{/* Delete Package Confirmation Modal */}}
{{if .DownloadGated}}
<div class="ui small modal" id="license-delete-package-modal">
<div class="header">{{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.licenses.delete_package"}}</div>
<div class="content">
@@ -336,5 +377,6 @@
</form>
</div>
</div>
{{end}}{{/* end DownloadGated for modals */}}
{{template "base/footer" .}}
@@ -39,6 +39,11 @@
<p class="help">{{ctx.Locale.Tr "repo.licenses.channels_help"}}</p>
</div>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.domain_restriction"}}</label>
<input name="domain_restriction" value="{{.Package.DomainRestriction}}" placeholder="example.com, example.org">
<p class="help">{{ctx.Locale.Tr "repo.licenses.domain_restriction_package_help"}}</p>
</div>
<div class="field">
<div class="ui checkbox">
<input name="is_active" type="checkbox" {{if .Package.IsActive}}checked{{end}}>
+396
View File
@@ -0,0 +1,396 @@
{{template "repo/settings/layout_head" (dict "pageClass" "repository settings advanced")}}
<div class="user-main-content twelve wide column">
<h4 class="ui top attached header">
{{ctx.Locale.Tr "repo.settings.advanced_settings"}}
</h4>
<div class="ui attached segment">
<form class="ui form" method="post">
<input type="hidden" name="action" value="advanced">
{{/* Code */}}
<h5 class="ui dividing header tw-flex tw-items-center tw-gap-2">{{svg "octicon-code" 16}} {{ctx.Locale.Tr "repo.code"}}</h5>
{{$isCodeEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeCode}}
{{$isCodeGlobalDisabled := ctx.Consts.RepoUnitTypeCode.UnitGlobalDisabled}}
<div class="inline field">
<div class="ui checkbox{{if $isCodeGlobalDisabled}} disabled{{end}}"{{if $isCodeGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
<input class="enable-system" name="enable_code" type="checkbox"{{if $isCodeEnabled}} checked{{end}}>
<label>{{ctx.Locale.Tr "repo.code.desc"}}</label>
</div>
</div>
{{/* Wiki */}}
<h5 class="ui dividing header tw-flex tw-items-center tw-gap-2">{{svg "octicon-book" 16}} {{ctx.Locale.Tr "repo.wiki"}}</h5>
{{$isInternalWikiEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeWiki}}
{{$isExternalWikiEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalWiki}}
{{$isWikiEnabled := or $isInternalWikiEnabled $isExternalWikiEnabled}}
{{$isWikiGlobalDisabled := ctx.Consts.RepoUnitTypeWiki.UnitGlobalDisabled}}
{{$isExternalWikiGlobalDisabled := ctx.Consts.RepoUnitTypeExternalWiki.UnitGlobalDisabled}}
{{$isBothWikiGlobalDisabled := and $isWikiGlobalDisabled $isExternalWikiGlobalDisabled}}
<div class="inline field">
<div class="ui checkbox{{if $isBothWikiGlobalDisabled}} disabled{{end}}"{{if $isBothWikiGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
<input class="enable-system" name="enable_wiki" type="checkbox" data-target="#wiki_box" {{if $isWikiEnabled}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.wiki_desc"}}</label>
</div>
</div>
<div class="field{{if not $isWikiEnabled}} disabled{{end}}" id="wiki_box">
<div class="field">
<div class="ui radio checkbox{{if $isWikiGlobalDisabled}} disabled{{end}}"{{if $isWikiGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
<input class="enable-system-radio" name="enable_external_wiki" type="radio" value="false" data-context="#internal_wiki_box" data-target="#external_wiki_box" {{if $isInternalWikiEnabled}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.use_internal_wiki"}}</label>
</div>
</div>
<div id="internal_wiki_box" class="field tw-pl-4 {{if not $isInternalWikiEnabled}}disabled{{end}}">
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.settings.default_wiki_branch_name"}}</label>
<input name="default_wiki_branch" value="{{.Repository.DefaultWikiBranch}}">
</div>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.settings.unit_visibility"}}</label>
<select name="wiki_visibility" class="ui dropdown">
<option value="not-set" {{if not (eq (.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeWiki).AnonymousAccessMode 1)}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.unit_visibility_private"}}</option>
<option value="anonymous-read" {{if eq (.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeWiki).AnonymousAccessMode 1}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.unit_visibility_public"}}</option>
</select>
</div>
</div>
<div class="field">
<div class="ui radio checkbox{{if $isExternalWikiGlobalDisabled}} disabled{{end}}"{{if $isExternalWikiGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
<input class="enable-system-radio" name="enable_external_wiki" type="radio" value="true" data-context="#internal_wiki_box" data-target="#external_wiki_box" {{if $isExternalWikiEnabled}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.use_external_wiki"}}</label>
</div>
</div>
<div id="external_wiki_box" class="field tw-pl-4 {{if not $isExternalWikiEnabled}}disabled{{end}}">
<label for="external_wiki_url">{{ctx.Locale.Tr "repo.settings.external_wiki_url"}}</label>
<input id="external_wiki_url" name="external_wiki_url" type="url" value="{{(.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeExternalWiki).ExternalWikiConfig.ExternalWikiURL}}">
<p class="help">{{ctx.Locale.Tr "repo.settings.external_wiki_url_desc"}}</p>
</div>
</div>
{{/* Issues */}}
<h5 class="ui dividing header tw-flex tw-items-center tw-gap-2">{{svg "octicon-issue-opened" 16}} {{ctx.Locale.Tr "repo.issues"}}</h5>
{{$isIssuesEnabled := or (.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeIssues) (.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalTracker)}}
{{$isIssuesGlobalDisabled := ctx.Consts.RepoUnitTypeIssues.UnitGlobalDisabled}}
{{$isExternalTrackerGlobalDisabled := ctx.Consts.RepoUnitTypeExternalTracker.UnitGlobalDisabled}}
{{$isIssuesAndExternalGlobalDisabled := and $isIssuesGlobalDisabled $isExternalTrackerGlobalDisabled}}
<div class="inline field">
<div class="ui checkbox{{if $isIssuesAndExternalGlobalDisabled}} disabled{{end}}"{{if $isIssuesAndExternalGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
<input class="enable-system" name="enable_issues" type="checkbox" data-target="#issue_box" {{if $isIssuesEnabled}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.issues_desc"}}</label>
</div>
</div>
<div class="field {{if not $isIssuesEnabled}}disabled{{end}}" id="issue_box">
<div class="field">
<div class="ui radio checkbox{{if $isIssuesGlobalDisabled}} disabled{{end}}"{{if $isIssuesGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
<input class="enable-system-radio" name="enable_external_tracker" type="radio" value="false" data-context="#internal_issue_box" data-target="#external_issue_box" {{if not (.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalTracker)}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.use_internal_issue_tracker"}}</label>
</div>
</div>
<div class="field tw-pl-4 {{if (.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalTracker)}}disabled{{end}}" id="internal_issue_box">
{{if .Repository.CanEnableTimetracker}}
<div class="field">
<div class="ui checkbox">
<input name="enable_timetracker" class="enable-system" data-target="#only_contributors" type="checkbox" {{if .Repository.IsTimetrackerEnabled ctx}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.enable_timetracker"}}</label>
</div>
</div>
<div class="field {{if not (.Repository.IsTimetrackerEnabled ctx)}}disabled{{end}}" id="only_contributors">
<div class="ui checkbox">
<input name="allow_only_contributors_to_track_time" type="checkbox" {{if .Repository.AllowOnlyContributorsToTrackTime ctx}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.allow_only_contributors_to_track_time"}}</label>
</div>
</div>
{{end}}
<div class="field">
<div class="ui checkbox">
<input name="enable_issue_dependencies" type="checkbox" {{if (.Repository.IsDependenciesEnabled ctx)}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.issues.dependency.setting"}}</label>
</div>
</div>
<div class="ui checkbox">
<input name="enable_close_issues_via_commit_in_any_branch" type="checkbox" {{if .Repository.CloseIssuesViaCommitInAnyBranch}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.admin_enable_close_issues_via_commit_in_any_branch"}}</label>
</div>
<div class="inline field tw-mt-2">
<label>{{ctx.Locale.Tr "repo.settings.unit_visibility"}}</label>
<select name="issues_visibility" class="ui dropdown">
<option value="not-set" {{if not (eq (.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeIssues).AnonymousAccessMode 1)}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.unit_visibility_private"}}</option>
<option value="anonymous-read" {{if eq (.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeIssues).AnonymousAccessMode 1}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.unit_visibility_public"}}</option>
</select>
</div>
</div>
<div class="field">
<div class="ui radio checkbox{{if $isExternalTrackerGlobalDisabled}} disabled{{end}}"{{if $isExternalTrackerGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
<input class="enable-system-radio" name="enable_external_tracker" type="radio" value="true" data-context="#internal_issue_box" data-target="#external_issue_box" {{if .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalTracker}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.use_external_issue_tracker"}}</label>
</div>
</div>
<div class="field tw-pl-4 {{if not (.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalTracker)}}disabled{{end}}" id="external_issue_box">
<div class="field">
<label for="external_tracker_url">{{ctx.Locale.Tr "repo.settings.external_tracker_url"}}</label>
<input id="external_tracker_url" name="external_tracker_url" type="url" value="{{(.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeExternalTracker).ExternalTrackerConfig.ExternalTrackerURL}}">
<p class="help">{{ctx.Locale.Tr "repo.settings.external_tracker_url_desc"}}</p>
</div>
<div class="field">
<label for="tracker_url_format">{{ctx.Locale.Tr "repo.settings.tracker_url_format"}}</label>
<input id="tracker_url_format" name="tracker_url_format" type="url" value="{{(.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeExternalTracker).ExternalTrackerConfig.ExternalTrackerFormat}}" placeholder="https://github.com/{user}/{repo}/issues/{index}">
<p class="help">{{ctx.Locale.Tr "repo.settings.tracker_url_format_desc"}}</p>
</div>
<div class="inline fields">
<label for="issue_style">{{ctx.Locale.Tr "repo.settings.tracker_issue_style"}}</label>
<div class="field">
<div class="ui radio checkbox">
{{$externalTracker := (.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeExternalTracker)}}
{{$externalTrackerStyle := $externalTracker.ExternalTrackerConfig.ExternalTrackerStyle}}
<input class="js-tracker-issue-style" name="tracker_issue_style" type="radio" value="numeric" {{if eq $externalTrackerStyle "numeric"}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.tracker_issue_style.numeric"}} <span class="ui light grey text">#1234</span></label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input class="js-tracker-issue-style" name="tracker_issue_style" type="radio" value="alphanumeric" {{if eq $externalTrackerStyle "alphanumeric"}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.tracker_issue_style.alphanumeric"}} <span class="ui light grey text">ABC-123 , DEFG-234</span></label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input class="js-tracker-issue-style" name="tracker_issue_style" type="radio" value="regexp" {{if eq $externalTrackerStyle "regexp"}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.tracker_issue_style.regexp"}} <span class="ui light grey text">(ISSUE-\d+) , ISSUE-(\d+)</span></label>
</div>
</div>
</div>
<div class="field {{if ne $externalTrackerStyle "regexp"}}disabled{{end}}" id="tracker-issue-style-regex-box">
<label for="external_tracker_regexp_pattern">{{ctx.Locale.Tr "repo.settings.tracker_issue_style.regexp_pattern"}}</label>
<input id="external_tracker_regexp_pattern" name="external_tracker_regexp_pattern" value="{{(.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeExternalTracker).ExternalTrackerConfig.ExternalTrackerRegexpPattern}}">
<p class="help">{{ctx.Locale.Tr "repo.settings.tracker_issue_style.regexp_pattern_desc"}}</p>
</div>
</div>
</div>
{{/* Projects */}}
<h5 class="ui dividing header tw-flex tw-items-center tw-gap-2">{{svg "octicon-project" 16}} {{ctx.Locale.Tr "repo.projects"}}</h5>
{{$isProjectsEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeProjects}}
{{$isProjectsGlobalDisabled := ctx.Consts.RepoUnitTypeProjects.UnitGlobalDisabled}}
{{$projectsUnit := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeProjects}}
<div class="inline field">
<div class="ui checkbox{{if $isProjectsGlobalDisabled}} disabled{{end}}"{{if $isProjectsGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
<input class="enable-system" name="enable_projects" type="checkbox" data-target="#projects_box" {{if $isProjectsEnabled}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.projects_desc"}}</label>
</div>
</div>
<div class="field {{if not $isProjectsEnabled}} disabled{{end}} tw-pl-4" id="projects_box">
<p>
{{ctx.Locale.Tr "repo.settings.projects_mode_desc"}}
</p>
<div class="ui dropdown selection">
<select name="projects_mode">
<option value="repo" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.GetProjectsMode "repo")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_repo"}}</option>
<option value="owner" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.GetProjectsMode "owner")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_owner"}}</option>
<option value="all" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.GetProjectsMode "all")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_all"}}</option>
</select>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="default text">
{{if (eq $projectsUnit.ProjectsConfig.GetProjectsMode "repo")}}
{{ctx.Locale.Tr "repo.settings.projects_mode_repo"}}
{{end}}
{{if (eq $projectsUnit.ProjectsConfig.GetProjectsMode "owner")}}
{{ctx.Locale.Tr "repo.settings.projects_mode_owner"}}
{{end}}
{{if (eq $projectsUnit.ProjectsConfig.GetProjectsMode "all")}}
{{ctx.Locale.Tr "repo.settings.projects_mode_all"}}
{{end}}
</div>
<div class="menu">
<div class="item" data-value="repo">{{ctx.Locale.Tr "repo.settings.projects_mode_repo"}}</div>
<div class="item" data-value="owner">{{ctx.Locale.Tr "repo.settings.projects_mode_owner"}}</div>
<div class="item" data-value="all">{{ctx.Locale.Tr "repo.settings.projects_mode_all"}}</div>
</div>
</div>
</div>
{{/* Releases */}}
<h5 class="ui dividing header tw-flex tw-items-center tw-gap-2">{{svg "octicon-tag" 16}} {{ctx.Locale.Tr "repo.releases"}}</h5>
{{$isReleasesEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeReleases}}
{{$isReleasesGlobalDisabled := ctx.Consts.RepoUnitTypeReleases.UnitGlobalDisabled}}
<div class="inline field">
<div class="ui checkbox{{if $isReleasesGlobalDisabled}} disabled{{end}}"{{if $isReleasesGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
<input class="enable-system" name="enable_releases" type="checkbox" data-target="#releases_visibility_box" {{if $isReleasesEnabled}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.releases_desc"}}</label>
</div>
</div>
<div class="field tw-pl-4{{if not $isReleasesEnabled}} disabled{{end}}" id="releases_visibility_box">
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.settings.unit_visibility"}}</label>
<select name="releases_visibility" class="ui dropdown">
<option value="not-set" {{if not (eq (.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeReleases).AnonymousAccessMode 1)}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.unit_visibility_private"}}</option>
<option value="anonymous-read" {{if eq (.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeReleases).AnonymousAccessMode 1}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.unit_visibility_public"}}</option>
</select>
<p class="help">{{ctx.Locale.Tr "repo.settings.unit_visibility_releases_help"}}</p>
</div>
</div>
{{/* Licensing */}}
<h5 class="ui dividing header tw-flex tw-items-center tw-gap-2">{{svg "octicon-broadcast" 16}} {{ctx.Locale.Tr "repo.settings.licensing_section"}}</h5>
<div class="inline field">
<div class="ui checkbox">
<input name="enable_licensing" type="checkbox" {{if .LicensingEnabled}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.enable_licensing"}}</label>
</div>
</div>
<p class="help">{{ctx.Locale.Tr "repo.settings.enable_licensing_help"}}</p>
{{/* Packages */}}
<h5 class="ui dividing header tw-flex tw-items-center tw-gap-2">{{svg "octicon-package" 16}} {{ctx.Locale.Tr "repo.packages"}}</h5>
{{$isPackagesEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypePackages}}
{{$isPackagesGlobalDisabled := ctx.Consts.RepoUnitTypePackages.UnitGlobalDisabled}}
<div class="inline field">
<div class="ui checkbox{{if $isPackagesGlobalDisabled}} disabled{{end}}"{{if $isPackagesGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
<input class="enable-system" name="enable_packages" type="checkbox" {{if $isPackagesEnabled}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.packages_desc"}}</label>
</div>
</div>
{{if not .IsMirror}}
{{/* Pull Requests */}}
<h5 class="ui dividing header tw-flex tw-items-center tw-gap-2">{{svg "octicon-git-pull-request" 16}} {{ctx.Locale.Tr "repo.pulls"}}</h5>
{{$pullRequestEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypePullRequests}}
{{$pullRequestGlobalDisabled := ctx.Consts.RepoUnitTypePullRequests.UnitGlobalDisabled}}
{{$prUnit := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypePullRequests}}
<div class="inline field">
<div class="ui checkbox{{if $pullRequestGlobalDisabled}} disabled{{end}}"{{if $pullRequestGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
<input class="enable-system" name="enable_pulls" type="checkbox" data-target="#pull_box" {{if $pullRequestEnabled}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.pulls_desc"}}</label>
</div>
</div>
<div class="field{{if not $pullRequestEnabled}} disabled{{end}}" id="pull_box">
<div class="field">
<p>
{{ctx.Locale.Tr "repo.settings.merge_style_desc"}}
</p>
</div>
<div class="field">
<div class="ui checkbox">
<input name="pulls_allow_merge" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowMerge)}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.pulls.merge_pull_request"}}</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input name="pulls_allow_rebase" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowRebase)}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.pulls.rebase_merge_pull_request"}}</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input name="pulls_allow_rebase_merge" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowRebaseMerge)}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.pulls.rebase_merge_commit_pull_request"}}</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input name="pulls_allow_squash" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowSquash)}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}}</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input name="pulls_allow_fast_forward_only" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowFastForwardOnly)}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}}</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input name="pulls_allow_manual_merge" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowManualMerge)}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.pulls.merge_manually"}}</label>
</div>
</div>
<div class="field">
<p>
{{ctx.Locale.Tr "repo.settings.default_merge_style_desc"}}
</p>
<div class="ui dropdown selection">
<select name="pulls_default_merge_style">
<option value="merge" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "merge")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.merge_pull_request"}}</option>
<option value="rebase" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "rebase")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.rebase_merge_pull_request"}}</option>
<option value="rebase-merge" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "rebase-merge")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.rebase_merge_commit_pull_request"}}</option>
<option value="squash" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "squash")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}}</option>
<option value="fast-forward-only" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "fast-forward-only")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}}</option>
</select>{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="default text">
{{if (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "merge")}}
{{ctx.Locale.Tr "repo.pulls.merge_pull_request"}}
{{end}}
{{if (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "rebase")}}
{{ctx.Locale.Tr "repo.pulls.rebase_merge_pull_request"}}
{{end}}
{{if (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "rebase-merge")}}
{{ctx.Locale.Tr "repo.pulls.rebase_merge_commit_pull_request"}}
{{end}}
{{if (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "squash")}}
{{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}}
{{end}}
{{if (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "fast-forward-only")}}
{{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}}
{{end}}
</div>
<div class="menu">
<div class="item" data-value="merge">{{ctx.Locale.Tr "repo.pulls.merge_pull_request"}}</div>
<div class="item" data-value="rebase">{{ctx.Locale.Tr "repo.pulls.rebase_merge_pull_request"}}</div>
<div class="item" data-value="rebase-merge">{{ctx.Locale.Tr "repo.pulls.rebase_merge_commit_pull_request"}}</div>
<div class="item" data-value="squash">{{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}}</div>
<div class="item" data-value="fast-forward-only">{{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}}</div>
</div>
</div>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pulls.default_target_branch"}}</label>
<div class="ui search selection dropdown">
<input type="hidden" name="default_target_branch" value="{{$prUnit.PullRequestsConfig.DefaultTargetBranch}}">
<div class="default text"></div>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
<div class="item" data-value="">{{ctx.Locale.Tr "repo.settings.pulls.default_target_branch_default" $.Repository.DefaultBranch}}</div>
{{range $branchName := $.Branches}}
<div class="item" data-value="{{$branchName}}">{{$branchName}}</div>
{{end}}
</div>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input name="default_allow_maintainer_edit" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.DefaultAllowMaintainerEdit)}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.pulls.default_allow_edits_from_maintainers"}}</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input name="pulls_allow_rebase_update" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowRebaseUpdate)}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.pulls.allow_rebase_update"}}</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input name="default_delete_branch_after_merge" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.DefaultDeleteBranchAfterMerge)}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.pulls.default_delete_branch_after_merge"}}</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input name="enable_autodetect_manual_merge" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AutodetectManualMerge)}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.pulls.enable_autodetect_manual_merge"}}</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input name="pulls_ignore_whitespace" type="checkbox" {{if and $pullRequestEnabled ($prUnit.PullRequestsConfig.IgnoreWhitespaceConflicts)}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.pulls.ignore_whitespace"}}</label>
</div>
</div>
</div>
{{end}}
<div class="field tw-mt-4">
<button class="ui primary button">{{ctx.Locale.Tr "repo.settings.update_settings"}}</button>
</div>
</form>
</div>
</div>
{{template "repo/settings/layout_footer" .}}
+116
View File
@@ -0,0 +1,116 @@
{{template "repo/settings/layout_head" (dict "pageClass" "repository settings custom-fields")}}
<div class="user-main-content twelve wide column">
<h4 class="ui top attached header tw-flex tw-items-center tw-justify-between">
<span>{{svg "octicon-list-unordered" 16}} {{ctx.Locale.Tr "repo.settings.custom_fields"}}</span>
<button class="ui primary small button show-modal" data-modal="#custom-field-create-modal">
{{svg "octicon-plus" 14}} {{ctx.Locale.Tr "repo.settings.custom_field_new"}}
</button>
</h4>
<div class="ui attached segment">
{{if .CustomFields}}
<table class="ui compact table">
<thead>
<tr>
<th>{{ctx.Locale.Tr "repo.settings.custom_field_name"}}</th>
<th>{{ctx.Locale.Tr "repo.settings.custom_field_type"}}</th>
<th>{{ctx.Locale.Tr "repo.settings.custom_field_required"}}</th>
<th>{{ctx.Locale.Tr "repo.licenses.status"}}</th>
<th></th>
</tr>
</thead>
<tbody>
{{range .CustomFields}}
<tr>
<td>
<strong>{{.Name}}</strong>
{{if .Description}}<br><small class="text grey">{{.Description}}</small>{{end}}
</td>
<td><code>{{.FieldType}}</code></td>
<td>{{if .Required}}{{svg "octicon-check" 14}}{{end}}</td>
<td>
{{if .IsActive}}<span class="ui green label">{{ctx.Locale.Tr "repo.licenses.active"}}</span>
{{else}}<span class="ui grey label">{{ctx.Locale.Tr "repo.licenses.inactive"}}</span>{{end}}
</td>
<td class="tw-text-right tw-flex tw-gap-1 tw-justify-end">
<button class="ui tiny button show-modal"
data-modal="#custom-field-edit-modal"
data-modal-custom-field-edit-modal-field-id="{{.ID}}"
data-modal-custom-field-edit-modal-field-name="{{.Name}}"
data-modal-custom-field-edit-modal-field-type="{{.FieldType}}"
data-modal-custom-field-edit-modal-field-desc="{{.Description}}"
data-modal-custom-field-edit-modal-field-options="{{.Options}}"
data-modal-custom-field-edit-modal-field-required="{{.Required}}"
data-modal-custom-field-edit-modal-field-active="{{.IsActive}}"
data-modal-custom-field-edit-modal-field-order="{{.SortOrder}}"
title="{{ctx.Locale.Tr "edit"}}">
{{svg "octicon-pencil" 14}}
</button>
<button class="ui tiny red button link-action"
data-url="{{$.RepoLink}}/settings/custom-fields/{{.ID}}/delete"
data-modal-confirm="{{ctx.Locale.Tr "repo.settings.custom_field_confirm_delete"}}"
title="{{ctx.Locale.Tr "delete"}}">
{{svg "octicon-trash" 14}}
</button>
</td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
<div class="empty-placeholder">
{{svg "octicon-list-unordered" 48}}
<h2>{{ctx.Locale.Tr "repo.settings.custom_fields_none"}}</h2>
<p>{{ctx.Locale.Tr "repo.settings.custom_fields_none_desc"}}</p>
</div>
{{end}}
</div>
</div>
{{/* Create Custom Field Modal */}}
<div class="ui small modal" id="custom-field-create-modal">
<div class="header">{{svg "octicon-plus"}} {{ctx.Locale.Tr "repo.settings.custom_field_new"}}</div>
<div class="content">
<form class="ui form" method="post" action="{{.RepoLink}}/settings/custom-fields">
{{$.CsrfTokenHtml}}
<div class="required field">
<label>{{ctx.Locale.Tr "repo.settings.custom_field_name"}}</label>
<input name="name" required placeholder="e.g. Priority, Sprint, Client Name">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.custom_field_type"}}</label>
<select name="field_type" class="ui dropdown">
<option value="text">Text</option>
<option value="number">Number</option>
<option value="date">Date</option>
<option value="dropdown">Dropdown</option>
<option value="checkbox">Checkbox</option>
<option value="url">URL</option>
</select>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.custom_field_description"}}</label>
<input name="description" placeholder="Help text shown below the field">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.custom_field_options"}}</label>
<input name="options" placeholder='["Option A","Option B","Option C"]'>
<p class="help">{{ctx.Locale.Tr "repo.settings.custom_field_options_help"}}</p>
</div>
<div class="two fields">
<div class="field">
<div class="ui checkbox">
<input name="required" type="checkbox">
<label>{{ctx.Locale.Tr "repo.settings.custom_field_required"}}</label>
</div>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.custom_field_sort_order"}}</label>
<input name="sort_order" type="number" value="0" min="0">
</div>
</div>
{{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.custom_field_create"))}}
</form>
</div>
</div>
{{template "repo/settings/layout_footer" .}}
+110
View File
@@ -0,0 +1,110 @@
{{template "repo/settings/layout_head" (dict "pageClass" "repository settings licensing")}}
<div class="user-main-content twelve wide column">
<h4 class="ui top attached header">
{{svg "octicon-broadcast" 16}} {{ctx.Locale.Tr "repo.settings.licensing_section"}}
</h4>
<div class="ui attached segment">
<form class="ui form" method="post" action="{{.Link}}">
<input type="hidden" name="action" value="licensing">
<div class="inline field">
<div class="ui checkbox">
<input name="enable_licensing" type="checkbox" {{if and .RepoUpdateConfig .RepoUpdateConfig.LicensingEnabled}}checked{{end}}>
<label><strong>{{ctx.Locale.Tr "repo.settings.enable_licensing"}}</strong></label>
</div>
<p class="help">{{ctx.Locale.Tr "repo.settings.licensing_section_desc"}}</p>
</div>
<div class="ui divider"></div>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.settings.update_platform"}}</label>
<select name="update_platform" class="ui dropdown">
<option value="joomla" {{if or (not .RepoUpdateConfig) (eq .RepoUpdateConfig.Platform "joomla") (eq .RepoUpdateConfig.Platform "")}}selected{{end}}>Joomla (updates.xml)</option>
<option value="dolibarr" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.Platform "dolibarr")}}selected{{end}}>Dolibarr (JSON)</option>
<option value="wordpress" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.Platform "wordpress")}}selected{{end}}>WordPress (JSON)</option>
<option value="composer" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.Platform "composer")}}selected{{end}}>Composer (packages.json)</option>
<option value="prestashop" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.Platform "prestashop")}}selected{{end}}>PrestaShop</option>
<option value="drupal" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.Platform "drupal")}}selected{{end}}>Drupal</option>
<option value="both" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.Platform "both")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.update_platform_both"}}</option>
</select>
<p class="help">{{ctx.Locale.Tr "repo.settings.update_platform_help"}}</p>
</div>
<div class="inline field">
<div class="ui checkbox">
<input name="require_update_key" type="checkbox" {{if and .RepoUpdateConfig .RepoUpdateConfig.RequireKey}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.require_update_key"}}</label>
</div>
<p class="help">{{ctx.Locale.Tr "repo.settings.require_update_key_help"}}</p>
</div>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.settings.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}}>{{ctx.Locale.Tr "org.settings.download_gating_none"}}</option>
<option value="prerelease" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.DownloadGating "prerelease")}}selected{{end}}>{{ctx.Locale.Tr "org.settings.download_gating_prerelease"}}</option>
<option value="all" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.DownloadGating "all")}}selected{{end}}>{{ctx.Locale.Tr "org.settings.download_gating_all"}}</option>
</select>
<p class="help">{{ctx.Locale.Tr "org.settings.download_gating_help"}}</p>
</div>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.settings.support_url"}}</label>
<input name="support_url" value="{{if .RepoUpdateConfig}}{{.RepoUpdateConfig.SupportURL}}{{end}}" placeholder="https://mokoconsulting.tech/support">
<p class="help">{{ctx.Locale.Tr "repo.settings.support_url_help"}}</p>
</div>
<div class="ui divider"></div>
<h5>{{ctx.Locale.Tr "repo.settings.extension_metadata"}}</h5>
<p class="help tw-mb-4">{{ctx.Locale.Tr "repo.settings.extension_metadata_desc"}}</p>
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.extension_name"}}</label>
<input name="extension_name" value="{{if .RepoUpdateConfig}}{{.RepoUpdateConfig.ExtensionName}}{{end}}" placeholder="pkg_myextension">
<p class="help">{{ctx.Locale.Tr "org.settings.extension_name_help"}}</p>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.display_name"}}</label>
<input name="display_name" value="{{if .RepoUpdateConfig}}{{.RepoUpdateConfig.DisplayName}}{{end}}" placeholder="Package - My Extension">
<p class="help">{{ctx.Locale.Tr "org.settings.display_name_help"}}</p>
</div>
</div>
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.extension_type"}}</label>
<select name="extension_type" class="ui dropdown">
<option value="">{{ctx.Locale.Tr "repo.settings.inherit_org"}}</option>
<option value="package" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.ExtensionType "package")}}selected{{end}}>Package</option>
<option value="component" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.ExtensionType "component")}}selected{{end}}>Component</option>
<option value="module" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.ExtensionType "module")}}selected{{end}}>Module</option>
<option value="plugin" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.ExtensionType "plugin")}}selected{{end}}>Plugin</option>
<option value="template" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.ExtensionType "template")}}selected{{end}}>Template</option>
<option value="library" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.ExtensionType "library")}}selected{{end}}>Library</option>
</select>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.target_version"}}</label>
<input name="target_version" value="{{if .RepoUpdateConfig}}{{.RepoUpdateConfig.TargetVersion}}{{end}}" placeholder="(5|6)\..*">
<p class="help">{{ctx.Locale.Tr "org.settings.target_version_help"}}</p>
</div>
</div>
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.maintainer"}}</label>
<input name="maintainer" value="{{if .RepoUpdateConfig}}{{.RepoUpdateConfig.Maintainer}}{{end}}" placeholder="Moko Consulting">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.php_minimum"}}</label>
<input name="php_minimum" value="{{if .RepoUpdateConfig}}{{.RepoUpdateConfig.PHPMinimum}}{{end}}" placeholder="8.1">
</div>
</div>
<div class="field">
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
</div>
</form>
</div>
</div>
{{template "repo/settings/layout_footer" .}}
+49
View File
@@ -0,0 +1,49 @@
{{template "repo/settings/layout_head" (dict "pageClass" "repository settings metadata")}}
<div class="user-main-content twelve wide column">
<h4 class="ui top attached header">
{{svg "octicon-list-unordered" 16}} {{ctx.Locale.Tr "repo.settings.metadata"}}
</h4>
<div class="ui attached segment">
{{if .CustomFieldDefs}}
<form class="ui form" method="post" action="{{.RepoLink}}/settings/metadata">
{{.CsrfTokenHtml}}
{{$values := .CustomFieldValues}}
{{$options := .CustomFieldOptions}}
{{range .CustomFieldDefs}}
{{$currentVal := index $values .ID}}
<div class="field">
<label>{{.Name}}{{if .Description}} <small class="text grey">({{.Description}})</small>{{end}}</label>
{{if .Options}}
{{$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}}
<div class="field tw-mt-4">
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "save"}}</button>
</div>
</form>
{{else}}
<p class="text grey">{{ctx.Locale.Tr "repo.settings.metadata_empty"}}</p>
{{end}}
</div>
</div>
{{template "repo/settings/layout_footer" .}}
+22 -11
View File
@@ -1,45 +1,56 @@
<div class="flex-container-nav">
<div class="ui fluid vertical menu">
<div class="header item">{{ctx.Locale.Tr "repo.settings"}}</div>
<div class="header item">{{svg "octicon-gear"}} {{ctx.Locale.Tr "repo.settings"}}</div>
<a class="{{if .PageIsSettingsOptions}}active {{end}}item" href="{{.RepoLink}}/settings">
{{ctx.Locale.Tr "repo.settings.options"}}
{{svg "octicon-gear"}} {{ctx.Locale.Tr "repo.settings.options"}}
</a>
<a class="{{if .PageIsSettingsAdvanced}}active {{end}}item" href="{{.RepoLink}}/settings/advanced">
{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings.advanced_settings"}}
</a>
{{if .LicensingEnabled}}
<a class="{{if .PageIsSettingsLicensing}}active {{end}}item" href="{{.RepoLink}}/settings/licensing">
{{svg "octicon-broadcast"}} {{ctx.Locale.Tr "repo.settings.licensing_section"}}
</a>
{{end}}
<a class="{{if .PageIsSettingsMetadata}}active {{end}}item" href="{{.RepoLink}}/settings/metadata">
{{svg "octicon-list-unordered"}} {{ctx.Locale.Tr "repo.settings.metadata"}}
</a>
{{if or .Repository.IsPrivate .Permission.HasAnyUnitPublicAccess}}
<a class="{{if .PageIsSettingsPublicAccess}}active {{end}}item" href="{{.RepoLink}}/settings/public_access">
{{ctx.Locale.Tr "repo.settings.public_access"}}
{{svg "octicon-eye"}} {{ctx.Locale.Tr "repo.settings.public_access"}}
</a>
{{end}}
<a class="{{if .PageIsSettingsCollaboration}}active {{end}}item" href="{{.RepoLink}}/settings/collaboration">
{{ctx.Locale.Tr "repo.settings.collaboration"}}
{{svg "octicon-people"}} {{ctx.Locale.Tr "repo.settings.collaboration"}}
</a>
{{if not DisableWebhooks}}
<a class="{{if .PageIsSettingsHooks}}active {{end}}item" href="{{.RepoLink}}/settings/hooks">
{{ctx.Locale.Tr "repo.settings.hooks"}}
{{svg "octicon-webhook"}} {{ctx.Locale.Tr "repo.settings.hooks"}}
</a>
{{end}}
{{if .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeCode}}
<a class="{{if .PageIsSettingsBranches}}active {{end}}item" href="{{.RepoLink}}/settings/branches">
{{ctx.Locale.Tr "repo.settings.branches"}}
{{svg "octicon-git-branch"}} {{ctx.Locale.Tr "repo.settings.branches"}}
</a>
<a class="{{if .PageIsSettingsTags}}active {{end}}item" href="{{.RepoLink}}/settings/tags">
{{ctx.Locale.Tr "repo.settings.tags"}}
{{svg "octicon-tag"}} {{ctx.Locale.Tr "repo.settings.tags"}}
</a>
{{if .SignedUser.CanEditGitHook}}
<a class="{{if .PageIsSettingsGitHooks}}active {{end}}item" href="{{.RepoLink}}/settings/hooks/git">
{{ctx.Locale.Tr "repo.settings.githooks"}}
{{svg "octicon-terminal"}} {{ctx.Locale.Tr "repo.settings.githooks"}}
</a>
{{end}}
<a class="{{if .PageIsSettingsKeys}}active {{end}}item" href="{{.RepoLink}}/settings/keys">
{{ctx.Locale.Tr "repo.settings.deploy_keys"}}
{{svg "octicon-key-asterisk"}} {{ctx.Locale.Tr "repo.settings.deploy_keys"}}
</a>
{{if .LFSStartServer}}
<a class="{{if .PageIsSettingsLFS}}active {{end}}item" href="{{.RepoLink}}/settings/lfs">
{{ctx.Locale.Tr "repo.settings.lfs"}}
{{svg "octicon-file-binary"}} {{ctx.Locale.Tr "repo.settings.lfs"}}
</a>
{{end}}
{{end}}
<details class="item toggleable-item" {{if or .PageIsSharedSettingsRunners .PageIsSharedSettingsSecrets .PageIsSharedSettingsVariables .PageIsActionsSettingsGeneral}}open{{end}}>
<summary>{{ctx.Locale.Tr "actions.actions"}}</summary>
<summary>{{svg "octicon-play"}} {{ctx.Locale.Tr "actions.actions"}}</summary>
<div class="menu">
<a class="{{if .PageIsActionsSettingsGeneral}}active {{end}}item" href="{{.RepoLink}}/settings/actions/general">
{{ctx.Locale.Tr "actions.general"}}
+46 -521
View File
@@ -287,481 +287,6 @@
</div>
{{end}}
{{/* FIXME: need to split the "Advance Settings" by units, there are too many options here */}}
<h4 class="ui top attached header">
{{ctx.Locale.Tr "repo.settings.advanced_settings"}}
</h4>
<div class="ui attached segment">
<form class="ui form" method="post">
<input type="hidden" name="action" value="advanced">
{{$isCodeEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeCode}}
{{$isCodeGlobalDisabled := ctx.Consts.RepoUnitTypeCode.UnitGlobalDisabled}}
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.code"}}</label>
<div class="ui checkbox{{if $isCodeGlobalDisabled}} disabled{{end}}"{{if $isCodeGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
<input class="enable-system" name="enable_code" type="checkbox"{{if $isCodeEnabled}} checked{{end}}>
<label>{{ctx.Locale.Tr "repo.code.desc"}}</label>
</div>
</div>
{{$isInternalWikiEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeWiki}}
{{$isExternalWikiEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalWiki}}
{{$isWikiEnabled := or $isInternalWikiEnabled $isExternalWikiEnabled}}
{{$isWikiGlobalDisabled := ctx.Consts.RepoUnitTypeWiki.UnitGlobalDisabled}}
{{$isExternalWikiGlobalDisabled := ctx.Consts.RepoUnitTypeExternalWiki.UnitGlobalDisabled}}
{{$isBothWikiGlobalDisabled := and $isWikiGlobalDisabled $isExternalWikiGlobalDisabled}}
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.wiki"}}</label>
<div class="ui checkbox{{if $isBothWikiGlobalDisabled}} disabled{{end}}"{{if $isBothWikiGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
<input class="enable-system" name="enable_wiki" type="checkbox" data-target="#wiki_box" {{if $isWikiEnabled}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.wiki_desc"}}</label>
</div>
</div>
<div class="field{{if not $isWikiEnabled}} disabled{{end}}" id="wiki_box">
<div class="field">
<div class="ui radio checkbox{{if $isWikiGlobalDisabled}} disabled{{end}}"{{if $isWikiGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
<input class="enable-system-radio" name="enable_external_wiki" type="radio" value="false" data-context="#internal_wiki_box" data-target="#external_wiki_box" {{if $isInternalWikiEnabled}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.use_internal_wiki"}}</label>
</div>
</div>
<div id="internal_wiki_box" class="field tw-pl-4 {{if not $isInternalWikiEnabled}}disabled{{end}}">
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.settings.default_wiki_branch_name"}}</label>
<input name="default_wiki_branch" value="{{.Repository.DefaultWikiBranch}}">
</div>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.settings.unit_visibility"}}</label>
<select name="wiki_visibility" class="ui dropdown">
<option value="not-set" {{if not (eq (.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeWiki).AnonymousAccessMode 1)}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.unit_visibility_private"}}</option>
<option value="anonymous-read" {{if eq (.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeWiki).AnonymousAccessMode 1}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.unit_visibility_public"}}</option>
</select>
</div>
</div>
<div class="field">
<div class="ui radio checkbox{{if $isExternalWikiGlobalDisabled}} disabled{{end}}"{{if $isExternalWikiGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
<input class="enable-system-radio" name="enable_external_wiki" type="radio" value="true" data-context="#internal_wiki_box" data-target="#external_wiki_box" {{if $isExternalWikiEnabled}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.use_external_wiki"}}</label>
</div>
</div>
<div id="external_wiki_box" class="field tw-pl-4 {{if not $isExternalWikiEnabled}}disabled{{end}}">
<label for="external_wiki_url">{{ctx.Locale.Tr "repo.settings.external_wiki_url"}}</label>
<input id="external_wiki_url" name="external_wiki_url" type="url" value="{{(.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeExternalWiki).ExternalWikiConfig.ExternalWikiURL}}">
<p class="help">{{ctx.Locale.Tr "repo.settings.external_wiki_url_desc"}}</p>
</div>
</div>
<div class="divider"></div>
{{$isIssuesEnabled := or (.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeIssues) (.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalTracker)}}
{{$isIssuesGlobalDisabled := ctx.Consts.RepoUnitTypeIssues.UnitGlobalDisabled}}
{{$isExternalTrackerGlobalDisabled := ctx.Consts.RepoUnitTypeExternalTracker.UnitGlobalDisabled}}
{{$isIssuesAndExternalGlobalDisabled := and $isIssuesGlobalDisabled $isExternalTrackerGlobalDisabled}}
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.issues"}}</label>
<div class="ui checkbox{{if $isIssuesAndExternalGlobalDisabled}} disabled{{end}}"{{if $isIssuesAndExternalGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
<input class="enable-system" name="enable_issues" type="checkbox" data-target="#issue_box" {{if $isIssuesEnabled}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.issues_desc"}}</label>
</div>
</div>
<div class="field {{if not $isIssuesEnabled}}disabled{{end}}" id="issue_box">
<div class="field">
<div class="ui radio checkbox{{if $isIssuesGlobalDisabled}} disabled{{end}}"{{if $isIssuesGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
<input class="enable-system-radio" name="enable_external_tracker" type="radio" value="false" data-context="#internal_issue_box" data-target="#external_issue_box" {{if not (.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalTracker)}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.use_internal_issue_tracker"}}</label>
</div>
</div>
<div class="field tw-pl-4 {{if (.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalTracker)}}disabled{{end}}" id="internal_issue_box">
{{if .Repository.CanEnableTimetracker}}
<div class="field">
<div class="ui checkbox">
<input name="enable_timetracker" class="enable-system" data-target="#only_contributors" type="checkbox" {{if .Repository.IsTimetrackerEnabled ctx}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.enable_timetracker"}}</label>
</div>
</div>
<div class="field {{if not (.Repository.IsTimetrackerEnabled ctx)}}disabled{{end}}" id="only_contributors">
<div class="ui checkbox">
<input name="allow_only_contributors_to_track_time" type="checkbox" {{if .Repository.AllowOnlyContributorsToTrackTime ctx}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.allow_only_contributors_to_track_time"}}</label>
</div>
</div>
{{end}}
<div class="field">
<div class="ui checkbox">
<input name="enable_issue_dependencies" type="checkbox" {{if (.Repository.IsDependenciesEnabled ctx)}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.issues.dependency.setting"}}</label>
</div>
</div>
<div class="ui checkbox">
<input name="enable_close_issues_via_commit_in_any_branch" type="checkbox" {{if .Repository.CloseIssuesViaCommitInAnyBranch}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.admin_enable_close_issues_via_commit_in_any_branch"}}</label>
</div>
<div class="inline field tw-mt-2">
<label>{{ctx.Locale.Tr "repo.settings.unit_visibility"}}</label>
<select name="issues_visibility" class="ui dropdown">
<option value="not-set" {{if not (eq (.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeIssues).AnonymousAccessMode 1)}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.unit_visibility_private"}}</option>
<option value="anonymous-read" {{if eq (.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeIssues).AnonymousAccessMode 1}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.unit_visibility_public"}}</option>
</select>
</div>
</div>
<div class="field">
<div class="ui radio checkbox{{if $isExternalTrackerGlobalDisabled}} disabled{{end}}"{{if $isExternalTrackerGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
<input class="enable-system-radio" name="enable_external_tracker" type="radio" value="true" data-context="#internal_issue_box" data-target="#external_issue_box" {{if .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalTracker}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.use_external_issue_tracker"}}</label>
</div>
</div>
<div class="field tw-pl-4 {{if not (.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalTracker)}}disabled{{end}}" id="external_issue_box">
<div class="field">
<label for="external_tracker_url">{{ctx.Locale.Tr "repo.settings.external_tracker_url"}}</label>
<input id="external_tracker_url" name="external_tracker_url" type="url" value="{{(.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeExternalTracker).ExternalTrackerConfig.ExternalTrackerURL}}">
<p class="help">{{ctx.Locale.Tr "repo.settings.external_tracker_url_desc"}}</p>
</div>
<div class="field">
<label for="tracker_url_format">{{ctx.Locale.Tr "repo.settings.tracker_url_format"}}</label>
<input id="tracker_url_format" name="tracker_url_format" type="url" value="{{(.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeExternalTracker).ExternalTrackerConfig.ExternalTrackerFormat}}" placeholder="https://github.com/{user}/{repo}/issues/{index}">
<p class="help">{{ctx.Locale.Tr "repo.settings.tracker_url_format_desc"}}</p>
</div>
<div class="inline fields">
<label for="issue_style">{{ctx.Locale.Tr "repo.settings.tracker_issue_style"}}</label>
<div class="field">
<div class="ui radio checkbox">
{{$externalTracker := (.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeExternalTracker)}}
{{$externalTrackerStyle := $externalTracker.ExternalTrackerConfig.ExternalTrackerStyle}}
<input class="js-tracker-issue-style" name="tracker_issue_style" type="radio" value="numeric" {{if eq $externalTrackerStyle "numeric"}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.tracker_issue_style.numeric"}} <span class="ui light grey text">#1234</span></label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input class="js-tracker-issue-style" name="tracker_issue_style" type="radio" value="alphanumeric" {{if eq $externalTrackerStyle "alphanumeric"}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.tracker_issue_style.alphanumeric"}} <span class="ui light grey text">ABC-123 , DEFG-234</span></label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input class="js-tracker-issue-style" name="tracker_issue_style" type="radio" value="regexp" {{if eq $externalTrackerStyle "regexp"}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.tracker_issue_style.regexp"}} <span class="ui light grey text">(ISSUE-\d+) , ISSUE-(\d+)</span></label>
</div>
</div>
</div>
<div class="field {{if ne $externalTrackerStyle "regexp"}}disabled{{end}}" id="tracker-issue-style-regex-box">
<label for="external_tracker_regexp_pattern">{{ctx.Locale.Tr "repo.settings.tracker_issue_style.regexp_pattern"}}</label>
<input id="external_tracker_regexp_pattern" name="external_tracker_regexp_pattern" value="{{(.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeExternalTracker).ExternalTrackerConfig.ExternalTrackerRegexpPattern}}">
<p class="help">{{ctx.Locale.Tr "repo.settings.tracker_issue_style.regexp_pattern_desc"}}</p>
</div>
</div>
</div>
<div class="divider"></div>
{{$isProjectsEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeProjects}}
{{$isProjectsGlobalDisabled := ctx.Consts.RepoUnitTypeProjects.UnitGlobalDisabled}}
{{$projectsUnit := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeProjects}}
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.projects"}}</label>
<div class="ui checkbox{{if $isProjectsGlobalDisabled}} disabled{{end}}"{{if $isProjectsGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
<input class="enable-system" name="enable_projects" type="checkbox" data-target="#projects_box" {{if $isProjectsEnabled}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.projects_desc"}}</label>
</div>
</div>
<div class="field {{if not $isProjectsEnabled}} disabled{{end}} tw-pl-4" id="projects_box">
<p>
{{ctx.Locale.Tr "repo.settings.projects_mode_desc"}}
</p>
<div class="ui dropdown selection">
<select name="projects_mode">
<option value="repo" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.GetProjectsMode "repo")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_repo"}}</option>
<option value="owner" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.GetProjectsMode "owner")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_owner"}}</option>
<option value="all" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.GetProjectsMode "all")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_all"}}</option>
</select>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="default text">
{{if (eq $projectsUnit.ProjectsConfig.GetProjectsMode "repo")}}
{{ctx.Locale.Tr "repo.settings.projects_mode_repo"}}
{{end}}
{{if (eq $projectsUnit.ProjectsConfig.GetProjectsMode "owner")}}
{{ctx.Locale.Tr "repo.settings.projects_mode_owner"}}
{{end}}
{{if (eq $projectsUnit.ProjectsConfig.GetProjectsMode "all")}}
{{ctx.Locale.Tr "repo.settings.projects_mode_all"}}
{{end}}
</div>
<div class="menu">
<div class="item" data-value="repo">{{ctx.Locale.Tr "repo.settings.projects_mode_repo"}}</div>
<div class="item" data-value="owner">{{ctx.Locale.Tr "repo.settings.projects_mode_owner"}}</div>
<div class="item" data-value="all">{{ctx.Locale.Tr "repo.settings.projects_mode_all"}}</div>
</div>
</div>
</div>
<div class="divider"></div>
{{$isReleasesEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeReleases}}
{{$isReleasesGlobalDisabled := ctx.Consts.RepoUnitTypeReleases.UnitGlobalDisabled}}
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.releases"}}</label>
<div class="ui checkbox{{if $isReleasesGlobalDisabled}} disabled{{end}}"{{if $isReleasesGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
<input class="enable-system" name="enable_releases" type="checkbox" data-target="#releases_visibility_box" {{if $isReleasesEnabled}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.releases_desc"}}</label>
</div>
</div>
<div class="field tw-pl-4{{if not $isReleasesEnabled}} disabled{{end}}" id="releases_visibility_box">
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.settings.unit_visibility"}}</label>
<select name="releases_visibility" class="ui dropdown">
<option value="not-set" {{if not (eq (.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeReleases).AnonymousAccessMode 1)}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.unit_visibility_private"}}</option>
<option value="anonymous-read" {{if eq (.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeReleases).AnonymousAccessMode 1}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.unit_visibility_public"}}</option>
</select>
<p class="help">{{ctx.Locale.Tr "repo.settings.unit_visibility_releases_help"}}</p>
</div>
</div>
<div class="divider"></div>
{{/* Licensing & Update Feeds */}}
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.settings.licensing_section"}}</label>
<div class="ui checkbox">
<input class="enable-system" name="enable_licensing" type="checkbox" data-target="#licensing_options_box" {{if and .RepoUpdateConfig .RepoUpdateConfig.LicensingEnabled}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.enable_licensing"}}</label>
</div>
</div>
<div class="field tw-pl-4{{if not (and .RepoUpdateConfig .RepoUpdateConfig.LicensingEnabled)}} disabled{{end}}" id="licensing_options_box">
<p class="help tw-mb-4">{{ctx.Locale.Tr "repo.settings.licensing_section_desc"}}</p>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.settings.update_platform"}}</label>
<select name="update_platform" class="ui dropdown">
<option value="joomla" {{if or (not .RepoUpdateConfig) (eq .RepoUpdateConfig.Platform "joomla") (eq .RepoUpdateConfig.Platform "")}}selected{{end}}>Joomla (updates.xml)</option>
<option value="dolibarr" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.Platform "dolibarr")}}selected{{end}}>Dolibarr (JSON)</option>
<option value="both" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.Platform "both")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.update_platform_both"}}</option>
</select>
<p class="help">{{ctx.Locale.Tr "repo.settings.update_platform_help"}}</p>
</div>
<div class="inline field">
<div class="ui checkbox">
<input name="require_update_key" type="checkbox" {{if and .RepoUpdateConfig .RepoUpdateConfig.RequireKey}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.require_update_key"}}</label>
</div>
<p class="help">{{ctx.Locale.Tr "repo.settings.require_update_key_help"}}</p>
</div>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.settings.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}}>{{ctx.Locale.Tr "org.settings.download_gating_none"}}</option>
<option value="prerelease" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.DownloadGating "prerelease")}}selected{{end}}>{{ctx.Locale.Tr "org.settings.download_gating_prerelease"}}</option>
<option value="all" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.DownloadGating "all")}}selected{{end}}>{{ctx.Locale.Tr "org.settings.download_gating_all"}}</option>
</select>
<p class="help">{{ctx.Locale.Tr "org.settings.download_gating_help"}}</p>
</div>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.settings.support_url"}}</label>
<input name="support_url" value="{{if .RepoUpdateConfig}}{{.RepoUpdateConfig.SupportURL}}{{end}}" placeholder="https://mokoconsulting.tech/support">
<p class="help">{{ctx.Locale.Tr "repo.settings.support_url_help"}}</p>
</div>
<div class="ui divider"></div>
<h6>{{ctx.Locale.Tr "repo.settings.extension_metadata"}}</h6>
<p class="help tw-mb-4">{{ctx.Locale.Tr "repo.settings.extension_metadata_desc"}}</p>
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.extension_name"}}</label>
<input name="extension_name" value="{{if .RepoUpdateConfig}}{{.RepoUpdateConfig.ExtensionName}}{{end}}" placeholder="pkg_myextension">
<p class="help">{{ctx.Locale.Tr "org.settings.extension_name_help"}}</p>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.display_name"}}</label>
<input name="display_name" value="{{if .RepoUpdateConfig}}{{.RepoUpdateConfig.DisplayName}}{{end}}" placeholder="Package - My Extension">
<p class="help">{{ctx.Locale.Tr "org.settings.display_name_help"}}</p>
</div>
</div>
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.extension_type"}}</label>
<select name="extension_type" class="ui dropdown">
<option value="">{{ctx.Locale.Tr "repo.settings.inherit_org"}}</option>
<option value="package" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.ExtensionType "package")}}selected{{end}}>Package</option>
<option value="component" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.ExtensionType "component")}}selected{{end}}>Component</option>
<option value="module" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.ExtensionType "module")}}selected{{end}}>Module</option>
<option value="plugin" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.ExtensionType "plugin")}}selected{{end}}>Plugin</option>
<option value="template" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.ExtensionType "template")}}selected{{end}}>Template</option>
<option value="library" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.ExtensionType "library")}}selected{{end}}>Library</option>
</select>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.target_version"}}</label>
<input name="target_version" value="{{if .RepoUpdateConfig}}{{.RepoUpdateConfig.TargetVersion}}{{end}}" placeholder="(5|6)\..*">
<p class="help">{{ctx.Locale.Tr "org.settings.target_version_help"}}</p>
</div>
</div>
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.maintainer"}}</label>
<input name="maintainer" value="{{if .RepoUpdateConfig}}{{.RepoUpdateConfig.Maintainer}}{{end}}" placeholder="Moko Consulting">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.php_minimum"}}</label>
<input name="php_minimum" value="{{if .RepoUpdateConfig}}{{.RepoUpdateConfig.PHPMinimum}}{{end}}" placeholder="8.1">
</div>
</div>
</div>
{{$isPackagesEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypePackages}}
{{$isPackagesGlobalDisabled := ctx.Consts.RepoUnitTypePackages.UnitGlobalDisabled}}
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.packages"}}</label>
<div class="ui checkbox{{if $isPackagesGlobalDisabled}} disabled{{end}}"{{if $isPackagesGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
<input class="enable-system" name="enable_packages" type="checkbox" {{if $isPackagesEnabled}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.packages_desc"}}</label>
</div>
</div>
{{if not .IsMirror}}
<div class="divider"></div>
{{$pullRequestEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypePullRequests}}
{{$pullRequestGlobalDisabled := ctx.Consts.RepoUnitTypePullRequests.UnitGlobalDisabled}}
{{$prUnit := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypePullRequests}}
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.pulls"}}</label>
<div class="ui checkbox{{if $pullRequestGlobalDisabled}} disabled{{end}}"{{if $pullRequestGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
<input class="enable-system" name="enable_pulls" type="checkbox" data-target="#pull_box" {{if $pullRequestEnabled}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.pulls_desc"}}</label>
</div>
</div>
<div class="field{{if not $pullRequestEnabled}} disabled{{end}}" id="pull_box">
<div class="field">
<p>
{{ctx.Locale.Tr "repo.settings.merge_style_desc"}}
</p>
</div>
<div class="field">
<div class="ui checkbox">
<input name="pulls_allow_merge" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowMerge)}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.pulls.merge_pull_request"}}</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input name="pulls_allow_rebase" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowRebase)}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.pulls.rebase_merge_pull_request"}}</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input name="pulls_allow_rebase_merge" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowRebaseMerge)}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.pulls.rebase_merge_commit_pull_request"}}</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input name="pulls_allow_squash" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowSquash)}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}}</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input name="pulls_allow_fast_forward_only" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowFastForwardOnly)}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}}</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input name="pulls_allow_manual_merge" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowManualMerge)}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.pulls.merge_manually"}}</label>
</div>
</div>
<div class="field">
<p>
{{ctx.Locale.Tr "repo.settings.default_merge_style_desc"}}
</p>
<div class="ui dropdown selection">
<select name="pulls_default_merge_style">
<option value="merge" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "merge")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.merge_pull_request"}}</option>
<option value="rebase" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "rebase")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.rebase_merge_pull_request"}}</option>
<option value="rebase-merge" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "rebase-merge")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.rebase_merge_commit_pull_request"}}</option>
<option value="squash" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "squash")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}}</option>
<option value="fast-forward-only" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "fast-forward-only")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}}</option>
</select>{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="default text">
{{if (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "merge")}}
{{ctx.Locale.Tr "repo.pulls.merge_pull_request"}}
{{end}}
{{if (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "rebase")}}
{{ctx.Locale.Tr "repo.pulls.rebase_merge_pull_request"}}
{{end}}
{{if (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "rebase-merge")}}
{{ctx.Locale.Tr "repo.pulls.rebase_merge_commit_pull_request"}}
{{end}}
{{if (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "squash")}}
{{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}}
{{end}}
{{if (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "fast-forward-only")}}
{{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}}
{{end}}
</div>
<div class="menu">
<div class="item" data-value="merge">{{ctx.Locale.Tr "repo.pulls.merge_pull_request"}}</div>
<div class="item" data-value="rebase">{{ctx.Locale.Tr "repo.pulls.rebase_merge_pull_request"}}</div>
<div class="item" data-value="rebase-merge">{{ctx.Locale.Tr "repo.pulls.rebase_merge_commit_pull_request"}}</div>
<div class="item" data-value="squash">{{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}}</div>
<div class="item" data-value="fast-forward-only">{{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}}</div>
</div>
</div>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.pulls.default_target_branch"}}</label>
<div class="ui search selection dropdown">
<input type="hidden" name="default_target_branch" value="{{$prUnit.PullRequestsConfig.DefaultTargetBranch}}">
<div class="default text"></div>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
<div class="item" data-value="">{{ctx.Locale.Tr "repo.settings.pulls.default_target_branch_default" $.Repository.DefaultBranch}}</div>
{{range $branchName := $.Branches}}
<div class="item" data-value="{{$branchName}}">{{$branchName}}</div>
{{end}}
</div>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input name="default_allow_maintainer_edit" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.DefaultAllowMaintainerEdit)}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.pulls.default_allow_edits_from_maintainers"}}</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input name="pulls_allow_rebase_update" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowRebaseUpdate)}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.pulls.allow_rebase_update"}}</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input name="default_delete_branch_after_merge" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.DefaultDeleteBranchAfterMerge)}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.pulls.default_delete_branch_after_merge"}}</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input name="enable_autodetect_manual_merge" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AutodetectManualMerge)}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.pulls.enable_autodetect_manual_merge"}}</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input name="pulls_ignore_whitespace" type="checkbox" {{if and $pullRequestEnabled ($prUnit.PullRequestsConfig.IgnoreWhitespaceConflicts)}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.pulls.ignore_whitespace"}}</label>
</div>
</div>
</div>
{{end}}
<div class="divider"></div>
<div class="field">
<button class="ui primary button">{{ctx.Locale.Tr "repo.settings.update_settings"}}</button>
</div>
</form>
</div>
<h4 class="ui top attached header">
{{ctx.Locale.Tr "repo.settings.signing_settings"}}
@@ -879,23 +404,34 @@
{{if not .Repository.IsFork}}
<div class="item tw-items-center">
<div class="item-main">
<div class="item-title">{{ctx.Locale.Tr "repo.visibility"}}</div>
{{if .IsSystemRepo}}
<div class="item-body">This is a system repository (dot-prefixed name). System repositories are always private and cannot be made public.</div>
{{else if .Repository.IsPrivate}}
<div class="item-body">{{ctx.Locale.Tr "repo.settings.visibility.public.text"}}</div>
{{else}}
<div class="item-body">{{ctx.Locale.Tr "repo.settings.visibility.private.text"}}</div>
{{end}}
<div class="item-title tw-flex tw-items-center tw-justify-between">
<span>{{ctx.Locale.Tr "repo.visibility"}}</span>
{{if .IsSystemRepo}}
<span class="ui grey label">System</span>
{{else if .Repository.IsHidden}}
<span class="ui red label">{{ctx.Locale.Tr "repo.settings.visibility.hidden.label"}}</span>
{{else if .Repository.IsPrivate}}
<span class="ui orange label">{{ctx.Locale.Tr "repo.settings.visibility.private.label"}}</span>
{{else}}
<span class="ui green label">{{ctx.Locale.Tr "repo.settings.visibility.public.label"}}</span>
{{end}}
</div>
<div class="item-body">
{{if .IsSystemRepo}}
This is a system repository. System repositories are always private.
{{else if .Repository.IsHidden}}
{{ctx.Locale.Tr "repo.settings.visibility.hidden.desc"}}
{{else if .Repository.IsPrivate}}
{{ctx.Locale.Tr "repo.settings.visibility.private.desc"}}
{{else}}
{{ctx.Locale.Tr "repo.settings.visibility.public.desc"}}
{{end}}
</div>
</div>
{{if not .IsSystemRepo}}
<div class="item-trailing">
<button class="ui basic red show-modal button" data-modal="#visibility-repo-modal">
{{if .Repository.IsPrivate}}
{{ctx.Locale.Tr "repo.settings.visibility.public.button"}}
{{else}}
{{ctx.Locale.Tr "repo.settings.visibility.private.button"}}
{{end}}
{{ctx.Locale.Tr "repo.settings.change_visibility"}}
</button>
</div>
{{end}}
@@ -1078,43 +614,32 @@
{{ctx.Locale.Tr "repo.visibility"}}
</div>
<div class="content">
{{if .Repository.IsPrivate}}
<p>{{ctx.Locale.Tr "repo.settings.visibility.public.bullet_title"}}</p>
<ul>
<li>{{ctx.Locale.Tr "repo.settings.visibility.public.bullet_one"}}</li>
</ul>
{{else}}
<p>{{ctx.Locale.Tr "repo.settings.visibility.private.bullet_title"}}</p>
<ul>
<li>{{ctx.Locale.Tr "repo.settings.visibility.private.bullet_one"}}</li>
<li>
{{ctx.Locale.Tr "repo.settings.visibility.private.bullet_two"}}
</li>
{{if or .Repository.NumStars .Repository.NumWatches .Repository.NumForks}}
<ul class="tw-my-0 tw-pl-4">
{{if .Repository.NumStars}}<li>{{ctx.Locale.Tr "repo.settings.visibility.private.stats_stars" .Repository.NumStars}}</li>{{end}}
{{if .Repository.NumWatches}}<li>{{ctx.Locale.Tr "repo.settings.visibility.private.stats_watchers" .Repository.NumWatches}}</li>{{end}}
{{if .Repository.NumForks}}<li>{{ctx.Locale.Tr "repo.settings.visibility.private.stats_forks" .Repository.NumForks}}</li>{{end}}
</ul>
{{end}}
</ul>
{{end}}
<form class="ui form tw-mt-5 form-fetch-action" action="{{.Link}}" method="post">
<div class="ui warning message">
<p>{{ctx.Locale.Tr "repo.settings.visibility.warning"}}</p>
</div>
<form class="ui form tw-mt-4 form-fetch-action" action="{{.Link}}" method="post">
<input type="hidden" name="action" value="visibility">
<input type="hidden" name="private" value="{{not .Repository.IsPrivate}}">
{{if not .Repository.IsPrivate}}
<div class="grouped fields">
<div class="field">
<label>
{{ctx.Locale.Tr "repo.settings.enter_repo_full_name_to_confirm"}}
<span class="tw-text-red">{{.Repository.FullName}}</span>
</label>
<div class="ui radio checkbox">
<input name="visibility" type="radio" value="public" {{if and (not .Repository.IsPrivate) (not .Repository.IsHidden)}}checked{{end}}>
<label><strong>{{ctx.Locale.Tr "repo.settings.visibility.public.label"}}</strong> — {{ctx.Locale.Tr "repo.settings.visibility.public.desc"}}</label>
</div>
</div>
<div class="required field">
<label>{{ctx.Locale.Tr "repo.repo_name"}}</label>
<input name="confirm_repo_name" required maxlength="200">
<div class="field">
<div class="ui radio checkbox">
<input name="visibility" type="radio" value="private" {{if and .Repository.IsPrivate (not .Repository.IsHidden)}}checked{{end}}>
<label><strong>{{ctx.Locale.Tr "repo.settings.visibility.private.label"}}</strong> — {{ctx.Locale.Tr "repo.settings.visibility.private.desc"}}</label>
</div>
</div>
{{end}}
{{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (Iif .Repository.IsPrivate (ctx.Locale.Tr "repo.settings.visibility.public.button") (ctx.Locale.Tr "repo.settings.visibility.private.button")))}}
<div class="field">
<div class="ui radio checkbox">
<input name="visibility" type="radio" value="hidden" {{if .Repository.IsHidden}}checked{{end}}>
<label><strong>{{ctx.Locale.Tr "repo.settings.visibility.hidden.label"}}</strong> — {{ctx.Locale.Tr "repo.settings.visibility.hidden.desc"}}</label>
</div>
</div>
</div>
{{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.change_visibility"))}}
</form>
</div>
</div>
+31
View File
@@ -0,0 +1,31 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content {{if .IsRepo}}repository{{end}}">
{{if .IsRepo}}{{template "repo/header" .}}{{end}}
<div class="ui container">
{{template "base/alert" .}}
<div class="status-page-error">
<div class="status-page-error-title">403 Access Denied</div>
<div class="tw-text-center">
<div class="tw-my-4">{{if .AccessDeniedPrompt}}{{.AccessDeniedPrompt}}{{else}}{{ctx.Locale.Tr "error403"}}{{end}}</div>
</div>
{{if not .IsSigned}}
<div class="tw-max-w-sm tw-mx-auto tw-mt-4">
<form class="ui form" action="{{AppSubUrl}}/user/login" method="post">
{{.CsrfTokenHtml}}
<input type="hidden" name="redirect_to" value="{{.CurrentURL}}">
<div class="required field">
<label>{{ctx.Locale.Tr "home.uname_holder"}}</label>
<input type="text" name="user_name" required autofocus>
</div>
<div class="required field">
<label>{{ctx.Locale.Tr "password"}}</label>
<input type="password" name="password" required>
</div>
<button class="ui primary fluid button tw-mt-2" type="submit">{{ctx.Locale.Tr "sign_in"}}</button>
</form>
</div>
{{end}}
</div>
</div>
</div>
{{template "base/footer" .}}
+14 -14
View File
@@ -1,41 +1,41 @@
<div class="flex-container-nav">
<div class="ui fluid vertical menu">
<div class="header item">{{ctx.Locale.Tr "user.settings"}}</div>
<div class="header item">{{svg "octicon-gear"}} {{ctx.Locale.Tr "user.settings"}}</div>
<a class="{{if .PageIsSettingsProfile}}active {{end}}item" href="{{AppSubUrl}}/user/settings">
{{ctx.Locale.Tr "settings.profile"}}
{{svg "octicon-person"}} {{ctx.Locale.Tr "settings.profile"}}
</a>
{{if not ($.UserDisabledFeatures.Contains "manage_credentials" "deletion")}}
<a class="{{if .PageIsSettingsAccount}}active {{end}}item" href="{{AppSubUrl}}/user/settings/account">
{{ctx.Locale.Tr "settings.account"}}
{{svg "octicon-shield-lock"}} {{ctx.Locale.Tr "settings.account"}}
</a>
{{end}}
{{if $.EnableNotifyMail}}
<a class="{{if .PageIsSettingsNotifications}}active {{end}}item" href="{{AppSubUrl}}/user/settings/notifications">
{{ctx.Locale.Tr "notifications"}}
{{svg "octicon-bell"}} {{ctx.Locale.Tr "notifications"}}
</a>
{{end}}
<a class="{{if .PageIsSettingsAppearance}}active {{end}}item" href="{{AppSubUrl}}/user/settings/appearance">
{{ctx.Locale.Tr "settings.appearance"}}
{{svg "octicon-paintbrush"}} {{ctx.Locale.Tr "settings.appearance"}}
</a>
{{if not ($.UserDisabledFeatures.Contains "manage_mfa" "manage_credentials")}}
<a class="{{if .PageIsSettingsSecurity}}active {{end}}item" href="{{AppSubUrl}}/user/settings/security">
{{ctx.Locale.Tr "settings.security"}}
{{svg "octicon-lock"}} {{ctx.Locale.Tr "settings.security"}}
</a>
{{end}}
<a class="{{if .PageIsSettingsBlockedUsers}}active {{end}}item" href="{{AppSubUrl}}/user/settings/blocked_users">
{{ctx.Locale.Tr "user.block.list"}}
{{svg "octicon-blocked"}} {{ctx.Locale.Tr "user.block.list"}}
</a>
<a class="{{if .PageIsSettingsApplications}}active {{end}}item" href="{{AppSubUrl}}/user/settings/applications">
{{ctx.Locale.Tr "settings.applications"}}
{{svg "octicon-apps"}} {{ctx.Locale.Tr "settings.applications"}}
</a>
{{if not ($.UserDisabledFeatures.Contains "manage_ssh_keys" "manage_gpg_keys")}}
<a class="{{if .PageIsSettingsKeys}}active {{end}}item" href="{{AppSubUrl}}/user/settings/keys">
{{ctx.Locale.Tr "settings.ssh_gpg_keys"}}
{{svg "octicon-key"}} {{ctx.Locale.Tr "settings.ssh_gpg_keys"}}
</a>
{{end}}
{{if .EnableActions}}
<details class="item toggleable-item" {{if or .PageIsUserSettingsActionsGeneral .PageIsSharedSettingsRunners .PageIsSharedSettingsSecrets .PageIsSharedSettingsVariables}}open{{end}}>
<summary>{{ctx.Locale.Tr "actions.actions"}}</summary>
<summary>{{svg "octicon-play"}} {{ctx.Locale.Tr "actions.actions"}}</summary>
<div class="menu">
<a class="{{if .PageIsUserSettingsActionsGeneral}}active {{end}}item" href="{{AppSubUrl}}/user/settings/actions/general">
{{ctx.Locale.Tr "actions.general"}}
@@ -54,19 +54,19 @@
{{end}}
{{if .EnablePackages}}
<a class="{{if .PageIsSettingsPackages}}active {{end}}item" href="{{AppSubUrl}}/user/settings/packages">
{{ctx.Locale.Tr "packages.title"}}
{{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}}
</a>
{{end}}
{{if not DisableWebhooks}}
<a class="{{if .PageIsSettingsHooks}}active {{end}}item" href="{{AppSubUrl}}/user/settings/hooks">
{{ctx.Locale.Tr "repo.settings.hooks"}}
{{svg "octicon-webhook"}} {{ctx.Locale.Tr "repo.settings.hooks"}}
</a>
{{end}}
<a class="{{if .PageIsSettingsOrganization}}active {{end}}item" href="{{AppSubUrl}}/user/settings/organization">
{{ctx.Locale.Tr "settings.organization"}}
{{svg "octicon-organization"}} {{ctx.Locale.Tr "settings.organization"}}
</a>
<a class="{{if .PageIsSettingsRepos}}active {{end}}item" href="{{AppSubUrl}}/user/settings/repos">
{{ctx.Locale.Tr "settings.repos"}}
{{svg "octicon-repo"}} {{ctx.Locale.Tr "settings.repos"}}
</a>
</div>
</div>
-103
View File
@@ -1,103 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later
VERSION: 05.14.00
-->
<updates>
<update>
<name>MokoGitea</name>
<description>MokoGitea dev build.</description>
<element>mokogitea</element>
<type>application</type>
<client>site</client>
<version>05.05.00-dev</version>
<creationDate>2026-05-30</creationDate>
<infourl title="MokoGitea">https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/development</infourl>
<downloads>
<downloadurl type="full" format="zip">https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/development/mokogitea-05.05.00-dev.zip</downloadurl>
</downloads>
<sha256>4fee9eb03e4b819a63bce2ceb54fdce0d3eb8bf5b31460fcc42e5ecd75cc856e</sha256>
<tags><tag>dev</tag></tags>
<changelogurl>https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md</changelogurl>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
<targetplatform name="go" version=".*"/>
</update>
<update>
<name>MokoGitea</name>
<description>MokoGitea alpha build.</description>
<element>mokogitea</element>
<type>application</type>
<client>site</client>
<version>05.05.00-alpha</version>
<creationDate>2026-05-30</creationDate>
<infourl title="MokoGitea">https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/alpha</infourl>
<downloads>
<downloadurl type="full" format="zip">https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/alpha/mokogitea-05.05.00-alpha.zip</downloadurl>
</downloads>
<sha256>4fee9eb03e4b819a63bce2ceb54fdce0d3eb8bf5b31460fcc42e5ecd75cc856e</sha256>
<tags><tag>alpha</tag></tags>
<changelogurl>https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md</changelogurl>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
<targetplatform name="go" version=".*"/>
</update>
<update>
<name>MokoGitea</name>
<description>MokoGitea beta build.</description>
<element>mokogitea</element>
<type>application</type>
<client>site</client>
<version>05.05.00-beta</version>
<creationDate>2026-05-30</creationDate>
<infourl title="MokoGitea">https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/beta</infourl>
<downloads>
<downloadurl type="full" format="zip">https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/beta/mokogitea-05.05.00-beta.zip</downloadurl>
</downloads>
<sha256>4fee9eb03e4b819a63bce2ceb54fdce0d3eb8bf5b31460fcc42e5ecd75cc856e</sha256>
<tags><tag>beta</tag></tags>
<changelogurl>https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md</changelogurl>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
<targetplatform name="go" version=".*"/>
</update>
<update>
<name>MokoGitea</name>
<description>MokoGitea rc build.</description>
<element>mokogitea</element>
<type>application</type>
<client>site</client>
<version>05.05.00-rc</version>
<creationDate>2026-05-30</creationDate>
<infourl title="MokoGitea">https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/release-candidate</infourl>
<downloads>
<downloadurl type="full" format="zip">https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/release-candidate/mokogitea-05.05.00-rc.zip</downloadurl>
</downloads>
<sha256>4fee9eb03e4b819a63bce2ceb54fdce0d3eb8bf5b31460fcc42e5ecd75cc856e</sha256>
<tags><tag>rc</tag></tags>
<changelogurl>https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md</changelogurl>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
<targetplatform name="go" version=".*"/>
</update>
<update>
<name>MokoGitea</name>
<description>MokoGitea stable build.</description>
<element>mokogitea</element>
<type>application</type>
<client>site</client>
<version>05.14.00</version>
<creationDate>2026-05-31</creationDate>
<infourl title='MokoGitea'>https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/stable</infourl>
<downloads>
<downloadurl type='full' format='zip'>https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/stable/mokogitea-05.14.00.zip</downloadurl>
</downloads>
<sha256>bec4bf5a1a841f8e72d9826451004db5d8afc70144231dfedc7fb01a6695955c</sha256>
<tags><tag>stable</tag></tags>
<changelogurl>https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md</changelogurl>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
<targetplatform name="go" version=".*" />
</update>
</updates>
+6 -4
View File
@@ -426,10 +426,11 @@ The update feed system currently supports:
| **Joomla** | `/{repo}/updates.xml` | XML with `<downloadkey>` | Production |
| **Dolibarr** | `/{repo}/updates/dolibarr.json` | JSON | Production |
| **WordPress** | `/{repo}/updates/wordpress.json` | PUC-compatible JSON | Production |
| **Drupal** | Planned | XML/JSON | Planned (#353) |
| **PrestaShop** | Planned | XML | Planned (#352) |
| **Composer** | Planned | packages.json | Planned (#354) |
| **WHMCS** | Planned | Custom | Planned (#355) |
| **Composer** | `/{repo}/updates/packages.json` | packages.json | Production |
| **PrestaShop** | `/{repo}/updates/prestashop.xml` | Module update XML | Production |
| **Drupal** | `/{repo}/updates/drupal.xml` | Update status XML | Production |
| **WHMCS** | `/{repo}/updates/whmcs.json` | Module update JSON | Production |
| **Changelog** | `/{repo}/changelog.xml` | Joomla changelog XML | Production |
All platforms share the same licensing backend — the same keys, packages, and validation work across all feed formats.
@@ -444,3 +445,4 @@ All platforms share the same licensing backend — the same keys, packages, and
| 1.2 | 2026-05-31 | Jonathan Miller (@jmiller) | Add permissions (TypeLicenses unit), renewal, auto-domain, custom keys, UI/UX cleanup |
| 1.3 | 2026-06-01 | Jonathan Miller (@jmiller) | Add package archiving, expanded delete permissions, migration v340, API renew, step-by-step guides |
| 1.4 | 2026-06-02 | Jonathan Miller (@jmiller) | WordPress feed, feed visibility modes, download gating, RepoScope enforcement, API package CRUD, settings API, combolist channel picker, double confirmation modals, extension metadata in repo settings, domain lock timer, Joomla-standard tags, SHA256 in XML, changelog XML, no-download release page mode |
| 1.5 | 2026-06-02 | Jonathan Miller (@jmiller) | All 7 platform feeds (Composer, PrestaShop, Drupal, WHMCS), enterprise sub-org hierarchy, three-level repo visibility (Public/Private/Hidden), styled 403 page with login form, separate licensing/advanced settings pages, icons on all navbars, manual stream mapping, configurable key prefix, feed always public, xorm column name fixes, security hardening |