Compare commits
163 Commits
9eadade2f4
...
stable
| Author | SHA1 | Date | |
|---|---|---|---|
| f329466d1c | |||
| 05a89339e1 | |||
| 75e640dd17 | |||
| 09f17439ec | |||
| 9d45a767e7 | |||
| ece24c6d38 | |||
| e0d4f5fd15 | |||
| c67e7373fb | |||
| cbaca15cda | |||
| 245b5a8e6a | |||
| d61680d5b3 | |||
| c2687a2595 | |||
| 7161b9cdee | |||
| 553abbb9d0 | |||
| d66d96adaf | |||
| a8b3f45522 | |||
| a0ec6bc6b4 | |||
| 5893a8b9dd | |||
| 0f1002f3c9 | |||
| 3a7b07590f | |||
| 1ab5b8a70d | |||
| ea77b253a9 | |||
| 878cac5d99 | |||
| 7dfb11070d | |||
| 24a33fdd4d | |||
| 9b0eaf937a | |||
| 2c394870de | |||
| c1604e96cf | |||
| 998f971246 | |||
| 7f3785e7de | |||
| 7ed335e6c3 | |||
| 4ed6e0175d | |||
| a86350eebb | |||
| 97ea4fc4d0 | |||
| faef50ec4d | |||
| 9b9e5ae964 | |||
| 82a48d69cc | |||
| 4e5b7c7e65 | |||
| e1ca5cdfc4 | |||
| 704d9d10be | |||
| 549e890cd0 | |||
| 857c51e030 | |||
| d8c4f1efaf | |||
| d9cdaacb77 | |||
| 4a4873c733 | |||
| 633980bd05 | |||
| 4bd6be7935 | |||
| cefff4878c | |||
| 47ac97d284 | |||
| be6f3091af | |||
| aeb36e4312 | |||
| 2135f4c37c | |||
| d7bc3c3879 | |||
| e7e2c5f7a2 | |||
| 45a0338fc3 | |||
| ce5f3570fb | |||
| 5acf10f766 | |||
| e7fd70e0f2 | |||
| 34b1ef6638 | |||
| ce05f9f3c6 | |||
| 6f9d7ca03a | |||
| 1c7d43df38 | |||
| b2b31f6c7b | |||
| 5ba1d0b2e5 | |||
| 1caf26453f | |||
| 1e90900f69 | |||
| b762c94a25 | |||
| 6070f7dbd4 | |||
| 082c550bc4 | |||
| b1da31420f | |||
| 30d7ddc375 | |||
| 8fa56271de | |||
| 8535928a04 | |||
| d0853b874f | |||
| 0fe1d769ea | |||
| 18372c84a7 | |||
| c26ad626bd | |||
| 7a5c2d146f | |||
| d85ee80a4e | |||
| f48ca157a3 | |||
| 2eb2ed67bf | |||
| 3ae1265fb6 | |||
| 6a23c3e90d | |||
| 5f327f9da7 | |||
| 2ba1a6795f | |||
| 15e1149eb5 | |||
| ed91aa3392 | |||
| 8b4ea10e02 | |||
| ad4bac1162 | |||
| 983ce46278 | |||
| 7130c79317 | |||
| c3f1a5ab40 | |||
| 0b2646880e | |||
| 49274afa40 | |||
| f6578969e2 | |||
| 1ca4996307 | |||
| f86527994b | |||
| c016c603b4 | |||
| 51a10782e6 | |||
| 3cd6146fa0 | |||
| 9a51bf23d4 | |||
| 902ee39e90 | |||
| aae7b65329 | |||
| 35f9cd2882 | |||
| b8ad5398a3 | |||
| 09aa8d8201 | |||
| b6e88e4baf | |||
| ff9e7183d6 | |||
| d274aabb4f | |||
| 33ebcd7726 | |||
| 3361ce9f90 | |||
| bcfae6d370 | |||
| c44766106c | |||
| 6bb6e2ffd8 | |||
| 48c9759639 | |||
| 90fb6169d0 | |||
| 5f6d25ff7b | |||
| 9adcac546f | |||
| b6b4d6f525 | |||
| 74279c55e3 | |||
| 78803e60df | |||
| 75316bf80a | |||
| 37d59e7b59 | |||
| 18fc79fa0a | |||
| 931d685593 | |||
| 9121f1b36a | |||
| f3ce51d629 | |||
| 1f505b48c7 | |||
| 4b07ccc578 | |||
| afda7abcbe | |||
| 798d9c3ae0 | |||
| 4d42205cc8 | |||
| e0f22dd397 | |||
| ecda05aa46 | |||
| 3a492c5bd5 | |||
| c947ebcb49 | |||
| eef6292832 | |||
| 27aeb19dda | |||
| d1b2fca784 | |||
| 971c5fc7a7 | |||
| cd36065464 | |||
| 0debc72356 | |||
| 1ef6ef5fd4 | |||
| 62a44a3668 | |||
| 3c456dfe85 | |||
| 7b75ce9564 | |||
| 3abd239397 | |||
| 1e69927cec | |||
| c71e622e11 | |||
| 2ba5e42113 | |||
| 7240deb822 | |||
| 727cff9eb8 | |||
| 4c715d8424 | |||
| c0acdd1f58 | |||
| c73109e2e6 | |||
| 3a159b7da6 | |||
| 7c8b20b779 | |||
| 4e8af85178 | |||
| aa1a67c4cb | |||
| 5642057c80 | |||
| 4dd27ccdb8 | |||
| 71a7ab04e5 | |||
| d6dc7533ff |
@@ -1,73 +0,0 @@
|
||||
# Populates the go module, build, and golangci-lint caches under the default
|
||||
# branch's cache scope so that PR runs have a warm fallback to restore from.
|
||||
#
|
||||
# GitHub Actions caches are scoped per ref: a PR run can only write to its own
|
||||
# branch's scope, but can read from the base branch's scope as a fallback.
|
||||
# PRs therefore cannot seed main's scope themselves. Running the same cache
|
||||
# steps on push-to-main is the only opportunity to populate that fallback
|
||||
# scope so fresh PR branches start with a useful cache on first run.
|
||||
|
||||
# A PR job's exact key lives in its own PR-scope (empty on first run, filled
|
||||
# by later runs of the same PR); on miss, actions/cache's restore-keys fall
|
||||
# back to prefix matches against entries this seeder saves in main's scope.
|
||||
|
||||
name: cache-seeder
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "go.sum"
|
||||
- ".golangci.yml"
|
||||
- ".github/actions/go-cache/action.yml"
|
||||
- ".github/workflows/cache-seeder.yml"
|
||||
|
||||
concurrency:
|
||||
group: cache-seeder
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
gobuild:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
cache: false
|
||||
- uses: ./.github/actions/go-cache
|
||||
with:
|
||||
cache-name: seed
|
||||
- run: make deps-backend
|
||||
- run: TAGS="bindata" make backend
|
||||
- run: TAGS="bindata gogit" GOEXPERIMENT="" make backend
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { job: lint-backend, tags: "bindata", target: "lint-backend" }
|
||||
- { job: lint-go-windows, tags: "bindata", target: "lint-go-windows" }
|
||||
- { job: lint-go-gogit, tags: "bindata gogit", target: "lint-go" }
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
cache: false
|
||||
- uses: ./.github/actions/go-cache
|
||||
with:
|
||||
cache-name: ${{ matrix.job }}
|
||||
lint-cache: "true"
|
||||
- run: make deps-backend deps-tools
|
||||
- run: make ${{ matrix.target }}
|
||||
env:
|
||||
TAGS: ${{ matrix.tags }}
|
||||
@@ -1,31 +0,0 @@
|
||||
name: cron-licenses
|
||||
|
||||
on:
|
||||
# schedule:
|
||||
# - cron: "7 0 * * 1" # every Monday at 00:07 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
cron-licenses:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'go-gitea/gitea'
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
- run: make generate-gitignore
|
||||
timeout-minutes: 40
|
||||
- name: push translations to repo
|
||||
uses: appleboy/git-push-action@3b2c8661652360dbf1afe1b319a49dbb739c39f1 # v1.2.0
|
||||
with:
|
||||
author_email: "teabot@gitea.io"
|
||||
author_name: GiteaBot
|
||||
branch: main
|
||||
commit: true
|
||||
commit_message: "[skip ci] Updated licenses and gitignores"
|
||||
remote: "git@github.com:go-gitea/gitea.git"
|
||||
ssh_key: ${{ secrets.DEPLOY_KEY }}
|
||||
@@ -1,32 +0,0 @@
|
||||
name: cron-renovate
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "23 * * * *" # hourly at :23
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: cron-renovate
|
||||
|
||||
env:
|
||||
RENOVATE_VERSION: 43.141.5 # renovate: datasource=docker depName=ghcr.io/renovatebot/renovate
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
cron-renovate:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'go-gitea/gitea' # prevent running on forks
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: renovatebot/github-action@f66d8679fcfcfa051abde6e7a623007173bf5164 # v46.1.12
|
||||
with:
|
||||
renovate-version: ${{ env.RENOVATE_VERSION }}
|
||||
configurationFile: renovate.json5
|
||||
token: ${{ secrets.RENOVATE_TOKEN }}
|
||||
env:
|
||||
RENOVATE_BINARY_SOURCE: install # auto-install go/node toolchains needed by post-upgrade tasks.
|
||||
RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '["^make (tidy|svg nolyfill)$"]'
|
||||
RENOVATE_REPOSITORIES: '["go-gitea/gitea"]'
|
||||
@@ -1,40 +0,0 @@
|
||||
name: cron-translations
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "7 0 * * *" # every day at 00:07 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
crowdin-pull:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'go-gitea/gitea'
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
download_sources: false
|
||||
download_translations: true
|
||||
push_translations: false
|
||||
push_sources: false
|
||||
create_pull_request: false
|
||||
config: crowdin.yml
|
||||
env:
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_KEY: ${{ secrets.CROWDIN_KEY }}
|
||||
- name: update locales
|
||||
run: ./build/update-locales.sh
|
||||
- name: push translations to repo
|
||||
uses: appleboy/git-push-action@3b2c8661652360dbf1afe1b319a49dbb739c39f1 # v1.2.0
|
||||
with:
|
||||
author_email: "teabot@gitea.io"
|
||||
author_name: GiteaBot
|
||||
branch: main
|
||||
commit: true
|
||||
commit_message: "[skip ci] Updated translations via Crowdin"
|
||||
remote: "git@github.com:go-gitea/gitea.git"
|
||||
ssh_key: ${{ secrets.DEPLOY_KEY }}
|
||||
@@ -1,125 +0,0 @@
|
||||
name: files-changed
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
backend:
|
||||
value: ${{ jobs.detect.outputs.backend }}
|
||||
frontend:
|
||||
value: ${{ jobs.detect.outputs.frontend }}
|
||||
docs:
|
||||
value: ${{ jobs.detect.outputs.docs }}
|
||||
actions:
|
||||
value: ${{ jobs.detect.outputs.actions }}
|
||||
templates:
|
||||
value: ${{ jobs.detect.outputs.templates }}
|
||||
docker:
|
||||
value: ${{ jobs.detect.outputs.docker }}
|
||||
dockerfile:
|
||||
value: ${{ jobs.detect.outputs.dockerfile }}
|
||||
swagger:
|
||||
value: ${{ jobs.detect.outputs.swagger }}
|
||||
yaml:
|
||||
value: ${{ jobs.detect.outputs.yaml }}
|
||||
json:
|
||||
value: ${{ jobs.detect.outputs.json }}
|
||||
e2e:
|
||||
value: ${{ jobs.detect.outputs.e2e }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
detect:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 3
|
||||
outputs:
|
||||
backend: ${{ steps.changes.outputs.backend }}
|
||||
frontend: ${{ steps.changes.outputs.frontend }}
|
||||
docs: ${{ steps.changes.outputs.docs }}
|
||||
actions: ${{ steps.changes.outputs.actions }}
|
||||
templates: ${{ steps.changes.outputs.templates }}
|
||||
docker: ${{ steps.changes.outputs.docker }}
|
||||
dockerfile: ${{ steps.changes.outputs.dockerfile }}
|
||||
swagger: ${{ steps.changes.outputs.swagger }}
|
||||
yaml: ${{ steps.changes.outputs.yaml }}
|
||||
json: ${{ steps.changes.outputs.json }}
|
||||
e2e: ${{ steps.changes.outputs.e2e }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
id: changes
|
||||
with:
|
||||
filters: |
|
||||
backend:
|
||||
- "**/*.go"
|
||||
- "templates/**/*.tmpl"
|
||||
- "assets/emoji.json"
|
||||
- "go.mod"
|
||||
- "go.sum"
|
||||
- "Makefile"
|
||||
- ".golangci.yml"
|
||||
- ".editorconfig"
|
||||
- "options/locale/locale_en-US.json"
|
||||
|
||||
frontend:
|
||||
- "*.js"
|
||||
- "*.ts"
|
||||
- "web_src/**"
|
||||
- "tools/*.js"
|
||||
- "tools/*.ts"
|
||||
- "assets/emoji.json"
|
||||
- "package.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "Makefile"
|
||||
- ".eslintrc.cjs"
|
||||
- ".npmrc"
|
||||
|
||||
docs:
|
||||
- "**/*.md"
|
||||
- ".markdownlint.yaml"
|
||||
- "package.json"
|
||||
- "pnpm-lock.yaml"
|
||||
|
||||
actions:
|
||||
- ".github/workflows/*"
|
||||
- "Makefile"
|
||||
|
||||
templates:
|
||||
- "tools/lint-templates-*.js"
|
||||
- "templates/**/*.tmpl"
|
||||
- "pyproject.toml"
|
||||
- "uv.lock"
|
||||
|
||||
docker:
|
||||
- ".github/workflows/pull-docker-dryrun.yml"
|
||||
- "Dockerfile"
|
||||
- "Dockerfile.rootless"
|
||||
- "docker/**"
|
||||
- "Makefile"
|
||||
|
||||
dockerfile:
|
||||
- "Dockerfile"
|
||||
- "Dockerfile.rootless"
|
||||
|
||||
swagger:
|
||||
- "templates/swagger/v1_json.tmpl"
|
||||
- "templates/swagger/v1_input.json"
|
||||
- "Makefile"
|
||||
- "package.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- ".spectral.yaml"
|
||||
|
||||
yaml:
|
||||
- "**/*.yml"
|
||||
- "**/*.yaml"
|
||||
- ".yamllint.yaml"
|
||||
- "pyproject.toml"
|
||||
|
||||
json:
|
||||
- "**/*.json"
|
||||
|
||||
e2e:
|
||||
- "tests/e2e/**"
|
||||
- "tools/test-e2e.sh"
|
||||
- "playwright.config.ts"
|
||||
@@ -1,178 +0,0 @@
|
||||
name: compliance
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
uses: ./.github/workflows/files-changed.yml
|
||||
|
||||
lint-backend:
|
||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
cache: false
|
||||
- uses: ./.github/actions/go-cache
|
||||
with:
|
||||
cache-name: lint-backend
|
||||
lint-cache: "true"
|
||||
- run: make deps-backend deps-tools
|
||||
- run: make lint-backend
|
||||
env:
|
||||
TAGS: bindata
|
||||
|
||||
lint-on-demand:
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
cache: false
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
|
||||
- run: make lint-spell
|
||||
|
||||
- if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true'
|
||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
- if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true'
|
||||
run: uv python install 3.14 && make deps-py lint-templates lint-yaml
|
||||
|
||||
- if: needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.swagger == 'true' || needs.files-changed.outputs.json == 'true'
|
||||
run: make deps-frontend lint-md lint-swagger lint-json
|
||||
|
||||
- if: needs.files-changed.outputs.actions == 'true'
|
||||
run: make lint-actions
|
||||
|
||||
lint-go-windows:
|
||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
cache: false
|
||||
- uses: ./.github/actions/go-cache
|
||||
with:
|
||||
cache-name: lint-go-windows
|
||||
lint-cache: "true"
|
||||
- run: make deps-backend deps-tools
|
||||
- run: make lint-go-windows
|
||||
env:
|
||||
TAGS: bindata
|
||||
GOOS: windows
|
||||
GOARCH: amd64
|
||||
|
||||
lint-go-gogit:
|
||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
cache: false
|
||||
- uses: ./.github/actions/go-cache
|
||||
with:
|
||||
cache-name: lint-go-gogit
|
||||
lint-cache: "true"
|
||||
- run: make deps-backend deps-tools
|
||||
- run: make lint-go
|
||||
env:
|
||||
TAGS: bindata gogit
|
||||
|
||||
checks-backend:
|
||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
cache: false
|
||||
- uses: ./.github/actions/go-cache
|
||||
with:
|
||||
cache-name: checks-backend
|
||||
build-cache: "false"
|
||||
- run: make deps-backend deps-tools
|
||||
- run: make --always-make checks-backend # ensure the "go-licenses" make target runs
|
||||
|
||||
frontend:
|
||||
if: needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
- run: make deps-frontend
|
||||
- run: make lint-frontend
|
||||
- run: make checks-frontend
|
||||
- run: make test-frontend
|
||||
- run: make frontend
|
||||
|
||||
backend:
|
||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
cache: false
|
||||
- uses: ./.github/actions/go-cache
|
||||
with:
|
||||
cache-name: compliance-backend
|
||||
- run: make deps-backend generate-go
|
||||
# no frontend build here as backend should be able to build, even without any frontend files
|
||||
# CGO is not used when cross-compile, so these steps also test if the code is compatible with CGO disabled
|
||||
- name: build-backend-arm64
|
||||
run: go build -o gitea_linux_arm64
|
||||
env:
|
||||
GOOS: linux
|
||||
GOARCH: arm64
|
||||
TAGS: bindata gogit
|
||||
- name: build-backend-windows
|
||||
run: go build -o gitea_windows
|
||||
env:
|
||||
GOOS: windows
|
||||
GOARCH: amd64
|
||||
TAGS: bindata gogit
|
||||
- name: build-backend-386
|
||||
run: go build -o gitea_linux_386
|
||||
env:
|
||||
GOOS: linux
|
||||
GOARCH: 386
|
||||
@@ -1,262 +0,0 @@
|
||||
name: db-tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
uses: ./.github/workflows/files-changed.yml
|
||||
|
||||
test-pgsql:
|
||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
pgsql:
|
||||
image: postgres:14
|
||||
env:
|
||||
POSTGRES_DB: test
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
- "5432:5432"
|
||||
ldap:
|
||||
image: gitea/test-openldap:latest
|
||||
ports:
|
||||
- "389:389"
|
||||
- "636:636"
|
||||
minio:
|
||||
# as github actions doesn't support "entrypoint", we need to use a non-official image
|
||||
# that has a custom entrypoint set to "minio server /data"
|
||||
image: bitnamilegacy/minio:2023.12.23
|
||||
env:
|
||||
MINIO_ROOT_USER: 123456
|
||||
MINIO_ROOT_PASSWORD: 12345678
|
||||
ports:
|
||||
- "9000:9000"
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
cache: false
|
||||
- uses: ./.github/actions/go-cache
|
||||
with:
|
||||
cache-name: pgsql
|
||||
- name: Add hosts to /etc/hosts
|
||||
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 pgsql ldap minio" | sudo tee -a /etc/hosts'
|
||||
- run: make deps-backend
|
||||
- run: make backend
|
||||
env:
|
||||
TAGS: bindata
|
||||
- name: run migration tests
|
||||
run: GITEA_TEST_DATABASE=pgsql make test-migration
|
||||
- name: run tests
|
||||
run: GITEA_TEST_DATABASE=pgsql make test-integration
|
||||
timeout-minutes: 50
|
||||
env:
|
||||
# pgsql is chosen to be the unlucky one to run with the slow "race detector", it is about 60% slower.
|
||||
GOTEST_FLAGS: -race -timeout=40m
|
||||
TAGS: bindata gogit
|
||||
TEST_LDAP: 1
|
||||
|
||||
test-sqlite:
|
||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
cache: false
|
||||
- uses: ./.github/actions/go-cache
|
||||
with:
|
||||
cache-name: sqlite
|
||||
- run: make deps-backend
|
||||
- run: make backend
|
||||
env:
|
||||
TAGS: bindata gogit
|
||||
GOEXPERIMENT:
|
||||
- name: run migration tests
|
||||
run: GITEA_TEST_DATABASE=sqlite make test-migration
|
||||
env:
|
||||
TAGS: bindata gogit
|
||||
- name: run tests
|
||||
run: GITEA_TEST_DATABASE=sqlite make test-integration
|
||||
timeout-minutes: 50
|
||||
env:
|
||||
# sqlite driver can contain large amount of Golang code, so don't use race detector for it, otherwise, extremely slow
|
||||
GOTEST_FLAGS: -timeout=40m
|
||||
TAGS: bindata gogit
|
||||
GOEXPERIMENT:
|
||||
|
||||
test-unit:
|
||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
elasticsearch:
|
||||
image: docker.elastic.co/elasticsearch/elasticsearch:8.19.14
|
||||
env:
|
||||
discovery.type: single-node
|
||||
xpack.security.enabled: false
|
||||
ports:
|
||||
- "9200:9200"
|
||||
meilisearch:
|
||||
image: getmeili/meilisearch:v1
|
||||
env:
|
||||
MEILI_ENV: development # disable auth
|
||||
ports:
|
||||
- "7700:7700"
|
||||
redis:
|
||||
image: redis
|
||||
options: >- # wait until redis has started
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 5s
|
||||
--health-timeout 3s
|
||||
--health-retries 10
|
||||
ports:
|
||||
- 6379:6379
|
||||
minio:
|
||||
image: bitnamilegacy/minio:2021.12.29
|
||||
env:
|
||||
MINIO_ACCESS_KEY: 123456
|
||||
MINIO_SECRET_KEY: 12345678
|
||||
ports:
|
||||
- "9000:9000"
|
||||
devstoreaccount1.azurite.local: # https://github.com/Azure/Azurite/issues/1583
|
||||
image: mcr.microsoft.com/azure-storage/azurite:latest
|
||||
ports:
|
||||
- 10000:10000
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
cache: false
|
||||
- uses: ./.github/actions/go-cache
|
||||
with:
|
||||
cache-name: unit
|
||||
build-cache-rotate: "true"
|
||||
- name: Add hosts to /etc/hosts
|
||||
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 minio devstoreaccount1.azurite.local mysql elasticsearch meilisearch smtpimap" | sudo tee -a /etc/hosts'
|
||||
- run: make deps-backend
|
||||
- run: make backend
|
||||
env:
|
||||
TAGS: bindata
|
||||
- name: unit-tests
|
||||
run: make test-backend test-check
|
||||
env:
|
||||
GOTEST_FLAGS: -race -timeout=20m
|
||||
TAGS: bindata
|
||||
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
|
||||
- name: unit-tests-gogit
|
||||
run: make test-backend test-check
|
||||
env:
|
||||
GOTEST_FLAGS: -race -timeout=20m
|
||||
TAGS: bindata gogit
|
||||
GOEXPERIMENT:
|
||||
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
|
||||
|
||||
test-mysql:
|
||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
mysql:
|
||||
# the bitnami mysql image has more options than the official one, it's easier to customize
|
||||
image: bitnamilegacy/mysql:8.4
|
||||
env:
|
||||
ALLOW_EMPTY_PASSWORD: true
|
||||
MYSQL_DATABASE: testgitea
|
||||
ports:
|
||||
- "3306:3306"
|
||||
options: >-
|
||||
--mount type=tmpfs,destination=/bitnami/mysql/data
|
||||
elasticsearch:
|
||||
image: docker.elastic.co/elasticsearch/elasticsearch:8.19.14
|
||||
env:
|
||||
discovery.type: single-node
|
||||
xpack.security.enabled: false
|
||||
ports:
|
||||
- "9200:9200"
|
||||
smtpimap:
|
||||
image: tabascoterrier/docker-imap-devel:latest
|
||||
ports:
|
||||
- "25:25"
|
||||
- "143:143"
|
||||
- "587:587"
|
||||
- "993:993"
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
cache: false
|
||||
- uses: ./.github/actions/go-cache
|
||||
with:
|
||||
cache-name: mysql
|
||||
- name: Add hosts to /etc/hosts
|
||||
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mysql elasticsearch smtpimap" | sudo tee -a /etc/hosts'
|
||||
- run: make deps-backend
|
||||
- run: make backend
|
||||
env:
|
||||
TAGS: bindata
|
||||
- name: run migration tests
|
||||
run: GITEA_TEST_DATABASE=mysql make test-migration
|
||||
- name: run tests
|
||||
run: GITEA_TEST_DATABASE=mysql make test-integration
|
||||
env:
|
||||
TAGS: bindata
|
||||
TEST_INDEXER_CODE_ES_URL: "http://elastic:changeme@elasticsearch:9200"
|
||||
|
||||
test-mssql:
|
||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
mssql:
|
||||
image: mcr.microsoft.com/mssql/server:2019-latest
|
||||
env:
|
||||
ACCEPT_EULA: Y
|
||||
MSSQL_PID: Standard
|
||||
SA_PASSWORD: MwantsaSecurePassword1
|
||||
ports:
|
||||
- "1433:1433"
|
||||
devstoreaccount1.azurite.local: # https://github.com/Azure/Azurite/issues/1583
|
||||
image: mcr.microsoft.com/azure-storage/azurite:latest
|
||||
ports:
|
||||
- 10000:10000
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
cache: false
|
||||
- uses: ./.github/actions/go-cache
|
||||
with:
|
||||
cache-name: mssql
|
||||
- name: Add hosts to /etc/hosts
|
||||
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mssql devstoreaccount1.azurite.local" | sudo tee -a /etc/hosts'
|
||||
- run: make deps-backend
|
||||
- run: make backend
|
||||
env:
|
||||
TAGS: bindata
|
||||
- run: GITEA_TEST_DATABASE=mssql make test-migration
|
||||
- name: run tests
|
||||
run: GITEA_TEST_DATABASE=mssql make test-integration
|
||||
timeout-minutes: 50
|
||||
env:
|
||||
TAGS: bindata
|
||||
@@ -1,47 +0,0 @@
|
||||
name: docker-dryrun
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
uses: ./.github/workflows/files-changed.yml
|
||||
|
||||
# QEMU-based build is slow (40-50 minutes), so run arm64 and riscv64 when dockerfile changes.
|
||||
# Run amd64 when any docker-related files change, which is fast (4 minutes).
|
||||
container-amd64:
|
||||
if: needs.files-changed.outputs.docker == 'true'
|
||||
needs: [files-changed]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: ./.github/actions/docker-dryrun
|
||||
with:
|
||||
platform: linux/amd64
|
||||
|
||||
container-arm64:
|
||||
if: needs.files-changed.outputs.dockerfile == 'true'
|
||||
needs: [files-changed]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: ./.github/actions/docker-dryrun
|
||||
with:
|
||||
platform: linux/arm64
|
||||
|
||||
container-riscv64:
|
||||
if: needs.files-changed.outputs.dockerfile == 'true'
|
||||
needs: [files-changed]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: ./.github/actions/docker-dryrun
|
||||
with:
|
||||
platform: linux/riscv64
|
||||
@@ -1,50 +0,0 @@
|
||||
name: e2e-tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
uses: ./.github/workflows/files-changed.yml
|
||||
|
||||
test-e2e:
|
||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.e2e == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
cache: false
|
||||
- uses: ./.github/actions/go-cache
|
||||
with:
|
||||
cache-name: e2e
|
||||
build-cache: "false"
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
- run: make deps-frontend
|
||||
- run: make frontend
|
||||
- run: make deps-backend
|
||||
- run: make backend
|
||||
env:
|
||||
TAGS: bindata
|
||||
- run: make playwright
|
||||
- run: make test-e2e
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
TAGS: bindata
|
||||
FORCE_COLOR: 1
|
||||
GITEA_TEST_E2E_DEBUG: 1
|
||||
@@ -1,20 +0,0 @@
|
||||
name: labeler
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
labeler:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
|
||||
with:
|
||||
sync-labels: true
|
||||
@@ -1,28 +0,0 @@
|
||||
name: pr-title
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint-pr-title:
|
||||
if: github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- run: make lint-pr-title
|
||||
env:
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
@@ -1,135 +0,0 @@
|
||||
name: release-nightly
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, release/v*]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
nightly-binary:
|
||||
runs-on: namespace-profile-gitea-release-binary
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
- run: make deps-frontend deps-backend
|
||||
# xgo build
|
||||
- run: make release
|
||||
env:
|
||||
TAGS: bindata
|
||||
- name: import gpg key
|
||||
id: import_gpg
|
||||
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
|
||||
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
|
||||
- name: sign binaries
|
||||
run: |
|
||||
for f in dist/release/*; do
|
||||
echo '${{ secrets.GPGSIGN_PASSPHRASE }}' | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u ${{ steps.import_gpg.outputs.fingerprint }} --output "$f.asc" "$f"
|
||||
done
|
||||
# clean branch name to get the folder name in S3
|
||||
- name: Get cleaned branch name
|
||||
id: clean_name
|
||||
run: |
|
||||
REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
|
||||
echo "Cleaned name is ${REF_NAME}"
|
||||
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
|
||||
- name: configure aws
|
||||
uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
|
||||
with:
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
- name: upload binaries to s3
|
||||
run: |
|
||||
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
|
||||
|
||||
nightly-container:
|
||||
runs-on: namespace-profile-gitea-release-docker
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write # to publish to ghcr.io
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
- uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
- name: Get cleaned branch name
|
||||
id: clean_name
|
||||
run: |
|
||||
REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
|
||||
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
|
||||
- uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
||||
id: meta
|
||||
with:
|
||||
images: |-
|
||||
gitea/gitea
|
||||
ghcr.io/go-gitea/gitea
|
||||
tags: |
|
||||
type=raw,value=${{ steps.clean_name.outputs.branch }}
|
||||
annotations: |
|
||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||
- uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
||||
id: meta_rootless
|
||||
with:
|
||||
images: |-
|
||||
gitea/gitea
|
||||
ghcr.io/go-gitea/gitea
|
||||
# each tag below will have the suffix of -rootless
|
||||
flavor: |
|
||||
suffix=-rootless
|
||||
tags: |
|
||||
type=raw,value=${{ steps.clean_name.outputs.branch }}
|
||||
annotations: |
|
||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GHCR using PAT
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: build regular docker image
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
|
||||
cache-to: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful,mode=max
|
||||
- name: build rootless docker image
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
push: true
|
||||
file: Dockerfile.rootless
|
||||
tags: ${{ steps.meta_rootless.outputs.tags }}
|
||||
annotations: ${{ steps.meta_rootless.outputs.annotations }}
|
||||
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootless
|
||||
cache-to: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootless,mode=max
|
||||
@@ -1,141 +0,0 @@
|
||||
name: release-tag-rc
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v1*-rc*"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
binary:
|
||||
runs-on: namespace-profile-gitea-release-binary
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
- run: make deps-frontend deps-backend
|
||||
# xgo build
|
||||
- run: make release
|
||||
env:
|
||||
TAGS: bindata
|
||||
- name: import gpg key
|
||||
id: import_gpg
|
||||
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
|
||||
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
|
||||
- name: sign binaries
|
||||
run: |
|
||||
for f in dist/release/*; do
|
||||
echo '${{ secrets.GPGSIGN_PASSPHRASE }}' | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u ${{ steps.import_gpg.outputs.fingerprint }} --output "$f.asc" "$f"
|
||||
done
|
||||
# clean branch name to get the folder name in S3
|
||||
- name: Get cleaned branch name
|
||||
id: clean_name
|
||||
run: |
|
||||
REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\/v//' -e 's/release\/v//')
|
||||
echo "Cleaned name is ${REF_NAME}"
|
||||
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||
- name: configure aws
|
||||
uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
|
||||
with:
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
- name: upload binaries to s3
|
||||
run: |
|
||||
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
|
||||
- name: Install GH CLI
|
||||
uses: dev-hanz-ops/install-gh-cli-action@af38ce09b1ec248aeb08eea2b16bbecea9e059f8 # v0.2.1
|
||||
with:
|
||||
gh-cli-version: 2.39.1
|
||||
- name: create github release
|
||||
run: |
|
||||
gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --draft --notes-from-tag dist/release/*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
|
||||
container:
|
||||
runs-on: namespace-profile-gitea-release-docker
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write # to publish to ghcr.io
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
- uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
- uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
||||
id: meta
|
||||
with:
|
||||
images: |-
|
||||
gitea/gitea
|
||||
ghcr.io/go-gitea/gitea
|
||||
flavor: |
|
||||
latest=false
|
||||
# 1.2.3-rc0
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
annotations: |
|
||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||
- uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
||||
id: meta_rootless
|
||||
with:
|
||||
images: |-
|
||||
gitea/gitea
|
||||
ghcr.io/go-gitea/gitea
|
||||
# each tag below will have the suffix of -rootless
|
||||
flavor: |
|
||||
latest=false
|
||||
suffix=-rootless
|
||||
# 1.2.3-rc0
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
annotations: |
|
||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GHCR using PAT
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: build regular container image
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
- name: build rootless container image
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
push: true
|
||||
file: Dockerfile.rootless
|
||||
tags: ${{ steps.meta_rootless.outputs.tags }}
|
||||
annotations: ${{ steps.meta_rootless.outputs.annotations }}
|
||||
@@ -1,153 +0,0 @@
|
||||
name: release-tag-version
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v1.*"
|
||||
- "!v1*-rc*"
|
||||
- "!v1*-dev"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
binary:
|
||||
runs-on: namespace-profile-gitea-release-binary
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write # to publish to ghcr.io
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
- run: make deps-frontend deps-backend
|
||||
# xgo build
|
||||
- run: make release
|
||||
env:
|
||||
TAGS: bindata
|
||||
- name: import gpg key
|
||||
id: import_gpg
|
||||
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
|
||||
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
|
||||
- name: sign binaries
|
||||
run: |
|
||||
for f in dist/release/*; do
|
||||
echo '${{ secrets.GPGSIGN_PASSPHRASE }}' | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u ${{ steps.import_gpg.outputs.fingerprint }} --output "$f.asc" "$f"
|
||||
done
|
||||
# clean branch name to get the folder name in S3
|
||||
- name: Get cleaned branch name
|
||||
id: clean_name
|
||||
run: |
|
||||
REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\/v//' -e 's/release\/v//')
|
||||
echo "Cleaned name is ${REF_NAME}"
|
||||
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||
- name: configure aws
|
||||
uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
|
||||
with:
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
- name: upload binaries to s3
|
||||
run: |
|
||||
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
|
||||
- name: Install GH CLI
|
||||
uses: dev-hanz-ops/install-gh-cli-action@af38ce09b1ec248aeb08eea2b16bbecea9e059f8 # v0.2.1
|
||||
with:
|
||||
gh-cli-version: 2.39.1
|
||||
- name: create github release
|
||||
run: |
|
||||
gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --notes-from-tag dist/release/*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
|
||||
container:
|
||||
runs-on: namespace-profile-gitea-release-docker
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write # to publish to ghcr.io
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
- uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
- uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
||||
id: meta
|
||||
with:
|
||||
images: |-
|
||||
gitea/gitea
|
||||
ghcr.io/go-gitea/gitea
|
||||
# this will generate tags in the following format:
|
||||
# latest
|
||||
# 1
|
||||
# 1.2
|
||||
# 1.2.3
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
annotations: |
|
||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||
- uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
||||
id: meta_rootless
|
||||
with:
|
||||
images: |-
|
||||
gitea/gitea
|
||||
ghcr.io/go-gitea/gitea
|
||||
# each tag below will have the suffix of -rootless
|
||||
flavor: |
|
||||
suffix=-rootless,onlatest=true
|
||||
# this will generate tags in the following format (with -rootless suffix added):
|
||||
# latest
|
||||
# 1
|
||||
# 1.2
|
||||
# 1.2.3
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
annotations: |
|
||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GHCR using PAT
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: build regular container image
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
- name: build rootless container image
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
push: true
|
||||
file: Dockerfile.rootless
|
||||
tags: ${{ steps.meta_rootless.outputs.tags }}
|
||||
annotations: ${{ steps.meta_rootless.outputs.annotations }}
|
||||
@@ -39,4 +39,4 @@ GITEA_TEST_E2E_FLAGS='<filepath>' make test-e2e # Single Playwright test
|
||||
- Add `Co-Authored-By` lines to all commits
|
||||
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
|
||||
- **Attribution**: `Authored-by: Moko Consulting`
|
||||
- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)
|
||||
- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/mokoplatform/wiki/Home)
|
||||
|
||||
@@ -8,7 +8,7 @@ contact_links:
|
||||
url: https://mokoconsulting.tech/
|
||||
about: Get help or ask questions through our website
|
||||
- name: 📚 MokoStandards Documentation
|
||||
url: https://code.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
url: https://code.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
about: View our coding standards and best practices
|
||||
- name: 🔒 Report a Security Vulnerability
|
||||
url: https://code.mokoconsulting.tech/mokoconsulting-tech/.github-private/security/advisories/new
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Automation
|
||||
# REPO: https://code.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# INGROUP: mokoplatform.Automation
|
||||
# REPO: https://code.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
# PATH: /.gitea/workflows/branch-protection.yml
|
||||
# BRIEF: Apply standardised branch protection rules to all governed repositories
|
||||
#
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
API="${GITEA_URL}/api/v1"
|
||||
|
||||
# Platform/standards/infra repos to exclude
|
||||
EXCLUDE="gitea-org-config org-profile gitea-private .mokogitea-private MokoStandards moko-platform MokoTesting"
|
||||
EXCLUDE="gitea-org-config org-profile gitea-private .mokogitea-private MokoStandards mokoplatform MokoTesting"
|
||||
EXCLUDE="$EXCLUDE MokoStandards-Template-Client MokoStandards-Template-Dolibarr MokoStandards-Template-Generic MokoStandards-Template-Joomla MokoDoliProjTemplate"
|
||||
|
||||
if [ -n "${{ inputs.repos }}" ]; then
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<moko-platform xmlns="https://standards.mokoconsulting.tech/moko-platform/1.0" schema-version="1.0">
|
||||
<mokoplatform xmlns="https://standards.mokoconsulting.tech/mokoplatform/1.0" schema-version="1.0">
|
||||
<identity>
|
||||
<name>MokoGitea</name>
|
||||
<org>MokoConsulting</org>
|
||||
<description>Moko fork of Gitea -- adding project board REST API endpoints and custom enhancements</description>
|
||||
<version>05.47.00</version>
|
||||
<description>Moko fork of Gitea - adding project board REST API endpoints and custom enhancements</description>
|
||||
<version>06.15.00</version>
|
||||
<version-prefix>v1.26.1+MOKO</version-prefix>
|
||||
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||
</identity>
|
||||
<governance>
|
||||
<platform>go</platform>
|
||||
<standards-version>05.00.00</standards-version>
|
||||
<standards-source>https://code.mokoconsulting.tech/MokoConsulting/moko-platform</standards-source>
|
||||
<standards-source>https://code.mokoconsulting.tech/MokoConsulting/mokoplatform</standards-source>
|
||||
</governance>
|
||||
<build>
|
||||
<language>Go</language>
|
||||
<package-type>application</package-type>
|
||||
<entry-point>./</entry-point>
|
||||
</build>
|
||||
</moko-platform>
|
||||
</mokoplatform>
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
DEFGROUP: gitea-api-mcp.Documentation
|
||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/gitea-api-mcp
|
||||
-->
|
||||
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
- **Renamed** package from `@mokoconsulting/gitea-api-mcp` to `@mokoconsulting/mokogitea-api-mcp` to distinguish Moko's forked Gitea MCP from upstream
|
||||
- **Renamed** McpServer name and bin entry to `mokogitea-api-mcp`
|
||||
|
||||
|
||||
## [0.0] - 2026-05-07
|
||||
|
||||
### Added
|
||||
|
||||
#### User / Auth (3 tools)
|
||||
- `gitea_me` -- Get the authenticated user info
|
||||
- `gitea_user_orgs` -- List organizations the authenticated user belongs to
|
||||
- `gitea_user_repos` -- List repositories owned by the authenticated user
|
||||
|
||||
#### Repositories (8 tools)
|
||||
- `gitea_repo_get` -- Get repository details
|
||||
- `gitea_repo_create` -- Create a new repository
|
||||
- `gitea_repo_delete` -- Delete a repository
|
||||
- `gitea_repo_edit` -- Edit repository settings
|
||||
- `gitea_repo_fork` -- Fork a repository
|
||||
- `gitea_repo_search` -- Search repositories
|
||||
- `gitea_org_repos` -- List repositories in an organization
|
||||
- `gitea_list_connections` -- List configured Gitea connections
|
||||
|
||||
#### File Contents (5 tools)
|
||||
- `gitea_file_get` -- Get file contents from a repository
|
||||
- `gitea_dir_get` -- Get directory contents (file listing) from a repository
|
||||
- `gitea_file_create_or_update` -- Create or update a file in a repository
|
||||
- `gitea_file_delete` -- Delete a file from a repository
|
||||
- `gitea_tree_get` -- Get the git tree for a repository (recursive file listing)
|
||||
|
||||
#### Branches (4 tools)
|
||||
- `gitea_branches_list` -- List branches in a repository
|
||||
- `gitea_branch_get` -- Get a specific branch
|
||||
- `gitea_branch_create` -- Create a new branch
|
||||
- `gitea_branch_delete` -- Delete a branch
|
||||
|
||||
#### Commits (2 tools)
|
||||
- `gitea_commits_list` -- List commits in a repository
|
||||
- `gitea_commit_get` -- Get a specific commit
|
||||
|
||||
#### Issues (7 tools)
|
||||
- `gitea_issues_list` -- List issues in a repository
|
||||
- `gitea_issue_get` -- Get a single issue by number
|
||||
- `gitea_issue_create` -- Create a new issue
|
||||
- `gitea_issue_update` -- Update an issue
|
||||
- `gitea_issue_comments_list` -- List comments on an issue
|
||||
- `gitea_issue_comment_create` -- Add a comment to an issue
|
||||
- `gitea_issue_search` -- Search issues across all repositories
|
||||
|
||||
#### Labels (2 tools)
|
||||
- `gitea_labels_list` -- List labels in a repository
|
||||
- `gitea_label_create` -- Create a label
|
||||
|
||||
#### Milestones (2 tools)
|
||||
- `gitea_milestones_list` -- List milestones in a repository
|
||||
- `gitea_milestone_create` -- Create a milestone
|
||||
|
||||
#### Pull Requests (6 tools)
|
||||
- `gitea_pulls_list` -- List pull requests
|
||||
- `gitea_pull_get` -- Get a single pull request
|
||||
- `gitea_pull_create` -- Create a pull request
|
||||
- `gitea_pull_merge` -- Merge a pull request
|
||||
- `gitea_pull_files` -- List files changed in a pull request
|
||||
- `gitea_pull_review_create` -- Create a pull request review
|
||||
|
||||
#### Releases (5 tools)
|
||||
- `gitea_releases_list` -- List releases
|
||||
- `gitea_release_get` -- Get a single release by ID
|
||||
- `gitea_release_latest` -- Get the latest release
|
||||
- `gitea_release_create` -- Create a new release
|
||||
- `gitea_release_delete` -- Delete a release
|
||||
|
||||
#### Tags (3 tools)
|
||||
- `gitea_tags_list` -- List tags
|
||||
- `gitea_tag_create` -- Create a tag
|
||||
- `gitea_tag_delete` -- Delete a tag
|
||||
|
||||
#### Actions (2 tools)
|
||||
- `gitea_actions_runs_list` -- List workflow runs for a repository
|
||||
- `gitea_actions_run_get` -- Get a specific workflow run
|
||||
|
||||
#### Organizations (3 tools)
|
||||
- `gitea_org_get` -- Get organization details
|
||||
- `gitea_org_teams_list` -- List teams in an organization
|
||||
- `gitea_org_members_list` -- List members of an organization
|
||||
|
||||
#### Users (2 tools)
|
||||
- `gitea_user_get` -- Get a user profile
|
||||
- `gitea_users_search` -- Search users
|
||||
|
||||
#### Webhooks (2 tools)
|
||||
- `gitea_webhooks_list` -- List webhooks for a repository
|
||||
- `gitea_webhook_create` -- Create a webhook
|
||||
|
||||
#### Wiki (2 tools)
|
||||
- `gitea_wiki_pages_list` -- List wiki pages
|
||||
- `gitea_wiki_page_get` -- Get a wiki page
|
||||
|
||||
#### Notifications (2 tools)
|
||||
- `gitea_notifications_list` -- List notifications for the authenticated user
|
||||
- `gitea_notifications_read` -- Mark all notifications as read
|
||||
|
||||
#### Generic (2 tools)
|
||||
- `gitea_api_request` -- Make a raw API request to any Gitea v1 endpoint
|
||||
- `gitea_list_connections` -- List configured Gitea connections
|
||||
|
||||
### Infrastructure
|
||||
- Multi-connection config support via `~/.gitea-api-mcp.json`
|
||||
- Token-based authentication (Gitea native `Authorization: token` header)
|
||||
- Built on `node:https` / `node:http` (zero HTTP dependencies)
|
||||
- MCP SDK v1.12.x with stdio transport
|
||||
|
||||
[0.0.1]: https://git.mokoconsulting.tech/MokoConsulting/gitea-api-mcp/releases/tag/v0.0.1
|
||||
@@ -1,18 +0,0 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci --production=false
|
||||
|
||||
COPY tsconfig.json ./
|
||||
COPY src/ ./src/
|
||||
RUN npx tsc && npm prune --production
|
||||
|
||||
EXPOSE 3100
|
||||
|
||||
ENV PORT=3100
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# SSE mode by default for Docker deployments
|
||||
CMD ["node", "dist/sse.js"]
|
||||
@@ -1,116 +0,0 @@
|
||||
# MokoGitea MCP Server
|
||||
|
||||
A comprehensive [Model Context Protocol](https://modelcontextprotocol.io) server for [Gitea](https://gitea.com) and [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea). 120+ tools for repos, issues, PRs, projects, releases, custom fields, statuses, priorities, and manifests.
|
||||
|
||||
Works with any Gitea instance. MokoGitea-specific features degrade gracefully on vanilla Gitea.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### npx (no install)
|
||||
|
||||
```bash
|
||||
GITEA_URL=https://gitea.example.com GITEA_TOKEN=your_token npx @mokoconsulting/mokogitea-mcp
|
||||
```
|
||||
|
||||
### Claude Code
|
||||
|
||||
Add to `.claude.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mokogitea": {
|
||||
"command": "npx",
|
||||
"args": ["@mokoconsulting/mokogitea-mcp"],
|
||||
"env": {
|
||||
"GITEA_URL": "https://gitea.example.com",
|
||||
"GITEA_TOKEN": "your_token"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Docker (SSE mode)
|
||||
|
||||
```bash
|
||||
docker run -p 3100:3100 \
|
||||
-e GITEA_URL=https://gitea.example.com \
|
||||
-e GITEA_TOKEN=your_token \
|
||||
mokoconsulting/mokogitea-mcp
|
||||
```
|
||||
|
||||
Connect MCP client to `http://localhost:3100/sse`.
|
||||
|
||||
### Multi-instance config
|
||||
|
||||
Create `~/.mcp_mokogitea.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"defaultConnection": "production",
|
||||
"connections": {
|
||||
"production": { "baseUrl": "https://gitea.example.com", "token": "your_token" },
|
||||
"dev": { "baseUrl": "https://dev.gitea.example.com", "token": "dev_token" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
| Method | Use Case |
|
||||
|--------|----------|
|
||||
| `GITEA_URL` + `GITEA_TOKEN` env vars | Single instance, quick setup |
|
||||
| `~/.mcp_mokogitea.json` config file | Multiple instances |
|
||||
| `GITEA_API_MCP_CONFIG` env var | Custom config path |
|
||||
| `GITEA_INSECURE=true` | Skip TLS verification |
|
||||
|
||||
## Tools (120+)
|
||||
|
||||
### Repositories
|
||||
`gitea_repo_create` `gitea_repo_get` `gitea_repo_edit` `gitea_repo_delete` `gitea_repo_search` `gitea_repo_fork` `gitea_repo_generate` `gitea_repo_languages` `gitea_repo_contributors` `gitea_repo_topics` `gitea_repo_topics_set`
|
||||
|
||||
### Issues
|
||||
`gitea_issue_create` (dedup by title) `gitea_issue_get` `gitea_issue_update` `gitea_issues_list` `gitea_issue_search` `gitea_issue_comment_create` `gitea_issue_comments_list` `gitea_issue_labels_set` `gitea_issue_bulk_set_status`
|
||||
|
||||
### Pull Requests
|
||||
`gitea_pull_create` `gitea_pull_get` `gitea_pulls_list` `gitea_pull_merge` `gitea_pull_files` `gitea_pull_review_create`
|
||||
|
||||
### Branches and Tags
|
||||
`gitea_branches_list` `gitea_branch_create` `gitea_branch_delete` `gitea_branch_get` `gitea_tags_list` `gitea_tag_create` `gitea_tag_delete`
|
||||
|
||||
### Releases
|
||||
`gitea_releases_list` `gitea_release_create` `gitea_release_get` `gitea_release_latest` `gitea_release_delete` `gitea_release_asset_upload` `gitea_release_asset_delete`
|
||||
|
||||
### Files and Trees
|
||||
`gitea_file_get` `gitea_file_create_or_update` `gitea_file_delete` `gitea_dir_get` `gitea_tree_get` `gitea_bulk_file_push`
|
||||
|
||||
### Projects
|
||||
`gitea_project_list` `gitea_project_create` `gitea_project_get` `gitea_project_update` `gitea_project_delete` `gitea_project_overview` `gitea_project_columns_list` `gitea_project_column_create` `gitea_project_column_delete` `gitea_project_cards_list` `gitea_project_card_add` `gitea_project_card_move` `gitea_project_card_remove`
|
||||
|
||||
### Organizations
|
||||
`gitea_org_get` `gitea_org_repos` `gitea_org_members_list` `gitea_org_teams_list` `gitea_org_labels_list` `gitea_org_label_create`
|
||||
|
||||
### Wiki
|
||||
`gitea_wiki_pages_list` `gitea_wiki_page_get`
|
||||
|
||||
### MokoGitea Extensions
|
||||
`gitea_manifest_get` `gitea_manifest_update` `gitea_org_custom_fields_list` `gitea_org_custom_field_create` `gitea_org_custom_field_delete` `gitea_issue_custom_fields_get` `gitea_issue_custom_fields_set` `gitea_org_issue_statuses_list` `gitea_issue_set_status` `gitea_org_issue_priorities_list` `gitea_issue_set_priority`
|
||||
|
||||
### Admin and Other
|
||||
`gitea_me` `gitea_users_search` `gitea_user_get` `gitea_notifications_list` `gitea_notifications_read` `gitea_commits_list` `gitea_commit_get` `gitea_compare` `gitea_webhooks_list` `gitea_webhook_create` `gitea_admin_users_list` `gitea_admin_orgs_list` `gitea_admin_cron_list` `gitea_admin_cron_run` `gitea_list_connections`
|
||||
|
||||
## SSE Server
|
||||
|
||||
For hosted deployments:
|
||||
|
||||
```
|
||||
GET / Server info
|
||||
GET /sse SSE connection endpoint
|
||||
POST /message Tool call messages
|
||||
GET /health Health check
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
GPL-3.0-or-later - [Moko Consulting](https://mokoconsulting.tech)
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"defaultConnection": "moko",
|
||||
"connections": {
|
||||
"moko": {
|
||||
"baseUrl": "https://git.mokoconsulting.tech",
|
||||
"token": "your-gitea-access-token"
|
||||
},
|
||||
"github-mirror": {
|
||||
"baseUrl": "https://gitea.example.com",
|
||||
"token": "your-other-token"
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
-1198
File diff suppressed because it is too large
Load Diff
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"name": "@mokoconsulting/mokogitea-mcp",
|
||||
"version": "1.1.0",
|
||||
"description": "MCP server for Gitea and MokoGitea - 120+ tools for repos, issues, PRs, projects, releases, custom fields, statuses, priorities, and manifests",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
"mokogitea-mcp": "dist/index.js",
|
||||
"mokogitea-mcp-sse": "dist/sse.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"start": "node dist/index.js",
|
||||
"start:sse": "node dist/sse.js",
|
||||
"setup": "node scripts/setup.mjs",
|
||||
"clean": "rm -rf dist/"
|
||||
},
|
||||
"keywords": [
|
||||
"mcp",
|
||||
"gitea",
|
||||
"mokogitea",
|
||||
"model-context-protocol",
|
||||
"claude",
|
||||
"ai",
|
||||
"git",
|
||||
"self-hosted",
|
||||
"api",
|
||||
"devops"
|
||||
],
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||
"zod": "^3.24.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.15.3",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"license": "GPL-3.0-or-later",
|
||||
"author": "Moko Consulting <hello@mokoconsulting.tech>",
|
||||
"homepage": "https://git.mokoconsulting.tech/MokoConsulting/mcp_mokogitea_api",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.mokoconsulting.tech/MokoConsulting/mcp_mokogitea_api.git"
|
||||
},
|
||||
"files": [
|
||||
"dist/",
|
||||
"config.example.json",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
# mcp_mokogitea_api PowerShell Profile
|
||||
# Source this with: . ./profile.ps1
|
||||
|
||||
$env:MCP_ROOT = $PSScriptRoot
|
||||
$env:TEMP = 'A:\temp'
|
||||
$env:TMP = 'A:\temp'
|
||||
|
||||
function mcp { Set-Location $PSScriptRoot }
|
||||
function mcp-src { Set-Location (Join-Path $PSScriptRoot 'src') }
|
||||
function mcp-build { Set-Location $PSScriptRoot; npm run build }
|
||||
function mcp-dev { Set-Location $PSScriptRoot; npm run dev }
|
||||
|
||||
Write-Host "mcp_mokogitea_api profile loaded" -ForegroundColor Cyan
|
||||
Write-Host " Commands: mcp-build, mcp-dev" -ForegroundColor DarkGray
|
||||
Write-Host " Navigate: mcp, mcp-src" -ForegroundColor DarkGray
|
||||
@@ -1,40 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* BRIEF: Interactive setup — prompts for Gitea connection details
|
||||
*/
|
||||
import { createInterface } from 'node:readline/promises';
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import { resolve } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
const CONFIG_PATH = resolve(homedir(), '.gitea-api-mcp.json');
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||
|
||||
async function prompt(q, d) { const a = await rl.question(`${q}${d ? ` [${d}]` : ''}: `); return a.trim() || d || ''; }
|
||||
async function promptRequired(q) { let a = ''; while (!a) { a = (await rl.question(`${q}: `)).trim(); if (!a) console.log(' Required.'); } return a; }
|
||||
|
||||
async function main() {
|
||||
console.log('\n=== gitea-api-mcp Setup ===\n');
|
||||
let existing = null;
|
||||
try { existing = JSON.parse(await readFile(CONFIG_PATH, 'utf-8')); console.log(`Existing: ${Object.keys(existing.connections).join(', ')}\n`); } catch {}
|
||||
|
||||
const name = await prompt('Connection name', 'moko');
|
||||
const baseUrl = await promptRequired('Gitea URL (e.g. https://git.mokoconsulting.tech)');
|
||||
const token = await promptRequired('Access token (Settings > Applications > Generate Token)');
|
||||
const insecure = (await prompt('Skip TLS verification? (y/N)', 'N')).toLowerCase() === 'y';
|
||||
|
||||
const conn = { baseUrl: baseUrl.replace(/\/+$/, ''), token };
|
||||
if (insecure) conn.insecure = true;
|
||||
|
||||
const config = existing ?? { defaultConnection: name, connections: {} };
|
||||
config.connections[name] = conn;
|
||||
if (!existing) config.defaultConnection = name;
|
||||
else if ((await prompt(`Set "${name}" as default? (y/N)`, 'N')).toLowerCase() === 'y') config.defaultConnection = name;
|
||||
|
||||
await writeFile(CONFIG_PATH, JSON.stringify(config, null, '\t') + '\n', 'utf-8');
|
||||
console.log(`\nConfig written to ${CONFIG_PATH}\n`);
|
||||
rl.close();
|
||||
}
|
||||
|
||||
main().catch(e => { console.error(e.message); rl.close(); process.exit(1); });
|
||||
@@ -1,120 +0,0 @@
|
||||
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
*
|
||||
* This file is part of a Moko Consulting project.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* FILE INFORMATION
|
||||
* DEFGROUP: gitea-api-mcp.Client
|
||||
* INGROUP: gitea-api-mcp
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/gitea-api-mcp
|
||||
* PATH: /src/client.ts
|
||||
* VERSION: 01.00.00
|
||||
* BRIEF: HTTP client for Gitea REST API v1
|
||||
*/
|
||||
|
||||
import * as https from 'node:https';
|
||||
import * as http from 'node:http';
|
||||
import type { GiteaConnection, ApiResponse } from './types.js';
|
||||
|
||||
const API_PREFIX = '/api/v1';
|
||||
const TIMEOUT_MS = 30_000;
|
||||
|
||||
export class GiteaClient {
|
||||
private readonly base_url: string;
|
||||
private readonly headers: Record<string, string>;
|
||||
private readonly insecure: boolean;
|
||||
|
||||
constructor(conn: GiteaConnection) {
|
||||
this.base_url = conn.baseUrl.replace(/\/+$/, '') + API_PREFIX;
|
||||
this.headers = {
|
||||
'Authorization': `token ${conn.token}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
this.insecure = conn.insecure ?? false;
|
||||
}
|
||||
|
||||
async get(endpoint: string, params?: Record<string, string>): Promise<ApiResponse> {
|
||||
return this.request(this.buildUrl(endpoint, params), 'GET');
|
||||
}
|
||||
|
||||
async post(endpoint: string, body?: unknown): Promise<ApiResponse> {
|
||||
return this.request(this.buildUrl(endpoint), 'POST', body);
|
||||
}
|
||||
|
||||
async patch(endpoint: string, body: unknown): Promise<ApiResponse> {
|
||||
return this.request(this.buildUrl(endpoint), 'PATCH', body);
|
||||
}
|
||||
|
||||
async put(endpoint: string, body: unknown): Promise<ApiResponse> {
|
||||
return this.request(this.buildUrl(endpoint), 'PUT', body);
|
||||
}
|
||||
|
||||
async delete(endpoint: string): Promise<ApiResponse> {
|
||||
return this.request(this.buildUrl(endpoint), 'DELETE');
|
||||
}
|
||||
|
||||
private buildUrl(endpoint: string, params?: Record<string, string>): string {
|
||||
const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
||||
const url = new URL(`${this.base_url}${path}`);
|
||||
if (params) {
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
url.searchParams.set(key, value);
|
||||
}
|
||||
}
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
private request(url: string, method: string, body?: unknown): Promise<ApiResponse> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const parsed = new URL(url);
|
||||
const is_https = parsed.protocol === 'https:';
|
||||
const transport = is_https ? https : http;
|
||||
|
||||
const options: https.RequestOptions = {
|
||||
hostname: parsed.hostname,
|
||||
port: parsed.port || (is_https ? 443 : 80),
|
||||
path: parsed.pathname + parsed.search,
|
||||
method,
|
||||
headers: { ...this.headers },
|
||||
timeout: TIMEOUT_MS,
|
||||
};
|
||||
|
||||
if (this.insecure && is_https) {
|
||||
options.rejectUnauthorized = false;
|
||||
}
|
||||
|
||||
const payload = body !== undefined ? JSON.stringify(body) : undefined;
|
||||
if (payload) {
|
||||
(options.headers as Record<string, string>)['Content-Length'] = Buffer.byteLength(payload).toString();
|
||||
}
|
||||
|
||||
const req = transport.request(options, (res) => {
|
||||
const chunks: Buffer[] = [];
|
||||
res.on('data', (chunk: Buffer) => chunks.push(chunk));
|
||||
res.on('end', () => {
|
||||
const raw = Buffer.concat(chunks).toString('utf-8');
|
||||
let data: unknown;
|
||||
try {
|
||||
data = JSON.parse(raw);
|
||||
} catch {
|
||||
data = raw;
|
||||
}
|
||||
resolve({ status: res.statusCode ?? 0, data });
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (err) => reject(err));
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
reject(new Error('Request timed out'));
|
||||
});
|
||||
|
||||
if (payload) {
|
||||
req.write(payload);
|
||||
}
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { resolve } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
import type { GiteaConfig, GiteaConnection } from './types.js';
|
||||
|
||||
const CONFIG_FILENAME = '.mcp_mokogitea.json';
|
||||
|
||||
export async function loadConfig(): Promise<GiteaConfig> {
|
||||
// Priority 1: Environment variables (zero-config single instance)
|
||||
if (process.env.GITEA_URL && process.env.GITEA_TOKEN) {
|
||||
const conn: GiteaConnection = {
|
||||
baseUrl: process.env.GITEA_URL,
|
||||
token: process.env.GITEA_TOKEN,
|
||||
insecure: process.env.GITEA_INSECURE === 'true',
|
||||
};
|
||||
return {
|
||||
connections: { default: conn },
|
||||
defaultConnection: 'default',
|
||||
};
|
||||
}
|
||||
|
||||
// Priority 2: Config file
|
||||
const config_path = process.env.GITEA_API_MCP_CONFIG
|
||||
? resolve(process.env.GITEA_API_MCP_CONFIG)
|
||||
: resolve(homedir(), CONFIG_FILENAME);
|
||||
|
||||
try {
|
||||
const raw = await readFile(config_path, 'utf-8');
|
||||
const parsed = JSON.parse(raw) as Partial<GiteaConfig>;
|
||||
|
||||
if (!parsed.connections || Object.keys(parsed.connections).length === 0) {
|
||||
throw new Error('No connections defined in config');
|
||||
}
|
||||
|
||||
return {
|
||||
connections: parsed.connections,
|
||||
defaultConnection: parsed.defaultConnection ?? Object.keys(parsed.connections)[0],
|
||||
};
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
throw new Error(
|
||||
`Failed to load config from ${config_path}: ${message}\n` +
|
||||
`Option 1: Set GITEA_URL and GITEA_TOKEN environment variables\n` +
|
||||
`Option 2: Create ${config_path} - see config.example.json for format`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function getConnection(config: GiteaConfig, name?: string): GiteaConnection {
|
||||
const key = name ?? config.defaultConnection;
|
||||
const conn = config.connections[key];
|
||||
if (!conn) {
|
||||
throw new Error(
|
||||
`Connection "${key}" not found. Available: ${Object.keys(config.connections).join(', ')}`,
|
||||
);
|
||||
}
|
||||
return conn;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,16 +0,0 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Creates a configured MCP server instance for use by both stdio and SSE transports.
|
||||
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import type { GiteaConfig } from './types.js';
|
||||
|
||||
// Import index.ts to register all tools on its exported `server` singleton,
|
||||
// then re-export a factory that initializes config and returns the server.
|
||||
import { server, initConfig } from './index.js';
|
||||
|
||||
export function createMcpServer(cfg: GiteaConfig): McpServer {
|
||||
initConfig(cfg);
|
||||
return server;
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// SSE transport entry point for MokoGitea MCP server.
|
||||
// Run with: node dist/sse.js
|
||||
// Or: GITEA_URL=https://gitea.example.com GITEA_TOKEN=xxx node dist/sse.js
|
||||
//
|
||||
// Listens on PORT (default 3100) and serves SSE at /sse with POST at /message.
|
||||
|
||||
import { createServer } from 'node:http';
|
||||
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
||||
import { createMcpServer } from './server.js';
|
||||
import { loadConfig } from './config.js';
|
||||
|
||||
const PORT = parseInt(process.env.PORT ?? '3100', 10);
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const config = await loadConfig();
|
||||
const transports = new Map<string, SSEServerTransport>();
|
||||
|
||||
const httpServer = createServer(async (req, res) => {
|
||||
// CORS headers for browser clients
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(204);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Health check
|
||||
if (req.url === '/health') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ status: 'ok', tools: 120 }));
|
||||
return;
|
||||
}
|
||||
|
||||
// SSE endpoint - client connects here
|
||||
if (req.url === '/sse' && req.method === 'GET') {
|
||||
const transport = new SSEServerTransport('/message', res);
|
||||
const sessionId = transport.sessionId;
|
||||
transports.set(sessionId, transport);
|
||||
|
||||
const server = createMcpServer(config);
|
||||
await server.connect(transport);
|
||||
|
||||
req.on('close', () => {
|
||||
transports.delete(sessionId);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Message endpoint - client sends tool calls here
|
||||
if (req.url?.startsWith('/message') && req.method === 'POST') {
|
||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||
const sessionId = url.searchParams.get('sessionId');
|
||||
if (!sessionId || !transports.has(sessionId)) {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Invalid or missing sessionId' }));
|
||||
return;
|
||||
}
|
||||
const transport = transports.get(sessionId)!;
|
||||
await transport.handlePostMessage(req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
// Root - info page
|
||||
if (req.url === '/' || req.url === '') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
name: '@mokoconsulting/mokogitea-mcp',
|
||||
version: '1.1.0',
|
||||
description: 'MCP server for Gitea and MokoGitea - 120+ tools',
|
||||
endpoints: {
|
||||
sse: '/sse',
|
||||
message: '/message',
|
||||
health: '/health',
|
||||
},
|
||||
docs: 'https://git.mokoconsulting.tech/MokoConsulting/mcp_mokogitea_api',
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeHead(404);
|
||||
res.end('Not found');
|
||||
});
|
||||
|
||||
httpServer.listen(PORT, () => {
|
||||
process.stderr.write(`MokoGitea MCP SSE server listening on port ${PORT}\n`);
|
||||
process.stderr.write(` SSE: http://localhost:${PORT}/sse\n`);
|
||||
process.stderr.write(` Health: http://localhost:${PORT}/health\n`);
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
process.stderr.write(`Fatal: ${err}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,37 +0,0 @@
|
||||
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
*
|
||||
* This file is part of a Moko Consulting project.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* FILE INFORMATION
|
||||
* DEFGROUP: gitea-api-mcp.Types
|
||||
* INGROUP: gitea-api-mcp
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/gitea-api-mcp
|
||||
* PATH: /src/types.ts
|
||||
* VERSION: 01.00.00
|
||||
* BRIEF: TypeScript type definitions for Gitea API MCP server
|
||||
*/
|
||||
|
||||
export interface GiteaConnection {
|
||||
baseUrl: string;
|
||||
token: string;
|
||||
/** Skip TLS certificate verification (self-signed certs) */
|
||||
insecure?: boolean;
|
||||
}
|
||||
|
||||
export interface GitHubBackupConfig {
|
||||
token: string;
|
||||
org: string;
|
||||
}
|
||||
|
||||
export interface GiteaConfig {
|
||||
connections: Record<string, GiteaConnection>;
|
||||
defaultConnection: string;
|
||||
github?: GitHubBackupConfig;
|
||||
}
|
||||
|
||||
export interface ApiResponse {
|
||||
status: number;
|
||||
data: unknown;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /.mokogitea/workflows/auto-bump.yml
|
||||
# VERSION: 09.02.00
|
||||
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
||||
|
||||
name: "Universal: Auto Version Bump"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- rc
|
||||
- 'feature/**'
|
||||
- 'patch/**'
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
bump:
|
||||
name: Version Bump
|
||||
runs-on: release
|
||||
if: >-
|
||||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||
!contains(github.event.head_commit.message, '[skip bump]') &&
|
||||
!startsWith(github.event.head_commit.message, 'Merge pull request')
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
run: |
|
||||
if ! command -v composer &> /dev/null; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||
fi
|
||||
if [ -d "/opt/moko-platform/cli" ]; then
|
||||
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
|
||||
else
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
|
||||
/tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
||||
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Bump version
|
||||
run: |
|
||||
php ${MOKO_CLI}/version_auto_bump.php \
|
||||
--path . --branch "${GITHUB_REF_NAME}" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
@@ -1,324 +1,341 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Release
|
||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||
# PATH: /templates/workflows/universal/auto-release.yml.template
|
||||
# VERSION: 05.00.00
|
||||
# BRIEF: Universal build & release � detects platform from manifest.xml
|
||||
#
|
||||
# +========================================================================+
|
||||
# | UNIVERSAL BUILD & RELEASE PIPELINE |
|
||||
# +========================================================================+
|
||||
# | |
|
||||
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
||||
# | |
|
||||
# | Platform-specific: |
|
||||
# | joomla: XML manifest, type-prefixed packages |
|
||||
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
||||
# | generic: README-only, no update stream |
|
||||
# | |
|
||||
# +========================================================================+
|
||||
|
||||
name: "Universal: Build & Release"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, closed]
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
action:
|
||||
description: 'Action to perform'
|
||||
required: false
|
||||
type: choice
|
||||
default: release
|
||||
options:
|
||||
- release
|
||||
- promote-rc
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────
|
||||
promote-rc:
|
||||
name: Promote to RC
|
||||
runs-on: release
|
||||
if: >-
|
||||
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
run: |
|
||||
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
||||
echo Using pre-installed /opt/moko-platform
|
||||
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
||||
else
|
||||
echo Falling back to fresh clone
|
||||
if ! command -v composer > /dev/null 2>&1; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
||||
fi
|
||||
rm -rf /tmp/moko-platform-api
|
||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api
|
||||
composer install --no-dev --no-interaction --quiet
|
||||
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Rename branch to rc
|
||||
run: |
|
||||
php ${MOKO_CLI}/branch_rename.php \
|
||||
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
||||
--pr "${{ github.event.pull_request.number }}"
|
||||
|
||||
- name: Checkout rc and configure git
|
||||
run: |
|
||||
git fetch origin rc
|
||||
git checkout rc
|
||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "gitea-actions[bot]"
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
|
||||
- name: Publish RC release
|
||||
run: |
|
||||
php ${MOKO_CLI}/release_publish.php \
|
||||
--path . --stability rc --bump minor --branch rc \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
||||
release:
|
||||
name: Build & Release Pipeline
|
||||
runs-on: release
|
||||
if: >-
|
||||
github.event.pull_request.merged == true ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure git for bot pushes
|
||||
run: |
|
||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "gitea-actions[bot]"
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
|
||||
- name: Check for merge conflict markers
|
||||
run: |
|
||||
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
||||
if [ -n "$CONFLICTS" ]; then
|
||||
echo "::error::Merge conflict markers found — aborting release"
|
||||
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
echo "No conflict markers found"
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
||||
run: |
|
||||
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
||||
echo Using pre-installed /opt/moko-platform
|
||||
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
||||
else
|
||||
echo Falling back to fresh clone
|
||||
if ! command -v composer > /dev/null 2>&1; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
||||
fi
|
||||
rm -rf /tmp/moko-platform-api
|
||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api
|
||||
composer install --no-dev --no-interaction --quiet
|
||||
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: "Publish stable release"
|
||||
run: |
|
||||
php ${MOKO_CLI}/release_publish.php \
|
||||
--path . --stability stable --bump minor --branch main \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
- name: Update release notes from CHANGELOG.md
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
|
||||
# Extract [Unreleased] section from changelog
|
||||
if [ -f "CHANGELOG.md" ]; then
|
||||
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
||||
[ -z "$NOTES" ] && NOTES="Stable release"
|
||||
else
|
||||
NOTES="Stable release"
|
||||
fi
|
||||
|
||||
# Update release body via API
|
||||
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
"${API_BASE}/releases/tags/stable" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||
|
||||
if [ -n "$RELEASE_ID" ]; then
|
||||
python3 -c "
|
||||
import json, urllib.request
|
||||
body = open('/dev/stdin').read()
|
||||
payload = json.dumps({'body': body}).encode()
|
||||
req = urllib.request.Request(
|
||||
'${API_BASE}/releases/${RELEASE_ID}',
|
||||
data=payload, method='PATCH',
|
||||
headers={
|
||||
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
urllib.request.urlopen(req)
|
||||
" <<< "$NOTES"
|
||||
echo "Release notes updated from CHANGELOG.md"
|
||||
fi
|
||||
|
||||
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
||||
- name: "Step 9: Mirror release to GitHub"
|
||||
if: >-
|
||||
steps.version.outputs.skip != 'true' &&
|
||||
secrets.GH_MIRROR_TOKEN != ''
|
||||
continue-on-error: true
|
||||
run: |
|
||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
php ${MOKO_CLI}/release_mirror.php \
|
||||
--version "$VERSION" --tag "$RELEASE_TAG" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
||||
--branch main 2>&1 || true
|
||||
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
|
||||
- name: "Step 10: Push main to GitHub mirror"
|
||||
if: >-
|
||||
steps.version.outputs.skip != 'true' &&
|
||||
secrets.GH_MIRROR_TOKEN != ''
|
||||
continue-on-error: true
|
||||
run: |
|
||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
|
||||
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
|
||||
git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
|
||||
git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
|
||||
git fetch origin main --depth=1
|
||||
git push github origin/main:refs/heads/main --force 2>/dev/null \
|
||||
&& echo "main branch pushed to GitHub mirror" \
|
||||
|| echo "WARNING: GitHub mirror push failed"
|
||||
|
||||
- name: "Step 11: Delete rc branch and recreate dev from main"
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
# Delete rc branch (ephemeral — created by promote-rc)
|
||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||
"${API_BASE}/branches/rc" 2>/dev/null \
|
||||
&& echo "Deleted rc branch" || echo "rc branch not found"
|
||||
|
||||
# Delete dev branch
|
||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
|
||||
|
||||
# Recreate dev from main (now includes version bump + changelog promotion)
|
||||
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${API_BASE}/branches" \
|
||||
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
|
||||
|
||||
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: "Step 12: Create version branch from main"
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||
BRANCH_NAME="version/${VERSION}"
|
||||
MAIN_SHA=$(git rev-parse HEAD)
|
||||
|
||||
# Delete old version branch if it exists (same version re-release)
|
||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
|
||||
|
||||
# Create version/XX.YY.ZZ from main
|
||||
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
|
||||
|
||||
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
|
||||
|
||||
# -- Dolibarr post-release: Reset dev version -----------------------------
|
||||
- name: "Post-release: Reset dev version"
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
php ${MOKO_CLI}/version_reset_dev.php \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
||||
--branch dev --path . 2>&1 || true
|
||||
|
||||
# -- Summary --------------------------------------------------------------
|
||||
- name: Pipeline Summary
|
||||
if: always()
|
||||
run: |
|
||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
|
||||
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
|
||||
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
|
||||
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
|
||||
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Release
|
||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||
# PATH: /templates/workflows/universal/auto-release.yml.template
|
||||
# VERSION: 05.00.00
|
||||
# BRIEF: Universal build & release � detects platform from manifest.xml
|
||||
#
|
||||
# +========================================================================+
|
||||
# | UNIVERSAL BUILD & RELEASE PIPELINE |
|
||||
# +========================================================================+
|
||||
# | |
|
||||
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
||||
# | |
|
||||
# | Platform-specific: |
|
||||
# | joomla: XML manifest, type-prefixed packages |
|
||||
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
||||
# | generic: README-only, no update stream |
|
||||
# | |
|
||||
# +========================================================================+
|
||||
|
||||
name: "Universal: Build & Release"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, closed]
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
action:
|
||||
description: 'Action to perform'
|
||||
required: false
|
||||
type: choice
|
||||
default: release
|
||||
options:
|
||||
- release
|
||||
- promote-rc
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────
|
||||
promote-rc:
|
||||
name: Promote to RC
|
||||
runs-on: release
|
||||
if: >-
|
||||
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
run: |
|
||||
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
||||
echo Using pre-installed /opt/moko-platform
|
||||
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
||||
else
|
||||
echo Falling back to fresh clone
|
||||
if ! command -v composer > /dev/null 2>&1; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
||||
fi
|
||||
rm -rf /tmp/moko-platform-api
|
||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api
|
||||
composer install --no-dev --no-interaction --quiet
|
||||
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Rename branch to rc
|
||||
run: |
|
||||
php ${MOKO_CLI}/branch_rename.php \
|
||||
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
||||
--pr "${{ github.event.pull_request.number }}"
|
||||
|
||||
- name: Checkout rc and configure git
|
||||
run: |
|
||||
git fetch origin rc
|
||||
git checkout rc
|
||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "gitea-actions[bot]"
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
|
||||
- name: Publish RC release
|
||||
run: |
|
||||
php ${MOKO_CLI}/release_publish.php \
|
||||
--path . --stability rc --bump minor --branch rc \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
||||
release:
|
||||
name: Build & Release Pipeline
|
||||
runs-on: release
|
||||
if: >-
|
||||
github.event.pull_request.merged == true ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure git for bot pushes
|
||||
run: |
|
||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "gitea-actions[bot]"
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
|
||||
- name: Check for merge conflict markers
|
||||
run: |
|
||||
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
||||
if [ -n "$CONFLICTS" ]; then
|
||||
echo "::error::Merge conflict markers found — aborting release"
|
||||
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
echo "No conflict markers found"
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
||||
run: |
|
||||
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
||||
echo Using pre-installed /opt/moko-platform
|
||||
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
||||
else
|
||||
echo Falling back to fresh clone
|
||||
if ! command -v composer > /dev/null 2>&1; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
||||
fi
|
||||
rm -rf /tmp/moko-platform-api
|
||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api
|
||||
composer install --no-dev --no-interaction --quiet
|
||||
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: "Determine version bump level"
|
||||
id: bump
|
||||
run: |
|
||||
# Fix/patch branches: version was already bumped by pre-release, just strip suffix
|
||||
# Feature/dev branches: bump minor for the new stable release
|
||||
HEAD_REF="${{ github.event.pull_request.head.ref || 'dev' }}"
|
||||
case "$HEAD_REF" in
|
||||
fix/*|patch/*|hotfix/*|bugfix/*) BUMP="none" ;;
|
||||
*) BUMP="minor" ;;
|
||||
esac
|
||||
echo "level=${BUMP}" >> "$GITHUB_OUTPUT"
|
||||
echo "Bump level: ${BUMP} (from branch: ${HEAD_REF})"
|
||||
|
||||
- name: "Publish stable release"
|
||||
run: |
|
||||
BUMP_FLAG=""
|
||||
if [ "${{ steps.bump.outputs.level }}" != "none" ]; then
|
||||
BUMP_FLAG="--bump ${{ steps.bump.outputs.level }}"
|
||||
fi
|
||||
php ${MOKO_CLI}/release_publish.php \
|
||||
--path . --stability stable ${BUMP_FLAG} --branch main \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
- name: Update release notes from CHANGELOG.md
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
|
||||
# Extract [Unreleased] section from changelog
|
||||
if [ -f "CHANGELOG.md" ]; then
|
||||
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
||||
[ -z "$NOTES" ] && NOTES="Stable release"
|
||||
else
|
||||
NOTES="Stable release"
|
||||
fi
|
||||
|
||||
# Update release body via API
|
||||
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
"${API_BASE}/releases/tags/stable" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||
|
||||
if [ -n "$RELEASE_ID" ]; then
|
||||
python3 -c "
|
||||
import json, urllib.request
|
||||
body = open('/dev/stdin').read()
|
||||
payload = json.dumps({'body': body}).encode()
|
||||
req = urllib.request.Request(
|
||||
'${API_BASE}/releases/${RELEASE_ID}',
|
||||
data=payload, method='PATCH',
|
||||
headers={
|
||||
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
urllib.request.urlopen(req)
|
||||
" <<< "$NOTES"
|
||||
echo "Release notes updated from CHANGELOG.md"
|
||||
fi
|
||||
|
||||
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
||||
- name: "Step 9: Mirror release to GitHub"
|
||||
if: >-
|
||||
steps.version.outputs.skip != 'true' &&
|
||||
secrets.GH_MIRROR_TOKEN != ''
|
||||
continue-on-error: true
|
||||
run: |
|
||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
php ${MOKO_CLI}/release_mirror.php \
|
||||
--version "$VERSION" --tag "$RELEASE_TAG" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
||||
--branch main 2>&1 || true
|
||||
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
|
||||
- name: "Step 10: Push main to GitHub mirror"
|
||||
if: >-
|
||||
steps.version.outputs.skip != 'true' &&
|
||||
secrets.GH_MIRROR_TOKEN != ''
|
||||
continue-on-error: true
|
||||
run: |
|
||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
|
||||
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
|
||||
git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
|
||||
git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
|
||||
git fetch origin main --depth=1
|
||||
git push github origin/main:refs/heads/main --force 2>/dev/null \
|
||||
&& echo "main branch pushed to GitHub mirror" \
|
||||
|| echo "WARNING: GitHub mirror push failed"
|
||||
|
||||
- name: "Step 11: Delete rc branch and recreate dev from main"
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
# Delete rc branch (ephemeral — created by promote-rc)
|
||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||
"${API_BASE}/branches/rc" 2>/dev/null \
|
||||
&& echo "Deleted rc branch" || echo "rc branch not found"
|
||||
|
||||
# Delete dev branch
|
||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
|
||||
|
||||
# Recreate dev from main (now includes version bump + changelog promotion)
|
||||
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${API_BASE}/branches" \
|
||||
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
|
||||
|
||||
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: "Step 12: Create version branch from main"
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||
BRANCH_NAME="version/${VERSION}"
|
||||
MAIN_SHA=$(git rev-parse HEAD)
|
||||
|
||||
# Delete old version branch if it exists (same version re-release)
|
||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
|
||||
|
||||
# Create version/XX.YY.ZZ from main
|
||||
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
|
||||
|
||||
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
|
||||
|
||||
# -- Dolibarr post-release: Reset dev version -----------------------------
|
||||
- name: "Post-release: Reset dev version"
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
php ${MOKO_CLI}/version_reset_dev.php \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
||||
--branch dev --path . 2>&1 || true
|
||||
|
||||
# -- Summary --------------------------------------------------------------
|
||||
- name: Pipeline Summary
|
||||
if: always()
|
||||
run: |
|
||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
|
||||
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
|
||||
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
|
||||
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
|
||||
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Universal
|
||||
# REPO: https://code.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /.mokogitea/workflows/branch-cleanup.yml
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Delete feature branches after PR merge
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
- name: Delete source branch
|
||||
run: |
|
||||
BRANCH="${{ github.event.pull_request.head.ref }}"
|
||||
API="${{ vars.GITEA_URL || 'https://code.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
|
||||
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
|
||||
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
|
||||
|
||||
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# DISABLED — auto-release Step 11 recreates dev from main after every release.
|
||||
# Cascade-dev is redundant and causes version conflicts when both main and dev
|
||||
# have different version numbers in templateDetails.xml / manifest.xml.
|
||||
name: "Cascade Main → Dev (DISABLED)"
|
||||
on: workflow_dispatch
|
||||
jobs:
|
||||
noop:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "Cascade disabled — auto-release handles dev recreation"
|
||||
@@ -0,0 +1,204 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.CI
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||
# PATH: /.gitea/workflows/ci-generic.yml
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: CI pipeline — lint, validate, and test for generic projects (PHP + Node.js)
|
||||
|
||||
name: "Generic: Project CI"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- dev/**
|
||||
- rc/**
|
||||
- version/**
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- dev/**
|
||||
- rc/**
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
# ── Lint & Validate ───────────────────────────────────────────────────
|
||||
lint:
|
||||
name: Lint & Validate
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Detect toolchain
|
||||
id: detect
|
||||
run: |
|
||||
HAS_PHP=false
|
||||
HAS_NODE=false
|
||||
[ -f "composer.json" ] && HAS_PHP=true
|
||||
[ -f "package.json" ] && HAS_NODE=true
|
||||
echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT"
|
||||
echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT"
|
||||
echo "Toolchain: PHP=$HAS_PHP Node=$HAS_NODE"
|
||||
|
||||
- name: Setup PHP
|
||||
if: steps.detect.outputs.has_php == 'true'
|
||||
run: |
|
||||
if ! command -v php &> /dev/null; then
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
||||
fi
|
||||
php -v
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.detect.outputs.has_node == 'true'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install PHP dependencies
|
||||
if: steps.detect.outputs.has_php == 'true'
|
||||
run: |
|
||||
if [ -f "composer.json" ]; then
|
||||
composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true
|
||||
fi
|
||||
|
||||
- name: Install Node.js dependencies
|
||||
if: steps.detect.outputs.has_node == 'true'
|
||||
run: |
|
||||
if [ -f "package.json" ]; then
|
||||
npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true
|
||||
fi
|
||||
|
||||
- name: PHP syntax check
|
||||
if: steps.detect.outputs.has_php == 'true'
|
||||
run: |
|
||||
ERRORS=0
|
||||
while IFS= read -r -d '' file; do
|
||||
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
|
||||
echo "::error file=${file}::PHP syntax error"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./node_modules/*" -print0)
|
||||
|
||||
echo "## PHP Lint" >> $GITHUB_STEP_SUMMARY
|
||||
if [ "$ERRORS" -eq 0 ]; then
|
||||
echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "${ERRORS} file(s) with syntax errors." >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: TypeScript/JavaScript lint
|
||||
if: steps.detect.outputs.has_node == 'true'
|
||||
run: |
|
||||
if [ -f "node_modules/.bin/eslint" ]; then
|
||||
npx eslint src/ --quiet 2>&1 || { echo "::error::ESLint errors found"; exit 1; }
|
||||
echo "## ESLint" >> $GITHUB_STEP_SUMMARY
|
||||
echo "All files passed ESLint." >> $GITHUB_STEP_SUMMARY
|
||||
elif [ -f ".eslintrc.json" ] || [ -f ".eslintrc.js" ] || [ -f "eslint.config.js" ]; then
|
||||
echo "::warning::ESLint config found but eslint not installed"
|
||||
else
|
||||
echo "No ESLint configured — skipping"
|
||||
fi
|
||||
|
||||
- name: TypeScript compile check
|
||||
if: steps.detect.outputs.has_node == 'true'
|
||||
run: |
|
||||
if [ -f "tsconfig.json" ] && [ -f "node_modules/.bin/tsc" ]; then
|
||||
npx tsc --noEmit 2>&1 || { echo "::error::TypeScript compilation errors"; exit 1; }
|
||||
echo "## TypeScript" >> $GITHUB_STEP_SUMMARY
|
||||
echo "TypeScript compilation passed." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
- name: PHPStan static analysis
|
||||
if: steps.detect.outputs.has_php == 'true'
|
||||
run: |
|
||||
if [ -f "phpstan.neon" ] && [ -f "vendor/bin/phpstan" ]; then
|
||||
vendor/bin/phpstan analyse --no-progress 2>&1 || { echo "::warning::PHPStan found issues"; }
|
||||
fi
|
||||
|
||||
# ── Tests ─────────────────────────────────────────────────────────────
|
||||
test:
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: lint
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Detect toolchain
|
||||
id: detect
|
||||
run: |
|
||||
HAS_PHP=false
|
||||
HAS_NODE=false
|
||||
[ -f "composer.json" ] && HAS_PHP=true
|
||||
[ -f "package.json" ] && HAS_NODE=true
|
||||
echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT"
|
||||
echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Setup PHP
|
||||
if: steps.detect.outputs.has_php == 'true'
|
||||
run: |
|
||||
if ! command -v php &> /dev/null; then
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.detect.outputs.has_node == 'true'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
[ -f "composer.json" ] && composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true
|
||||
[ -f "package.json" ] && { npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true; }
|
||||
|
||||
- name: Run PHP tests
|
||||
if: steps.detect.outputs.has_php == 'true'
|
||||
run: |
|
||||
if [ -f "vendor/bin/phpunit" ]; then
|
||||
vendor/bin/phpunit --testdox 2>&1
|
||||
echo "## PHPUnit" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Tests passed." >> $GITHUB_STEP_SUMMARY
|
||||
elif [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then
|
||||
echo "::warning::PHPUnit config found but phpunit not installed"
|
||||
else
|
||||
echo "No PHPUnit configured — skipping"
|
||||
fi
|
||||
|
||||
- name: Run Node.js tests
|
||||
if: steps.detect.outputs.has_node == 'true'
|
||||
run: |
|
||||
if jq -e '.scripts.test' package.json > /dev/null 2>&1; then
|
||||
npm test 2>&1
|
||||
echo "## Node.js Tests" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Tests passed." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "No test script in package.json — skipping"
|
||||
fi
|
||||
|
||||
- name: Build check
|
||||
run: |
|
||||
if [ -f "Makefile" ]; then
|
||||
make build 2>&1 || echo "::warning::Build failed or not configured"
|
||||
elif [ -f "package.json" ] && jq -e '.scripts.build' package.json > /dev/null 2>&1; then
|
||||
npm run build 2>&1 || echo "::warning::Build failed"
|
||||
fi
|
||||
@@ -0,0 +1,87 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Maintenance
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||
# PATH: /.gitea/workflows/cleanup.yml
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
||||
|
||||
name: "Universal: Repository Cleanup"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 3 * * 0' # Weekly on Sunday at 03:00 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
name: Clean Merged Branches
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
|
||||
- name: Delete merged branches
|
||||
env:
|
||||
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
run: |
|
||||
echo "=== Merged Branch Cleanup ==="
|
||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||
|
||||
# List branches via API
|
||||
BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
|
||||
"${API}/branches?limit=50" | jq -r '.[].name')
|
||||
|
||||
DELETED=0
|
||||
for BRANCH in $BRANCHES; do
|
||||
# Skip protected branches
|
||||
case "$BRANCH" in
|
||||
main|master|develop|release/*|hotfix/*) continue ;;
|
||||
esac
|
||||
|
||||
# Check if branch is merged into main
|
||||
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
|
||||
echo " Deleting merged branch: ${BRANCH}"
|
||||
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
|
||||
"${API}/branches/${BRANCH}" 2>/dev/null || true
|
||||
DELETED=$((DELETED + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Deleted ${DELETED} merged branch(es)"
|
||||
|
||||
- name: Clean old workflow runs
|
||||
env:
|
||||
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
run: |
|
||||
echo "=== Workflow Run Cleanup ==="
|
||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
|
||||
|
||||
# Get old completed runs
|
||||
RUNS=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
|
||||
"${API}/actions/runs?status=completed&limit=50" | \
|
||||
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
|
||||
|
||||
DELETED=0
|
||||
for RUN_ID in $RUNS; do
|
||||
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
|
||||
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
|
||||
DELETED=$((DELETED + 1))
|
||||
done
|
||||
|
||||
echo "Deleted ${DELETED} old workflow run(s)"
|
||||
@@ -0,0 +1,126 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Deploy
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
|
||||
# PATH: /templates/workflows/joomla/deploy-manual.yml.template
|
||||
# VERSION: 04.07.00
|
||||
# BRIEF: Manual SFTP deploy to dev server for Joomla repos
|
||||
|
||||
name: "Universal: Deploy to Dev (Manual)"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
clear_remote:
|
||||
description: 'Delete all remote files before uploading'
|
||||
required: false
|
||||
default: 'false'
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: SFTP Deploy to Dev
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Setup PHP
|
||||
run: |
|
||||
php -v && composer --version
|
||||
|
||||
- name: Setup MokoStandards tools
|
||||
env:
|
||||
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
|
||||
MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
|
||||
run: |
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
||||
/tmp/mokostandards-api 2>/dev/null || true
|
||||
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
|
||||
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
||||
fi
|
||||
|
||||
- name: Check FTP configuration
|
||||
id: check
|
||||
env:
|
||||
HOST: ${{ vars.DEV_FTP_HOST }}
|
||||
PATH_VAR: ${{ vars.DEV_FTP_PATH }}
|
||||
PORT: ${{ vars.DEV_FTP_PORT }}
|
||||
run: |
|
||||
if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then
|
||||
echo "DEV_FTP_HOST or DEV_FTP_PATH not configured -- cannot deploy"
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
echo "host=$HOST" >> "$GITHUB_OUTPUT"
|
||||
|
||||
REMOTE="${PATH_VAR%/}"
|
||||
echo "remote=$REMOTE" >> "$GITHUB_OUTPUT"
|
||||
|
||||
[ -z "$PORT" ] && PORT="22"
|
||||
echo "port=$PORT" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Deploy via SFTP
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
env:
|
||||
SFTP_KEY: ${{ secrets.DEV_FTP_KEY }}
|
||||
SFTP_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
|
||||
SFTP_USER: ${{ vars.DEV_FTP_USERNAME }}
|
||||
run: |
|
||||
SOURCE_DIR="src"
|
||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ -- nothing to deploy"; exit 0; }
|
||||
|
||||
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
|
||||
"${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \
|
||||
> /tmp/sftp-config.json
|
||||
|
||||
if [ -n "$SFTP_KEY" ]; then
|
||||
echo "$SFTP_KEY" > /tmp/deploy_key
|
||||
chmod 600 /tmp/deploy_key
|
||||
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
|
||||
else
|
||||
printf ',"password":"%s"}' "$SFTP_PASS" >> /tmp/sftp-config.json
|
||||
fi
|
||||
|
||||
DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
|
||||
[ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote)
|
||||
|
||||
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
|
||||
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
|
||||
php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
|
||||
else
|
||||
php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
|
||||
fi
|
||||
|
||||
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
if [ "${{ steps.check.outputs.skip }}" = "true" ]; then
|
||||
echo "### Deploy Skipped -- FTP not configured" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Host | \`${{ steps.check.outputs.host }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Remote | \`${{ steps.check.outputs.remote }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Clear | ${{ inputs.clear_remote }} |" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
@@ -11,7 +11,7 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version tag (e.g. v1.26.1-moko.05.01.00)'
|
||||
description: 'Version tag'
|
||||
required: true
|
||||
default: 'latest'
|
||||
environment:
|
||||
@@ -28,9 +28,9 @@ concurrency:
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
REGISTRY: code.mokoconsulting.tech
|
||||
REGISTRY: git.mokoconsulting.tech
|
||||
IMAGE: mokoconsulting/mokogitea
|
||||
DEPLOY_HOST: code.mokoconsulting.tech
|
||||
DEPLOY_HOST: git.mokoconsulting.tech
|
||||
DEPLOY_PORT: 2918
|
||||
DEPLOY_USER: mokoconsulting
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
@@ -47,8 +47,6 @@ jobs:
|
||||
- name: Determine settings
|
||||
id: config
|
||||
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
|
||||
VERSION=$(git describe --tags --always 2>/dev/null || echo "dev-$(git rev-parse --short HEAD)")
|
||||
ENV="production"
|
||||
@@ -56,206 +54,97 @@ jobs:
|
||||
VERSION="${{ github.event.inputs.version }}"
|
||||
ENV="${{ github.event.inputs.environment }}"
|
||||
fi
|
||||
|
||||
if [ "$ENV" = "production" ]; then
|
||||
echo "compose_dir=/opt/gitea" >> $GITHUB_OUTPUT
|
||||
echo "container=mokogitea" >> $GITHUB_OUTPUT
|
||||
echo "source_dir=/opt/gitea/source" >> $GITHUB_OUTPUT
|
||||
echo "branch=main" >> $GITHUB_OUTPUT
|
||||
echo "tag=${VERSION}" >> $GITHUB_OUTPUT
|
||||
echo "instance_url=https://code.mokoconsulting.tech" >> $GITHUB_OUTPUT
|
||||
echo "tag=$VERSION" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "compose_dir=/opt/gitea-dev" >> $GITHUB_OUTPUT
|
||||
echo "container=mokogitea-dev" >> $GITHUB_OUTPUT
|
||||
echo "source_dir=/opt/gitea-dev/source" >> $GITHUB_OUTPUT
|
||||
echo "branch=dev" >> $GITHUB_OUTPUT
|
||||
echo "tag=${VERSION}-dev" >> $GITHUB_OUTPUT
|
||||
echo "instance_url=https://git.dev.mokoconsulting.tech" >> $GITHUB_OUTPUT
|
||||
echo "tag=$VERSION-dev" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Enable maintenance mode
|
||||
- name: Write deploy key
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
INSTANCE_URL: ${{ steps.config.outputs.instance_url }}
|
||||
DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
run: |
|
||||
echo "Enabling 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":true}' \
|
||||
|| echo "WARNING: Could not enable maintenance mode (instance may be down)"
|
||||
mkdir -p ~/.ssh
|
||||
echo "$DEPLOY_KEY" > ~/.ssh/deploy_key
|
||||
chmod 600 ~/.ssh/deploy_key
|
||||
|
||||
- name: Build and deploy via SSH
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
REGISTRY_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
TAG: ${{ steps.config.outputs.tag }}
|
||||
BRANCH: ${{ steps.config.outputs.branch }}
|
||||
SOURCE_DIR: ${{ steps.config.outputs.source_dir }}
|
||||
COMPOSE_DIR: ${{ steps.config.outputs.compose_dir }}
|
||||
CONTAINER: ${{ steps.config.outputs.container }}
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
|
||||
chmod 600 ~/.ssh/deploy_key
|
||||
HEALTH_FMT='${{ '{{' }}.State.Health.Status${{ '}}' }}'
|
||||
IMAGE_FMT='Image: ${{ '{{' }}.Config.Image${{ '}}' }}'
|
||||
|
||||
SSH_CMD="ssh -i ~/.ssh/deploy_key -p ${{ env.DEPLOY_PORT }} -o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }}"
|
||||
ssh -i ~/.ssh/deploy_key -p ${{ env.DEPLOY_PORT }} \
|
||||
-o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||
-o ServerAliveInterval=30 -o ServerAliveCountMax=10 \
|
||||
${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} bash -s <<DEPLOY_EOF
|
||||
set -e
|
||||
echo 'SSH connected'
|
||||
|
||||
$SSH_CMD "echo 'SSH connected'"
|
||||
echo 'Cleaning Docker build cache...'
|
||||
docker builder prune -af 2>/dev/null || true
|
||||
docker image prune -af 2>/dev/null || true
|
||||
free -m | head -3
|
||||
|
||||
# Pre-deploy cleanup: free disk and memory for the build
|
||||
$SSH_CMD "
|
||||
echo 'Cleaning Docker build cache and unused images...'
|
||||
docker builder prune -af 2>/dev/null || true
|
||||
docker image prune -af 2>/dev/null || true
|
||||
echo 'Clearing swap...'
|
||||
sudo swapoff -a && sudo swapon -a 2>/dev/null || true
|
||||
echo 'Cleanup complete'
|
||||
free -m | head -3
|
||||
"
|
||||
echo 'Pulling source...'
|
||||
if [ ! -d $SOURCE_DIR/.git ]; then
|
||||
git clone -b $BRANCH https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-APP.git $SOURCE_DIR
|
||||
fi
|
||||
cd $SOURCE_DIR
|
||||
# Ensure remote points to MokoGitea-APP (not the upstream fork)
|
||||
git remote set-url origin https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-APP.git 2>/dev/null || true
|
||||
git fetch origin $BRANCH
|
||||
git reset --hard origin/$BRANCH
|
||||
|
||||
# Pull latest source
|
||||
$SSH_CMD "
|
||||
set -e
|
||||
if [ ! -d ${SOURCE_DIR}/.git ]; then
|
||||
git clone -b ${BRANCH} https://code.mokoconsulting.tech/MokoConsulting/MokoGitea.git ${SOURCE_DIR}
|
||||
echo 'Building Docker image...'
|
||||
docker build --no-cache --build-arg GOFLAGS='-p 1' \
|
||||
--tag ${{ env.REGISTRY }}/${{ env.IMAGE }}:$TAG \
|
||||
--tag ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest \
|
||||
-f Dockerfile .
|
||||
|
||||
echo 'Pushing to registry...'
|
||||
echo '$REGISTRY_TOKEN' | docker login ${{ env.REGISTRY }} -u ${{ env.DEPLOY_USER }} --password-stdin
|
||||
docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:$TAG
|
||||
docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest
|
||||
|
||||
echo 'Restarting container...'
|
||||
cd $COMPOSE_DIR
|
||||
sed -i 's|${{ env.IMAGE }}:[^ ]*|${{ env.IMAGE }}:$TAG|' docker-compose.yml
|
||||
docker compose up -d $CONTAINER
|
||||
|
||||
echo 'Health check...'
|
||||
for i in 1 2 3 4 5 6 7 8; do
|
||||
sleep 15
|
||||
if docker inspect --format='$HEALTH_FMT' $CONTAINER 2>/dev/null | grep -q healthy; then
|
||||
echo 'Container healthy!'
|
||||
docker inspect --format='$IMAGE_FMT' $CONTAINER
|
||||
exit 0
|
||||
fi
|
||||
cd ${SOURCE_DIR}
|
||||
git fetch origin ${BRANCH}
|
||||
git reset --hard origin/${BRANCH}
|
||||
"
|
||||
|
||||
# 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 moko version from tag (e.g. v1.26.1-moko.05.01.01 -> 05.01.01)
|
||||
MOKO_VER=$(echo "$TAG" | sed -n 's/.*-moko\.\(.*\)/\1/p')
|
||||
if [ -z "$MOKO_VER" ]; then
|
||||
echo "Could not extract moko version from tag: $TAG"
|
||||
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"
|
||||
echo "Waiting... (attempt \$i/8)"
|
||||
done
|
||||
echo 'Health check failed'
|
||||
docker logs $CONTAINER --tail 20
|
||||
exit 1
|
||||
DEPLOY_EOF
|
||||
|
||||
- name: Verify
|
||||
run: |
|
||||
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
|
||||
if: failure()
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Security
|
||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||
# PATH: /templates/workflows/gitleaks.yml.template
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
||||
#
|
||||
# +========================================================================+
|
||||
# | SECRET SCANNING |
|
||||
# +========================================================================+
|
||||
# | |
|
||||
# | Scans commits for leaked secrets using Gitleaks. |
|
||||
# | |
|
||||
# | - PR scan: only new commits in the PR |
|
||||
# | - Scheduled: full repo scan weekly |
|
||||
# | - Alerts via ntfy on findings |
|
||||
# | |
|
||||
# +========================================================================+
|
||||
|
||||
name: "Universal: Secret Scanning"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- 'dev/**'
|
||||
schedule:
|
||||
- cron: '0 5 * * 1' # Weekly Monday 05:00 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
||||
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
|
||||
|
||||
jobs:
|
||||
gitleaks:
|
||||
name: Gitleaks Secret Scan
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Gitleaks
|
||||
run: |
|
||||
GITLEAKS_VERSION="8.21.2"
|
||||
curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
|
||||
| tar -xz -C /usr/local/bin gitleaks
|
||||
gitleaks version
|
||||
|
||||
- name: Scan for secrets
|
||||
id: scan
|
||||
run: |
|
||||
echo "### Secret Scanning" >> $GITHUB_STEP_SUMMARY
|
||||
ARGS="--source . --verbose --report-format json --report-path /tmp/gitleaks-report.json"
|
||||
|
||||
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||
# Scan only PR commits
|
||||
ARGS="$ARGS --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}"
|
||||
echo "Scanning PR commits only" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "Full repository scan" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
if gitleaks detect $ARGS 2>&1; then
|
||||
echo "result=clean" >> "$GITHUB_OUTPUT"
|
||||
echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "result=found" >> "$GITHUB_OUTPUT"
|
||||
FINDINGS=$(jq length /tmp/gitleaks-report.json 2>/dev/null || echo "unknown")
|
||||
echo "**${FINDINGS} potential secret(s) detected.**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Review the findings and rotate any exposed credentials immediately." >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Notify on findings
|
||||
if: failure() && steps.scan.outputs.result == 'found'
|
||||
run: |
|
||||
REPO="${{ github.event.repository.name }}"
|
||||
curl -sS \
|
||||
-H "Title: ${REPO} — secrets detected in code" \
|
||||
-H "Tags: rotating_light,key" \
|
||||
-H "Priority: urgent" \
|
||||
-d "Gitleaks found potential secrets. Review and rotate credentials immediately." \
|
||||
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
||||
@@ -4,8 +4,8 @@
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Automation
|
||||
# VERSION: 05.47.00
|
||||
# INGROUP: mokoplatform.Automation
|
||||
# VERSION: 06.15.00
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
name: "Universal: Issue Branch"
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Notifications
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||
# PATH: /.gitea/workflows/notify.yml
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
||||
|
||||
name: "Universal: Notifications"
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- "Joomla Build & Release"
|
||||
- "Joomla Extension CI"
|
||||
- "Deploy"
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
||||
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-releases' }}
|
||||
|
||||
jobs:
|
||||
notify:
|
||||
name: Send Notification
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.workflow_run.conclusion == 'success' ||
|
||||
github.event.workflow_run.conclusion == 'failure'
|
||||
|
||||
steps:
|
||||
- name: Notify on success (releases only)
|
||||
if: >-
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
contains(github.event.workflow_run.name, 'Release')
|
||||
run: |
|
||||
REPO="${{ github.event.repository.name }}"
|
||||
WORKFLOW="${{ github.event.workflow_run.name }}"
|
||||
URL="${{ github.event.workflow_run.html_url }}"
|
||||
|
||||
curl -sS \
|
||||
-H "Title: ${REPO} released" \
|
||||
-H "Tags: white_check_mark,package" \
|
||||
-H "Priority: default" \
|
||||
-H "Click: ${URL}" \
|
||||
-d "${WORKFLOW} completed successfully." \
|
||||
"${NTFY_URL}/${NTFY_TOPIC}"
|
||||
|
||||
- name: Notify on failure
|
||||
if: github.event.workflow_run.conclusion == 'failure'
|
||||
run: |
|
||||
REPO="${{ github.event.repository.name }}"
|
||||
WORKFLOW="${{ github.event.workflow_run.name }}"
|
||||
URL="${{ github.event.workflow_run.html_url }}"
|
||||
|
||||
curl -sS \
|
||||
-H "Title: ${REPO} workflow failed" \
|
||||
-H "Tags: x,warning" \
|
||||
-H "Priority: high" \
|
||||
-H "Click: ${URL}" \
|
||||
-d "${WORKFLOW} failed. Check the run for details." \
|
||||
"${NTFY_URL}/${NTFY_TOPIC}"
|
||||
@@ -48,4 +48,4 @@ jobs:
|
||||
working-directory: .mokogitea/mcp
|
||||
run: |
|
||||
npm publish --registry ${{ github.server_url }}/api/packages/${{ github.repository_owner }}/npm/ \
|
||||
--//$(echo "${{ github.server_url }}" | sed 's|https://||')/api/packages/${{ github.repository_owner }}/npm/:_authToken=${{ secrets.GITEA_TOKEN }}
|
||||
--//$(echo "${{ github.server_url }}" | sed 's|https://||')/api/packages/${{ github.repository_owner }}/npm/:_authToken=${{ secrets.MOKOGITEA_TOKEN }}
|
||||
|
||||
@@ -1,243 +1,11 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||
# VERSION: 05.01.00
|
||||
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
|
||||
|
||||
name: "Universal: Pre-Release"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches:
|
||||
- dev
|
||||
pull_request_target:
|
||||
types: [synchronize, opened, reopened]
|
||||
branches:
|
||||
- main
|
||||
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 || 'development' }})"
|
||||
runs-on: release
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') ||
|
||||
(github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main')
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }}
|
||||
|
||||
- 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: |
|
||||
php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
||||
|
||||
- name: Resolve metadata and bump version
|
||||
id: meta
|
||||
run: |
|
||||
# Auto-detect stability: RC for PRs targeting main, else use input or default to development
|
||||
if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then
|
||||
STABILITY="release-candidate"
|
||||
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 dev --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
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||
# VERSION: 05.01.00
|
||||
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
|
||||
@@ -0,0 +1,66 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoPlatform.Universal
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
# PATH: /.mokogitea/workflows/rc-revert.yml
|
||||
# VERSION: 09.23.00
|
||||
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
|
||||
|
||||
name: "RC Revert"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
revert:
|
||||
name: Rename rc/ back to dev/
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.pull_request.merged == false &&
|
||||
startsWith(github.event.pull_request.head.ref, 'rc/')
|
||||
|
||||
steps:
|
||||
- name: Rename branch
|
||||
run: |
|
||||
BRANCH="${{ github.event.pull_request.head.ref }}"
|
||||
SUFFIX="${BRANCH#rc/}"
|
||||
DEV_BRANCH="dev/${SUFFIX}"
|
||||
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
# Create dev/ branch from rc/ branch
|
||||
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"new_branch_name\": \"${DEV_BRANCH}\", \"old_branch_name\": \"${BRANCH}\"}" \
|
||||
"${API}" 2>/dev/null || true)
|
||||
|
||||
if [ "$STATUS" = "201" ]; then
|
||||
echo "Created branch: ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Delete rc/ branch
|
||||
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
|
||||
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
"${API}/${ENCODED}" 2>/dev/null || true)
|
||||
|
||||
if [ "$STATUS" = "204" ]; then
|
||||
echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "::warning::Failed to delete ${BRANCH} (HTTP ${STATUS})"
|
||||
fi
|
||||
|
||||
echo "### RC Reverted" >> $GITHUB_STEP_SUMMARY
|
||||
echo "${BRANCH} → ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -0,0 +1,82 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Security
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||
# PATH: /.gitea/workflows/security-audit.yml
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Dependency vulnerability scanning for composer and npm packages
|
||||
|
||||
name: "Universal: Security Audit"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 6 * * 1' # Weekly on Monday at 06:00 UTC
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'composer.json'
|
||||
- 'composer.lock'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
||||
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
|
||||
|
||||
jobs:
|
||||
audit:
|
||||
name: Dependency Audit
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Composer audit
|
||||
if: hashFiles('composer.lock') != ''
|
||||
run: |
|
||||
echo "=== Composer Security Audit ==="
|
||||
if ! command -v composer &> /dev/null; then
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq php-cli composer >/dev/null 2>&1
|
||||
fi
|
||||
composer audit --format=plain 2>&1 | tee /tmp/composer-audit.txt
|
||||
RESULT=$?
|
||||
if [ $RESULT -ne 0 ]; then
|
||||
echo "::warning::Composer vulnerabilities found"
|
||||
echo "composer_vulnerable=true" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "No known vulnerabilities in composer dependencies"
|
||||
fi
|
||||
|
||||
- name: NPM audit
|
||||
if: hashFiles('package-lock.json') != ''
|
||||
run: |
|
||||
echo "=== NPM Security Audit ==="
|
||||
npm audit --production 2>&1 | tee /tmp/npm-audit.txt || true
|
||||
if npm audit --production 2>&1 | grep -q "found 0 vulnerabilities"; then
|
||||
echo "No known vulnerabilities in npm dependencies"
|
||||
else
|
||||
echo "::warning::NPM vulnerabilities found"
|
||||
echo "npm_vulnerable=true" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Notify on vulnerabilities
|
||||
if: env.composer_vulnerable == 'true' || env.npm_vulnerable == 'true'
|
||||
run: |
|
||||
REPO="${{ github.event.repository.name }}"
|
||||
curl -sS \
|
||||
-H "Title: ${REPO} has vulnerable dependencies" \
|
||||
-H "Tags: lock,warning" \
|
||||
-H "Priority: high" \
|
||||
-d "Security audit found vulnerabilities. Review dependency updates." \
|
||||
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
- name: Sync upstream bugs
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_MIRROR_TOKEN }}
|
||||
MOKOGITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKOGITEA_URL: https://code.mokoconsulting.tech
|
||||
MOKOGITEA_REPO: MokoConsulting/MokoGitea
|
||||
UPSTREAM_BRANCH: release/v1.26
|
||||
|
||||
+52
-220
@@ -1,229 +1,61 @@
|
||||
# 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
|
||||
`v{upstream}-moko.{major}.{minor}` (e.g. `v1.26.1-moko.06.03`).
|
||||
|
||||
## [v1.26.1-moko.06.10] - 2026-06-06
|
||||
## [06.14.00] --- 2026-06-09
|
||||
|
||||
* 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
|
||||
* feat(api): issue status/priority/type exposed in REST API - GET/PATCH on issues now includes status_id, priority_id, type_id with resolved names
|
||||
* feat(api): org-level issue metadata endpoints - GET /orgs/{org}/issue-statuses, /issue-priorities, /issue-types
|
||||
* feat(wiki): org wiki tab - inline wiki rendering from convention repos (wiki / wiki-private)
|
||||
* feat(wiki): public/private wiki toggle dropdown (same UX as org profile README selector)
|
||||
* feat(wiki): external wiki support - link to an outside URL from the org wiki tab
|
||||
* feat(settings): wiki mode setting in org settings (internal repos vs external URL)
|
||||
* feat(mcp): 5 new MCP tools - gitea_org_issue_statuses_list, gitea_org_issue_priorities_list, gitea_org_issue_types_list, gitea_issue_set_status, gitea_issue_set_priority
|
||||
* feat(mcp): gitea_issue_create and gitea_issue_update now accept status_id, priority_id, type_id
|
||||
|
||||
* MIGRATIONS
|
||||
* 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 moko-platform/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
|
||||
|
||||
## [v1.26.1-moko.03] - 2026-05-15
|
||||
|
||||
* FEATURES
|
||||
* feat(api): Bulk issue operations — add/remove/replace labels, close/reopen, set milestone, assignees (#21)
|
||||
* INFRASTRUCTURE
|
||||
* Grafana: Standardized kiosk header across all 14 playlist dashboards
|
||||
* PROCESS
|
||||
* Reopened 9 closed issues lacking documented testing proof
|
||||
* Created `pending: testing` label for features awaiting verification
|
||||
* Established policy: issues must not be closed without documented testing proof
|
||||
|
||||
## [1.26.1](https://github.com/go-gitea/gitea/releases/tag/v1.26.1) - 2026-04-21
|
||||
|
||||
* BUGFIXES
|
||||
* Add event.schedule context for schedule actions task (#37320) (#37348)
|
||||
* Fix an issue where changing an organization's visibility caused problems when users had forked its repositories. (#37324) (#37344)
|
||||
* Use modern "git update-index --cacheinfo" syntax to support more file names (#37338) (#37343)
|
||||
* Fix URL related escaping for oauth2 (#37334) (#37340)
|
||||
* When the requested arch rpm is missing fall back to noarch (#37236) (#37339)
|
||||
* Fix actions concurrency groups cross-branch leak (#37311) (#37331)
|
||||
* Fix bug when accessing user badges (#37321) (#37329)
|
||||
* Fix AppFullLink (#37325) (#37328)
|
||||
* Fix container auth for public instance (#37290) (#37294)
|
||||
* Enhance GetActionWorkflow to support fallback references (#37189) (#37283)
|
||||
* Fix vite manifest update masking build errors (#37279) (#37310)
|
||||
* Fix Mermaid diagrams failing when node labels contain line breaks (#37296) (#37299)
|
||||
* Use TriggerEvent instead of Event in workflow runs API response for scheduled runs (#37288) #37360
|
||||
* Add URL to Learn more about blocking a user. (#37355) #37367
|
||||
* Fix button layout shift when collapsing file tree in editor (#37363) #37375
|
||||
* Fix org team assignee/reviewer lookups for team member permissions (#37365) #37391
|
||||
* Fix repo init README EOL (#37388) #37399
|
||||
* Fix: dump with default zip type produces uncompressed zip (#37401)#37402
|
||||
* migration 354: add wiki_mode and wiki_url columns to user table for org wiki settings
|
||||
|
||||
@@ -18,7 +18,7 @@ Custom Gitea fork with Project Board API
|
||||
|
||||
---
|
||||
|
||||
**Category:** Infrastructure | **Platform:** [moko-platform wiki](https://code.mokoconsulting.tech/MokoConsulting/moko-platform/wiki)
|
||||
**Category:** Infrastructure | **Platform:** [MokoPlatform wiki](https://code.mokoconsulting.tech/MokoConsulting/MokoPlatform/wiki)
|
||||
|
||||
---
|
||||
|
||||
@@ -40,4 +40,4 @@ This project is licensed under the GNU General Public License v3.0 or later -- s
|
||||
|
||||
---
|
||||
|
||||
*[Moko Consulting](https://mokoconsulting.tech) -- [MokoStandards](https://code.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||
*[Moko Consulting](https://mokoconsulting.tech) -- [MokoStandards](https://code.mokoconsulting.tech/MokoConsulting/MokoPlatform/wiki/Home)*
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// Copyright 2026 The MokoGitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
||||
)
|
||||
|
||||
// OrgSetting stores AI configuration for an organization.
|
||||
type OrgSetting struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
OrgID int64 `xorm:"UNIQUE NOT NULL"`
|
||||
Enabled bool `xorm:"NOT NULL DEFAULT true"`
|
||||
APIKeyEncrypted string `xorm:"TEXT"`
|
||||
Model string `xorm:"VARCHAR(50) NOT NULL DEFAULT 'claude-sonnet-4-6'"`
|
||||
RateLimitRequests int `xorm:"NOT NULL DEFAULT 100"`
|
||||
RateLimitTokensMonth int64 `xorm:"NOT NULL DEFAULT 5000000"`
|
||||
CreatedUnix timeutil.TimeStamp
|
||||
UpdatedUnix timeutil.TimeStamp
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(OrgSetting))
|
||||
db.RegisterModel(new(RepoSetting))
|
||||
db.RegisterModel(new(UsageLog))
|
||||
}
|
||||
|
||||
// TableName returns the table name for OrgSetting.
|
||||
func (OrgSetting) TableName() string {
|
||||
return "ai_org_setting"
|
||||
}
|
||||
|
||||
// RepoSetting stores AI configuration for a repository.
|
||||
type RepoSetting struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"UNIQUE NOT NULL"`
|
||||
Enabled bool `xorm:"NOT NULL DEFAULT true"`
|
||||
AutoReview bool `xorm:"NOT NULL DEFAULT true"`
|
||||
Strictness string `xorm:"VARCHAR(20) NOT NULL DEFAULT 'standard'"`
|
||||
IgnorePatterns string `xorm:"TEXT"`
|
||||
CreatedUnix timeutil.TimeStamp
|
||||
UpdatedUnix timeutil.TimeStamp
|
||||
}
|
||||
|
||||
// TableName returns the table name for RepoSetting.
|
||||
func (RepoSetting) TableName() string {
|
||||
return "ai_repo_setting"
|
||||
}
|
||||
|
||||
// UsageLog records AI token usage per action.
|
||||
type UsageLog struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
OrgID int64 `xorm:"INDEX NOT NULL"`
|
||||
RepoID int64 `xorm:"INDEX NOT NULL"`
|
||||
TriggeredByID int64
|
||||
ActionType string `xorm:"VARCHAR(20) NOT NULL"` // review, chat, agent
|
||||
Model string `xorm:"VARCHAR(50)"`
|
||||
TokensInput int64
|
||||
TokensOutput int64
|
||||
DurationMs int64
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for UsageLog.
|
||||
func (UsageLog) TableName() string {
|
||||
return "ai_usage_log"
|
||||
}
|
||||
|
||||
// GetOrgSetting returns the AI settings for an org, or nil if not configured.
|
||||
func GetOrgSetting(ctx context.Context, orgID int64) (*OrgSetting, error) {
|
||||
setting := &OrgSetting{OrgID: orgID}
|
||||
has, err := db.GetEngine(ctx).Where("org_id = ?", orgID).Get(setting)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, nil
|
||||
}
|
||||
return setting, nil
|
||||
}
|
||||
|
||||
// GetRepoSetting returns the AI settings for a repo, or nil if not configured.
|
||||
func GetRepoSetting(ctx context.Context, repoID int64) (*RepoSetting, error) {
|
||||
setting := &RepoSetting{RepoID: repoID}
|
||||
has, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Get(setting)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, nil
|
||||
}
|
||||
return setting, nil
|
||||
}
|
||||
|
||||
// CreateOrgSetting inserts a new org AI setting.
|
||||
func CreateOrgSetting(ctx context.Context, setting *OrgSetting) error {
|
||||
setting.CreatedUnix = timeutil.TimeStampNow()
|
||||
setting.UpdatedUnix = timeutil.TimeStampNow()
|
||||
_, err := db.GetEngine(ctx).Insert(setting)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateOrgSetting updates an existing org AI setting.
|
||||
func UpdateOrgSetting(ctx context.Context, setting *OrgSetting) error {
|
||||
setting.UpdatedUnix = timeutil.TimeStampNow()
|
||||
_, err := db.GetEngine(ctx).ID(setting.ID).AllCols().Update(setting)
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateRepoSetting inserts a new repo AI setting.
|
||||
func CreateRepoSetting(ctx context.Context, setting *RepoSetting) error {
|
||||
setting.CreatedUnix = timeutil.TimeStampNow()
|
||||
setting.UpdatedUnix = timeutil.TimeStampNow()
|
||||
_, err := db.GetEngine(ctx).Insert(setting)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateRepoSetting updates an existing repo AI setting.
|
||||
func UpdateRepoSetting(ctx context.Context, setting *RepoSetting) error {
|
||||
setting.UpdatedUnix = timeutil.TimeStampNow()
|
||||
_, err := db.GetEngine(ctx).ID(setting.ID).AllCols().Update(setting)
|
||||
return err
|
||||
}
|
||||
|
||||
// LogUsage records an AI usage event.
|
||||
func LogUsage(ctx context.Context, log *UsageLog) error {
|
||||
log.CreatedUnix = timeutil.TimeStampNow()
|
||||
_, err := db.GetEngine(ctx).Insert(log)
|
||||
return err
|
||||
}
|
||||
@@ -428,6 +428,11 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(348, "Add issue priority definitions table", v1_27.AddIssuePriorityDefTable),
|
||||
newMigration(349, "Add security scanning tables", v1_27.AddSecurityScanningTables),
|
||||
newMigration(350, "Add issue type definitions table", v1_27.AddIssueTypeDefTable),
|
||||
newMigration(351, "Add CDN public flag to attachments", v1_27.AddAttachmentCDNPublic),
|
||||
newMigration(352, "Add version prefix and element name to repo manifest", v1_27.AddManifestVersionPrefixAndElement),
|
||||
newMigration(353, "Add distribution metadata fields to repo manifest", v1_27.AddManifestDistributionFields),
|
||||
newMigration(354, "Add org wiki settings to user table", v1_27.AddOrgWikiSettings),
|
||||
newMigration(355, "Migrate update server metadata to repo manifest", v1_27.MigrateUpdateServerFieldsToManifest),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright 2026 The MokoGitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_27
|
||||
|
||||
import (
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type aiOrgSetting struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
OrgID int64 `xorm:"UNIQUE NOT NULL"`
|
||||
Enabled bool `xorm:"NOT NULL DEFAULT true"`
|
||||
APIKeyEncrypted string `xorm:"TEXT"`
|
||||
Model string `xorm:"VARCHAR(50) NOT NULL DEFAULT 'claude-sonnet-4-6'"`
|
||||
RateLimitRequests int `xorm:"NOT NULL DEFAULT 100"`
|
||||
RateLimitTokensMonth int64 `xorm:"NOT NULL DEFAULT 5000000"`
|
||||
CreatedUnix timeutil.TimeStamp
|
||||
UpdatedUnix timeutil.TimeStamp
|
||||
}
|
||||
|
||||
func (aiOrgSetting) TableName() string {
|
||||
return "ai_org_setting"
|
||||
}
|
||||
|
||||
type aiRepoSetting struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"UNIQUE NOT NULL"`
|
||||
Enabled bool `xorm:"NOT NULL DEFAULT true"`
|
||||
AutoReview bool `xorm:"NOT NULL DEFAULT true"`
|
||||
Strictness string `xorm:"VARCHAR(20) NOT NULL DEFAULT 'standard'"`
|
||||
IgnorePatterns string `xorm:"TEXT"`
|
||||
CreatedUnix timeutil.TimeStamp
|
||||
UpdatedUnix timeutil.TimeStamp
|
||||
}
|
||||
|
||||
func (aiRepoSetting) TableName() string {
|
||||
return "ai_repo_setting"
|
||||
}
|
||||
|
||||
type aiUsageLog struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
OrgID int64 `xorm:"INDEX NOT NULL"`
|
||||
RepoID int64 `xorm:"INDEX NOT NULL"`
|
||||
TriggeredByID int64
|
||||
ActionType string `xorm:"VARCHAR(20) NOT NULL"`
|
||||
Model string `xorm:"VARCHAR(50)"`
|
||||
TokensInput int64
|
||||
TokensOutput int64
|
||||
DurationMs int64
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX"`
|
||||
}
|
||||
|
||||
func (aiUsageLog) TableName() string {
|
||||
return "ai_usage_log"
|
||||
}
|
||||
|
||||
func AddAITables(x *xorm.Engine) error {
|
||||
return x.Sync(new(aiOrgSetting), new(aiRepoSetting), new(aiUsageLog))
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
// AddRepoManifestTable creates the repo_manifest table for storing
|
||||
// moko-platform manifest settings per repository.
|
||||
// mokoplatform manifest settings per repository.
|
||||
func AddRepoManifestTable(x *xorm.Engine) error {
|
||||
type RepoManifest struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package v1_27
|
||||
|
||||
import "xorm.io/xorm"
|
||||
|
||||
// AddAttachmentCDNPublic adds the cdn_public column to the attachment table.
|
||||
func AddAttachmentCDNPublic(x *xorm.Engine) error {
|
||||
type Attachment struct {
|
||||
CDNPublic bool `xorm:"NOT NULL DEFAULT false 'cdn_public'"`
|
||||
}
|
||||
return x.Sync(new(Attachment))
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package v1_27
|
||||
|
||||
import "xorm.io/xorm"
|
||||
|
||||
// AddManifestVersionPrefixAndElement adds version_prefix and element_name columns to repo_manifest.
|
||||
func AddManifestVersionPrefixAndElement(x *xorm.Engine) error {
|
||||
type RepoManifest struct {
|
||||
VersionPrefix string `xorm:"TEXT 'version_prefix'"`
|
||||
ElementName string `xorm:"TEXT 'element_name'"`
|
||||
}
|
||||
return x.Sync(new(RepoManifest))
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package v1_27
|
||||
|
||||
import "xorm.io/xorm"
|
||||
|
||||
// AddManifestDistributionFields adds distribution metadata fields to repo_manifest
|
||||
// for update server feed generation (consolidating from UpdateStreamConfig).
|
||||
func AddManifestDistributionFields(x *xorm.Engine) error {
|
||||
type RepoManifest struct {
|
||||
DisplayName string `xorm:"TEXT 'display_name'"`
|
||||
Maintainer string `xorm:"TEXT 'maintainer'"`
|
||||
MaintainerURL string `xorm:"TEXT 'maintainer_url'"`
|
||||
InfoURL string `xorm:"TEXT 'info_url'"`
|
||||
TargetVersion string `xorm:"TEXT 'target_version'"`
|
||||
PHPMinimum string `xorm:"VARCHAR(20) 'php_minimum'"`
|
||||
}
|
||||
return x.Sync(new(RepoManifest))
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package v1_27
|
||||
|
||||
import "xorm.io/xorm"
|
||||
|
||||
// AddOrgWikiSettings adds wiki_mode and wiki_url columns to the user table
|
||||
// for configuring org-level wiki behavior (internal convention repos vs external link).
|
||||
func AddOrgWikiSettings(x *xorm.Engine) error {
|
||||
type User struct {
|
||||
WikiMode string `xorm:"VARCHAR(20) NOT NULL DEFAULT '' 'wiki_mode'"`
|
||||
WikiURL string `xorm:"TEXT 'wiki_url'"`
|
||||
}
|
||||
return x.Sync(new(User))
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package v1_27
|
||||
|
||||
import "xorm.io/xorm"
|
||||
|
||||
// MigrateUpdateServerFieldsToManifest copies extension metadata from
|
||||
// update_stream_config into repo_manifest where the manifest fields are empty.
|
||||
// This consolidates the source of truth into repo_manifest.
|
||||
func MigrateUpdateServerFieldsToManifest(x *xorm.Engine) error {
|
||||
// Copy display_name from config to manifest where manifest is empty
|
||||
_, err := x.Exec(`
|
||||
UPDATE repo_manifest m
|
||||
INNER JOIN update_stream_config c ON m.repo_id = c.repo_id
|
||||
SET m.display_name = c.display_name
|
||||
WHERE (m.display_name IS NULL OR m.display_name = '') AND c.display_name != ''
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy extension_name → element_name
|
||||
_, err = x.Exec(`
|
||||
UPDATE repo_manifest m
|
||||
INNER JOIN update_stream_config c ON m.repo_id = c.repo_id
|
||||
SET m.element_name = c.extension_name
|
||||
WHERE (m.element_name IS NULL OR m.element_name = '') AND c.extension_name != ''
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy extension_type → package_type
|
||||
_, err = x.Exec(`
|
||||
UPDATE repo_manifest m
|
||||
INNER JOIN update_stream_config c ON m.repo_id = c.repo_id
|
||||
SET m.package_type = c.extension_type
|
||||
WHERE (m.package_type IS NULL OR m.package_type = '') AND c.extension_type != ''
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy target_version
|
||||
_, err = x.Exec(`
|
||||
UPDATE repo_manifest m
|
||||
INNER JOIN update_stream_config c ON m.repo_id = c.repo_id
|
||||
SET m.target_version = c.target_version
|
||||
WHERE (m.target_version IS NULL OR m.target_version = '') AND c.target_version != ''
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy maintainer
|
||||
_, err = x.Exec(`
|
||||
UPDATE repo_manifest m
|
||||
INNER JOIN update_stream_config c ON m.repo_id = c.repo_id
|
||||
SET m.maintainer = c.maintainer
|
||||
WHERE (m.maintainer IS NULL OR m.maintainer = '') AND c.maintainer != ''
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy maintainer_url
|
||||
_, err = x.Exec(`
|
||||
UPDATE repo_manifest m
|
||||
INNER JOIN update_stream_config c ON m.repo_id = c.repo_id
|
||||
SET m.maintainer_url = c.maintainer_url
|
||||
WHERE (m.maintainer_url IS NULL OR m.maintainer_url = '') AND c.maintainer_url != ''
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy info_url
|
||||
_, err = x.Exec(`
|
||||
UPDATE repo_manifest m
|
||||
INNER JOIN update_stream_config c ON m.repo_id = c.repo_id
|
||||
SET m.info_url = c.info_url
|
||||
WHERE (m.info_url IS NULL OR m.info_url = '') AND c.info_url != ''
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy php_minimum
|
||||
_, err = x.Exec(`
|
||||
UPDATE repo_manifest m
|
||||
INNER JOIN update_stream_config c ON m.repo_id = c.repo_id
|
||||
SET m.php_minimum = c.php_minimum
|
||||
WHERE (m.php_minimum IS NULL OR m.php_minimum = '') AND c.php_minimum != ''
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy platform from config to manifest where manifest platform is empty
|
||||
_, err = x.Exec(`
|
||||
UPDATE repo_manifest m
|
||||
INNER JOIN update_stream_config c ON m.repo_id = c.repo_id
|
||||
SET m.platform = c.platform
|
||||
WHERE (m.platform IS NULL OR m.platform = '') AND c.platform != ''
|
||||
`)
|
||||
return err
|
||||
}
|
||||
@@ -31,6 +31,7 @@ type Attachment struct {
|
||||
Name string
|
||||
DownloadCount int64 `xorm:"DEFAULT 0"`
|
||||
Size int64 `xorm:"DEFAULT 0"`
|
||||
CDNPublic bool `xorm:"NOT NULL DEFAULT false 'cdn_public'"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
CustomDownloadURL string `xorm:"-"`
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@ func init() {
|
||||
db.RegisterModel(new(RepoManifest))
|
||||
}
|
||||
|
||||
// RepoManifest stores moko-platform manifest settings for a repository.
|
||||
// RepoManifest stores mokoplatform manifest settings for a repository.
|
||||
// These fields correspond to the .mokogitea/manifest.xml schema and are
|
||||
// exposed via API for use by Actions workflows and the moko-platform CLI.
|
||||
// exposed via API for use by Actions workflows and the mokoplatform CLI.
|
||||
type RepoManifest struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"UNIQUE INDEX NOT NULL 'repo_id'"`
|
||||
@@ -31,9 +31,21 @@ type RepoManifest struct {
|
||||
|
||||
// governance section
|
||||
Platform string `xorm:"VARCHAR(50) 'platform'"` // go, php, node, python, etc.
|
||||
StandardsVersion string `xorm:"VARCHAR(20) 'standards_version'"` // moko-platform standards version
|
||||
StandardsVersion string `xorm:"VARCHAR(20) 'standards_version'"` // mokoplatform standards version
|
||||
StandardsSource string `xorm:"TEXT 'standards_source'"` // URL to standards repo
|
||||
|
||||
// versioning
|
||||
VersionPrefix string `xorm:"TEXT 'version_prefix'"` // tag prefix stripped for version display, e.g. "v1.26.1-moko."
|
||||
ElementName string `xorm:"TEXT 'element_name'"` // full element name override, e.g. "pkg_mokowaas" (auto-constructed if empty)
|
||||
|
||||
// distribution metadata (used by update server feed generation)
|
||||
DisplayName string `xorm:"TEXT 'display_name'"` // human-readable name for update feeds, e.g. "Package - MokoWaaS"
|
||||
Maintainer string `xorm:"TEXT 'maintainer'"` // maintainer/author name
|
||||
MaintainerURL string `xorm:"TEXT 'maintainer_url'"` // maintainer website
|
||||
InfoURL string `xorm:"TEXT 'info_url'"` // extension info/product page URL
|
||||
TargetVersion string `xorm:"TEXT 'target_version'"` // target platform version regex, e.g. "(5|6)\..*"
|
||||
PHPMinimum string `xorm:"VARCHAR(20) 'php_minimum'"` // minimum PHP version, e.g. "8.1"
|
||||
|
||||
// build section
|
||||
Language string `xorm:"VARCHAR(50) 'language'"` // Go, PHP, TypeScript, etc.
|
||||
PackageType string `xorm:"VARCHAR(50) 'package_type'"` // application, library, plugin, module, component, package
|
||||
@@ -47,6 +59,45 @@ func (RepoManifest) TableName() string {
|
||||
return "repo_manifest"
|
||||
}
|
||||
|
||||
// joomlaTypePrefix maps Joomla extension types to their element name prefixes.
|
||||
var joomlaTypePrefix = map[string]string{
|
||||
"component": "com_",
|
||||
"module": "mod_",
|
||||
"plugin": "plg_",
|
||||
"package": "pkg_",
|
||||
"template": "tpl_",
|
||||
"library": "lib_",
|
||||
"file": "file_",
|
||||
}
|
||||
|
||||
// AutoElementName returns the auto-constructed Joomla element name (e.g. pkg_mokowaas).
|
||||
func (m *RepoManifest) AutoElementName() string {
|
||||
if m.Name == "" || m.PackageType == "" {
|
||||
return ""
|
||||
}
|
||||
if prefix, ok := joomlaTypePrefix[m.PackageType]; ok {
|
||||
return prefix + m.Name
|
||||
}
|
||||
return m.Name
|
||||
}
|
||||
|
||||
// FullElementName returns the effective element name: override if set, otherwise auto-constructed.
|
||||
func (m *RepoManifest) FullElementName() string {
|
||||
if m.ElementName != "" {
|
||||
return m.ElementName
|
||||
}
|
||||
return m.AutoElementName()
|
||||
}
|
||||
|
||||
// ElementNameMismatch returns true if an override is set that differs from the auto-constructed name.
|
||||
func (m *RepoManifest) ElementNameMismatch() bool {
|
||||
if m.ElementName == "" {
|
||||
return false
|
||||
}
|
||||
auto := m.AutoElementName()
|
||||
return auto != "" && m.ElementName != auto
|
||||
}
|
||||
|
||||
// GetRepoManifest returns the manifest for a repo, or nil if none exists.
|
||||
func GetRepoManifest(ctx context.Context, repoID int64) (*RepoManifest, error) {
|
||||
m := new(RepoManifest)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package licenses
|
||||
package updateserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package licenses
|
||||
package updateserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package licenses
|
||||
package updateserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package licenses
|
||||
package updateserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package licenses
|
||||
package updateserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
+7
-1
@@ -1,7 +1,7 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package licenses
|
||||
package updateserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -82,6 +82,12 @@ func (c *UpdateStreamConfig) GetCustomStreams() []StreamDef {
|
||||
return streams
|
||||
}
|
||||
|
||||
// DeleteRepoConfig removes the repo-level update stream config override.
|
||||
func DeleteRepoConfig(ctx context.Context, repoID int64) error {
|
||||
_, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Delete(new(UpdateStreamConfig))
|
||||
return err
|
||||
}
|
||||
|
||||
// GetActiveStreams returns the effective streams for this config.
|
||||
func (c *UpdateStreamConfig) GetActiveStreams() []StreamDef {
|
||||
if c.StreamMode == "custom" {
|
||||
@@ -153,6 +153,8 @@ type User struct {
|
||||
Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 0"`
|
||||
RepoAdminChangeTeamAccess bool `xorm:"NOT NULL DEFAULT false"`
|
||||
ParentOrgID int64 `xorm:"INDEX DEFAULT 0"` // 0 = no parent (top-level org)
|
||||
WikiMode string `xorm:"VARCHAR(20) NOT NULL DEFAULT '' 'wiki_mode'"` // "" = internal (convention repos), "external" = link to WikiURL
|
||||
WikiURL string `xorm:"TEXT 'wiki_url'"` // external wiki URL (used when WikiMode == "external")
|
||||
|
||||
// Preferences
|
||||
DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"`
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright 2026 The MokoGitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
// AI settings
|
||||
var (
|
||||
AI = struct {
|
||||
Enabled bool
|
||||
DefaultModel string `ini:"DEFAULT_MODEL"`
|
||||
DefaultKey string `ini:"DEFAULT_API_KEY"`
|
||||
ClaudeBinPath string `ini:"CLAUDE_BIN_PATH"`
|
||||
}{
|
||||
Enabled: false,
|
||||
DefaultModel: "claude-sonnet-4-6",
|
||||
}
|
||||
)
|
||||
|
||||
func loadAIFrom(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("ai")
|
||||
AI.Enabled = sec.Key("ENABLED").MustBool(AI.Enabled)
|
||||
AI.DefaultModel = sec.Key("DEFAULT_MODEL").MustString(AI.DefaultModel)
|
||||
AI.DefaultKey = sec.Key("DEFAULT_API_KEY").String()
|
||||
AI.ClaudeBinPath = sec.Key("CLAUDE_BIN_PATH").MustString("claude")
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package setting
|
||||
|
||||
import "time"
|
||||
|
||||
// CDN holds configuration for the built-in CDN asset delivery system.
|
||||
var CDN = struct {
|
||||
Enabled bool
|
||||
Domain string // e.g. "cdn.mokoconsulting.tech"
|
||||
CacheTTL time.Duration // Cache-Control max-age for CDN responses
|
||||
AllowedOrigins []string // CORS origins allowed to fetch CDN assets
|
||||
AllowedIPs []string // IP/CIDR allowlist (empty = allow all)
|
||||
AllowedDomains []string // Referrer domain allowlist (empty = allow all)
|
||||
MaxFileSize int64 // max file size to serve (bytes)
|
||||
}{
|
||||
Enabled: false,
|
||||
Domain: "",
|
||||
CacheTTL: 24 * time.Hour,
|
||||
MaxFileSize: 100 * 1024 * 1024, // 100MB
|
||||
}
|
||||
|
||||
func loadCDNFrom(cfg ConfigProvider) {
|
||||
sec := cfg.Section("cdn")
|
||||
CDN.Enabled = sec.Key("ENABLED").MustBool(false)
|
||||
CDN.Domain = sec.Key("DOMAIN").String()
|
||||
CDN.CacheTTL = sec.Key("CACHE_TTL").MustDuration(CDN.CacheTTL)
|
||||
CDN.MaxFileSize = sec.Key("MAX_FILE_SIZE").MustInt64(CDN.MaxFileSize)
|
||||
|
||||
CDN.AllowedOrigins = sec.Key("ALLOWED_ORIGINS").Strings(",")
|
||||
CDN.AllowedIPs = sec.Key("ALLOWED_IPS").Strings(",")
|
||||
CDN.AllowedDomains = sec.Key("ALLOWED_DOMAINS").Strings(",")
|
||||
}
|
||||
@@ -178,6 +178,7 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error {
|
||||
loadOtherFrom(cfg)
|
||||
loadUpdateCheckerFrom(cfg)
|
||||
loadNtfyFrom(cfg)
|
||||
loadCDNFrom(cfg)
|
||||
loadLoginNotificationFrom(cfg)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -84,6 +84,14 @@ type Issue struct {
|
||||
PinOrder int `json:"pin_order"`
|
||||
// The version of the issue content for optimistic locking
|
||||
ContentVersion int `json:"content_version"`
|
||||
|
||||
// Issue metadata (org-level definitions)
|
||||
StatusID int64 `json:"status_id"`
|
||||
StatusName string `json:"status_name"`
|
||||
PriorityID int64 `json:"priority_id"`
|
||||
PriorityName string `json:"priority_name"`
|
||||
TypeID int64 `json:"type_id"`
|
||||
TypeName string `json:"type_name"`
|
||||
}
|
||||
|
||||
// CreateIssueOption options to create one issue
|
||||
@@ -106,6 +114,10 @@ type CreateIssueOption struct {
|
||||
Closed bool `json:"closed"`
|
||||
// custom field values keyed by field name
|
||||
CustomFields map[string]string `json:"custom_fields,omitempty"`
|
||||
// org-level issue metadata IDs (auto-assigned from org defaults when 0)
|
||||
StatusID int64 `json:"status_id"`
|
||||
PriorityID int64 `json:"priority_id"`
|
||||
TypeID int64 `json:"type_id"`
|
||||
}
|
||||
|
||||
// EditIssueOption options for editing an issue
|
||||
@@ -125,6 +137,10 @@ type EditIssueOption struct {
|
||||
RemoveDeadline *bool `json:"unset_due_date"`
|
||||
// The current version of the issue content to detect conflicts during editing
|
||||
ContentVersion *int `json:"content_version"`
|
||||
// org-level issue metadata IDs
|
||||
StatusID *int64 `json:"status_id,omitempty"`
|
||||
PriorityID *int64 `json:"priority_id,omitempty"`
|
||||
TypeID *int64 `json:"type_id,omitempty"`
|
||||
}
|
||||
|
||||
// EditDeadlineOption options for creating a deadline
|
||||
@@ -141,6 +157,39 @@ type IssueDeadline struct {
|
||||
Deadline *time.Time `json:"due_date"`
|
||||
}
|
||||
|
||||
// IssueStatusDef represents an org-level issue status definition
|
||||
// swagger:model
|
||||
type IssueStatusDef struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Color string `json:"color"`
|
||||
Description string `json:"description"`
|
||||
ClosesIssue bool `json:"closes_issue"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
}
|
||||
|
||||
// IssuePriorityDef represents an org-level issue priority definition
|
||||
// swagger:model
|
||||
type IssuePriorityDef struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Color string `json:"color"`
|
||||
Description string `json:"description"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
}
|
||||
|
||||
// IssueTypeDef represents an org-level issue type definition
|
||||
// swagger:model
|
||||
type IssueTypeDef struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Color string `json:"color"`
|
||||
Description string `json:"description"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
}
|
||||
|
||||
// IssueFormFieldType defines issue form field type, can be "markdown", "textarea", "input", "dropdown" or "checkboxes"
|
||||
//
|
||||
// swagger:enum IssueFormFieldType
|
||||
|
||||
@@ -2734,12 +2734,19 @@
|
||||
"repo.settings.support_url_help": "Shown when downloads are gated. Can point to your wiki, product page, or external support site.",
|
||||
"repo.settings.custom_fields": "Custom Fields",
|
||||
"repo.settings.manifest": "Manifest",
|
||||
"repo.settings.manifest_desc": "Project identity, governance, and build settings from the moko-platform manifest. These are accessible via API for Actions workflows and the moko-platform CLI.",
|
||||
"repo.settings.manifest_desc": "Project identity, governance, and build settings from the MokoPlatform manifest. These are accessible via API for Actions workflows and the MokoPlatform CLI.",
|
||||
"repo.settings.manifest_identity": "Identity",
|
||||
"repo.settings.manifest_name": "Project Name",
|
||||
"repo.settings.manifest_element_name": "Element Name",
|
||||
"repo.settings.manifest_element_name_help": "Base name used to construct the Joomla element identifier (e.g. 'mokowaas'). Combined with the extension type to produce the full element name.",
|
||||
"repo.settings.manifest_element_full": "Full Element Name",
|
||||
"repo.settings.manifest_element_full_help": "Auto-constructed from type + name. Leave blank to use the default, or override for non-standard naming.",
|
||||
"repo.settings.manifest_element_mismatch": "Warning: this overrides the auto-constructed name '%s'. Make sure this matches your Joomla extension's element identifier.",
|
||||
"repo.settings.manifest_package_type_help": "Maps to the Joomla extension type and determines the element prefix (com_, mod_, plg_, pkg_, tpl_, lib_, file_).",
|
||||
"repo.settings.manifest_org": "Organization",
|
||||
"repo.settings.manifest_description": "Description",
|
||||
"repo.settings.manifest_version": "Version",
|
||||
"repo.settings.manifest_version_prefix": "Version Prefix",
|
||||
"repo.settings.manifest_license_spdx": "License (SPDX)",
|
||||
"repo.settings.manifest_license_name": "License Name",
|
||||
"repo.settings.manifest_governance": "Governance",
|
||||
@@ -2749,6 +2756,13 @@
|
||||
"repo.settings.manifest_build": "Build",
|
||||
"repo.settings.manifest_language": "Language",
|
||||
"repo.settings.manifest_package_type": "Package Type",
|
||||
"repo.settings.manifest_distribution": "Distribution",
|
||||
"repo.settings.manifest_display_name": "Display Name",
|
||||
"repo.settings.manifest_maintainer": "Maintainer",
|
||||
"repo.settings.manifest_maintainer_url": "Maintainer URL",
|
||||
"repo.settings.manifest_info_url": "Info / Product URL",
|
||||
"repo.settings.manifest_target_version": "Target Platform Version",
|
||||
"repo.settings.manifest_php_minimum": "Minimum PHP Version",
|
||||
"repo.settings.manifest_entry_point": "Entry Point",
|
||||
"repo.settings.manifest_save": "Save Manifest",
|
||||
"repo.settings.manifest_saved": "Manifest settings saved.",
|
||||
@@ -2831,6 +2845,8 @@
|
||||
"repo.release.message": "Describe this release",
|
||||
"repo.release.prerelease_desc": "Mark as Pre-Release",
|
||||
"repo.release.prerelease_helper": "Mark this release unsuitable for production use.",
|
||||
"repo.release.cdn_public": "CDN",
|
||||
"repo.release.cdn_public_tooltip": "Make this asset available via the CDN. Disabled when the release is assigned to an update stream.",
|
||||
"repo.release.cancel": "Cancel",
|
||||
"repo.release.publish": "Publish Release",
|
||||
"repo.release.save_draft": "Save Draft",
|
||||
@@ -2960,6 +2976,8 @@
|
||||
"org.settings.custom_field_options": "Options (JSON)",
|
||||
"org.settings.custom_field_options_help": "For dropdown fields, enter options as a JSON array.",
|
||||
"org.settings.custom_field_description": "Description",
|
||||
"org.settings.custom_field_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_updated": "Custom field updated.",
|
||||
"org.settings.custom_field_deleted": "Custom field deleted.",
|
||||
|
||||
@@ -1306,11 +1306,11 @@ func Routes() *web.Router {
|
||||
m.Combo("/{timetrackingusername}").Get(repo.ListTrackedTimesByUser)
|
||||
}, mustEnableIssues, reqToken())
|
||||
m.Group("/wiki", func() {
|
||||
m.Combo("/page/{pageName}").
|
||||
m.Combo("/page/*").
|
||||
Get(repo.GetWikiPage).
|
||||
Patch(mustNotBeArchived, reqToken(), reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.EditWikiPage).
|
||||
Delete(mustNotBeArchived, reqToken(), reqRepoWriter(unit.TypeWiki), repo.DeleteWikiPage)
|
||||
m.Get("/revisions/{pageName}", repo.ListPageRevisions)
|
||||
m.Get("/revisions/*", repo.ListPageRevisions)
|
||||
m.Post("/new", reqToken(), mustNotBeArchived, reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.NewWikiPage)
|
||||
m.Get("/pages", repo.ListWikiPages)
|
||||
}, mustEnableWiki)
|
||||
@@ -1479,7 +1479,10 @@ func Routes() *web.Router {
|
||||
Delete(reqToken(), repo.DeleteTopic)
|
||||
}, reqAdmin())
|
||||
}, reqAnyRepoReader())
|
||||
m.Combo("/manifest", reqRepoReader(unit.TypeCode)).
|
||||
m.Combo("/metadata", reqRepoReader(unit.TypeCode)).
|
||||
Get(repo.GetRepoManifest).
|
||||
Put(reqToken(), reqAdmin(), repo.UpdateRepoManifest)
|
||||
m.Combo("/manifest", reqRepoReader(unit.TypeCode)). // backward compat
|
||||
Get(repo.GetRepoManifest).
|
||||
Put(reqToken(), reqAdmin(), repo.UpdateRepoManifest)
|
||||
// MokoGitea badge engine
|
||||
@@ -1773,6 +1776,9 @@ func Routes() *web.Router {
|
||||
m.Post("", reqToken(), reqOrgOwnership(), org.CreateOrgCustomField)
|
||||
m.Delete("/{id}", reqToken(), reqOrgOwnership(), org.DeleteOrgCustomField)
|
||||
})
|
||||
m.Get("/issue-statuses", org.ListIssueStatuses)
|
||||
m.Get("/issue-priorities", org.ListIssuePriorities)
|
||||
m.Get("/issue-types", org.ListIssueTypes)
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true), checkTokenPublicOnly())
|
||||
m.Group("/teams/{teamid}", func() {
|
||||
m.Combo("").Get(reqToken(), org.GetTeam).
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
|
||||
api "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
)
|
||||
|
||||
// ListIssueStatuses returns active issue status definitions for an org.
|
||||
func ListIssueStatuses(ctx *context.APIContext) {
|
||||
// swagger:operation GET /orgs/{org}/issue-statuses organization orgListIssueStatuses
|
||||
// ---
|
||||
// summary: List an organization's issue status definitions
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// description: "IssueStatusDefList"
|
||||
// schema:
|
||||
// type: array
|
||||
// items:
|
||||
// "$ref": "#/definitions/IssueStatusDef"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
defs, err := issues_model.GetIssueStatusDefsByOrg(ctx, ctx.Org.Organization.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
result := make([]*api.IssueStatusDef, 0, len(defs))
|
||||
for _, d := range defs {
|
||||
result = append(result, &api.IssueStatusDef{
|
||||
ID: d.ID,
|
||||
Name: d.Name,
|
||||
Color: d.Color,
|
||||
Description: d.Description,
|
||||
ClosesIssue: d.ClosesIssue,
|
||||
SortOrder: d.SortOrder,
|
||||
})
|
||||
}
|
||||
ctx.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// ListIssuePriorities returns active issue priority definitions for an org.
|
||||
func ListIssuePriorities(ctx *context.APIContext) {
|
||||
// swagger:operation GET /orgs/{org}/issue-priorities organization orgListIssuePriorities
|
||||
// ---
|
||||
// summary: List an organization's issue priority definitions
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// description: "IssuePriorityDefList"
|
||||
// schema:
|
||||
// type: array
|
||||
// items:
|
||||
// "$ref": "#/definitions/IssuePriorityDef"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
defs, err := issues_model.GetIssuePriorityDefsByOrg(ctx, ctx.Org.Organization.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
result := make([]*api.IssuePriorityDef, 0, len(defs))
|
||||
for _, d := range defs {
|
||||
result = append(result, &api.IssuePriorityDef{
|
||||
ID: d.ID,
|
||||
Name: d.Name,
|
||||
Color: d.Color,
|
||||
Description: d.Description,
|
||||
SortOrder: d.SortOrder,
|
||||
IsDefault: d.IsDefault,
|
||||
})
|
||||
}
|
||||
ctx.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// ListIssueTypes returns active issue type definitions for an org.
|
||||
func ListIssueTypes(ctx *context.APIContext) {
|
||||
// swagger:operation GET /orgs/{org}/issue-types organization orgListIssueTypes
|
||||
// ---
|
||||
// summary: List an organization's issue type definitions
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// description: "IssueTypeDefList"
|
||||
// schema:
|
||||
// type: array
|
||||
// items:
|
||||
// "$ref": "#/definitions/IssueTypeDef"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
defs, err := issues_model.GetIssueTypeDefsByOrg(ctx, ctx.Org.Organization.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
result := make([]*api.IssueTypeDef, 0, len(defs))
|
||||
for _, d := range defs {
|
||||
result = append(result, &api.IssueTypeDef{
|
||||
ID: d.ID,
|
||||
Name: d.Name,
|
||||
Color: d.Color,
|
||||
Description: d.Description,
|
||||
SortOrder: d.SortOrder,
|
||||
IsDefault: d.IsDefault,
|
||||
})
|
||||
}
|
||||
ctx.JSON(http.StatusOK, result)
|
||||
}
|
||||
@@ -722,6 +722,22 @@ func CreateIssue(ctx *context.APIContext) {
|
||||
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 errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.APIError(http.StatusForbidden, err)
|
||||
@@ -733,24 +749,65 @@ func CreateIssue(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
// Save custom field values if provided (resolve field names to IDs).
|
||||
if len(form.CustomFields) > 0 {
|
||||
defs, defErr := issues_model.GetCustomFieldsByOwner(ctx, ctx.Repo.Repository.OwnerID, issues_model.CustomFieldScopeIssue)
|
||||
if defErr != nil {
|
||||
ctx.APIErrorInternal(defErr)
|
||||
// Save custom field values (reuse defs from validation above).
|
||||
if len(customFieldDefs) > 0 && len(form.CustomFields) > 0 {
|
||||
vals := make(map[int64]string)
|
||||
for _, def := range customFieldDefs {
|
||||
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).
|
||||
// Use provided value if > 0, otherwise auto-assign org default.
|
||||
if form.StatusID > 0 {
|
||||
if err := issues_model.SetIssueStatusID(ctx, issue.ID, form.StatusID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if len(defs) > 0 {
|
||||
vals := make(map[int64]string)
|
||||
for _, def := range defs {
|
||||
if v, ok := form.CustomFields[def.Name]; ok {
|
||||
vals[def.ID] = v
|
||||
} 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 len(vals) > 0 {
|
||||
if setErr := issues_model.SetCustomFieldValues(ctx, issue.ID, vals); setErr != nil {
|
||||
ctx.APIErrorInternal(setErr)
|
||||
return
|
||||
}
|
||||
}
|
||||
if form.PriorityID > 0 {
|
||||
if err := issues_model.SetIssuePriorityID(ctx, issue.ID, form.PriorityID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
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 err := issues_model.SetIssueTypeID(ctx, issue.ID, form.TypeID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -980,6 +1037,26 @@ func EditIssue(ctx *context.APIContext) {
|
||||
}
|
||||
}
|
||||
|
||||
// Update org-level issue metadata (status/priority/type)
|
||||
if canWrite && form.StatusID != nil {
|
||||
if err := issues_model.SetIssueStatusID(ctx, issue.ID, *form.StatusID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if canWrite && form.PriorityID != nil {
|
||||
if err := issues_model.SetIssuePriorityID(ctx, issue.ID, *form.PriorityID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if canWrite && form.TypeID != nil {
|
||||
if err := issues_model.SetIssueTypeID(ctx, issue.ID, *form.TypeID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Refetch from database to assign some automatic values
|
||||
issue, err = issues_model.GetIssueByID(ctx, issue.ID)
|
||||
if err != nil {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/web"
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
|
||||
// GetLicenseSettings returns the licensing/update stream settings for a repo.
|
||||
func GetLicenseSettings(ctx *context.APIContext) {
|
||||
cfg := licenses.GetEffectiveConfig(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID)
|
||||
cfg := updateserver_model.GetEffectiveConfig(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID)
|
||||
if cfg == nil {
|
||||
ctx.JSON(http.StatusOK, &structs.LicenseSettings{})
|
||||
return
|
||||
@@ -42,7 +42,7 @@ func GetLicenseSettings(ctx *context.APIContext) {
|
||||
func UpdateLicenseSettings(ctx *context.APIContext) {
|
||||
form := web.GetForm(ctx).(*structs.LicenseSettings)
|
||||
|
||||
cfg := &licenses.UpdateStreamConfig{
|
||||
cfg := &updateserver_model.UpdateStreamConfig{
|
||||
OwnerID: ctx.Repo.Repository.OwnerID,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
LicensingEnabled: form.LicensingEnabled,
|
||||
@@ -61,7 +61,7 @@ func UpdateLicenseSettings(ctx *context.APIContext) {
|
||||
StreamMode: "joomla",
|
||||
}
|
||||
|
||||
if err := licenses.SaveConfig(ctx, cfg); err != nil {
|
||||
if err := updateserver_model.SaveConfig(ctx, cfg); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
@@ -70,7 +70,7 @@ func UpdateLicenseSettings(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
// verifyPackageOwnership checks that a package belongs to the current repo's owner.
|
||||
func verifyPackageOwnership(ctx *context.APIContext, pkg *licenses.LicensePackage) bool {
|
||||
func verifyPackageOwnership(ctx *context.APIContext, pkg *updateserver_model.LicensePackage) bool {
|
||||
if pkg.OwnerID != ctx.Repo.Repository.OwnerID {
|
||||
ctx.APIErrorNotFound(nil)
|
||||
return false
|
||||
@@ -79,7 +79,7 @@ func verifyPackageOwnership(ctx *context.APIContext, pkg *licenses.LicensePackag
|
||||
}
|
||||
|
||||
// verifyKeyOwnership checks that a key belongs to the current repo's owner.
|
||||
func verifyKeyOwnership(ctx *context.APIContext, key *licenses.LicenseKey) bool {
|
||||
func verifyKeyOwnership(ctx *context.APIContext, key *updateserver_model.LicenseKey) bool {
|
||||
if key.OwnerID != ctx.Repo.Repository.OwnerID {
|
||||
ctx.APIErrorNotFound(nil)
|
||||
return false
|
||||
@@ -87,7 +87,7 @@ func verifyKeyOwnership(ctx *context.APIContext, key *licenses.LicenseKey) bool
|
||||
return true
|
||||
}
|
||||
|
||||
func toLicensePackageAPI(pkg *licenses.LicensePackage) *structs.LicensePackage {
|
||||
func toLicensePackageAPI(pkg *updateserver_model.LicensePackage) *structs.LicensePackage {
|
||||
return &structs.LicensePackage{
|
||||
ID: pkg.ID,
|
||||
OwnerID: pkg.OwnerID,
|
||||
@@ -103,7 +103,7 @@ func toLicensePackageAPI(pkg *licenses.LicensePackage) *structs.LicensePackage {
|
||||
}
|
||||
}
|
||||
|
||||
func toLicenseKeyAPI(key *licenses.LicenseKey) *structs.LicenseKey {
|
||||
func toLicenseKeyAPI(key *updateserver_model.LicenseKey) *structs.LicenseKey {
|
||||
lk := &structs.LicenseKey{
|
||||
ID: key.ID,
|
||||
PackageID: key.PackageID,
|
||||
@@ -134,7 +134,7 @@ func toLicenseKeyAPI(key *licenses.LicenseKey) *structs.LicenseKey {
|
||||
|
||||
// ListLicensePackages lists license packages for the repo owner.
|
||||
func ListLicensePackages(ctx *context.APIContext) {
|
||||
pkgs, err := licenses.ListLicensePackages(ctx, ctx.Repo.Repository.OwnerID)
|
||||
pkgs, err := updateserver_model.ListLicensePackages(ctx, ctx.Repo.Repository.OwnerID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
@@ -151,7 +151,7 @@ func ListLicensePackages(ctx *context.APIContext) {
|
||||
func CreateLicensePackage(ctx *context.APIContext) {
|
||||
form := web.GetForm(ctx).(*structs.CreateLicensePackageOption)
|
||||
|
||||
pkg := &licenses.LicensePackage{
|
||||
pkg := &updateserver_model.LicensePackage{
|
||||
OwnerID: ctx.Repo.Repository.OwnerID,
|
||||
Name: form.Name,
|
||||
Description: form.Description,
|
||||
@@ -165,7 +165,7 @@ func CreateLicensePackage(ctx *context.APIContext) {
|
||||
pkg.RepoScope = "all"
|
||||
}
|
||||
|
||||
if err := licenses.CreateLicensePackage(ctx, pkg); err != nil {
|
||||
if err := updateserver_model.CreateLicensePackage(ctx, pkg); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
@@ -178,7 +178,7 @@ func EditLicensePackage(ctx *context.APIContext) {
|
||||
form := web.GetForm(ctx).(*structs.EditLicensePackageOption)
|
||||
pkgID := ctx.PathParamInt64("id")
|
||||
|
||||
pkg, err := licenses.GetLicensePackageByID(ctx, pkgID)
|
||||
pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID)
|
||||
if err != nil {
|
||||
ctx.APIErrorNotFound(err)
|
||||
return
|
||||
@@ -187,7 +187,7 @@ func EditLicensePackage(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if pkg.Name == licenses.MasterPackageName {
|
||||
if pkg.Name == updateserver_model.MasterPackageName {
|
||||
ctx.APIError(http.StatusForbidden, "master package cannot be edited")
|
||||
return
|
||||
}
|
||||
@@ -214,7 +214,7 @@ func EditLicensePackage(ctx *context.APIContext) {
|
||||
pkg.IsActive = *form.IsActive
|
||||
}
|
||||
|
||||
if err := licenses.UpdateLicensePackage(ctx, pkg); err != nil {
|
||||
if err := updateserver_model.UpdateLicensePackage(ctx, pkg); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
@@ -226,7 +226,7 @@ func EditLicensePackage(ctx *context.APIContext) {
|
||||
func DeleteLicensePackage(ctx *context.APIContext) {
|
||||
pkgID := ctx.PathParamInt64("id")
|
||||
|
||||
pkg, err := licenses.GetLicensePackageByID(ctx, pkgID)
|
||||
pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID)
|
||||
if err != nil {
|
||||
ctx.APIErrorNotFound(err)
|
||||
return
|
||||
@@ -235,12 +235,12 @@ func DeleteLicensePackage(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if pkg.Name == licenses.MasterPackageName {
|
||||
if pkg.Name == updateserver_model.MasterPackageName {
|
||||
ctx.APIError(http.StatusForbidden, "master package cannot be deleted")
|
||||
return
|
||||
}
|
||||
|
||||
if err := licenses.DeleteLicensePackage(ctx, pkgID); err != nil {
|
||||
if err := updateserver_model.DeleteLicensePackage(ctx, pkgID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
@@ -252,7 +252,7 @@ func DeleteLicensePackage(ctx *context.APIContext) {
|
||||
func ArchiveLicensePackage(ctx *context.APIContext) {
|
||||
pkgID := ctx.PathParamInt64("id")
|
||||
|
||||
pkg, err := licenses.GetLicensePackageByID(ctx, pkgID)
|
||||
pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID)
|
||||
if err != nil {
|
||||
ctx.APIErrorNotFound(err)
|
||||
return
|
||||
@@ -261,12 +261,12 @@ func ArchiveLicensePackage(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if pkg.Name == licenses.MasterPackageName {
|
||||
if pkg.Name == updateserver_model.MasterPackageName {
|
||||
ctx.APIError(http.StatusForbidden, "master package cannot be archived")
|
||||
return
|
||||
}
|
||||
|
||||
if err := licenses.ArchiveLicensePackage(ctx, pkgID); err != nil {
|
||||
if err := updateserver_model.ArchiveLicensePackage(ctx, pkgID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
@@ -277,7 +277,7 @@ func ArchiveLicensePackage(ctx *context.APIContext) {
|
||||
// UnarchiveLicensePackage restores an archived license package via API.
|
||||
func UnarchiveLicensePackage(ctx *context.APIContext) {
|
||||
pkgID := ctx.PathParamInt64("id")
|
||||
pkg, err := licenses.GetLicensePackageByID(ctx, pkgID)
|
||||
pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID)
|
||||
if err != nil {
|
||||
ctx.APIErrorNotFound(err)
|
||||
return
|
||||
@@ -285,7 +285,7 @@ func UnarchiveLicensePackage(ctx *context.APIContext) {
|
||||
if !verifyPackageOwnership(ctx, pkg) {
|
||||
return
|
||||
}
|
||||
if err := licenses.UnarchiveLicensePackage(ctx, pkgID); err != nil {
|
||||
if err := updateserver_model.UnarchiveLicensePackage(ctx, pkgID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
@@ -294,7 +294,7 @@ func UnarchiveLicensePackage(ctx *context.APIContext) {
|
||||
|
||||
// ListLicenseKeys lists license keys for the repo owner.
|
||||
func ListLicenseKeys(ctx *context.APIContext) {
|
||||
keys, err := licenses.ListLicenseKeys(ctx, ctx.Repo.Repository.OwnerID)
|
||||
keys, err := updateserver_model.ListLicenseKeys(ctx, ctx.Repo.Repository.OwnerID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
@@ -311,7 +311,7 @@ func ListLicenseKeys(ctx *context.APIContext) {
|
||||
func CreateLicenseKey(ctx *context.APIContext) {
|
||||
form := web.GetForm(ctx).(*structs.CreateLicenseKeyOption)
|
||||
|
||||
key := &licenses.LicenseKey{
|
||||
key := &updateserver_model.LicenseKey{
|
||||
PackageID: form.PackageID,
|
||||
OwnerID: ctx.Repo.Repository.OwnerID,
|
||||
LicenseeName: form.LicenseeName,
|
||||
@@ -329,7 +329,7 @@ func CreateLicenseKey(ctx *context.APIContext) {
|
||||
key.ExpiresUnix = timeutil.TimeStamp(form.ExpiresAt.Unix())
|
||||
} else {
|
||||
// Auto-calculate from package duration.
|
||||
pkg, err := licenses.GetLicensePackageByID(ctx, form.PackageID)
|
||||
pkg, err := updateserver_model.GetLicensePackageByID(ctx, form.PackageID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
@@ -344,7 +344,7 @@ func CreateLicenseKey(ctx *context.APIContext) {
|
||||
}
|
||||
}
|
||||
|
||||
rawKey, err := licenses.CreateLicenseKey(ctx, key)
|
||||
rawKey, err := updateserver_model.CreateLicenseKey(ctx, key)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
@@ -362,7 +362,7 @@ func EditLicenseKey(ctx *context.APIContext) {
|
||||
form := web.GetForm(ctx).(*structs.EditLicenseKeyOption)
|
||||
keyID := ctx.PathParamInt64("id")
|
||||
|
||||
key, err := licenses.GetLicenseKeyByID(ctx, keyID)
|
||||
key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID)
|
||||
if err != nil {
|
||||
ctx.APIErrorNotFound(err)
|
||||
return
|
||||
@@ -395,7 +395,7 @@ func EditLicenseKey(ctx *context.APIContext) {
|
||||
key.ExpiresUnix = timeutil.TimeStamp(form.ExpiresAt.Unix())
|
||||
}
|
||||
|
||||
if err := licenses.UpdateLicenseKey(ctx, key); err != nil {
|
||||
if err := updateserver_model.UpdateLicenseKey(ctx, key); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
@@ -409,7 +409,7 @@ func PurchaseLicenseKey(ctx *context.APIContext) {
|
||||
|
||||
// Idempotency check: if payment_ref already exists, return existing key.
|
||||
if form.PaymentRef != "" {
|
||||
existing, err := licenses.GetLicenseKeyByPaymentRef(ctx, form.PaymentRef)
|
||||
existing, err := updateserver_model.GetLicenseKeyByPaymentRef(ctx, form.PaymentRef)
|
||||
if err == nil {
|
||||
resp := &structs.LicenseKeyCreated{
|
||||
LicenseKey: *toLicenseKeyAPI(existing),
|
||||
@@ -420,7 +420,7 @@ func PurchaseLicenseKey(ctx *context.APIContext) {
|
||||
}
|
||||
}
|
||||
|
||||
pkg, err := licenses.GetLicensePackageByID(ctx, form.PackageID)
|
||||
pkg, err := updateserver_model.GetLicensePackageByID(ctx, form.PackageID)
|
||||
if err != nil {
|
||||
ctx.APIErrorNotFound(err)
|
||||
return
|
||||
@@ -429,7 +429,7 @@ func PurchaseLicenseKey(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
key := &licenses.LicenseKey{
|
||||
key := &updateserver_model.LicenseKey{
|
||||
PackageID: form.PackageID,
|
||||
OwnerID: ctx.Repo.Repository.OwnerID,
|
||||
LicenseeName: form.LicenseeName,
|
||||
@@ -444,7 +444,7 @@ func PurchaseLicenseKey(ctx *context.APIContext) {
|
||||
key.ExpiresUnix = timeutil.TimeStamp(expires.Unix())
|
||||
}
|
||||
|
||||
rawKey, err := licenses.CreateLicenseKey(ctx, key)
|
||||
rawKey, err := updateserver_model.CreateLicenseKey(ctx, key)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
@@ -460,7 +460,7 @@ func PurchaseLicenseKey(ctx *context.APIContext) {
|
||||
// RenewLicenseKey extends a key's expiration by its package duration.
|
||||
func RenewLicenseKey(ctx *context.APIContext) {
|
||||
keyID := ctx.PathParamInt64("id")
|
||||
key, err := licenses.GetLicenseKeyByID(ctx, keyID)
|
||||
key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID)
|
||||
if err != nil {
|
||||
ctx.APIErrorNotFound(err)
|
||||
return
|
||||
@@ -469,7 +469,7 @@ func RenewLicenseKey(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
pkg, err := licenses.GetLicensePackageByID(ctx, key.PackageID)
|
||||
pkg, err := updateserver_model.GetLicensePackageByID(ctx, key.PackageID)
|
||||
if err != nil {
|
||||
ctx.APIErrorNotFound(err)
|
||||
return
|
||||
@@ -480,20 +480,20 @@ func RenewLicenseKey(ctx *context.APIContext) {
|
||||
days = 365
|
||||
}
|
||||
|
||||
if err := licenses.RenewLicenseKey(ctx, keyID, days); err != nil {
|
||||
if err := updateserver_model.RenewLicenseKey(ctx, keyID, days); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Reload key to get updated fields.
|
||||
key, _ = licenses.GetLicenseKeyByID(ctx, keyID)
|
||||
key, _ = updateserver_model.GetLicenseKeyByID(ctx, keyID)
|
||||
ctx.JSON(http.StatusOK, toLicenseKeyAPI(key))
|
||||
}
|
||||
|
||||
// RevokeLicenseKey deactivates a license key via API.
|
||||
func RevokeLicenseKey(ctx *context.APIContext) {
|
||||
keyID := ctx.PathParamInt64("id")
|
||||
key, err := licenses.GetLicenseKeyByID(ctx, keyID)
|
||||
key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID)
|
||||
if err != nil {
|
||||
ctx.APIErrorNotFound(err)
|
||||
return
|
||||
@@ -503,7 +503,7 @@ func RevokeLicenseKey(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
key.IsActive = false
|
||||
if err := licenses.UpdateLicenseKey(ctx, key); err != nil {
|
||||
if err := updateserver_model.UpdateLicenseKey(ctx, key); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
@@ -514,7 +514,7 @@ func RevokeLicenseKey(ctx *context.APIContext) {
|
||||
// DeleteLicenseKey deletes a license key.
|
||||
func DeleteLicenseKey(ctx *context.APIContext) {
|
||||
keyID := ctx.PathParamInt64("id")
|
||||
key, err := licenses.GetLicenseKeyByID(ctx, keyID)
|
||||
key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID)
|
||||
if err != nil {
|
||||
ctx.APIErrorNotFound(err)
|
||||
return
|
||||
@@ -522,7 +522,7 @@ func DeleteLicenseKey(ctx *context.APIContext) {
|
||||
if !verifyKeyOwnership(ctx, key) {
|
||||
return
|
||||
}
|
||||
if err := licenses.DeleteLicenseKey(ctx, keyID); err != nil {
|
||||
if err := updateserver_model.DeleteLicenseKey(ctx, keyID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
@@ -533,7 +533,7 @@ func DeleteLicenseKey(ctx *context.APIContext) {
|
||||
func ValidateLicenseKey(ctx *context.APIContext) {
|
||||
form := web.GetForm(ctx).(*structs.ValidateLicenseKeyOption)
|
||||
|
||||
key, pkg, err := licenses.ValidateLicenseKey(ctx, form.Key, form.Domain)
|
||||
key, pkg, err := updateserver_model.ValidateLicenseKey(ctx, form.Key, form.Domain)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusOK, &structs.ValidateLicenseKeyResponse{
|
||||
Valid: false,
|
||||
@@ -542,7 +542,7 @@ func ValidateLicenseKey(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
_ = licenses.TouchHeartbeat(ctx, key.ID)
|
||||
_ = updateserver_model.TouchHeartbeat(ctx, key.ID)
|
||||
|
||||
var expiresAt *time.Time
|
||||
if key.ExpiresUnix > 0 {
|
||||
@@ -555,7 +555,7 @@ func ValidateLicenseKey(ctx *context.APIContext) {
|
||||
maxSites = pkg.MaxSites
|
||||
}
|
||||
|
||||
sitesUsed, _ := licenses.CountUniqueDomainsByKey(ctx, key.ID)
|
||||
sitesUsed, _ := updateserver_model.CountUniqueDomainsByKey(ctx, key.ID)
|
||||
|
||||
ctx.JSON(http.StatusOK, &structs.ValidateLicenseKeyResponse{
|
||||
Valid: true,
|
||||
@@ -569,7 +569,7 @@ func ValidateLicenseKey(ctx *context.APIContext) {
|
||||
|
||||
// GetLicenseKeyUsage returns usage logs for a license key.
|
||||
func GetLicenseKeyUsage(ctx *context.APIContext) {
|
||||
usages, err := licenses.GetRecentUsage(ctx, ctx.PathParamInt64("id"), 100)
|
||||
usages, err := updateserver_model.GetRecentUsage(ctx, ctx.PathParamInt64("id"), 100)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
|
||||
@@ -19,9 +19,17 @@ type apiManifest struct {
|
||||
Version string `json:"version"`
|
||||
LicenseSPDX string `json:"license_spdx"`
|
||||
LicenseName string `json:"license_name"`
|
||||
VersionPrefix string `json:"version_prefix"`
|
||||
ElementName string `json:"element_name"`
|
||||
Platform string `json:"platform"`
|
||||
StandardsVersion string `json:"standards_version"`
|
||||
StandardsSource string `json:"standards_source"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Maintainer string `json:"maintainer"`
|
||||
MaintainerURL string `json:"maintainer_url"`
|
||||
InfoURL string `json:"info_url"`
|
||||
TargetVersion string `json:"target_version"`
|
||||
PHPMinimum string `json:"php_minimum"`
|
||||
Language string `json:"language"`
|
||||
PackageType string `json:"package_type"`
|
||||
EntryPoint string `json:"entry_point"`
|
||||
@@ -60,9 +68,17 @@ func GetRepoManifest(ctx *context.APIContext) {
|
||||
Version: m.Version,
|
||||
LicenseSPDX: m.LicenseSPDX,
|
||||
LicenseName: m.LicenseName,
|
||||
VersionPrefix: m.VersionPrefix,
|
||||
ElementName: m.FullElementName(),
|
||||
Platform: m.Platform,
|
||||
StandardsVersion: m.StandardsVersion,
|
||||
StandardsSource: m.StandardsSource,
|
||||
DisplayName: m.DisplayName,
|
||||
Maintainer: m.Maintainer,
|
||||
MaintainerURL: m.MaintainerURL,
|
||||
InfoURL: m.InfoURL,
|
||||
TargetVersion: m.TargetVersion,
|
||||
PHPMinimum: m.PHPMinimum,
|
||||
Language: m.Language,
|
||||
PackageType: m.PackageType,
|
||||
EntryPoint: m.EntryPoint,
|
||||
@@ -95,9 +111,17 @@ func UpdateRepoManifest(ctx *context.APIContext) {
|
||||
Version: req.Version,
|
||||
LicenseSPDX: req.LicenseSPDX,
|
||||
LicenseName: req.LicenseName,
|
||||
VersionPrefix: req.VersionPrefix,
|
||||
ElementName: req.ElementName,
|
||||
Platform: req.Platform,
|
||||
StandardsVersion: req.StandardsVersion,
|
||||
StandardsSource: req.StandardsSource,
|
||||
DisplayName: req.DisplayName,
|
||||
Maintainer: req.Maintainer,
|
||||
MaintainerURL: req.MaintainerURL,
|
||||
InfoURL: req.InfoURL,
|
||||
TargetVersion: req.TargetVersion,
|
||||
PHPMinimum: req.PHPMinimum,
|
||||
Language: req.Language,
|
||||
PackageType: req.PackageType,
|
||||
EntryPoint: req.EntryPoint,
|
||||
@@ -115,9 +139,17 @@ func UpdateRepoManifest(ctx *context.APIContext) {
|
||||
Version: m.Version,
|
||||
LicenseSPDX: m.LicenseSPDX,
|
||||
LicenseName: m.LicenseName,
|
||||
VersionPrefix: m.VersionPrefix,
|
||||
ElementName: m.FullElementName(),
|
||||
Platform: m.Platform,
|
||||
StandardsVersion: m.StandardsVersion,
|
||||
StandardsSource: m.StandardsSource,
|
||||
DisplayName: m.DisplayName,
|
||||
Maintainer: m.Maintainer,
|
||||
MaintainerURL: m.MaintainerURL,
|
||||
InfoURL: m.InfoURL,
|
||||
TargetVersion: m.TargetVersion,
|
||||
PHPMinimum: m.PHPMinimum,
|
||||
Language: m.Language,
|
||||
PackageType: m.PackageType,
|
||||
EntryPoint: m.EntryPoint,
|
||||
|
||||
+25
-11
@@ -12,6 +12,7 @@ import (
|
||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/gitrepo"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
||||
api "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/util"
|
||||
@@ -136,7 +137,7 @@ func EditWikiPage(ctx *context.APIContext) {
|
||||
|
||||
form := web.GetForm(ctx).(*api.CreateWikiPageOptions)
|
||||
|
||||
oldWikiName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("pageName"))
|
||||
oldWikiName := wiki_service.WebPathFromRequest(ctx.PathParam("*"))
|
||||
newWikiName := wiki_service.UserTitleToWebPath("", form.Title)
|
||||
|
||||
if len(newWikiName) == 0 {
|
||||
@@ -242,7 +243,7 @@ func DeleteWikiPage(ctx *context.APIContext) {
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
|
||||
wikiName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("pageName"))
|
||||
wikiName := wiki_service.WebPathFromRequest(ctx.PathParam("*"))
|
||||
|
||||
if err := wiki_service.DeleteWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName); err != nil {
|
||||
if err.Error() == "file does not exist" {
|
||||
@@ -307,14 +308,23 @@ func ListWikiPages(ctx *context.APIContext) {
|
||||
skip := (page - 1) * limit
|
||||
maxNum := page * limit
|
||||
|
||||
entries, err := commit.ListEntries()
|
||||
entries, err := commit.ListEntriesRecursiveFast()
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
pages := make([]*api.WikiPageMetaData, 0, len(entries))
|
||||
for i, entry := range entries {
|
||||
if i < skip || i >= maxNum || !entry.IsRegular() {
|
||||
|
||||
// Filter to regular files only and count for pagination.
|
||||
var regularEntries []*git.TreeEntry
|
||||
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
|
||||
}
|
||||
c, err := wikiRepo.GetCommitByPath(entry.Name())
|
||||
@@ -333,8 +343,8 @@ func ListWikiPages(ctx *context.APIContext) {
|
||||
pages = append(pages, wiki_service.ToWikiPageMetaData(wikiName, c, ctx.Repo.Repository))
|
||||
}
|
||||
|
||||
ctx.SetLinkHeader(int64(len(entries)), limit)
|
||||
ctx.SetTotalCountHeader(int64(len(entries)))
|
||||
ctx.SetLinkHeader(int64(len(regularEntries)), limit)
|
||||
ctx.SetTotalCountHeader(int64(len(regularEntries)))
|
||||
ctx.JSON(http.StatusOK, pages)
|
||||
}
|
||||
|
||||
@@ -368,7 +378,7 @@ func GetWikiPage(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
// get requested pagename
|
||||
pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("pageName"))
|
||||
pageName := wiki_service.WebPathFromRequest(ctx.PathParam("*"))
|
||||
|
||||
wikiPage := getWikiPage(ctx, pageName)
|
||||
if !ctx.Written() {
|
||||
@@ -418,7 +428,7 @@ func ListPageRevisions(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
// get requested pagename
|
||||
pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("pageName"))
|
||||
pageName := wiki_service.WebPathFromRequest(ctx.PathParam("*"))
|
||||
if len(pageName) == 0 {
|
||||
pageName = "Home"
|
||||
}
|
||||
@@ -499,11 +509,15 @@ func findWikiRepoCommit(ctx *context.APIContext) (*git.Repository, *git.Commit)
|
||||
func wikiContentsByEntry(ctx *context.APIContext, entry *git.TreeEntry) string {
|
||||
blob := entry.Blob()
|
||||
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 ""
|
||||
}
|
||||
content, err := blob.GetBlobContentBase64(nil)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
// Return the error details but don't abort — the page metadata
|
||||
// is still useful even without content.
|
||||
log.Error("wikiContentsByEntry: GetBlobContentBase64 for %s: %v", entry.Name(), err)
|
||||
return ""
|
||||
}
|
||||
return content
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
licenses_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/organization"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/renderhelper"
|
||||
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["IsOrganizationOwner"] = ctx.Org.IsOwner
|
||||
|
||||
orgCfg, _ := licenses_model.GetOrgConfig(ctx, ctx.Org.Organization.ID)
|
||||
orgCfg, _ := updateserver_model.GetOrgConfig(ctx, ctx.Org.Organization.ID)
|
||||
ctx.Data["OrgLicensingEnabled"] = orgCfg != nil && orgCfg.LicensingEnabled
|
||||
if orgCfg != nil && orgCfg.LicensingEnabled {
|
||||
numPkgs, _ := licenses_model.CountOrgPackages(ctx, ctx.Org.Organization.ID)
|
||||
numPkgs, _ := updateserver_model.CountOrgPackages(ctx, ctx.Org.Organization.ID)
|
||||
ctx.Data["NumOrgLicensePackages"] = numPkgs
|
||||
}
|
||||
ctx.Data["IsPublicMember"] = func(uid int64) bool {
|
||||
|
||||
+49
-49
@@ -9,7 +9,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/perm"
|
||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
unit_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit"
|
||||
@@ -27,7 +27,7 @@ type OrgChannelItem struct {
|
||||
Label string
|
||||
}
|
||||
|
||||
func buildOrgChannelItems(streams []licenses.StreamDef) []OrgChannelItem {
|
||||
func buildOrgChannelItems(streams []updateserver_model.StreamDef) []OrgChannelItem {
|
||||
items := make([]OrgChannelItem, 0, len(streams))
|
||||
for _, s := range streams {
|
||||
label := s.Name
|
||||
@@ -62,7 +62,7 @@ func parseOrgAllowedChannels(s string) []string {
|
||||
|
||||
// LicensePackageDisplay is used in templates.
|
||||
type LicensePackageDisplay struct {
|
||||
*licenses.LicensePackage
|
||||
*updateserver_model.LicensePackage
|
||||
KeyCount int64
|
||||
Created time.Time
|
||||
}
|
||||
@@ -79,7 +79,7 @@ func Licenses(ctx *context.Context) {
|
||||
|
||||
// Auto-create master key if has write access.
|
||||
if canWriteLicenses {
|
||||
newMasterKey, err := licenses.EnsureMasterKey(ctx, ownerID)
|
||||
newMasterKey, err := updateserver_model.EnsureMasterKey(ctx, ownerID)
|
||||
if err != nil {
|
||||
ctx.ServerError("EnsureMasterKey", err)
|
||||
return
|
||||
@@ -89,7 +89,7 @@ func Licenses(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
pkgs, err := licenses.ListLicensePackagesWithAncestors(ctx, ownerID)
|
||||
pkgs, err := updateserver_model.ListLicensePackagesWithAncestors(ctx, ownerID)
|
||||
if err != nil {
|
||||
ctx.ServerError("ListLicensePackages", err)
|
||||
return
|
||||
@@ -97,7 +97,7 @@ func Licenses(ctx *context.Context) {
|
||||
|
||||
var display []LicensePackageDisplay
|
||||
for _, pkg := range pkgs {
|
||||
count, _ := licenses.CountKeysByPackage(ctx, pkg.ID)
|
||||
count, _ := updateserver_model.CountKeysByPackage(ctx, pkg.ID)
|
||||
display = append(display, LicensePackageDisplay{
|
||||
LicensePackage: pkg,
|
||||
KeyCount: count,
|
||||
@@ -110,11 +110,11 @@ func Licenses(ctx *context.Context) {
|
||||
searchQuery := strings.TrimSpace(ctx.FormString("q"))
|
||||
ctx.Data["SearchQuery"] = searchQuery
|
||||
|
||||
var keys []*licenses.LicenseKey
|
||||
var keys []*updateserver_model.LicenseKey
|
||||
if searchQuery != "" {
|
||||
keys, err = licenses.SearchLicenseKeysWithAncestors(ctx, ownerID, searchQuery)
|
||||
keys, err = updateserver_model.SearchLicenseKeysWithAncestors(ctx, ownerID, searchQuery)
|
||||
} else {
|
||||
keys, err = licenses.ListLicenseKeysWithAncestors(ctx, ownerID)
|
||||
keys, err = updateserver_model.ListLicenseKeysWithAncestors(ctx, ownerID)
|
||||
}
|
||||
if err != nil {
|
||||
ctx.ServerError("ListLicenseKeys", err)
|
||||
@@ -128,10 +128,10 @@ func Licenses(ctx *context.Context) {
|
||||
ctx.Data["OrgLicensingEnabled"] = true
|
||||
|
||||
// Load archived packages.
|
||||
archivedPkgs, _ := licenses.ListArchivedLicensePackages(ctx, ownerID)
|
||||
archivedPkgs, _ := updateserver_model.ListArchivedLicensePackages(ctx, ownerID)
|
||||
var archivedDisplay []LicensePackageDisplay
|
||||
for _, pkg := range archivedPkgs {
|
||||
count, _ := licenses.CountKeysByPackage(ctx, pkg.ID)
|
||||
count, _ := updateserver_model.CountKeysByPackage(ctx, pkg.ID)
|
||||
archivedDisplay = append(archivedDisplay, LicensePackageDisplay{
|
||||
LicensePackage: pkg,
|
||||
KeyCount: count,
|
||||
@@ -140,12 +140,12 @@ func Licenses(ctx *context.Context) {
|
||||
}
|
||||
ctx.Data["ArchivedPackages"] = archivedDisplay
|
||||
|
||||
orgCfg, _ := licenses.GetOrgConfig(ctx, ownerID)
|
||||
var orgStreams []licenses.StreamDef
|
||||
orgCfg, _ := updateserver_model.GetOrgConfig(ctx, ownerID)
|
||||
var orgStreams []updateserver_model.StreamDef
|
||||
if orgCfg != nil {
|
||||
orgStreams = orgCfg.GetActiveStreams()
|
||||
} else {
|
||||
orgStreams = licenses.DefaultJoomlaStreams()
|
||||
orgStreams = updateserver_model.DefaultJoomlaStreams()
|
||||
}
|
||||
ctx.Data["AvailableStreams"] = orgStreams
|
||||
ctx.Data["ChannelItems"] = buildOrgChannelItems(orgStreams)
|
||||
@@ -182,7 +182,7 @@ func LicensesCreatePackage(ctx *context.Context) {
|
||||
repoScope = "all"
|
||||
}
|
||||
|
||||
pkg := &licenses.LicensePackage{
|
||||
pkg := &updateserver_model.LicensePackage{
|
||||
OwnerID: ctx.Org.Organization.ID,
|
||||
Name: name,
|
||||
Description: ctx.FormString("description"),
|
||||
@@ -194,7 +194,7 @@ func LicensesCreatePackage(ctx *context.Context) {
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if err := licenses.CreateLicensePackage(ctx, pkg); err != nil {
|
||||
if err := updateserver_model.CreateLicensePackage(ctx, pkg); err != nil {
|
||||
ctx.ServerError("CreateLicensePackage", err)
|
||||
return
|
||||
}
|
||||
@@ -212,13 +212,13 @@ func LicensesGenerateKey(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
pkg, err := licenses.GetLicensePackageByID(ctx, packageID)
|
||||
pkg, err := updateserver_model.GetLicensePackageByID(ctx, packageID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicensePackageByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
key := &licenses.LicenseKey{
|
||||
key := &updateserver_model.LicenseKey{
|
||||
PackageID: packageID,
|
||||
OwnerID: ctx.Org.Organization.ID,
|
||||
IsActive: true,
|
||||
@@ -233,13 +233,13 @@ func LicensesGenerateKey(ctx *context.Context) {
|
||||
var rawKey string
|
||||
customKey := strings.TrimSpace(ctx.FormString("custom_key"))
|
||||
if customKey != "" && (ctx.IsUserSiteAdmin() || ctx.Org.IsOwner) {
|
||||
if err := licenses.CreateLicenseKeyCustom(ctx, key, customKey); err != nil {
|
||||
if err := updateserver_model.CreateLicenseKeyCustom(ctx, key, customKey); err != nil {
|
||||
ctx.ServerError("CreateLicenseKeyCustom", err)
|
||||
return
|
||||
}
|
||||
rawKey = customKey
|
||||
} else {
|
||||
rawKey, err = licenses.CreateLicenseKey(ctx, key)
|
||||
rawKey, err = updateserver_model.CreateLicenseKey(ctx, key)
|
||||
if err != nil {
|
||||
ctx.ServerError("CreateLicenseKey", err)
|
||||
return
|
||||
@@ -253,10 +253,10 @@ func LicensesGenerateKey(ctx *context.Context) {
|
||||
ctx.Data["NewKeyCreated"] = rawKey
|
||||
|
||||
ownerID := ctx.Org.Organization.ID
|
||||
pkgs, _ := licenses.ListLicensePackages(ctx, ownerID)
|
||||
pkgs, _ := updateserver_model.ListLicensePackages(ctx, ownerID)
|
||||
var display []LicensePackageDisplay
|
||||
for _, p := range pkgs {
|
||||
count, _ := licenses.CountKeysByPackage(ctx, p.ID)
|
||||
count, _ := updateserver_model.CountKeysByPackage(ctx, p.ID)
|
||||
display = append(display, LicensePackageDisplay{
|
||||
LicensePackage: p,
|
||||
KeyCount: count,
|
||||
@@ -264,14 +264,14 @@ func LicensesGenerateKey(ctx *context.Context) {
|
||||
})
|
||||
}
|
||||
ctx.Data["LicensePackages"] = display
|
||||
keys, _ := licenses.ListLicenseKeys(ctx, ownerID)
|
||||
keys, _ := updateserver_model.ListLicenseKeys(ctx, ownerID)
|
||||
ctx.Data["LicenseKeys"] = keys
|
||||
|
||||
orgCfg, _ := licenses.GetOrgConfig(ctx, ownerID)
|
||||
orgCfg, _ := updateserver_model.GetOrgConfig(ctx, ownerID)
|
||||
if orgCfg != nil {
|
||||
ctx.Data["AvailableStreams"] = orgCfg.GetActiveStreams()
|
||||
} else {
|
||||
ctx.Data["AvailableStreams"] = licenses.DefaultJoomlaStreams()
|
||||
ctx.Data["AvailableStreams"] = updateserver_model.DefaultJoomlaStreams()
|
||||
}
|
||||
|
||||
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.
|
||||
func LicensesEditPackage(ctx *context.Context) {
|
||||
pkgID := ctx.PathParamInt64("id")
|
||||
pkg, err := licenses.GetLicensePackageByID(ctx, pkgID)
|
||||
pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicensePackageByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
if pkg.Name == licenses.MasterPackageName {
|
||||
if pkg.Name == updateserver_model.MasterPackageName {
|
||||
ctx.Flash.Error("Master package cannot be edited")
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/-/licenses")
|
||||
return
|
||||
@@ -301,12 +301,12 @@ func LicensesEditPackage(ctx *context.Context) {
|
||||
selectedChannels := parseOrgAllowedChannels(pkg.AllowedChannels)
|
||||
ctx.Data["SelectedChannels"] = selectedChannels
|
||||
|
||||
orgCfg, _ := licenses.GetOrgConfig(ctx, ctx.Org.Organization.ID)
|
||||
var editStreams []licenses.StreamDef
|
||||
orgCfg, _ := updateserver_model.GetOrgConfig(ctx, ctx.Org.Organization.ID)
|
||||
var editStreams []updateserver_model.StreamDef
|
||||
if orgCfg != nil {
|
||||
editStreams = orgCfg.GetActiveStreams()
|
||||
} else {
|
||||
editStreams = licenses.DefaultJoomlaStreams()
|
||||
editStreams = updateserver_model.DefaultJoomlaStreams()
|
||||
}
|
||||
ctx.Data["AvailableStreams"] = editStreams
|
||||
ctx.Data["ChannelItems"] = buildOrgChannelItems(editStreams)
|
||||
@@ -318,13 +318,13 @@ func LicensesEditPackage(ctx *context.Context) {
|
||||
// LicensesEditPackagePost saves edits to an org license package.
|
||||
func LicensesEditPackagePost(ctx *context.Context) {
|
||||
pkgID := ctx.PathParamInt64("id")
|
||||
pkg, err := licenses.GetLicensePackageByID(ctx, pkgID)
|
||||
pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicensePackageByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
if pkg.Name == licenses.MasterPackageName {
|
||||
if pkg.Name == updateserver_model.MasterPackageName {
|
||||
ctx.Flash.Error("Master package cannot be edited")
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/-/licenses")
|
||||
return
|
||||
@@ -349,7 +349,7 @@ func LicensesEditPackagePost(ctx *context.Context) {
|
||||
|
||||
pkg.IsActive = ctx.FormString("is_active") == "on"
|
||||
|
||||
if err := licenses.UpdateLicensePackage(ctx, pkg); err != nil {
|
||||
if err := updateserver_model.UpdateLicensePackage(ctx, pkg); err != nil {
|
||||
ctx.ServerError("UpdateLicensePackage", err)
|
||||
return
|
||||
}
|
||||
@@ -366,17 +366,17 @@ func canOrgDeleteLicenses(ctx *context.Context) bool {
|
||||
// LicensesArchivePackage archives an org license package.
|
||||
func LicensesArchivePackage(ctx *context.Context) {
|
||||
pkgID := ctx.PathParamInt64("id")
|
||||
pkg, err := licenses.GetLicensePackageByID(ctx, pkgID)
|
||||
pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicensePackageByID", err)
|
||||
return
|
||||
}
|
||||
if pkg.Name == licenses.MasterPackageName {
|
||||
if pkg.Name == updateserver_model.MasterPackageName {
|
||||
ctx.Flash.Error("Master package cannot be archived")
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/-/licenses")
|
||||
return
|
||||
}
|
||||
if err := licenses.ArchiveLicensePackage(ctx, pkgID); err != nil {
|
||||
if err := updateserver_model.ArchiveLicensePackage(ctx, pkgID); err != nil {
|
||||
ctx.ServerError("ArchiveLicensePackage", err)
|
||||
return
|
||||
}
|
||||
@@ -387,7 +387,7 @@ func LicensesArchivePackage(ctx *context.Context) {
|
||||
// LicensesUnarchivePackage restores an archived org license package.
|
||||
func LicensesUnarchivePackage(ctx *context.Context) {
|
||||
pkgID := ctx.PathParamInt64("id")
|
||||
if err := licenses.UnarchiveLicensePackage(ctx, pkgID); err != nil {
|
||||
if err := updateserver_model.UnarchiveLicensePackage(ctx, pkgID); err != nil {
|
||||
ctx.ServerError("UnarchiveLicensePackage", err)
|
||||
return
|
||||
}
|
||||
@@ -402,17 +402,17 @@ func LicensesDeletePackage(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
pkgID := ctx.PathParamInt64("id")
|
||||
pkg, err := licenses.GetLicensePackageByID(ctx, pkgID)
|
||||
pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicensePackageByID", err)
|
||||
return
|
||||
}
|
||||
if pkg.Name == licenses.MasterPackageName {
|
||||
if pkg.Name == updateserver_model.MasterPackageName {
|
||||
ctx.Flash.Error("Master package cannot be deleted")
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/-/licenses")
|
||||
return
|
||||
}
|
||||
if err := licenses.DeleteLicensePackage(ctx, pkgID); err != nil {
|
||||
if err := updateserver_model.DeleteLicensePackage(ctx, pkgID); err != nil {
|
||||
ctx.ServerError("DeleteLicensePackage", err)
|
||||
return
|
||||
}
|
||||
@@ -424,7 +424,7 @@ func LicensesDeletePackage(ctx *context.Context) {
|
||||
// LicensesEditKey shows the edit form for an org license key.
|
||||
func LicensesEditKey(ctx *context.Context) {
|
||||
keyID := ctx.PathParamInt64("id")
|
||||
key, err := licenses.GetLicenseKeyByID(ctx, keyID)
|
||||
key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicenseKeyByID", err)
|
||||
return
|
||||
@@ -452,7 +452,7 @@ func LicensesEditKey(ctx *context.Context) {
|
||||
// LicensesEditKeyPost saves edits to an org license key.
|
||||
func LicensesEditKeyPost(ctx *context.Context) {
|
||||
keyID := ctx.PathParamInt64("id")
|
||||
key, err := licenses.GetLicenseKeyByID(ctx, keyID)
|
||||
key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicenseKeyByID", err)
|
||||
return
|
||||
@@ -481,7 +481,7 @@ func LicensesEditKeyPost(ctx *context.Context) {
|
||||
key.ExpiresUnix = 0
|
||||
}
|
||||
|
||||
if err := licenses.UpdateLicenseKey(ctx, key); err != nil {
|
||||
if err := updateserver_model.UpdateLicenseKey(ctx, key); err != nil {
|
||||
ctx.ServerError("UpdateLicenseKey", err)
|
||||
return
|
||||
}
|
||||
@@ -493,14 +493,14 @@ func LicensesEditKeyPost(ctx *context.Context) {
|
||||
// LicensesRevokeKey handles POST to revoke an org license key.
|
||||
func LicensesRevokeKey(ctx *context.Context) {
|
||||
keyID := ctx.PathParamInt64("id")
|
||||
key, err := licenses.GetLicenseKeyByID(ctx, keyID)
|
||||
key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicenseKeyByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
key.IsActive = false
|
||||
if err := licenses.UpdateLicenseKey(ctx, key); err != nil {
|
||||
if err := updateserver_model.UpdateLicenseKey(ctx, key); err != nil {
|
||||
ctx.ServerError("UpdateLicenseKey", err)
|
||||
return
|
||||
}
|
||||
@@ -512,13 +512,13 @@ func LicensesRevokeKey(ctx *context.Context) {
|
||||
// LicensesRenewKey extends a license key's expiration by the package's duration.
|
||||
func LicensesRenewKey(ctx *context.Context) {
|
||||
keyID := ctx.PathParamInt64("id")
|
||||
key, err := licenses.GetLicenseKeyByID(ctx, keyID)
|
||||
key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicenseKeyByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
pkg, err := licenses.GetLicensePackageByID(ctx, key.PackageID)
|
||||
pkg, err := updateserver_model.GetLicensePackageByID(ctx, key.PackageID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicensePackageByID", err)
|
||||
return
|
||||
@@ -529,7 +529,7 @@ func LicensesRenewKey(ctx *context.Context) {
|
||||
days = 365 // default to 1 year for lifetime packages
|
||||
}
|
||||
|
||||
if err := licenses.RenewLicenseKey(ctx, keyID, days); err != nil {
|
||||
if err := updateserver_model.RenewLicenseKey(ctx, keyID, days); err != nil {
|
||||
ctx.ServerError("RenewLicenseKey", err)
|
||||
return
|
||||
}
|
||||
@@ -545,7 +545,7 @@ func LicensesDeleteKey(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
keyID := ctx.PathParamInt64("id")
|
||||
if err := licenses.DeleteLicenseKey(ctx, keyID); err != nil {
|
||||
if err := updateserver_model.DeleteLicenseKey(ctx, keyID); err != nil {
|
||||
ctx.ServerError("DeleteLicenseKey", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -126,6 +126,28 @@ func SettingsPost(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// Save wiki mode settings.
|
||||
wikiMode := ctx.FormString("wiki_mode")
|
||||
if wikiMode != "" && wikiMode != "external" {
|
||||
wikiMode = ""
|
||||
}
|
||||
wikiURL := ctx.FormString("wiki_url")
|
||||
if wikiMode == "external" && wikiURL != "" {
|
||||
u, urlErr := url.Parse(wikiURL)
|
||||
if urlErr != nil || (u.Scheme != "http" && u.Scheme != "https") {
|
||||
ctx.Flash.Error("Wiki URL must be a valid http or https URL")
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/settings")
|
||||
return
|
||||
}
|
||||
}
|
||||
orgUser := org.AsUser()
|
||||
orgUser.WikiMode = wikiMode
|
||||
orgUser.WikiURL = wikiURL
|
||||
if err := user_model.UpdateUserCols(ctx, orgUser, "wiki_mode", "wiki_url"); err != nil {
|
||||
ctx.ServerError("UpdateUserCols(wiki)", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace("Organization setting updated: %s", org.Name)
|
||||
ctx.Flash.Success(ctx.Tr("org.settings.update_setting_success"))
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/settings")
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
)
|
||||
@@ -22,7 +22,7 @@ func SettingsUpdateStreams(ctx *context.Context) {
|
||||
|
||||
orgID := ctx.Org.Organization.ID
|
||||
|
||||
cfg, err := licenses.GetOrgConfig(ctx, orgID)
|
||||
cfg, err := updateserver_model.GetOrgConfig(ctx, orgID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetOrgConfig", err)
|
||||
return
|
||||
@@ -37,7 +37,7 @@ func SettingsUpdateStreams(ctx *context.Context) {
|
||||
func SettingsUpdateStreamsPost(ctx *context.Context) {
|
||||
orgID := ctx.Org.Organization.ID
|
||||
|
||||
cfg := &licenses.UpdateStreamConfig{
|
||||
cfg := &updateserver_model.UpdateStreamConfig{
|
||||
OwnerID: orgID,
|
||||
RepoID: 0,
|
||||
StreamMode: ctx.FormString("stream_mode"),
|
||||
@@ -64,7 +64,7 @@ func SettingsUpdateStreamsPost(ctx *context.Context) {
|
||||
cfg.StreamMode = "joomla"
|
||||
}
|
||||
|
||||
if err := licenses.SaveConfig(ctx, cfg); err != nil {
|
||||
if err := updateserver_model.SaveConfig(ctx, cfg); err != nil {
|
||||
ctx.ServerError("SaveConfig", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/renderhelper"
|
||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/gitrepo"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/markup/markdown"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/util"
|
||||
shared_user "code.mokoconsulting.tech/MokoConsulting/MokoGitea/routers/web/shared/user"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
)
|
||||
|
||||
const tplOrgWiki templates.TplName = "org/wiki/view"
|
||||
|
||||
// OrgWikiPage represents a single page in the org wiki sidebar.
|
||||
type OrgWikiPage struct {
|
||||
Name string
|
||||
SubURL string
|
||||
}
|
||||
|
||||
// Wiki renders the org wiki tab.
|
||||
func Wiki(ctx *context.Context) {
|
||||
org := ctx.Org.Organization
|
||||
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["PageIsViewWiki"] = true
|
||||
ctx.Data["Title"] = org.DisplayName() + " - Wiki"
|
||||
|
||||
// Determine which wiki repo to use (public vs member).
|
||||
viewAs := ctx.FormString("view_as", util.Iif(ctx.Org.IsMember, "member", "public"))
|
||||
viewAsMember := viewAs == "member"
|
||||
|
||||
wikiRepo, commit := findOrgWikiCommit(ctx, org.ID, util.Iif(viewAsMember, shared_user.RepoNameWikiPrivate, shared_user.RepoNameWikiPublic))
|
||||
if wikiRepo == nil && viewAsMember {
|
||||
// Fall back to public wiki if member wiki doesn't exist.
|
||||
wikiRepo, commit = findOrgWikiCommit(ctx, org.ID, shared_user.RepoNameWikiPublic)
|
||||
viewAsMember = false
|
||||
}
|
||||
if wikiRepo == nil && !viewAsMember {
|
||||
// Fall back to member wiki if public wiki doesn't exist.
|
||||
wikiRepo, commit = findOrgWikiCommit(ctx, org.ID, shared_user.RepoNameWikiPrivate)
|
||||
viewAsMember = true
|
||||
}
|
||||
|
||||
ctx.Data["IsViewingWikiAsMember"] = viewAsMember
|
||||
|
||||
// Check whether both repos exist (for the dropdown toggle).
|
||||
publicExists := shared_user.OrgWikiRepoExists(ctx, org.ID, shared_user.RepoNameWikiPublic)
|
||||
privateExists := shared_user.OrgWikiRepoExists(ctx, org.ID, shared_user.RepoNameWikiPrivate)
|
||||
ctx.Data["ShowWikiViewSelector"] = publicExists && privateExists && ctx.Org.IsMember
|
||||
|
||||
if wikiRepo == nil || commit == nil {
|
||||
ctx.Data["WikiEmpty"] = true
|
||||
ctx.HTML(http.StatusOK, tplOrgWiki)
|
||||
return
|
||||
}
|
||||
ctx.Data["WikiRepoLink"] = wikiRepo.Link()
|
||||
|
||||
// Build page list from repo root.
|
||||
entries, err := commit.ListEntries()
|
||||
if err != nil {
|
||||
ctx.ServerError("ListEntries", err)
|
||||
return
|
||||
}
|
||||
pages := make([]OrgWikiPage, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
if !entry.IsRegular() {
|
||||
continue
|
||||
}
|
||||
name := entry.Name()
|
||||
if !isMarkdownFile(name) {
|
||||
continue
|
||||
}
|
||||
displayName := strings.TrimSuffix(name, path.Ext(name))
|
||||
if strings.EqualFold(displayName, "_sidebar") || strings.EqualFold(displayName, "_footer") {
|
||||
continue
|
||||
}
|
||||
pages = append(pages, OrgWikiPage{
|
||||
Name: displayName,
|
||||
SubURL: displayName,
|
||||
})
|
||||
}
|
||||
ctx.Data["Pages"] = pages
|
||||
|
||||
// Determine which page to render.
|
||||
pageName := ctx.PathParamRaw("*")
|
||||
if pageName == "" {
|
||||
pageName = "Home"
|
||||
}
|
||||
ctx.Data["CurrentPage"] = pageName
|
||||
|
||||
// Try to find the file: exact match, then with .md extension.
|
||||
blob := findWikiBlob(commit, pageName)
|
||||
if blob == nil {
|
||||
// Page not found — show empty state with page list.
|
||||
ctx.Data["WikiPageNotFound"] = true
|
||||
ctx.HTML(http.StatusOK, tplOrgWiki)
|
||||
return
|
||||
}
|
||||
|
||||
content, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetBlobContent", err)
|
||||
return
|
||||
}
|
||||
|
||||
rctx := renderhelper.NewRenderContextRepoFile(ctx, wikiRepo, renderhelper.RepoFileOptions{
|
||||
CurrentRefPath: path.Join("branch", util.PathEscapeSegments(wikiRepo.DefaultBranch)),
|
||||
})
|
||||
renderedContent, err := markdown.RenderString(rctx, content)
|
||||
if err != nil {
|
||||
log.Error("Failed to render org wiki page %q: %v", pageName, err)
|
||||
ctx.ServerError("RenderString", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["WikiContent"] = renderedContent
|
||||
|
||||
// Render _Sidebar if it exists.
|
||||
sidebarBlob := findWikiBlob(commit, "_Sidebar")
|
||||
if sidebarBlob != nil {
|
||||
sidebarContent, err := sidebarBlob.GetBlobContent(setting.UI.MaxDisplayFileSize)
|
||||
if err == nil {
|
||||
rendered, err := markdown.RenderString(rctx, sidebarContent)
|
||||
if err == nil {
|
||||
ctx.Data["WikiSidebarHTML"] = rendered
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render _Footer if it exists.
|
||||
footerBlob := findWikiBlob(commit, "_Footer")
|
||||
if footerBlob != nil {
|
||||
footerContent, err := footerBlob.GetBlobContent(setting.UI.MaxDisplayFileSize)
|
||||
if err == nil {
|
||||
rendered, err := markdown.RenderString(rctx, footerContent)
|
||||
if err == nil {
|
||||
ctx.Data["WikiFooterHTML"] = rendered
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplOrgWiki)
|
||||
}
|
||||
|
||||
// findOrgWikiCommit locates the profile repo's wiki and returns its HEAD commit.
|
||||
// The org wiki lives in the .wiki.git sidecar of the profile repo (e.g. .profile.wiki.git).
|
||||
func findOrgWikiCommit(ctx *context.Context, orgID int64, repoName string) (*repo_model.Repository, *git.Commit) {
|
||||
dbRepo, err := repo_model.GetRepositoryByName(ctx, orgID, repoName)
|
||||
if err != nil {
|
||||
if !repo_model.IsErrRepoNotExist(err) {
|
||||
log.Error("findOrgWikiCommit: GetRepositoryByName(%d, %s): %v", orgID, repoName, err)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Open the wiki git repo (.wiki.git sidecar), not the main repo.
|
||||
wikiGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, dbRepo.WikiStorageRepo())
|
||||
if err != nil {
|
||||
// Wiki repo doesn't exist yet — not an error, just no wiki.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
branch := dbRepo.DefaultWikiBranch
|
||||
if branch == "" {
|
||||
branch = "main"
|
||||
}
|
||||
commit, err := wikiGitRepo.GetBranchCommit(branch)
|
||||
if err != nil {
|
||||
log.Error("findOrgWikiCommit: GetBranchCommit wiki(%s, %s): %v", dbRepo.FullName(), branch, err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return dbRepo, commit
|
||||
}
|
||||
|
||||
// findWikiBlob looks up a markdown file in the commit by name.
|
||||
// Tries exact match first, then appends .md.
|
||||
func findWikiBlob(commit *git.Commit, name string) *git.Blob {
|
||||
// Try exact match (e.g., "Home.md").
|
||||
if blob, _ := commit.GetBlobByPath(name); blob != nil {
|
||||
return blob
|
||||
}
|
||||
// Try with .md extension (e.g., "Home" → "Home.md").
|
||||
if blob, _ := commit.GetBlobByPath(name + ".md"); blob != nil {
|
||||
return blob
|
||||
}
|
||||
// Try with .markdown extension.
|
||||
if blob, _ := commit.GetBlobByPath(name + ".markdown"); blob != nil {
|
||||
return blob
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isMarkdownFile returns true if the filename looks like a markdown file.
|
||||
func isMarkdownFile(name string) bool {
|
||||
ext := strings.ToLower(path.Ext(name))
|
||||
return ext == ".md" || ext == ".markdown"
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
|
||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/httplib"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/storage"
|
||||
)
|
||||
|
||||
// CDNHandler serves release assets via the CDN hostname.
|
||||
// URL format: /:owner/:repo/releases/:tag/:filename
|
||||
// Only assets with cdn_public=true are served.
|
||||
func CDNHandler(w http.ResponseWriter, req *http.Request) {
|
||||
if !setting.CDN.Enabled {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
if !cdnCheckIPAllowed(req) {
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if !cdnCheckReferrerAllowed(req) {
|
||||
http.Error(w, "Forbidden: referrer not allowed", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse: /:owner/:repo/releases/:tag/:filename
|
||||
urlPath := strings.TrimPrefix(req.URL.Path, "/")
|
||||
parts := strings.SplitN(urlPath, "/", 6)
|
||||
|
||||
// Minimum: owner/repo/releases/tag/filename = 5 parts
|
||||
if len(parts) < 5 || parts[2] != "releases" {
|
||||
http.Error(w, "Not Found: expected /:owner/:repo/releases/:tag/:filename", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
ownerName := parts[0]
|
||||
repoName := parts[1]
|
||||
tagName := parts[3]
|
||||
fileName := parts[4]
|
||||
// Allow filenames with slashes (parts[5] if present)
|
||||
if len(parts) == 6 {
|
||||
fileName = parts[4] + "/" + parts[5]
|
||||
}
|
||||
|
||||
// Load repository
|
||||
repo, err := repo_model.GetRepositoryByOwnerAndName(req.Context(), ownerName, repoName)
|
||||
if err != nil {
|
||||
if repo_model.IsErrRepoNotExist(err) {
|
||||
http.NotFound(w, req)
|
||||
} else {
|
||||
log.Error("CDN: GetRepositoryByOwnerAndName %s/%s: %v", ownerName, repoName, err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Look up the release by tag
|
||||
release, err := repo_model.GetRelease(req.Context(), repo.ID, tagName)
|
||||
if err != nil {
|
||||
if repo_model.IsErrReleaseNotExist(err) {
|
||||
http.NotFound(w, req)
|
||||
} else {
|
||||
log.Error("CDN: GetRelease %s/%s tag=%s: %v", ownerName, repoName, tagName, err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Don't serve draft releases via CDN
|
||||
if release.IsDraft {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
// If the release is assigned to an update stream, CDN is disabled -
|
||||
// the update server handles distribution for streamed releases.
|
||||
if stream := updateserver_model.GetReleaseStream(req.Context(), release.ID); stream != "" {
|
||||
http.Error(w, "Forbidden: release is served via update stream", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Find the specific attachment by filename
|
||||
attach, err := repo_model.GetAttachmentByReleaseIDFileName(req.Context(), release.ID, fileName)
|
||||
if err != nil || attach == nil {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
// Only serve assets marked as CDN public
|
||||
if !attach.CDNPublic {
|
||||
http.Error(w, "Forbidden: asset is not CDN-enabled", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Check file size limit
|
||||
if setting.CDN.MaxFileSize > 0 && attach.Size > setting.CDN.MaxFileSize {
|
||||
http.Error(w, "File too large for CDN delivery", http.StatusRequestEntityTooLarge)
|
||||
return
|
||||
}
|
||||
|
||||
// CORS headers
|
||||
if len(setting.CDN.AllowedOrigins) > 0 {
|
||||
origin := req.Header.Get("Origin")
|
||||
for _, allowed := range setting.CDN.AllowedOrigins {
|
||||
if allowed == "*" || allowed == origin {
|
||||
w.Header().Set("Access-Control-Allow-Origin", allowed)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
}
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS")
|
||||
w.Header().Set("Access-Control-Max-Age", "86400")
|
||||
|
||||
if req.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
// ETag based on attachment UUID (immutable for same content)
|
||||
etag := `"` + attach.UUID + `"`
|
||||
w.Header().Set("Etag", etag)
|
||||
|
||||
// 304 Not Modified check
|
||||
if inm := req.Header.Get("If-None-Match"); inm != "" {
|
||||
for item := range strings.SplitSeq(inm, ",") {
|
||||
item = strings.TrimPrefix(strings.TrimSpace(item), "W/")
|
||||
if item == etag {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Last-Modified
|
||||
lastModified := attach.CreatedUnix.AsTimePtr()
|
||||
if lastModified != nil {
|
||||
w.Header().Set("Last-Modified", lastModified.UTC().Format(http.TimeFormat))
|
||||
}
|
||||
|
||||
// CDN cache headers
|
||||
cacheTTL := int(setting.CDN.CacheTTL.Seconds())
|
||||
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d, no-transform", cacheTTL))
|
||||
|
||||
// Increment download count
|
||||
if err := attach.IncreaseDownloadCount(req.Context()); err != nil {
|
||||
log.Error("CDN: IncreaseDownloadCount: %v", err)
|
||||
}
|
||||
|
||||
// Try direct storage URL (S3/object storage)
|
||||
if setting.Attachment.Storage.ServeDirect() {
|
||||
u, err := storage.Attachments.ServeDirectURL(attach.RelativePath(), attach.Name, req.Method, nil)
|
||||
if u != nil && err == nil {
|
||||
http.Redirect(w, req, u.String(), http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Serve from local storage
|
||||
fr, err := storage.Attachments.Open(attach.RelativePath())
|
||||
if err != nil {
|
||||
log.Error("CDN: storage.Open %s: %v", attach.RelativePath(), err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer fr.Close()
|
||||
|
||||
httplib.ServeUserContentByFile(req, w, fr, httplib.ServeHeaderOptions{
|
||||
Filename: attach.Name,
|
||||
CacheIsPublic: true,
|
||||
CacheDuration: setting.CDN.CacheTTL,
|
||||
})
|
||||
}
|
||||
|
||||
// cdnCheckIPAllowed checks if the request IP is in the configured allowlist.
|
||||
func cdnCheckIPAllowed(req *http.Request) bool {
|
||||
if len(setting.CDN.AllowedIPs) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
remoteIP := cdnGetRemoteIP(req)
|
||||
if remoteIP == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, cidr := range setting.CDN.AllowedIPs {
|
||||
cidr = strings.TrimSpace(cidr)
|
||||
if cidr == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.Contains(cidr, "/") {
|
||||
if remoteIP.Equal(net.ParseIP(cidr)) {
|
||||
return true
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
_, network, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
log.Warn("CDN: invalid CIDR in AllowedIPs: %s", cidr)
|
||||
continue
|
||||
}
|
||||
if network.Contains(remoteIP) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// cdnCheckReferrerAllowed checks if the request referrer domain is allowed.
|
||||
func cdnCheckReferrerAllowed(req *http.Request) bool {
|
||||
if len(setting.CDN.AllowedDomains) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
referer := req.Header.Get("Referer")
|
||||
if referer == "" {
|
||||
return true // direct requests always allowed
|
||||
}
|
||||
|
||||
for _, domain := range setting.CDN.AllowedDomains {
|
||||
domain = strings.TrimSpace(strings.ToLower(domain))
|
||||
if domain == "" {
|
||||
continue
|
||||
}
|
||||
if domain == "*" {
|
||||
return true
|
||||
}
|
||||
refLower := strings.ToLower(referer)
|
||||
if strings.Contains(refLower, "://"+domain+"/") || strings.Contains(refLower, "://"+domain+":") ||
|
||||
strings.HasSuffix(refLower, "://"+domain) {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(domain, "*.") {
|
||||
baseDomain := domain[2:]
|
||||
if strings.Contains(refLower, "."+baseDomain+"/") || strings.Contains(refLower, "."+baseDomain+":") ||
|
||||
strings.HasSuffix(refLower, "."+baseDomain) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// cdnGetRemoteIP extracts the client IP, checking proxy headers.
|
||||
func cdnGetRemoteIP(req *http.Request) net.IP {
|
||||
if xff := req.Header.Get("X-Forwarded-For"); xff != "" {
|
||||
parts := strings.SplitN(xff, ",", 2)
|
||||
if ip := net.ParseIP(strings.TrimSpace(parts[0])); ip != nil {
|
||||
return ip
|
||||
}
|
||||
}
|
||||
if xri := req.Header.Get("X-Real-IP"); xri != "" {
|
||||
if ip := net.ParseIP(strings.TrimSpace(xri)); ip != nil {
|
||||
return ip
|
||||
}
|
||||
}
|
||||
host, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||
if err != nil {
|
||||
return net.ParseIP(req.RemoteAddr)
|
||||
}
|
||||
return net.ParseIP(host)
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
|
||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
)
|
||||
@@ -58,7 +58,7 @@ func ServeChangelogXML(ctx *context.Context) {
|
||||
}
|
||||
|
||||
// Get extension metadata for element name and type.
|
||||
cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
|
||||
cfg := updateserver_model.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
|
||||
element := strings.ToLower(repo.Name)
|
||||
extType := "component"
|
||||
if cfg != nil {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
)
|
||||
@@ -20,7 +20,7 @@ func CheckDownloadGating(ctx *context.Context, tagName string) bool {
|
||||
repo := ctx.Repo.Repository
|
||||
|
||||
// Check effective config (repo override → org default).
|
||||
cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
|
||||
cfg := updateserver_model.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
|
||||
if cfg == nil || !cfg.LicensingEnabled {
|
||||
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.
|
||||
if gating == "prerelease" && tagName != "" {
|
||||
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
|
||||
matched := licenses.MatchStreamFromTag(tagName, false, streams)
|
||||
streams := updateserver_model.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
|
||||
matched := updateserver_model.MatchStreamFromTag(tagName, false, streams)
|
||||
if matched == "stable" {
|
||||
return true // stable releases are public
|
||||
}
|
||||
@@ -60,14 +60,14 @@ func CheckDownloadGating(ctx *context.Context, tagName string) bool {
|
||||
}
|
||||
|
||||
domain := ctx.FormString("domain")
|
||||
key, _, err := licenses.ValidateLicenseKeyForRepo(ctx, rawKey, domain, repo.ID)
|
||||
key, _, err := updateserver_model.ValidateLicenseKeyForRepo(ctx, rawKey, domain, repo.ID)
|
||||
if err != nil {
|
||||
log.Debug("Download gating: key validation failed: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
// Record heartbeat on successful download validation.
|
||||
_ = licenses.TouchHeartbeat(ctx, key.ID)
|
||||
_ = updateserver_model.TouchHeartbeat(ctx, key.ID)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ func GetSupportURL(ctx *context.Context) string {
|
||||
if ctx.Repo == nil || ctx.Repo.Repository == nil {
|
||||
return ""
|
||||
}
|
||||
cfg := licenses.GetEffectiveConfig(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID)
|
||||
cfg := updateserver_model.GetEffectiveConfig(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID)
|
||||
if cfg != nil && cfg.SupportURL != "" {
|
||||
return cfg.SupportURL
|
||||
}
|
||||
|
||||
@@ -142,6 +142,14 @@ func NewIssue(ctx *context.Context) {
|
||||
|
||||
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.
|
||||
customFieldDefs, cfErr := issues_model.GetCustomFieldsByOwner(ctx, ctx.Repo.Repository.OwnerID, issues_model.CustomFieldScopeIssue)
|
||||
if cfErr != nil {
|
||||
@@ -408,6 +416,17 @@ func NewIssuePost(ctx *context.Context) {
|
||||
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.
|
||||
saveCustomFieldsFromForm(ctx, repo.OwnerID, issue.ID)
|
||||
|
||||
@@ -429,6 +448,7 @@ func NewIssuePost(ctx *context.Context) {
|
||||
|
||||
// saveCustomFieldsFromForm reads custom field values from the form
|
||||
// (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) {
|
||||
defs, err := issues_model.GetCustomFieldsByOwner(ctx, ownerID, issues_model.CustomFieldScopeIssue)
|
||||
if err != nil {
|
||||
@@ -443,6 +463,8 @@ func saveCustomFieldsFromForm(ctx *context.Context, ownerID, issueID int64) {
|
||||
v := ctx.Req.FormValue(fmt.Sprintf("custom-field-%d", def.ID))
|
||||
if 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 {
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
|
||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
unit_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/json"
|
||||
@@ -48,7 +48,7 @@ type ChannelItem struct {
|
||||
}
|
||||
|
||||
// buildChannelItems converts stream definitions into combolist items.
|
||||
func buildChannelItems(streams []licenses.StreamDef) []ChannelItem {
|
||||
func buildChannelItems(streams []updateserver_model.StreamDef) []ChannelItem {
|
||||
items := make([]ChannelItem, 0, len(streams))
|
||||
for _, s := range streams {
|
||||
label := s.Name
|
||||
@@ -62,7 +62,7 @@ func buildChannelItems(streams []licenses.StreamDef) []ChannelItem {
|
||||
|
||||
// LicensePackageDisplay is used in templates.
|
||||
type LicensePackageDisplay struct {
|
||||
*licenses.LicensePackage
|
||||
*updateserver_model.LicensePackage
|
||||
KeyCount int64
|
||||
Created time.Time
|
||||
}
|
||||
@@ -80,7 +80,7 @@ func Licenses(ctx *context.Context) {
|
||||
|
||||
// Auto-create master package + key if admin and none exist.
|
||||
if canWriteLicenses {
|
||||
newMasterKey, err := licenses.EnsureMasterKey(ctx, ownerID)
|
||||
newMasterKey, err := updateserver_model.EnsureMasterKey(ctx, ownerID)
|
||||
if err != nil {
|
||||
ctx.ServerError("EnsureMasterKey", err)
|
||||
return
|
||||
@@ -91,10 +91,10 @@ func Licenses(ctx *context.Context) {
|
||||
}
|
||||
|
||||
// Always load the master key for display (prefix + status).
|
||||
masterKey, _ := licenses.GetMasterKey(ctx, ownerID)
|
||||
masterKey, _ := updateserver_model.GetMasterKey(ctx, ownerID)
|
||||
ctx.Data["MasterKey"] = masterKey
|
||||
|
||||
pkgs, err := licenses.ListLicensePackages(ctx, ownerID)
|
||||
pkgs, err := updateserver_model.ListLicensePackages(ctx, ownerID)
|
||||
if err != nil {
|
||||
ctx.ServerError("ListLicensePackages", err)
|
||||
return
|
||||
@@ -102,7 +102,7 @@ func Licenses(ctx *context.Context) {
|
||||
|
||||
var display []LicensePackageDisplay
|
||||
for _, pkg := range pkgs {
|
||||
count, _ := licenses.CountKeysByPackage(ctx, pkg.ID)
|
||||
count, _ := updateserver_model.CountKeysByPackage(ctx, pkg.ID)
|
||||
display = append(display, LicensePackageDisplay{
|
||||
LicensePackage: pkg,
|
||||
KeyCount: count,
|
||||
@@ -115,11 +115,11 @@ func Licenses(ctx *context.Context) {
|
||||
searchQuery := strings.TrimSpace(ctx.FormString("q"))
|
||||
ctx.Data["SearchQuery"] = searchQuery
|
||||
|
||||
var keys []*licenses.LicenseKey
|
||||
var keys []*updateserver_model.LicenseKey
|
||||
if searchQuery != "" {
|
||||
keys, err = licenses.SearchLicenseKeys(ctx, ownerID, searchQuery)
|
||||
keys, err = updateserver_model.SearchLicenseKeys(ctx, ownerID, searchQuery)
|
||||
} else {
|
||||
keys, err = licenses.ListLicenseKeys(ctx, ownerID)
|
||||
keys, err = updateserver_model.ListLicenseKeys(ctx, ownerID)
|
||||
}
|
||||
if err != nil {
|
||||
ctx.ServerError("ListLicenseKeys", err)
|
||||
@@ -129,10 +129,10 @@ func Licenses(ctx *context.Context) {
|
||||
ctx.Data["CanDelete"] = canDeleteLicenses(ctx)
|
||||
|
||||
// Load archived packages.
|
||||
archivedPkgs, _ := licenses.ListArchivedLicensePackages(ctx, ownerID)
|
||||
archivedPkgs, _ := updateserver_model.ListArchivedLicensePackages(ctx, ownerID)
|
||||
var archivedDisplay []LicensePackageDisplay
|
||||
for _, pkg := range archivedPkgs {
|
||||
count, _ := licenses.CountKeysByPackage(ctx, pkg.ID)
|
||||
count, _ := updateserver_model.CountKeysByPackage(ctx, pkg.ID)
|
||||
archivedDisplay = append(archivedDisplay, LicensePackageDisplay{
|
||||
LicensePackage: pkg,
|
||||
KeyCount: count,
|
||||
@@ -142,12 +142,12 @@ func Licenses(ctx *context.Context) {
|
||||
ctx.Data["ArchivedPackages"] = archivedDisplay
|
||||
|
||||
// Load available streams for the channels combolist.
|
||||
orgCfg, _ := licenses.GetOrgConfig(ctx, ownerID)
|
||||
var streams []licenses.StreamDef
|
||||
orgCfg, _ := updateserver_model.GetOrgConfig(ctx, ownerID)
|
||||
var streams []updateserver_model.StreamDef
|
||||
if orgCfg != nil {
|
||||
streams = orgCfg.GetActiveStreams()
|
||||
} else {
|
||||
streams = licenses.DefaultJoomlaStreams()
|
||||
streams = updateserver_model.DefaultJoomlaStreams()
|
||||
}
|
||||
ctx.Data["AvailableStreams"] = streams
|
||||
ctx.Data["ChannelItems"] = buildChannelItems(streams)
|
||||
@@ -186,7 +186,7 @@ func LicensesCreatePackage(ctx *context.Context) {
|
||||
repoScope = "all"
|
||||
}
|
||||
|
||||
pkg := &licenses.LicensePackage{
|
||||
pkg := &updateserver_model.LicensePackage{
|
||||
OwnerID: ctx.Repo.Repository.OwnerID,
|
||||
Name: name,
|
||||
Description: ctx.FormString("description"),
|
||||
@@ -199,7 +199,7 @@ func LicensesCreatePackage(ctx *context.Context) {
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if err := licenses.CreateLicensePackage(ctx, pkg); err != nil {
|
||||
if err := updateserver_model.CreateLicensePackage(ctx, pkg); err != nil {
|
||||
ctx.ServerError("CreateLicensePackage", err)
|
||||
return
|
||||
}
|
||||
@@ -213,21 +213,21 @@ func LicensesRegenerateMasterKey(ctx *context.Context) {
|
||||
ownerID := ctx.Repo.Repository.OwnerID
|
||||
|
||||
// Deactivate the old master key.
|
||||
oldKey, _ := licenses.GetMasterKey(ctx, ownerID)
|
||||
oldKey, _ := updateserver_model.GetMasterKey(ctx, ownerID)
|
||||
if oldKey != nil {
|
||||
oldKey.IsActive = false
|
||||
_ = licenses.UpdateLicenseKey(ctx, oldKey)
|
||||
_ = updateserver_model.UpdateLicenseKey(ctx, oldKey)
|
||||
}
|
||||
|
||||
// Find the master package.
|
||||
pkgs, err := licenses.ListLicensePackages(ctx, ownerID)
|
||||
pkgs, err := updateserver_model.ListLicensePackages(ctx, ownerID)
|
||||
if err != nil {
|
||||
ctx.ServerError("ListLicensePackages", err)
|
||||
return
|
||||
}
|
||||
var masterPkg *licenses.LicensePackage
|
||||
var masterPkg *updateserver_model.LicensePackage
|
||||
for _, pkg := range pkgs {
|
||||
if pkg.Name == licenses.MasterPackageName {
|
||||
if pkg.Name == updateserver_model.MasterPackageName {
|
||||
masterPkg = pkg
|
||||
break
|
||||
}
|
||||
@@ -239,13 +239,13 @@ func LicensesRegenerateMasterKey(ctx *context.Context) {
|
||||
}
|
||||
|
||||
// Create a new master key.
|
||||
newKey := &licenses.LicenseKey{
|
||||
newKey := &updateserver_model.LicenseKey{
|
||||
PackageID: masterPkg.ID,
|
||||
OwnerID: ownerID,
|
||||
IsInternal: true,
|
||||
IsActive: true,
|
||||
}
|
||||
rawKey, err := licenses.CreateLicenseKey(ctx, newKey)
|
||||
rawKey, err := updateserver_model.CreateLicenseKey(ctx, newKey)
|
||||
if err != nil {
|
||||
ctx.ServerError("CreateLicenseKey", err)
|
||||
return
|
||||
@@ -270,7 +270,7 @@ func LicensesGenerateKey(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
pkg, err := licenses.GetLicensePackageByID(ctx, packageID)
|
||||
pkg, err := updateserver_model.GetLicensePackageByID(ctx, packageID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicensePackageByID", err)
|
||||
return
|
||||
@@ -282,7 +282,7 @@ func LicensesGenerateKey(ctx *context.Context) {
|
||||
domainRestriction = pkg.DomainRestriction
|
||||
}
|
||||
|
||||
key := &licenses.LicenseKey{
|
||||
key := &updateserver_model.LicenseKey{
|
||||
PackageID: packageID,
|
||||
OwnerID: ctx.Repo.Repository.OwnerID,
|
||||
IsActive: true,
|
||||
@@ -301,13 +301,13 @@ func LicensesGenerateKey(ctx *context.Context) {
|
||||
var rawKey string
|
||||
customKey := strings.TrimSpace(ctx.FormString("custom_key"))
|
||||
if customKey != "" && (ctx.IsUserSiteAdmin() || ctx.Repo.Permission.IsOwner()) {
|
||||
if err := licenses.CreateLicenseKeyCustom(ctx, key, customKey); err != nil {
|
||||
if err := updateserver_model.CreateLicenseKeyCustom(ctx, key, customKey); err != nil {
|
||||
ctx.ServerError("CreateLicenseKeyCustom", err)
|
||||
return
|
||||
}
|
||||
rawKey = customKey
|
||||
} else {
|
||||
rawKey, err = licenses.CreateLicenseKey(ctx, key)
|
||||
rawKey, err = updateserver_model.CreateLicenseKey(ctx, key)
|
||||
if err != nil {
|
||||
ctx.ServerError("CreateLicenseKey", err)
|
||||
return
|
||||
@@ -323,10 +323,10 @@ func LicensesGenerateKey(ctx *context.Context) {
|
||||
|
||||
// Re-render the page with the new key displayed.
|
||||
ownerID := ctx.Repo.Repository.OwnerID
|
||||
pkgs, _ := licenses.ListLicensePackages(ctx, ownerID)
|
||||
pkgs, _ := updateserver_model.ListLicensePackages(ctx, ownerID)
|
||||
var display []LicensePackageDisplay
|
||||
for _, p := range pkgs {
|
||||
count, _ := licenses.CountKeysByPackage(ctx, p.ID)
|
||||
count, _ := updateserver_model.CountKeysByPackage(ctx, p.ID)
|
||||
display = append(display, LicensePackageDisplay{
|
||||
LicensePackage: p,
|
||||
KeyCount: count,
|
||||
@@ -334,15 +334,15 @@ func LicensesGenerateKey(ctx *context.Context) {
|
||||
})
|
||||
}
|
||||
ctx.Data["LicensePackages"] = display
|
||||
keys, _ := licenses.ListLicenseKeys(ctx, ownerID)
|
||||
keys, _ := updateserver_model.ListLicenseKeys(ctx, ownerID)
|
||||
ctx.Data["LicenseKeys"] = keys
|
||||
|
||||
orgCfg, _ := licenses.GetOrgConfig(ctx, ownerID)
|
||||
var genStreams []licenses.StreamDef
|
||||
orgCfg, _ := updateserver_model.GetOrgConfig(ctx, ownerID)
|
||||
var genStreams []updateserver_model.StreamDef
|
||||
if orgCfg != nil {
|
||||
genStreams = orgCfg.GetActiveStreams()
|
||||
} else {
|
||||
genStreams = licenses.DefaultJoomlaStreams()
|
||||
genStreams = updateserver_model.DefaultJoomlaStreams()
|
||||
}
|
||||
ctx.Data["AvailableStreams"] = genStreams
|
||||
ctx.Data["ChannelItems"] = buildChannelItems(genStreams)
|
||||
@@ -353,14 +353,14 @@ func LicensesGenerateKey(ctx *context.Context) {
|
||||
// LicensesRevokeKey handles POST to revoke a license key.
|
||||
func LicensesRevokeKey(ctx *context.Context) {
|
||||
keyID := ctx.PathParamInt64("id")
|
||||
key, err := licenses.GetLicenseKeyByID(ctx, keyID)
|
||||
key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicenseKeyByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
key.IsActive = false
|
||||
if err := licenses.UpdateLicenseKey(ctx, key); err != nil {
|
||||
if err := updateserver_model.UpdateLicenseKey(ctx, key); err != nil {
|
||||
ctx.ServerError("UpdateLicenseKey", err)
|
||||
return
|
||||
}
|
||||
@@ -375,7 +375,7 @@ const tplLicensesEditKey templates.TplName = "repo/licenses_edit_key"
|
||||
// LicensesEditKey shows the edit form for a license key.
|
||||
func LicensesEditKey(ctx *context.Context) {
|
||||
keyID := ctx.PathParamInt64("id")
|
||||
key, err := licenses.GetLicenseKeyByID(ctx, keyID)
|
||||
key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicenseKeyByID", err)
|
||||
return
|
||||
@@ -404,7 +404,7 @@ func LicensesEditKey(ctx *context.Context) {
|
||||
// LicensesEditKeyPost saves edits to a license key.
|
||||
func LicensesEditKeyPost(ctx *context.Context) {
|
||||
keyID := ctx.PathParamInt64("id")
|
||||
key, err := licenses.GetLicenseKeyByID(ctx, keyID)
|
||||
key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicenseKeyByID", err)
|
||||
return
|
||||
@@ -433,7 +433,7 @@ func LicensesEditKeyPost(ctx *context.Context) {
|
||||
key.ExpiresUnix = 0
|
||||
}
|
||||
|
||||
if err := licenses.UpdateLicenseKey(ctx, key); err != nil {
|
||||
if err := updateserver_model.UpdateLicenseKey(ctx, key); err != nil {
|
||||
ctx.ServerError("UpdateLicenseKey", err)
|
||||
return
|
||||
}
|
||||
@@ -445,13 +445,13 @@ func LicensesEditKeyPost(ctx *context.Context) {
|
||||
// LicensesEditPackage shows the edit form for a license package.
|
||||
func LicensesEditPackage(ctx *context.Context) {
|
||||
pkgID := ctx.PathParamInt64("id")
|
||||
pkg, err := licenses.GetLicensePackageByID(ctx, pkgID)
|
||||
pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicensePackageByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
if pkg.Name == licenses.MasterPackageName {
|
||||
if pkg.Name == updateserver_model.MasterPackageName {
|
||||
ctx.Flash.Error("Master package cannot be edited")
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
|
||||
return
|
||||
@@ -465,12 +465,12 @@ func LicensesEditPackage(ctx *context.Context) {
|
||||
ctx.Data["SelectedChannels"] = selectedChannels
|
||||
|
||||
ownerID := ctx.Repo.Repository.OwnerID
|
||||
orgCfg, _ := licenses.GetOrgConfig(ctx, ownerID)
|
||||
var editStreams []licenses.StreamDef
|
||||
orgCfg, _ := updateserver_model.GetOrgConfig(ctx, ownerID)
|
||||
var editStreams []updateserver_model.StreamDef
|
||||
if orgCfg != nil {
|
||||
editStreams = orgCfg.GetActiveStreams()
|
||||
} else {
|
||||
editStreams = licenses.DefaultJoomlaStreams()
|
||||
editStreams = updateserver_model.DefaultJoomlaStreams()
|
||||
}
|
||||
ctx.Data["AvailableStreams"] = editStreams
|
||||
ctx.Data["ChannelItems"] = buildChannelItems(editStreams)
|
||||
@@ -482,13 +482,13 @@ func LicensesEditPackage(ctx *context.Context) {
|
||||
// LicensesEditPackagePost saves edits to a license package.
|
||||
func LicensesEditPackagePost(ctx *context.Context) {
|
||||
pkgID := ctx.PathParamInt64("id")
|
||||
pkg, err := licenses.GetLicensePackageByID(ctx, pkgID)
|
||||
pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicensePackageByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
if pkg.Name == licenses.MasterPackageName {
|
||||
if pkg.Name == updateserver_model.MasterPackageName {
|
||||
ctx.Flash.Error("Master package cannot be edited")
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
|
||||
return
|
||||
@@ -518,7 +518,7 @@ func LicensesEditPackagePost(ctx *context.Context) {
|
||||
pkg.DomainRestriction = strings.TrimSpace(ctx.FormString("domain_restriction"))
|
||||
pkg.IsActive = ctx.FormString("is_active") == "on"
|
||||
|
||||
if err := licenses.UpdateLicensePackage(ctx, pkg); err != nil {
|
||||
if err := updateserver_model.UpdateLicensePackage(ctx, pkg); err != nil {
|
||||
ctx.ServerError("UpdateLicensePackage", err)
|
||||
return
|
||||
}
|
||||
@@ -535,17 +535,17 @@ func canDeleteLicenses(ctx *context.Context) bool {
|
||||
// LicensesArchivePackage archives a license package.
|
||||
func LicensesArchivePackage(ctx *context.Context) {
|
||||
pkgID := ctx.PathParamInt64("id")
|
||||
pkg, err := licenses.GetLicensePackageByID(ctx, pkgID)
|
||||
pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicensePackageByID", err)
|
||||
return
|
||||
}
|
||||
if pkg.Name == licenses.MasterPackageName {
|
||||
if pkg.Name == updateserver_model.MasterPackageName {
|
||||
ctx.Flash.Error("Master package cannot be archived")
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
|
||||
return
|
||||
}
|
||||
if err := licenses.ArchiveLicensePackage(ctx, pkgID); err != nil {
|
||||
if err := updateserver_model.ArchiveLicensePackage(ctx, pkgID); err != nil {
|
||||
ctx.ServerError("ArchiveLicensePackage", err)
|
||||
return
|
||||
}
|
||||
@@ -556,7 +556,7 @@ func LicensesArchivePackage(ctx *context.Context) {
|
||||
// LicensesUnarchivePackage removes archive status from a package.
|
||||
func LicensesUnarchivePackage(ctx *context.Context) {
|
||||
pkgID := ctx.PathParamInt64("id")
|
||||
if err := licenses.UnarchiveLicensePackage(ctx, pkgID); err != nil {
|
||||
if err := updateserver_model.UnarchiveLicensePackage(ctx, pkgID); err != nil {
|
||||
ctx.ServerError("UnarchiveLicensePackage", err)
|
||||
return
|
||||
}
|
||||
@@ -571,17 +571,17 @@ func LicensesDeletePackage(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
pkgID := ctx.PathParamInt64("id")
|
||||
pkg, err := licenses.GetLicensePackageByID(ctx, pkgID)
|
||||
pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicensePackageByID", err)
|
||||
return
|
||||
}
|
||||
if pkg.Name == licenses.MasterPackageName {
|
||||
if pkg.Name == updateserver_model.MasterPackageName {
|
||||
ctx.Flash.Error("Master package cannot be deleted")
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
|
||||
return
|
||||
}
|
||||
if err := licenses.DeleteLicensePackage(ctx, pkgID); err != nil {
|
||||
if err := updateserver_model.DeleteLicensePackage(ctx, pkgID); err != nil {
|
||||
ctx.ServerError("DeleteLicensePackage", err)
|
||||
return
|
||||
}
|
||||
@@ -593,13 +593,13 @@ func LicensesDeletePackage(ctx *context.Context) {
|
||||
// LicensesRenewKey extends a license key's expiration by the package's duration.
|
||||
func LicensesRenewKey(ctx *context.Context) {
|
||||
keyID := ctx.PathParamInt64("id")
|
||||
key, err := licenses.GetLicenseKeyByID(ctx, keyID)
|
||||
key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicenseKeyByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
pkg, err := licenses.GetLicensePackageByID(ctx, key.PackageID)
|
||||
pkg, err := updateserver_model.GetLicensePackageByID(ctx, key.PackageID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicensePackageByID", err)
|
||||
return
|
||||
@@ -610,7 +610,7 @@ func LicensesRenewKey(ctx *context.Context) {
|
||||
days = 365 // default to 1 year for lifetime packages
|
||||
}
|
||||
|
||||
if err := licenses.RenewLicenseKey(ctx, keyID, days); err != nil {
|
||||
if err := updateserver_model.RenewLicenseKey(ctx, keyID, days); err != nil {
|
||||
ctx.ServerError("RenewLicenseKey", err)
|
||||
return
|
||||
}
|
||||
@@ -626,7 +626,7 @@ func LicensesDeleteKey(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
keyID := ctx.PathParamInt64("id")
|
||||
if err := licenses.DeleteLicenseKey(ctx, keyID); err != nil {
|
||||
if err := updateserver_model.DeleteLicenseKey(ctx, keyID); err != nil {
|
||||
ctx.ServerError("DeleteLicenseKey", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
|
||||
licenses_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/renderhelper"
|
||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
"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).
|
||||
if ctx.Data["LicensingEnabled"] == true {
|
||||
ownerID := ctx.Repo.Repository.OwnerID
|
||||
orgCfg, _ := licenses_model.GetOrgConfig(ctx, ownerID)
|
||||
orgCfg, _ := updateserver_model.GetOrgConfig(ctx, ownerID)
|
||||
if orgCfg != nil {
|
||||
ctx.Data["AvailableStreams"] = orgCfg.GetActiveStreams()
|
||||
} else {
|
||||
ctx.Data["AvailableStreams"] = licenses_model.DefaultJoomlaStreams()
|
||||
ctx.Data["AvailableStreams"] = updateserver_model.DefaultJoomlaStreams()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,7 +534,7 @@ func NewReleasePost(ctx *context.Context) {
|
||||
}
|
||||
// Save manual stream assignment if specified.
|
||||
if streamName := form.UpdateStream; streamName != "" {
|
||||
_ = licenses_model.SetReleaseStream(ctx, rel.ID, rel.RepoID, streamName)
|
||||
_ = updateserver_model.SetReleaseStream(ctx, rel.ID, rel.RepoID, streamName)
|
||||
}
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/releases")
|
||||
return
|
||||
@@ -596,7 +596,10 @@ func EditRelease(ctx *context.Context) {
|
||||
ctx.Data["content"] = rel.Note
|
||||
ctx.Data["prerelease"] = rel.IsPrerelease
|
||||
ctx.Data["IsDraft"] = rel.IsDraft
|
||||
ctx.Data["ReleaseStream"] = licenses_model.GetReleaseStream(ctx, rel.ID)
|
||||
releaseStream := updateserver_model.GetReleaseStream(ctx, rel.ID)
|
||||
ctx.Data["ReleaseStream"] = releaseStream
|
||||
ctx.Data["ReleaseHasStream"] = releaseStream != ""
|
||||
ctx.Data["CDNEnabled"] = setting.CDN.Enabled
|
||||
|
||||
rel.Repo = ctx.Repo.Repository
|
||||
if err := rel.LoadAttributes(ctx); err != nil {
|
||||
@@ -679,10 +682,32 @@ func EditReleasePost(ctx *context.Context) {
|
||||
}
|
||||
// Save manual stream assignment.
|
||||
if streamName := form.UpdateStream; streamName != "" {
|
||||
_ = licenses_model.SetReleaseStream(ctx, rel.ID, rel.RepoID, streamName)
|
||||
_ = updateserver_model.SetReleaseStream(ctx, rel.ID, rel.RepoID, streamName)
|
||||
} else {
|
||||
_ = licenses_model.DeleteReleaseStream(ctx, rel.ID)
|
||||
_ = updateserver_model.DeleteReleaseStream(ctx, rel.ID)
|
||||
}
|
||||
|
||||
// Update per-asset CDN visibility flags.
|
||||
if setting.CDN.Enabled {
|
||||
const cdnPrefix = "attachment-cdn-"
|
||||
cdnUUIDs := make(map[string]bool)
|
||||
for k := range ctx.Req.Form {
|
||||
if strings.HasPrefix(k, cdnPrefix) {
|
||||
cdnUUIDs[k[len(cdnPrefix):]] = true
|
||||
}
|
||||
}
|
||||
// Load all attachments for this release to update cdn_public
|
||||
if err := repo_model.GetReleaseAttachments(ctx, rel); err == nil {
|
||||
for _, attach := range rel.Attachments {
|
||||
wantCDN := cdnUUIDs[attach.UUID]
|
||||
if attach.CDNPublic != wantCDN {
|
||||
attach.CDNPublic = wantCDN
|
||||
_ = repo_model.UpdateAttachmentByUUID(ctx, attach, "cdn_public")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/releases")
|
||||
}
|
||||
|
||||
|
||||
@@ -4,59 +4,15 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
licenses_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
)
|
||||
|
||||
const tplSettingsLicensing templates.TplName = "repo/settings/licensing"
|
||||
|
||||
// LicensingSettings displays the licensing settings page.
|
||||
// LicensingSettings redirects to the manifest page where licensing is now consolidated.
|
||||
func LicensingSettings(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings.licensing_section")
|
||||
ctx.Data["PageIsSettingsLicensing"] = true
|
||||
|
||||
repoCfg, _ := licenses_model.GetRepoConfig(ctx, ctx.Repo.Repository.ID)
|
||||
ctx.Data["RepoUpdateConfig"] = repoCfg
|
||||
|
||||
ctx.HTML(http.StatusOK, tplSettingsLicensing)
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/settings/updateserver")
|
||||
}
|
||||
|
||||
// LicensingSettingsPost saves the licensing settings.
|
||||
// LicensingSettingsPost redirects POST to the manifest page.
|
||||
func LicensingSettingsPost(ctx *context.Context) {
|
||||
repo := ctx.Repo.Repository
|
||||
|
||||
updatePlatform := ctx.FormString("update_platform")
|
||||
if updatePlatform == "" {
|
||||
updatePlatform = "joomla"
|
||||
}
|
||||
|
||||
updateCfg := &licenses_model.UpdateStreamConfig{
|
||||
OwnerID: repo.OwnerID,
|
||||
RepoID: repo.ID,
|
||||
Platform: updatePlatform,
|
||||
LicensingEnabled: ctx.FormString("enable_licensing") == "on",
|
||||
RequireKey: ctx.FormString("require_update_key") == "on",
|
||||
DownloadGating: ctx.FormString("download_gating"),
|
||||
SupportURL: ctx.FormString("support_url"),
|
||||
ExtensionName: ctx.FormString("extension_name"),
|
||||
DisplayName: ctx.FormString("display_name"),
|
||||
ExtensionType: ctx.FormString("extension_type"),
|
||||
TargetVersion: ctx.FormString("target_version"),
|
||||
Maintainer: ctx.FormString("maintainer"),
|
||||
PHPMinimum: ctx.FormString("php_minimum"),
|
||||
StreamMode: "joomla",
|
||||
}
|
||||
|
||||
if err := licenses_model.SaveConfig(ctx, updateCfg); err != nil {
|
||||
log.Error("SaveConfig: %v", err)
|
||||
ctx.ServerError("SaveConfig", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/settings/licensing")
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/settings/updateserver")
|
||||
}
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
)
|
||||
|
||||
const tplSettingsManifest templates.TplName = "repo/settings/manifest"
|
||||
|
||||
// manifestXML mirrors the .mokogitea/manifest.xml schema for XML parsing.
|
||||
type manifestXML struct {
|
||||
XMLName xml.Name `xml:"moko-platform"`
|
||||
Identity manifestIdentity `xml:"identity"`
|
||||
Governance manifestGovernance `xml:"governance"`
|
||||
Build manifestBuild `xml:"build"`
|
||||
}
|
||||
|
||||
type manifestIdentity struct {
|
||||
Name string `xml:"name"`
|
||||
Org string `xml:"org"`
|
||||
Description string `xml:"description"`
|
||||
Version string `xml:"version"`
|
||||
License manifestLicense `xml:"license"`
|
||||
}
|
||||
|
||||
type manifestLicense struct {
|
||||
SPDX string `xml:"spdx,attr"`
|
||||
Name string `xml:",chardata"`
|
||||
}
|
||||
|
||||
type manifestGovernance struct {
|
||||
Platform string `xml:"platform"`
|
||||
StandardsVersion string `xml:"standards-version"`
|
||||
StandardsSource string `xml:"standards-source"`
|
||||
}
|
||||
|
||||
type manifestBuild struct {
|
||||
Language string `xml:"language"`
|
||||
PackageType string `xml:"package-type"`
|
||||
EntryPoint string `xml:"entry-point"`
|
||||
}
|
||||
|
||||
// ManifestSettings displays the repo manifest settings page.
|
||||
// On first visit, if no manifest exists in DB but .mokogitea/manifest.xml
|
||||
// exists in the repo, it auto-migrates the XML values into the database.
|
||||
func ManifestSettings(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings.manifest")
|
||||
ctx.Data["PageIsSettingsManifest"] = true
|
||||
|
||||
repoID := ctx.Repo.Repository.ID
|
||||
manifest, err := repo_model.GetRepoManifest(ctx, repoID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRepoManifest", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Auto-detect and migrate .mokogitea/manifest.xml if no DB record exists.
|
||||
if manifest == nil {
|
||||
manifest = tryMigrateManifestXML(ctx)
|
||||
}
|
||||
|
||||
if manifest == nil {
|
||||
// No manifest found — provide empty defaults from repo metadata.
|
||||
manifest = &repo_model.RepoManifest{
|
||||
RepoID: repoID,
|
||||
Name: ctx.Repo.Repository.Name,
|
||||
Org: ctx.Repo.Repository.OwnerName,
|
||||
Description: ctx.Repo.Repository.Description,
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data["Manifest"] = manifest
|
||||
ctx.HTML(http.StatusOK, tplSettingsManifest)
|
||||
}
|
||||
|
||||
// ManifestSettingsPost saves manifest settings from the form.
|
||||
func ManifestSettingsPost(ctx *context.Context) {
|
||||
manifest := &repo_model.RepoManifest{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Name: ctx.FormString("name"),
|
||||
Org: ctx.FormString("org"),
|
||||
Description: ctx.FormString("description"),
|
||||
Version: ctx.FormString("version"),
|
||||
LicenseSPDX: ctx.FormString("license_spdx"),
|
||||
LicenseName: ctx.FormString("license_name"),
|
||||
Platform: ctx.FormString("platform"),
|
||||
StandardsVersion: ctx.FormString("standards_version"),
|
||||
StandardsSource: ctx.FormString("standards_source"),
|
||||
Language: ctx.FormString("language"),
|
||||
PackageType: ctx.FormString("package_type"),
|
||||
EntryPoint: ctx.FormString("entry_point"),
|
||||
}
|
||||
|
||||
if err := repo_model.CreateOrUpdateRepoManifest(ctx, manifest); err != nil {
|
||||
ctx.ServerError("CreateOrUpdateRepoManifest", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.manifest_saved"))
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/settings/manifest")
|
||||
}
|
||||
|
||||
// tryMigrateManifestXML reads .mokogitea/manifest.xml from the repo,
|
||||
// parses it, and stores the values in the DB. Returns nil if no file found.
|
||||
func tryMigrateManifestXML(ctx *context.Context) *repo_model.RepoManifest {
|
||||
if ctx.Repo.GitRepo == nil || ctx.Repo.Commit == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(".mokogitea/manifest.xml")
|
||||
if err != nil || entry == nil {
|
||||
return nil // no manifest.xml found — not an error
|
||||
}
|
||||
|
||||
reader, err := entry.Blob().DataAsync()
|
||||
if err != nil {
|
||||
log.Error("ManifestMigrate: read blob: %v", err)
|
||||
return nil
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
var mxml manifestXML
|
||||
if err := xml.NewDecoder(reader).Decode(&mxml); err != nil {
|
||||
log.Error("ManifestMigrate: parse XML: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
manifest := &repo_model.RepoManifest{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Name: mxml.Identity.Name,
|
||||
Org: mxml.Identity.Org,
|
||||
Description: mxml.Identity.Description,
|
||||
Version: mxml.Identity.Version,
|
||||
LicenseSPDX: mxml.Identity.License.SPDX,
|
||||
LicenseName: mxml.Identity.License.Name,
|
||||
Platform: mxml.Governance.Platform,
|
||||
StandardsVersion: mxml.Governance.StandardsVersion,
|
||||
StandardsSource: mxml.Governance.StandardsSource,
|
||||
Language: mxml.Build.Language,
|
||||
PackageType: mxml.Build.PackageType,
|
||||
EntryPoint: mxml.Build.EntryPoint,
|
||||
}
|
||||
|
||||
if err := repo_model.CreateOrUpdateRepoManifest(ctx, manifest); err != nil {
|
||||
log.Error("ManifestMigrate: save to DB: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Info("ManifestMigrate: migrated .mokogitea/manifest.xml for repo %s/%s",
|
||||
ctx.Repo.Repository.OwnerName, ctx.Repo.Repository.Name)
|
||||
|
||||
ctx.Flash.Info(fmt.Sprintf("Manifest settings imported from .mokogitea/manifest.xml. You can now delete the file from the repository."))
|
||||
return manifest
|
||||
}
|
||||
@@ -9,20 +9,39 @@ import (
|
||||
"net/http"
|
||||
|
||||
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/services/context"
|
||||
)
|
||||
|
||||
const tplSettingsMetadata templates.TplName = "repo/settings/metadata"
|
||||
|
||||
// Metadata displays the repo metadata page (repo-scoped custom field values).
|
||||
// Metadata displays the consolidated metadata page:
|
||||
// project identity (manifest), update server config, and repo-scoped custom fields.
|
||||
func Metadata(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings.metadata")
|
||||
ctx.Data["Title"] = "Metadata"
|
||||
ctx.Data["PageIsSettingsMetadata"] = true
|
||||
|
||||
ownerID := ctx.Repo.Repository.OwnerID
|
||||
repoID := ctx.Repo.Repository.ID
|
||||
ownerID := ctx.Repo.Repository.OwnerID
|
||||
|
||||
// Load manifest (project identity).
|
||||
manifest, err := repo_model.GetRepoManifest(ctx, repoID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRepoManifest", err)
|
||||
return
|
||||
}
|
||||
if manifest == nil {
|
||||
manifest = &repo_model.RepoManifest{
|
||||
RepoID: repoID,
|
||||
Name: ctx.Repo.Repository.Name,
|
||||
Org: ctx.Repo.Repository.OwnerName,
|
||||
Description: ctx.Repo.Repository.Description,
|
||||
}
|
||||
}
|
||||
ctx.Data["Manifest"] = manifest
|
||||
|
||||
// Load repo-scoped custom fields.
|
||||
fields, _ := issues_model.GetCustomFieldsByOwner(ctx, ownerID, issues_model.CustomFieldScopeRepo)
|
||||
ctx.Data["CustomFieldDefs"] = fields
|
||||
|
||||
@@ -45,8 +64,79 @@ func Metadata(ctx *context.Context) {
|
||||
ctx.HTML(http.StatusOK, tplSettingsMetadata)
|
||||
}
|
||||
|
||||
// MetadataPost saves repo-scoped custom field values.
|
||||
// MetadataPost routes to the correct sub-handler based on the action param.
|
||||
func MetadataPost(ctx *context.Context) {
|
||||
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
|
||||
ownerID := ctx.Repo.Repository.OwnerID
|
||||
|
||||
@@ -59,6 +149,7 @@ func MetadataPost(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.metadata_saved"))
|
||||
ctx.Flash.Success("Custom fields saved")
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/settings/metadata")
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/organization"
|
||||
licenses_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/perm"
|
||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
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
|
||||
func Settings(ctx *context.Context) {
|
||||
repoCfg, _ := licenses_model.GetRepoConfig(ctx, ctx.Repo.Repository.ID)
|
||||
repoCfg, _ := updateserver_model.GetRepoConfig(ctx, ctx.Repo.Repository.ID)
|
||||
ctx.Data["RepoUpdateConfig"] = repoCfg
|
||||
ctx.HTML(http.StatusOK, tplSettingsOptions)
|
||||
}
|
||||
@@ -675,29 +675,37 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
// Save update server platform and require-key settings.
|
||||
updatePlatform := form.UpdatePlatform
|
||||
if updatePlatform == "" {
|
||||
updatePlatform = "joomla"
|
||||
}
|
||||
updateCfg := &licenses_model.UpdateStreamConfig{
|
||||
OwnerID: repo.OwnerID,
|
||||
RepoID: repo.ID,
|
||||
Platform: updatePlatform,
|
||||
LicensingEnabled: form.EnableLicensing,
|
||||
RequireKey: form.RequireUpdateKey,
|
||||
DownloadGating: form.DownloadGating,
|
||||
SupportURL: form.SupportURL,
|
||||
ExtensionName: form.ExtensionName,
|
||||
DisplayName: form.DisplayName,
|
||||
ExtensionType: form.ExtensionType,
|
||||
TargetVersion: form.TargetVersion,
|
||||
Maintainer: form.Maintainer,
|
||||
PHPMinimum: form.PHPMinimum,
|
||||
StreamMode: "joomla", // inherit org default
|
||||
}
|
||||
if err := licenses_model.SaveConfig(ctx, updateCfg); err != nil {
|
||||
log.Error("SaveConfig: %v", err)
|
||||
// Save update server settings. If disabled, delete repo-level config
|
||||
// so it falls through to org defaults cleanly.
|
||||
if !form.EnableLicensing {
|
||||
// Remove repo-level override so org config takes effect
|
||||
if err := updateserver_model.DeleteRepoConfig(ctx, repo.ID); err != nil {
|
||||
log.Error("DeleteRepoConfig: %v", err)
|
||||
}
|
||||
} else {
|
||||
updatePlatform := form.UpdatePlatform
|
||||
if updatePlatform == "" {
|
||||
updatePlatform = "joomla"
|
||||
}
|
||||
updateCfg := &updateserver_model.UpdateStreamConfig{
|
||||
OwnerID: repo.OwnerID,
|
||||
RepoID: repo.ID,
|
||||
Platform: updatePlatform,
|
||||
LicensingEnabled: form.EnableLicensing,
|
||||
RequireKey: form.RequireUpdateKey,
|
||||
DownloadGating: form.DownloadGating,
|
||||
SupportURL: form.SupportURL,
|
||||
ExtensionName: form.ExtensionName,
|
||||
DisplayName: form.DisplayName,
|
||||
ExtensionType: form.ExtensionType,
|
||||
TargetVersion: form.TargetVersion,
|
||||
Maintainer: form.Maintainer,
|
||||
PHPMinimum: form.PHPMinimum,
|
||||
StreamMode: "joomla",
|
||||
}
|
||||
if err := updateserver_model.SaveConfig(ctx, updateCfg); err != nil {
|
||||
log.Error("SaveConfig: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Trace("Repository advanced settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
)
|
||||
|
||||
const tplSettingsUpdateServer templates.TplName = "repo/settings/updateserver"
|
||||
|
||||
// UpdateServerSettings displays the update server settings page.
|
||||
func UpdateServerSettings(ctx *context.Context) {
|
||||
ctx.Data["Title"] = "Update Server"
|
||||
ctx.Data["PageIsSettingsUpdateServer"] = true
|
||||
|
||||
repoCfg, _ := updateserver_model.GetRepoConfig(ctx, ctx.Repo.Repository.ID)
|
||||
ctx.Data["RepoUpdateConfig"] = repoCfg
|
||||
|
||||
ctx.HTML(http.StatusOK, tplSettingsUpdateServer)
|
||||
}
|
||||
|
||||
// UpdateServerSettingsPost saves update server visibility and gating settings.
|
||||
func UpdateServerSettingsPost(ctx *context.Context) {
|
||||
repo := ctx.Repo.Repository
|
||||
|
||||
enabled := ctx.FormString("enable_licensing") == "on"
|
||||
|
||||
if !enabled {
|
||||
if err := updateserver_model.DeleteRepoConfig(ctx, repo.ID); err != nil {
|
||||
log.Error("DeleteRepoConfig: %v", err)
|
||||
}
|
||||
} else {
|
||||
// Load existing config to preserve platform and other fields.
|
||||
existing, _ := updateserver_model.GetRepoConfig(ctx, repo.ID)
|
||||
platform := "joomla"
|
||||
streamMode := "joomla"
|
||||
supportURL := ""
|
||||
if existing != nil {
|
||||
platform = existing.Platform
|
||||
streamMode = existing.StreamMode
|
||||
supportURL = existing.SupportURL
|
||||
}
|
||||
|
||||
updateCfg := &updateserver_model.UpdateStreamConfig{
|
||||
OwnerID: repo.OwnerID,
|
||||
RepoID: repo.ID,
|
||||
Platform: platform,
|
||||
LicensingEnabled: true,
|
||||
RequireKey: ctx.FormString("require_update_key") == "on",
|
||||
DownloadGating: ctx.FormString("download_gating"),
|
||||
FeedVisibility: ctx.FormString("feed_visibility"),
|
||||
SupportURL: supportURL,
|
||||
StreamMode: streamMode,
|
||||
}
|
||||
|
||||
if err := updateserver_model.SaveConfig(ctx, updateCfg); err != nil {
|
||||
log.Error("SaveConfig: %v", err)
|
||||
ctx.ServerError("SaveConfig", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Flash.Success("Update server settings saved")
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/settings/updateserver")
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/json"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
@@ -33,15 +33,15 @@ func validateUpdateKey(ctx *context.Context) (allowedChannels []string, ok bool,
|
||||
}
|
||||
|
||||
domain := ctx.FormString("domain")
|
||||
key, pkg, err := licenses.ValidateLicenseKeyForRepo(ctx, rawKey, domain, ctx.Repo.Repository.ID)
|
||||
key, pkg, err := updateserver_model.ValidateLicenseKeyForRepo(ctx, rawKey, domain, ctx.Repo.Repository.ID)
|
||||
if err != nil {
|
||||
log.Debug("License key validation failed: %v", err)
|
||||
return nil, false, false
|
||||
}
|
||||
|
||||
// Update heartbeat and record usage.
|
||||
_ = licenses.TouchHeartbeat(ctx, key.ID)
|
||||
_ = licenses.RecordUsage(ctx, &licenses.LicenseKeyUsage{
|
||||
_ = updateserver_model.TouchHeartbeat(ctx, key.ID)
|
||||
_ = updateserver_model.RecordUsage(ctx, &updateserver_model.LicenseKeyUsage{
|
||||
KeyID: key.ID,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Domain: domain,
|
||||
@@ -93,7 +93,7 @@ func ServeUpdatesXML(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
repoCfg, _ := licenses.GetRepoConfig(ctx, ctx.Repo.Repository.ID)
|
||||
repoCfg, _ := updateserver_model.GetRepoConfig(ctx, ctx.Repo.Repository.ID)
|
||||
// Show <downloadkey> only when downloads are gated (prerelease or all).
|
||||
// No gating = no license keys needed = no downloadkey element.
|
||||
requireKey := repoCfg != nil && repoCfg.DownloadGating != "" && repoCfg.DownloadGating != "none"
|
||||
|
||||
@@ -137,6 +137,8 @@ type PrepareOwnerHeaderResult struct {
|
||||
const (
|
||||
RepoNameProfilePrivate = ".profile-private"
|
||||
RepoNameProfile = ".profile"
|
||||
RepoNameWikiPublic = ".profile"
|
||||
RepoNameWikiPrivate = ".profile-private"
|
||||
)
|
||||
|
||||
func RenderUserOrgHeader(ctx *context.Context) (result *PrepareOwnerHeaderResult, err error) {
|
||||
@@ -155,6 +157,18 @@ func RenderUserOrgHeader(ctx *context.Context) (result *PrepareOwnerHeaderResult
|
||||
result.ProfilePrivateRepo, result.ProfilePrivateReadmeBlob = FindOwnerProfileReadme(ctx, ctx.Doer, RepoNameProfilePrivate)
|
||||
result.HasOrgProfileReadme = result.ProfilePublicReadmeBlob != nil || result.ProfilePrivateReadmeBlob != nil
|
||||
ctx.Data["HasOrgProfileReadme"] = result.HasOrgProfileReadme // many pages need it to show the "overview" tab
|
||||
|
||||
// Check if org has a wiki (internal convention repos or external URL).
|
||||
orgUser := ctx.ContextUser
|
||||
if orgUser.WikiMode == "external" && orgUser.WikiURL != "" {
|
||||
ctx.Data["HasOrgWiki"] = true
|
||||
ctx.Data["OrgWikiIsExternal"] = true
|
||||
ctx.Data["OrgWikiExternalURL"] = orgUser.WikiURL
|
||||
} else {
|
||||
hasWiki := OrgWikiRepoExists(ctx, ctx.ContextUser.ID, RepoNameWikiPublic) ||
|
||||
OrgWikiRepoExists(ctx, ctx.ContextUser.ID, RepoNameWikiPrivate)
|
||||
ctx.Data["HasOrgWiki"] = hasWiki
|
||||
}
|
||||
} else {
|
||||
_, profileReadmeBlob := FindOwnerProfileReadme(ctx, ctx.Doer)
|
||||
ctx.Data["HasUserProfileReadme"] = profileReadmeBlob != nil
|
||||
@@ -194,3 +208,20 @@ func loadHeaderCount(ctx *context.Context) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// OrgWikiRepoExists checks whether a profile repo's wiki exists and has content.
|
||||
func OrgWikiRepoExists(ctx *context.Context, ownerID int64, repoName string) bool {
|
||||
dbRepo, err := repo_model.GetRepositoryByName(ctx, ownerID, repoName)
|
||||
if err != nil {
|
||||
log.Trace("OrgWikiRepoExists: repo %s not found for owner %d: %v", repoName, ownerID, err)
|
||||
return false
|
||||
}
|
||||
wikiRepo := dbRepo.WikiStorageRepo()
|
||||
_, err = gitrepo.GetDefaultBranch(ctx, wikiRepo)
|
||||
if err != nil {
|
||||
log.Error("OrgWikiRepoExists: GetDefaultBranch for wiki of %s failed: %v (path: %s)", dbRepo.FullName(), err, wikiRepo.RelativePath())
|
||||
return false
|
||||
}
|
||||
log.Trace("OrgWikiRepoExists: wiki found for %s", dbRepo.FullName())
|
||||
return true
|
||||
}
|
||||
|
||||
+22
-2
@@ -260,6 +260,20 @@ func Routes() *web.Router {
|
||||
// GetHead allows a HEAD request redirect to GET if HEAD method is not defined for that route
|
||||
routes.BeforeRouting(chi_middleware.GetHead)
|
||||
|
||||
// CDN hostname handler - intercepts requests on the CDN domain before any
|
||||
// session/auth middleware runs, serving only CDN-public release assets.
|
||||
if setting.CDN.Enabled && setting.CDN.Domain != "" {
|
||||
routes.BeforeRouting(func(resp http.ResponseWriter, req *http.Request) {
|
||||
host := req.Host
|
||||
if idx := strings.Index(host, ":"); idx >= 0 {
|
||||
host = host[:idx]
|
||||
}
|
||||
if strings.EqualFold(host, setting.CDN.Domain) {
|
||||
repo.CDNHandler(resp, req)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
routes.Head("/", misc.DummyOK) // for health check - doesn't need to be passed through gzip handler
|
||||
routes.Methods("GET, HEAD", "/assets/site-manifest.json", misc.SiteManifest)
|
||||
routes.Methods("GET, HEAD, OPTIONS", "/assets/*", routing.MarkLogLevelTrace, public.AssetsCors(), public.FileHandlerFunc())
|
||||
@@ -1148,6 +1162,11 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
m.Get("/repositories", org.Repositories)
|
||||
m.Get("/heatmap", user.DashboardHeatmap)
|
||||
|
||||
m.Group("/wiki", func() {
|
||||
m.Get("", org.Wiki)
|
||||
m.Get("/*", org.Wiki)
|
||||
})
|
||||
|
||||
m.Group("/projects", func() {
|
||||
m.Group("", func() {
|
||||
m.Get("", org.Projects)
|
||||
@@ -1210,9 +1229,10 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
m.Group("", func() {
|
||||
m.Combo("/advanced").Get(repo_setting.AdvancedSettings).Post(web.Bind(forms.RepoSettingForm{}), repo_setting.SettingsPost)
|
||||
}, repo_setting.SettingsCtxData)
|
||||
m.Combo("/licensing").Get(repo_setting.LicensingSettings).Post(repo_setting.LicensingSettingsPost)
|
||||
m.Combo("/manifest").Get(repo_setting.ManifestSettings).Post(repo_setting.ManifestSettingsPost)
|
||||
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.Combo("").Get(repo_setting.SecuritySettings).Post(repo_setting.SecuritySettingsPost)
|
||||
m.Post("/scan", repo_setting.SecurityScanNow)
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
|
||||
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
|
||||
licenses_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
|
||||
access_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/perm/access"
|
||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
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
|
||||
// releases and downloads via license key, even without membership.
|
||||
orgCfg, _ := licenses_model.GetOrgConfig(ctx, repo.OwnerID)
|
||||
repoCfg, _ := licenses_model.GetRepoConfig(ctx, repo.ID)
|
||||
orgCfg, _ := updateserver_model.GetOrgConfig(ctx, repo.OwnerID)
|
||||
repoCfg, _ := updateserver_model.GetRepoConfig(ctx, repo.ID)
|
||||
licensingEnabled := (orgCfg != nil && orgCfg.LicensingEnabled) ||
|
||||
(repoCfg != nil && repoCfg.LicensingEnabled)
|
||||
|
||||
@@ -652,12 +652,12 @@ func repoAssignmentPrepareTemplateData(ctx *Context, data *repoAssignmentPrepare
|
||||
}
|
||||
|
||||
// Check if licensing is enabled for this repo/org.
|
||||
orgCfg, _ := licenses_model.GetOrgConfig(ctx, repo.OwnerID)
|
||||
repoUpdateCfg, _ := licenses_model.GetRepoConfig(ctx, repo.ID)
|
||||
orgCfg, _ := updateserver_model.GetOrgConfig(ctx, repo.OwnerID)
|
||||
repoUpdateCfg, _ := updateserver_model.GetRepoConfig(ctx, repo.ID)
|
||||
licensingEnabled := (orgCfg != nil && orgCfg.LicensingEnabled) ||
|
||||
(repoUpdateCfg != nil && repoUpdateCfg.LicensingEnabled)
|
||||
|
||||
numLicensePackages, _ := db.Count[licenses_model.LicensePackage](ctx, licenses_model.FindLicensePackageOptions{
|
||||
numLicensePackages, _ := db.Count[updateserver_model.LicensePackage](ctx, updateserver_model.FindLicensePackageOptions{
|
||||
OwnerID: repo.OwnerID,
|
||||
})
|
||||
ctx.Data["NumLicensePackages"] = numLicensePackages
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user