Compare commits

..

2 Commits

Author SHA1 Message Date
gitea-actions[bot] 041adc50e5 chore(version): pre-release bump to 06.13.01-dev [skip ci] 2026-06-11 20:33:20 +00:00
jmiller cd305a2332 ci(pre-release): sync universal v05 workflow with chore/** branch trigger
Generic: Project CI / Tests (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Project CI / Lint & Validate (push) Successful in 43s
Universal: Auto Version Bump / Version Bump (push) Failing after 6s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m16s
2026-06-11 20:31:40 +00:00
80 changed files with 2826 additions and 1129 deletions
+73
View File
@@ -0,0 +1,73 @@
# Populates the go module, build, and golangci-lint caches under the default
# branch's cache scope so that PR runs have a warm fallback to restore from.
#
# GitHub Actions caches are scoped per ref: a PR run can only write to its own
# branch's scope, but can read from the base branch's scope as a fallback.
# PRs therefore cannot seed main's scope themselves. Running the same cache
# steps on push-to-main is the only opportunity to populate that fallback
# scope so fresh PR branches start with a useful cache on first run.
# A PR job's exact key lives in its own PR-scope (empty on first run, filled
# by later runs of the same PR); on miss, actions/cache's restore-keys fall
# back to prefix matches against entries this seeder saves in main's scope.
name: cache-seeder
on:
push:
branches:
- main
paths:
- "go.sum"
- ".golangci.yml"
- ".github/actions/go-cache/action.yml"
- ".github/workflows/cache-seeder.yml"
concurrency:
group: cache-seeder
cancel-in-progress: true
permissions:
contents: read
jobs:
gobuild:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: seed
- run: make deps-backend
- run: TAGS="bindata" make backend
- run: TAGS="bindata gogit" GOEXPERIMENT="" make backend
lint:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- { job: lint-backend, tags: "bindata", target: "lint-backend" }
- { job: lint-go-windows, tags: "bindata", target: "lint-go-windows" }
- { job: lint-go-gogit, tags: "bindata gogit", target: "lint-go" }
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: ${{ matrix.job }}
lint-cache: "true"
- run: make deps-backend deps-tools
- run: make ${{ matrix.target }}
env:
TAGS: ${{ matrix.tags }}
+31
View File
@@ -0,0 +1,31 @@
name: cron-licenses
on:
# schedule:
# - cron: "7 0 * * 1" # every Monday at 00:07 UTC
workflow_dispatch:
jobs:
cron-licenses:
runs-on: ubuntu-latest
if: github.repository == 'go-gitea/gitea'
permissions:
contents: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
- run: make generate-gitignore
timeout-minutes: 40
- name: push translations to repo
uses: appleboy/git-push-action@3b2c8661652360dbf1afe1b319a49dbb739c39f1 # v1.2.0
with:
author_email: "teabot@gitea.io"
author_name: GiteaBot
branch: main
commit: true
commit_message: "[skip ci] Updated licenses and gitignores"
remote: "git@github.com:go-gitea/gitea.git"
ssh_key: ${{ secrets.DEPLOY_KEY }}
+32
View File
@@ -0,0 +1,32 @@
name: cron-renovate
on:
schedule:
- cron: "23 * * * *" # hourly at :23
workflow_dispatch:
concurrency:
group: cron-renovate
env:
RENOVATE_VERSION: 43.141.5 # renovate: datasource=docker depName=ghcr.io/renovatebot/renovate
permissions:
contents: read
jobs:
cron-renovate:
runs-on: ubuntu-latest
if: github.repository == 'go-gitea/gitea' # prevent running on forks
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: renovatebot/github-action@f66d8679fcfcfa051abde6e7a623007173bf5164 # v46.1.12
with:
renovate-version: ${{ env.RENOVATE_VERSION }}
configurationFile: renovate.json5
token: ${{ secrets.RENOVATE_TOKEN }}
env:
RENOVATE_BINARY_SOURCE: install # auto-install go/node toolchains needed by post-upgrade tasks.
RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '["^make (tidy|svg nolyfill)$"]'
RENOVATE_REPOSITORIES: '["go-gitea/gitea"]'
+40
View File
@@ -0,0 +1,40 @@
name: cron-translations
on:
schedule:
- cron: "7 0 * * *" # every day at 00:07 UTC
workflow_dispatch:
jobs:
crowdin-pull:
runs-on: ubuntu-latest
if: github.repository == 'go-gitea/gitea'
permissions:
contents: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2
with:
upload_sources: true
upload_translations: false
download_sources: false
download_translations: true
push_translations: false
push_sources: false
create_pull_request: false
config: crowdin.yml
env:
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_KEY: ${{ secrets.CROWDIN_KEY }}
- name: update locales
run: ./build/update-locales.sh
- name: push translations to repo
uses: appleboy/git-push-action@3b2c8661652360dbf1afe1b319a49dbb739c39f1 # v1.2.0
with:
author_email: "teabot@gitea.io"
author_name: GiteaBot
branch: main
commit: true
commit_message: "[skip ci] Updated translations via Crowdin"
remote: "git@github.com:go-gitea/gitea.git"
ssh_key: ${{ secrets.DEPLOY_KEY }}
+125
View File
@@ -0,0 +1,125 @@
name: files-changed
on:
workflow_call:
outputs:
backend:
value: ${{ jobs.detect.outputs.backend }}
frontend:
value: ${{ jobs.detect.outputs.frontend }}
docs:
value: ${{ jobs.detect.outputs.docs }}
actions:
value: ${{ jobs.detect.outputs.actions }}
templates:
value: ${{ jobs.detect.outputs.templates }}
docker:
value: ${{ jobs.detect.outputs.docker }}
dockerfile:
value: ${{ jobs.detect.outputs.dockerfile }}
swagger:
value: ${{ jobs.detect.outputs.swagger }}
yaml:
value: ${{ jobs.detect.outputs.yaml }}
json:
value: ${{ jobs.detect.outputs.json }}
e2e:
value: ${{ jobs.detect.outputs.e2e }}
permissions:
contents: read
jobs:
detect:
runs-on: ubuntu-latest
timeout-minutes: 3
outputs:
backend: ${{ steps.changes.outputs.backend }}
frontend: ${{ steps.changes.outputs.frontend }}
docs: ${{ steps.changes.outputs.docs }}
actions: ${{ steps.changes.outputs.actions }}
templates: ${{ steps.changes.outputs.templates }}
docker: ${{ steps.changes.outputs.docker }}
dockerfile: ${{ steps.changes.outputs.dockerfile }}
swagger: ${{ steps.changes.outputs.swagger }}
yaml: ${{ steps.changes.outputs.yaml }}
json: ${{ steps.changes.outputs.json }}
e2e: ${{ steps.changes.outputs.e2e }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: changes
with:
filters: |
backend:
- "**/*.go"
- "templates/**/*.tmpl"
- "assets/emoji.json"
- "go.mod"
- "go.sum"
- "Makefile"
- ".golangci.yml"
- ".editorconfig"
- "options/locale/locale_en-US.json"
frontend:
- "*.js"
- "*.ts"
- "web_src/**"
- "tools/*.js"
- "tools/*.ts"
- "assets/emoji.json"
- "package.json"
- "pnpm-lock.yaml"
- "Makefile"
- ".eslintrc.cjs"
- ".npmrc"
docs:
- "**/*.md"
- ".markdownlint.yaml"
- "package.json"
- "pnpm-lock.yaml"
actions:
- ".github/workflows/*"
- "Makefile"
templates:
- "tools/lint-templates-*.js"
- "templates/**/*.tmpl"
- "pyproject.toml"
- "uv.lock"
docker:
- ".github/workflows/pull-docker-dryrun.yml"
- "Dockerfile"
- "Dockerfile.rootless"
- "docker/**"
- "Makefile"
dockerfile:
- "Dockerfile"
- "Dockerfile.rootless"
swagger:
- "templates/swagger/v1_json.tmpl"
- "templates/swagger/v1_input.json"
- "Makefile"
- "package.json"
- "pnpm-lock.yaml"
- ".spectral.yaml"
yaml:
- "**/*.yml"
- "**/*.yaml"
- ".yamllint.yaml"
- "pyproject.toml"
json:
- "**/*.json"
e2e:
- "tests/e2e/**"
- "tools/test-e2e.sh"
- "playwright.config.ts"
+178
View File
@@ -0,0 +1,178 @@
name: compliance
on:
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
files-changed:
uses: ./.github/workflows/files-changed.yml
lint-backend:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: lint-backend
lint-cache: "true"
- run: make deps-backend deps-tools
- run: make lint-backend
env:
TAGS: bindata
lint-on-demand:
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- run: make lint-spell
- if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true'
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
- if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true'
run: uv python install 3.14 && make deps-py lint-templates lint-yaml
- if: needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.swagger == 'true' || needs.files-changed.outputs.json == 'true'
run: make deps-frontend lint-md lint-swagger lint-json
- if: needs.files-changed.outputs.actions == 'true'
run: make lint-actions
lint-go-windows:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: lint-go-windows
lint-cache: "true"
- run: make deps-backend deps-tools
- run: make lint-go-windows
env:
TAGS: bindata
GOOS: windows
GOARCH: amd64
lint-go-gogit:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: lint-go-gogit
lint-cache: "true"
- run: make deps-backend deps-tools
- run: make lint-go
env:
TAGS: bindata gogit
checks-backend:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: checks-backend
build-cache: "false"
- run: make deps-backend deps-tools
- run: make --always-make checks-backend # ensure the "go-licenses" make target runs
frontend:
if: needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- run: make deps-frontend
- run: make lint-frontend
- run: make checks-frontend
- run: make test-frontend
- run: make frontend
backend:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: compliance-backend
- run: make deps-backend generate-go
# no frontend build here as backend should be able to build, even without any frontend files
# CGO is not used when cross-compile, so these steps also test if the code is compatible with CGO disabled
- name: build-backend-arm64
run: go build -o gitea_linux_arm64
env:
GOOS: linux
GOARCH: arm64
TAGS: bindata gogit
- name: build-backend-windows
run: go build -o gitea_windows
env:
GOOS: windows
GOARCH: amd64
TAGS: bindata gogit
- name: build-backend-386
run: go build -o gitea_linux_386
env:
GOOS: linux
GOARCH: 386
+262
View File
@@ -0,0 +1,262 @@
name: db-tests
on:
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
files-changed:
uses: ./.github/workflows/files-changed.yml
test-pgsql:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
services:
pgsql:
image: postgres:14
env:
POSTGRES_DB: test
POSTGRES_PASSWORD: postgres
ports:
- "5432:5432"
ldap:
image: gitea/test-openldap:latest
ports:
- "389:389"
- "636:636"
minio:
# as github actions doesn't support "entrypoint", we need to use a non-official image
# that has a custom entrypoint set to "minio server /data"
image: bitnamilegacy/minio:2023.12.23
env:
MINIO_ROOT_USER: 123456
MINIO_ROOT_PASSWORD: 12345678
ports:
- "9000:9000"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: pgsql
- name: Add hosts to /etc/hosts
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 pgsql ldap minio" | sudo tee -a /etc/hosts'
- run: make deps-backend
- run: make backend
env:
TAGS: bindata
- name: run migration tests
run: GITEA_TEST_DATABASE=pgsql make test-migration
- name: run tests
run: GITEA_TEST_DATABASE=pgsql make test-integration
timeout-minutes: 50
env:
# pgsql is chosen to be the unlucky one to run with the slow "race detector", it is about 60% slower.
GOTEST_FLAGS: -race -timeout=40m
TAGS: bindata gogit
TEST_LDAP: 1
test-sqlite:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: sqlite
- run: make deps-backend
- run: make backend
env:
TAGS: bindata gogit
GOEXPERIMENT:
- name: run migration tests
run: GITEA_TEST_DATABASE=sqlite make test-migration
env:
TAGS: bindata gogit
- name: run tests
run: GITEA_TEST_DATABASE=sqlite make test-integration
timeout-minutes: 50
env:
# sqlite driver can contain large amount of Golang code, so don't use race detector for it, otherwise, extremely slow
GOTEST_FLAGS: -timeout=40m
TAGS: bindata gogit
GOEXPERIMENT:
test-unit:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.19.14
env:
discovery.type: single-node
xpack.security.enabled: false
ports:
- "9200:9200"
meilisearch:
image: getmeili/meilisearch:v1
env:
MEILI_ENV: development # disable auth
ports:
- "7700:7700"
redis:
image: redis
options: >- # wait until redis has started
--health-cmd "redis-cli ping"
--health-interval 5s
--health-timeout 3s
--health-retries 10
ports:
- 6379:6379
minio:
image: bitnamilegacy/minio:2021.12.29
env:
MINIO_ACCESS_KEY: 123456
MINIO_SECRET_KEY: 12345678
ports:
- "9000:9000"
devstoreaccount1.azurite.local: # https://github.com/Azure/Azurite/issues/1583
image: mcr.microsoft.com/azure-storage/azurite:latest
ports:
- 10000:10000
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: unit
build-cache-rotate: "true"
- name: Add hosts to /etc/hosts
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 minio devstoreaccount1.azurite.local mysql elasticsearch meilisearch smtpimap" | sudo tee -a /etc/hosts'
- run: make deps-backend
- run: make backend
env:
TAGS: bindata
- name: unit-tests
run: make test-backend test-check
env:
GOTEST_FLAGS: -race -timeout=20m
TAGS: bindata
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
- name: unit-tests-gogit
run: make test-backend test-check
env:
GOTEST_FLAGS: -race -timeout=20m
TAGS: bindata gogit
GOEXPERIMENT:
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
test-mysql:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
services:
mysql:
# the bitnami mysql image has more options than the official one, it's easier to customize
image: bitnamilegacy/mysql:8.4
env:
ALLOW_EMPTY_PASSWORD: true
MYSQL_DATABASE: testgitea
ports:
- "3306:3306"
options: >-
--mount type=tmpfs,destination=/bitnami/mysql/data
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.19.14
env:
discovery.type: single-node
xpack.security.enabled: false
ports:
- "9200:9200"
smtpimap:
image: tabascoterrier/docker-imap-devel:latest
ports:
- "25:25"
- "143:143"
- "587:587"
- "993:993"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: mysql
- name: Add hosts to /etc/hosts
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mysql elasticsearch smtpimap" | sudo tee -a /etc/hosts'
- run: make deps-backend
- run: make backend
env:
TAGS: bindata
- name: run migration tests
run: GITEA_TEST_DATABASE=mysql make test-migration
- name: run tests
run: GITEA_TEST_DATABASE=mysql make test-integration
env:
TAGS: bindata
TEST_INDEXER_CODE_ES_URL: "http://elastic:changeme@elasticsearch:9200"
test-mssql:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
services:
mssql:
image: mcr.microsoft.com/mssql/server:2019-latest
env:
ACCEPT_EULA: Y
MSSQL_PID: Standard
SA_PASSWORD: MwantsaSecurePassword1
ports:
- "1433:1433"
devstoreaccount1.azurite.local: # https://github.com/Azure/Azurite/issues/1583
image: mcr.microsoft.com/azure-storage/azurite:latest
ports:
- 10000:10000
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: mssql
- name: Add hosts to /etc/hosts
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mssql devstoreaccount1.azurite.local" | sudo tee -a /etc/hosts'
- run: make deps-backend
- run: make backend
env:
TAGS: bindata
- run: GITEA_TEST_DATABASE=mssql make test-migration
- name: run tests
run: GITEA_TEST_DATABASE=mssql make test-integration
timeout-minutes: 50
env:
TAGS: bindata
+47
View File
@@ -0,0 +1,47 @@
name: docker-dryrun
on:
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
files-changed:
uses: ./.github/workflows/files-changed.yml
# QEMU-based build is slow (40-50 minutes), so run arm64 and riscv64 when dockerfile changes.
# Run amd64 when any docker-related files change, which is fast (4 minutes).
container-amd64:
if: needs.files-changed.outputs.docker == 'true'
needs: [files-changed]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/docker-dryrun
with:
platform: linux/amd64
container-arm64:
if: needs.files-changed.outputs.dockerfile == 'true'
needs: [files-changed]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/docker-dryrun
with:
platform: linux/arm64
container-riscv64:
if: needs.files-changed.outputs.dockerfile == 'true'
needs: [files-changed]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/docker-dryrun
with:
platform: linux/riscv64
+50
View File
@@ -0,0 +1,50 @@
name: e2e-tests
on:
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
files-changed:
uses: ./.github/workflows/files-changed.yml
test-e2e:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.e2e == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: e2e
build-cache: "false"
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- run: make deps-frontend
- run: make frontend
- run: make deps-backend
- run: make backend
env:
TAGS: bindata
- run: make playwright
- run: make test-e2e
timeout-minutes: 10
env:
TAGS: bindata
FORCE_COLOR: 1
GITEA_TEST_E2E_DEBUG: 1
+20
View File
@@ -0,0 +1,20 @@
name: labeler
on:
pull_request_target:
types: [opened, synchronize, reopened]
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
labeler:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
with:
sync-labels: true
+28
View File
@@ -0,0 +1,28 @@
name: pr-title
on:
pull_request:
types:
- opened
- edited
- reopened
- synchronize
- ready_for_review
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
lint-pr-title:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- run: make lint-pr-title
env:
PR_TITLE: ${{ github.event.pull_request.title }}
+135
View File
@@ -0,0 +1,135 @@
name: release-nightly
on:
push:
branches: [main, release/v*]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
nightly-binary:
runs-on: namespace-profile-gitea-release-binary
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- run: make deps-frontend deps-backend
# xgo build
- run: make release
env:
TAGS: bindata
- name: import gpg key
id: import_gpg
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
with:
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
- name: sign binaries
run: |
for f in dist/release/*; do
echo '${{ secrets.GPGSIGN_PASSPHRASE }}' | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u ${{ steps.import_gpg.outputs.fingerprint }} --output "$f.asc" "$f"
done
# clean branch name to get the folder name in S3
- name: Get cleaned branch name
id: clean_name
run: |
REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
echo "Cleaned name is ${REF_NAME}"
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
- name: configure aws
uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
with:
aws-region: ${{ secrets.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: upload binaries to s3
run: |
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
nightly-container:
runs-on: namespace-profile-gitea-release-docker
permissions:
contents: read
packages: write # to publish to ghcr.io
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Get cleaned branch name
id: clean_name
run: |
REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
- uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
id: meta
with:
images: |-
gitea/gitea
ghcr.io/go-gitea/gitea
tags: |
type=raw,value=${{ steps.clean_name.outputs.branch }}
annotations: |
org.opencontainers.image.authors="maintainers@gitea.io"
- uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
id: meta_rootless
with:
images: |-
gitea/gitea
ghcr.io/go-gitea/gitea
# each tag below will have the suffix of -rootless
flavor: |
suffix=-rootless
tags: |
type=raw,value=${{ steps.clean_name.outputs.branch }}
annotations: |
org.opencontainers.image.authors="maintainers@gitea.io"
- name: Login to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: build regular docker image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
push: true
tags: ${{ steps.meta.outputs.tags }}
annotations: ${{ steps.meta.outputs.annotations }}
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
cache-to: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful,mode=max
- name: build rootless docker image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
push: true
file: Dockerfile.rootless
tags: ${{ steps.meta_rootless.outputs.tags }}
annotations: ${{ steps.meta_rootless.outputs.annotations }}
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootless
cache-to: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootless,mode=max
+141
View File
@@ -0,0 +1,141 @@
name: release-tag-rc
on:
push:
tags:
- "v1*-rc*"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
binary:
runs-on: namespace-profile-gitea-release-binary
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- run: make deps-frontend deps-backend
# xgo build
- run: make release
env:
TAGS: bindata
- name: import gpg key
id: import_gpg
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
with:
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
- name: sign binaries
run: |
for f in dist/release/*; do
echo '${{ secrets.GPGSIGN_PASSPHRASE }}' | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u ${{ steps.import_gpg.outputs.fingerprint }} --output "$f.asc" "$f"
done
# clean branch name to get the folder name in S3
- name: Get cleaned branch name
id: clean_name
run: |
REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\/v//' -e 's/release\/v//')
echo "Cleaned name is ${REF_NAME}"
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
- name: configure aws
uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
with:
aws-region: ${{ secrets.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: upload binaries to s3
run: |
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
- name: Install GH CLI
uses: dev-hanz-ops/install-gh-cli-action@af38ce09b1ec248aeb08eea2b16bbecea9e059f8 # v0.2.1
with:
gh-cli-version: 2.39.1
- name: create github release
run: |
gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --draft --notes-from-tag dist/release/*
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
container:
runs-on: namespace-profile-gitea-release-docker
permissions:
contents: read
packages: write # to publish to ghcr.io
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
id: meta
with:
images: |-
gitea/gitea
ghcr.io/go-gitea/gitea
flavor: |
latest=false
# 1.2.3-rc0
tags: |
type=semver,pattern={{version}}
annotations: |
org.opencontainers.image.authors="maintainers@gitea.io"
- uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
id: meta_rootless
with:
images: |-
gitea/gitea
ghcr.io/go-gitea/gitea
# each tag below will have the suffix of -rootless
flavor: |
latest=false
suffix=-rootless
# 1.2.3-rc0
tags: |
type=semver,pattern={{version}}
annotations: |
org.opencontainers.image.authors="maintainers@gitea.io"
- name: Login to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: build regular container image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
push: true
tags: ${{ steps.meta.outputs.tags }}
annotations: ${{ steps.meta.outputs.annotations }}
- name: build rootless container image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
push: true
file: Dockerfile.rootless
tags: ${{ steps.meta_rootless.outputs.tags }}
annotations: ${{ steps.meta_rootless.outputs.annotations }}
+153
View File
@@ -0,0 +1,153 @@
name: release-tag-version
on:
push:
tags:
- "v1.*"
- "!v1*-rc*"
- "!v1*-dev"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
binary:
runs-on: namespace-profile-gitea-release-binary
permissions:
contents: read
packages: write # to publish to ghcr.io
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- run: make deps-frontend deps-backend
# xgo build
- run: make release
env:
TAGS: bindata
- name: import gpg key
id: import_gpg
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
with:
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
- name: sign binaries
run: |
for f in dist/release/*; do
echo '${{ secrets.GPGSIGN_PASSPHRASE }}' | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u ${{ steps.import_gpg.outputs.fingerprint }} --output "$f.asc" "$f"
done
# clean branch name to get the folder name in S3
- name: Get cleaned branch name
id: clean_name
run: |
REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\/v//' -e 's/release\/v//')
echo "Cleaned name is ${REF_NAME}"
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
- name: configure aws
uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
with:
aws-region: ${{ secrets.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: upload binaries to s3
run: |
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
- name: Install GH CLI
uses: dev-hanz-ops/install-gh-cli-action@af38ce09b1ec248aeb08eea2b16bbecea9e059f8 # v0.2.1
with:
gh-cli-version: 2.39.1
- name: create github release
run: |
gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --notes-from-tag dist/release/*
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
container:
runs-on: namespace-profile-gitea-release-docker
permissions:
contents: read
packages: write # to publish to ghcr.io
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
id: meta
with:
images: |-
gitea/gitea
ghcr.io/go-gitea/gitea
# this will generate tags in the following format:
# latest
# 1
# 1.2
# 1.2.3
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}}
annotations: |
org.opencontainers.image.authors="maintainers@gitea.io"
- uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
id: meta_rootless
with:
images: |-
gitea/gitea
ghcr.io/go-gitea/gitea
# each tag below will have the suffix of -rootless
flavor: |
suffix=-rootless,onlatest=true
# this will generate tags in the following format (with -rootless suffix added):
# latest
# 1
# 1.2
# 1.2.3
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}}
annotations: |
org.opencontainers.image.authors="maintainers@gitea.io"
- name: Login to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: build regular container image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
push: true
tags: ${{ steps.meta.outputs.tags }}
annotations: ${{ steps.meta.outputs.annotations }}
- name: build rootless container image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: linux/amd64,linux/arm64,linux/riscv64
push: true
file: Dockerfile.rootless
tags: ${{ steps.meta_rootless.outputs.tags }}
annotations: ${{ steps.meta_rootless.outputs.annotations }}
+1 -1
View File
@@ -4,7 +4,7 @@
<name>MokoGitea</name> <name>MokoGitea</name>
<org>MokoConsulting</org> <org>MokoConsulting</org>
<description>Moko fork of Gitea - adding project board REST API endpoints and custom enhancements</description> <description>Moko fork of Gitea - adding project board REST API endpoints and custom enhancements</description>
<version>06.15.00</version> <version>06.13.01</version>
<version-prefix>v1.26.1+MOKO</version-prefix> <version-prefix>v1.26.1+MOKO</version-prefix>
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license> <license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
</identity> </identity>
+3 -3
View File
@@ -33,11 +33,11 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }} token: ${{ secrets.GA_TOKEN }}
- name: Delete merged branches - name: Delete merged branches
env: env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: | run: |
echo "=== Merged Branch Cleanup ===" echo "=== Merged Branch Cleanup ==="
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
@@ -66,7 +66,7 @@ jobs:
- name: Clean old workflow runs - name: Clean old workflow runs
env: env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: | run: |
echo "=== Workflow Run Cleanup ===" echo "=== Workflow Run Cleanup ==="
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
+4 -4
View File
@@ -42,10 +42,10 @@ jobs:
- name: Setup MokoStandards tools - name: Setup MokoStandards tools
env: env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }} GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }} MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }} MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}' COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: | run: |
git clone --depth 1 --branch main --quiet \ git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \ "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
+184 -62
View File
@@ -11,7 +11,7 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
version: version:
description: 'Version tag' description: 'Version tag (e.g. v1.26.1+MOKO06.12.00)'
required: true required: true
default: 'latest' default: 'latest'
environment: environment:
@@ -28,9 +28,9 @@ concurrency:
cancel-in-progress: false cancel-in-progress: false
env: env:
REGISTRY: git.mokoconsulting.tech REGISTRY: code.mokoconsulting.tech
IMAGE: mokoconsulting/mokogitea IMAGE: mokoconsulting/mokogitea
DEPLOY_HOST: git.mokoconsulting.tech DEPLOY_HOST: code.mokoconsulting.tech
DEPLOY_PORT: 2918 DEPLOY_PORT: 2918
DEPLOY_USER: mokoconsulting DEPLOY_USER: mokoconsulting
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
@@ -47,6 +47,8 @@ jobs:
- name: Determine settings - name: Determine settings
id: config id: config
run: | run: |
# On push to main, auto-deploy to production with git-derived version.
# On workflow_dispatch, use the provided inputs.
if [ "${{ github.event_name }}" = "push" ]; then if [ "${{ github.event_name }}" = "push" ]; then
VERSION=$(git describe --tags --always 2>/dev/null || echo "dev-$(git rev-parse --short HEAD)") VERSION=$(git describe --tags --always 2>/dev/null || echo "dev-$(git rev-parse --short HEAD)")
ENV="production" ENV="production"
@@ -54,97 +56,217 @@ jobs:
VERSION="${{ github.event.inputs.version }}" VERSION="${{ github.event.inputs.version }}"
ENV="${{ github.event.inputs.environment }}" ENV="${{ github.event.inputs.environment }}"
fi fi
if [ "$ENV" = "production" ]; then if [ "$ENV" = "production" ]; then
echo "compose_dir=/opt/gitea" >> $GITHUB_OUTPUT echo "compose_dir=/opt/gitea" >> $GITHUB_OUTPUT
echo "container=mokogitea" >> $GITHUB_OUTPUT echo "container=mokogitea" >> $GITHUB_OUTPUT
echo "source_dir=/opt/gitea/source" >> $GITHUB_OUTPUT echo "source_dir=/opt/gitea/source" >> $GITHUB_OUTPUT
echo "branch=main" >> $GITHUB_OUTPUT echo "branch=main" >> $GITHUB_OUTPUT
echo "tag=$VERSION" >> $GITHUB_OUTPUT echo "tag=${VERSION}" >> $GITHUB_OUTPUT
echo "instance_url=https://code.mokoconsulting.tech" >> $GITHUB_OUTPUT
else else
echo "compose_dir=/opt/gitea-dev" >> $GITHUB_OUTPUT echo "compose_dir=/opt/gitea-dev" >> $GITHUB_OUTPUT
echo "container=mokogitea-dev" >> $GITHUB_OUTPUT echo "container=mokogitea-dev" >> $GITHUB_OUTPUT
echo "source_dir=/opt/gitea-dev/source" >> $GITHUB_OUTPUT echo "source_dir=/opt/gitea-dev/source" >> $GITHUB_OUTPUT
echo "branch=dev" >> $GITHUB_OUTPUT echo "branch=dev" >> $GITHUB_OUTPUT
echo "tag=$VERSION-dev" >> $GITHUB_OUTPUT echo "tag=${VERSION}-dev" >> $GITHUB_OUTPUT
echo "instance_url=https://git.dev.mokoconsulting.tech" >> $GITHUB_OUTPUT
fi fi
- name: Write deploy key - name: Enable maintenance mode
env: env:
DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }} GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
INSTANCE_URL: ${{ steps.config.outputs.instance_url }}
run: | run: |
mkdir -p ~/.ssh echo "Enabling maintenance mode on ${INSTANCE_URL}..."
echo "$DEPLOY_KEY" > ~/.ssh/deploy_key curl -sf -X POST \
chmod 600 ~/.ssh/deploy_key -H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/x-www-form-urlencoded" \
"${INSTANCE_URL}/-/admin/config" \
-d 'key=instance.maintenance_mode&value={"AdminWebAccessOnly":true}' \
|| echo "WARNING: Could not enable maintenance mode (instance may be down)"
- name: Build and deploy via SSH - name: Build and deploy via SSH
env: env:
REGISTRY_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
TAG: ${{ steps.config.outputs.tag }} TAG: ${{ steps.config.outputs.tag }}
BRANCH: ${{ steps.config.outputs.branch }} BRANCH: ${{ steps.config.outputs.branch }}
SOURCE_DIR: ${{ steps.config.outputs.source_dir }} SOURCE_DIR: ${{ steps.config.outputs.source_dir }}
COMPOSE_DIR: ${{ steps.config.outputs.compose_dir }} COMPOSE_DIR: ${{ steps.config.outputs.compose_dir }}
CONTAINER: ${{ steps.config.outputs.container }} CONTAINER: ${{ steps.config.outputs.container }}
run: | run: |
HEALTH_FMT='${{ '{{' }}.State.Health.Status${{ '}}' }}' mkdir -p ~/.ssh
IMAGE_FMT='Image: ${{ '{{' }}.Config.Image${{ '}}' }}' echo "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh -i ~/.ssh/deploy_key -p ${{ env.DEPLOY_PORT }} \ SSH_CMD="ssh -i ~/.ssh/deploy_key -p ${{ env.DEPLOY_PORT }} -o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }}"
-o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o ServerAliveInterval=30 -o ServerAliveCountMax=10 \
${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} bash -s <<DEPLOY_EOF
set -e
echo 'SSH connected'
echo 'Cleaning Docker build cache...' $SSH_CMD "echo 'SSH connected'"
docker builder prune -af 2>/dev/null || true
docker image prune -af 2>/dev/null || true
free -m | head -3
echo 'Pulling source...' # Pre-deploy cleanup: free disk and memory for the build
if [ ! -d $SOURCE_DIR/.git ]; then $SSH_CMD "
git clone -b $BRANCH https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-APP.git $SOURCE_DIR echo 'Cleaning Docker build cache and unused images...'
fi docker builder prune -af 2>/dev/null || true
cd $SOURCE_DIR docker image prune -af 2>/dev/null || true
# Ensure remote points to MokoGitea-APP (not the upstream fork) echo 'Clearing swap...'
git remote set-url origin https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-APP.git 2>/dev/null || true sudo swapoff -a && sudo swapon -a 2>/dev/null || true
git fetch origin $BRANCH echo 'Cleanup complete'
git reset --hard origin/$BRANCH free -m | head -3
"
echo 'Building Docker image...' # Pull latest source
docker build --no-cache --build-arg GOFLAGS='-p 1' \ $SSH_CMD "
--tag ${{ env.REGISTRY }}/${{ env.IMAGE }}:$TAG \ set -e
--tag ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest \ if [ ! -d ${SOURCE_DIR}/.git ]; then
-f Dockerfile . git clone -b ${BRANCH} https://code.mokoconsulting.tech/MokoConsulting/MokoGitea.git ${SOURCE_DIR}
echo 'Pushing to registry...'
echo '$REGISTRY_TOKEN' | docker login ${{ env.REGISTRY }} -u ${{ env.DEPLOY_USER }} --password-stdin
docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:$TAG
docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest
echo 'Restarting container...'
cd $COMPOSE_DIR
sed -i 's|${{ env.IMAGE }}:[^ ]*|${{ env.IMAGE }}:$TAG|' docker-compose.yml
docker compose up -d $CONTAINER
echo 'Health check...'
for i in 1 2 3 4 5 6 7 8; do
sleep 15
if docker inspect --format='$HEALTH_FMT' $CONTAINER 2>/dev/null | grep -q healthy; then
echo 'Container healthy!'
docker inspect --format='$IMAGE_FMT' $CONTAINER
exit 0
fi fi
echo "Waiting... (attempt \$i/8)" cd ${SOURCE_DIR}
done git fetch origin ${BRANCH}
echo 'Health check failed' git reset --hard origin/${BRANCH}
docker logs $CONTAINER --tail 20 "
exit 1
DEPLOY_EOF # Build Docker image
$SSH_CMD "
set -e
cd ${SOURCE_DIR}
docker build --no-cache --build-arg GOFLAGS='-p 1' \
--tag ${{ env.REGISTRY }}/${{ env.IMAGE }}:${TAG} \
--tag ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest \
-f Dockerfile .
"
# Push to container registry
$SSH_CMD "
set -e
docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:${TAG}
docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest
"
# Update compose and restart
$SSH_CMD "
set -e
cd ${COMPOSE_DIR}
sed -i 's|${{ env.IMAGE }}:[^ ]*|${{ env.IMAGE }}:${TAG}|' docker-compose.yml
docker compose up -d ${CONTAINER}
"
# Health check
$SSH_CMD "
for i in 1 2 3 4 5 6 7 8; do
sleep 15
if docker inspect --format='{{.State.Health.Status}}' ${CONTAINER} 2>/dev/null | grep -q healthy; then
echo 'Container healthy!'
docker inspect --format='Image: {{.Config.Image}}' ${CONTAINER}
exit 0
fi
echo \"Waiting... (attempt \$i/8)\"
done
echo 'Health check failed'
docker logs ${CONTAINER} --tail 20
exit 1
"
- name: Update updates.xml
if: success()
env:
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
TAG: ${{ steps.config.outputs.tag }}
INSTANCE_URL: ${{ steps.config.outputs.instance_url }}
DEPLOY_ENV: ${{ github.event.inputs.environment || 'production' }}
run: |
# Only update updates.xml for production stable releases
if [ "$DEPLOY_ENV" != "production" ]; then
echo "Skipping updates.xml — dev deployments don't update stable channel"
exit 0
fi
# Extract project version by stripping the version prefix from the tag.
# Reads prefix from manifest API (e.g. "v1.26.1+MOKO"), falls back to legacy pattern.
API_BASE="https://${REGISTRY}/api/v1/repos/MokoConsulting/MokoGitea"
PREFIX=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
"${API_BASE}/manifest" | python3 -c "import json,sys; print(json.load(sys.stdin).get('version_prefix',''))" 2>/dev/null || true)
if [ -n "$PREFIX" ]; then
MOKO_VER="${TAG#$PREFIX}"
else
# Legacy fallback: strip everything up to and including "-moko."
MOKO_VER=$(echo "$TAG" | sed -n 's/.*-moko\.\(.*\)/\1/p')
fi
if [ -z "$MOKO_VER" ]; then
echo "Could not extract version from tag: $TAG (prefix: ${PREFIX:-none})"
exit 0
fi
RELEASE_URL="https://${REGISTRY}/MokoConsulting/MokoGitea/releases/tag/${TAG}"
DOCKER_IMG="${REGISTRY}/${IMAGE}:${TAG}"
python3 << PYEOF
import json, os, re, base64, urllib.request
token = os.environ["GITEA_TOKEN"]
registry = os.environ["REGISTRY"]
tag = os.environ["TAG"]
moko_ver = os.environ["MOKO_VER"]
release_url = os.environ["RELEASE_URL"]
docker_img = os.environ["DOCKER_IMG"]
api = f"https://{registry}/api/v1/repos/MokoConsulting/MokoGitea"
# Fetch current updates.xml
req = urllib.request.Request(f"{api}/contents/updates.xml?ref=main",
headers={"Authorization": f"token {token}"})
with urllib.request.urlopen(req) as resp:
data = json.loads(resp.read())
sha = data["sha"]
content = base64.b64decode(data["content"]).decode("utf-8")
# Update stable channel — match the <update> block containing <tag>stable</tag>
def replace_channel(xml, channel, ver, url, docker):
pattern = rf"(<update>\s*<name>MokoGitea</name>[\s\S]*?<tags><tag>{channel}</tag></tags>[\s\S]*?</update>)"
def replacer(m):
block = m.group(1)
block = re.sub(r"<version>[^<]*</version>", f"<version>{ver}</version>", block)
block = re.sub(r"(<infourl[^>]*>)[^<]*(</infourl>)", rf"\1{url}\2", block)
block = re.sub(r"(<downloadurl[^>]*>)[^<]*(</downloadurl>)", rf"\1{docker}\2", block)
return block
return re.sub(pattern, replacer, xml)
content = replace_channel(content, "stable", moko_ver, release_url, docker_img)
content = re.sub(r"VERSION: [^\n]*", f"VERSION: {moko_ver}", content)
# Push updated file
encoded = base64.b64encode(content.encode()).decode()
payload = json.dumps({
"message": f"chore(ci): update updates.xml to {moko_ver}",
"content": encoded,
"sha": sha,
"branch": "main",
}).encode()
req = urllib.request.Request(f"{api}/contents/updates.xml",
data=payload, method="PUT",
headers={"Authorization": f"token {token}", "Content-Type": "application/json"})
with urllib.request.urlopen(req) as resp:
print(f"updates.xml updated to {moko_ver}")
PYEOF
- name: Disable maintenance mode
if: always()
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
INSTANCE_URL: ${{ steps.config.outputs.instance_url }}
run: |
echo "Disabling maintenance mode on ${INSTANCE_URL}..."
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/x-www-form-urlencoded" \
"${INSTANCE_URL}/-/admin/config" \
-d 'key=instance.maintenance_mode&value={"AdminWebAccessOnly":false}' \
|| echo "WARNING: Could not disable maintenance mode"
- name: Verify - name: Verify
run: | run: |
sleep 5 sleep 5
curl -sf https://${{ env.DEPLOY_HOST }}/api/healthz && echo " API healthy" curl -sf https://${{ env.DEPLOY_HOST }}/api/healthz && echo " API healthy"
- name: Notify on failure - name: Notify on failure
if: failure() if: failure()
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokoplatform.Automation # INGROUP: mokoplatform.Automation
# VERSION: 06.15.00 # VERSION: 06.13.01
# BRIEF: Auto-create feature branch when an issue is opened # BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch" name: "Universal: Issue Branch"
+1 -1
View File
@@ -48,4 +48,4 @@ jobs:
working-directory: .mokogitea/mcp working-directory: .mokogitea/mcp
run: | run: |
npm publish --registry ${{ github.server_url }}/api/packages/${{ github.repository_owner }}/npm/ \ npm publish --registry ${{ github.server_url }}/api/packages/${{ github.repository_owner }}/npm/ \
--//$(echo "${{ github.server_url }}" | sed 's|https://||')/api/packages/${{ github.repository_owner }}/npm/:_authToken=${{ secrets.MOKOGITEA_TOKEN }} --//$(echo "${{ github.server_url }}" | sed 's|https://||')/api/packages/${{ github.repository_owner }}/npm/:_authToken=${{ secrets.GITEA_TOKEN }}
+242 -1
View File
@@ -8,4 +8,245 @@
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /templates/workflows/universal/pre-release.yml.template # PATH: /templates/workflows/universal/pre-release.yml.template
# VERSION: 05.01.00 # VERSION: 05.01.00
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches # BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
name: "Universal: Pre-Release"
on:
push:
branches:
- dev
- 'fix/**'
- 'patch/**'
- 'hotfix/**'
- 'bugfix/**'
- 'chore/**'
- alpha
- beta
- rc
workflow_dispatch:
inputs:
stability:
description: 'Pre-release channel'
required: true
type: choice
options:
- development
- alpha
- beta
- release-candidate
permissions:
contents: write
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
jobs:
build:
name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})"
runs-on: release
if: >-
github.event_name == 'workflow_dispatch' ||
github.event_name == 'push'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }}
ref: ${{ github.ref_name }}
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
# Use pre-installed /opt/moko-platform if available (updated by cron every 6h)
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/cli/manifest_element.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
echo Using pre-installed /opt/moko-platform
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
else
echo Falling back to fresh clone
if ! command -v composer > /dev/null 2>&1; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
fi
rm -rf /tmp/moko-platform-api
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
fi
- name: Detect platform
id: platform
run: |
# Auto-detect and update platform if not set in manifest
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
php ${MOKO_CLI}/manifest_read.php --path . --github-output
- name: Resolve metadata and bump version
id: meta
run: |
# Auto-detect stability from branch name on push, or use input on dispatch
if [ "${{ github.event_name }}" = "push" ]; then
case "${{ github.ref_name }}" in
rc) STABILITY="release-candidate" ;;
alpha) STABILITY="alpha" ;;
beta) STABILITY="beta" ;;
*) STABILITY="development" ;;
esac
else
STABILITY="${{ inputs.stability || 'development' }}"
fi
case "$STABILITY" in
development) SUFFIX="-dev"; TAG="development" ;;
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
beta) SUFFIX="-beta"; TAG="beta" ;;
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
esac
# Bump version via CLI: patch for dev/alpha/beta, minor for RC
case "$STABILITY" in
release-candidate) BUMP="minor" ;;
*) BUMP="patch" ;;
esac
php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true
# Set stability suffix and verify consistency
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01")
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
php ${MOKO_CLI}/version_set_platform.php \
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
# Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml
php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true
# Append suffix for output
if [ -n "$SUFFIX" ]; then
VERSION="${VERSION}${SUFFIX}"
fi
# Commit version bump
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git add -A
git diff --cached --quiet || {
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
git push origin HEAD 2>&1
}
# Auto-detect element via manifest_element.php
php ${MOKO_CLI}/manifest_element.php \
--path . --version "$VERSION" --stability "$STABILITY" \
--repo "${GITEA_REPO}" --github-output
# Read back element outputs
EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
[ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
- name: Create release
id: release
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_create.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
- name: Update release notes from CHANGELOG.md
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
if [ -f "CHANGELOG.md" ]; then
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
else
NOTES="Release ${VERSION}"
fi
# Update release body via API
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
"${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -n "$RELEASE_ID" ]; then
python3 -c "
import json, urllib.request
body = open('/dev/stdin').read()
payload = json.dumps({'body': body}).encode()
req = urllib.request.Request(
'${API_BASE}/releases/${RELEASE_ID}',
data=payload, method='PATCH',
headers={
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
'Content-Type': 'application/json'
})
urllib.request.urlopen(req)
" <<< "$NOTES"
echo "Release notes updated from CHANGELOG.md"
fi
- name: Build package and upload
id: package
run: |
VERSION="${{ steps.meta.outputs.version }}"
TAG="${{ steps.meta.outputs.tag }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_package.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --output /tmp || true
# updates.xml is generated dynamically by MokoGitea license server
# No need to build, commit, or sync updates.xml from workflows
- name: "Delete lesser pre-release channels (cascade)"
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
php ${MOKO_CLI}/release_cascade.php \
--stability "${{ steps.meta.outputs.stability }}" \
--token "${TOKEN}" \
--api-base "${API_BASE}"
- name: Summary
if: always()
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
SHA256="${{ steps.package.outputs.sha256_zip }}"
echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
- name: Sync upstream bugs - name: Sync upstream bugs
env: env:
GH_TOKEN: ${{ secrets.GH_MIRROR_TOKEN }} GH_TOKEN: ${{ secrets.GH_MIRROR_TOKEN }}
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} MOKOGITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
MOKOGITEA_URL: https://code.mokoconsulting.tech MOKOGITEA_URL: https://code.mokoconsulting.tech
MOKOGITEA_REPO: MokoConsulting/MokoGitea MOKOGITEA_REPO: MokoConsulting/MokoGitea
UPSTREAM_BRANCH: release/v1.26 UPSTREAM_BRANCH: release/v1.26
+211 -43
View File
@@ -1,51 +1,9 @@
# Changelog # Changelog
## [Unreleased]
## [06.15.00] --- 2026-06-12
## [06.15.00] --- 2026-06-12
* FEATURES
* feat(custom-fields): required flag UI and API validation (#597, PR #612)
* Required checkbox in org custom field settings
* Red asterisk indicator on required fields
* API returns 422 when required custom fields are missing
* Validation runs before issue creation (no orphaned issues)
* feat(issues): make status_id, priority_id, type_id required on issue create (#598, PR #613)
* `CreateIssueOption` fields changed from optional `*int64` to `int64`
* Auto-assigns org defaults when value is 0
* MCP `gitea_issue_create` now requires these fields (pass 0 for defaults)
* Explicit metadata errors now return 500 instead of being silently discarded
## [06.14.00] --- 2026-06-11
* FIXES
* fix: return 404 for update feeds when update server is disabled (#589, PR #599)
* fix(ui): raw file button opens in new tab with rel="noopener noreferrer" (#581, PR #600)
* fix: update server feed generation bugs (#601, PR #605)
* default targetplatform changed from `(5|6)\\..*` to `6\\..*` for Joomla 6 compat
* `<client>` uses string values `site`/`administrator` per Joomla update spec (#611)
* pre-release version suffix number preserved (e.g. `-rc2` not `-rc`)
* feed generator uses `FullElementName()` for auto-constructed element names
* fix: wiki API sub-page support and content response (#606, #607, PR #608)
* wiki routes use wildcard to support pages with path separators
* `ListWikiPages` returns pages in subdirectories
* error logging for empty content_base64 responses
* fix: deploy workflow clones wrong repo and runs swapoff (#609)
* removed `swapoff -a` that crashed MySQL during deploys
* fixed source repo URL from MokoGitea to MokoGitea-APP
* MCP
* metadata update tool now exposes element_name, display_name, description, license_name, language fields
## [06.14.00] --- 2026-06-11
All notable changes to MokoGitea are documented here. Versions follow the format All notable changes to MokoGitea are documented here. Versions follow the format
`v{upstream}-moko.{major}.{minor}` (e.g. `v1.26.1-moko.06.03`). `v{upstream}-moko.{major}.{minor}` (e.g. `v1.26.1-moko.06.03`).
## [06.14.00] --- 2026-06-09 ## [Unreleased]
* FEATURES * FEATURES
* feat(api): issue status/priority/type exposed in REST API - GET/PATCH on issues now includes status_id, priority_id, type_id with resolved names * feat(api): issue status/priority/type exposed in REST API - GET/PATCH on issues now includes status_id, priority_id, type_id with resolved names
@@ -59,3 +17,213 @@ All notable changes to MokoGitea are documented here. Versions follow the format
* MIGRATIONS * MIGRATIONS
* migration 354: add wiki_mode and wiki_url columns to user table for org wiki settings * migration 354: add wiki_mode and wiki_url columns to user table for org wiki settings
## [v1.26.1-moko.06.12] - 2026-06-07
* FEATURES
* feat(security): dependency vulnerability scanner - parses go.mod, package.json, composer.json, requirements.txt and checks against OSV.dev API (#551)
* feat(cdn): built-in CDN for release asset delivery via cdn.mokoconsulting.tech with per-asset public/private toggles (#561)
* feat(cdn): IP/CIDR and referrer domain allowlists for CDN abuse prevention
* feat(cdn): releases in update streams excluded from CDN (update server takes precedence)
* FIXES
* fix(licensing): hide "Require license key" option for Joomla update servers (Joomla limitation)
* fix(settings): remove duplicate description from manifest page (#559)
* INFRASTRUCTURE
* chore: rename moko-platform to MokoPlatform across codebase (#548)
* CDN CNAME: cdn.mokoconsulting.tech with auto-TLS via Let's Encrypt
* Nginx reverse proxy for CDN hostname on production server
* DreamHost MCP server path and API key configured
## [v1.26.1-moko.06.10] - 2026-06-06
* FEATURES
* feat(issues): first-class Type field with 12 auto-seeded defaults (Bug, Feature, Enhancement, Task, Documentation, Security, Roadmap, Client, Dolibarr, Infrastructure, Joomla, WaaS)
* feat(issues): first-class Status field with 13 auto-seeded defaults including 7 Pending states
* feat(issues): first-class Priority field with 4 auto-seeded defaults (Critical, High, Medium, Low)
* feat(issues): Type/Status/Priority colored badges in issue list view
* feat(issues): status dropdown replaces close/reopen button in comment form
* feat(security): built-in security scanning platform with secret scanner (15 patterns)
* feat(security): Security tab in repo navigation with alerts, scan controls
* feat(wiki): hierarchical folder navigation with sidebar tree and breadcrumbs
* feat(ui): well-known file tabs (README/LICENSE/CONTRIBUTING/SECURITY/CHANGELOG)
* feat(settings): repo manifest settings with REST API and auto-sync on push
* feat(mcp): public MCP server published to npm (@mokoconsulting/mokogitea-mcp)
* feat(mcp): SSE transport, env var config, Docker support, 120+ tools
* feat(mcp): issue dedup on create, type_id/status_id/priority_id params
* MIGRATIONS
* All org labels migrated to first-class Type/Status/Priority fields and deleted
* Type custom field (id=9) migrated to type_id and deleted
* Status custom field (id=1) deleted (replaced by first-class field)
* Priority labels migrated to priority_id
* Pending labels migrated to status definitions
* Scope labels migrated to type definitions
* Manifests populated for all 61 repos via API
* FIXES
* fix(ui): dashboard issue count badges use label spans instead of strong tags
* fix(wiki): directory check before raw redirect for folder navigation
* fix(wiki): proper display names in sidebar tree (strip dash markers)
* fix: replace non-ASCII em dashes with hyphens for hook compatibility
* fix: hookify __init__.py for stop hook JSON validation
* INFRASTRUCTURE
* npm: @mokoconsulting/mokogitea-mcp@1.1.0 and @mokoconsulting/mokowaas-mcp@1.0.0
* MCP servers consolidated under mokoplatform/mcp/servers/
* Remote MCP repos renamed to hyphens
* Wiki restructured into features/, api/, operations/ folders
* Swagger API docs enabled at /api/swagger
## [v1.26.1-moko.06.04] - 2026-06-06
* 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)
* Domain lock grace period (DomainLockHours)
* Domain restriction on packages and keys (comma-separated allowed domains)
* RepoScope enforcement — packages scoped to specific repos
* 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-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] - 2026-05-31
* BREAKING CHANGES
* Deprecated Issue.Ref branch selector UI (#307)
* Removed branch/tag selector from issue sidebar and new issue form
* DB column and commit-close logic preserved for backward compatibility
* FEATURES
* feat(ui): generic combo-multiselect component (#361)
* Reusable dropdown with search, checkable items, and selected-items display
* Template: `shared/combolist.tmpl`
* feat(updates): extension metadata settings for update feed generation
* feat(licenses): platform enforcement, key deletion, expired key cleanup
* 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 to code.mokoconsulting.tech (#336, #337, #344)
* fix(blame): set HasSourceRenderedToggle for renderable files (#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(go): replace ValuesRepository with maps.Values (Go 1.21+) (#357)
* 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, 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)
* fix(licenses): remove duplicate DeleteLicenseKey declaration
* 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
* INFRASTRUCTURE
* fix(ci): auto-deploy to production on merge to main (#235)
## [v1.26.1-moko.04] - 2026-05-24
* SECURITY
* Backport 12 upstream v1.26.2 security fixes:
* golang.org/x/net v0.55.0 security update (#140)
* Token scope enforcement on raw/media/attachment downloads (#141)
* OAuth PKCE hardening and refresh token replay protection (#142)
* Wiki git write and LFS token access enforcement (#143)
* Public-only token filtering in API queries (#144)
* Artifact signature payload hardening (#146)
* AWS credentials encryption (#161)
* Mermaid v11.15.0 security update (#162)
* Composer package permission check (#164)
* BUGFIXES
* fix(actions): nil pointer dereference in concurrency during PR creation (#136)
* fix(ui): actions runs list broken row layout (#138)
* fix: scheduled action panic with null event payload
* fix: treat email addresses case-insensitively
* fix: .mod lexer panic — removed invalid AMPL mapping
* FEATURES
* Joomla-style updates.xml with channel selection
* Update checker with configurable CHANNEL setting
* Admin dashboard update banner with docker pull command
* Upstream bug sync workflow — daily automated issue creation
* PR RC release workflow — auto-build RC on PR to main
* INFRASTRUCTURE
* New 3-part versioning: v{upstream}-moko.{major}.{minor}.{patch}
* Branding updates: error pages, home page, settings link
* Deploy workflow updated for new version format
* PROCESS
* Created `type: bug` and `upstream` labels for automated issue tracking
* Closed 24 upstream bug/security issues after backporting
+1
Submodule mcp-mokogitea-api added at c9eb6cfc89
@@ -1,7 +1,7 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech> // Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package updateserver package licenses
import ( import (
"context" "context"
@@ -1,7 +1,7 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech> // Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package updateserver package licenses
import ( import (
"context" "context"
@@ -1,7 +1,7 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech> // Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package updateserver package licenses
import ( import (
"context" "context"
@@ -1,7 +1,7 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech> // Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package updateserver package licenses
import ( import (
"context" "context"
@@ -1,7 +1,7 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech> // Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package updateserver package licenses
import ( import (
"context" "context"
@@ -1,7 +1,7 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech> // Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package updateserver package licenses
import ( import (
"context" "context"
-1
View File
@@ -432,7 +432,6 @@ func prepareMigrationTasks() []*migration {
newMigration(352, "Add version prefix and element name to repo manifest", v1_27.AddManifestVersionPrefixAndElement), newMigration(352, "Add version prefix and element name to repo manifest", v1_27.AddManifestVersionPrefixAndElement),
newMigration(353, "Add distribution metadata fields to repo manifest", v1_27.AddManifestDistributionFields), newMigration(353, "Add distribution metadata fields to repo manifest", v1_27.AddManifestDistributionFields),
newMigration(354, "Add org wiki settings to user table", v1_27.AddOrgWikiSettings), newMigration(354, "Add org wiki settings to user table", v1_27.AddOrgWikiSettings),
newMigration(355, "Migrate update server metadata to repo manifest", v1_27.MigrateUpdateServerFieldsToManifest),
} }
return preparedMigrations return preparedMigrations
} }
-108
View File
@@ -1,108 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import "xorm.io/xorm"
// MigrateUpdateServerFieldsToManifest copies extension metadata from
// update_stream_config into repo_manifest where the manifest fields are empty.
// This consolidates the source of truth into repo_manifest.
func MigrateUpdateServerFieldsToManifest(x *xorm.Engine) error {
// Copy display_name from config to manifest where manifest is empty
_, err := x.Exec(`
UPDATE repo_manifest m
INNER JOIN update_stream_config c ON m.repo_id = c.repo_id
SET m.display_name = c.display_name
WHERE (m.display_name IS NULL OR m.display_name = '') AND c.display_name != ''
`)
if err != nil {
return err
}
// Copy extension_name → element_name
_, err = x.Exec(`
UPDATE repo_manifest m
INNER JOIN update_stream_config c ON m.repo_id = c.repo_id
SET m.element_name = c.extension_name
WHERE (m.element_name IS NULL OR m.element_name = '') AND c.extension_name != ''
`)
if err != nil {
return err
}
// Copy extension_type → package_type
_, err = x.Exec(`
UPDATE repo_manifest m
INNER JOIN update_stream_config c ON m.repo_id = c.repo_id
SET m.package_type = c.extension_type
WHERE (m.package_type IS NULL OR m.package_type = '') AND c.extension_type != ''
`)
if err != nil {
return err
}
// Copy target_version
_, err = x.Exec(`
UPDATE repo_manifest m
INNER JOIN update_stream_config c ON m.repo_id = c.repo_id
SET m.target_version = c.target_version
WHERE (m.target_version IS NULL OR m.target_version = '') AND c.target_version != ''
`)
if err != nil {
return err
}
// Copy maintainer
_, err = x.Exec(`
UPDATE repo_manifest m
INNER JOIN update_stream_config c ON m.repo_id = c.repo_id
SET m.maintainer = c.maintainer
WHERE (m.maintainer IS NULL OR m.maintainer = '') AND c.maintainer != ''
`)
if err != nil {
return err
}
// Copy maintainer_url
_, err = x.Exec(`
UPDATE repo_manifest m
INNER JOIN update_stream_config c ON m.repo_id = c.repo_id
SET m.maintainer_url = c.maintainer_url
WHERE (m.maintainer_url IS NULL OR m.maintainer_url = '') AND c.maintainer_url != ''
`)
if err != nil {
return err
}
// Copy info_url
_, err = x.Exec(`
UPDATE repo_manifest m
INNER JOIN update_stream_config c ON m.repo_id = c.repo_id
SET m.info_url = c.info_url
WHERE (m.info_url IS NULL OR m.info_url = '') AND c.info_url != ''
`)
if err != nil {
return err
}
// Copy php_minimum
_, err = x.Exec(`
UPDATE repo_manifest m
INNER JOIN update_stream_config c ON m.repo_id = c.repo_id
SET m.php_minimum = c.php_minimum
WHERE (m.php_minimum IS NULL OR m.php_minimum = '') AND c.php_minimum != ''
`)
if err != nil {
return err
}
// Copy platform from config to manifest where manifest platform is empty
_, err = x.Exec(`
UPDATE repo_manifest m
INNER JOIN update_stream_config c ON m.repo_id = c.repo_id
SET m.platform = c.platform
WHERE (m.platform IS NULL OR m.platform = '') AND c.platform != ''
`)
return err
}
+4 -4
View File
@@ -114,10 +114,10 @@ type CreateIssueOption struct {
Closed bool `json:"closed"` Closed bool `json:"closed"`
// custom field values keyed by field name // custom field values keyed by field name
CustomFields map[string]string `json:"custom_fields,omitempty"` CustomFields map[string]string `json:"custom_fields,omitempty"`
// org-level issue metadata IDs (auto-assigned from org defaults when 0) // org-level issue metadata IDs
StatusID int64 `json:"status_id"` StatusID *int64 `json:"status_id,omitempty"`
PriorityID int64 `json:"priority_id"` PriorityID *int64 `json:"priority_id,omitempty"`
TypeID int64 `json:"type_id"` TypeID *int64 `json:"type_id,omitempty"`
} }
// EditIssueOption options for editing an issue // EditIssueOption options for editing an issue
-2
View File
@@ -2976,8 +2976,6 @@
"org.settings.custom_field_options": "Options (JSON)", "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_options_help": "For dropdown fields, enter options as a JSON array.",
"org.settings.custom_field_description": "Description", "org.settings.custom_field_description": "Description",
"org.settings.custom_field_required": "Required",
"org.settings.custom_field_required_help": "When checked, this field must be filled in when creating or editing issues.",
"org.settings.custom_field_created": "Custom field created.", "org.settings.custom_field_created": "Custom field created.",
"org.settings.custom_field_updated": "Custom field updated.", "org.settings.custom_field_updated": "Custom field updated.",
"org.settings.custom_field_deleted": "Custom field deleted.", "org.settings.custom_field_deleted": "Custom field deleted.",
+3 -6
View File
@@ -1306,11 +1306,11 @@ func Routes() *web.Router {
m.Combo("/{timetrackingusername}").Get(repo.ListTrackedTimesByUser) m.Combo("/{timetrackingusername}").Get(repo.ListTrackedTimesByUser)
}, mustEnableIssues, reqToken()) }, mustEnableIssues, reqToken())
m.Group("/wiki", func() { m.Group("/wiki", func() {
m.Combo("/page/*"). m.Combo("/page/{pageName}").
Get(repo.GetWikiPage). Get(repo.GetWikiPage).
Patch(mustNotBeArchived, reqToken(), reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.EditWikiPage). Patch(mustNotBeArchived, reqToken(), reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.EditWikiPage).
Delete(mustNotBeArchived, reqToken(), reqRepoWriter(unit.TypeWiki), repo.DeleteWikiPage) Delete(mustNotBeArchived, reqToken(), reqRepoWriter(unit.TypeWiki), repo.DeleteWikiPage)
m.Get("/revisions/*", repo.ListPageRevisions) m.Get("/revisions/{pageName}", repo.ListPageRevisions)
m.Post("/new", reqToken(), mustNotBeArchived, reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.NewWikiPage) m.Post("/new", reqToken(), mustNotBeArchived, reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.NewWikiPage)
m.Get("/pages", repo.ListWikiPages) m.Get("/pages", repo.ListWikiPages)
}, mustEnableWiki) }, mustEnableWiki)
@@ -1479,10 +1479,7 @@ func Routes() *web.Router {
Delete(reqToken(), repo.DeleteTopic) Delete(reqToken(), repo.DeleteTopic)
}, reqAdmin()) }, reqAdmin())
}, reqAnyRepoReader()) }, reqAnyRepoReader())
m.Combo("/metadata", reqRepoReader(unit.TypeCode)). m.Combo("/manifest", reqRepoReader(unit.TypeCode)).
Get(repo.GetRepoManifest).
Put(reqToken(), reqAdmin(), repo.UpdateRepoManifest)
m.Combo("/manifest", reqRepoReader(unit.TypeCode)). // backward compat
Get(repo.GetRepoManifest). Get(repo.GetRepoManifest).
Put(reqToken(), reqAdmin(), repo.UpdateRepoManifest) Put(reqToken(), reqAdmin(), repo.UpdateRepoManifest)
// MokoGitea badge engine // MokoGitea badge engine
+25 -62
View File
@@ -722,22 +722,6 @@ func CreateIssue(ctx *context.APIContext) {
form.Labels = make([]int64, 0) form.Labels = make([]int64, 0)
} }
// Validate required custom fields BEFORE creating the issue to avoid
// leaving orphaned issues when validation fails.
customFieldDefs, defErr := issues_model.GetCustomFieldsByOwner(ctx, ctx.Repo.Repository.OwnerID, issues_model.CustomFieldScopeIssue)
if defErr != nil {
ctx.APIErrorInternal(defErr)
return
}
for _, def := range customFieldDefs {
if def.Required {
if v, ok := form.CustomFields[def.Name]; !ok || strings.TrimSpace(v) == "" {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("custom field %q is required", def.Name))
return
}
}
}
if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs, form.Projects); err != nil { if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs, form.Projects); err != nil {
if errors.Is(err, user_model.ErrBlockedUser) { if errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err) ctx.APIError(http.StatusForbidden, err)
@@ -749,68 +733,47 @@ func CreateIssue(ctx *context.APIContext) {
return return
} }
// Save custom field values (reuse defs from validation above). // Save custom field values if provided (resolve field names to IDs).
if len(customFieldDefs) > 0 && len(form.CustomFields) > 0 { if len(form.CustomFields) > 0 {
vals := make(map[int64]string) defs, defErr := issues_model.GetCustomFieldsByOwner(ctx, ctx.Repo.Repository.OwnerID, issues_model.CustomFieldScopeIssue)
for _, def := range customFieldDefs { if defErr != nil {
if v, ok := form.CustomFields[def.Name]; ok { ctx.APIErrorInternal(defErr)
vals[def.ID] = v return
}
} }
if len(vals) > 0 { if len(defs) > 0 {
if setErr := issues_model.SetCustomFieldValues(ctx, issue.ID, vals); setErr != nil { vals := make(map[int64]string)
ctx.APIErrorInternal(setErr) for _, def := range defs {
return 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
}
} }
} }
} }
// Set org-level issue metadata (status/priority/type). // Set org-level issue metadata (status/priority/type) if provided
// Use provided value if > 0, otherwise auto-assign org default. if form.StatusID != nil && *form.StatusID > 0 {
if form.StatusID > 0 { if err := issues_model.SetIssueStatusID(ctx, issue.ID, *form.StatusID); err != nil {
if err := issues_model.SetIssueStatusID(ctx, issue.ID, form.StatusID); err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
} }
} else {
if defs, err := issues_model.GetIssueStatusDefsByOrg(ctx, ctx.Repo.Repository.OwnerID); err == nil {
for _, d := range defs {
if !d.ClosesIssue {
_ = issues_model.SetIssueStatusID(ctx, issue.ID, d.ID)
break
}
}
}
} }
if form.PriorityID > 0 { if form.PriorityID != nil && *form.PriorityID > 0 {
if err := issues_model.SetIssuePriorityID(ctx, issue.ID, form.PriorityID); err != nil { if err := issues_model.SetIssuePriorityID(ctx, issue.ID, *form.PriorityID); err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
} }
} else {
if defs, err := issues_model.GetIssuePriorityDefsByOrg(ctx, ctx.Repo.Repository.OwnerID); err == nil {
for _, d := range defs {
if d.IsDefault {
_ = issues_model.SetIssuePriorityID(ctx, issue.ID, d.ID)
break
}
}
}
} }
if form.TypeID > 0 { if form.TypeID != nil && *form.TypeID > 0 {
if err := issues_model.SetIssueTypeID(ctx, issue.ID, form.TypeID); err != nil { if err := issues_model.SetIssueTypeID(ctx, issue.ID, *form.TypeID); err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
} }
} else {
if defs, err := issues_model.GetIssueTypeDefsByOrg(ctx, ctx.Repo.Repository.OwnerID); err == nil {
for _, d := range defs {
if d.IsDefault {
_ = issues_model.SetIssueTypeID(ctx, issue.ID, d.ID)
break
}
}
}
} }
if form.Closed { if form.Closed {
+44 -44
View File
@@ -7,7 +7,7 @@ import (
"net/http" "net/http"
"time" "time"
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/web" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/web"
@@ -16,7 +16,7 @@ import (
// GetLicenseSettings returns the licensing/update stream settings for a repo. // GetLicenseSettings returns the licensing/update stream settings for a repo.
func GetLicenseSettings(ctx *context.APIContext) { func GetLicenseSettings(ctx *context.APIContext) {
cfg := updateserver_model.GetEffectiveConfig(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID) cfg := licenses.GetEffectiveConfig(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID)
if cfg == nil { if cfg == nil {
ctx.JSON(http.StatusOK, &structs.LicenseSettings{}) ctx.JSON(http.StatusOK, &structs.LicenseSettings{})
return return
@@ -42,7 +42,7 @@ func GetLicenseSettings(ctx *context.APIContext) {
func UpdateLicenseSettings(ctx *context.APIContext) { func UpdateLicenseSettings(ctx *context.APIContext) {
form := web.GetForm(ctx).(*structs.LicenseSettings) form := web.GetForm(ctx).(*structs.LicenseSettings)
cfg := &updateserver_model.UpdateStreamConfig{ cfg := &licenses.UpdateStreamConfig{
OwnerID: ctx.Repo.Repository.OwnerID, OwnerID: ctx.Repo.Repository.OwnerID,
RepoID: ctx.Repo.Repository.ID, RepoID: ctx.Repo.Repository.ID,
LicensingEnabled: form.LicensingEnabled, LicensingEnabled: form.LicensingEnabled,
@@ -61,7 +61,7 @@ func UpdateLicenseSettings(ctx *context.APIContext) {
StreamMode: "joomla", StreamMode: "joomla",
} }
if err := updateserver_model.SaveConfig(ctx, cfg); err != nil { if err := licenses.SaveConfig(ctx, cfg); err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
} }
@@ -70,7 +70,7 @@ func UpdateLicenseSettings(ctx *context.APIContext) {
} }
// verifyPackageOwnership checks that a package belongs to the current repo's owner. // verifyPackageOwnership checks that a package belongs to the current repo's owner.
func verifyPackageOwnership(ctx *context.APIContext, pkg *updateserver_model.LicensePackage) bool { func verifyPackageOwnership(ctx *context.APIContext, pkg *licenses.LicensePackage) bool {
if pkg.OwnerID != ctx.Repo.Repository.OwnerID { if pkg.OwnerID != ctx.Repo.Repository.OwnerID {
ctx.APIErrorNotFound(nil) ctx.APIErrorNotFound(nil)
return false return false
@@ -79,7 +79,7 @@ func verifyPackageOwnership(ctx *context.APIContext, pkg *updateserver_model.Lic
} }
// verifyKeyOwnership checks that a key belongs to the current repo's owner. // verifyKeyOwnership checks that a key belongs to the current repo's owner.
func verifyKeyOwnership(ctx *context.APIContext, key *updateserver_model.LicenseKey) bool { func verifyKeyOwnership(ctx *context.APIContext, key *licenses.LicenseKey) bool {
if key.OwnerID != ctx.Repo.Repository.OwnerID { if key.OwnerID != ctx.Repo.Repository.OwnerID {
ctx.APIErrorNotFound(nil) ctx.APIErrorNotFound(nil)
return false return false
@@ -87,7 +87,7 @@ func verifyKeyOwnership(ctx *context.APIContext, key *updateserver_model.License
return true return true
} }
func toLicensePackageAPI(pkg *updateserver_model.LicensePackage) *structs.LicensePackage { func toLicensePackageAPI(pkg *licenses.LicensePackage) *structs.LicensePackage {
return &structs.LicensePackage{ return &structs.LicensePackage{
ID: pkg.ID, ID: pkg.ID,
OwnerID: pkg.OwnerID, OwnerID: pkg.OwnerID,
@@ -103,7 +103,7 @@ func toLicensePackageAPI(pkg *updateserver_model.LicensePackage) *structs.Licens
} }
} }
func toLicenseKeyAPI(key *updateserver_model.LicenseKey) *structs.LicenseKey { func toLicenseKeyAPI(key *licenses.LicenseKey) *structs.LicenseKey {
lk := &structs.LicenseKey{ lk := &structs.LicenseKey{
ID: key.ID, ID: key.ID,
PackageID: key.PackageID, PackageID: key.PackageID,
@@ -134,7 +134,7 @@ func toLicenseKeyAPI(key *updateserver_model.LicenseKey) *structs.LicenseKey {
// ListLicensePackages lists license packages for the repo owner. // ListLicensePackages lists license packages for the repo owner.
func ListLicensePackages(ctx *context.APIContext) { func ListLicensePackages(ctx *context.APIContext) {
pkgs, err := updateserver_model.ListLicensePackages(ctx, ctx.Repo.Repository.OwnerID) pkgs, err := licenses.ListLicensePackages(ctx, ctx.Repo.Repository.OwnerID)
if err != nil { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
@@ -151,7 +151,7 @@ func ListLicensePackages(ctx *context.APIContext) {
func CreateLicensePackage(ctx *context.APIContext) { func CreateLicensePackage(ctx *context.APIContext) {
form := web.GetForm(ctx).(*structs.CreateLicensePackageOption) form := web.GetForm(ctx).(*structs.CreateLicensePackageOption)
pkg := &updateserver_model.LicensePackage{ pkg := &licenses.LicensePackage{
OwnerID: ctx.Repo.Repository.OwnerID, OwnerID: ctx.Repo.Repository.OwnerID,
Name: form.Name, Name: form.Name,
Description: form.Description, Description: form.Description,
@@ -165,7 +165,7 @@ func CreateLicensePackage(ctx *context.APIContext) {
pkg.RepoScope = "all" pkg.RepoScope = "all"
} }
if err := updateserver_model.CreateLicensePackage(ctx, pkg); err != nil { if err := licenses.CreateLicensePackage(ctx, pkg); err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
} }
@@ -178,7 +178,7 @@ func EditLicensePackage(ctx *context.APIContext) {
form := web.GetForm(ctx).(*structs.EditLicensePackageOption) form := web.GetForm(ctx).(*structs.EditLicensePackageOption)
pkgID := ctx.PathParamInt64("id") pkgID := ctx.PathParamInt64("id")
pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID) pkg, err := licenses.GetLicensePackageByID(ctx, pkgID)
if err != nil { if err != nil {
ctx.APIErrorNotFound(err) ctx.APIErrorNotFound(err)
return return
@@ -187,7 +187,7 @@ func EditLicensePackage(ctx *context.APIContext) {
return return
} }
if pkg.Name == updateserver_model.MasterPackageName { if pkg.Name == licenses.MasterPackageName {
ctx.APIError(http.StatusForbidden, "master package cannot be edited") ctx.APIError(http.StatusForbidden, "master package cannot be edited")
return return
} }
@@ -214,7 +214,7 @@ func EditLicensePackage(ctx *context.APIContext) {
pkg.IsActive = *form.IsActive pkg.IsActive = *form.IsActive
} }
if err := updateserver_model.UpdateLicensePackage(ctx, pkg); err != nil { if err := licenses.UpdateLicensePackage(ctx, pkg); err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
} }
@@ -226,7 +226,7 @@ func EditLicensePackage(ctx *context.APIContext) {
func DeleteLicensePackage(ctx *context.APIContext) { func DeleteLicensePackage(ctx *context.APIContext) {
pkgID := ctx.PathParamInt64("id") pkgID := ctx.PathParamInt64("id")
pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID) pkg, err := licenses.GetLicensePackageByID(ctx, pkgID)
if err != nil { if err != nil {
ctx.APIErrorNotFound(err) ctx.APIErrorNotFound(err)
return return
@@ -235,12 +235,12 @@ func DeleteLicensePackage(ctx *context.APIContext) {
return return
} }
if pkg.Name == updateserver_model.MasterPackageName { if pkg.Name == licenses.MasterPackageName {
ctx.APIError(http.StatusForbidden, "master package cannot be deleted") ctx.APIError(http.StatusForbidden, "master package cannot be deleted")
return return
} }
if err := updateserver_model.DeleteLicensePackage(ctx, pkgID); err != nil { if err := licenses.DeleteLicensePackage(ctx, pkgID); err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
} }
@@ -252,7 +252,7 @@ func DeleteLicensePackage(ctx *context.APIContext) {
func ArchiveLicensePackage(ctx *context.APIContext) { func ArchiveLicensePackage(ctx *context.APIContext) {
pkgID := ctx.PathParamInt64("id") pkgID := ctx.PathParamInt64("id")
pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID) pkg, err := licenses.GetLicensePackageByID(ctx, pkgID)
if err != nil { if err != nil {
ctx.APIErrorNotFound(err) ctx.APIErrorNotFound(err)
return return
@@ -261,12 +261,12 @@ func ArchiveLicensePackage(ctx *context.APIContext) {
return return
} }
if pkg.Name == updateserver_model.MasterPackageName { if pkg.Name == licenses.MasterPackageName {
ctx.APIError(http.StatusForbidden, "master package cannot be archived") ctx.APIError(http.StatusForbidden, "master package cannot be archived")
return return
} }
if err := updateserver_model.ArchiveLicensePackage(ctx, pkgID); err != nil { if err := licenses.ArchiveLicensePackage(ctx, pkgID); err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
} }
@@ -277,7 +277,7 @@ func ArchiveLicensePackage(ctx *context.APIContext) {
// UnarchiveLicensePackage restores an archived license package via API. // UnarchiveLicensePackage restores an archived license package via API.
func UnarchiveLicensePackage(ctx *context.APIContext) { func UnarchiveLicensePackage(ctx *context.APIContext) {
pkgID := ctx.PathParamInt64("id") pkgID := ctx.PathParamInt64("id")
pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID) pkg, err := licenses.GetLicensePackageByID(ctx, pkgID)
if err != nil { if err != nil {
ctx.APIErrorNotFound(err) ctx.APIErrorNotFound(err)
return return
@@ -285,7 +285,7 @@ func UnarchiveLicensePackage(ctx *context.APIContext) {
if !verifyPackageOwnership(ctx, pkg) { if !verifyPackageOwnership(ctx, pkg) {
return return
} }
if err := updateserver_model.UnarchiveLicensePackage(ctx, pkgID); err != nil { if err := licenses.UnarchiveLicensePackage(ctx, pkgID); err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
} }
@@ -294,7 +294,7 @@ func UnarchiveLicensePackage(ctx *context.APIContext) {
// ListLicenseKeys lists license keys for the repo owner. // ListLicenseKeys lists license keys for the repo owner.
func ListLicenseKeys(ctx *context.APIContext) { func ListLicenseKeys(ctx *context.APIContext) {
keys, err := updateserver_model.ListLicenseKeys(ctx, ctx.Repo.Repository.OwnerID) keys, err := licenses.ListLicenseKeys(ctx, ctx.Repo.Repository.OwnerID)
if err != nil { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
@@ -311,7 +311,7 @@ func ListLicenseKeys(ctx *context.APIContext) {
func CreateLicenseKey(ctx *context.APIContext) { func CreateLicenseKey(ctx *context.APIContext) {
form := web.GetForm(ctx).(*structs.CreateLicenseKeyOption) form := web.GetForm(ctx).(*structs.CreateLicenseKeyOption)
key := &updateserver_model.LicenseKey{ key := &licenses.LicenseKey{
PackageID: form.PackageID, PackageID: form.PackageID,
OwnerID: ctx.Repo.Repository.OwnerID, OwnerID: ctx.Repo.Repository.OwnerID,
LicenseeName: form.LicenseeName, LicenseeName: form.LicenseeName,
@@ -329,7 +329,7 @@ func CreateLicenseKey(ctx *context.APIContext) {
key.ExpiresUnix = timeutil.TimeStamp(form.ExpiresAt.Unix()) key.ExpiresUnix = timeutil.TimeStamp(form.ExpiresAt.Unix())
} else { } else {
// Auto-calculate from package duration. // Auto-calculate from package duration.
pkg, err := updateserver_model.GetLicensePackageByID(ctx, form.PackageID) pkg, err := licenses.GetLicensePackageByID(ctx, form.PackageID)
if err != nil { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
@@ -344,7 +344,7 @@ func CreateLicenseKey(ctx *context.APIContext) {
} }
} }
rawKey, err := updateserver_model.CreateLicenseKey(ctx, key) rawKey, err := licenses.CreateLicenseKey(ctx, key)
if err != nil { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
@@ -362,7 +362,7 @@ func EditLicenseKey(ctx *context.APIContext) {
form := web.GetForm(ctx).(*structs.EditLicenseKeyOption) form := web.GetForm(ctx).(*structs.EditLicenseKeyOption)
keyID := ctx.PathParamInt64("id") keyID := ctx.PathParamInt64("id")
key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID) key, err := licenses.GetLicenseKeyByID(ctx, keyID)
if err != nil { if err != nil {
ctx.APIErrorNotFound(err) ctx.APIErrorNotFound(err)
return return
@@ -395,7 +395,7 @@ func EditLicenseKey(ctx *context.APIContext) {
key.ExpiresUnix = timeutil.TimeStamp(form.ExpiresAt.Unix()) key.ExpiresUnix = timeutil.TimeStamp(form.ExpiresAt.Unix())
} }
if err := updateserver_model.UpdateLicenseKey(ctx, key); err != nil { if err := licenses.UpdateLicenseKey(ctx, key); err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
} }
@@ -409,7 +409,7 @@ func PurchaseLicenseKey(ctx *context.APIContext) {
// Idempotency check: if payment_ref already exists, return existing key. // Idempotency check: if payment_ref already exists, return existing key.
if form.PaymentRef != "" { if form.PaymentRef != "" {
existing, err := updateserver_model.GetLicenseKeyByPaymentRef(ctx, form.PaymentRef) existing, err := licenses.GetLicenseKeyByPaymentRef(ctx, form.PaymentRef)
if err == nil { if err == nil {
resp := &structs.LicenseKeyCreated{ resp := &structs.LicenseKeyCreated{
LicenseKey: *toLicenseKeyAPI(existing), LicenseKey: *toLicenseKeyAPI(existing),
@@ -420,7 +420,7 @@ func PurchaseLicenseKey(ctx *context.APIContext) {
} }
} }
pkg, err := updateserver_model.GetLicensePackageByID(ctx, form.PackageID) pkg, err := licenses.GetLicensePackageByID(ctx, form.PackageID)
if err != nil { if err != nil {
ctx.APIErrorNotFound(err) ctx.APIErrorNotFound(err)
return return
@@ -429,7 +429,7 @@ func PurchaseLicenseKey(ctx *context.APIContext) {
return return
} }
key := &updateserver_model.LicenseKey{ key := &licenses.LicenseKey{
PackageID: form.PackageID, PackageID: form.PackageID,
OwnerID: ctx.Repo.Repository.OwnerID, OwnerID: ctx.Repo.Repository.OwnerID,
LicenseeName: form.LicenseeName, LicenseeName: form.LicenseeName,
@@ -444,7 +444,7 @@ func PurchaseLicenseKey(ctx *context.APIContext) {
key.ExpiresUnix = timeutil.TimeStamp(expires.Unix()) key.ExpiresUnix = timeutil.TimeStamp(expires.Unix())
} }
rawKey, err := updateserver_model.CreateLicenseKey(ctx, key) rawKey, err := licenses.CreateLicenseKey(ctx, key)
if err != nil { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
@@ -460,7 +460,7 @@ func PurchaseLicenseKey(ctx *context.APIContext) {
// RenewLicenseKey extends a key's expiration by its package duration. // RenewLicenseKey extends a key's expiration by its package duration.
func RenewLicenseKey(ctx *context.APIContext) { func RenewLicenseKey(ctx *context.APIContext) {
keyID := ctx.PathParamInt64("id") keyID := ctx.PathParamInt64("id")
key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID) key, err := licenses.GetLicenseKeyByID(ctx, keyID)
if err != nil { if err != nil {
ctx.APIErrorNotFound(err) ctx.APIErrorNotFound(err)
return return
@@ -469,7 +469,7 @@ func RenewLicenseKey(ctx *context.APIContext) {
return return
} }
pkg, err := updateserver_model.GetLicensePackageByID(ctx, key.PackageID) pkg, err := licenses.GetLicensePackageByID(ctx, key.PackageID)
if err != nil { if err != nil {
ctx.APIErrorNotFound(err) ctx.APIErrorNotFound(err)
return return
@@ -480,20 +480,20 @@ func RenewLicenseKey(ctx *context.APIContext) {
days = 365 days = 365
} }
if err := updateserver_model.RenewLicenseKey(ctx, keyID, days); err != nil { if err := licenses.RenewLicenseKey(ctx, keyID, days); err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
} }
// Reload key to get updated fields. // Reload key to get updated fields.
key, _ = updateserver_model.GetLicenseKeyByID(ctx, keyID) key, _ = licenses.GetLicenseKeyByID(ctx, keyID)
ctx.JSON(http.StatusOK, toLicenseKeyAPI(key)) ctx.JSON(http.StatusOK, toLicenseKeyAPI(key))
} }
// RevokeLicenseKey deactivates a license key via API. // RevokeLicenseKey deactivates a license key via API.
func RevokeLicenseKey(ctx *context.APIContext) { func RevokeLicenseKey(ctx *context.APIContext) {
keyID := ctx.PathParamInt64("id") keyID := ctx.PathParamInt64("id")
key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID) key, err := licenses.GetLicenseKeyByID(ctx, keyID)
if err != nil { if err != nil {
ctx.APIErrorNotFound(err) ctx.APIErrorNotFound(err)
return return
@@ -503,7 +503,7 @@ func RevokeLicenseKey(ctx *context.APIContext) {
} }
key.IsActive = false key.IsActive = false
if err := updateserver_model.UpdateLicenseKey(ctx, key); err != nil { if err := licenses.UpdateLicenseKey(ctx, key); err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
} }
@@ -514,7 +514,7 @@ func RevokeLicenseKey(ctx *context.APIContext) {
// DeleteLicenseKey deletes a license key. // DeleteLicenseKey deletes a license key.
func DeleteLicenseKey(ctx *context.APIContext) { func DeleteLicenseKey(ctx *context.APIContext) {
keyID := ctx.PathParamInt64("id") keyID := ctx.PathParamInt64("id")
key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID) key, err := licenses.GetLicenseKeyByID(ctx, keyID)
if err != nil { if err != nil {
ctx.APIErrorNotFound(err) ctx.APIErrorNotFound(err)
return return
@@ -522,7 +522,7 @@ func DeleteLicenseKey(ctx *context.APIContext) {
if !verifyKeyOwnership(ctx, key) { if !verifyKeyOwnership(ctx, key) {
return return
} }
if err := updateserver_model.DeleteLicenseKey(ctx, keyID); err != nil { if err := licenses.DeleteLicenseKey(ctx, keyID); err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
} }
@@ -533,7 +533,7 @@ func DeleteLicenseKey(ctx *context.APIContext) {
func ValidateLicenseKey(ctx *context.APIContext) { func ValidateLicenseKey(ctx *context.APIContext) {
form := web.GetForm(ctx).(*structs.ValidateLicenseKeyOption) form := web.GetForm(ctx).(*structs.ValidateLicenseKeyOption)
key, pkg, err := updateserver_model.ValidateLicenseKey(ctx, form.Key, form.Domain) key, pkg, err := licenses.ValidateLicenseKey(ctx, form.Key, form.Domain)
if err != nil { if err != nil {
ctx.JSON(http.StatusOK, &structs.ValidateLicenseKeyResponse{ ctx.JSON(http.StatusOK, &structs.ValidateLicenseKeyResponse{
Valid: false, Valid: false,
@@ -542,7 +542,7 @@ func ValidateLicenseKey(ctx *context.APIContext) {
return return
} }
_ = updateserver_model.TouchHeartbeat(ctx, key.ID) _ = licenses.TouchHeartbeat(ctx, key.ID)
var expiresAt *time.Time var expiresAt *time.Time
if key.ExpiresUnix > 0 { if key.ExpiresUnix > 0 {
@@ -555,7 +555,7 @@ func ValidateLicenseKey(ctx *context.APIContext) {
maxSites = pkg.MaxSites maxSites = pkg.MaxSites
} }
sitesUsed, _ := updateserver_model.CountUniqueDomainsByKey(ctx, key.ID) sitesUsed, _ := licenses.CountUniqueDomainsByKey(ctx, key.ID)
ctx.JSON(http.StatusOK, &structs.ValidateLicenseKeyResponse{ ctx.JSON(http.StatusOK, &structs.ValidateLicenseKeyResponse{
Valid: true, Valid: true,
@@ -569,7 +569,7 @@ func ValidateLicenseKey(ctx *context.APIContext) {
// GetLicenseKeyUsage returns usage logs for a license key. // GetLicenseKeyUsage returns usage logs for a license key.
func GetLicenseKeyUsage(ctx *context.APIContext) { func GetLicenseKeyUsage(ctx *context.APIContext) {
usages, err := updateserver_model.GetRecentUsage(ctx, ctx.PathParamInt64("id"), 100) usages, err := licenses.GetRecentUsage(ctx, ctx.PathParamInt64("id"), 100)
if err != nil { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
+11 -25
View File
@@ -12,7 +12,6 @@ import (
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/gitrepo" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/gitrepo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
api "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" api "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/util" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/util"
@@ -137,7 +136,7 @@ func EditWikiPage(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.CreateWikiPageOptions) form := web.GetForm(ctx).(*api.CreateWikiPageOptions)
oldWikiName := wiki_service.WebPathFromRequest(ctx.PathParam("*")) oldWikiName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("pageName"))
newWikiName := wiki_service.UserTitleToWebPath("", form.Title) newWikiName := wiki_service.UserTitleToWebPath("", form.Title)
if len(newWikiName) == 0 { if len(newWikiName) == 0 {
@@ -243,7 +242,7 @@ func DeleteWikiPage(ctx *context.APIContext) {
// "423": // "423":
// "$ref": "#/responses/repoArchivedError" // "$ref": "#/responses/repoArchivedError"
wikiName := wiki_service.WebPathFromRequest(ctx.PathParam("*")) wikiName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("pageName"))
if err := wiki_service.DeleteWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName); err != nil { if err := wiki_service.DeleteWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName); err != nil {
if err.Error() == "file does not exist" { if err.Error() == "file does not exist" {
@@ -308,23 +307,14 @@ func ListWikiPages(ctx *context.APIContext) {
skip := (page - 1) * limit skip := (page - 1) * limit
maxNum := page * limit maxNum := page * limit
entries, err := commit.ListEntriesRecursiveFast() entries, err := commit.ListEntries()
if err != nil { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
} }
pages := make([]*api.WikiPageMetaData, 0, len(entries))
// Filter to regular files only and count for pagination. for i, entry := range entries {
var regularEntries []*git.TreeEntry if i < skip || i >= maxNum || !entry.IsRegular() {
for _, entry := range entries {
if entry.IsRegular() {
regularEntries = append(regularEntries, entry)
}
}
pages := make([]*api.WikiPageMetaData, 0, min(len(regularEntries), limit))
for i, entry := range regularEntries {
if i < skip || i >= maxNum {
continue continue
} }
c, err := wikiRepo.GetCommitByPath(entry.Name()) c, err := wikiRepo.GetCommitByPath(entry.Name())
@@ -343,8 +333,8 @@ func ListWikiPages(ctx *context.APIContext) {
pages = append(pages, wiki_service.ToWikiPageMetaData(wikiName, c, ctx.Repo.Repository)) pages = append(pages, wiki_service.ToWikiPageMetaData(wikiName, c, ctx.Repo.Repository))
} }
ctx.SetLinkHeader(int64(len(regularEntries)), limit) ctx.SetLinkHeader(int64(len(entries)), limit)
ctx.SetTotalCountHeader(int64(len(regularEntries))) ctx.SetTotalCountHeader(int64(len(entries)))
ctx.JSON(http.StatusOK, pages) ctx.JSON(http.StatusOK, pages)
} }
@@ -378,7 +368,7 @@ func GetWikiPage(ctx *context.APIContext) {
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
// get requested pagename // get requested pagename
pageName := wiki_service.WebPathFromRequest(ctx.PathParam("*")) pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("pageName"))
wikiPage := getWikiPage(ctx, pageName) wikiPage := getWikiPage(ctx, pageName)
if !ctx.Written() { if !ctx.Written() {
@@ -428,7 +418,7 @@ func ListPageRevisions(ctx *context.APIContext) {
} }
// get requested pagename // get requested pagename
pageName := wiki_service.WebPathFromRequest(ctx.PathParam("*")) pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("pageName"))
if len(pageName) == 0 { if len(pageName) == 0 {
pageName = "Home" pageName = "Home"
} }
@@ -509,15 +499,11 @@ func findWikiRepoCommit(ctx *context.APIContext) (*git.Repository, *git.Commit)
func wikiContentsByEntry(ctx *context.APIContext, entry *git.TreeEntry) string { func wikiContentsByEntry(ctx *context.APIContext, entry *git.TreeEntry) string {
blob := entry.Blob() blob := entry.Blob()
if blob.Size() > setting.API.DefaultMaxBlobSize { if blob.Size() > setting.API.DefaultMaxBlobSize {
log.Warn("wikiContentsByEntry: blob %s exceeds max size (%d > %d), content omitted",
entry.Name(), blob.Size(), setting.API.DefaultMaxBlobSize)
return "" return ""
} }
content, err := blob.GetBlobContentBase64(nil) content, err := blob.GetBlobContentBase64(nil)
if err != nil { if err != nil {
// Return the error details but don't abort — the page metadata ctx.APIErrorInternal(err)
// is still useful even without content.
log.Error("wikiContentsByEntry: GetBlobContentBase64 for %s: %v", entry.Name(), err)
return "" return ""
} }
return content return content
+3 -3
View File
@@ -9,7 +9,7 @@ import (
"strings" "strings"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver" licenses_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/organization" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/organization"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/renderhelper" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/renderhelper"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
@@ -109,10 +109,10 @@ func home(ctx *context.Context, viewRepositories bool) {
ctx.Data["IsOrganizationMember"] = ctx.Org.IsMember ctx.Data["IsOrganizationMember"] = ctx.Org.IsMember
ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner
orgCfg, _ := updateserver_model.GetOrgConfig(ctx, ctx.Org.Organization.ID) orgCfg, _ := licenses_model.GetOrgConfig(ctx, ctx.Org.Organization.ID)
ctx.Data["OrgLicensingEnabled"] = orgCfg != nil && orgCfg.LicensingEnabled ctx.Data["OrgLicensingEnabled"] = orgCfg != nil && orgCfg.LicensingEnabled
if orgCfg != nil && orgCfg.LicensingEnabled { if orgCfg != nil && orgCfg.LicensingEnabled {
numPkgs, _ := updateserver_model.CountOrgPackages(ctx, ctx.Org.Organization.ID) numPkgs, _ := licenses_model.CountOrgPackages(ctx, ctx.Org.Organization.ID)
ctx.Data["NumOrgLicensePackages"] = numPkgs ctx.Data["NumOrgLicensePackages"] = numPkgs
} }
ctx.Data["IsPublicMember"] = func(uid int64) bool { ctx.Data["IsPublicMember"] = func(uid int64) bool {
+49 -49
View File
@@ -9,7 +9,7 @@ import (
"strings" "strings"
"time" "time"
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/perm" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/perm"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
unit_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit" unit_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit"
@@ -27,7 +27,7 @@ type OrgChannelItem struct {
Label string Label string
} }
func buildOrgChannelItems(streams []updateserver_model.StreamDef) []OrgChannelItem { func buildOrgChannelItems(streams []licenses.StreamDef) []OrgChannelItem {
items := make([]OrgChannelItem, 0, len(streams)) items := make([]OrgChannelItem, 0, len(streams))
for _, s := range streams { for _, s := range streams {
label := s.Name label := s.Name
@@ -62,7 +62,7 @@ func parseOrgAllowedChannels(s string) []string {
// LicensePackageDisplay is used in templates. // LicensePackageDisplay is used in templates.
type LicensePackageDisplay struct { type LicensePackageDisplay struct {
*updateserver_model.LicensePackage *licenses.LicensePackage
KeyCount int64 KeyCount int64
Created time.Time Created time.Time
} }
@@ -79,7 +79,7 @@ func Licenses(ctx *context.Context) {
// Auto-create master key if has write access. // Auto-create master key if has write access.
if canWriteLicenses { if canWriteLicenses {
newMasterKey, err := updateserver_model.EnsureMasterKey(ctx, ownerID) newMasterKey, err := licenses.EnsureMasterKey(ctx, ownerID)
if err != nil { if err != nil {
ctx.ServerError("EnsureMasterKey", err) ctx.ServerError("EnsureMasterKey", err)
return return
@@ -89,7 +89,7 @@ func Licenses(ctx *context.Context) {
} }
} }
pkgs, err := updateserver_model.ListLicensePackagesWithAncestors(ctx, ownerID) pkgs, err := licenses.ListLicensePackagesWithAncestors(ctx, ownerID)
if err != nil { if err != nil {
ctx.ServerError("ListLicensePackages", err) ctx.ServerError("ListLicensePackages", err)
return return
@@ -97,7 +97,7 @@ func Licenses(ctx *context.Context) {
var display []LicensePackageDisplay var display []LicensePackageDisplay
for _, pkg := range pkgs { for _, pkg := range pkgs {
count, _ := updateserver_model.CountKeysByPackage(ctx, pkg.ID) count, _ := licenses.CountKeysByPackage(ctx, pkg.ID)
display = append(display, LicensePackageDisplay{ display = append(display, LicensePackageDisplay{
LicensePackage: pkg, LicensePackage: pkg,
KeyCount: count, KeyCount: count,
@@ -110,11 +110,11 @@ func Licenses(ctx *context.Context) {
searchQuery := strings.TrimSpace(ctx.FormString("q")) searchQuery := strings.TrimSpace(ctx.FormString("q"))
ctx.Data["SearchQuery"] = searchQuery ctx.Data["SearchQuery"] = searchQuery
var keys []*updateserver_model.LicenseKey var keys []*licenses.LicenseKey
if searchQuery != "" { if searchQuery != "" {
keys, err = updateserver_model.SearchLicenseKeysWithAncestors(ctx, ownerID, searchQuery) keys, err = licenses.SearchLicenseKeysWithAncestors(ctx, ownerID, searchQuery)
} else { } else {
keys, err = updateserver_model.ListLicenseKeysWithAncestors(ctx, ownerID) keys, err = licenses.ListLicenseKeysWithAncestors(ctx, ownerID)
} }
if err != nil { if err != nil {
ctx.ServerError("ListLicenseKeys", err) ctx.ServerError("ListLicenseKeys", err)
@@ -128,10 +128,10 @@ func Licenses(ctx *context.Context) {
ctx.Data["OrgLicensingEnabled"] = true ctx.Data["OrgLicensingEnabled"] = true
// Load archived packages. // Load archived packages.
archivedPkgs, _ := updateserver_model.ListArchivedLicensePackages(ctx, ownerID) archivedPkgs, _ := licenses.ListArchivedLicensePackages(ctx, ownerID)
var archivedDisplay []LicensePackageDisplay var archivedDisplay []LicensePackageDisplay
for _, pkg := range archivedPkgs { for _, pkg := range archivedPkgs {
count, _ := updateserver_model.CountKeysByPackage(ctx, pkg.ID) count, _ := licenses.CountKeysByPackage(ctx, pkg.ID)
archivedDisplay = append(archivedDisplay, LicensePackageDisplay{ archivedDisplay = append(archivedDisplay, LicensePackageDisplay{
LicensePackage: pkg, LicensePackage: pkg,
KeyCount: count, KeyCount: count,
@@ -140,12 +140,12 @@ func Licenses(ctx *context.Context) {
} }
ctx.Data["ArchivedPackages"] = archivedDisplay ctx.Data["ArchivedPackages"] = archivedDisplay
orgCfg, _ := updateserver_model.GetOrgConfig(ctx, ownerID) orgCfg, _ := licenses.GetOrgConfig(ctx, ownerID)
var orgStreams []updateserver_model.StreamDef var orgStreams []licenses.StreamDef
if orgCfg != nil { if orgCfg != nil {
orgStreams = orgCfg.GetActiveStreams() orgStreams = orgCfg.GetActiveStreams()
} else { } else {
orgStreams = updateserver_model.DefaultJoomlaStreams() orgStreams = licenses.DefaultJoomlaStreams()
} }
ctx.Data["AvailableStreams"] = orgStreams ctx.Data["AvailableStreams"] = orgStreams
ctx.Data["ChannelItems"] = buildOrgChannelItems(orgStreams) ctx.Data["ChannelItems"] = buildOrgChannelItems(orgStreams)
@@ -182,7 +182,7 @@ func LicensesCreatePackage(ctx *context.Context) {
repoScope = "all" repoScope = "all"
} }
pkg := &updateserver_model.LicensePackage{ pkg := &licenses.LicensePackage{
OwnerID: ctx.Org.Organization.ID, OwnerID: ctx.Org.Organization.ID,
Name: name, Name: name,
Description: ctx.FormString("description"), Description: ctx.FormString("description"),
@@ -194,7 +194,7 @@ func LicensesCreatePackage(ctx *context.Context) {
IsActive: true, IsActive: true,
} }
if err := updateserver_model.CreateLicensePackage(ctx, pkg); err != nil { if err := licenses.CreateLicensePackage(ctx, pkg); err != nil {
ctx.ServerError("CreateLicensePackage", err) ctx.ServerError("CreateLicensePackage", err)
return return
} }
@@ -212,13 +212,13 @@ func LicensesGenerateKey(ctx *context.Context) {
return return
} }
pkg, err := updateserver_model.GetLicensePackageByID(ctx, packageID) pkg, err := licenses.GetLicensePackageByID(ctx, packageID)
if err != nil { if err != nil {
ctx.ServerError("GetLicensePackageByID", err) ctx.ServerError("GetLicensePackageByID", err)
return return
} }
key := &updateserver_model.LicenseKey{ key := &licenses.LicenseKey{
PackageID: packageID, PackageID: packageID,
OwnerID: ctx.Org.Organization.ID, OwnerID: ctx.Org.Organization.ID,
IsActive: true, IsActive: true,
@@ -233,13 +233,13 @@ func LicensesGenerateKey(ctx *context.Context) {
var rawKey string var rawKey string
customKey := strings.TrimSpace(ctx.FormString("custom_key")) customKey := strings.TrimSpace(ctx.FormString("custom_key"))
if customKey != "" && (ctx.IsUserSiteAdmin() || ctx.Org.IsOwner) { if customKey != "" && (ctx.IsUserSiteAdmin() || ctx.Org.IsOwner) {
if err := updateserver_model.CreateLicenseKeyCustom(ctx, key, customKey); err != nil { if err := licenses.CreateLicenseKeyCustom(ctx, key, customKey); err != nil {
ctx.ServerError("CreateLicenseKeyCustom", err) ctx.ServerError("CreateLicenseKeyCustom", err)
return return
} }
rawKey = customKey rawKey = customKey
} else { } else {
rawKey, err = updateserver_model.CreateLicenseKey(ctx, key) rawKey, err = licenses.CreateLicenseKey(ctx, key)
if err != nil { if err != nil {
ctx.ServerError("CreateLicenseKey", err) ctx.ServerError("CreateLicenseKey", err)
return return
@@ -253,10 +253,10 @@ func LicensesGenerateKey(ctx *context.Context) {
ctx.Data["NewKeyCreated"] = rawKey ctx.Data["NewKeyCreated"] = rawKey
ownerID := ctx.Org.Organization.ID ownerID := ctx.Org.Organization.ID
pkgs, _ := updateserver_model.ListLicensePackages(ctx, ownerID) pkgs, _ := licenses.ListLicensePackages(ctx, ownerID)
var display []LicensePackageDisplay var display []LicensePackageDisplay
for _, p := range pkgs { for _, p := range pkgs {
count, _ := updateserver_model.CountKeysByPackage(ctx, p.ID) count, _ := licenses.CountKeysByPackage(ctx, p.ID)
display = append(display, LicensePackageDisplay{ display = append(display, LicensePackageDisplay{
LicensePackage: p, LicensePackage: p,
KeyCount: count, KeyCount: count,
@@ -264,14 +264,14 @@ func LicensesGenerateKey(ctx *context.Context) {
}) })
} }
ctx.Data["LicensePackages"] = display ctx.Data["LicensePackages"] = display
keys, _ := updateserver_model.ListLicenseKeys(ctx, ownerID) keys, _ := licenses.ListLicenseKeys(ctx, ownerID)
ctx.Data["LicenseKeys"] = keys ctx.Data["LicenseKeys"] = keys
orgCfg, _ := updateserver_model.GetOrgConfig(ctx, ownerID) orgCfg, _ := licenses.GetOrgConfig(ctx, ownerID)
if orgCfg != nil { if orgCfg != nil {
ctx.Data["AvailableStreams"] = orgCfg.GetActiveStreams() ctx.Data["AvailableStreams"] = orgCfg.GetActiveStreams()
} else { } else {
ctx.Data["AvailableStreams"] = updateserver_model.DefaultJoomlaStreams() ctx.Data["AvailableStreams"] = licenses.DefaultJoomlaStreams()
} }
ctx.HTML(http.StatusOK, tplOrgLicenses) ctx.HTML(http.StatusOK, tplOrgLicenses)
@@ -283,13 +283,13 @@ const tplOrgLicensesEditKey templates.TplName = "repo/licenses_edit_key"
// LicensesEditPackage shows the edit form for an org license package. // LicensesEditPackage shows the edit form for an org license package.
func LicensesEditPackage(ctx *context.Context) { func LicensesEditPackage(ctx *context.Context) {
pkgID := ctx.PathParamInt64("id") pkgID := ctx.PathParamInt64("id")
pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID) pkg, err := licenses.GetLicensePackageByID(ctx, pkgID)
if err != nil { if err != nil {
ctx.ServerError("GetLicensePackageByID", err) ctx.ServerError("GetLicensePackageByID", err)
return return
} }
if pkg.Name == updateserver_model.MasterPackageName { if pkg.Name == licenses.MasterPackageName {
ctx.Flash.Error("Master package cannot be edited") ctx.Flash.Error("Master package cannot be edited")
ctx.Redirect(ctx.Org.OrgLink + "/-/licenses") ctx.Redirect(ctx.Org.OrgLink + "/-/licenses")
return return
@@ -301,12 +301,12 @@ func LicensesEditPackage(ctx *context.Context) {
selectedChannels := parseOrgAllowedChannels(pkg.AllowedChannels) selectedChannels := parseOrgAllowedChannels(pkg.AllowedChannels)
ctx.Data["SelectedChannels"] = selectedChannels ctx.Data["SelectedChannels"] = selectedChannels
orgCfg, _ := updateserver_model.GetOrgConfig(ctx, ctx.Org.Organization.ID) orgCfg, _ := licenses.GetOrgConfig(ctx, ctx.Org.Organization.ID)
var editStreams []updateserver_model.StreamDef var editStreams []licenses.StreamDef
if orgCfg != nil { if orgCfg != nil {
editStreams = orgCfg.GetActiveStreams() editStreams = orgCfg.GetActiveStreams()
} else { } else {
editStreams = updateserver_model.DefaultJoomlaStreams() editStreams = licenses.DefaultJoomlaStreams()
} }
ctx.Data["AvailableStreams"] = editStreams ctx.Data["AvailableStreams"] = editStreams
ctx.Data["ChannelItems"] = buildOrgChannelItems(editStreams) ctx.Data["ChannelItems"] = buildOrgChannelItems(editStreams)
@@ -318,13 +318,13 @@ func LicensesEditPackage(ctx *context.Context) {
// LicensesEditPackagePost saves edits to an org license package. // LicensesEditPackagePost saves edits to an org license package.
func LicensesEditPackagePost(ctx *context.Context) { func LicensesEditPackagePost(ctx *context.Context) {
pkgID := ctx.PathParamInt64("id") pkgID := ctx.PathParamInt64("id")
pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID) pkg, err := licenses.GetLicensePackageByID(ctx, pkgID)
if err != nil { if err != nil {
ctx.ServerError("GetLicensePackageByID", err) ctx.ServerError("GetLicensePackageByID", err)
return return
} }
if pkg.Name == updateserver_model.MasterPackageName { if pkg.Name == licenses.MasterPackageName {
ctx.Flash.Error("Master package cannot be edited") ctx.Flash.Error("Master package cannot be edited")
ctx.Redirect(ctx.Org.OrgLink + "/-/licenses") ctx.Redirect(ctx.Org.OrgLink + "/-/licenses")
return return
@@ -349,7 +349,7 @@ func LicensesEditPackagePost(ctx *context.Context) {
pkg.IsActive = ctx.FormString("is_active") == "on" pkg.IsActive = ctx.FormString("is_active") == "on"
if err := updateserver_model.UpdateLicensePackage(ctx, pkg); err != nil { if err := licenses.UpdateLicensePackage(ctx, pkg); err != nil {
ctx.ServerError("UpdateLicensePackage", err) ctx.ServerError("UpdateLicensePackage", err)
return return
} }
@@ -366,17 +366,17 @@ func canOrgDeleteLicenses(ctx *context.Context) bool {
// LicensesArchivePackage archives an org license package. // LicensesArchivePackage archives an org license package.
func LicensesArchivePackage(ctx *context.Context) { func LicensesArchivePackage(ctx *context.Context) {
pkgID := ctx.PathParamInt64("id") pkgID := ctx.PathParamInt64("id")
pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID) pkg, err := licenses.GetLicensePackageByID(ctx, pkgID)
if err != nil { if err != nil {
ctx.ServerError("GetLicensePackageByID", err) ctx.ServerError("GetLicensePackageByID", err)
return return
} }
if pkg.Name == updateserver_model.MasterPackageName { if pkg.Name == licenses.MasterPackageName {
ctx.Flash.Error("Master package cannot be archived") ctx.Flash.Error("Master package cannot be archived")
ctx.Redirect(ctx.Org.OrgLink + "/-/licenses") ctx.Redirect(ctx.Org.OrgLink + "/-/licenses")
return return
} }
if err := updateserver_model.ArchiveLicensePackage(ctx, pkgID); err != nil { if err := licenses.ArchiveLicensePackage(ctx, pkgID); err != nil {
ctx.ServerError("ArchiveLicensePackage", err) ctx.ServerError("ArchiveLicensePackage", err)
return return
} }
@@ -387,7 +387,7 @@ func LicensesArchivePackage(ctx *context.Context) {
// LicensesUnarchivePackage restores an archived org license package. // LicensesUnarchivePackage restores an archived org license package.
func LicensesUnarchivePackage(ctx *context.Context) { func LicensesUnarchivePackage(ctx *context.Context) {
pkgID := ctx.PathParamInt64("id") pkgID := ctx.PathParamInt64("id")
if err := updateserver_model.UnarchiveLicensePackage(ctx, pkgID); err != nil { if err := licenses.UnarchiveLicensePackage(ctx, pkgID); err != nil {
ctx.ServerError("UnarchiveLicensePackage", err) ctx.ServerError("UnarchiveLicensePackage", err)
return return
} }
@@ -402,17 +402,17 @@ func LicensesDeletePackage(ctx *context.Context) {
return return
} }
pkgID := ctx.PathParamInt64("id") pkgID := ctx.PathParamInt64("id")
pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID) pkg, err := licenses.GetLicensePackageByID(ctx, pkgID)
if err != nil { if err != nil {
ctx.ServerError("GetLicensePackageByID", err) ctx.ServerError("GetLicensePackageByID", err)
return return
} }
if pkg.Name == updateserver_model.MasterPackageName { if pkg.Name == licenses.MasterPackageName {
ctx.Flash.Error("Master package cannot be deleted") ctx.Flash.Error("Master package cannot be deleted")
ctx.Redirect(ctx.Org.OrgLink + "/-/licenses") ctx.Redirect(ctx.Org.OrgLink + "/-/licenses")
return return
} }
if err := updateserver_model.DeleteLicensePackage(ctx, pkgID); err != nil { if err := licenses.DeleteLicensePackage(ctx, pkgID); err != nil {
ctx.ServerError("DeleteLicensePackage", err) ctx.ServerError("DeleteLicensePackage", err)
return return
} }
@@ -424,7 +424,7 @@ func LicensesDeletePackage(ctx *context.Context) {
// LicensesEditKey shows the edit form for an org license key. // LicensesEditKey shows the edit form for an org license key.
func LicensesEditKey(ctx *context.Context) { func LicensesEditKey(ctx *context.Context) {
keyID := ctx.PathParamInt64("id") keyID := ctx.PathParamInt64("id")
key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID) key, err := licenses.GetLicenseKeyByID(ctx, keyID)
if err != nil { if err != nil {
ctx.ServerError("GetLicenseKeyByID", err) ctx.ServerError("GetLicenseKeyByID", err)
return return
@@ -452,7 +452,7 @@ func LicensesEditKey(ctx *context.Context) {
// LicensesEditKeyPost saves edits to an org license key. // LicensesEditKeyPost saves edits to an org license key.
func LicensesEditKeyPost(ctx *context.Context) { func LicensesEditKeyPost(ctx *context.Context) {
keyID := ctx.PathParamInt64("id") keyID := ctx.PathParamInt64("id")
key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID) key, err := licenses.GetLicenseKeyByID(ctx, keyID)
if err != nil { if err != nil {
ctx.ServerError("GetLicenseKeyByID", err) ctx.ServerError("GetLicenseKeyByID", err)
return return
@@ -481,7 +481,7 @@ func LicensesEditKeyPost(ctx *context.Context) {
key.ExpiresUnix = 0 key.ExpiresUnix = 0
} }
if err := updateserver_model.UpdateLicenseKey(ctx, key); err != nil { if err := licenses.UpdateLicenseKey(ctx, key); err != nil {
ctx.ServerError("UpdateLicenseKey", err) ctx.ServerError("UpdateLicenseKey", err)
return return
} }
@@ -493,14 +493,14 @@ func LicensesEditKeyPost(ctx *context.Context) {
// LicensesRevokeKey handles POST to revoke an org license key. // LicensesRevokeKey handles POST to revoke an org license key.
func LicensesRevokeKey(ctx *context.Context) { func LicensesRevokeKey(ctx *context.Context) {
keyID := ctx.PathParamInt64("id") keyID := ctx.PathParamInt64("id")
key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID) key, err := licenses.GetLicenseKeyByID(ctx, keyID)
if err != nil { if err != nil {
ctx.ServerError("GetLicenseKeyByID", err) ctx.ServerError("GetLicenseKeyByID", err)
return return
} }
key.IsActive = false key.IsActive = false
if err := updateserver_model.UpdateLicenseKey(ctx, key); err != nil { if err := licenses.UpdateLicenseKey(ctx, key); err != nil {
ctx.ServerError("UpdateLicenseKey", err) ctx.ServerError("UpdateLicenseKey", err)
return return
} }
@@ -512,13 +512,13 @@ func LicensesRevokeKey(ctx *context.Context) {
// LicensesRenewKey extends a license key's expiration by the package's duration. // LicensesRenewKey extends a license key's expiration by the package's duration.
func LicensesRenewKey(ctx *context.Context) { func LicensesRenewKey(ctx *context.Context) {
keyID := ctx.PathParamInt64("id") keyID := ctx.PathParamInt64("id")
key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID) key, err := licenses.GetLicenseKeyByID(ctx, keyID)
if err != nil { if err != nil {
ctx.ServerError("GetLicenseKeyByID", err) ctx.ServerError("GetLicenseKeyByID", err)
return return
} }
pkg, err := updateserver_model.GetLicensePackageByID(ctx, key.PackageID) pkg, err := licenses.GetLicensePackageByID(ctx, key.PackageID)
if err != nil { if err != nil {
ctx.ServerError("GetLicensePackageByID", err) ctx.ServerError("GetLicensePackageByID", err)
return return
@@ -529,7 +529,7 @@ func LicensesRenewKey(ctx *context.Context) {
days = 365 // default to 1 year for lifetime packages days = 365 // default to 1 year for lifetime packages
} }
if err := updateserver_model.RenewLicenseKey(ctx, keyID, days); err != nil { if err := licenses.RenewLicenseKey(ctx, keyID, days); err != nil {
ctx.ServerError("RenewLicenseKey", err) ctx.ServerError("RenewLicenseKey", err)
return return
} }
@@ -545,7 +545,7 @@ func LicensesDeleteKey(ctx *context.Context) {
return return
} }
keyID := ctx.PathParamInt64("id") keyID := ctx.PathParamInt64("id")
if err := updateserver_model.DeleteLicenseKey(ctx, keyID); err != nil { if err := licenses.DeleteLicenseKey(ctx, keyID); err != nil {
ctx.ServerError("DeleteLicenseKey", err) ctx.ServerError("DeleteLicenseKey", err)
return return
} }
+4 -4
View File
@@ -7,7 +7,7 @@ import (
"net/http" "net/http"
"strings" "strings"
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
) )
@@ -22,7 +22,7 @@ func SettingsUpdateStreams(ctx *context.Context) {
orgID := ctx.Org.Organization.ID orgID := ctx.Org.Organization.ID
cfg, err := updateserver_model.GetOrgConfig(ctx, orgID) cfg, err := licenses.GetOrgConfig(ctx, orgID)
if err != nil { if err != nil {
ctx.ServerError("GetOrgConfig", err) ctx.ServerError("GetOrgConfig", err)
return return
@@ -37,7 +37,7 @@ func SettingsUpdateStreams(ctx *context.Context) {
func SettingsUpdateStreamsPost(ctx *context.Context) { func SettingsUpdateStreamsPost(ctx *context.Context) {
orgID := ctx.Org.Organization.ID orgID := ctx.Org.Organization.ID
cfg := &updateserver_model.UpdateStreamConfig{ cfg := &licenses.UpdateStreamConfig{
OwnerID: orgID, OwnerID: orgID,
RepoID: 0, RepoID: 0,
StreamMode: ctx.FormString("stream_mode"), StreamMode: ctx.FormString("stream_mode"),
@@ -64,7 +64,7 @@ func SettingsUpdateStreamsPost(ctx *context.Context) {
cfg.StreamMode = "joomla" cfg.StreamMode = "joomla"
} }
if err := updateserver_model.SaveConfig(ctx, cfg); err != nil { if err := licenses.SaveConfig(ctx, cfg); err != nil {
ctx.ServerError("SaveConfig", err) ctx.ServerError("SaveConfig", err)
return return
} }
+10 -12
View File
@@ -157,8 +157,7 @@ func Wiki(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplOrgWiki) ctx.HTML(http.StatusOK, tplOrgWiki)
} }
// findOrgWikiCommit locates the profile repo's wiki and returns its HEAD commit. // findOrgWikiCommit locates the convention wiki repo and returns its HEAD commit.
// The org wiki lives in the .wiki.git sidecar of the profile repo (e.g. .profile.wiki.git).
func findOrgWikiCommit(ctx *context.Context, orgID int64, repoName string) (*repo_model.Repository, *git.Commit) { func findOrgWikiCommit(ctx *context.Context, orgID int64, repoName string) (*repo_model.Repository, *git.Commit) {
dbRepo, err := repo_model.GetRepositoryByName(ctx, orgID, repoName) dbRepo, err := repo_model.GetRepositoryByName(ctx, orgID, repoName)
if err != nil { if err != nil {
@@ -168,20 +167,19 @@ func findOrgWikiCommit(ctx *context.Context, orgID int64, repoName string) (*rep
return nil, nil return nil, nil
} }
// Open the wiki git repo (.wiki.git sidecar), not the main repo. if dbRepo.IsEmpty {
wikiGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, dbRepo.WikiStorageRepo())
if err != nil {
// Wiki repo doesn't exist yet — not an error, just no wiki.
return nil, nil return nil, nil
} }
branch := dbRepo.DefaultWikiBranch gitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, dbRepo)
if branch == "" {
branch = "main"
}
commit, err := wikiGitRepo.GetBranchCommit(branch)
if err != nil { if err != nil {
log.Error("findOrgWikiCommit: GetBranchCommit wiki(%s, %s): %v", dbRepo.FullName(), branch, err) log.Error("findOrgWikiCommit: OpenRepository(%s): %v", dbRepo.FullName(), err)
return nil, nil
}
commit, err := gitRepo.GetBranchCommit(dbRepo.DefaultBranch)
if err != nil {
log.Error("findOrgWikiCommit: GetBranchCommit(%s, %s): %v", dbRepo.FullName(), dbRepo.DefaultBranch, err)
return nil, nil return nil, nil
} }
+2 -2
View File
@@ -9,7 +9,7 @@ import (
"net/http" "net/http"
"strings" "strings"
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver" licenses_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/httplib" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/httplib"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
@@ -87,7 +87,7 @@ func CDNHandler(w http.ResponseWriter, req *http.Request) {
// If the release is assigned to an update stream, CDN is disabled - // If the release is assigned to an update stream, CDN is disabled -
// the update server handles distribution for streamed releases. // the update server handles distribution for streamed releases.
if stream := updateserver_model.GetReleaseStream(req.Context(), release.ID); stream != "" { if stream := licenses_model.GetReleaseStream(req.Context(), release.ID); stream != "" {
http.Error(w, "Forbidden: release is served via update stream", http.StatusForbidden) http.Error(w, "Forbidden: release is served via update stream", http.StatusForbidden)
return return
} }
+2 -2
View File
@@ -10,7 +10,7 @@ import (
"strings" "strings"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
) )
@@ -58,7 +58,7 @@ func ServeChangelogXML(ctx *context.Context) {
} }
// Get extension metadata for element name and type. // Get extension metadata for element name and type.
cfg := updateserver_model.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID) cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
element := strings.ToLower(repo.Name) element := strings.ToLower(repo.Name)
extType := "component" extType := "component"
if cfg != nil { if cfg != nil {
+7 -7
View File
@@ -4,7 +4,7 @@
package repo package repo
import ( import (
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
) )
@@ -20,7 +20,7 @@ func CheckDownloadGating(ctx *context.Context, tagName string) bool {
repo := ctx.Repo.Repository repo := ctx.Repo.Repository
// Check effective config (repo override → org default). // Check effective config (repo override → org default).
cfg := updateserver_model.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID) cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
if cfg == nil || !cfg.LicensingEnabled { if cfg == nil || !cfg.LicensingEnabled {
return true // licensing not enabled — allow all downloads return true // licensing not enabled — allow all downloads
} }
@@ -38,8 +38,8 @@ func CheckDownloadGating(ctx *context.Context, tagName string) bool {
// For prerelease-only gating, check if this is a prerelease tag. // For prerelease-only gating, check if this is a prerelease tag.
if gating == "prerelease" && tagName != "" { if gating == "prerelease" && tagName != "" {
streams := updateserver_model.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID) streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
matched := updateserver_model.MatchStreamFromTag(tagName, false, streams) matched := licenses.MatchStreamFromTag(tagName, false, streams)
if matched == "stable" { if matched == "stable" {
return true // stable releases are public return true // stable releases are public
} }
@@ -60,14 +60,14 @@ func CheckDownloadGating(ctx *context.Context, tagName string) bool {
} }
domain := ctx.FormString("domain") domain := ctx.FormString("domain")
key, _, err := updateserver_model.ValidateLicenseKeyForRepo(ctx, rawKey, domain, repo.ID) key, _, err := licenses.ValidateLicenseKeyForRepo(ctx, rawKey, domain, repo.ID)
if err != nil { if err != nil {
log.Debug("Download gating: key validation failed: %v", err) log.Debug("Download gating: key validation failed: %v", err)
return false return false
} }
// Record heartbeat on successful download validation. // Record heartbeat on successful download validation.
_ = updateserver_model.TouchHeartbeat(ctx, key.ID) _ = licenses.TouchHeartbeat(ctx, key.ID)
return true return true
} }
@@ -76,7 +76,7 @@ func GetSupportURL(ctx *context.Context) string {
if ctx.Repo == nil || ctx.Repo.Repository == nil { if ctx.Repo == nil || ctx.Repo.Repository == nil {
return "" return ""
} }
cfg := updateserver_model.GetEffectiveConfig(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID) cfg := licenses.GetEffectiveConfig(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID)
if cfg != nil && cfg.SupportURL != "" { if cfg != nil && cfg.SupportURL != "" {
return cfg.SupportURL return cfg.SupportURL
} }
-22
View File
@@ -142,14 +142,6 @@ func NewIssue(ctx *context.Context) {
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.Permission.CanWrite(unit.TypeIssues) ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.Permission.CanWrite(unit.TypeIssues)
// Load org-level status/priority/type definitions for the new issue sidebar.
issueStatusDefs, _ := issues_model.GetIssueStatusDefsByOrg(ctx, ctx.Repo.Repository.OwnerID)
ctx.Data["IssueStatusDefs"] = issueStatusDefs
issuePriorityDefs, _ := issues_model.GetIssuePriorityDefsByOrg(ctx, ctx.Repo.Repository.OwnerID)
ctx.Data["IssuePriorityDefs"] = issuePriorityDefs
issueTypeDefs, _ := issues_model.GetIssueTypeDefsByOrg(ctx, ctx.Repo.Repository.OwnerID)
ctx.Data["IssueTypeDefs"] = issueTypeDefs
// Load org-level issue-scoped custom fields for the new issue sidebar. // Load org-level issue-scoped custom fields for the new issue sidebar.
customFieldDefs, cfErr := issues_model.GetCustomFieldsByOwner(ctx, ctx.Repo.Repository.OwnerID, issues_model.CustomFieldScopeIssue) customFieldDefs, cfErr := issues_model.GetCustomFieldsByOwner(ctx, ctx.Repo.Repository.OwnerID, issues_model.CustomFieldScopeIssue)
if cfErr != nil { if cfErr != nil {
@@ -416,17 +408,6 @@ func NewIssuePost(ctx *context.Context) {
return return
} }
// Save status/priority/type from sidebar dropdowns.
if statusID := ctx.FormInt64("status_id"); statusID > 0 {
_ = issues_model.SetIssueStatusID(ctx, issue.ID, statusID)
}
if priorityID := ctx.FormInt64("priority_id"); priorityID > 0 {
_ = issues_model.SetIssuePriorityID(ctx, issue.ID, priorityID)
}
if typeID := ctx.FormInt64("type_id"); typeID > 0 {
_ = issues_model.SetIssueTypeID(ctx, issue.ID, typeID)
}
// Save custom field values submitted from the new issue form. // Save custom field values submitted from the new issue form.
saveCustomFieldsFromForm(ctx, repo.OwnerID, issue.ID) saveCustomFieldsFromForm(ctx, repo.OwnerID, issue.ID)
@@ -448,7 +429,6 @@ func NewIssuePost(ctx *context.Context) {
// saveCustomFieldsFromForm reads custom field values from the form // saveCustomFieldsFromForm reads custom field values from the form
// (submitted as "custom-field-{fieldID}") and persists them for the issue. // (submitted as "custom-field-{fieldID}") and persists them for the issue.
// Returns true if all required fields are satisfied.
func saveCustomFieldsFromForm(ctx *context.Context, ownerID, issueID int64) { func saveCustomFieldsFromForm(ctx *context.Context, ownerID, issueID int64) {
defs, err := issues_model.GetCustomFieldsByOwner(ctx, ownerID, issues_model.CustomFieldScopeIssue) defs, err := issues_model.GetCustomFieldsByOwner(ctx, ownerID, issues_model.CustomFieldScopeIssue)
if err != nil { if err != nil {
@@ -463,8 +443,6 @@ func saveCustomFieldsFromForm(ctx *context.Context, ownerID, issueID int64) {
v := ctx.Req.FormValue(fmt.Sprintf("custom-field-%d", def.ID)) v := ctx.Req.FormValue(fmt.Sprintf("custom-field-%d", def.ID))
if v != "" { if v != "" {
vals[def.ID] = v vals[def.ID] = v
} else if def.Required {
ctx.Flash.Error(fmt.Sprintf("Custom field %q is required", def.Name))
} }
} }
if len(vals) > 0 { if len(vals) > 0 {
+58 -58
View File
@@ -9,7 +9,7 @@ import (
"strings" "strings"
"time" "time"
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
unit_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit" unit_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/json" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/json"
@@ -48,7 +48,7 @@ type ChannelItem struct {
} }
// buildChannelItems converts stream definitions into combolist items. // buildChannelItems converts stream definitions into combolist items.
func buildChannelItems(streams []updateserver_model.StreamDef) []ChannelItem { func buildChannelItems(streams []licenses.StreamDef) []ChannelItem {
items := make([]ChannelItem, 0, len(streams)) items := make([]ChannelItem, 0, len(streams))
for _, s := range streams { for _, s := range streams {
label := s.Name label := s.Name
@@ -62,7 +62,7 @@ func buildChannelItems(streams []updateserver_model.StreamDef) []ChannelItem {
// LicensePackageDisplay is used in templates. // LicensePackageDisplay is used in templates.
type LicensePackageDisplay struct { type LicensePackageDisplay struct {
*updateserver_model.LicensePackage *licenses.LicensePackage
KeyCount int64 KeyCount int64
Created time.Time Created time.Time
} }
@@ -80,7 +80,7 @@ func Licenses(ctx *context.Context) {
// Auto-create master package + key if admin and none exist. // Auto-create master package + key if admin and none exist.
if canWriteLicenses { if canWriteLicenses {
newMasterKey, err := updateserver_model.EnsureMasterKey(ctx, ownerID) newMasterKey, err := licenses.EnsureMasterKey(ctx, ownerID)
if err != nil { if err != nil {
ctx.ServerError("EnsureMasterKey", err) ctx.ServerError("EnsureMasterKey", err)
return return
@@ -91,10 +91,10 @@ func Licenses(ctx *context.Context) {
} }
// Always load the master key for display (prefix + status). // Always load the master key for display (prefix + status).
masterKey, _ := updateserver_model.GetMasterKey(ctx, ownerID) masterKey, _ := licenses.GetMasterKey(ctx, ownerID)
ctx.Data["MasterKey"] = masterKey ctx.Data["MasterKey"] = masterKey
pkgs, err := updateserver_model.ListLicensePackages(ctx, ownerID) pkgs, err := licenses.ListLicensePackages(ctx, ownerID)
if err != nil { if err != nil {
ctx.ServerError("ListLicensePackages", err) ctx.ServerError("ListLicensePackages", err)
return return
@@ -102,7 +102,7 @@ func Licenses(ctx *context.Context) {
var display []LicensePackageDisplay var display []LicensePackageDisplay
for _, pkg := range pkgs { for _, pkg := range pkgs {
count, _ := updateserver_model.CountKeysByPackage(ctx, pkg.ID) count, _ := licenses.CountKeysByPackage(ctx, pkg.ID)
display = append(display, LicensePackageDisplay{ display = append(display, LicensePackageDisplay{
LicensePackage: pkg, LicensePackage: pkg,
KeyCount: count, KeyCount: count,
@@ -115,11 +115,11 @@ func Licenses(ctx *context.Context) {
searchQuery := strings.TrimSpace(ctx.FormString("q")) searchQuery := strings.TrimSpace(ctx.FormString("q"))
ctx.Data["SearchQuery"] = searchQuery ctx.Data["SearchQuery"] = searchQuery
var keys []*updateserver_model.LicenseKey var keys []*licenses.LicenseKey
if searchQuery != "" { if searchQuery != "" {
keys, err = updateserver_model.SearchLicenseKeys(ctx, ownerID, searchQuery) keys, err = licenses.SearchLicenseKeys(ctx, ownerID, searchQuery)
} else { } else {
keys, err = updateserver_model.ListLicenseKeys(ctx, ownerID) keys, err = licenses.ListLicenseKeys(ctx, ownerID)
} }
if err != nil { if err != nil {
ctx.ServerError("ListLicenseKeys", err) ctx.ServerError("ListLicenseKeys", err)
@@ -129,10 +129,10 @@ func Licenses(ctx *context.Context) {
ctx.Data["CanDelete"] = canDeleteLicenses(ctx) ctx.Data["CanDelete"] = canDeleteLicenses(ctx)
// Load archived packages. // Load archived packages.
archivedPkgs, _ := updateserver_model.ListArchivedLicensePackages(ctx, ownerID) archivedPkgs, _ := licenses.ListArchivedLicensePackages(ctx, ownerID)
var archivedDisplay []LicensePackageDisplay var archivedDisplay []LicensePackageDisplay
for _, pkg := range archivedPkgs { for _, pkg := range archivedPkgs {
count, _ := updateserver_model.CountKeysByPackage(ctx, pkg.ID) count, _ := licenses.CountKeysByPackage(ctx, pkg.ID)
archivedDisplay = append(archivedDisplay, LicensePackageDisplay{ archivedDisplay = append(archivedDisplay, LicensePackageDisplay{
LicensePackage: pkg, LicensePackage: pkg,
KeyCount: count, KeyCount: count,
@@ -142,12 +142,12 @@ func Licenses(ctx *context.Context) {
ctx.Data["ArchivedPackages"] = archivedDisplay ctx.Data["ArchivedPackages"] = archivedDisplay
// Load available streams for the channels combolist. // Load available streams for the channels combolist.
orgCfg, _ := updateserver_model.GetOrgConfig(ctx, ownerID) orgCfg, _ := licenses.GetOrgConfig(ctx, ownerID)
var streams []updateserver_model.StreamDef var streams []licenses.StreamDef
if orgCfg != nil { if orgCfg != nil {
streams = orgCfg.GetActiveStreams() streams = orgCfg.GetActiveStreams()
} else { } else {
streams = updateserver_model.DefaultJoomlaStreams() streams = licenses.DefaultJoomlaStreams()
} }
ctx.Data["AvailableStreams"] = streams ctx.Data["AvailableStreams"] = streams
ctx.Data["ChannelItems"] = buildChannelItems(streams) ctx.Data["ChannelItems"] = buildChannelItems(streams)
@@ -186,7 +186,7 @@ func LicensesCreatePackage(ctx *context.Context) {
repoScope = "all" repoScope = "all"
} }
pkg := &updateserver_model.LicensePackage{ pkg := &licenses.LicensePackage{
OwnerID: ctx.Repo.Repository.OwnerID, OwnerID: ctx.Repo.Repository.OwnerID,
Name: name, Name: name,
Description: ctx.FormString("description"), Description: ctx.FormString("description"),
@@ -199,7 +199,7 @@ func LicensesCreatePackage(ctx *context.Context) {
IsActive: true, IsActive: true,
} }
if err := updateserver_model.CreateLicensePackage(ctx, pkg); err != nil { if err := licenses.CreateLicensePackage(ctx, pkg); err != nil {
ctx.ServerError("CreateLicensePackage", err) ctx.ServerError("CreateLicensePackage", err)
return return
} }
@@ -213,21 +213,21 @@ func LicensesRegenerateMasterKey(ctx *context.Context) {
ownerID := ctx.Repo.Repository.OwnerID ownerID := ctx.Repo.Repository.OwnerID
// Deactivate the old master key. // Deactivate the old master key.
oldKey, _ := updateserver_model.GetMasterKey(ctx, ownerID) oldKey, _ := licenses.GetMasterKey(ctx, ownerID)
if oldKey != nil { if oldKey != nil {
oldKey.IsActive = false oldKey.IsActive = false
_ = updateserver_model.UpdateLicenseKey(ctx, oldKey) _ = licenses.UpdateLicenseKey(ctx, oldKey)
} }
// Find the master package. // Find the master package.
pkgs, err := updateserver_model.ListLicensePackages(ctx, ownerID) pkgs, err := licenses.ListLicensePackages(ctx, ownerID)
if err != nil { if err != nil {
ctx.ServerError("ListLicensePackages", err) ctx.ServerError("ListLicensePackages", err)
return return
} }
var masterPkg *updateserver_model.LicensePackage var masterPkg *licenses.LicensePackage
for _, pkg := range pkgs { for _, pkg := range pkgs {
if pkg.Name == updateserver_model.MasterPackageName { if pkg.Name == licenses.MasterPackageName {
masterPkg = pkg masterPkg = pkg
break break
} }
@@ -239,13 +239,13 @@ func LicensesRegenerateMasterKey(ctx *context.Context) {
} }
// Create a new master key. // Create a new master key.
newKey := &updateserver_model.LicenseKey{ newKey := &licenses.LicenseKey{
PackageID: masterPkg.ID, PackageID: masterPkg.ID,
OwnerID: ownerID, OwnerID: ownerID,
IsInternal: true, IsInternal: true,
IsActive: true, IsActive: true,
} }
rawKey, err := updateserver_model.CreateLicenseKey(ctx, newKey) rawKey, err := licenses.CreateLicenseKey(ctx, newKey)
if err != nil { if err != nil {
ctx.ServerError("CreateLicenseKey", err) ctx.ServerError("CreateLicenseKey", err)
return return
@@ -270,7 +270,7 @@ func LicensesGenerateKey(ctx *context.Context) {
return return
} }
pkg, err := updateserver_model.GetLicensePackageByID(ctx, packageID) pkg, err := licenses.GetLicensePackageByID(ctx, packageID)
if err != nil { if err != nil {
ctx.ServerError("GetLicensePackageByID", err) ctx.ServerError("GetLicensePackageByID", err)
return return
@@ -282,7 +282,7 @@ func LicensesGenerateKey(ctx *context.Context) {
domainRestriction = pkg.DomainRestriction domainRestriction = pkg.DomainRestriction
} }
key := &updateserver_model.LicenseKey{ key := &licenses.LicenseKey{
PackageID: packageID, PackageID: packageID,
OwnerID: ctx.Repo.Repository.OwnerID, OwnerID: ctx.Repo.Repository.OwnerID,
IsActive: true, IsActive: true,
@@ -301,13 +301,13 @@ func LicensesGenerateKey(ctx *context.Context) {
var rawKey string var rawKey string
customKey := strings.TrimSpace(ctx.FormString("custom_key")) customKey := strings.TrimSpace(ctx.FormString("custom_key"))
if customKey != "" && (ctx.IsUserSiteAdmin() || ctx.Repo.Permission.IsOwner()) { if customKey != "" && (ctx.IsUserSiteAdmin() || ctx.Repo.Permission.IsOwner()) {
if err := updateserver_model.CreateLicenseKeyCustom(ctx, key, customKey); err != nil { if err := licenses.CreateLicenseKeyCustom(ctx, key, customKey); err != nil {
ctx.ServerError("CreateLicenseKeyCustom", err) ctx.ServerError("CreateLicenseKeyCustom", err)
return return
} }
rawKey = customKey rawKey = customKey
} else { } else {
rawKey, err = updateserver_model.CreateLicenseKey(ctx, key) rawKey, err = licenses.CreateLicenseKey(ctx, key)
if err != nil { if err != nil {
ctx.ServerError("CreateLicenseKey", err) ctx.ServerError("CreateLicenseKey", err)
return return
@@ -323,10 +323,10 @@ func LicensesGenerateKey(ctx *context.Context) {
// Re-render the page with the new key displayed. // Re-render the page with the new key displayed.
ownerID := ctx.Repo.Repository.OwnerID ownerID := ctx.Repo.Repository.OwnerID
pkgs, _ := updateserver_model.ListLicensePackages(ctx, ownerID) pkgs, _ := licenses.ListLicensePackages(ctx, ownerID)
var display []LicensePackageDisplay var display []LicensePackageDisplay
for _, p := range pkgs { for _, p := range pkgs {
count, _ := updateserver_model.CountKeysByPackage(ctx, p.ID) count, _ := licenses.CountKeysByPackage(ctx, p.ID)
display = append(display, LicensePackageDisplay{ display = append(display, LicensePackageDisplay{
LicensePackage: p, LicensePackage: p,
KeyCount: count, KeyCount: count,
@@ -334,15 +334,15 @@ func LicensesGenerateKey(ctx *context.Context) {
}) })
} }
ctx.Data["LicensePackages"] = display ctx.Data["LicensePackages"] = display
keys, _ := updateserver_model.ListLicenseKeys(ctx, ownerID) keys, _ := licenses.ListLicenseKeys(ctx, ownerID)
ctx.Data["LicenseKeys"] = keys ctx.Data["LicenseKeys"] = keys
orgCfg, _ := updateserver_model.GetOrgConfig(ctx, ownerID) orgCfg, _ := licenses.GetOrgConfig(ctx, ownerID)
var genStreams []updateserver_model.StreamDef var genStreams []licenses.StreamDef
if orgCfg != nil { if orgCfg != nil {
genStreams = orgCfg.GetActiveStreams() genStreams = orgCfg.GetActiveStreams()
} else { } else {
genStreams = updateserver_model.DefaultJoomlaStreams() genStreams = licenses.DefaultJoomlaStreams()
} }
ctx.Data["AvailableStreams"] = genStreams ctx.Data["AvailableStreams"] = genStreams
ctx.Data["ChannelItems"] = buildChannelItems(genStreams) ctx.Data["ChannelItems"] = buildChannelItems(genStreams)
@@ -353,14 +353,14 @@ func LicensesGenerateKey(ctx *context.Context) {
// LicensesRevokeKey handles POST to revoke a license key. // LicensesRevokeKey handles POST to revoke a license key.
func LicensesRevokeKey(ctx *context.Context) { func LicensesRevokeKey(ctx *context.Context) {
keyID := ctx.PathParamInt64("id") keyID := ctx.PathParamInt64("id")
key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID) key, err := licenses.GetLicenseKeyByID(ctx, keyID)
if err != nil { if err != nil {
ctx.ServerError("GetLicenseKeyByID", err) ctx.ServerError("GetLicenseKeyByID", err)
return return
} }
key.IsActive = false key.IsActive = false
if err := updateserver_model.UpdateLicenseKey(ctx, key); err != nil { if err := licenses.UpdateLicenseKey(ctx, key); err != nil {
ctx.ServerError("UpdateLicenseKey", err) ctx.ServerError("UpdateLicenseKey", err)
return return
} }
@@ -375,7 +375,7 @@ const tplLicensesEditKey templates.TplName = "repo/licenses_edit_key"
// LicensesEditKey shows the edit form for a license key. // LicensesEditKey shows the edit form for a license key.
func LicensesEditKey(ctx *context.Context) { func LicensesEditKey(ctx *context.Context) {
keyID := ctx.PathParamInt64("id") keyID := ctx.PathParamInt64("id")
key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID) key, err := licenses.GetLicenseKeyByID(ctx, keyID)
if err != nil { if err != nil {
ctx.ServerError("GetLicenseKeyByID", err) ctx.ServerError("GetLicenseKeyByID", err)
return return
@@ -404,7 +404,7 @@ func LicensesEditKey(ctx *context.Context) {
// LicensesEditKeyPost saves edits to a license key. // LicensesEditKeyPost saves edits to a license key.
func LicensesEditKeyPost(ctx *context.Context) { func LicensesEditKeyPost(ctx *context.Context) {
keyID := ctx.PathParamInt64("id") keyID := ctx.PathParamInt64("id")
key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID) key, err := licenses.GetLicenseKeyByID(ctx, keyID)
if err != nil { if err != nil {
ctx.ServerError("GetLicenseKeyByID", err) ctx.ServerError("GetLicenseKeyByID", err)
return return
@@ -433,7 +433,7 @@ func LicensesEditKeyPost(ctx *context.Context) {
key.ExpiresUnix = 0 key.ExpiresUnix = 0
} }
if err := updateserver_model.UpdateLicenseKey(ctx, key); err != nil { if err := licenses.UpdateLicenseKey(ctx, key); err != nil {
ctx.ServerError("UpdateLicenseKey", err) ctx.ServerError("UpdateLicenseKey", err)
return return
} }
@@ -445,13 +445,13 @@ func LicensesEditKeyPost(ctx *context.Context) {
// LicensesEditPackage shows the edit form for a license package. // LicensesEditPackage shows the edit form for a license package.
func LicensesEditPackage(ctx *context.Context) { func LicensesEditPackage(ctx *context.Context) {
pkgID := ctx.PathParamInt64("id") pkgID := ctx.PathParamInt64("id")
pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID) pkg, err := licenses.GetLicensePackageByID(ctx, pkgID)
if err != nil { if err != nil {
ctx.ServerError("GetLicensePackageByID", err) ctx.ServerError("GetLicensePackageByID", err)
return return
} }
if pkg.Name == updateserver_model.MasterPackageName { if pkg.Name == licenses.MasterPackageName {
ctx.Flash.Error("Master package cannot be edited") ctx.Flash.Error("Master package cannot be edited")
ctx.Redirect(ctx.Repo.RepoLink + "/licenses") ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
return return
@@ -465,12 +465,12 @@ func LicensesEditPackage(ctx *context.Context) {
ctx.Data["SelectedChannels"] = selectedChannels ctx.Data["SelectedChannels"] = selectedChannels
ownerID := ctx.Repo.Repository.OwnerID ownerID := ctx.Repo.Repository.OwnerID
orgCfg, _ := updateserver_model.GetOrgConfig(ctx, ownerID) orgCfg, _ := licenses.GetOrgConfig(ctx, ownerID)
var editStreams []updateserver_model.StreamDef var editStreams []licenses.StreamDef
if orgCfg != nil { if orgCfg != nil {
editStreams = orgCfg.GetActiveStreams() editStreams = orgCfg.GetActiveStreams()
} else { } else {
editStreams = updateserver_model.DefaultJoomlaStreams() editStreams = licenses.DefaultJoomlaStreams()
} }
ctx.Data["AvailableStreams"] = editStreams ctx.Data["AvailableStreams"] = editStreams
ctx.Data["ChannelItems"] = buildChannelItems(editStreams) ctx.Data["ChannelItems"] = buildChannelItems(editStreams)
@@ -482,13 +482,13 @@ func LicensesEditPackage(ctx *context.Context) {
// LicensesEditPackagePost saves edits to a license package. // LicensesEditPackagePost saves edits to a license package.
func LicensesEditPackagePost(ctx *context.Context) { func LicensesEditPackagePost(ctx *context.Context) {
pkgID := ctx.PathParamInt64("id") pkgID := ctx.PathParamInt64("id")
pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID) pkg, err := licenses.GetLicensePackageByID(ctx, pkgID)
if err != nil { if err != nil {
ctx.ServerError("GetLicensePackageByID", err) ctx.ServerError("GetLicensePackageByID", err)
return return
} }
if pkg.Name == updateserver_model.MasterPackageName { if pkg.Name == licenses.MasterPackageName {
ctx.Flash.Error("Master package cannot be edited") ctx.Flash.Error("Master package cannot be edited")
ctx.Redirect(ctx.Repo.RepoLink + "/licenses") ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
return return
@@ -518,7 +518,7 @@ func LicensesEditPackagePost(ctx *context.Context) {
pkg.DomainRestriction = strings.TrimSpace(ctx.FormString("domain_restriction")) pkg.DomainRestriction = strings.TrimSpace(ctx.FormString("domain_restriction"))
pkg.IsActive = ctx.FormString("is_active") == "on" pkg.IsActive = ctx.FormString("is_active") == "on"
if err := updateserver_model.UpdateLicensePackage(ctx, pkg); err != nil { if err := licenses.UpdateLicensePackage(ctx, pkg); err != nil {
ctx.ServerError("UpdateLicensePackage", err) ctx.ServerError("UpdateLicensePackage", err)
return return
} }
@@ -535,17 +535,17 @@ func canDeleteLicenses(ctx *context.Context) bool {
// LicensesArchivePackage archives a license package. // LicensesArchivePackage archives a license package.
func LicensesArchivePackage(ctx *context.Context) { func LicensesArchivePackage(ctx *context.Context) {
pkgID := ctx.PathParamInt64("id") pkgID := ctx.PathParamInt64("id")
pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID) pkg, err := licenses.GetLicensePackageByID(ctx, pkgID)
if err != nil { if err != nil {
ctx.ServerError("GetLicensePackageByID", err) ctx.ServerError("GetLicensePackageByID", err)
return return
} }
if pkg.Name == updateserver_model.MasterPackageName { if pkg.Name == licenses.MasterPackageName {
ctx.Flash.Error("Master package cannot be archived") ctx.Flash.Error("Master package cannot be archived")
ctx.Redirect(ctx.Repo.RepoLink + "/licenses") ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
return return
} }
if err := updateserver_model.ArchiveLicensePackage(ctx, pkgID); err != nil { if err := licenses.ArchiveLicensePackage(ctx, pkgID); err != nil {
ctx.ServerError("ArchiveLicensePackage", err) ctx.ServerError("ArchiveLicensePackage", err)
return return
} }
@@ -556,7 +556,7 @@ func LicensesArchivePackage(ctx *context.Context) {
// LicensesUnarchivePackage removes archive status from a package. // LicensesUnarchivePackage removes archive status from a package.
func LicensesUnarchivePackage(ctx *context.Context) { func LicensesUnarchivePackage(ctx *context.Context) {
pkgID := ctx.PathParamInt64("id") pkgID := ctx.PathParamInt64("id")
if err := updateserver_model.UnarchiveLicensePackage(ctx, pkgID); err != nil { if err := licenses.UnarchiveLicensePackage(ctx, pkgID); err != nil {
ctx.ServerError("UnarchiveLicensePackage", err) ctx.ServerError("UnarchiveLicensePackage", err)
return return
} }
@@ -571,17 +571,17 @@ func LicensesDeletePackage(ctx *context.Context) {
return return
} }
pkgID := ctx.PathParamInt64("id") pkgID := ctx.PathParamInt64("id")
pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID) pkg, err := licenses.GetLicensePackageByID(ctx, pkgID)
if err != nil { if err != nil {
ctx.ServerError("GetLicensePackageByID", err) ctx.ServerError("GetLicensePackageByID", err)
return return
} }
if pkg.Name == updateserver_model.MasterPackageName { if pkg.Name == licenses.MasterPackageName {
ctx.Flash.Error("Master package cannot be deleted") ctx.Flash.Error("Master package cannot be deleted")
ctx.Redirect(ctx.Repo.RepoLink + "/licenses") ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
return return
} }
if err := updateserver_model.DeleteLicensePackage(ctx, pkgID); err != nil { if err := licenses.DeleteLicensePackage(ctx, pkgID); err != nil {
ctx.ServerError("DeleteLicensePackage", err) ctx.ServerError("DeleteLicensePackage", err)
return return
} }
@@ -593,13 +593,13 @@ func LicensesDeletePackage(ctx *context.Context) {
// LicensesRenewKey extends a license key's expiration by the package's duration. // LicensesRenewKey extends a license key's expiration by the package's duration.
func LicensesRenewKey(ctx *context.Context) { func LicensesRenewKey(ctx *context.Context) {
keyID := ctx.PathParamInt64("id") keyID := ctx.PathParamInt64("id")
key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID) key, err := licenses.GetLicenseKeyByID(ctx, keyID)
if err != nil { if err != nil {
ctx.ServerError("GetLicenseKeyByID", err) ctx.ServerError("GetLicenseKeyByID", err)
return return
} }
pkg, err := updateserver_model.GetLicensePackageByID(ctx, key.PackageID) pkg, err := licenses.GetLicensePackageByID(ctx, key.PackageID)
if err != nil { if err != nil {
ctx.ServerError("GetLicensePackageByID", err) ctx.ServerError("GetLicensePackageByID", err)
return return
@@ -610,7 +610,7 @@ func LicensesRenewKey(ctx *context.Context) {
days = 365 // default to 1 year for lifetime packages days = 365 // default to 1 year for lifetime packages
} }
if err := updateserver_model.RenewLicenseKey(ctx, keyID, days); err != nil { if err := licenses.RenewLicenseKey(ctx, keyID, days); err != nil {
ctx.ServerError("RenewLicenseKey", err) ctx.ServerError("RenewLicenseKey", err)
return return
} }
@@ -626,7 +626,7 @@ func LicensesDeleteKey(ctx *context.Context) {
return return
} }
keyID := ctx.PathParamInt64("id") keyID := ctx.PathParamInt64("id")
if err := updateserver_model.DeleteLicenseKey(ctx, keyID); err != nil { if err := licenses.DeleteLicenseKey(ctx, keyID); err != nil {
ctx.ServerError("DeleteLicenseKey", err) ctx.ServerError("DeleteLicenseKey", err)
return return
} }
+7 -7
View File
@@ -14,7 +14,7 @@ import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git" git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver" licenses_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/renderhelper" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/renderhelper"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit"
@@ -359,11 +359,11 @@ func newReleaseCommon(ctx *context.Context) {
// Load available streams for the stream selector (when licensing enabled). // Load available streams for the stream selector (when licensing enabled).
if ctx.Data["LicensingEnabled"] == true { if ctx.Data["LicensingEnabled"] == true {
ownerID := ctx.Repo.Repository.OwnerID ownerID := ctx.Repo.Repository.OwnerID
orgCfg, _ := updateserver_model.GetOrgConfig(ctx, ownerID) orgCfg, _ := licenses_model.GetOrgConfig(ctx, ownerID)
if orgCfg != nil { if orgCfg != nil {
ctx.Data["AvailableStreams"] = orgCfg.GetActiveStreams() ctx.Data["AvailableStreams"] = orgCfg.GetActiveStreams()
} else { } else {
ctx.Data["AvailableStreams"] = updateserver_model.DefaultJoomlaStreams() ctx.Data["AvailableStreams"] = licenses_model.DefaultJoomlaStreams()
} }
} }
@@ -534,7 +534,7 @@ func NewReleasePost(ctx *context.Context) {
} }
// Save manual stream assignment if specified. // Save manual stream assignment if specified.
if streamName := form.UpdateStream; streamName != "" { if streamName := form.UpdateStream; streamName != "" {
_ = updateserver_model.SetReleaseStream(ctx, rel.ID, rel.RepoID, streamName) _ = licenses_model.SetReleaseStream(ctx, rel.ID, rel.RepoID, streamName)
} }
ctx.Redirect(ctx.Repo.RepoLink + "/releases") ctx.Redirect(ctx.Repo.RepoLink + "/releases")
return return
@@ -596,7 +596,7 @@ func EditRelease(ctx *context.Context) {
ctx.Data["content"] = rel.Note ctx.Data["content"] = rel.Note
ctx.Data["prerelease"] = rel.IsPrerelease ctx.Data["prerelease"] = rel.IsPrerelease
ctx.Data["IsDraft"] = rel.IsDraft ctx.Data["IsDraft"] = rel.IsDraft
releaseStream := updateserver_model.GetReleaseStream(ctx, rel.ID) releaseStream := licenses_model.GetReleaseStream(ctx, rel.ID)
ctx.Data["ReleaseStream"] = releaseStream ctx.Data["ReleaseStream"] = releaseStream
ctx.Data["ReleaseHasStream"] = releaseStream != "" ctx.Data["ReleaseHasStream"] = releaseStream != ""
ctx.Data["CDNEnabled"] = setting.CDN.Enabled ctx.Data["CDNEnabled"] = setting.CDN.Enabled
@@ -682,9 +682,9 @@ func EditReleasePost(ctx *context.Context) {
} }
// Save manual stream assignment. // Save manual stream assignment.
if streamName := form.UpdateStream; streamName != "" { if streamName := form.UpdateStream; streamName != "" {
_ = updateserver_model.SetReleaseStream(ctx, rel.ID, rel.RepoID, streamName) _ = licenses_model.SetReleaseStream(ctx, rel.ID, rel.RepoID, streamName)
} else { } else {
_ = updateserver_model.DeleteReleaseStream(ctx, rel.ID) _ = licenses_model.DeleteReleaseStream(ctx, rel.ID)
} }
// Update per-asset CDN visibility flags. // Update per-asset CDN visibility flags.
+57 -4
View File
@@ -4,15 +4,68 @@
package setting package setting
import ( import (
"net/http"
licenses_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
) )
// LicensingSettings redirects to the manifest page where licensing is now consolidated. const tplSettingsLicensing templates.TplName = "repo/settings/licensing"
// LicensingSettings displays the licensing settings page.
func LicensingSettings(ctx *context.Context) { func LicensingSettings(ctx *context.Context) {
ctx.Redirect(ctx.Repo.RepoLink + "/settings/updateserver") 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 redirects POST to the manifest page. // LicensingSettingsPost saves the licensing settings.
func LicensingSettingsPost(ctx *context.Context) { func LicensingSettingsPost(ctx *context.Context) {
ctx.Redirect(ctx.Repo.RepoLink + "/settings/updateserver") repo := ctx.Repo.Repository
updatePlatform := ctx.FormString("update_platform")
if updatePlatform == "" {
updatePlatform = "joomla"
}
enabled := ctx.FormString("enable_licensing") == "on"
if !enabled {
// Remove repo-level override so org config takes effect
if err := licenses_model.DeleteRepoConfig(ctx, repo.ID); err != nil {
log.Error("DeleteRepoConfig: %v", err)
}
} else {
updateCfg := &licenses_model.UpdateStreamConfig{
OwnerID: repo.OwnerID,
RepoID: repo.ID,
Platform: updatePlatform,
LicensingEnabled: true,
RequireKey: ctx.FormString("require_update_key") == "on",
DownloadGating: ctx.FormString("download_gating"),
SupportURL: ctx.FormString("support_url"),
ExtensionName: ctx.FormString("extension_name"),
DisplayName: ctx.FormString("display_name"),
ExtensionType: ctx.FormString("extension_type"),
TargetVersion: ctx.FormString("target_version"),
Maintainer: ctx.FormString("maintainer"),
PHPMinimum: ctx.FormString("php_minimum"),
StreamMode: "joomla",
}
if err := licenses_model.SaveConfig(ctx, updateCfg); err != nil {
log.Error("SaveConfig: %v", err)
ctx.ServerError("SaveConfig", err)
return
}
}
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/licensing")
} }
+189
View File
@@ -0,0 +1,189 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package setting
import (
"encoding/xml"
"fmt"
"net/http"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
const tplSettingsManifest templates.TplName = "repo/settings/manifest"
// manifestXML mirrors the .mokogitea/manifest.xml schema for XML parsing.
type manifestXML struct {
XMLName xml.Name `xml:"mokoplatform"`
Identity manifestIdentity `xml:"identity"`
Governance manifestGovernance `xml:"governance"`
Distribution manifestDistribution `xml:"distribution"`
Build manifestBuild `xml:"build"`
}
type manifestIdentity struct {
Name string `xml:"name"`
Org string `xml:"org"`
Description string `xml:"description"`
Version string `xml:"version"`
VersionPrefix string `xml:"version-prefix"`
License manifestLicense `xml:"license"`
}
type manifestLicense struct {
SPDX string `xml:"spdx,attr"`
Name string `xml:",chardata"`
}
type manifestGovernance struct {
Platform string `xml:"platform"`
StandardsVersion string `xml:"standards-version"`
StandardsSource string `xml:"standards-source"`
}
type manifestDistribution struct {
DisplayName string `xml:"display-name"`
Maintainer string `xml:"maintainer"`
MaintainerURL string `xml:"maintainer-url"`
InfoURL string `xml:"info-url"`
TargetVersion string `xml:"target-version"`
PHPMinimum string `xml:"php-minimum"`
}
type manifestBuild struct {
Language string `xml:"language"`
PackageType string `xml:"package-type"`
EntryPoint string `xml:"entry-point"`
}
// ManifestSettings displays the repo manifest settings page.
// On first visit, if no manifest exists in DB but .mokogitea/manifest.xml
// exists in the repo, it auto-migrates the XML values into the database.
func ManifestSettings(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.manifest")
ctx.Data["PageIsSettingsManifest"] = true
repoID := ctx.Repo.Repository.ID
manifest, err := repo_model.GetRepoManifest(ctx, repoID)
if err != nil {
ctx.ServerError("GetRepoManifest", err)
return
}
// Auto-detect and migrate .mokogitea/manifest.xml if no DB record exists.
if manifest == nil {
manifest = tryMigrateManifestXML(ctx)
}
if manifest == nil {
// No manifest found — provide empty defaults from repo metadata.
manifest = &repo_model.RepoManifest{
RepoID: repoID,
Name: ctx.Repo.Repository.Name,
Org: ctx.Repo.Repository.OwnerName,
Description: ctx.Repo.Repository.Description,
}
}
ctx.Data["Manifest"] = manifest
ctx.HTML(http.StatusOK, tplSettingsManifest)
}
// ManifestSettingsPost saves manifest settings from the form.
func ManifestSettingsPost(ctx *context.Context) {
manifest := &repo_model.RepoManifest{
RepoID: ctx.Repo.Repository.ID,
Name: ctx.FormString("name"),
Org: ctx.FormString("org"),
Description: ctx.Repo.Repository.Description,
Version: ctx.FormString("version"),
LicenseSPDX: ctx.FormString("license_spdx"),
LicenseName: ctx.FormString("license_name"),
VersionPrefix: ctx.FormString("version_prefix"),
ElementName: ctx.FormString("element_name"),
Platform: ctx.FormString("platform"),
StandardsVersion: ctx.FormString("standards_version"),
StandardsSource: ctx.FormString("standards_source"),
DisplayName: ctx.FormString("display_name"),
Maintainer: ctx.FormString("maintainer"),
MaintainerURL: ctx.FormString("maintainer_url"),
InfoURL: ctx.FormString("info_url"),
TargetVersion: ctx.FormString("target_version"),
PHPMinimum: ctx.FormString("php_minimum"),
Language: ctx.FormString("language"),
PackageType: ctx.FormString("package_type"),
EntryPoint: ctx.FormString("entry_point"),
}
if err := repo_model.CreateOrUpdateRepoManifest(ctx, manifest); err != nil {
ctx.ServerError("CreateOrUpdateRepoManifest", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.manifest_saved"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/manifest")
}
// tryMigrateManifestXML reads .mokogitea/manifest.xml from the repo,
// parses it, and stores the values in the DB. Returns nil if no file found.
func tryMigrateManifestXML(ctx *context.Context) *repo_model.RepoManifest {
if ctx.Repo.GitRepo == nil || ctx.Repo.Commit == nil {
return nil
}
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(".mokogitea/manifest.xml")
if err != nil || entry == nil {
return nil // no manifest.xml found — not an error
}
reader, err := entry.Blob().DataAsync()
if err != nil {
log.Error("ManifestMigrate: read blob: %v", err)
return nil
}
defer reader.Close()
var mxml manifestXML
if err := xml.NewDecoder(reader).Decode(&mxml); err != nil {
log.Error("ManifestMigrate: parse XML: %v", err)
return nil
}
manifest := &repo_model.RepoManifest{
RepoID: ctx.Repo.Repository.ID,
Name: mxml.Identity.Name,
Org: mxml.Identity.Org,
Description: mxml.Identity.Description,
Version: mxml.Identity.Version,
LicenseSPDX: mxml.Identity.License.SPDX,
LicenseName: mxml.Identity.License.Name,
VersionPrefix: mxml.Identity.VersionPrefix,
Platform: mxml.Governance.Platform,
StandardsVersion: mxml.Governance.StandardsVersion,
StandardsSource: mxml.Governance.StandardsSource,
DisplayName: mxml.Distribution.DisplayName,
Maintainer: mxml.Distribution.Maintainer,
MaintainerURL: mxml.Distribution.MaintainerURL,
InfoURL: mxml.Distribution.InfoURL,
TargetVersion: mxml.Distribution.TargetVersion,
PHPMinimum: mxml.Distribution.PHPMinimum,
Language: mxml.Build.Language,
PackageType: mxml.Build.PackageType,
EntryPoint: mxml.Build.EntryPoint,
}
if err := repo_model.CreateOrUpdateRepoManifest(ctx, manifest); err != nil {
log.Error("ManifestMigrate: save to DB: %v", err)
return nil
}
log.Info("ManifestMigrate: migrated .mokogitea/manifest.xml for repo %s/%s",
ctx.Repo.Repository.OwnerName, ctx.Repo.Repository.Name)
ctx.Flash.Info(fmt.Sprintf("Manifest settings imported from .mokogitea/manifest.xml. You can now delete the file from the repository."))
return manifest
}
+5 -96
View File
@@ -9,39 +9,20 @@ import (
"net/http" "net/http"
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues" issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
) )
const tplSettingsMetadata templates.TplName = "repo/settings/metadata" const tplSettingsMetadata templates.TplName = "repo/settings/metadata"
// Metadata displays the consolidated metadata page: // Metadata displays the repo metadata page (repo-scoped custom field values).
// project identity (manifest), update server config, and repo-scoped custom fields.
func Metadata(ctx *context.Context) { func Metadata(ctx *context.Context) {
ctx.Data["Title"] = "Metadata" ctx.Data["Title"] = ctx.Tr("repo.settings.metadata")
ctx.Data["PageIsSettingsMetadata"] = true ctx.Data["PageIsSettingsMetadata"] = true
repoID := ctx.Repo.Repository.ID
ownerID := ctx.Repo.Repository.OwnerID ownerID := ctx.Repo.Repository.OwnerID
repoID := ctx.Repo.Repository.ID
// Load manifest (project identity).
manifest, err := repo_model.GetRepoManifest(ctx, repoID)
if err != nil {
ctx.ServerError("GetRepoManifest", err)
return
}
if manifest == nil {
manifest = &repo_model.RepoManifest{
RepoID: repoID,
Name: ctx.Repo.Repository.Name,
Org: ctx.Repo.Repository.OwnerName,
Description: ctx.Repo.Repository.Description,
}
}
ctx.Data["Manifest"] = manifest
// Load repo-scoped custom fields.
fields, _ := issues_model.GetCustomFieldsByOwner(ctx, ownerID, issues_model.CustomFieldScopeRepo) fields, _ := issues_model.GetCustomFieldsByOwner(ctx, ownerID, issues_model.CustomFieldScopeRepo)
ctx.Data["CustomFieldDefs"] = fields ctx.Data["CustomFieldDefs"] = fields
@@ -64,79 +45,8 @@ func Metadata(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplSettingsMetadata) ctx.HTML(http.StatusOK, tplSettingsMetadata)
} }
// MetadataPost routes to the correct sub-handler based on the action param. // MetadataPost saves repo-scoped custom field values.
func MetadataPost(ctx *context.Context) { func MetadataPost(ctx *context.Context) {
switch ctx.FormString("action") {
case "manifest":
saveManifest(ctx)
case "customfields":
saveCustomFields(ctx)
default:
saveManifest(ctx)
}
}
// spdxToName maps SPDX identifiers to human-readable license names.
var spdxToName = map[string]string{
"GPL-3.0-or-later": "GNU General Public License v3 or later",
"GPL-2.0-or-later": "GNU General Public License v2 or later",
"MIT": "MIT License",
"Apache-2.0": "Apache License 2.0",
"BSD-3-Clause": "BSD 3-Clause License",
"BSD-2-Clause": "BSD 2-Clause License",
"LGPL-3.0-or-later": "GNU Lesser General Public License v3 or later",
"MPL-2.0": "Mozilla Public License 2.0",
"ISC": "ISC License",
"AGPL-3.0-or-later": "GNU Affero General Public License v3 or later",
"Unlicense": "The Unlicense",
"proprietary": "Proprietary",
}
func saveManifest(ctx *context.Context) {
spdx := ctx.FormString("license_spdx")
licenseName := spdxToName[spdx]
// Preserve existing values for fields removed from the UI.
existing, _ := repo_model.GetRepoManifest(ctx, ctx.Repo.Repository.ID)
manifest := &repo_model.RepoManifest{
RepoID: ctx.Repo.Repository.ID,
Name: ctx.FormString("name"),
Org: ctx.FormString("org"),
Description: ctx.Repo.Repository.Description,
Version: ctx.FormString("version"),
LicenseSPDX: spdx,
LicenseName: licenseName,
VersionPrefix: ctx.FormString("version_prefix"),
Platform: ctx.FormString("platform"),
InfoURL: ctx.FormString("info_url"),
TargetVersion: ctx.FormString("target_version"),
PHPMinimum: ctx.FormString("php_minimum"),
PackageType: ctx.FormString("package_type"),
EntryPoint: ctx.FormString("entry_point"),
}
// Preserve fields not in the UI but still in the model.
if existing != nil {
manifest.ElementName = existing.ElementName
manifest.StandardsVersion = existing.StandardsVersion
manifest.StandardsSource = existing.StandardsSource
manifest.DisplayName = existing.DisplayName
manifest.Maintainer = existing.Maintainer
manifest.MaintainerURL = existing.MaintainerURL
manifest.Language = existing.Language
}
if err := repo_model.CreateOrUpdateRepoManifest(ctx, manifest); err != nil {
ctx.ServerError("CreateOrUpdateRepoManifest", err)
return
}
ctx.Flash.Success("Project identity saved")
ctx.Redirect(ctx.Repo.RepoLink + "/settings/metadata")
}
func saveCustomFields(ctx *context.Context) {
repoID := ctx.Repo.Repository.ID repoID := ctx.Repo.Repository.ID
ownerID := ctx.Repo.Repository.OwnerID ownerID := ctx.Repo.Repository.OwnerID
@@ -149,7 +59,6 @@ func saveCustomFields(ctx *context.Context) {
} }
} }
ctx.Flash.Success("Custom fields saved") ctx.Flash.Success(ctx.Tr("repo.settings.metadata_saved"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/metadata") ctx.Redirect(ctx.Repo.RepoLink + "/settings/metadata")
} }
+5 -5
View File
@@ -12,7 +12,7 @@ import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/organization" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/organization"
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver" licenses_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/perm" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/perm"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
unit_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit" unit_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit"
@@ -101,7 +101,7 @@ func SettingsCtxData(ctx *context.Context) {
// Settings show a repository's settings page // Settings show a repository's settings page
func Settings(ctx *context.Context) { func Settings(ctx *context.Context) {
repoCfg, _ := updateserver_model.GetRepoConfig(ctx, ctx.Repo.Repository.ID) repoCfg, _ := licenses_model.GetRepoConfig(ctx, ctx.Repo.Repository.ID)
ctx.Data["RepoUpdateConfig"] = repoCfg ctx.Data["RepoUpdateConfig"] = repoCfg
ctx.HTML(http.StatusOK, tplSettingsOptions) ctx.HTML(http.StatusOK, tplSettingsOptions)
} }
@@ -679,7 +679,7 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
// so it falls through to org defaults cleanly. // so it falls through to org defaults cleanly.
if !form.EnableLicensing { if !form.EnableLicensing {
// Remove repo-level override so org config takes effect // Remove repo-level override so org config takes effect
if err := updateserver_model.DeleteRepoConfig(ctx, repo.ID); err != nil { if err := licenses_model.DeleteRepoConfig(ctx, repo.ID); err != nil {
log.Error("DeleteRepoConfig: %v", err) log.Error("DeleteRepoConfig: %v", err)
} }
} else { } else {
@@ -687,7 +687,7 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
if updatePlatform == "" { if updatePlatform == "" {
updatePlatform = "joomla" updatePlatform = "joomla"
} }
updateCfg := &updateserver_model.UpdateStreamConfig{ updateCfg := &licenses_model.UpdateStreamConfig{
OwnerID: repo.OwnerID, OwnerID: repo.OwnerID,
RepoID: repo.ID, RepoID: repo.ID,
Platform: updatePlatform, Platform: updatePlatform,
@@ -703,7 +703,7 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
PHPMinimum: form.PHPMinimum, PHPMinimum: form.PHPMinimum,
StreamMode: "joomla", StreamMode: "joomla",
} }
if err := updateserver_model.SaveConfig(ctx, updateCfg); err != nil { if err := licenses_model.SaveConfig(ctx, updateCfg); err != nil {
log.Error("SaveConfig: %v", err) log.Error("SaveConfig: %v", err)
} }
} }
-71
View File
@@ -1,71 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package setting
import (
"net/http"
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
const tplSettingsUpdateServer templates.TplName = "repo/settings/updateserver"
// UpdateServerSettings displays the update server settings page.
func UpdateServerSettings(ctx *context.Context) {
ctx.Data["Title"] = "Update Server"
ctx.Data["PageIsSettingsUpdateServer"] = true
repoCfg, _ := updateserver_model.GetRepoConfig(ctx, ctx.Repo.Repository.ID)
ctx.Data["RepoUpdateConfig"] = repoCfg
ctx.HTML(http.StatusOK, tplSettingsUpdateServer)
}
// UpdateServerSettingsPost saves update server visibility and gating settings.
func UpdateServerSettingsPost(ctx *context.Context) {
repo := ctx.Repo.Repository
enabled := ctx.FormString("enable_licensing") == "on"
if !enabled {
if err := updateserver_model.DeleteRepoConfig(ctx, repo.ID); err != nil {
log.Error("DeleteRepoConfig: %v", err)
}
} else {
// Load existing config to preserve platform and other fields.
existing, _ := updateserver_model.GetRepoConfig(ctx, repo.ID)
platform := "joomla"
streamMode := "joomla"
supportURL := ""
if existing != nil {
platform = existing.Platform
streamMode = existing.StreamMode
supportURL = existing.SupportURL
}
updateCfg := &updateserver_model.UpdateStreamConfig{
OwnerID: repo.OwnerID,
RepoID: repo.ID,
Platform: platform,
LicensingEnabled: true,
RequireKey: ctx.FormString("require_update_key") == "on",
DownloadGating: ctx.FormString("download_gating"),
FeedVisibility: ctx.FormString("feed_visibility"),
SupportURL: supportURL,
StreamMode: streamMode,
}
if err := updateserver_model.SaveConfig(ctx, updateCfg); err != nil {
log.Error("SaveConfig: %v", err)
ctx.ServerError("SaveConfig", err)
return
}
}
ctx.Flash.Success("Update server settings saved")
ctx.Redirect(ctx.Repo.RepoLink + "/settings/updateserver")
}
+5 -5
View File
@@ -7,7 +7,7 @@ import (
"net/http" "net/http"
"strings" "strings"
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/json" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/json"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
@@ -33,15 +33,15 @@ func validateUpdateKey(ctx *context.Context) (allowedChannels []string, ok bool,
} }
domain := ctx.FormString("domain") domain := ctx.FormString("domain")
key, pkg, err := updateserver_model.ValidateLicenseKeyForRepo(ctx, rawKey, domain, ctx.Repo.Repository.ID) key, pkg, err := licenses.ValidateLicenseKeyForRepo(ctx, rawKey, domain, ctx.Repo.Repository.ID)
if err != nil { if err != nil {
log.Debug("License key validation failed: %v", err) log.Debug("License key validation failed: %v", err)
return nil, false, false return nil, false, false
} }
// Update heartbeat and record usage. // Update heartbeat and record usage.
_ = updateserver_model.TouchHeartbeat(ctx, key.ID) _ = licenses.TouchHeartbeat(ctx, key.ID)
_ = updateserver_model.RecordUsage(ctx, &updateserver_model.LicenseKeyUsage{ _ = licenses.RecordUsage(ctx, &licenses.LicenseKeyUsage{
KeyID: key.ID, KeyID: key.ID,
RepoID: ctx.Repo.Repository.ID, RepoID: ctx.Repo.Repository.ID,
Domain: domain, Domain: domain,
@@ -93,7 +93,7 @@ func ServeUpdatesXML(ctx *context.Context) {
return return
} }
repoCfg, _ := updateserver_model.GetRepoConfig(ctx, ctx.Repo.Repository.ID) repoCfg, _ := licenses.GetRepoConfig(ctx, ctx.Repo.Repository.ID)
// Show <downloadkey> only when downloads are gated (prerelease or all). // Show <downloadkey> only when downloads are gated (prerelease or all).
// No gating = no license keys needed = no downloadkey element. // No gating = no license keys needed = no downloadkey element.
requireKey := repoCfg != nil && repoCfg.DownloadGating != "" && repoCfg.DownloadGating != "none" requireKey := repoCfg != nil && repoCfg.DownloadGating != "" && repoCfg.DownloadGating != "none"
+4 -12
View File
@@ -137,8 +137,8 @@ type PrepareOwnerHeaderResult struct {
const ( const (
RepoNameProfilePrivate = ".profile-private" RepoNameProfilePrivate = ".profile-private"
RepoNameProfile = ".profile" RepoNameProfile = ".profile"
RepoNameWikiPublic = ".profile" RepoNameWikiPublic = "wiki"
RepoNameWikiPrivate = ".profile-private" RepoNameWikiPrivate = "wiki-private"
) )
func RenderUserOrgHeader(ctx *context.Context) (result *PrepareOwnerHeaderResult, err error) { func RenderUserOrgHeader(ctx *context.Context) (result *PrepareOwnerHeaderResult, err error) {
@@ -209,19 +209,11 @@ func loadHeaderCount(ctx *context.Context) error {
return nil return nil
} }
// OrgWikiRepoExists checks whether a profile repo's wiki exists and has content. // OrgWikiRepoExists checks whether a convention wiki repo exists and is non-empty.
func OrgWikiRepoExists(ctx *context.Context, ownerID int64, repoName string) bool { func OrgWikiRepoExists(ctx *context.Context, ownerID int64, repoName string) bool {
dbRepo, err := repo_model.GetRepositoryByName(ctx, ownerID, repoName) dbRepo, err := repo_model.GetRepositoryByName(ctx, ownerID, repoName)
if err != nil { if err != nil || dbRepo.IsEmpty {
log.Trace("OrgWikiRepoExists: repo %s not found for owner %d: %v", repoName, ownerID, err)
return false return false
} }
wikiRepo := dbRepo.WikiStorageRepo()
_, err = gitrepo.GetDefaultBranch(ctx, wikiRepo)
if err != nil {
log.Error("OrgWikiRepoExists: GetDefaultBranch for wiki of %s failed: %v (path: %s)", dbRepo.FullName(), err, wikiRepo.RelativePath())
return false
}
log.Trace("OrgWikiRepoExists: wiki found for %s", dbRepo.FullName())
return true return true
} }
+2 -3
View File
@@ -1229,10 +1229,9 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Group("", func() { m.Group("", func() {
m.Combo("/advanced").Get(repo_setting.AdvancedSettings).Post(web.Bind(forms.RepoSettingForm{}), repo_setting.SettingsPost) m.Combo("/advanced").Get(repo_setting.AdvancedSettings).Post(web.Bind(forms.RepoSettingForm{}), repo_setting.SettingsPost)
}, repo_setting.SettingsCtxData) }, repo_setting.SettingsCtxData)
m.Combo("/licensing").Get(repo_setting.LicensingSettings).Post(repo_setting.LicensingSettingsPost)
m.Combo("/manifest").Get(repo_setting.ManifestSettings).Post(repo_setting.ManifestSettingsPost)
m.Combo("/metadata").Get(repo_setting.Metadata).Post(repo_setting.MetadataPost) m.Combo("/metadata").Get(repo_setting.Metadata).Post(repo_setting.MetadataPost)
m.Combo("/updateserver").Get(repo_setting.UpdateServerSettings).Post(repo_setting.UpdateServerSettingsPost)
m.Combo("/manifest").Get(repo_setting.LicensingSettings).Post(repo_setting.LicensingSettingsPost) // redirect
m.Combo("/licensing").Get(repo_setting.LicensingSettings).Post(repo_setting.LicensingSettingsPost) // redirect
m.Group("/security", func() { m.Group("/security", func() {
m.Combo("").Get(repo_setting.SecuritySettings).Post(repo_setting.SecuritySettingsPost) m.Combo("").Get(repo_setting.SecuritySettings).Post(repo_setting.SecuritySettingsPost)
m.Post("/scan", repo_setting.SecurityScanNow) m.Post("/scan", repo_setting.SecurityScanNow)
+6 -6
View File
@@ -18,7 +18,7 @@ import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git" git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues" issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver" licenses_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
access_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/perm/access" access_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/perm/access"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
unit_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit" unit_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit"
@@ -438,8 +438,8 @@ func repoAssignmentLegacy(ctx *Context, data *repoAssignmentPrepareDataStruct) {
// Check if licensing is enabled — licensed repos allow access to // Check if licensing is enabled — licensed repos allow access to
// releases and downloads via license key, even without membership. // releases and downloads via license key, even without membership.
orgCfg, _ := updateserver_model.GetOrgConfig(ctx, repo.OwnerID) orgCfg, _ := licenses_model.GetOrgConfig(ctx, repo.OwnerID)
repoCfg, _ := updateserver_model.GetRepoConfig(ctx, repo.ID) repoCfg, _ := licenses_model.GetRepoConfig(ctx, repo.ID)
licensingEnabled := (orgCfg != nil && orgCfg.LicensingEnabled) || licensingEnabled := (orgCfg != nil && orgCfg.LicensingEnabled) ||
(repoCfg != nil && repoCfg.LicensingEnabled) (repoCfg != nil && repoCfg.LicensingEnabled)
@@ -652,12 +652,12 @@ func repoAssignmentPrepareTemplateData(ctx *Context, data *repoAssignmentPrepare
} }
// Check if licensing is enabled for this repo/org. // Check if licensing is enabled for this repo/org.
orgCfg, _ := updateserver_model.GetOrgConfig(ctx, repo.OwnerID) orgCfg, _ := licenses_model.GetOrgConfig(ctx, repo.OwnerID)
repoUpdateCfg, _ := updateserver_model.GetRepoConfig(ctx, repo.ID) repoUpdateCfg, _ := licenses_model.GetRepoConfig(ctx, repo.ID)
licensingEnabled := (orgCfg != nil && orgCfg.LicensingEnabled) || licensingEnabled := (orgCfg != nil && orgCfg.LicensingEnabled) ||
(repoUpdateCfg != nil && repoUpdateCfg.LicensingEnabled) (repoUpdateCfg != nil && repoUpdateCfg.LicensingEnabled)
numLicensePackages, _ := db.Count[updateserver_model.LicensePackage](ctx, updateserver_model.FindLicensePackageOptions{ numLicensePackages, _ := db.Count[licenses_model.LicensePackage](ctx, licenses_model.FindLicensePackageOptions{
OwnerID: repo.OwnerID, OwnerID: repo.OwnerID,
}) })
ctx.Data["NumLicensePackages"] = numLicensePackages ctx.Data["NumLicensePackages"] = numLicensePackages
+6 -11
View File
@@ -4,7 +4,7 @@
package context package context
import ( import (
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver" licenses_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user" user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
@@ -42,16 +42,11 @@ func RepoAssignmentPublicFeed() func(ctx *Context) {
repo.Owner = owner repo.Owner = owner
ctx.Repo.Repository = repo ctx.Repo.Repository = repo
// Check if the update server is enabled (repo config → org config). // Load update config for platform-aware routing.
// Return 404 when neither level has LicensingEnabled=true. repoUpdateCfg, _ := licenses_model.GetRepoConfig(ctx, repo.ID)
cfg := updateserver_model.GetEffectiveConfig(ctx, owner.ID, repo.ID) if repoUpdateCfg != nil {
if cfg == nil || !cfg.LicensingEnabled { ctx.Data["RepoUpdatePlatform"] = repoUpdateCfg.Platform
ctx.NotFound(nil) } else {
return
}
ctx.Data["RepoUpdatePlatform"] = cfg.Platform
if cfg.Platform == "" {
ctx.Data["RepoUpdatePlatform"] = "joomla" ctx.Data["RepoUpdatePlatform"] = "joomla"
} }
+2 -2
View File
@@ -9,7 +9,7 @@ import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git" git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver" licenses_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user" user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/webhook" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/webhook"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git/gitcmd" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git/gitcmd"
@@ -167,7 +167,7 @@ func registerCleanupExpiredLicenseKeys() {
Schedule: "@weekly", Schedule: "@weekly",
}, func(ctx context.Context, _ *user_model.User, config Config) error { }, func(ctx context.Context, _ *user_model.User, config Config) error {
// Delete non-internal keys that expired more than 365 days ago. // Delete non-internal keys that expired more than 365 days ago.
deleted, err := updateserver_model.DeleteExpiredKeys(ctx, 365) deleted, err := licenses_model.DeleteExpiredKeys(ctx, 365)
if err != nil { if err != nil {
return err return err
} }
+4 -4
View File
@@ -11,7 +11,7 @@ import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git" git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver" licenses_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user" user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/container" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/container"
@@ -192,8 +192,8 @@ func validateTagAgainstStreams(ctx context.Context, rel *repo_model.Release) err
} }
// Check if licensing is enabled at org or repo level. // Check if licensing is enabled at org or repo level.
orgCfg, _ := updateserver_model.GetOrgConfig(ctx, repo.OwnerID) orgCfg, _ := licenses_model.GetOrgConfig(ctx, repo.OwnerID)
repoCfg, _ := updateserver_model.GetRepoConfig(ctx, repo.ID) repoCfg, _ := licenses_model.GetRepoConfig(ctx, repo.ID)
licensingEnabled := (orgCfg != nil && orgCfg.LicensingEnabled) || licensingEnabled := (orgCfg != nil && orgCfg.LicensingEnabled) ||
(repoCfg != nil && repoCfg.LicensingEnabled) (repoCfg != nil && repoCfg.LicensingEnabled)
@@ -203,7 +203,7 @@ func validateTagAgainstStreams(ctx context.Context, rel *repo_model.Release) err
// Check that the tag contains a stream-compatible suffix. // Check that the tag contains a stream-compatible suffix.
// Any prerelease suffix in the tag must match a configured stream suffix. // Any prerelease suffix in the tag must match a configured stream suffix.
streams := updateserver_model.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID) streams := licenses_model.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
lower := strings.ToLower(rel.TagName) lower := strings.ToLower(rel.TagName)
for _, s := range streams { for _, s := range streams {
if s.Suffix == "" { if s.Suffix == "" {
+4 -4
View File
@@ -10,7 +10,7 @@ import (
"time" "time"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
) )
@@ -67,7 +67,7 @@ func GenerateComposerJSON(ctx context.Context, repo *repo_model.Repository, lice
baseURL := strings.TrimSuffix(setting.AppURL, "/") baseURL := strings.TrimSuffix(setting.AppURL, "/")
repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name) repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name)
cfg := updateserver_model.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID) cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
meta := resolveExtensionMetadata(ctx, repo, cfg) meta := resolveExtensionMetadata(ctx, repo, cfg)
// Composer package name: vendor/package (override with resolved extension name if set) // Composer package name: vendor/package (override with resolved extension name if set)
@@ -91,7 +91,7 @@ func GenerateComposerJSON(ctx context.Context, repo *repo_model.Repository, lice
phpMin = ">=" + meta.PHPMinimum phpMin = ">=" + meta.PHPMinimum
} }
streams := updateserver_model.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID) streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
versions := make(map[string]ComposerVersion) versions := make(map[string]ComposerVersion)
for _, rel := range releases { for _, rel := range releases {
@@ -99,7 +99,7 @@ func GenerateComposerJSON(ctx context.Context, repo *repo_model.Repository, lice
continue continue
} }
ch := updateserver_model.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams) ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
if ch != "stable" { if ch != "stable" {
continue // Composer only serves stable versions continue // Composer only serves stable versions
} }
+3 -3
View File
@@ -10,7 +10,7 @@ import (
"time" "time"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
) )
@@ -59,7 +59,7 @@ func GenerateDolibarrJSON(ctx context.Context, repo *repo_model.Repository, allo
} }
// Resolve effective streams. // Resolve effective streams.
streams := updateserver_model.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID) streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
// Track best release per channel. // Track best release per channel.
bestByChannel := make(map[string]*repo_model.Release) bestByChannel := make(map[string]*repo_model.Release)
@@ -67,7 +67,7 @@ func GenerateDolibarrJSON(ctx context.Context, repo *repo_model.Repository, allo
if rel.IsDraft || rel.IsTag { if rel.IsDraft || rel.IsTag {
continue continue
} }
ch := updateserver_model.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams) ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
existing, ok := bestByChannel[ch] existing, ok := bestByChannel[ch]
if !ok || rel.CreatedUnix > existing.CreatedUnix { if !ok || rel.CreatedUnix > existing.CreatedUnix {
bestByChannel[ch] = rel bestByChannel[ch] = rel
+4 -4
View File
@@ -10,7 +10,7 @@ import (
"strings" "strings"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
) )
@@ -65,12 +65,12 @@ func GenerateDrupalXML(ctx context.Context, repo *repo_model.Repository, allowed
baseURL := strings.TrimSuffix(setting.AppURL, "/") baseURL := strings.TrimSuffix(setting.AppURL, "/")
repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name) repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name)
cfg := updateserver_model.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID) cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
meta := resolveExtensionMetadata(ctx, repo, cfg) meta := resolveExtensionMetadata(ctx, repo, cfg)
shortName := meta.Element shortName := meta.Element
title := meta.DisplayName title := meta.DisplayName
streams := updateserver_model.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID) streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
channelAllowed := make(map[string]bool) channelAllowed := make(map[string]bool)
if len(allowedChannels) > 0 { if len(allowedChannels) > 0 {
@@ -97,7 +97,7 @@ func GenerateDrupalXML(ctx context.Context, repo *repo_model.Repository, allowed
if rel.IsDraft || rel.IsTag { if rel.IsDraft || rel.IsTag {
continue continue
} }
ch := updateserver_model.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams) ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
if len(channelAllowed) > 0 && !channelAllowed[ch] { if len(channelAllowed) > 0 && !channelAllowed[ch] {
continue continue
} }
+103 -72
View File
@@ -13,7 +13,8 @@ import (
"time" "time"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver" 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" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/storage" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/storage"
@@ -111,7 +112,7 @@ func channelFromTag(tagName string, isPrerelease bool) string {
// isStreamName checks if a string matches any stream name (indicating the tag // isStreamName checks if a string matches any stream name (indicating the tag
// is a stream name, not a version number). // is a stream name, not a version number).
func isStreamName(s string, streams []updateserver_model.StreamDef) bool { func isStreamName(s string, streams []licenses.StreamDef) bool {
for _, st := range streams { for _, st := range streams {
if strings.EqualFold(st.Name, s) { if strings.EqualFold(st.Name, s) {
return true return true
@@ -162,7 +163,7 @@ func NormalizeChannel(ch string) string {
} }
// extensionMetadata holds resolved metadata for feed generation. // extensionMetadata holds resolved metadata for feed generation.
// Fields are resolved with priority: manifest → config table (gating only) → default. // Fields are resolved with priority: custom field → config table → default.
type extensionMetadata struct { type extensionMetadata struct {
Element string Element string
DisplayName string DisplayName string
@@ -175,60 +176,101 @@ type extensionMetadata struct {
KeyPrefix string KeyPrefix string
} }
// resolveExtensionMetadata loads extension metadata from the repo manifest API. // resolveExtensionMetadata loads extension metadata with cascading fallback:
// The manifest is the single source of truth for extension identity fields. // org-level repo-scoped custom fields → update_stream_config → repo-derived defaults.
// The config table is only used for licensing/gating fields not in the manifest. func resolveExtensionMetadata(ctx context.Context, repo *repo_model.Repository, cfg *licenses.UpdateStreamConfig) extensionMetadata {
func resolveExtensionMetadata(ctx context.Context, repo *repo_model.Repository, cfg *updateserver_model.UpdateStreamConfig) extensionMetadata {
m := extensionMetadata{ m := extensionMetadata{
Element: strings.ToLower(repo.Name), Element: strings.ToLower(repo.Name),
DisplayName: fmt.Sprintf("%s - %s", repo.Owner.Name, repo.Name), DisplayName: fmt.Sprintf("%s - %s", repo.Owner.Name, repo.Name),
ExtType: "component", ExtType: "component",
TargetVersion: "6\\..*", TargetVersion: "(5|6)\\..*",
} }
// Manifest is the source of truth for extension metadata. // Apply config table values.
manifest, err := repo_model.GetRepoManifest(ctx, repo.ID)
if err != nil {
log.Error("resolveExtensionMetadata: GetRepoManifest for repo %d: %v", repo.ID, err)
}
if manifest != nil {
if elem := manifest.FullElementName(); elem != "" {
m.Element = elem
}
if manifest.PackageType != "" {
m.ExtType = manifest.PackageType
}
if manifest.DisplayName != "" {
m.DisplayName = manifest.DisplayName
}
if manifest.TargetVersion != "" {
m.TargetVersion = manifest.TargetVersion
}
if manifest.PHPMinimum != "" {
m.PHPMinimum = manifest.PHPMinimum
}
if manifest.Description != "" {
m.Description = manifest.Description
}
if manifest.InfoURL != "" {
m.SupportURL = manifest.InfoURL
}
}
// Config table: only licensing/gating fields (not in manifest).
if cfg != nil { if cfg != nil {
if cfg.ExtensionName != "" {
m.Element = cfg.ExtensionName
}
if cfg.DisplayName != "" {
m.DisplayName = cfg.DisplayName
}
if cfg.ExtensionType != "" {
m.ExtType = cfg.ExtensionType
}
if cfg.TargetVersion != "" {
m.TargetVersion = cfg.TargetVersion
}
if cfg.PHPMinimum != "" {
m.PHPMinimum = cfg.PHPMinimum
}
if cfg.Description != "" {
m.Description = cfg.Description
}
if cfg.SupportURL != "" {
m.SupportURL = cfg.SupportURL
}
if cfg.DownloadGating != "" { if cfg.DownloadGating != "" {
m.DownloadGating = cfg.DownloadGating m.DownloadGating = cfg.DownloadGating
} }
if cfg.KeyPrefix != "" { if cfg.KeyPrefix != "" {
m.KeyPrefix = cfg.KeyPrefix m.KeyPrefix = cfg.KeyPrefix
} }
// SupportURL from config as fallback if manifest.InfoURL is empty }
if m.SupportURL == "" && cfg.SupportURL != "" {
m.SupportURL = cfg.SupportURL // 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 return m
} }
@@ -259,7 +301,7 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require
// Load extension metadata with cascading fallback: // Load extension metadata with cascading fallback:
// custom fields → config table → repo-derived defaults. // custom fields → config table → repo-derived defaults.
cfg := updateserver_model.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID) cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
meta := resolveExtensionMetadata(ctx, repo, cfg) meta := resolveExtensionMetadata(ctx, repo, cfg)
element := meta.Element element := meta.Element
@@ -280,7 +322,7 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require
} }
// Resolve effective streams (repo override → org default → Joomla default). // Resolve effective streams (repo override → org default → Joomla default).
streams := updateserver_model.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID) streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
// Track best (latest) release per channel to emit one entry per channel. // Track best (latest) release per channel to emit one entry per channel.
bestByChannel := make(map[string]*repo_model.Release) bestByChannel := make(map[string]*repo_model.Release)
@@ -288,7 +330,7 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require
if rel.IsDraft || rel.IsTag { if rel.IsDraft || rel.IsTag {
continue continue
} }
ch := updateserver_model.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams) ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
existing, ok := bestByChannel[ch] existing, ok := bestByChannel[ch]
if !ok || rel.CreatedUnix > existing.CreatedUnix { if !ok || rel.CreatedUnix > existing.CreatedUnix {
bestByChannel[ch] = rel bestByChannel[ch] = rel
@@ -318,7 +360,6 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require
// Load attachments for download URLs. // Load attachments for download URLs.
if err := rel.LoadAttributes(ctx); err != nil { if err := rel.LoadAttributes(ctx); err != nil {
log.Error("GenerateJoomlaXML: LoadAttributes for release %d (tag %s): %v", rel.ID, rel.TagName, err)
continue continue
} }
@@ -362,13 +403,11 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require
if version == "" { if version == "" {
version = rel.TagName version = rel.TagName
} }
// Append channel suffix only if the version doesn't already
// contain one (e.g. "1.2.3-rc2" already has "-rc").
suffix := stream.Suffix suffix := stream.Suffix
if suffix == "" { if suffix == "" {
suffix = channelSuffix(ch) suffix = channelSuffix(ch) // fallback for Joomla defaults
} }
if suffix != "" && !versionHasChannelSuffix(version) { if suffix != "" {
version = version + suffix version = version + suffix
} }
@@ -383,12 +422,13 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require
infoURL = meta.SupportURL infoURL = meta.SupportURL
} }
// Joomla <client> element uses string values per the update server spec. // Joomla <client> element: packages use client_id=0 in #__extensions,
// Joomla's XML parser maps these to client_id internally (0/1). // 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" client := "site"
switch extType { if extType == "package" {
case "package", "component", "library", "file": client = "0"
client = "administrator"
} }
u := xmlUpdate{ u := xmlUpdate{
@@ -438,9 +478,8 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require
return append([]byte(xml.Header), output...), nil return append([]byte(xml.Header), output...), nil
} }
// versionRegex matches semantic version patterns with optional pre-release // versionRegex matches semantic version patterns like 1.0.0, 02.29.04, etc.
// suffixes, e.g. 1.0.0, 02.29.04, 1.2.3-rc2, 1.0.0-beta1. var versionRegex = regexp.MustCompile(`(\d+\.\d+(?:\.\d+)?)`)
var versionRegex = regexp.MustCompile(`(\d+\.\d+(?:\.\d+)?(?:-(?:dev|alpha|beta|rc)\d*)?)`)
// extractVersion finds a version number from a tag name or release title. // extractVersion finds a version number from a tag name or release title.
// Tries: (1) strip common prefixes for version-style tags, (2) regex match for embedded versions. // Tries: (1) strip common prefixes for version-style tags, (2) regex match for embedded versions.
@@ -450,10 +489,13 @@ func extractVersion(s string) string {
v = strings.TrimPrefix(v, "v") v = strings.TrimPrefix(v, "v")
v = strings.TrimPrefix(v, "release-") v = strings.TrimPrefix(v, "release-")
v = strings.TrimPrefix(v, "release/") v = strings.TrimPrefix(v, "release/")
// Do not strip channel suffixes (e.g. -rc2, -beta1) here. // Strip channel suffixes.
// The caller appends stream suffixes only when the version for _, suffix := range []string{"-dev", "-alpha", "-beta", "-rc", "-development", "-release-candidate"} {
// doesn't already contain one, preserving the original if idx := strings.Index(strings.ToLower(v), suffix); idx > 0 {
// pre-release number for correct Joomla version comparison. v = v[:idx]
break
}
}
// If result looks like a version (starts with digit), use it. // If result looks like a version (starts with digit), use it.
if len(v) > 0 && v[0] >= '0' && v[0] <= '9' { if len(v) > 0 && v[0] >= '0' && v[0] <= '9' {
return strings.TrimSpace(v) return strings.TrimSpace(v)
@@ -467,17 +509,6 @@ func extractVersion(s string) string {
return "" return ""
} }
// channelSuffixRegex matches a pre-release channel suffix at the end of a
// version string, e.g. "-rc", "-rc2", "-beta1", "-dev". Anchored to avoid
// false positives like "-devtools".
var channelSuffixRegex = regexp.MustCompile(`(?i)-(dev|alpha|beta|rc)\d*$`)
// versionHasChannelSuffix checks if a version string already ends with a
// pre-release channel suffix (e.g. "-rc2", "-beta1", "-dev").
func versionHasChannelSuffix(version string) bool {
return channelSuffixRegex.MatchString(version)
}
// channelSuffix returns the version suffix for a channel. // channelSuffix returns the version suffix for a channel.
func channelSuffix(channel string) string { func channelSuffix(channel string) string {
switch channel { switch channel {
+4 -4
View File
@@ -11,7 +11,7 @@ import (
"time" "time"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
) )
@@ -54,7 +54,7 @@ func GeneratePrestaShopXML(ctx context.Context, repo *repo_model.Repository, all
baseURL := strings.TrimSuffix(setting.AppURL, "/") baseURL := strings.TrimSuffix(setting.AppURL, "/")
repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name) repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name)
cfg := updateserver_model.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID) cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
meta := resolveExtensionMetadata(ctx, repo, cfg) meta := resolveExtensionMetadata(ctx, repo, cfg)
moduleName := meta.Element moduleName := meta.Element
displayName := meta.DisplayName displayName := meta.DisplayName
@@ -64,7 +64,7 @@ func GeneratePrestaShopXML(ctx context.Context, repo *repo_model.Repository, all
maintainer = cfg.Maintainer maintainer = cfg.Maintainer
} }
streams := updateserver_model.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID) streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
// Channel filtering. // Channel filtering.
channelAllowed := make(map[string]bool) channelAllowed := make(map[string]bool)
@@ -80,7 +80,7 @@ func GeneratePrestaShopXML(ctx context.Context, repo *repo_model.Repository, all
if rel.IsDraft || rel.IsTag { if rel.IsDraft || rel.IsTag {
continue continue
} }
ch := updateserver_model.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams) ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
existing, ok := bestByChannel[ch] existing, ok := bestByChannel[ch]
if !ok || rel.CreatedUnix > existing.CreatedUnix { if !ok || rel.CreatedUnix > existing.CreatedUnix {
bestByChannel[ch] = rel bestByChannel[ch] = rel
+4 -4
View File
@@ -10,7 +10,7 @@ import (
"time" "time"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
) )
@@ -49,7 +49,7 @@ func GenerateWHMCSJSON(ctx context.Context, repo *repo_model.Repository, license
baseURL := strings.TrimSuffix(setting.AppURL, "/") baseURL := strings.TrimSuffix(setting.AppURL, "/")
repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name) repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name)
cfg := updateserver_model.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID) cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
meta := resolveExtensionMetadata(ctx, repo, cfg) meta := resolveExtensionMetadata(ctx, repo, cfg)
displayName := meta.DisplayName displayName := meta.DisplayName
description := meta.Description description := meta.Description
@@ -64,7 +64,7 @@ func GenerateWHMCSJSON(ctx context.Context, repo *repo_model.Repository, license
} }
} }
streams := updateserver_model.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID) streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
// Find latest stable release. // Find latest stable release.
var latestStable *repo_model.Release var latestStable *repo_model.Release
@@ -72,7 +72,7 @@ func GenerateWHMCSJSON(ctx context.Context, repo *repo_model.Repository, license
if rel.IsDraft || rel.IsTag { if rel.IsDraft || rel.IsTag {
continue continue
} }
ch := updateserver_model.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams) ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
if ch == "stable" { if ch == "stable" {
if latestStable == nil || rel.CreatedUnix > latestStable.CreatedUnix { if latestStable == nil || rel.CreatedUnix > latestStable.CreatedUnix {
latestStable = rel latestStable = rel
+6 -6
View File
@@ -11,7 +11,7 @@ import (
"time" "time"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
) )
@@ -59,7 +59,7 @@ func GenerateWordPressJSON(ctx context.Context, repo *repo_model.Repository, lic
// Load extension metadata with cascading fallback: // Load extension metadata with cascading fallback:
// custom fields → config table → repo-derived defaults. // custom fields → config table → repo-derived defaults.
cfg := updateserver_model.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID) cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
meta := resolveExtensionMetadata(ctx, repo, cfg) meta := resolveExtensionMetadata(ctx, repo, cfg)
slug := meta.Element slug := meta.Element
@@ -84,13 +84,13 @@ func GenerateWordPressJSON(ctx context.Context, repo *repo_model.Repository, lic
} }
// Resolve streams and find the latest stable release. // Resolve streams and find the latest stable release.
streams := updateserver_model.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID) streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
var latestStable *repo_model.Release var latestStable *repo_model.Release
for _, rel := range releases { for _, rel := range releases {
if rel.IsDraft || rel.IsTag { if rel.IsDraft || rel.IsTag {
continue continue
} }
ch := updateserver_model.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams) ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
if ch == "stable" { if ch == "stable" {
if latestStable == nil || rel.CreatedUnix > latestStable.CreatedUnix { if latestStable == nil || rel.CreatedUnix > latestStable.CreatedUnix {
latestStable = rel latestStable = rel
@@ -171,14 +171,14 @@ func GenerateWordPressJSON(ctx context.Context, repo *repo_model.Repository, lic
} }
// buildWordPressChangelog builds an HTML changelog from multiple releases. // buildWordPressChangelog builds an HTML changelog from multiple releases.
func buildWordPressChangelog(ctx context.Context, releases []*repo_model.Release, streams []updateserver_model.StreamDef) string { func buildWordPressChangelog(ctx context.Context, releases []*repo_model.Release, streams []licenses.StreamDef) string {
var b strings.Builder var b strings.Builder
count := 0 count := 0
for _, rel := range releases { for _, rel := range releases {
if rel.IsDraft || rel.IsTag || rel.Note == "" { if rel.IsDraft || rel.IsTag || rel.Note == "" {
continue continue
} }
ch := updateserver_model.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams) ch := licenses.ResolveReleaseStream(ctx, rel.ID, rel.TagName, rel.IsPrerelease, streams)
if ch != "stable" { if ch != "stable" {
continue continue
} }
+1 -2
View File
@@ -323,8 +323,7 @@ func TestWebPathConversion(t *testing.T) {
} }
func TestWebPathFromRequest(t *testing.T) { func TestWebPathFromRequest(t *testing.T) {
// MokoGitea: slashes are preserved for folder-based wiki navigation. assert.Equal(t, WebPath("a%2Fb"), WebPathFromRequest("a/b"))
assert.Equal(t, WebPath("a/b"), WebPathFromRequest("a/b"))
assert.Equal(t, WebPath("a"), WebPathFromRequest("a")) assert.Equal(t, WebPath("a"), WebPathFromRequest("a"))
assert.Equal(t, WebPath("b"), WebPathFromRequest("a/../b")) assert.Equal(t, WebPath("b"), WebPathFromRequest("a/../b"))
} }
+1 -8
View File
@@ -19,7 +19,7 @@
<tbody> <tbody>
{{range .CustomFields}} {{range .CustomFields}}
<tr> <tr>
<td><strong>{{.Name}}</strong>{{if .Required}} <span class="tw-text-red" data-tooltip-content="Required">*</span>{{end}}{{if .Description}}<br><small class="text grey">{{.Description}}</small>{{end}}</td> <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>{{if eq .Scope "issue"}}{{svg "octicon-issue-opened" 14}} Issue{{else}}{{svg "octicon-repo" 14}} Repo{{end}}</td>
<td><code>{{.FieldType}}</code></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>{{if .Options}}<code class="tw-text-xs">{{.Options}}</code>{{else}}<span class="text grey">-</span>{{end}}</td>
@@ -79,13 +79,6 @@
<input name="description" placeholder="Help text shown to users"> <input name="description" placeholder="Help text shown to users">
</div> </div>
</div> </div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" name="required">
<label>{{ctx.Locale.Tr "org.settings.custom_field_required"}}</label>
</div>
<p class="help">{{ctx.Locale.Tr "org.settings.custom_field_required_help"}}</p>
</div>
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "org.settings.custom_field_add"}}</button> <button class="ui primary button" type="submit">{{ctx.Locale.Tr "org.settings.custom_field_add"}}</button>
</form> </form>
</div> </div>
+2 -2
View File
@@ -68,11 +68,11 @@
<div class="field"> <div class="field">
<div class="ui radio checkbox"> <div class="ui radio checkbox">
<input class="enable-system-radio" name="wiki_mode" type="radio" value="" data-context="#external_wiki_box" data-target="#internal_wiki_box" {{if eq .Org.WikiMode ""}}checked{{end}}> <input class="enable-system-radio" name="wiki_mode" type="radio" value="" data-context="#external_wiki_box" data-target="#internal_wiki_box" {{if eq .Org.WikiMode ""}}checked{{end}}>
<label>Internal wiki (uses <code>.profile</code> / <code>.profile-private</code> repo wikis)</label> <label>Internal wiki (uses <code>wiki</code> / <code>wiki-private</code> repos)</label>
</div> </div>
</div> </div>
<div id="internal_wiki_box" class="field tw-pl-4 {{if ne .Org.WikiMode ""}}disabled{{end}}"> <div id="internal_wiki_box" class="field tw-pl-4 {{if ne .Org.WikiMode ""}}disabled{{end}}">
<p class="help">Enable the wiki on <code>.profile</code> (public) and/or <code>.profile-private</code> (members-only) repos.</p> <p class="help">Create repos named <code>wiki</code> (public) and/or <code>wiki-private</code> (members-only) under this organization.</p>
</div> </div>
<div class="field"> <div class="field">
<div class="ui radio checkbox"> <div class="ui radio checkbox">
+2 -2
View File
@@ -11,8 +11,8 @@
This organization doesn't have a wiki yet. This organization doesn't have a wiki yet.
</div> </div>
<p class="tw-text-center"> <p class="tw-text-center">
Enable the wiki on the <code>.profile</code> (public) or <code>.profile-private</code> (members-only) Create a repository named <code>wiki</code> (public) or <code>wiki-private</code> (members-only)
repository to get started. with markdown files to get started.
</p> </p>
</div> </div>
{{else}} {{else}}
+1 -1
View File
@@ -17,7 +17,7 @@
</div> </div>
<div class="file-header-right file-actions tw-flex tw-items-center tw-flex-wrap"> <div class="file-header-right file-actions tw-flex tw-items-center tw-flex-wrap">
<div class="ui buttons"> <div class="ui buttons">
<a class="ui tiny button" href="{{$.RawFileLink}}" target="_blank" rel="noopener noreferrer">{{ctx.Locale.Tr "repo.file_raw"}}</a> <a class="ui tiny button" href="{{$.RawFileLink}}">{{ctx.Locale.Tr "repo.file_raw"}}</a>
{{if or .RefFullName.IsBranch .RefFullName.IsTag}} {{if or .RefFullName.IsBranch .RefFullName.IsTag}}
<a class="ui tiny button" href="{{.RepoLink}}/src/commit/{{.CommitID | PathEscape}}/{{.TreePath | PathEscapeSegments}}">{{ctx.Locale.Tr "repo.file_permalink"}}</a> <a class="ui tiny button" href="{{.RepoLink}}/src/commit/{{.CommitID | PathEscape}}/{{.TreePath | PathEscapeSegments}}">{{ctx.Locale.Tr "repo.file_permalink"}}</a>
{{end}} {{end}}
+3 -49
View File
@@ -59,52 +59,6 @@
{{end}} {{end}}
{{template "repo/issue/sidebar/assignee_list" $.IssuePageMetaData}} {{template "repo/issue/sidebar/assignee_list" $.IssuePageMetaData}}
{{if .IssueTypeDefs}}
<div class="divider"></div>
<div class="tw-flex tw-items-center tw-justify-between tw-gap-2">
<span class="text grey tw-text-sm">{{ctx.Locale.Tr "repo.issues.type"}} <span class="tw-text-red">*</span></span>
<select name="type_id" class="ui compact mini dropdown tw-max-w-48" required>
<option value="">—</option>
{{range .IssueTypeDefs}}
<option value="{{.ID}}" {{if .IsDefault}}selected{{end}}
{{if .Color}}style="border-left: 3px solid {{.Color}}"{{end}}>
{{.Name}}
</option>
{{end}}
</select>
</div>
{{end}}
{{if .IssuePriorityDefs}}
<div class="tw-flex tw-items-center tw-justify-between tw-gap-2 tw-mt-2">
<span class="text grey tw-text-sm">{{ctx.Locale.Tr "repo.issues.priority"}} <span class="tw-text-red">*</span></span>
<select name="priority_id" class="ui compact mini dropdown tw-max-w-48" required>
<option value="">—</option>
{{range .IssuePriorityDefs}}
<option value="{{.ID}}" {{if .IsDefault}}selected{{end}}
{{if .Color}}style="border-left: 3px solid {{.Color}}"{{end}}>
{{.Name}}
</option>
{{end}}
</select>
</div>
{{end}}
{{if .IssueStatusDefs}}
<div class="tw-flex tw-items-center tw-justify-between tw-gap-2 tw-mt-2">
<span class="text grey tw-text-sm">{{ctx.Locale.Tr "repo.issues.status"}} <span class="tw-text-red">*</span></span>
<select name="status_id" class="ui compact mini dropdown tw-max-w-48" required>
<option value="">—</option>
{{range .IssueStatusDefs}}
<option value="{{.ID}}" {{if and (eq .SortOrder 1) (not .ClosesIssue)}}selected{{end}}
{{if .Color}}style="border-left: 3px solid {{.Color}}"{{end}}>
{{.Name}}{{if .ClosesIssue}}{{end}}
</option>
{{end}}
</select>
</div>
{{end}}
{{if .CustomFieldDefs}} {{if .CustomFieldDefs}}
<div class="divider"></div> <div class="divider"></div>
<div class="tw-flex tw-flex-col tw-gap-2"> <div class="tw-flex tw-flex-col tw-gap-2">
@@ -113,17 +67,17 @@
{{range .CustomFieldDefs}} {{range .CustomFieldDefs}}
{{$currentVal := index $values .ID}} {{$currentVal := index $values .ID}}
<div class="tw-flex tw-items-center tw-justify-between tw-gap-2"> <div class="tw-flex tw-items-center tw-justify-between tw-gap-2">
<span class="text grey tw-text-sm" {{if .Description}}title="{{.Description}}"{{end}}>{{.Name}}{{if .Required}} <span class="tw-text-red">*</span>{{end}}</span> <span class="text grey tw-text-sm" {{if .Description}}title="{{.Description}}"{{end}}>{{.Name}}</span>
{{if ne .Options ""}} {{if ne .Options ""}}
{{$opts := index $fieldOptions .ID}} {{$opts := index $fieldOptions .ID}}
<select name="custom-field-{{.ID}}" class="ui compact mini dropdown tw-max-w-48" {{if .Required}}required{{end}}> <select name="custom-field-{{.ID}}" class="ui compact mini dropdown tw-max-w-48">
<option value="">—</option> <option value="">—</option>
{{range $opts}} {{range $opts}}
<option value="{{.}}" {{if eq . $currentVal}}selected{{end}}>{{.}}</option> <option value="{{.}}" {{if eq . $currentVal}}selected{{end}}>{{.}}</option>
{{end}} {{end}}
</select> </select>
{{else}} {{else}}
<input name="custom-field-{{.ID}}" type="text" class="tw-max-w-48 tw-text-sm" value="{{$currentVal}}" placeholder="—" {{if .Required}}required{{end}}> <input name="custom-field-{{.ID}}" type="text" class="tw-max-w-48 tw-text-sm" value="{{$currentVal}}" placeholder="—">
{{end}} {{end}}
</div> </div>
{{end}} {{end}}
+1 -1
View File
@@ -31,7 +31,7 @@
<strong>{{ctx.Locale.Tr "repo.audio_not_supported_in_browser"}}</strong> <strong>{{ctx.Locale.Tr "repo.audio_not_supported_in_browser"}}</strong>
</audio> </audio>
{{else}} {{else}}
<a href="{{$.RawFileLink}}" rel="nofollow noopener noreferrer" target="_blank" class="tw-p-4">{{ctx.Locale.Tr "repo.file_view_raw"}}</a> <a href="{{$.RawFileLink}}" rel="nofollow" class="tw-p-4">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
{{end}} {{end}}
</div> </div>
{{else if .FileSize}} {{else if .FileSize}}
+148
View File
@@ -0,0 +1,148 @@
{{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings manifest")}}
<h4 class="ui top attached header">
{{ctx.Locale.Tr "repo.settings.manifest"}}
</h4>
<div class="ui attached segment">
<p class="text grey">{{ctx.Locale.Tr "repo.settings.manifest_desc"}}</p>
<form class="ui form" method="post" action="{{.RepoLink}}/settings/manifest">
{{.CsrfTokenHtml}}
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.manifest_identity"}}</h5>
<div class="two fields">
<div class="field">
{{if eq .Manifest.Platform "joomla"}}
<label>{{ctx.Locale.Tr "repo.settings.manifest_element_name"}}</label>
<input name="name" value="{{.Manifest.Name}}" placeholder="e.g. mokowaas">
<p class="help">{{ctx.Locale.Tr "repo.settings.manifest_element_name_help"}}</p>
{{else}}
<label>{{ctx.Locale.Tr "repo.settings.manifest_name"}}</label>
<input name="name" value="{{.Manifest.Name}}" placeholder="Project name">
{{end}}
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_org"}}</label>
<input name="org" value="{{.Manifest.Org}}" placeholder="Organization">
</div>
</div>
<div class="four fields">
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_version"}}</label>
<input name="version" value="{{.Manifest.Version}}" placeholder="e.g. 06.00.00">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_version_prefix"}}</label>
<input name="version_prefix" value="{{.Manifest.VersionPrefix}}" placeholder="e.g. v1.26.1-moko.">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_license_spdx"}}</label>
<input name="license_spdx" value="{{.Manifest.LicenseSPDX}}" placeholder="e.g. GPL-3.0-or-later">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_license_name"}}</label>
<input name="license_name" value="{{.Manifest.LicenseName}}" placeholder="e.g. GNU General Public License v3">
</div>
</div>
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.manifest_governance"}}</h5>
<div class="three fields">
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_platform"}}</label>
<select name="platform" class="ui dropdown">
<option value="">—</option>
{{$platform := .Manifest.Platform}}
{{range $val := StringUtils.Split "joomla,wordpress,dolibarr,go,mcp,platform,generic" ","}}
<option value="{{$val}}" {{if eq $val $platform}}selected{{end}}>{{$val}}</option>
{{end}}
</select>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_standards_version"}}</label>
<input name="standards_version" value="{{.Manifest.StandardsVersion}}" placeholder="e.g. 05.00.00">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_standards_source"}}</label>
<input name="standards_source" value="{{.Manifest.StandardsSource}}" placeholder="URL to standards repo">
</div>
</div>
{{if or (eq .Manifest.Platform "joomla") (eq .Manifest.Platform "wordpress") (eq .Manifest.Platform "dolibarr")}}
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.manifest_distribution"}}</h5>
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_display_name"}}</label>
<input name="display_name" value="{{.Manifest.DisplayName}}" placeholder="e.g. Package - MokoWaaS">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_info_url"}}</label>
<input name="info_url" value="{{.Manifest.InfoURL}}" placeholder="https://mokoconsulting.tech/product/...">
</div>
</div>
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_maintainer"}}</label>
<input name="maintainer" value="{{.Manifest.Maintainer}}" placeholder="Moko Consulting">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_maintainer_url"}}</label>
<input name="maintainer_url" value="{{.Manifest.MaintainerURL}}" placeholder="https://mokoconsulting.tech">
</div>
</div>
{{if or (eq .Manifest.Platform "joomla") (eq .Manifest.Platform "wordpress")}}
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_target_version"}}</label>
<input name="target_version" value="{{.Manifest.TargetVersion}}" placeholder="e.g. (5|6)\..*">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_php_minimum"}}</label>
<input name="php_minimum" value="{{.Manifest.PHPMinimum}}" placeholder="e.g. 8.1">
</div>
</div>
{{end}}
{{end}}
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.manifest_build"}}</h5>
<div class="three fields">
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_language"}}</label>
<select name="language" class="ui dropdown">
<option value="">—</option>
{{$lang := .Manifest.Language}}
{{range $val := StringUtils.Split "Go,PHP,TypeScript,JavaScript,Python,Ruby,Java,C#,Rust,Shell,SQL,CSS,HTML" ","}}
<option value="{{$val}}" {{if eq $val $lang}}selected{{end}}>{{$val}}</option>
{{end}}
</select>
</div>
{{if eq .Manifest.Platform "joomla"}}
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_package_type"}}</label>
<select name="package_type" class="ui dropdown">
<option value="">—</option>
{{$pkgType := .Manifest.PackageType}}
{{range $val := StringUtils.Split "component,module,plugin,package,template,library,file" ","}}
<option value="{{$val}}" {{if eq $val $pkgType}}selected{{end}}>{{$val}}</option>
{{end}}
</select>
<p class="help">{{ctx.Locale.Tr "repo.settings.manifest_package_type_help"}}</p>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_element_full"}}</label>
<input name="element_name" value="{{.Manifest.ElementName}}" placeholder="{{.Manifest.AutoElementName}}">
{{if .Manifest.ElementNameMismatch}}
<p class="help tw-text-yellow-600">{{ctx.Locale.Tr "repo.settings.manifest_element_mismatch" .Manifest.AutoElementName}}</p>
{{else}}
<p class="help">{{ctx.Locale.Tr "repo.settings.manifest_element_full_help"}}</p>
{{end}}
</div>
{{end}}
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_entry_point"}}</label>
<input name="entry_point" value="{{.Manifest.EntryPoint}}" placeholder="e.g. ./ or src/index.ts">
</div>
</div>
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "repo.settings.manifest_save"}}</button>
</form>
</div>
{{template "repo/settings/layout_footer" .}}
+44 -148
View File
@@ -1,153 +1,49 @@
{{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings metadata")}} {{template "repo/settings/layout_head" (dict "pageClass" "repository settings metadata")}}
<h4 class="ui top attached header"> <div class="user-main-content twelve wide column">
{{svg "octicon-file-code" 16}} Project Identity <h4 class="ui top attached header">
</h4> {{svg "octicon-list-unordered" 16}} {{ctx.Locale.Tr "repo.settings.metadata"}}
<div class="ui attached segment"> </h4>
<form class="ui form" method="post" action="{{.RepoLink}}/settings/metadata?action=manifest"> <div class="ui attached segment">
{{.CsrfTokenHtml}} {{if .CustomFieldDefs}}
<form class="ui form" method="post" action="{{.RepoLink}}/settings/metadata">
<div class="two fields"> {{.CsrfTokenHtml}}
<div class="field"> {{$values := .CustomFieldValues}}
{{if eq .Manifest.Platform "joomla"}} {{$options := .CustomFieldOptions}}
<label>Element Name</label> {{range .CustomFieldDefs}}
<input name="name" value="{{.Manifest.Name}}" placeholder="e.g. mokowaas"> {{$currentVal := index $values .ID}}
{{else}} <div class="field">
<label>Project Name</label> <label>{{.Name}}{{if .Description}} <small class="text grey">({{.Description}})</small>{{end}}</label>
<input name="name" value="{{.Manifest.Name}}" placeholder="Project name"> {{if .Options}}
{{end}} {{$opts := index $options .ID}}
</div> <select name="field_{{.ID}}" class="ui dropdown">
<div class="field"> <option value="">—</option>
<label>Organization</label> {{range $opts}}
<input name="org" value="{{.Manifest.Org}}" placeholder="Organization"> <option value="{{.}}" {{if eq . $currentVal}}selected{{end}}>{{.}}</option>
</div> {{end}}
</div> </select>
<div class="four fields"> {{else if eq (printf "%s" .FieldType) "checkbox"}}
<div class="field"> <div class="ui checkbox">
<label>Version</label> <input type="checkbox" name="field_{{.ID}}" value="true" {{if eq $currentVal "true"}}checked{{end}}>
<input name="version" value="{{.Manifest.Version}}" placeholder="e.g. 06.00.00"> <label></label>
</div> </div>
<div class="field"> {{else if eq (printf "%s" .FieldType) "number"}}
<label>Version Prefix</label> <input type="number" name="field_{{.ID}}" value="{{$currentVal}}">
<input name="version_prefix" value="{{.Manifest.VersionPrefix}}" placeholder="e.g. v1.26.1-moko."> {{else if eq (printf "%s" .FieldType) "url"}}
</div> <input type="url" name="field_{{.ID}}" value="{{$currentVal}}" placeholder="https://...">
<div class="field"> {{else if eq (printf "%s" .FieldType) "date"}}
<label>License</label> <input type="date" name="field_{{.ID}}" value="{{$currentVal}}">
<select name="license_spdx" class="ui dropdown"> {{else}}
<option value="">—</option> <input type="text" name="field_{{.ID}}" value="{{$currentVal}}">
{{$lic := .Manifest.LicenseSPDX}}
<option value="GPL-3.0-or-later" {{if eq $lic "GPL-3.0-or-later"}}selected{{end}}>GPL-3.0-or-later</option>
<option value="GPL-2.0-or-later" {{if eq $lic "GPL-2.0-or-later"}}selected{{end}}>GPL-2.0-or-later</option>
<option value="MIT" {{if eq $lic "MIT"}}selected{{end}}>MIT</option>
<option value="Apache-2.0" {{if eq $lic "Apache-2.0"}}selected{{end}}>Apache-2.0</option>
<option value="BSD-3-Clause" {{if eq $lic "BSD-3-Clause"}}selected{{end}}>BSD-3-Clause</option>
<option value="BSD-2-Clause" {{if eq $lic "BSD-2-Clause"}}selected{{end}}>BSD-2-Clause</option>
<option value="LGPL-3.0-or-later" {{if eq $lic "LGPL-3.0-or-later"}}selected{{end}}>LGPL-3.0-or-later</option>
<option value="MPL-2.0" {{if eq $lic "MPL-2.0"}}selected{{end}}>MPL-2.0</option>
<option value="ISC" {{if eq $lic "ISC"}}selected{{end}}>ISC</option>
<option value="AGPL-3.0-or-later" {{if eq $lic "AGPL-3.0-or-later"}}selected{{end}}>AGPL-3.0-or-later</option>
<option value="Unlicense" {{if eq $lic "Unlicense"}}selected{{end}}>Unlicense</option>
<option value="proprietary" {{if eq $lic "proprietary"}}selected{{end}}>Proprietary</option>
</select>
</div>
</div>
<h5 class="ui dividing header">Governance</h5>
<div class="two fields">
<div class="field">
<label>Platform</label>
<select name="platform" class="ui dropdown">
<option value="">—</option>
{{$platform := .Manifest.Platform}}
{{range $val := StringUtils.Split "joomla,wordpress,dolibarr,go,mcp,platform,generic" ","}}
<option value="{{$val}}" {{if eq $val $platform}}selected{{end}}>{{$val}}</option>
{{end}}
</select>
</div>
<div class="field">
<label>Info URL</label>
<input name="info_url" value="{{.Manifest.InfoURL}}" placeholder="https://mokoconsulting.tech/product/...">
</div>
</div>
{{if or (eq .Manifest.Platform "joomla") (eq .Manifest.Platform "wordpress")}}
<div class="two fields">
<div class="field">
<label>Target Platform Version</label>
<input name="target_version" value="{{.Manifest.TargetVersion}}" placeholder="e.g. (5|6)\..*">
</div>
<div class="field">
<label>PHP Minimum</label>
<input name="php_minimum" value="{{.Manifest.PHPMinimum}}" placeholder="e.g. 8.1">
</div>
</div>
{{end}}
{{if eq .Manifest.Platform "joomla"}}
<h5 class="ui dividing header">Build</h5>
<div class="two fields">
<div class="field">
<label>Extension Type</label>
<select name="package_type" class="ui dropdown">
<option value="">—</option>
{{$pkgType := .Manifest.PackageType}}
{{range $val := StringUtils.Split "component,module,plugin,package,template,library,file" ","}}
<option value="{{$val}}" {{if eq $val $pkgType}}selected{{end}}>{{$val}}</option>
{{end}}
</select>
</div>
<div class="field">
<label>Entry Point</label>
<input name="entry_point" value="{{.Manifest.EntryPoint}}" placeholder="e.g. ./ or src/index.ts">
</div>
</div>
{{end}}
<button class="ui primary button" type="submit">Save Project Identity</button>
</form>
</div>
{{if .CustomFieldDefs}}
<h4 class="ui top attached header">
{{svg "octicon-list-unordered" 16}} Custom Fields
</h4>
<div class="ui attached segment">
<form class="ui form" method="post" action="{{.RepoLink}}/settings/metadata?action=customfields">
{{.CsrfTokenHtml}}
{{$values := .CustomFieldValues}}
{{$options := .CustomFieldOptions}}
{{range .CustomFieldDefs}}
{{$currentVal := index $values .ID}}
<div class="field">
<label>{{.Name}}{{if .Required}} <span class="tw-text-red">*</span>{{end}}{{if .Description}} <small class="text grey">({{.Description}})</small>{{end}}</label>
{{if .Options}}
{{$opts := index $options .ID}}
<select name="field_{{.ID}}" class="ui dropdown" {{if .Required}}required{{end}}>
<option value="">—</option>
{{range $opts}}
<option value="{{.}}" {{if eq . $currentVal}}selected{{end}}>{{.}}</option>
{{end}} {{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> </div>
{{else if eq (printf "%s" .FieldType) "number"}}
<input type="number" name="field_{{.ID}}" value="{{$currentVal}}" {{if .Required}}required{{end}}>
{{else if eq (printf "%s" .FieldType) "url"}}
<input type="url" name="field_{{.ID}}" value="{{$currentVal}}" placeholder="https://..." {{if .Required}}required{{end}}>
{{else if eq (printf "%s" .FieldType) "date"}}
<input type="date" name="field_{{.ID}}" value="{{$currentVal}}" {{if .Required}}required{{end}}>
{{else}}
<input type="text" name="field_{{.ID}}" value="{{$currentVal}}" {{if .Required}}required{{end}}>
{{end}} {{end}}
</div> <div class="field tw-mt-4">
{{end}} <button class="ui primary button" type="submit">{{ctx.Locale.Tr "save"}}</button>
<div class="field tw-mt-4"> </div>
<button class="ui primary button" type="submit">Save Custom Fields</button> </form>
{{else}}
<p class="text grey">{{ctx.Locale.Tr "repo.settings.metadata_empty"}}</p>
{{end}}
</div> </div>
</form> </div>
</div>
{{end}}
{{template "repo/settings/layout_footer" .}} {{template "repo/settings/layout_footer" .}}
+8 -5
View File
@@ -7,14 +7,17 @@
<a class="{{if .PageIsSettingsAdvanced}}active {{end}}item" href="{{.RepoLink}}/settings/advanced"> <a class="{{if .PageIsSettingsAdvanced}}active {{end}}item" href="{{.RepoLink}}/settings/advanced">
{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings.advanced_settings"}} {{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings.advanced_settings"}}
</a> </a>
<a class="{{if .PageIsSettingsMetadata}}active {{end}}item" href="{{.RepoLink}}/settings/metadata">
{{svg "octicon-file-code"}} Metadata
</a>
{{if .LicensingEnabled}} {{if .LicensingEnabled}}
<a class="{{if .PageIsSettingsUpdateServer}}active {{end}}item" href="{{.RepoLink}}/settings/updateserver"> <a class="{{if .PageIsSettingsLicensing}}active {{end}}item" href="{{.RepoLink}}/settings/licensing">
{{svg "octicon-broadcast"}} Update Server {{svg "octicon-broadcast"}} {{ctx.Locale.Tr "repo.settings.licensing_section"}}
</a> </a>
{{end}} {{end}}
<a class="{{if .PageIsSettingsManifest}}active {{end}}item" href="{{.RepoLink}}/settings/manifest">
{{svg "octicon-file-code"}} {{ctx.Locale.Tr "repo.settings.manifest"}}
</a>
<a class="{{if .PageIsSettingsMetadata}}active {{end}}item" href="{{.RepoLink}}/settings/metadata">
{{svg "octicon-list-unordered"}} {{ctx.Locale.Tr "repo.settings.metadata"}}
</a>
<a class="{{if .PageIsSettingsSecurity}}active {{end}}item" href="{{.RepoLink}}/settings/security"> <a class="{{if .PageIsSettingsSecurity}}active {{end}}item" href="{{.RepoLink}}/settings/security">
{{svg "octicon-shield"}} {{ctx.Locale.Tr "repo.settings.security"}} {{svg "octicon-shield"}} {{ctx.Locale.Tr "repo.settings.security"}}
</a> </a>
-51
View File
@@ -1,51 +0,0 @@
{{template "repo/settings/layout_head" (dict "pageClass" "repository settings updateserver")}}
<div class="user-main-content twelve wide column">
<h4 class="ui top attached header">
{{svg "octicon-broadcast" 16}} Update Server
</h4>
<div class="ui attached segment">
<form class="ui form" method="post" action="{{.RepoLink}}/settings/updateserver">
{{.CsrfTokenHtml}}
<div class="inline field">
<div class="ui checkbox">
<input name="enable_licensing" type="checkbox" {{if and .RepoUpdateConfig .RepoUpdateConfig.LicensingEnabled}}checked{{end}}>
<label><strong>Enable Update Server</strong></label>
</div>
<p class="help">Serve update feeds from releases and show the Licenses tab for optional key management.</p>
</div>
<div class="ui divider"></div>
<div class="inline field">
<label>Feed Visibility</label>
<select name="feed_visibility" class="ui dropdown">
<option value="public" {{if or (not .RepoUpdateConfig) (eq .RepoUpdateConfig.FeedVisibility "") (eq .RepoUpdateConfig.FeedVisibility "public")}}selected{{end}}>Public (show versions and downloads)</option>
<option value="no-download" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.FeedVisibility "no-download")}}selected{{end}}>No downloads (show versions only)</option>
<option value="hidden" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.FeedVisibility "hidden")}}selected{{end}}>Hidden (require license key)</option>
</select>
</div>
<div class="inline field">
<label>Download Gating</label>
<select name="download_gating" class="ui dropdown">
<option value="none" {{if or (not .RepoUpdateConfig) (eq .RepoUpdateConfig.DownloadGating "") (eq .RepoUpdateConfig.DownloadGating "none")}}selected{{end}}>None</option>
<option value="prerelease" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.DownloadGating "prerelease")}}selected{{end}}>Pre-release only</option>
<option value="all" {{if and .RepoUpdateConfig (eq .RepoUpdateConfig.DownloadGating "all")}}selected{{end}}>All releases</option>
</select>
</div>
<div class="inline field">
<div class="ui checkbox">
<input name="require_update_key" type="checkbox" {{if and .RepoUpdateConfig .RepoUpdateConfig.RequireKey}}checked{{end}}>
<label>Require license key for update feed</label>
</div>
</div>
<div class="field">
<button class="ui primary button">Save</button>
</div>
</form>
</div>
</div>
{{template "repo/settings/layout_footer" .}}
+2 -2
View File
@@ -46,7 +46,7 @@
</div> </div>
{{if not .ReadmeInList}} {{if not .ReadmeInList}}
<div class="ui buttons tw-mr-1"> <div class="ui buttons tw-mr-1">
<a class="ui mini basic button" href="{{$.RawFileLink}}" target="_blank" rel="noopener noreferrer">{{ctx.Locale.Tr "repo.file_raw"}}</a> <a class="ui mini basic button" href="{{$.RawFileLink}}">{{ctx.Locale.Tr "repo.file_raw"}}</a>
{{if or .RefFullName.IsBranch .RefFullName.IsTag}} {{if or .RefFullName.IsBranch .RefFullName.IsTag}}
<a class="ui mini basic button" href="{{.RepoLink}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}">{{ctx.Locale.Tr "repo.file_permalink"}}</a> <a class="ui mini basic button" href="{{.RepoLink}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}">{{ctx.Locale.Tr "repo.file_permalink"}}</a>
{{end}} {{end}}
@@ -132,7 +132,7 @@
{{else}} {{else}}
<div class="file-view-render-container"> <div class="file-view-render-container">
<div class="file-view-raw-prompt tw-p-4"> <div class="file-view-raw-prompt tw-p-4">
<a href="{{$.RawFileLink}}" rel="nofollow noopener noreferrer" target="_blank">{{ctx.Locale.Tr "repo.file_view_raw"}}</a> <a href="{{$.RawFileLink}}" rel="nofollow">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
</div> </div>
</div> </div>
{{end}} {{end}}
+1 -1
View File
@@ -1,4 +1,4 @@
<div class="file-not-rendered-prompt"> <div class="file-not-rendered-prompt">
{{ctx.Locale.Tr "repo.file_too_large"}} {{ctx.Locale.Tr "repo.file_too_large"}}
{{if .RawFileLink}}<a href="{{.RawFileLink}}" rel="nofollow noopener noreferrer" target="_blank">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>{{end}} {{if .RawFileLink}}<a href="{{.RawFileLink}}" rel="nofollow">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>{{end}}
</div> </div>