Compare commits

..

61 Commits

Author SHA1 Message Date
jmiller 1bf7313e40 chore: sync updates.xml 05.05.00 from main [skip ci] 2026-05-30 17:59:40 +00:00
Jonathan Miller 6c06384966 feat(updates): built-in Joomla update server endpoint
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m4s
Add GET /{owner}/{repo}/updates.xml that dynamically generates a
Joomla-compatible updates.xml from the repository's releases.

Features:
- Automatically maps release tags to channels (stable/rc/beta/alpha/dev)
- Finds .zip attachments for download URLs, falls back to archive URL
- Emits one entry per channel (latest release wins)
- Extracts version from tag names, strips common prefixes
- Publicly accessible (no auth required) for Joomla update clients

This is Phase 1 of #239 — the core dynamic update feed generation.
Future phases will add license key gating, Dolibarr support, and
repo settings UI.

Ref #239

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 12:54:31 -05:00
jmiller 4b6df79ae0 Merge pull request 'feat(admin): configurable default landing page from site administration' (#241) from feat/admin-landing-page into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Universal: Build & Release / Promote to RC (pull_request) Failing after 12s
PR RC Release / Build RC Release (pull_request) Successful in 29s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 8m23s
feat(admin): configurable default landing page (#240) (#241)
2026-05-30 17:18:16 +00:00
Jonathan Miller 1d1482a3dc feat(admin): configurable default landing page from site administration
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m6s
Add a dynamic config option to set the default landing page for
unauthenticated visitors from Site Administration > Settings. Options:
- Home (default explore page)
- Explore (repository explore)
- Organizations (org explore)
- Login (redirect to login)
- Custom path (any internal URL like /MokoConsulting)

The setting takes effect immediately without restart, using the same
dynamic config system as maintenance mode and web banner. Falls back
to the static LANDING_PAGE setting from app.ini if not configured.

Closes #240

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 12:17:40 -05:00
jmiller fba9c7eed9 chore: sync updates.xml 05.02.00 from main [skip ci] 2026-05-30 16:12:32 +00:00
jmiller 086c506a10 Merge pull request 'docs: update CHANGELOG for actions bot rebrand and deploy trigger' (#236) from chore/changelog-actions-bot into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Failing after 13s
PR RC Release / Build RC Release (pull_request) Successful in 33s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 8m28s
docs: update CHANGELOG for actions bot rebrand and deploy trigger (#236)
2026-05-30 16:07:31 +00:00
Jonathan Miller 5bf2f57f53 docs: update CHANGELOG for actions bot rebrand and deploy trigger
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m9s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 11:06:40 -05:00
jmiller d3bca854e5 chore: add .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-05-30 16:03:30 +00:00
jmiller ce344910b2 Merge pull request 'fix(ci): auto-deploy to production on merge to main' (#235) from fix/deploy-on-main-merge into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
fix(ci): auto-deploy to production on merge to main (#235)
2026-05-30 15:59:42 +00:00
Jonathan Miller 2fe549482d fix(ci): auto-deploy to production on merge to main
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 3s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m17s
Add push trigger on main branch to deploy-mokogitea.yml so merges
to main automatically deploy to production. When triggered by push,
the version is derived from git describe and environment defaults to
production. Manual workflow_dispatch still works for explicit version
deployments and dev environment targeting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 10:58:52 -05:00
jmiller 524b38f494 Merge pull request 'feat(actions): rebrand actions bot user and add branch protection whitelist' (#234) from feat/actions-bot-rebrand-and-branch-protection into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
feat(actions): rebrand actions bot user and add branch protection whitelist (#234)
2026-05-30 15:48:06 +00:00
Jonathan Miller d4824dc05b feat(actions): rebrand actions bot user and add branch protection whitelist
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m13s
Rebrand the built-in actions bot user from upstream Gitea naming to
MokoGitea branding:
- Name: gitea-actions → mokogitea-actions
- FullName: Gitea Actions → MokoGitea Actions
- Email: teabot@gitea.io → mokogitea-actions[bot]@mokoconsulting.tech

Add backward-compatible name recognition so all three bot name variants
(mokogitea-actions, gitea-actions, github-actions) with optional [bot]
suffix resolve to the same system user.

Add WhitelistActionsUser, MergeWhitelistActionsUser, and
ForcePushAllowlistActionsUser toggles to branch protection rules,
allowing CI/CD workflows to push to protected branches when explicitly
enabled. Previously the actions bot (virtual user ID -2) could never be
added to whitelist because updateUserWhitelist() only validates real
database users.

Closes #233

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 10:37:43 -05:00
jmiller 33b2ae2d4d chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-05-30 15:04:24 +00:00
jmiller 69f1a77459 chore: sync .mokogitea/workflows/auto-bump.yml from moko-platform [skip ci] 2026-05-30 15:01:54 +00:00
jmiller 0915b05b0f chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-05-30 01:16:46 +00:00
jmiller 6f38f89765 chore: sync .mokogitea/workflows/auto-bump.yml from moko-platform [skip ci] 2026-05-29 10:32:14 +00:00
jmiller 1b0c86847d chore: sync .mokogitea/workflows/update-server.yml from moko-platform [skip ci] 2026-05-28 20:51:14 +00:00
jmiller 789d3c9aa8 chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-05-28 20:46:21 +00:00
jmiller 700cc77d0b chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-05-28 20:28:27 +00:00
jmiller c268970505 chore: sync .mokogitea/workflows/pre-release.yml from moko-platform [skip ci] 2026-05-28 20:09:19 +00:00
jmiller 1f09979c19 chore: sync .mokogitea/workflows/update-server.yml from moko-platform [skip ci] 2026-05-28 20:05:59 +00:00
jmiller 7786dee9d4 chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-05-28 20:02:47 +00:00
Moko Consulting 7959864835 fix(workflows): rename remaining old secrets in repo-specific workflows [skip bump]
Universal: Auto Version Bump / Version Bump (push) Has been skipped
2026-05-28 14:47:27 -05:00
Moko Consulting 20fef17dac fix(workflows): GITHUB_TOKEN→GH_MIRROR_TOKEN (reserved name) [skip bump]
Universal: Auto Version Bump / Version Bump (push) Has been skipped
2026-05-28 14:36:39 -05:00
Moko Consulting 4630327b02 chore(workflows): sync universal workflows, rename secrets [skip bump]
Universal: Auto Version Bump / Version Bump (push) Has been skipped
2026-05-28 14:31:00 -05:00
Jonathan Miller 4624385501 feat(ci): update version branch on every stable release
Universal: Auto Version Bump / Version Bump (push) Successful in 6s
Add Step 12 to auto-release pipeline that recreates the version branch
from main after each stable release. Also mirrors the version branch
to GitHub alongside main.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 21:11:32 -05:00
Jonathan Miller 9721728b45 Merge remote-tracking branch 'origin/dev' into dev
Universal: Auto Version Bump / Version Bump (push) Successful in 6s
2026-05-26 21:07:04 -05:00
jmiller 38ed8eaeea Merge pull request #2 from mokoconsulting-tech/feat/appname-branding
feat(branding): replace hardcoded Gitea/MokoGitea with APP_NAME setting
2026-05-26 20:37:40 -05:00
Jonathan Miller 1608b5c4b9 feat(branding): replace hardcoded Gitea/MokoGitea with APP_NAME setting
Add runtime ${APP_NAME} placeholder substitution in locale strings so
all user-facing text reflects the configured APP_NAME from app.ini.
Replace 52 hardcoded locale strings, template literals, HTTP auth
realm headers, and Swagger API titles/descriptions with the
configurable value.

Closes #1

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 20:19:20 -05:00
jmiller 6654d7605d chore(ci): update pre-release.yml from moko-platform [skip ci] 2026-05-26 22:51:04 +00:00
jmiller a7e39fa992 chore(ci): update auto-bump.yml from moko-platform [skip ci] 2026-05-26 22:49:53 +00:00
jmiller 7c014dc4da chore(ci): update auto-release.yml from moko-platform [skip ci] 2026-05-26 22:48:40 +00:00
jmiller d0ca5eff28 chore(ci): update pre-release.yml from moko-platform [skip ci] 2026-05-26 22:37:09 +00:00
jmiller e946d49bf3 chore(ci): update auto-release.yml from moko-platform [skip ci] 2026-05-26 22:35:47 +00:00
Jonathan Miller c3eb57f124 Merge remote-tracking branch 'origin/main' into rc/05.06.00
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 22s
Universal: Build & Release / Promote Pre-Release to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 1m0s
# Conflicts:
#	.mokogitea/workflows/update-server.yml
2026-05-26 17:35:19 -05:00
jmiller c0662ab118 chore(ci): update auto-bump.yml from moko-platform [skip ci] 2026-05-26 22:25:31 +00:00
jmiller fce8389296 chore(ci): update auto-bump.yml from moko-platform [skip ci] 2026-05-26 22:25:24 +00:00
jmiller eeaf9f036b chore(ci): update auto-release.yml from moko-platform [skip ci] 2026-05-26 22:24:14 +00:00
jmiller 9957fe56ca chore(ci): update auto-release.yml from moko-platform [skip ci] 2026-05-26 22:24:08 +00:00
jmiller dcc4a49cdb chore(ci): update pre-release.yml from moko-platform [skip ci] 2026-05-26 22:13:35 +00:00
jmiller f15a0ed7e4 chore(ci): update pre-release.yml from moko-platform [skip ci] 2026-05-26 22:13:29 +00:00
jmiller 0e0ce94353 chore(ci): add auto-bump.yml from moko-platform [skip ci] 2026-05-26 22:12:24 +00:00
jmiller 455f12e21b chore(ci): add auto-bump.yml from moko-platform [skip ci] 2026-05-26 22:12:18 +00:00
jmiller 360d0b1b1f Merge pull request 'fix(security): backport upstream v1.26.2 security fixes' (#226) from fix/225-security-backports into dev
fix(security): backport upstream v1.26.2 security fixes (#226)
2026-05-26 22:05:08 +00:00
jmiller e8ce4ae60b Merge pull request 'fix(actions): retry workflow insertion on database deadlock' (#221) from fix/220-actions-deadlock-retry into dev
fix(actions): retry workflow insertion on database deadlock (#221)
2026-05-26 22:04:54 +00:00
Jonathan Miller 775766bc64 chore(deps): bump go-git/go-git/v5 to 5.19.0 (security)
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Failing after 20s
Addresses security fixes in the go-git library. Upstream backport of
go-gitea/gitea#37608.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 16:49:33 -05:00
Giteabot cc61032697 fix(git): Fix smart http request scope bug (#37583) (#37605)
Backport #37583 by @lunny

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Nicolas <bircni@icloud.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-05-26 16:39:17 -05:00
Nicolas 6d9f0d9727 Fix basic auth bug (#37503)
Backport for #37486
2026-05-26 16:37:50 -05:00
Giteabot 00d862f737 fix: make clone URL respect public URL detection setting (#37615) (#37617)
Backport #37615 by @wxiaoguang

Fix #37614

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-05-26 16:28:21 -05:00
Giteabot 233144e33e fix(auth): set User-Agent on avatar fetch and sync avatar on link-account register (#37564) (#37588) (#37726)
Backport #37588 by @pandareen

## Summary

Fixes
[go-gitea/gitea#37564](https://github.com/go-gitea/gitea/issues/37564):
when an OIDC provider returns a `picture` claim, Gitea is supposed to
download that image as the user's avatar (if `[oauth2_client]
UPDATE_AVATAR = true`). Two latent bugs prevented this from working
consistently:

1. **Default Go User-Agent rejected by some image hosts.**
`oauth2UpdateAvatarIfNeed` used `http.Get`, which sends `User-Agent:
Go-http-client/1.1`. Hosts like `upload.wikimedia.org` reject that UA
with `403`, and every error path silently returned, so the user was left
with an identicon and **no log line** to diagnose the issue.
2. **Link-account *register* path skipped avatar sync.** First-time OIDC
sign-ins where auto-registration is disabled (or required a
username/password retype) go through `LinkAccountPostRegister`, which
created the user but never called `oauth2SignInSync`. So the avatar /
full name / SSH keys from the IdP were dropped on the floor for those
users, even though the existing-account-link path (`oauth2LinkAccount`)
and the auto-register path (`handleOAuth2SignIn`) both already did the
sync.

## Changes

- `routers/web/auth/oauth.go` — `oauth2UpdateAvatarIfNeed` now uses
`http.NewRequest` + `http.DefaultClient.Do`, sets `User-Agent: Gitea
<version>`, and logs every failure path at `Warn` (invalid URL, fetch
error, non-200, body read error, oversize body, upload error). No silent
failures.
- `routers/web/auth/linkaccount.go` — `LinkAccountPostRegister` now
calls `oauth2SignInSync` after a successful user creation, mirroring the
auto-register and link-existing-account flows.
- `tests/integration/oauth_avatar_test.go` — new
`TestOAuth2AvatarFromPicture` integration test with five sub-cases:
- `AutoRegister_FetchesAvatarFromPictureWithGiteaUA` — happy path,
asserts `use_custom_avatar=true`, an avatar hash is set, exactly one
HTTP request was made, and the request carried a `Gitea ` UA. The mock
server enforces the UA prefix to mirror real-world hosts that reject
Go's default UA.
- `AutoRegister_NonOK_DoesNotUpdateAvatar` — server returns 403; user's
avatar must remain unset.
- `AutoRegister_EmptyPicture_NoFetch` — empty `picture` claim must not
trigger any HTTP request.
- `AutoRegister_UpdateAvatarFalse_NoFetch` — `UPDATE_AVATAR=false` must
not trigger any HTTP request.
- `LinkAccountRegister_FetchesAvatarFromPicture` — guards the
`linkaccount.go` fix; without the new `oauth2SignInSync` call this
assertion fails.

## Test plan

- [x] `go test -tags 'sqlite sqlite_unlock_notify' -run
'^TestOAuth2AvatarFromPicture$' ./tests/integration/ -v` — 5/5 sub-tests
pass.
- [x] Manual: log in as a Keycloak user with `picture` claim pointing at
`https://avatars.githubusercontent.com/u/9919?v=4` — Gitea avatar is
replaced with the GitHub picture.
- [x] Manual: same flow with `https://upload.wikimedia.org/...` —
request now succeeds (or returns a clearly logged `Warn` line if
rate-limited with `429`); previously it silently 403'd.
- [x] Manual: `UPDATE_AVATAR=false` — user keeps the identicon, no
outbound request in container logs.
- [ ] Reviewer: please double-check that no other call sites of
`oauth2UpdateAvatarIfNeed` rely on the old `http.Get` behaviour.

## Related

- Upstream issue: go-gitea/gitea#37564
--------------------------------------------


AI Editor was used in this PR

---------

Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: pandareen <7270563+pandareen@users.noreply.github.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Nicolas <bircni@icloud.com>
2026-05-26 16:28:21 -05:00
Zettat123 38beba655b fix(actions): wrong assumption that run id always >= job id (#37737) (#37742)
Backport #37737

Fix #37734

Follow up #37008

The `jobNum >= runNum` check is useless. Removed it to support `job_id <
run_id`
2026-05-26 16:28:20 -05:00
jmiller 3b79d57d45 chore: sync .mokogitea/workflows/update-server.yml from moko-platform [skip ci] 2026-05-26 20:12:56 +00:00
jmiller 871cb495b1 chore: sync .mokogitea/workflows/pre-release.yml from moko-platform [skip ci] 2026-05-26 20:11:02 +00:00
jmiller d66ca8db83 chore(ci): add update-server.yml universal workflow [skip ci] 2026-05-26 19:56:49 +00:00
jmiller 01f6722ccc chore(ci): add update-server.yml universal workflow [skip ci] 2026-05-26 19:56:42 +00:00
jmiller 7019f08f74 chore(ci): update pre-release.yml from moko-platform [skip ci] 2026-05-26 19:35:56 +00:00
jmiller aa6c3fc4ed chore(ci): update pre-release.yml from moko-platform [skip ci] 2026-05-26 19:35:48 +00:00
jmiller b577f8f4f4 chore(ci): update auto-release.yml from moko-platform [skip ci] 2026-05-26 19:35:42 +00:00
jmiller 5ffe68499d chore(ci): update auto-release.yml from moko-platform [skip ci] 2026-05-26 19:35:33 +00:00
jmiller 65456962b4 chore: add .mokogitea/manifest.xml
Authored-by: Moko Consulting
2026-05-26 19:19:06 +00:00
jmiller 020b160403 chore: add .mokogitea/workflows/update-server.yml from moko-platform [skip ci] 2026-05-26 19:04:20 +00:00
64 changed files with 2006 additions and 1302 deletions
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<moko-platform xmlns="https://standards.mokoconsulting.tech/moko-platform/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>01.00.00</version>
<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://git.mokoconsulting.tech/MokoConsulting/moko-platform</standards-source>
</governance>
<build>
<language>Go</language>
<package-type>application</package-type>
<entry-point>./</entry-point>
</build>
</moko-platform>
+66
View File
@@ -0,0 +1,66 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.mokogitea/workflows/auto-bump.yml
# VERSION: 09.02.00
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
name: "Universal: Auto Version Bump"
on:
push:
branches:
- dev
- rc
- 'feature/**'
- 'patch/**'
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
permissions:
contents: write
jobs:
bump:
name: Version Bump
runs-on: release
if: >-
!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[skip bump]') &&
!startsWith(github.event.head_commit.message, 'Merge pull request')
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1
- name: Setup moko-platform tools
run: |
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
if [ -d "/opt/moko-platform/cli" ]; then
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
else
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
fi
- name: Bump version
run: |
php ${MOKO_CLI}/version_auto_bump.php \
--path . --branch "${GITHUB_REF_NAME}" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
+137 -628
View File
@@ -26,13 +26,20 @@
name: "Universal: Build & Release"
on:
push:
pull_request:
types: [opened, closed]
branches:
- main
paths:
- 'src/**'
- 'htdocs/**'
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
@@ -44,28 +51,98 @@ permissions:
contents: write
jobs:
release:
name: Build & Release Pipeline
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────
promote-rc:
name: Promote to RC
runs-on: release
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
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.GA_TOKEN }}
fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN }}"}}'
run: |
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
# Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api
composer install --no-dev --no-interaction --quiet
- name: Rename branch to rc
run: |
php /tmp/moko-platform-api/cli/branch_rename.php \
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
--pr "${{ github.event.pull_request.number }}"
- name: Checkout rc and configure git
run: |
git fetch origin rc
git checkout rc
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- name: Publish RC release
run: |
php /tmp/moko-platform-api/cli/release_publish.php \
--path . --stability rc --bump minor --branch rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}"
- name: Summary
if: always()
run: |
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
echo "Branch renamed to rc, minor bump, RC + lesser stream releases built, updates.xml synced" >> $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: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
run: |
# Ensure PHP + Composer are available
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
# Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform-api
@@ -73,633 +150,58 @@ jobs:
composer install --no-dev --no-interaction --quiet
# -- PLATFORM DETECTION ---------------------------------------------------
- name: Detect platform
id: platform
- name: "Publish stable release"
run: |
php /tmp/moko-platform-api/cli/manifest_read.php --path . --github-output
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1 || true)
echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT"
echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT"
- name: "Step 1: Read version"
id: version
run: |
VERSION=$(php /tmp/moko-platform-api/cli/version_read.php --path .)
if [ -z "$VERSION" ]; then
echo "::error::No VERSION in README.md"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
MAJOR=$(echo "$VERSION" | cut -d. -f1)
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "release_tag=v${MAJOR}" >> "$GITHUB_OUTPUT"
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT"
- name: "Step 1b: Bump version"
id: bump
if: steps.version.outputs.skip != 'true'
run: |
MOKO_API="/tmp/moko-platform-api/cli"
BUMP=$(php ${MOKO_API}/version_bump.php --path . --minor)
VERSION=$(echo "$BUMP" | grep -oP '\d{2}\.\d{2}\.\d{2}$' || true)
[ -z "$VERSION" ] && VERSION=$(php ${MOKO_API}/version_read.php --path .)
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "Bumped to: ${VERSION}"
- name: Check if already released
if: steps.version.outputs.skip != 'true'
id: check
run: |
TAG="${{ steps.version.outputs.release_tag }}"
BRANCH="${{ steps.version.outputs.branch }}"
TAG_EXISTS=false
BRANCH_EXISTS=false
git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true
git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true
echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT"
echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT"
# Tag and branch may persist across patch releases — never skip
echo "already_released=false" >> "$GITHUB_OUTPUT"
# -- SANITY CHECKS -------------------------------------------------------
- name: "Sanity: Pre-release validation"
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
ERRORS=0
PLATFORM="${{ steps.platform.outputs.platform }}"
MANIFEST="${{ steps.platform.outputs.manifest }}"
MOD_FILE="${{ steps.platform.outputs.mod_file }}"
echo "## Pre-Release Sanity Checks (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# -- Version drift check (must pass before release) --------
README_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1)
if [ "$README_VER" != "$VERSION" ]; then
echo "- Version drift: README says \`${README_VER}\` but releasing \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "- Version consistent: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
fi
# Check CHANGELOG version matches
CL_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' CHANGELOG.md 2>/dev/null | head -1)
if [ -n "$CL_VER" ] && [ "$CL_VER" != "$VERSION" ]; then
echo "- CHANGELOG drift: \`${CL_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
fi
# Check composer.json version if present
if [ -f "composer.json" ]; then
COMP_VER=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' composer.json 2>/dev/null | head -1)
if [ -n "$COMP_VER" ] && [ "$COMP_VER" != "$VERSION" ]; then
echo "- composer.json drift: \`${COMP_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
fi
fi
# Common checks
if [ ! -f "LICENSE" ]; then
echo "- Missing LICENSE file" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "- LICENSE present" >> $GITHUB_STEP_SUMMARY
fi
if [ ! -d "src" ] && [ ! -d "htdocs" ]; then
echo "- Warning: No src/ or htdocs/ directory" >> $GITHUB_STEP_SUMMARY
else
echo "- Source directory present" >> $GITHUB_STEP_SUMMARY
fi
# -- Platform-specific checks --------
case "$PLATFORM" in
joomla)
if [ -n "$MANIFEST" ]; then
XML_VER=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
if [ -n "$XML_VER" ] && [ "$XML_VER" != "$VERSION" ]; then
echo "- Manifest drift: \`${XML_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "- Manifest version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
fi
TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null)
echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY
else
echo "- No Joomla XML manifest (WaaS site)" >> $GITHUB_STEP_SUMMARY
fi ;;
dolibarr)
if [ -n "$MOD_FILE" ]; then
MOD_VER=$(sed -n "s/.*\\\$this->version = '\([^']*\)'.*/\1/p" "$MOD_FILE" 2>/dev/null | head -1)
if [ -n "$MOD_VER" ] && [ "$MOD_VER" != "$VERSION" ]; then
echo "- Module drift: \`${MOD_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "- Module version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
fi
else
echo "- No mod*.class.php found" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
fi
if [ ! -f "update.txt" ]; then
echo "- Missing update.txt" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
fi ;;
*) echo "- Generic platform no manifest checks" >> $GITHUB_STEP_SUMMARY ;;
esac
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$ERRORS" -gt 0 ]; then
echo "**${ERRORS} error(s) — release may be incomplete**" >> $GITHUB_STEP_SUMMARY
else
echo "**All sanity checks passed**" >> $GITHUB_STEP_SUMMARY
fi
# -- STEP 2: Create or update version/XX.YY archive branch ---------------
# Always runs — every version change on main archives to version/XX.YY
- name: "Step 2: Version archive branch"
if: steps.check.outputs.already_released != 'true'
run: |
BRANCH="${{ steps.version.outputs.branch }}"
IS_MINOR="${{ steps.version.outputs.is_minor }}"
PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}')
# Check if branch exists
if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then
git push origin HEAD:"$BRANCH" --force
echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY
else
git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH"
git push origin "$BRANCH" --force
echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
fi
# -- STEP 3: Set platform version ----------------------------------------
- name: "Step 3: Set platform version"
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
php /tmp/moko-platform-api/cli/version_set_platform.php \
--path . --version "$VERSION" --branch main
# -- STEP 4: Update version badges ----------------------------------------
- name: "Step 4: Update version badges"
if: steps.version.outputs.skip != 'true'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
php /tmp/moko-platform-api/cli/badge_update.php --path . --version "${VERSION}" 2>/dev/null || true
- name: "Step 5: Write update stream"
if: >-
steps.version.outputs.skip != 'true' &&
steps.platform.outputs.platform == 'joomla'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
php /tmp/moko-platform-api/cli/updates_xml_build.php \
--path . --version "${VERSION}" --stability stable \
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
--github-output
- name: Commit release changes
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
run: |
if git diff --quiet && git diff --cached --quiet; then
echo "No changes to commit"
exit 0
fi
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
# Set push URL with token for branch-protected repos
git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git add -A
git commit -m "chore(release): build ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push -u origin HEAD
# -- STEP 6: Create tag ---------------------------------------------------
- name: "Step 6: Create git tag"
if: >-
steps.version.outputs.skip != 'true' &&
steps.check.outputs.tag_exists != 'true' &&
steps.version.outputs.is_minor == 'true'
run: |
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
# Only create the major release tag if it doesn't exist yet
if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then
git tag "$RELEASE_TAG"
git push origin "$RELEASE_TAG"
echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
else
echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY
fi
echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY
# -- STEP 7: Create or update Gitea Release --------------------------------
- name: "Step 7: Gitea Release"
if: >-
steps.version.outputs.skip != 'true'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
BRANCH="${{ steps.version.outputs.branch }}"
MAJOR="${{ steps.version.outputs.major }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Reuse metadata from Step 5 (single source of truth)
EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}"
EXT_NAME="${{ steps.updates.outputs.ext_name }}"
EXT_TYPE="${{ steps.updates.outputs.ext_type }}"
EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}"
# Fallbacks if Step 5 was skipped
if [ -z "$EXT_ELEMENT" ]; then
EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
fi
[ -z "$EXT_NAME" ] && EXT_NAME="${GITEA_REPO}"
NOTES=$(php /tmp/moko-platform-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null)
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
# Build release name: "Pretty Name VERSION (type_element-VERSION)"
TYPE_PREFIX=""
case "${EXT_TYPE}" in
plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;;
module) TYPE_PREFIX="mod_" ;;
component) TYPE_PREFIX="com_" ;;
template) TYPE_PREFIX="tpl_" ;;
library) TYPE_PREFIX="lib_" ;;
package) TYPE_PREFIX="pkg_" ;;
esac
RELEASE_NAME="${EXT_NAME} ${VERSION} (${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION})"
# Delete existing release if present (overwrite, not append)
EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true)
EXISTING_ID=$(echo "$EXISTING" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true)
if [ -n "$EXISTING_ID" ]; then
curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/releases/${EXISTING_ID}" 2>/dev/null || true
curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/tags/${RELEASE_TAG}" 2>/dev/null || true
echo "Deleted previous stable release (id: ${EXISTING_ID})"
fi
# Create fresh release
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
-H "Content-Type: application/json" \
"${API_BASE}/releases" \
-d "$(python3 -c "import json; print(json.dumps({
'tag_name': '${RELEASE_TAG}',
'name': '${RELEASE_NAME}',
'body': '''## ${VERSION} ($(date +%Y-%m-%d))\n${NOTES}''',
'target_commitish': '${BRANCH}'
}))")"
echo "Release created: ${RELEASE_NAME}" >> $GITHUB_STEP_SUMMARY
# -- STEP 8: Build Joomla install ZIP + SHA-256 checksum ------------------
- name: "Step 8: Build package and update checksum"
if: >-
steps.version.outputs.skip != 'true'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
REPO="${{ github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# All ZIPs upload to the major release tag (vXX)
RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true)
RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -z "$RELEASE_ID" ]; then
echo "No release ${RELEASE_TAG} found — skipping ZIP upload"
exit 0
fi
# Find extension element name from manifest
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)
[ -z "$MANIFEST" ] && exit 0
# Reuse element from Step 5, with same fallback chain
EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}"
if [ -z "$EXT_ELEMENT" ]; then
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(sed -n 's/.*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
fi
# ZIP name: type_folder_element-VERSION (e.g. plg_system_mokojgdpc-01.01.00.zip)
EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
EXT_FOLDER=$(sed -n 's/.*<extension[^>]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
TYPE_PREFIX=""
case "${EXT_TYPE}" in
plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;;
module) TYPE_PREFIX="mod_" ;;
component) TYPE_PREFIX="com_" ;;
template) TYPE_PREFIX="tpl_" ;;
library) TYPE_PREFIX="lib_" ;;
package) TYPE_PREFIX="pkg_" ;;
esac
ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip"
TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz"
# -- Build install packages from src/ ----------------------------
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/"; exit 0; }
# ZIP package (type-aware via moko-platform PHP API)
php /tmp/moko-platform-api/cli/joomla_build.php --path . --version "${VERSION}" --output /tmp
# Match the expected ZIP_NAME for upload
BUILT_ZIP=$(ls /tmp/${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip 2>/dev/null | head -1 || true)
if [ -n "$BUILT_ZIP" ] && [ "$BUILT_ZIP" != "/tmp/${ZIP_NAME}" ]; then
mv "$BUILT_ZIP" "/tmp/${ZIP_NAME}"
fi
# tar.gz package (flat source archive)
tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" --exclude='.ftpignore' --exclude='sftp-config*' --exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' .
ZIP_SIZE=$(stat -c%s "/tmp/${ZIP_NAME}" 2>/dev/null || stat -f%z "/tmp/${ZIP_NAME}" 2>/dev/null || echo "unknown")
TAR_SIZE=$(stat -c%s "/tmp/${TAR_NAME}" 2>/dev/null || stat -f%z "/tmp/${TAR_NAME}" 2>/dev/null || echo "unknown")
# -- Calculate SHA-256 for both ----------------------------------
SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1)
SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1)
# -- Delete existing assets with same name before uploading ------
ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]")
for ASSET_NAME in "$ZIP_NAME" "$TAR_NAME"; do
ASSET_ID=$(echo "$ASSETS" | python3 -c "
import sys,json
assets = json.load(sys.stdin)
for a in assets:
if a['name'] == '${ASSET_NAME}':
print(a['id']); break
" 2>/dev/null || true)
if [ -n "$ASSET_ID" ]; then
curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true
fi
done
# -- Upload both to release tag ----------------------------------
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
-H "Content-Type: application/octet-stream" \
--data-binary @"/tmp/${ZIP_NAME}" \
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" > /dev/null 2>&1 || true
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
-H "Content-Type: application/octet-stream" \
--data-binary @"/tmp/${TAR_NAME}" \
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${TAR_NAME}" > /dev/null 2>&1 || true
# -- Update updates.xml with both download formats ---------------
if [ -f "updates.xml" ]; then
ZIP_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}"
TAR_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${TAR_NAME}"
# Use Python to update only the stable entry's downloads + sha256
export PY_ZIP_URL="$ZIP_URL" PY_TAR_URL="$TAR_URL" PY_SHA="$SHA256_ZIP"
python3 << 'PYEOF'
import re, os
with open("updates.xml") as f:
content = f.read()
zip_url = os.environ["PY_ZIP_URL"]
tar_url = os.environ["PY_TAR_URL"]
sha = os.environ["PY_SHA"]
# Find the stable update block and replace its downloads + sha256
def replace_stable(m):
block = m.group(0)
# Replace downloads block
new_downloads = (
" <downloads>\n"
f" <downloadurl type=\"full\" format=\"zip\">{zip_url}</downloadurl>\n"
" </downloads>"
)
block = re.sub(r' <downloads>.*?</downloads>', new_downloads, block, flags=re.DOTALL)
# Add or replace sha256
if '<sha256>' in block:
block = re.sub(r' <sha256>.*?</sha256>', f' <sha256>{sha}</sha256>', block)
else:
block = block.replace('</downloads>', f'</downloads>\n <sha256>{sha}</sha256>')
return block
content = re.sub(
r' <update>.*?<tag>stable</tag>.*?</update>',
replace_stable,
content,
flags=re.DOTALL
)
with open("updates.xml", "w") as f:
f.write(content)
PYEOF
CURRENT_BRANCH="${{ github.ref_name }}"
git add updates.xml
git commit -m "chore(release): ZIP + tar.gz for ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" || true
git push || true
# Sync updates.xml to main via direct API (always runs — may be on version/XX branch)
GA_TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}"
FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \
"${API}/contents/updates.xml?ref=main" | jq -r '.sha // empty')
if [ -n "$FILE_SHA" ]; then
CONTENT=$(base64 -w0 updates.xml)
curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/contents/updates.xml" \
-d "$(jq -n \
--arg content "$CONTENT" \
--arg sha "$FILE_SHA" \
--arg msg "chore: sync updates.xml ${VERSION} [skip ci]" \
--arg branch "main" \
'{content: $content, sha: $sha, message: $msg, branch: $branch}'
)" > /dev/null 2>&1 \
&& echo "updates.xml synced to main via API" \
|| echo "WARNING: failed to sync updates.xml to main"
else
echo "WARNING: could not get updates.xml SHA from main"
fi
fi
echo "### Packages" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Package | Size | SHA-256 |" >> $GITHUB_STEP_SUMMARY
echo "|---------|------|---------|" >> $GITHUB_STEP_SUMMARY
echo "| \`${ZIP_NAME}\` | ${ZIP_SIZE} | \`${SHA256_ZIP}\` |" >> $GITHUB_STEP_SUMMARY
echo "| \`${TAR_NAME}\` | ${TAR_SIZE} | \`${SHA256_TAR}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Release | \`${RELEASE_TAG}\` | |" >> $GITHUB_STEP_SUMMARY
echo "| Download | [${ZIP_NAME}](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}) |" >> $GITHUB_STEP_SUMMARY
# -- STEP 8b: Update release description with changelog + SHA ----------------
- name: "Step 8b: Update release body with changelog and SHA"
if: steps.version.outputs.skip != 'true'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}"
EXT_TYPE="${{ steps.updates.outputs.ext_type }}"
EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}"
# Build TYPE_PREFIX to match Step 8's ZIP naming
TYPE_PREFIX=""
case "${EXT_TYPE}" in
plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;;
module) TYPE_PREFIX="mod_" ;;
component) TYPE_PREFIX="com_" ;;
template) TYPE_PREFIX="tpl_" ;;
library) TYPE_PREFIX="lib_" ;;
package) TYPE_PREFIX="pkg_" ;;
esac
ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip"
TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz"
# Get SHA from the built files
SHA256_ZIP=""
[ -f "/tmp/${ZIP_NAME}" ] && SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1)
SHA256_TAR=""
[ -f "/tmp/${TAR_NAME}" ] && SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1)
# Extract latest changelog entry (strip the ## header to avoid duplicate)
CHANGELOG=""
if [ -f "CHANGELOG.md" ]; then
CHANGELOG=$(sed -n "/^## \[*${VERSION}/,/^## \[*[0-9]/p" CHANGELOG.md | sed '$d' | sed '1d')
[ -z "$CHANGELOG" ] && CHANGELOG=$(sed -n '/^## /,/^## /p' CHANGELOG.md | sed '$d' | sed '1d' | head -30)
fi
# Build release body (single header, no duplicate from changelog)
BODY="## ${VERSION} ($(date +%Y-%m-%d))\n\n"
if [ -n "$CHANGELOG" ]; then
BODY="${BODY}${CHANGELOG}\n\n"
fi
BODY="${BODY}---\n\n### Checksums\n\n"
BODY="${BODY}| File | SHA-256 |\n|------|--------|\n"
[ -n "$SHA256_ZIP" ] && BODY="${BODY}| \`${ZIP_NAME}\` | \`${SHA256_ZIP}\` |\n"
[ -n "$SHA256_TAR" ] && BODY="${BODY}| \`${TAR_NAME}\` | \`${SHA256_TAR}\` |\n"
# Get release ID and update body
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null | \
python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then
python3 -c "
import json, urllib.request
body = '''$(printf '%b' "$BODY")'''
data = json.dumps({'body': body}).encode()
req = urllib.request.Request(
'${API_BASE}/releases/${RELEASE_ID}',
data=data,
headers={'Authorization': 'token ${{ secrets.GA_TOKEN }}', 'Content-Type': 'application/json'},
method='PATCH'
)
urllib.request.urlopen(req)
" 2>/dev/null && echo "Release body updated with changelog + SHA" >> $GITHUB_STEP_SUMMARY
fi
php /tmp/moko-platform-api/cli/release_publish.php \
--path . --stability stable --bump minor --branch main \
--token "${{ secrets.MOKOGITEA_TOKEN }}"
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
- name: "Step 9: Mirror release to GitHub"
if: >-
steps.version.outputs.skip != 'true' &&
steps.version.outputs.stability == 'stable' &&
secrets.GH_TOKEN != ''
secrets.GH_MIRROR_TOKEN != ''
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
MAJOR="${{ steps.version.outputs.major }}"
BRANCH="${{ steps.version.outputs.branch }}"
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
NOTES=$(php /tmp/moko-platform-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null || true)
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
echo "$NOTES" > /tmp/release_notes.md
EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".tag_name // empty" || true)
if [ -z "$EXISTING" ]; then
gh release create "$RELEASE_TAG" \
--repo "$GH_REPO" \
--title "v${MAJOR} (latest: ${VERSION})" \
--notes-file /tmp/release_notes.md \
--target "$BRANCH" || true
else
gh release edit "$RELEASE_TAG" \
--repo "$GH_REPO" \
--title "v${MAJOR} (latest: ${VERSION})" || true
fi
# Upload assets to GitHub mirror
for PKG in /tmp/${EXT_ELEMENT:-pkg}-${VERSION}.*; do
if [ -f "$PKG" ]; then
_RELID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".id // empty")
[ -n "$_RELID" ] && curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" -H "Content-Type: application/octet-stream" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/${_RELID}/assets?name=$(basename $PKG)" --data-binary "@$PKG" > /dev/null 2>&1 || true
fi
done
echo "GitHub mirror updated: ${GH_REPO} ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php /tmp/moko-platform-api/cli/release_mirror.php \
--version "$VERSION" --tag "$RELEASE_TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
--branch main 2>&1 || true
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
- name: "Step 10: Push main to GitHub mirror"
if: >-
steps.version.outputs.skip != 'true' &&
secrets.GH_TOKEN != ''
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_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
git remote set-url github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
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"
# -- Clean up lesser pre-releases (cascade) ---------------------------------
# stable → deletes all | rc → beta,alpha,dev | beta → alpha,dev | alpha → dev
- name: "Delete lesser pre-release channels"
continue-on-error: true
run: |
php /tmp/moko-platform-api/cli/release_cascade.php \
--stability stable \
--token "${{ secrets.GA_TOKEN }}" \
--org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
--gitea-url "${GITEA_URL}" 2>/dev/null || true
- name: "Step 11: Delete and recreate dev branch from main"
- 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.GA_TOKEN }}"
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}" \
@@ -711,30 +213,37 @@ jobs:
"${API_BASE}/branches" \
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
# -- Dolibarr post-release: Reset dev version -----------------------------
- name: "Dolibarr: Reset dev version"
if: >-
steps.version.outputs.skip != 'true' &&
steps.platform.outputs.platform == 'dolibarr' &&
steps.platform.outputs.mod_file != ''
- 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.GA_TOKEN }}"
MOD_FILE="${{ steps.platform.outputs.mod_file }}"
ENCODED_PATH=$(echo "$MOD_FILE" | sed 's|^\./||' | python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip()))")
FILE_RESP=$(curl -sf -H "Authorization: token ${TOKEN}" "${API_BASE}/contents/${ENCODED_PATH}?ref=dev" 2>/dev/null || true)
FILE_SHA=$(echo "$FILE_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
FILE_CONTENT=$(echo "$FILE_RESP" | python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin).get('content','')).decode())" 2>/dev/null || true)
if [ -n "$FILE_SHA" ] && [ -n "$FILE_CONTENT" ]; then
UPDATED=$(echo "$FILE_CONTENT" | sed "s/\$this->version = '[^']*'/\$this->version = 'development'/")
ENCODED=$(echo "$UPDATED" | base64 -w0)
curl -sf -X PUT -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/contents/${ENCODED_PATH}" \
-d "$(jq -n --arg content \"$ENCODED\" --arg sha \"$FILE_SHA\" --arg msg \"chore(version): reset dev version [skip ci]\" --arg branch \"dev\" '{content:$content,sha:$sha,message:$msg,branch:$branch}')" > /dev/null 2>&1 || true
fi
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
BRANCH_NAME="version/${VERSION}"
MAIN_SHA=$(git rev-parse HEAD)
# Delete old version branch if it exists (same version re-release)
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
# Create version/XX.YY.ZZ from main
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
# -- Dolibarr post-release: Reset dev version -----------------------------
- name: "Post-release: Reset dev version"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php /tmp/moko-platform-api/cli/version_reset_dev.php \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
--branch dev --path . 2>&1 || true
# -- Summary --------------------------------------------------------------
- name: Pipeline Summary
+19 -4
View File
@@ -5,6 +5,9 @@
name: Deploy MokoGitea
on:
push:
branches:
- main
workflow_dispatch:
inputs:
version:
@@ -36,11 +39,23 @@ jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout source (for version detection)
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Determine settings
id: config
run: |
VERSION="${{ github.event.inputs.version }}"
ENV="${{ github.event.inputs.environment }}"
# 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"
else
VERSION="${{ github.event.inputs.version }}"
ENV="${{ github.event.inputs.environment }}"
fi
if [ "$ENV" = "production" ]; then
echo "compose_dir=/opt/gitea" >> $GITHUB_OUTPUT
@@ -143,10 +158,10 @@ jobs:
- name: Update updates.xml
if: success()
env:
GITEA_TOKEN: ${{ secrets.GA_TOKEN }}
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
TAG: ${{ steps.config.outputs.tag }}
INSTANCE_URL: ${{ steps.config.outputs.instance_url }}
DEPLOY_ENV: ${{ github.event.inputs.environment }}
DEPLOY_ENV: ${{ github.event.inputs.environment || 'production' }}
run: |
# Only update updates.xml for production stable releases
if [ "$DEPLOY_ENV" != "production" ]; then
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
steps:
- name: Create branch and comment
run: |
TOKEN="${{ secrets.GA_TOKEN }}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
ISSUE_NUM="${{ github.event.issue.number }}"
ISSUE_TITLE="${{ github.event.issue.title }}"
+90 -90
View File
@@ -1,90 +1,90 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# Enforces branch merge policy:
# feature/* → dev only
# fix/* → dev only
# hotfix/* → dev or main (emergency)
# dev → main only
# alpha/* → dev only
# beta/* → dev only
# rc/* → main only
name: Branch Policy Check
on:
pull_request:
types: [opened, synchronize, reopened, edited]
jobs:
check-target:
name: Verify merge target
runs-on: ubuntu-latest
steps:
- name: Check branch policy
run: |
HEAD="${{ github.head_ref }}"
BASE="${{ github.base_ref }}"
echo "PR: ${HEAD} → ${BASE}"
ALLOWED=true
REASON=""
case "$HEAD" in
feature/*|feat/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Feature branches must target 'dev', not '${BASE}'"
fi
;;
fix/*|bugfix/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Fix branches must target 'dev', not '${BASE}'"
fi
;;
hotfix/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
fi
;;
alpha/*|beta/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Pre-release branches must target 'dev', not '${BASE}'"
fi
;;
rc/*)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Release candidate branches must target 'main', not '${BASE}'"
fi
;;
dev)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Dev branch can only merge into 'main', not '${BASE}'"
fi
;;
esac
if [ "$ALLOWED" = false ]; then
echo "::error::${REASON}"
echo ""
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "Branch policy: OK (${HEAD} → ${BASE})"
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# Enforces branch merge policy:
# feature/* → dev only
# fix/* → dev only
# hotfix/* → dev or main (emergency)
# dev → main only
# alpha/* → dev only
# beta/* → dev only
# rc/* → main only
name: Branch Policy Check
on:
pull_request:
types: [opened, synchronize, reopened, edited]
jobs:
check-target:
name: Verify merge target
runs-on: ubuntu-latest
steps:
- name: Check branch policy
run: |
HEAD="${{ github.head_ref }}"
BASE="${{ github.base_ref }}"
echo "PR: ${HEAD} → ${BASE}"
ALLOWED=true
REASON=""
case "$HEAD" in
feature/*|feat/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Feature branches must target 'dev', not '${BASE}'"
fi
;;
fix/*|bugfix/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Fix branches must target 'dev', not '${BASE}'"
fi
;;
hotfix/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
fi
;;
alpha/*|beta/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Pre-release branches must target 'dev', not '${BASE}'"
fi
;;
rc/*)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Release candidate branches must target 'main', not '${BASE}'"
fi
;;
dev)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Dev branch can only merge into 'main', not '${BASE}'"
fi
;;
esac
if [ "$ALLOWED" = false ]; then
echo "::error::${REASON}"
echo ""
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "Branch policy: OK (${HEAD} → ${BASE})"
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
+236
View File
@@ -0,0 +1,236 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.CI
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/universal/pr-check.yml.template
# VERSION: 05.00.00
# BRIEF: PR gate — branch policy + code validation before merge
name: "Universal: PR Check"
on:
pull_request:
types: [opened, synchronize, reopened, edited]
permissions:
contents: read
pull-requests: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
# ── Branch Policy ──────────────────────────────────────────────────────
branch-policy:
name: Branch Policy
runs-on: ubuntu-latest
steps:
- name: Check branch merge target
run: |
HEAD="${{ github.head_ref }}"
BASE="${{ github.base_ref }}"
echo "PR: ${HEAD} → ${BASE}"
ALLOWED=true
REASON=""
case "$HEAD" in
feature/*|feat/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Feature branches must target 'dev', not '${BASE}'"
fi
;;
fix/*|bugfix/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Fix branches must target 'dev', not '${BASE}'"
fi
;;
patch/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
ALLOWED=false
REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
fi
;;
hotfix/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
fi
;;
rc)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="RC branch can only merge into 'main', not '${BASE}'"
fi
;;
dev)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Dev branch can only merge into 'main', not '${BASE}'"
fi
;;
esac
if [ "$ALLOWED" = false ]; then
echo "::error::${REASON}"
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "Branch policy: OK (${HEAD} → ${BASE})"
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
# ── Code Validation ────────────────────────────────────────────────────
validate:
name: Validate PR
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Detect platform
id: platform
run: |
# Read platform from XML manifest (<platform> tag) or plain text fallback
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
[ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
[ -z "$PLATFORM" ] && PLATFORM="generic"
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
- name: Setup PHP
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
run: |
if ! command -v php &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
fi
- name: PHP syntax check
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
run: |
ERRORS=0
while IFS= read -r -d '' file; do
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
ERRORS=$((ERRORS + 1))
fi
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
echo "PHP lint: ${ERRORS} error(s)"
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
- name: Validate platform manifest
run: |
PLATFORM="${{ steps.platform.outputs.platform }}"
case "$PLATFORM" in
joomla)
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -z "$MANIFEST" ]; then
echo "::warning::No Joomla manifest found (WaaS site)"
exit 0
fi
echo "Manifest: ${MANIFEST}"
if command -v php &> /dev/null; then
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
fi
for ELEMENT in name version description; do
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
done
echo "Joomla manifest valid"
;;
dolibarr)
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
if [ -z "$MOD_FILE" ]; then
echo "::error::No mod*.class.php found"
exit 1
fi
echo "Dolibarr module: ${MOD_FILE}"
;;
*)
echo "Generic platform — no manifest validation"
;;
esac
- name: Check update stream format
run: |
PLATFORM="${{ steps.platform.outputs.platform }}"
case "$PLATFORM" in
joomla)
if [ -f "updates.xml" ]; then
if command -v php &> /dev/null; then
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
fi
echo "updates.xml valid"
fi
;;
dolibarr)
[ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
;;
esac
- name: Check changelog has unreleased entry
run: |
if [ ! -f "CHANGELOG.md" ]; then
echo "::warning::No CHANGELOG.md found"
exit 0
fi
# Check for content under [Unreleased] section
if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
echo "::error::CHANGELOG.md missing [Unreleased] section"
exit 1
fi
# Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
- name: Verify package source
run: |
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ ! -d "$SOURCE_DIR" ]; then
echo "::warning::No src/ or htdocs/ directory"
exit 0
fi
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
echo "Source: ${FILE_COUNT} files"
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
# ── Pre-Release RC Build ─────────────────────────────────────────────────
pre-release:
name: Build RC Package
runs-on: ubuntu-latest
needs: [branch-policy, validate]
steps:
- name: Trigger RC pre-release
env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
REPO: ${{ github.repository }}
BRANCH: ${{ github.head_ref }}
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: |
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
+2 -2
View File
@@ -108,7 +108,7 @@ jobs:
- name: Create RC release
if: steps.guard.outputs.skip != 'true'
env:
GITEA_TOKEN: ${{ secrets.GA_TOKEN }}
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
RC_TAG: ${{ steps.version.outputs.tag }}
RC_VERSION: ${{ steps.version.outputs.version }}
PR_TITLE: ${{ github.event.pull_request.title }}
@@ -155,7 +155,7 @@ jobs:
- name: Commit updates.xml
if: steps.guard.outputs.skip != 'true'
env:
GITEA_TOKEN: ${{ secrets.GA_TOKEN }}
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
HEAD_REF: ${{ github.event.pull_request.head.ref }}
PR_NUM: ${{ github.event.pull_request.number }}
run: |
+233 -375
View File
@@ -1,375 +1,233 @@
# 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:
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 }})"
runs-on: release
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GA_TOKEN }}
- name: Setup tools
run: |
# Update moko-platform CLI tools if available; install PHP if missing
if command -v moko-platform-update &> /dev/null; then
moko-platform-update
elif [ -d "/opt/moko-platform" ]; then
cd /opt/moko-platform && git pull origin main --quiet 2>/dev/null || true
else
if ! command -v php &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl >/dev/null 2>&1
fi
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
/tmp/moko-platform-api
fi
# Set MOKO_CLI to whichever path exists
if [ -d "/opt/moko-platform/cli" ]; then
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
else
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
fi
- name: Detect platform
id: platform
run: |
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1 | tr -d '[:space:]')
[ -z "$PLATFORM" ] && PLATFORM="generic"
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
MANIFEST=$(find ./src -maxdepth 1 -name "pkg_*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
[ -z "$MANIFEST" ] && MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "*/packages/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
[ -z "$MANIFEST" ] && MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT"
echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT"
- name: Resolve metadata and bump version
id: meta
run: |
STABILITY="${{ inputs.stability }}"
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
# Patch bump via CLI tool
php ${MOKO_CLI}/version_bump.php --path .
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
[ -z "$VERSION" ] && VERSION="00.00.01"
TODAY=$(date +%Y-%m-%d)
# Update platform-specific manifest
PLATFORM="${{ steps.platform.outputs.platform }}"
MANIFEST="${{ steps.platform.outputs.manifest }}"
MOD_FILE="${{ steps.platform.outputs.mod_file }}"
php ${MOKO_CLI}/version_set_platform.php \
--path . --version "$VERSION" --branch "${{ github.ref_name }}" 2>/dev/null || true
# 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://jmiller:${{ secrets.GA_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 (platform-aware)
EXT_ELEMENT=""
case "$PLATFORM" in
joomla)
if [ -n "$MANIFEST" ]; then
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
if [ -z "$EXT_ELEMENT" ]; then
EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
case "$EXT_ELEMENT" in
templatedetails|manifest) EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
esac
fi
else
EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
fi
;;
dolibarr)
if [ -n "$MOD_FILE" ]; then
MOD_BASENAME=$(basename "$MOD_FILE" .class.php)
EXT_ELEMENT=$(echo "$MOD_BASENAME" | sed 's/^mod//' | tr '[:upper:]' '[:lower:]')
else
EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
fi
;;
*)
EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
;;
esac
ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.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 "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT"
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
- name: Build package
run: |
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ ! -d "$SOURCE_DIR" ]; then
echo "::error::No src/ or htdocs/ directory"
exit 1
fi
MANIFEST="${{ steps.meta.outputs.manifest }}"
EXT_TYPE=""
if [ -n "$MANIFEST" ]; then
EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
fi
EXCLUDES="sftp-config* .ftpignore *.ppk *.pem *.key .env* *.local .build-trigger"
mkdir -p build/package
if [ "$EXT_TYPE" = "package" ] && [ -d "${SOURCE_DIR}/packages" ]; then
echo "=== Building Joomla PACKAGE (multi-extension) ==="
for ext_dir in "${SOURCE_DIR}"/packages/*/; do
[ ! -d "$ext_dir" ] && continue
EXT_NAME=$(basename "$ext_dir")
echo " Packaging sub-extension: ${EXT_NAME}"
cd "$ext_dir"
zip -r "../../build/package/${EXT_NAME}.zip" . -x $EXCLUDES
cd "$OLDPWD"
done
for f in "${SOURCE_DIR}"/*.xml "${SOURCE_DIR}"/*.php; do
[ -f "$f" ] && cp "$f" build/package/
done
else
echo "=== Building standard extension ==="
rsync -a \
--exclude='sftp-config*' \
--exclude='.ftpignore' \
--exclude='*.ppk' \
--exclude='*.pem' \
--exclude='*.key' \
--exclude='.env*' \
--exclude='*.local' \
--exclude='.build-trigger' \
"${SOURCE_DIR}/" build/package/
fi
- name: Create ZIP
id: zip
run: |
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
cd build/package
zip -r "../${ZIP_NAME}" .
cd ..
SHA256=$(sha256sum "${ZIP_NAME}" | cut -d' ' -f1)
echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT"
echo "ZIP: ${ZIP_NAME} (SHA: ${SHA256:0:16}...)"
- name: Create or replace Gitea release
id: release
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
SHA256="${{ steps.zip.outputs.sha256 }}"
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
EXT_ELEMENT="${{ steps.meta.outputs.ext_element }}"
TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
BRANCH=$(git branch --show-current)
BODY="## ${VERSION} ($(date +%Y-%m-%d))
**Channel:** ${STABILITY}
**SHA-256:** \`${SHA256}\`"
# Delete existing release
EXISTING_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \
"${API}/releases/tags/${TAG}" | jq -r '.id // empty' 2>/dev/null)
if [ -n "$EXISTING_ID" ]; then
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
"${API}/releases/${EXISTING_ID}" 2>/dev/null || true
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
"${API}/tags/${TAG}" 2>/dev/null || true
fi
# Create release
RELEASE_ID=$(curl -sS -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API}/releases" \
-d "$(jq -n \
--arg tag "$TAG" \
--arg target "$BRANCH" \
--arg name "${EXT_ELEMENT} ${VERSION} (${STABILITY})" \
--arg body "$BODY" \
'{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: true}'
)" | jq -r '.id')
echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT"
# Upload ZIP
curl -sS -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/octet-stream" \
"${API}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \
--data-binary "@build/${ZIP_NAME}"
echo "Released: ${EXT_ELEMENT} ${VERSION} (${STABILITY})"
- name: Update updates.xml
if: steps.platform.outputs.platform == 'joomla'
run: |
STABILITY="${{ steps.meta.outputs.stability }}"
VERSION="${{ steps.meta.outputs.version }}"
SHA256="${{ steps.zip.outputs.sha256 }}"
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
TAG="${{ steps.meta.outputs.tag }}"
if [ ! -f "updates.xml" ]; then
echo "No updates.xml -- skipping"
exit 0
fi
# Map stability to XML tag name
case "$STABILITY" in
development) XML_TAG="development" ;;
alpha) XML_TAG="alpha" ;;
beta) XML_TAG="beta" ;;
release-candidate) XML_TAG="rc" ;;
*) XML_TAG="$STABILITY" ;;
esac
DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${TAG}/${ZIP_NAME}"
# Use PHP to update the channel in updates.xml
php -r '
$xml_tag = $argv[1];
$version = $argv[2];
$sha256 = $argv[3];
$url = $argv[4];
$date = date("Y-m-d");
$content = file_get_contents("updates.xml");
$pattern = "/(<update>(?:(?!<\/update>).)*?<tag>" . preg_quote($xml_tag) . "<\/tag>.*?<\/update>)/s";
$content = preg_replace_callback($pattern, function($m) use ($version, $sha256, $url, $date) {
$block = $m[0];
$block = preg_replace("/<version>[^<]*<\/version>/", "<version>{$version}</version>", $block);
if (strpos($block, "<sha256>") !== false) {
$block = preg_replace("/<sha256>[^<]*<\/sha256>/", "<sha256>{$sha256}</sha256>", $block);
} else {
$block = str_replace("</downloads>", "</downloads>\n <sha256>{$sha256}</sha256>", $block);
}
$block = preg_replace("/(<downloadurl[^>]*>)[^<]*(<\/downloadurl>)/", "\${1}{$url}\${2}", $block);
return $block;
}, $content);
file_put_contents("updates.xml", $content);
echo "Updated {$xml_tag} channel: version={$version}\n";
' "$XML_TAG" "$VERSION" "$SHA256" "$DOWNLOAD_URL"
# Commit and push
if ! git diff --quiet updates.xml 2>/dev/null; then
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git add updates.xml
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
git push origin HEAD 2>&1 || echo "WARNING: push failed"
fi
- name: "Sync updates.xml to all branches"
if: steps.platform.outputs.platform == 'joomla'
run: |
CURRENT_BRANCH="${{ github.ref_name }}"
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
for BRANCH in main dev; do
[ "$BRANCH" = "$CURRENT_BRANCH" ] && continue
echo "Syncing updates.xml -> ${BRANCH}"
git fetch origin "${BRANCH}" 2>/dev/null || continue
git checkout "origin/${BRANCH}" -- . 2>/dev/null || continue
git checkout "${CURRENT_BRANCH}" -- updates.xml
if ! git diff --quiet updates.xml 2>/dev/null; then
git add updates.xml
git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]"
git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed"
fi
git checkout "${CURRENT_BRANCH}" 2>/dev/null
done
- 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.GA_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.zip.outputs.sha256 }}"
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: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
name: "Universal: Pre-Release"
on:
pull_request:
types: [closed]
branches:
- dev
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.pull_request.merged == true && github.event.pull_request.base.ref == 'dev')
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }}
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
# Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
- name: Detect platform
id: platform
run: |
php ${MOKO_CLI}/manifest_read.php --path . --github-output
- name: Resolve metadata and bump version
id: meta
run: |
STABILITY="${{ inputs.stability || 'development' }}"
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
# Read current version (bump already handled by push workflow)
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
[ -z "$VERSION" ] && VERSION="00.00.01"
# Strip any existing suffix from version before applying stability
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
# Verify version consistency across all files
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
# Update VERSION variable with suffix
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: 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
- name: Update updates.xml
if: steps.platform.outputs.platform == 'joomla'
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
SHA256="${{ steps.package.outputs.sha256_zip }}"
if [ ! -f "updates.xml" ]; then
echo "No updates.xml -- skipping"
exit 0
fi
SHA_FLAG=""
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
php ${MOKO_CLI}/updates_xml_build.php \
--path . --version "${VERSION}" --stability "${STABILITY}" \
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
${SHA_FLAG}
# Commit and push
if ! git diff --quiet updates.xml 2>/dev/null; then
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git add updates.xml
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
git push origin HEAD 2>&1 || echo "WARNING: push failed"
fi
- name: "Sync updates.xml to all branches"
if: steps.platform.outputs.platform == 'joomla'
run: |
CURRENT_BRANCH="${{ github.ref_name }}"
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
for BRANCH in main dev; do
[ "$BRANCH" = "$CURRENT_BRANCH" ] && continue
echo "Syncing updates.xml -> ${BRANCH}"
git fetch origin "${BRANCH}" 2>/dev/null || continue
git checkout "origin/${BRANCH}" -- updates.xml 2>/dev/null || continue
git checkout "${CURRENT_BRANCH}" -- updates.xml
if ! git diff --quiet updates.xml 2>/dev/null; then
git add updates.xml
git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]"
git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed"
fi
git checkout "${CURRENT_BRANCH}" 2>/dev/null
done
- 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
+312
View File
@@ -0,0 +1,312 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /templates/workflows/update-server.yml
# VERSION: 05.00.00
# BRIEF: Pre-release build + update server XML for dev/alpha/beta/rc branches
#
# Thin wrapper around moko-platform CLI tools.
# Builds packages, updates updates.xml, and optionally deploys via SFTP.
#
# Joomla filters update entries by the user's "Minimum Stability" setting.
name: "Update Server"
on:
push:
branches:
- 'dev'
- 'dev/**'
- 'alpha/**'
- 'beta/**'
- 'rc/**'
paths:
- 'src/**'
- 'htdocs/**'
pull_request:
types: [closed]
branches:
- 'dev'
- 'dev/**'
- 'alpha/**'
- 'beta/**'
- 'rc/**'
paths:
- 'src/**'
- 'htdocs/**'
workflow_dispatch:
inputs:
stability:
description: 'Stability tag'
required: true
default: 'development'
type: choice
options:
- development
- alpha
- beta
- rc
- stable
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:
update-xml:
name: Update Server
runs-on: release
if: >-
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 0
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.MOKOGITEA_TOKEN }}"}}}'
run: |
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
# Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform 2>/dev/null || true
if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then
cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi
echo "MOKO_CLI=/tmp/moko-platform/cli" >> "$GITHUB_ENV"
- name: Detect platform
id: platform
run: php ${MOKO_CLI}/manifest_read.php --path . --github-output
- name: Resolve stability and bump version
id: meta
run: |
BRANCH="${{ github.ref_name }}"
# Configure git for bot pushes
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"
# Auto-bump patch version
php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "0.0.0")
# Strip any existing suffix before applying stability
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
# Determine stability from branch or manual input
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
STABILITY="${{ inputs.stability }}"
elif [[ "$BRANCH" == rc/* ]]; then
STABILITY="rc"
elif [[ "$BRANCH" == beta/* ]]; then
STABILITY="beta"
elif [[ "$BRANCH" == alpha/* ]]; then
STABILITY="alpha"
else
STABILITY="development"
fi
# Version suffix per stability stream
case "$STABILITY" in
development) SUFFIX="-dev"; TAG="development" ;;
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
beta) SUFFIX="-beta"; TAG="beta" ;;
rc) SUFFIX="-rc"; TAG="release-candidate" ;;
*) SUFFIX=""; TAG="stable" ;;
esac
# Propagate version with stability suffix to all manifest files
php ${MOKO_CLI}/version_set_platform.php \
--path . --version "$VERSION" --branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
# Re-read version (now includes suffix from version_set_platform)
if [ -n "$SUFFIX" ]; then
VERSION="${VERSION}${SUFFIX}"
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "display_version=${VERSION}" >> "$GITHUB_OUTPUT"
# Commit version bump if changed
git add -A
git diff --cached --quiet || {
git commit -m "chore(version): auto-bump ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push
}
- name: Create release and upload package
id: package
run: |
VERSION="${{ steps.meta.outputs.version }}"
TAG="${{ steps.meta.outputs.tag }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Create or update Gitea release
php ${MOKO_CLI}/release_create.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
# Build package and upload
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
- name: Update updates.xml
if: steps.platform.outputs.platform == 'joomla'
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
SHA256="${{ steps.package.outputs.sha256_zip }}"
if [ ! -f "updates.xml" ]; then
echo "No updates.xml — skipping"
exit 0
fi
SHA_FLAG=""
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
php ${MOKO_CLI}/updates_xml_build.php \
--path . --version "${VERSION}" --stability "${STABILITY}" \
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
${SHA_FLAG}
# Commit and push updates.xml
git add updates.xml
git diff --cached --quiet || {
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
git push
}
- name: Sync updates.xml to main
if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla'
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
FILE_SHA=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
"${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
python3 -c "
import base64, json, urllib.request, sys
with open('updates.xml', 'rb') as f:
content = base64.b64encode(f.read()).decode()
payload = json.dumps({
'content': content,
'sha': '${FILE_SHA}',
'message': 'chore: sync updates.xml from ${{ steps.meta.outputs.stability }} [skip ci]',
'branch': 'main'
}).encode()
req = urllib.request.Request(
'${API_BASE}/contents/updates.xml',
data=payload, method='PUT',
headers={
'Authorization': 'token ${GITEA_TOKEN}',
'Content-Type': 'application/json'
})
try:
urllib.request.urlopen(req)
print('updates.xml synced to main')
except Exception as e:
print(f'WARNING: sync to main failed: {e}', file=sys.stderr)
"
fi
- name: SFTP deploy to dev server
if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
env:
DEV_HOST: ${{ vars.DEV_FTP_HOST }}
DEV_PATH: ${{ vars.DEV_FTP_PATH }}
DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
DEV_PORT: ${{ vars.DEV_FTP_PORT }}
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
run: |
# Permission check: admin or maintain role required
ACTOR="${{ github.actor }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
"${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
case "$PERMISSION" in
admin|maintain|write) ;;
*)
echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
exit 0
;;
esac
[ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && exit 0
PORT="${DEV_PORT:-22}"
REMOTE="${DEV_PATH%/}"
[ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
"$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
if [ -n "$DEV_KEY" ]; then
echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
else
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
fi
PLATFORM=$(php ${MOKO_CLI}/platform_detect.php --path . 2>/dev/null || true)
if [ "$PLATFORM" = "waas-component" ] && [ -f "${MOKO_CLI}/../deploy/deploy-joomla.php" ]; then
php ${MOKO_CLI}/../deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
elif [ -f "${MOKO_CLI}/../deploy/deploy-sftp.php" ]; then
php ${MOKO_CLI}/../deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
fi
rm -f /tmp/deploy_key /tmp/sftp-config.json
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
- name: Summary
if: always()
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
DISPLAY="${{ steps.meta.outputs.display_version }}"
echo "## Update Server" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${DISPLAY}\` |" >> $GITHUB_STEP_SUMMARY
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
steps:
- name: Sync upstream bugs
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GH_TOKEN: ${{ secrets.GH_MIRROR_TOKEN }}
MOKOGITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
MOKOGITEA_URL: https://git.mokoconsulting.tech
MOKOGITEA_REPO: MokoConsulting/MokoGitea
+17
View File
@@ -4,6 +4,23 @@ This changelog goes through the changes that have been made in each release
without substantial changes to our git log; to see the highlights of what has
been added to each release, please refer to the [blog](https://blog.gitea.com).
## [v1.26.1-moko.05.06.00] - 2026-05-30
* FEATURES
* feat(actions): rebrand actions bot user to mokogitea-actions (#233, #234)
* Name: gitea-actions → mokogitea-actions, FullName: MokoGitea Actions
* Email: mokogitea-actions[bot]@mokoconsulting.tech
* Backward-compatible: recognizes github-actions[bot], gitea-actions[bot], mokogitea-actions[bot]
* feat(actions): add actions bot user to branch protection whitelist (#233, #234)
* New toggles: WhitelistActionsUser, MergeWhitelistActionsUser, ForcePushAllowlistActionsUser
* Allows CI/CD workflows to push/merge/force-push to protected branches when enabled
* DB migration v334 adds the three boolean columns
* Exposed in API (create/edit branch protection) and web UI settings
* INFRASTRUCTURE
* fix(ci): auto-deploy to production on merge to main (#235)
* Deploy workflow now triggers on push to main, not just manual dispatch
* Version derived from git describe for auto-deploys
## [v1.26.1-moko.04.00.00] - 2026-05-24
* SECURITY
+9 -4
View File
@@ -34,16 +34,19 @@ const (
swaggerSpecPath = "templates/swagger/v1_json.tmpl"
openapi3OutPath = "templates/swagger/v1_openapi3_json.tmpl"
appSubUrlVar = "{{.SwaggerAppSubUrl}}"
appVerVar = "{{.SwaggerAppVer}}"
appSubUrlVar = "{{.SwaggerAppSubUrl}}"
appVerVar = "{{.SwaggerAppVer}}"
appNameVar = "{{.SwaggerAppName}}"
appSubUrlPlaceholder = "GITEA_APP_SUB_URL_PLACEHOLDER"
appVerPlaceholder = "0.0.0-gitea-placeholder"
appSubUrlPlaceholder = "GITEA_APP_SUB_URL_PLACEHOLDER"
appVerPlaceholder = "0.0.0-gitea-placeholder"
appNamePlaceholder = "GiteaAppNamePlaceholder"
)
var (
appSubUrlRe = regexp.MustCompile(regexp.QuoteMeta(appSubUrlVar))
appVerRe = regexp.MustCompile(regexp.QuoteMeta(appVerVar))
appNameRe = regexp.MustCompile(regexp.QuoteMeta(appNameVar))
enumScanDirs = []string{
"modules/structs",
@@ -70,6 +73,7 @@ func main() {
cleaned := appSubUrlRe.ReplaceAll(data, []byte(appSubUrlPlaceholder))
cleaned = appVerRe.ReplaceAll(cleaned, []byte(appVerPlaceholder))
cleaned = appNameRe.ReplaceAll(cleaned, []byte(appNamePlaceholder))
oas3, err := openapi3gen.Convert(cleaned, astEnumMap)
if err != nil {
@@ -87,6 +91,7 @@ func main() {
result := strings.ReplaceAll(string(out), appSubUrlPlaceholder, appSubUrlVar)
result = strings.ReplaceAll(result, appVerPlaceholder, appVerVar)
result = strings.ReplaceAll(result, appNamePlaceholder, appNameVar)
result = strings.TrimSpace(result)
if err := os.WriteFile(openapi3OutPath, []byte(result), 0o644); err != nil {
+7 -8
View File
@@ -1,6 +1,6 @@
module git.mokoconsulting.tech/MokoConsulting/MokoGitea
go 1.26.2
go 1.26.3
// rfc5280 said: "The serial number is an integer assigned by the CA to each certificate."
// But some CAs use negative serial number, just relax the check. related:
@@ -9,6 +9,7 @@ godebug x509negativeserial=1
require (
code.gitea.io/actions-proto-go v0.4.1
code.gitea.io/gitea v1.26.2
code.gitea.io/sdk/gitea v0.24.1
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570
connectrpc.com/connect v1.19.1
@@ -52,8 +53,8 @@ require (
github.com/go-chi/cors v1.2.2
github.com/go-co-op/gocron/v2 v2.19.1
github.com/go-enry/go-enry/v2 v2.9.5
github.com/go-git/go-billy/v5 v5.8.0
github.com/go-git/go-git/v5 v5.18.0
github.com/go-git/go-billy/v5 v5.9.0
github.com/go-git/go-git/v5 v5.19.0
github.com/go-ldap/ldap/v3 v3.4.13
github.com/go-redsync/redsync/v4 v4.16.0
github.com/go-sql-driver/mysql v1.9.3
@@ -242,17 +243,18 @@ require (
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/nwaples/rardecode/v2 v2.2.2 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/oasdiff/yaml v0.0.9 // indirect
github.com/oasdiff/yaml3 v0.0.12 // indirect
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/olekukonko/errors v1.2.0 // indirect
github.com/olekukonko/ll v0.1.8 // indirect
github.com/olekukonko/tablewriter v1.1.4 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/olivere/elastic/v7 v7.0.32 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pierrec/lz4/v4 v4.1.26 // indirect
github.com/pjbgf/sha1cd v0.5.0 // indirect
github.com/pjbgf/sha1cd v0.6.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
@@ -265,7 +267,6 @@ require (
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/skeema/knownhosts v1.3.2 // indirect
github.com/smartystreets/assertions v1.1.1 // indirect
github.com/sorairolake/lzip-go v0.3.8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
@@ -277,7 +278,6 @@ require (
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/zeebo/assert v1.3.0 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
go.etcd.io/bbolt v1.4.3 // indirect
go.uber.org/atomic v1.11.0 // indirect
@@ -287,7 +287,6 @@ require (
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.44.0 // indirect
+14 -38
View File
@@ -2,6 +2,8 @@ cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdB
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
code.gitea.io/actions-proto-go v0.4.1 h1:l0EYhjsgpUe/1VABo2eK7zcoNX2W44WOnb0MSLrKfls=
code.gitea.io/actions-proto-go v0.4.1/go.mod h1:mn7Wkqz6JbnTOHQpot3yDeHx+O5C9EGhMEE+htvHBas=
code.gitea.io/gitea v1.26.2 h1:i0oTSOGXnB3WLILa0lRzwi4KFIkKIEZnoyCtYiajtYY=
code.gitea.io/gitea v1.26.2/go.mod h1:K2pVuCKcxMzEl/KBD3b4GsWIOu6ZH74g8lJYiACcnsM=
code.gitea.io/gitea-vet v0.2.3 h1:gdFmm6WOTM65rE8FUBTRzeQZYzXePKSSB1+r574hWwI=
code.gitea.io/gitea-vet v0.2.3/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE=
code.gitea.io/sdk/gitea v0.24.1 h1:hpaqcdGcBmfMpV7JSbBJVwE99qo+WqGreJYKrDKEyW8=
@@ -269,6 +271,8 @@ github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY=
github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
@@ -300,12 +304,12 @@ github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e h1:oRq/fiirun5Hql
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0=
github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY=
github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.18.0 h1:O831KI+0PR51hM2kep6T8k+w0/LIAD490gvqMCvL5hM=
github.com/go-git/go-git/v5 v5.18.0/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo=
github.com/go-git/go-git/v5 v5.19.0 h1:+WkVUQZSy/F1Gb13udrMKjIM2PrzsNfDKFSfo5tkMtc=
github.com/go-git/go-git/v5 v5.19.0/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
@@ -326,7 +330,6 @@ github.com/go-redsync/redsync/v4 v4.16.0 h1:bNcOzeHH9d3s6pghU9NJFMPrQa41f5Nx3L4Y
github.com/go-redsync/redsync/v4 v4.16.0/go.mod h1:V4gagqgyASWBZuwx4xGzu72aZNb/6Mo05byUa3mVmKQ=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
@@ -354,12 +357,6 @@ github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8J
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
@@ -373,9 +370,6 @@ github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl76
github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs=
github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@@ -570,7 +564,6 @@ github.com/niklasfasching/go-org v1.9.1 h1:/3s4uTPOF06pImGa2Yvlp24yKXZoTYM+nsIlM
github.com/niklasfasching/go-org v1.9.1/go.mod h1:ZAGFFkWvUQcpazmi/8nHqwvARpr1xpb+Es67oUGX/48=
github.com/nwaples/rardecode/v2 v2.2.2 h1:/5oL8dzYivRM/tqX9VcTSWfbpwcbwKG1QtSJr3b3KcU=
github.com/nwaples/rardecode/v2 v2.2.2/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/oasdiff/yaml v0.0.9 h1:zQOvd2UKoozsSsAknnWoDJlSK4lC0mpmjfDsfqNwX48=
@@ -585,14 +578,13 @@ github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8=
github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw=
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
github.com/olivere/elastic/v7 v7.0.32 h1:R7CXvbu8Eq+WlsLgxmKVKPox0oOwAE/2T9Si5BnvK6E=
github.com/olivere/elastic/v7 v7.0.32/go.mod h1:c7PVmLe3Fxq77PIfY/bZmxY/TAamBhCzZ8xDOE09a9k=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -608,8 +600,8 @@ github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -693,7 +685,6 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -801,8 +792,8 @@ golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ss
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
golang.org/x/image v0.40.0 h1:Tw4GyDXMo+daZN1znreBRC3VayR1aLFUyUEOLUdW1a8=
golang.org/x/image v0.40.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
@@ -820,9 +811,7 @@ golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -841,7 +830,6 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -857,16 +845,12 @@ golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -915,7 +899,6 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200928182047-19e03678916f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
@@ -930,12 +913,6 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260401020348-3a24fdc17823 h1:
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401020348-3a24fdc17823/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -953,7 +930,6 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+18
View File
@@ -48,6 +48,9 @@ type ProtectedBranch struct {
ForcePushAllowlistUserIDs []int64 `xorm:"JSON TEXT"`
ForcePushAllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
ForcePushAllowlistDeployKeys bool `xorm:"NOT NULL DEFAULT false"`
WhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
MergeWhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
ForcePushAllowlistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"`
StatusCheckContexts []string `xorm:"JSON TEXT"`
EnableApprovalsWhitelist bool `xorm:"NOT NULL DEFAULT false"`
@@ -124,6 +127,11 @@ func (protectBranch *ProtectedBranch) CanUserPush(ctx context.Context, user *use
return false
}
// Allow the actions bot user if explicitly whitelisted.
if user.IsActions() && protectBranch.WhitelistActionsUser {
return true
}
if !protectBranch.EnableWhitelist {
if err := protectBranch.LoadRepo(ctx); err != nil {
log.Error("LoadRepo: %v", err)
@@ -161,6 +169,11 @@ func (protectBranch *ProtectedBranch) CanUserForcePush(ctx context.Context, user
return false
}
// Allow the actions bot user if explicitly whitelisted.
if user.IsActions() && protectBranch.ForcePushAllowlistActionsUser {
return protectBranch.CanUserPush(ctx, user)
}
if !protectBranch.EnableForcePushAllowlist {
return protectBranch.CanUserPush(ctx, user)
}
@@ -183,6 +196,11 @@ func (protectBranch *ProtectedBranch) CanUserForcePush(ctx context.Context, user
// IsUserMergeWhitelisted checks if some user is whitelisted to merge to this branch
func IsUserMergeWhitelisted(ctx context.Context, protectBranch *ProtectedBranch, userID int64, permissionInRepo access_model.Permission) bool {
// Allow the actions bot user if explicitly whitelisted.
if userID == user_model.ActionsUserID && protectBranch.MergeWhitelistActionsUser {
return true
}
if !protectBranch.EnableMergeWhitelist {
// Then we need to fall back on whether the user has write permission
return permissionInRepo.CanWrite(unit.TypeCode)
+1
View File
@@ -411,6 +411,7 @@ func prepareMigrationTasks() []*migration {
newMigration(331, "Add ActionRunAttempt model and related action fields", v1_27.AddActionRunAttemptModel),
newMigration(332, "Add org-level branch protection rulesets", v1_27.AddOrgProtectedBranchTable),
newMigration(333, "Add require_2fa to user table for org enforcement", v1_27.AddRequire2FAToUser),
newMigration(334, "Add actions user whitelist to protected branches", v1_27.AddActionsUserWhitelistToProtectedBranch),
}
return preparedMigrations
}
+17
View File
@@ -0,0 +1,17 @@
// Copyright 2026 The MokoGitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import "xorm.io/xorm"
// AddActionsUserWhitelistToProtectedBranch adds toggle fields that allow
// the built-in actions bot user to bypass branch protection rules.
func AddActionsUserWhitelistToProtectedBranch(x *xorm.Engine) error {
type ProtectedBranch struct {
WhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
MergeWhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
ForcePushAllowlistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
}
return x.Sync(new(ProtectedBranch))
}
+2 -2
View File
@@ -54,9 +54,9 @@ func GenerateRandomAvatar(ctx context.Context, u *User) error {
// AvatarLinkWithSize returns a link to the user's avatar with size. size <= 0 means default size
func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string {
// ghost user was deleted, Gitea actions is a bot user, 0 means the user should be a virtual user
// ghost user was deleted, actions bot is a system user, 0 means the user should be a virtual user
// which comes from git configure information
if u.IsGhost() || u.IsGiteaActions() || u.ID <= 0 {
if u.IsGhost() || u.IsActions() || u.ID <= 0 {
return avatars.DefaultAvatarLink()
}
+6 -4
View File
@@ -510,9 +510,9 @@ func (u *User) GitName() string {
}
// IsMailable checks if a user is eligible to receive emails.
// System users like Ghost and Gitea Actions are excluded.
// System users like Ghost and the actions bot are excluded.
func (u *User) IsMailable() bool {
return u.IsActive && !u.IsGiteaActions() && !u.IsGhost()
return u.IsActive && !u.IsActions() && !u.IsGhost()
}
// IsUserExist checks if given username exist,
@@ -627,8 +627,10 @@ var (
"swagger.v1.json",
"openapi3.v1.json",
"ghost", // reserved name for deleted users (id: -1)
"gitea-actions", // gitea builtin user (id: -2)
"ghost", // reserved name for deleted users (id: -1)
"mokogitea-actions", // actions bot user (id: -2)
"gitea-actions", // legacy actions bot name
"github-actions", // legacy actions bot name
}
// These names are reserved for user accounts: user's keys, user's rss feed, user's avatar, etc.
+24 -5
View File
@@ -34,8 +34,12 @@ func (u *User) IsGhost() bool {
const (
ActionsUserID int64 = -2
ActionsUserName = "gitea-actions"
ActionsUserEmail = "teabot@gitea.io"
ActionsUserName = "mokogitea-actions"
ActionsUserEmail = "mokogitea-actions[bot]@mokoconsulting.tech"
// Legacy names recognized as aliases for the actions bot user.
ActionsUserNameLegacyGitea = "gitea-actions"
ActionsUserNameLegacyGitHub = "github-actions"
)
// NewActionsUser creates and returns a fake user for running the actions.
@@ -45,7 +49,7 @@ func NewActionsUser() *User {
Name: ActionsUserName,
LowerName: ActionsUserName,
IsActive: true,
FullName: "Gitea Actions",
FullName: "MokoGitea Actions",
Email: ActionsUserEmail,
KeepEmailPrivate: true,
LoginName: ActionsUserName,
@@ -75,15 +79,30 @@ func GetActionsUserTaskID(u *User) (int64, bool) {
return 0, false
}
func (u *User) IsGiteaActions() bool {
// IsActions checks whether this user is the built-in actions bot.
func (u *User) IsActions() bool {
return u != nil && u.ID == ActionsUserID
}
// IsGiteaActions is a deprecated alias for IsActions.
func (u *User) IsGiteaActions() bool {
return u.IsActions()
}
// isActionsName returns true if the given name (case-insensitive, with
// optional "[bot]" suffix stripped) matches any known actions bot name.
func isActionsName(name string) bool {
clean := strings.TrimSuffix(name, "[bot]")
return strings.EqualFold(clean, ActionsUserName) ||
strings.EqualFold(clean, ActionsUserNameLegacyGitea) ||
strings.EqualFold(clean, ActionsUserNameLegacyGitHub)
}
func GetSystemUserByName(name string) *User {
if strings.EqualFold(name, GhostUserName) {
return NewGhostUser()
}
if strings.EqualFold(name, ActionsUserName) {
if isActionsName(name) {
return NewActionsUser()
}
return nil
+30 -4
View File
@@ -25,13 +25,39 @@ func TestSystemUser(t *testing.T) {
uid, u, err = GetPossibleUserByID(t.Context(), -2)
require.NoError(t, err)
assert.Equal(t, int64(-2), uid)
assert.Equal(t, "gitea-actions", u.Name)
assert.Equal(t, "gitea-actions", u.LowerName)
assert.True(t, u.IsGiteaActions())
assert.Equal(t, "mokogitea-actions", u.Name)
assert.Equal(t, "mokogitea-actions", u.LowerName)
assert.True(t, u.IsActions())
assert.True(t, u.IsGiteaActions()) // deprecated alias
// canonical name lookup
u = GetSystemUserByName("mokogitea-actions")
require.NotNil(t, u)
assert.Equal(t, "MokoGitea Actions", u.FullName)
// legacy name lookups
u = GetSystemUserByName("Gitea-actionS")
require.NotNil(t, u)
assert.Equal(t, "Gitea Actions", u.FullName)
assert.Equal(t, "MokoGitea Actions", u.FullName)
u = GetSystemUserByName("github-actions")
require.NotNil(t, u)
assert.Equal(t, "MokoGitea Actions", u.FullName)
// [bot] suffix lookups
u = GetSystemUserByName("mokogitea-actions[bot]")
require.NotNil(t, u)
assert.Equal(t, "MokoGitea Actions", u.FullName)
u = GetSystemUserByName("gitea-actions[bot]")
require.NotNil(t, u)
u = GetSystemUserByName("github-actions[bot]")
require.NotNil(t, u)
// unknown name returns nil
u = GetSystemUserByName("unknown-bot")
assert.Nil(t, u)
uid, u, err = GetPossibleUserByID(t.Context(), 999999)
require.NoError(t, err)
+21
View File
@@ -5,6 +5,7 @@ package log
import (
"context"
"net/url"
"reflect"
"runtime"
"strings"
@@ -226,6 +227,8 @@ func (l *LoggerImpl) Log(skip int, event *Event, format string, logArgs ...any)
}
} else if ls := asLogStringer(v); ls != nil {
msgArgs[i] = logStringFormatter{v: ls}
} else if str, ok := v.(string); ok {
msgArgs[i] = protectSensitiveInfo(str)
}
}
@@ -235,6 +238,24 @@ func (l *LoggerImpl) Log(skip int, event *Event, format string, logArgs ...any)
l.SendLogEvent(event)
}
func protectSensitiveInfo(s string) string {
u, err := url.Parse(s)
if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
return s
}
q := u.Query()
for _, vals := range q {
for i := range vals {
vals[i] = "_"
}
}
masked := &url.URL{Scheme: u.Scheme, Host: u.Host, Path: u.Path, RawQuery: q.Encode()}
if u.User != nil {
masked.User = url.User("_masked_")
}
return masked.String()
}
func (l *LoggerImpl) GetLevel() Level {
return Level(l.level.Load())
}
+7
View File
@@ -177,3 +177,10 @@ func TestLoggerExpressionFilter(t *testing.T) {
assert.Equal(t, []string{"foo\n", "foo bar\n", "by filename\n"}, w1.FetchLogs())
}
func TestProtectSensitiveInfo(t *testing.T) {
assert.Empty(t, protectSensitiveInfo(""))
assert.Equal(t, "mailto:user@example.com", protectSensitiveInfo("mailto:user@example.com"))
assert.Equal(t, "https://example.com", protectSensitiveInfo("https://example.com"))
assert.Equal(t, "https://_masked_@example.com/path?k=_", protectSensitiveInfo("https://u:p@example.com/path?k=v#hash"))
}
+1
View File
@@ -81,6 +81,7 @@ func initDefaultConfig() {
Instance: &InstanceStruct{
WebBanner: config.NewOption[WebBannerType]("instance.web_banner"),
MaintenanceMode: config.NewOption[MaintenanceModeType]("instance.maintenance_mode"),
LandingPage: config.NewOption[LandingPageType]("instance.landing_page").WithFileConfig(config.CfgSecKey{Sec: "server", Key: "LANDING_PAGE"}),
},
}
}
+28
View File
@@ -52,7 +52,35 @@ func (m MaintenanceModeType) IsActive() bool {
return true
}
// LandingPageType configures the default page for unauthenticated visitors.
// Mode values: "home", "explore", "organizations", "login", or "custom".
// When Mode is "custom", CustomPath holds the redirect target (e.g. "/MokoConsulting").
type LandingPageType struct {
Mode string // home, explore, organizations, login, custom
CustomPath string // only used when Mode == "custom"
}
// URL returns the redirect path for the configured landing page.
func (lp LandingPageType) URL() string {
switch lp.Mode {
case "explore":
return "/explore"
case "organizations":
return "/explore/organizations"
case "login":
return "/user/login"
case "custom":
if lp.CustomPath != "" {
return lp.CustomPath
}
return "/"
default:
return "/"
}
}
type InstanceStruct struct {
WebBanner *config.Option[WebBannerType]
MaintenanceMode *config.Option[MaintenanceModeType]
LandingPage *config.Option[LandingPageType]
}
+9
View File
@@ -42,14 +42,17 @@ type BranchProtection struct {
PushWhitelistUsernames []string `json:"push_whitelist_usernames"`
PushWhitelistTeams []string `json:"push_whitelist_teams"`
PushWhitelistDeployKeys bool `json:"push_whitelist_deploy_keys"`
PushWhitelistActionsUser bool `json:"push_whitelist_actions_user"`
EnableForcePush bool `json:"enable_force_push"`
EnableForcePushAllowlist bool `json:"enable_force_push_allowlist"`
ForcePushAllowlistUsernames []string `json:"force_push_allowlist_usernames"`
ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"`
ForcePushAllowlistDeployKeys bool `json:"force_push_allowlist_deploy_keys"`
ForcePushAllowlistActionsUser bool `json:"force_push_allowlist_actions_user"`
EnableMergeWhitelist bool `json:"enable_merge_whitelist"`
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"`
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
MergeWhitelistActionsUser bool `json:"merge_whitelist_actions_user"`
EnableStatusCheck bool `json:"enable_status_check"`
StatusCheckContexts []string `json:"status_check_contexts"`
RequiredApprovals int64 `json:"required_approvals"`
@@ -84,14 +87,17 @@ type CreateBranchProtectionOption struct {
PushWhitelistUsernames []string `json:"push_whitelist_usernames"`
PushWhitelistTeams []string `json:"push_whitelist_teams"`
PushWhitelistDeployKeys bool `json:"push_whitelist_deploy_keys"`
PushWhitelistActionsUser bool `json:"push_whitelist_actions_user"`
EnableForcePush bool `json:"enable_force_push"`
EnableForcePushAllowlist bool `json:"enable_force_push_allowlist"`
ForcePushAllowlistUsernames []string `json:"force_push_allowlist_usernames"`
ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"`
ForcePushAllowlistDeployKeys bool `json:"force_push_allowlist_deploy_keys"`
ForcePushAllowlistActionsUser bool `json:"force_push_allowlist_actions_user"`
EnableMergeWhitelist bool `json:"enable_merge_whitelist"`
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"`
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
MergeWhitelistActionsUser bool `json:"merge_whitelist_actions_user"`
EnableStatusCheck bool `json:"enable_status_check"`
StatusCheckContexts []string `json:"status_check_contexts"`
RequiredApprovals int64 `json:"required_approvals"`
@@ -117,14 +123,17 @@ type EditBranchProtectionOption struct {
PushWhitelistUsernames []string `json:"push_whitelist_usernames"`
PushWhitelistTeams []string `json:"push_whitelist_teams"`
PushWhitelistDeployKeys *bool `json:"push_whitelist_deploy_keys"`
PushWhitelistActionsUser *bool `json:"push_whitelist_actions_user"`
EnableForcePush *bool `json:"enable_force_push"`
EnableForcePushAllowlist *bool `json:"enable_force_push_allowlist"`
ForcePushAllowlistUsernames []string `json:"force_push_allowlist_usernames"`
ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"`
ForcePushAllowlistDeployKeys *bool `json:"force_push_allowlist_deploy_keys"`
ForcePushAllowlistActionsUser *bool `json:"force_push_allowlist_actions_user"`
EnableMergeWhitelist *bool `json:"enable_merge_whitelist"`
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"`
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
MergeWhitelistActionsUser *bool `json:"merge_whitelist_actions_user"`
EnableStatusCheck *bool `json:"enable_status_check"`
StatusCheckContexts []string `json:"status_check_contexts"`
RequiredApprovals *int64 `json:"required_approvals"`
+17
View File
@@ -8,6 +8,8 @@ import (
"strings"
"testing"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
"github.com/stretchr/testify/assert"
)
@@ -66,6 +68,21 @@ func TestLocaleStore(t *testing.T) {
assert.Equal(t, "&lt;no-such&gt;", string(res))
}
func TestLocaleAppNameSubstitution(t *testing.T) {
setting.AppName = "TestApp"
ls := NewLocaleStore()
assert.NoError(t, ls.AddLocaleByJSON("lang1", "Lang1", []byte(`{"greeting":"Welcome to ${APP_NAME}","plain":"No placeholder here"}`), nil))
lang1, _ := ls.Locale("lang1")
assert.Equal(t, "Welcome to TestApp", lang1.TrString("greeting"))
assert.Equal(t, "No placeholder here", lang1.TrString("plain"))
// Verify it responds to runtime AppName changes
setting.AppName = "ChangedApp"
assert.Equal(t, "Welcome to ChangedApp", lang1.TrString("greeting"))
}
func TestLocaleStoreMoreSource(t *testing.T) {
testData1 := []byte(`
{
+5
View File
@@ -9,9 +9,11 @@ import (
"html"
"html/template"
"slices"
"strings"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/json"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
)
// This file implements the static LocaleStore that will not watch for changes
@@ -142,6 +144,9 @@ func (l *locale) TrString(trKey string, trArgs ...any) string {
if format == "" { // still missing, use the key itself
format = html.EscapeString(trKey)
}
if strings.Contains(format, "${APP_NAME}") {
format = strings.ReplaceAll(format, "${APP_NAME}", setting.AppName)
}
msg, err := Format(format, trArgs...)
if err != nil {
log.Error("Error whilst formatting %q in %s: %v", trKey, l.langName, err)
+63 -52
View File
@@ -25,7 +25,7 @@
"enable_javascript": "This website requires JavaScript.",
"toc": "Table of Contents",
"licenses": "Licenses",
"return_to_gitea": "Return to MokoGitea",
"return_to_gitea": "Return to ${APP_NAME}",
"more_items": "More items",
"username": "Username",
"email": "Email Address",
@@ -222,7 +222,7 @@
"filter.string.asc": "AZ",
"filter.string.desc": "ZA",
"error.occurred": "An error occurred",
"error.report_message": "If you believe that this is a MokoGitea bug, please search for issues on <a href=\"%s\" target=\"_blank\">GitHub</a> or open a new issue if necessary.",
"error.report_message": "If you believe that this is a ${APP_NAME} bug, please search for issues on <a href=\"%s\" target=\"_blank\">GitHub</a> or open a new issue if necessary.",
"error.not_found": "The target couldn't be found.",
"error.permission_denied": "Permission denied.",
"error.network_error": "Network error",
@@ -230,16 +230,16 @@
"startpage.install": "Easy to install",
"startpage.install_desc": "Simply <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%[1]s\">run the binary</a> for your platform, ship it with <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%[2]s\">Docker</a>, or get it <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%[3]s\">packaged</a>.",
"startpage.platform": "Cross-platform",
"startpage.platform_desc": "MokoGitea runs anywhere <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">Go</a> can compile for: Windows, macOS, Linux, ARM, etc. Choose the one you love!",
"startpage.platform_desc": "${APP_NAME} runs anywhere <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">Go</a> can compile for: Windows, macOS, Linux, ARM, etc. Choose the one you love!",
"startpage.lightweight": "Lightweight",
"startpage.lightweight_desc": "MokoGitea has low minimal requirements and can run on an inexpensive Raspberry Pi. Save your machine energy!",
"startpage.lightweight_desc": "${APP_NAME} has low minimal requirements and can run on an inexpensive Raspberry Pi. Save your machine energy!",
"startpage.license": "Open Source",
"startpage.license_desc": "Go get <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%[1]s\">%[2]s</a>! Join us by <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%[3]s\">contributing</a> to make this project even better. Don't hesitate to contribute!",
"install.install": "Installation",
"install.installing_desc": "Installing now, please wait…",
"install.title": "Initial Configuration",
"install.docker_helper": "If you run MokoGitea inside Docker, please read the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">documentation</a> before changing any settings.",
"install.require_db_desc": "MokoGitea requires MySQL, PostgreSQL, MSSQL, SQLite3 or TiDB (MySQL protocol).",
"install.docker_helper": "If you run ${APP_NAME} inside Docker, please read the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">documentation</a> before changing any settings.",
"install.require_db_desc": "${APP_NAME} requires MySQL, PostgreSQL, MSSQL, SQLite3 or TiDB (MySQL protocol).",
"install.db_title": "Database Settings",
"install.db_type": "Database Type",
"install.host": "Host",
@@ -250,12 +250,12 @@
"install.db_schema_helper": "Leave blank for database default (\"public\").",
"install.ssl_mode": "SSL",
"install.path": "Path",
"install.sqlite_helper": "File path for the SQLite3 database.<br>Enter an absolute path if you run MokoGitea as a service.",
"install.reinstall_error": "You are trying to install into an existing MokoGitea database",
"install.reinstall_confirm_message": "Re-installing with an existing MokoGitea database can cause multiple problems. In most cases, you should use your existing \"app.ini\" to run MokoGitea. If you know what you are doing, confirm the following:",
"install.sqlite_helper": "File path for the SQLite3 database.<br>Enter an absolute path if you run ${APP_NAME} as a service.",
"install.reinstall_error": "You are trying to install into an existing ${APP_NAME} database",
"install.reinstall_confirm_message": "Re-installing with an existing ${APP_NAME} database can cause multiple problems. In most cases, you should use your existing \"app.ini\" to run ${APP_NAME}. If you know what you are doing, confirm the following:",
"install.reinstall_confirm_check_1": "The data encrypted by the SECRET_KEY in app.ini may be lost: users may not be able to log in with 2FA/OTP and mirrors may not function correctly. By checking this box, you confirm that the current app.ini file contains the correct SECRET_KEY.",
"install.reinstall_confirm_check_2": "The repositories and settings may need to be resynchronized. By checking this box, you confirm that you will resynchronize the hooks for the repositories and authorized_keys file manually. You confirm that you will ensure that repository and mirror settings are correct.",
"install.reinstall_confirm_check_3": "You confirm that you are absolutely sure that this MokoGitea is running with the correct app.ini location and that you are sure that you have to re-install. You confirm that you acknowledge the above risks.",
"install.reinstall_confirm_check_3": "You confirm that you are absolutely sure that this ${APP_NAME} is running with the correct app.ini location and that you are sure that you have to re-install. You confirm that you acknowledge the above risks.",
"install.err_empty_db_path": "The SQLite3 database path cannot be empty.",
"install.no_admin_and_disable_registration": "You cannot disable user self-registration without creating an administrator account.",
"install.err_empty_admin_password": "The administrator password cannot be empty.",
@@ -271,14 +271,14 @@
"install.lfs_path": "Git LFS Root Path",
"install.lfs_path_helper": "Files tracked by Git LFS will be stored in this directory. Leave empty to disable.",
"install.run_user": "Run As Username",
"install.run_user_helper": "The operating system username that MokoGitea runs as, it must have write access to the data paths. This value is auto-detected and cannot be changed here. To use a different user, restart MokoGitea under that account.",
"install.run_user_helper": "The operating system username that ${APP_NAME} runs as, it must have write access to the data paths. This value is auto-detected and cannot be changed here. To use a different user, restart ${APP_NAME} under that account.",
"install.domain": "Server Domain",
"install.domain_helper": "Domain or host address for the server.",
"install.ssh_port": "SSH Server Port",
"install.ssh_port_helper": "Port number your SSH server listens on. Leave empty to disable.",
"install.http_port": "MokoGitea HTTP Listen Port",
"install.http_port_helper": "Port number the MokoGitea web server will listen on.",
"install.app_url": "MokoGitea Base URL",
"install.http_port": "${APP_NAME} HTTP Listen Port",
"install.http_port_helper": "Port number the ${APP_NAME} web server will listen on.",
"install.app_url": "${APP_NAME} Base URL",
"install.app_url_helper": "Base address for HTTP(S) clone URLs and email notifications.",
"install.log_root_path": "Log Path",
"install.log_root_path_helper": "Log files will be written to this directory.",
@@ -288,7 +288,7 @@
"install.smtp_port": "SMTP Port",
"install.smtp_from": "Send Email As",
"install.smtp_from_invalid": "The \"Send Email As\" address is invalid",
"install.smtp_from_helper": "Email address MokoGitea will use. Enter a plain email address or use the \"Name\" <email@example.com> format.",
"install.smtp_from_helper": "Email address ${APP_NAME} will use. Enter a plain email address or use the \"Name\" <email@example.com> format.",
"install.mailer_user": "SMTP Username",
"install.mailer_password": "SMTP Password",
"install.register_confirm": "Require Email Confirmation to Register",
@@ -311,7 +311,7 @@
"install.admin_password": "Password",
"install.confirm_password": "Confirm Password",
"install.admin_email": "Email Address",
"install.install_btn_confirm": "Install MokoGitea",
"install.install_btn_confirm": "Install ${APP_NAME}",
"install.test_git_failed": "Could not test 'git' command: %v",
"install.invalid_db_setting": "The database settings are invalid: %v",
"install.invalid_db_table": "The database table \"%s\" is invalid: %v",
@@ -385,7 +385,7 @@
"auth.forgot_password_title": "Forgot Password",
"auth.forgot_password": "Forgot password?",
"auth.need_account": "Need an account?",
"auth.sign_up_tip": "You are registering the first account in the system, which has administrator privileges. Please carefully remember your username and password. If you forget the username or password, please refer to the MokoGitea documentation to recover the account.",
"auth.sign_up_tip": "You are registering the first account in the system, which has administrator privileges. Please carefully remember your username and password. If you forget the username or password, please refer to the ${APP_NAME} documentation to recover the account.",
"auth.sign_up_now": "Register now.",
"auth.sign_up_successful": "Account was successfully created. Welcome!",
"auth.confirmation_mail_sent_prompt_ex": "A new confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the registration process. If your registration email address is incorrect, you can sign in again and change it.",
@@ -409,7 +409,7 @@
"auth.reset_password_helper": "Recover Account",
"auth.reset_password_wrong_user": "You are signed in as %s, but the account recovery link is meant for %s",
"auth.password_too_short": "Password length cannot be less than %d characters.",
"auth.non_local_account": "Non-local users cannot update their password through the MokoGitea web interface.",
"auth.non_local_account": "Non-local users cannot update their password through the ${APP_NAME} web interface.",
"auth.verify": "Verify",
"auth.scratch_code": "Scratch code",
"auth.use_scratch_code": "Use a scratch code",
@@ -726,7 +726,7 @@
"settings.retype_new_password": "Confirm New Password",
"settings.password_incorrect": "The current password is incorrect.",
"settings.change_password_success": "Your password has been updated. Sign in using your new password from now on.",
"settings.password_change_disabled": "Non-local users cannot update their password through the MokoGitea web interface.",
"settings.password_change_disabled": "Non-local users cannot update their password through the ${APP_NAME} web interface.",
"settings.emails": "Email Addresses",
"settings.manage_emails": "Manage Email Addresses",
"settings.manage_themes": "Select default theme",
@@ -734,7 +734,7 @@
"settings.email_desc": "Your primary email address will be used for notifications, password recovery and, provided that it is not hidden, web-based Git operations.",
"settings.theme_desc": "This will be your default theme across the site.",
"settings.theme_colorblindness_help": "Color blindness Theme Support",
"settings.theme_colorblindness_prompt": "MokoGitea only has a few themes with basic color blindness support, which only have a few colors defined. The work is still in progress. More improvements could be made by defining more colors in the theme CSS files.",
"settings.theme_colorblindness_prompt": "${APP_NAME} only has a few themes with basic color blindness support, which only have a few colors defined. The work is still in progress. More improvements could be made by defining more colors in the theme CSS files.",
"settings.primary": "Primary",
"settings.activated": "Activated",
"settings.requires_activation": "Requires activation",
@@ -843,7 +843,7 @@
"settings.unbind_success": "The social account has been removed successfully.",
"settings.manage_access_token": "Manage Access Tokens",
"settings.generate_new_token": "Generate New Token",
"settings.tokens_desc": "These tokens grant access to your account using the Gitea API.",
"settings.tokens_desc": "These tokens grant access to your account using the ${APP_NAME} API.",
"settings.token_name": "Token Name",
"settings.generate_token": "Generate Token",
"settings.generate_token_success": "Your new token has been generated. Copy it now as it will not be shown again.",
@@ -869,7 +869,7 @@
"settings.permissions_list": "Permissions:",
"settings.manage_oauth2_applications": "Manage OAuth2 Applications",
"settings.edit_oauth2_application": "Edit OAuth2 Application",
"settings.oauth2_applications_desc": "OAuth2 applications enable your third-party application to securely authenticate users at this MokoGitea instance.",
"settings.oauth2_applications_desc": "OAuth2 applications enable your third-party application to securely authenticate users at this ${APP_NAME} instance.",
"settings.remove_oauth2_application": "Remove OAuth2 Application",
"settings.remove_oauth2_application_desc": "Removing an OAuth2 application will revoke access to all signed access tokens. Continue?",
"settings.remove_oauth2_application_success": "The application has been deleted.",
@@ -890,9 +890,9 @@
"settings.oauth2_application_edit": "Edit",
"settings.oauth2_application_create_description": "OAuth2 applications give your third-party application access to user accounts on this instance.",
"settings.oauth2_application_remove_description": "Removing an OAuth2 application will prevent it from accessing authorized user accounts on this instance. Continue?",
"settings.oauth2_application_locked": "MokoGitea pre-registers some OAuth2 applications on startup if enabled in config. To prevent unexpected behavior, these can neither be edited nor removed. Please refer to the OAuth2 documentation for more information.",
"settings.oauth2_application_locked": "${APP_NAME} pre-registers some OAuth2 applications on startup if enabled in config. To prevent unexpected behavior, these can neither be edited nor removed. Please refer to the OAuth2 documentation for more information.",
"settings.authorized_oauth2_applications": "Authorized OAuth2 Applications",
"settings.authorized_oauth2_applications_description": "You have granted access to your personal MokoGitea account to these third-party applications. Please revoke access for applications you no longer need.",
"settings.authorized_oauth2_applications_description": "You have granted access to your personal ${APP_NAME} account to these third-party applications. Please revoke access for applications you no longer need.",
"settings.revoke_key": "Revoke",
"settings.revoke_oauth2_grant": "Revoke Access",
"settings.revoke_oauth2_grant_description": "Revoking access for this third-party application will prevent this application from accessing your data. Are you sure?",
@@ -923,11 +923,11 @@
"settings.webauthn_key_loss_warning": "If you lose your security keys, you will lose access to your account.",
"settings.webauthn_alternative_tip": "You may want to configure an additional authentication method.",
"settings.manage_account_links": "Manage Linked Accounts",
"settings.manage_account_links_desc": "These external accounts are linked to your MokoGitea account.",
"settings.account_links_not_available": "No external accounts are currently linked to your MokoGitea account.",
"settings.manage_account_links_desc": "These external accounts are linked to your ${APP_NAME} account.",
"settings.account_links_not_available": "No external accounts are currently linked to your ${APP_NAME} account.",
"settings.link_account": "Link Account",
"settings.remove_account_link": "Remove Linked Account",
"settings.remove_account_link_desc": "Removing a linked account will revoke its access to your MokoGitea account. Continue?",
"settings.remove_account_link_desc": "Removing a linked account will revoke its access to your ${APP_NAME} account. Continue?",
"settings.remove_account_link_success": "The linked account has been removed.",
"settings.hooks.desc": "Add webhooks which will be triggered for <strong>all repositories</strong> that you own.",
"settings.orgs_none": "You are not a member of any organizations.",
@@ -943,7 +943,7 @@
"settings.email_notifications.disable": "Disable Email Notifications",
"settings.email_notifications.submit": "Set Email Preference",
"settings.email_notifications.andyourown": "And Your Own Notifications",
"settings.email_notifications.actions.desc": "Notifications for workflow runs on repositories set up with <a target=\"_blank\" href=\"%s\">Gitea Actions</a>.",
"settings.email_notifications.actions.desc": "Notifications for workflow runs on repositories set up with <a target=\"_blank\" href=\"%s\">${APP_NAME} Actions</a>.",
"settings.email_notifications.actions.failure_only": "Only notify for failed workflow runs",
"settings.visibility": "User visibility",
"settings.visibility.public": "Public",
@@ -1125,7 +1125,7 @@
"repo.migrate.github.description": "Migrate data from github.com or other GitHub instances.",
"repo.migrate.git.description": "Migrate a repository only from any Git service.",
"repo.migrate.gitlab.description": "Migrate data from gitlab.com or other GitLab instances.",
"repo.migrate.gitea.description": "Migrate data from gitea.com or other Gitea instances.",
"repo.migrate.gitea.description": "Migrate data from other ${APP_NAME} instances.",
"repo.migrate.gogs.description": "Migrate data from notabug.org or other Gogs instances.",
"repo.migrate.onedev.description": "Migrate data from code.onedev.io or other OneDev instances.",
"repo.migrate.codebase.description": "Migrate data from codebasehq.com.",
@@ -1891,7 +1891,7 @@
"repo.pulls.cmd_instruction_checkout_title": "Checkout",
"repo.pulls.cmd_instruction_checkout_desc": "From your project repository, check out a new branch and test the changes.",
"repo.pulls.cmd_instruction_merge_title": "Merge",
"repo.pulls.cmd_instruction_merge_desc": "Merge the changes and update on MokoGitea.",
"repo.pulls.cmd_instruction_merge_desc": "Merge the changes and update on ${APP_NAME}.",
"repo.pulls.cmd_instruction_merge_warning": "Warning: This operation cannot merge pull request because \"autodetect manual merge\" is not enabled.",
"repo.pulls.clear_merge_message": "Clear merge message",
"repo.pulls.clear_merge_message_hint": "Clearing the merge message will only remove the commit message content and keep generated git trailers such as \"Co-Authored-By…\".",
@@ -2199,11 +2199,11 @@
"repo.settings.trust_model.collaborator.long": "Collaborator: Trust signatures by collaborators",
"repo.settings.trust_model.collaborator.desc": "Valid signatures by collaborators of this repository will be marked \"trusted\", whether they match the committer or not. Otherwise, valid signatures will be marked \"untrusted\" if the signature matches the committer and \"unmatched\" if not.",
"repo.settings.trust_model.committer": "Committer",
"repo.settings.trust_model.committer.long": "Committer: Trust signatures that match committers. This matches GitHub's behavior and will force commits signed by MokoGitea to have MokoGitea as the committer.",
"repo.settings.trust_model.committer.desc": "Valid signatures will only be marked \"trusted\" if they match the committer, otherwise they will be marked \"unmatched\". This forces MokoGitea to be the committer on signed commits, with the actual committer marked as Co-authored-by: and Co-committed-by: trailer in the commit. The default MokoGitea key must match a user in the database.",
"repo.settings.trust_model.committer.long": "Committer: Trust signatures that match committers. This matches GitHub's behavior and will force commits signed by ${APP_NAME} to have ${APP_NAME} as the committer.",
"repo.settings.trust_model.committer.desc": "Valid signatures will only be marked \"trusted\" if they match the committer, otherwise they will be marked \"unmatched\". This forces ${APP_NAME} to be the committer on signed commits, with the actual committer marked as Co-authored-by: and Co-committed-by: trailer in the commit. The default ${APP_NAME} key must match a user in the database.",
"repo.settings.trust_model.collaboratorcommitter": "Collaborator+Committer",
"repo.settings.trust_model.collaboratorcommitter.long": "Collaborator+Committer: Trust signatures by collaborators which match the committer",
"repo.settings.trust_model.collaboratorcommitter.desc": "Valid signatures by collaborators of this repository will be marked \"trusted\" if they match the committer. Otherwise, valid signatures will be marked \"untrusted\" if the signature matches the committer and \"unmatched\" otherwise. This will force MokoGitea to be marked as the committer on signed commits, with the actual committer marked as Co-Authored-By: and Co-Committed-By: trailer in the commit. The default MokoGitea key must match a user in the database.",
"repo.settings.trust_model.collaboratorcommitter.desc": "Valid signatures by collaborators of this repository will be marked \"trusted\" if they match the committer. Otherwise, valid signatures will be marked \"untrusted\" if the signature matches the committer and \"unmatched\" otherwise. This will force ${APP_NAME} to be marked as the committer on signed commits, with the actual committer marked as Co-Authored-By: and Co-Committed-By: trailer in the commit. The default ${APP_NAME} key must match a user in the database.",
"repo.settings.wiki_delete": "Delete Wiki Data",
"repo.settings.wiki_delete_desc": "Deleting repository wiki data is permanent and cannot be undone.",
"repo.settings.wiki_delete_notices_1": "- This will permanently delete and disable the repository wiki for %s.",
@@ -2240,7 +2240,7 @@
"repo.settings.remove_team_success": "The team's access to the repository has been removed.",
"repo.settings.add_webhook": "Add Webhook",
"repo.settings.add_webhook.invalid_channel_name": "Webhook channel name cannot be empty and cannot contain only a # character.",
"repo.settings.hooks_desc": "Webhooks automatically make HTTP POST requests to a server when certain MokoGitea events trigger. Read more in the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">webhooks guide</a>.",
"repo.settings.hooks_desc": "Webhooks automatically make HTTP POST requests to a server when certain ${APP_NAME} events trigger. Read more in the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">webhooks guide</a>.",
"repo.settings.webhook_deletion": "Remove Webhook",
"repo.settings.webhook_deletion_desc": "Removing a webhook deletes its settings and delivery history. Continue?",
"repo.settings.webhook_deletion_success": "The webhook has been removed.",
@@ -2258,7 +2258,7 @@
"repo.settings.githooks_desc": "Git Hooks are powered by Git itself. You can edit hook files below to set up custom operations.",
"repo.settings.githook_edit_desc": "If the hook is inactive, sample content will be presented. Leaving content to an empty value will disable this hook.",
"repo.settings.update_githook": "Update Hook",
"repo.settings.add_webhook_desc": "MokoGitea will send <code>POST</code> requests with a specified content type to the target URL. Read more in the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">webhooks guide</a>.",
"repo.settings.add_webhook_desc": "${APP_NAME} will send <code>POST</code> requests with a specified content type to the target URL. Read more in the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">webhooks guide</a>.",
"repo.settings.payload_url": "Target URL",
"repo.settings.http_method": "HTTP Method",
"repo.settings.content_type": "POST Content Type",
@@ -2326,9 +2326,9 @@
"repo.settings.event_pull_request_merge": "Pull Request Merge",
"repo.settings.event_header_workflow": "Workflow Events",
"repo.settings.event_workflow_run": "Workflow Run",
"repo.settings.event_workflow_run_desc": "Gitea Actions Workflow run queued, waiting, in progress, or completed.",
"repo.settings.event_workflow_run_desc": "${APP_NAME} Actions Workflow run queued, waiting, in progress, or completed.",
"repo.settings.event_workflow_job": "Workflow Jobs",
"repo.settings.event_workflow_job_desc": "Gitea Actions Workflow job queued, waiting, in progress, or completed.",
"repo.settings.event_workflow_job_desc": "${APP_NAME} Actions Workflow job queued, waiting, in progress, or completed.",
"repo.settings.event_package": "Package",
"repo.settings.event_package_desc": "Package created or deleted in a repository.",
"repo.settings.branch_filter": "Branch filter",
@@ -2349,7 +2349,7 @@
"repo.settings.slack_domain": "Domain",
"repo.settings.slack_channel": "Channel",
"repo.settings.add_web_hook_desc": "Integrate <a target=\"_blank\" rel=\"noreferrer\" href=\"%s\">%s</a> into your repository.",
"repo.settings.web_hook_name_gitea": "MokoGitea",
"repo.settings.web_hook_name_gitea": "${APP_NAME}",
"repo.settings.web_hook_name_gogs": "Gogs",
"repo.settings.web_hook_name_slack": "Slack",
"repo.settings.web_hook_name_discord": "Discord",
@@ -2404,15 +2404,18 @@
"repo.settings.protect_whitelist_committers": "Allowlist Restricted Push",
"repo.settings.protect_whitelist_committers_desc": "Only allowlisted users or teams will be allowed to push to this branch (but not force push).",
"repo.settings.protect_whitelist_deploy_keys": "Allowlist deploy keys with write access to push.",
"repo.settings.protect_whitelist_actions_user": "Allowlist actions bot user to push.",
"repo.settings.protect_whitelist_users": "Allowlisted users for pushing:",
"repo.settings.protect_whitelist_teams": "Allowlisted teams for pushing:",
"repo.settings.protect_force_push_allowlist_users": "Allowlisted users for force pushing:",
"repo.settings.protect_force_push_allowlist_teams": "Allowlisted teams for force pushing:",
"repo.settings.protect_force_push_allowlist_deploy_keys": "Allowlist deploy keys with push access to force push.",
"repo.settings.protect_force_push_allowlist_actions_user": "Allowlist actions bot user to force push.",
"repo.settings.protect_merge_whitelist_committers": "Enable Merge Allowlist",
"repo.settings.protect_merge_whitelist_committers_desc": "Allow only allowlisted users or teams to merge pull requests into this branch.",
"repo.settings.protect_merge_whitelist_users": "Allowlisted users for merging:",
"repo.settings.protect_merge_whitelist_teams": "Allowlisted teams for merging:",
"repo.settings.protect_merge_whitelist_actions_user": "Allowlist actions bot user to merge.",
"repo.settings.protect_check_status_contexts": "Enable Status Check",
"repo.settings.protect_status_check_patterns": "Status check patterns:",
"repo.settings.protect_status_check_patterns_desc": "Enter patterns to specify which status checks must pass before branches can be merged into a branch that matches this rule. Each line specifies a pattern. Patterns cannot be empty.",
@@ -2634,7 +2637,7 @@
"repo.release.delete_release": "Delete Release",
"repo.release.delete_tag": "Delete Tag",
"repo.release.deletion": "Delete Release",
"repo.release.deletion_desc": "Deleting a release only removes it from MokoGitea. It will not affect the Git tag, the contents of your repository or its history. Continue?",
"repo.release.deletion_desc": "Deleting a release only removes it from ${APP_NAME}. It will not affect the Git tag, the contents of your repository or its history. Continue?",
"repo.release.deletion_success": "The release has been deleted.",
"repo.release.deletion_tag_desc": "Will delete this tag from repository. Repository contents and history remain unchanged. Continue?",
"repo.release.deletion_tag_success": "The tag has been deleted.",
@@ -2913,7 +2916,7 @@
"admin.last_page": "Last",
"admin.total": "Total: %d",
"admin.settings": "Admin Settings",
"admin.dashboard.new_version_hint": "MokoGitea %s is now available, you are running %s. Check <a target=\"_blank\" rel=\"noreferrer\" href=\"%s\">the blog</a> for more details.",
"admin.dashboard.new_version_hint": "${APP_NAME} %s is now available, you are running %s. Check <a target=\"_blank\" rel=\"noreferrer\" href=\"%s\">the blog</a> for more details.",
"admin.dashboard.statistic": "Summary",
"admin.dashboard.maintenance_operations": "Maintenance Operations",
"admin.dashboard.system_status": "System Status",
@@ -2949,8 +2952,8 @@
"admin.dashboard.deleted_branches_cleanup": "Clean up deleted branches",
"admin.dashboard.update_migration_poster_id": "Update migration poster IDs",
"admin.dashboard.git_gc_repos": "Garbage-collect all repositories",
"admin.dashboard.resync_all_sshkeys": "Update the '.ssh/authorized_keys' file with MokoGitea SSH keys",
"admin.dashboard.resync_all_sshprincipals": "Update the '.ssh/authorized_principals' file with MokoGitea SSH principals",
"admin.dashboard.resync_all_sshkeys": "Update the '.ssh/authorized_keys' file with ${APP_NAME} SSH keys",
"admin.dashboard.resync_all_sshprincipals": "Update the '.ssh/authorized_principals' file with ${APP_NAME} SSH principals",
"admin.dashboard.resync_all_hooks": "Resynchronize git hooks of all repositories (pre-receive, update, post-receive, proc-receive, ...)",
"admin.dashboard.reinit_missing_repos": "Reinitialize all missing Git repositories for which records exist",
"admin.dashboard.sync_external_users": "Synchronize external user data",
@@ -3030,7 +3033,7 @@
"admin.users.is_admin": "Is Administrator",
"admin.users.is_restricted": "Is Restricted",
"admin.users.allow_git_hook": "May Create Git Hooks",
"admin.users.allow_git_hook_tooltip": "Git Hooks are executed as the OS user running MokoGitea and will have the same level of host access. As a result, users with this special Git Hook privilege can access and modify all MokoGitea repositories as well as the database used by MokoGitea. Consequently they are also able to gain MokoGitea administrator privileges.",
"admin.users.allow_git_hook_tooltip": "Git Hooks are executed as the OS user running ${APP_NAME} and will have the same level of host access. As a result, users with this special Git Hook privilege can access and modify all ${APP_NAME} repositories as well as the database used by ${APP_NAME}. Consequently they are also able to gain ${APP_NAME} administrator privileges.",
"admin.users.allow_import_local": "May Import Local Repositories",
"admin.users.allow_create_organization": "May Create Organizations",
"admin.users.update_profile": "Update User Account",
@@ -3100,11 +3103,11 @@
"admin.packages.size": "Size",
"admin.packages.published": "Published",
"admin.defaulthooks": "Default Webhooks",
"admin.defaulthooks.desc": "Webhooks automatically make HTTP POST requests to a server when certain MokoGitea events trigger. Webhooks defined here are defaults and will be copied into all new repositories. Read more in the <a target=\"_blank\" rel=\"noopener\" href=\"%s\">webhooks guide</a>.",
"admin.defaulthooks.desc": "Webhooks automatically make HTTP POST requests to a server when certain ${APP_NAME} events trigger. Webhooks defined here are defaults and will be copied into all new repositories. Read more in the <a target=\"_blank\" rel=\"noopener\" href=\"%s\">webhooks guide</a>.",
"admin.defaulthooks.add_webhook": "Add Default Webhook",
"admin.defaulthooks.update_webhook": "Update Default Webhook",
"admin.systemhooks": "System Webhooks",
"admin.systemhooks.desc": "Webhooks automatically make HTTP POST requests to a server when certain MokoGitea events trigger. Webhooks defined here will act on all repositories on the system, so please consider any performance implications this may have. Read more in the <a target=\"_blank\" rel=\"noopener\" href=\"%s\">webhooks guide</a>.",
"admin.systemhooks.desc": "Webhooks automatically make HTTP POST requests to a server when certain ${APP_NAME} events trigger. Webhooks defined here will act on all repositories on the system, so please consider any performance implications this may have. Read more in the <a target=\"_blank\" rel=\"noopener\" href=\"%s\">webhooks guide</a>.",
"admin.systemhooks.add_webhook": "Add System Webhook",
"admin.systemhooks.update_webhook": "Update System Webhook",
"admin.auths.auth_manage_panel": "Authentication Source Management",
@@ -3125,7 +3128,7 @@
"admin.auths.user_base": "User Search Base",
"admin.auths.user_dn": "User DN",
"admin.auths.attribute_username": "Username Attribute",
"admin.auths.attribute_username_placeholder": "Leave empty to use the username entered in MokoGitea.",
"admin.auths.attribute_username_placeholder": "Leave empty to use the username entered in ${APP_NAME}.",
"admin.auths.attribute_name": "First Name Attribute",
"admin.auths.attribute_surname": "Surname Attribute",
"admin.auths.attribute_mail": "Email Attribute",
@@ -3232,7 +3235,7 @@
"admin.auths.invalid_openIdConnectAutoDiscoveryURL": "Invalid Auto Discovery URL (this must be a valid URL starting with http:// or https://)",
"admin.config.server_config": "Server Configuration",
"admin.config.app_name": "Site Title",
"admin.config.app_ver": "MokoGitea Version",
"admin.config.app_ver": "${APP_NAME} Version",
"admin.config.custom_conf": "Configuration File Path",
"admin.config.custom_file_root_path": "Custom File Root Path",
"admin.config.disable_router_log": "Disable Router Log",
@@ -3272,7 +3275,7 @@
"admin.config.service_config": "Service Configuration",
"admin.config.register_email_confirm": "Require Email Confirmation to Register",
"admin.config.disable_register": "Disable Self-Registration",
"admin.config.allow_only_internal_registration": "Allow Registration Only Through MokoGitea itself",
"admin.config.allow_only_internal_registration": "Allow Registration Only Through ${APP_NAME} itself",
"admin.config.allow_only_external_registration": "Allow Registration Only Through External Services",
"admin.config.enable_openid_signup": "Enable OpenID Self-Registration",
"admin.config.enable_openid_signin": "Enable OpenID Sign-In",
@@ -3325,6 +3328,14 @@
"admin.config.common.start_time": "Start time",
"admin.config.common.end_time": "End time",
"admin.config.common.skip_time_check": "Leave time empty (clear the field) to skip time check",
"admin.config.instance_landing_page": "Default Landing Page",
"admin.config.landing_page.home": "Home — default home page",
"admin.config.landing_page.explore": "Explore — repository explore page",
"admin.config.landing_page.organizations": "Organizations — organization explore page",
"admin.config.landing_page.login": "Login — redirect to login page",
"admin.config.landing_page.custom": "Custom path — redirect to a specific URL path",
"admin.config.landing_page.custom_path": "Custom path",
"admin.config.landing_page.custom_path_help": "Internal path to redirect unauthenticated visitors to (e.g. /MokoConsulting or /MokoConsulting/MokoGitea/wiki).",
"admin.config.instance_maintenance": "Instance Maintenance",
"admin.config.instance_maintenance_mode.admin_web_access_only": "Only allow admin to access the web UI",
"admin.config.instance_web_banner.enabled": "Show banner",
@@ -3414,11 +3425,11 @@
"admin.self_check.no_problem_found": "No problem found yet.",
"admin.self_check.startup_warnings": "Startup warnings:",
"admin.self_check.database_collation_mismatch": "Expect database to use collation: %s",
"admin.self_check.database_collation_case_insensitive": "Database is using collation %s, which is a case-insensitive collation. Although MokoGitea could work with it, there might be some rare cases which don't work as expected.",
"admin.self_check.database_collation_case_insensitive": "Database is using collation %s, which is a case-insensitive collation. Although ${APP_NAME} could work with it, there might be some rare cases which don't work as expected.",
"admin.self_check.database_inconsistent_collation_columns": "Database is using collation %s, but these columns are using mismatched collations. This might cause some unexpected problems.",
"admin.self_check.database_fix_mysql": "For MySQL/MariaDB users, you could use the \"gitea doctor convert\" command to fix the collation problems, or you could also fix the problem manually with \"ALTER ... COLLATE ...\" SQL queries.",
"admin.self_check.database_fix_mssql": "For MSSQL users, you could only fix the problem manually with \"ALTER ... COLLATE ...\" SQL queries at the moment.",
"admin.self_check.location_origin_mismatch": "Current URL (%[1]s) doesn't match the URL seen by MokoGitea (%[2]s). If you are using a reverse proxy, please make sure the \"Host\" and \"X-Forwarded-Proto\" headers are set correctly.",
"admin.self_check.location_origin_mismatch": "Current URL (%[1]s) doesn't match the URL seen by ${APP_NAME} (%[2]s). If you are using a reverse proxy, please make sure the \"Host\" and \"X-Forwarded-Proto\" headers are set correctly.",
"action.create_repo": "created repository <a href=\"%s\">%s</a>",
"action.rename_repo": "renamed repository from <code>%[1]s</code> to <a href=\"%[2]s\">%[3]s</a>",
"action.commit_repo": "pushed to <a href=\"%[2]s\">%[3]s</a> at <a href=\"%[1]s\">%[4]s</a>",
@@ -3771,8 +3782,8 @@
"actions.runs.status_no_select": "All status",
"actions.runs.no_results": "No results matched.",
"actions.runs.no_workflows": "There are no workflows yet.",
"actions.runs.no_workflows.quick_start": "Don't know how to start with Gitea Actions? See <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">the quick start guide</a>.",
"actions.runs.no_workflows.documentation": "For more information on Gitea Actions, see <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">the documentation</a>.",
"actions.runs.no_workflows.quick_start": "Don't know how to start with ${APP_NAME} Actions? See <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">the quick start guide</a>.",
"actions.runs.no_workflows.documentation": "For more information on ${APP_NAME} Actions, see <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">the documentation</a>.",
"actions.runs.no_runs": "The workflow has no runs yet.",
"actions.runs.empty_commit_message": "(empty commit message)",
"actions.runs.expire_log_message": "Logs have been purged because they were too old.",
+19
View File
@@ -758,10 +758,13 @@ func CreateBranchProtection(ctx *context.APIContext) {
CanPush: form.EnablePush,
EnableWhitelist: form.EnablePush && form.EnablePushWhitelist,
WhitelistDeployKeys: form.EnablePush && form.EnablePushWhitelist && form.PushWhitelistDeployKeys,
WhitelistActionsUser: form.EnablePush && form.EnablePushWhitelist && form.PushWhitelistActionsUser,
CanForcePush: form.EnablePush && form.EnableForcePush,
EnableForcePushAllowlist: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist,
ForcePushAllowlistDeployKeys: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist && form.ForcePushAllowlistDeployKeys,
ForcePushAllowlistActionsUser: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist && form.ForcePushAllowlistActionsUser,
EnableMergeWhitelist: form.EnableMergeWhitelist,
MergeWhitelistActionsUser: form.EnableMergeWhitelist && form.MergeWhitelistActionsUser,
EnableStatusCheck: form.EnableStatusCheck,
StatusCheckContexts: form.StatusCheckContexts,
EnableApprovalsWhitelist: form.EnableApprovalsWhitelist,
@@ -861,17 +864,22 @@ func EditBranchProtection(ctx *context.APIContext) {
protectBranch.CanPush = false
protectBranch.EnableWhitelist = false
protectBranch.WhitelistDeployKeys = false
protectBranch.WhitelistActionsUser = false
} else {
protectBranch.CanPush = true
if form.EnablePushWhitelist != nil {
if !*form.EnablePushWhitelist {
protectBranch.EnableWhitelist = false
protectBranch.WhitelistDeployKeys = false
protectBranch.WhitelistActionsUser = false
} else {
protectBranch.EnableWhitelist = true
if form.PushWhitelistDeployKeys != nil {
protectBranch.WhitelistDeployKeys = *form.PushWhitelistDeployKeys
}
if form.PushWhitelistActionsUser != nil {
protectBranch.WhitelistActionsUser = *form.PushWhitelistActionsUser
}
}
}
}
@@ -882,17 +890,22 @@ func EditBranchProtection(ctx *context.APIContext) {
protectBranch.CanForcePush = false
protectBranch.EnableForcePushAllowlist = false
protectBranch.ForcePushAllowlistDeployKeys = false
protectBranch.ForcePushAllowlistActionsUser = false
} else {
protectBranch.CanForcePush = true
if form.EnableForcePushAllowlist != nil {
if !*form.EnableForcePushAllowlist {
protectBranch.EnableForcePushAllowlist = false
protectBranch.ForcePushAllowlistDeployKeys = false
protectBranch.ForcePushAllowlistActionsUser = false
} else {
protectBranch.EnableForcePushAllowlist = true
if form.ForcePushAllowlistDeployKeys != nil {
protectBranch.ForcePushAllowlistDeployKeys = *form.ForcePushAllowlistDeployKeys
}
if form.ForcePushAllowlistActionsUser != nil {
protectBranch.ForcePushAllowlistActionsUser = *form.ForcePushAllowlistActionsUser
}
}
}
}
@@ -904,6 +917,12 @@ func EditBranchProtection(ctx *context.APIContext) {
if form.EnableMergeWhitelist != nil {
protectBranch.EnableMergeWhitelist = *form.EnableMergeWhitelist
if !*form.EnableMergeWhitelist {
protectBranch.MergeWhitelistActionsUser = false
}
}
if form.MergeWhitelistActionsUser != nil && protectBranch.EnableMergeWhitelist {
protectBranch.MergeWhitelistActionsUser = *form.MergeWhitelistActionsUser
}
if form.EnableStatusCheck != nil {
+5
View File
@@ -253,6 +253,11 @@ func LinkAccountPostRegister(ctx *context.Context) {
return
}
oauth2SignInSync(ctx, linkAccountData.AuthSourceID, u, linkAccountData.GothUser)
if ctx.Written() {
return
}
authSource, err := auth.GetSourceByID(ctx, linkAccountData.AuthSourceID)
if err != nil {
ctx.ServerError("GetSourceByID", err)
+37 -15
View File
@@ -13,6 +13,7 @@ import (
"net/url"
"sort"
"strings"
"time"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/auth"
user_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
@@ -301,21 +302,42 @@ func showLinkingLogin(ctx *context.Context, authSourceID int64, gothUser goth.Us
ctx.Redirect(setting.AppSubURL + "/user/link_account")
}
func oauth2UpdateAvatarIfNeed(ctx *context.Context, url string, u *user_model.User) {
if setting.OAuth2Client.UpdateAvatar && len(url) > 0 {
resp, err := http.Get(url)
if err == nil {
defer func() {
_ = resp.Body.Close()
}()
}
// ignore any error
if err == nil && resp.StatusCode == http.StatusOK {
data, err := io.ReadAll(io.LimitReader(resp.Body, setting.Avatar.MaxFileSize+1))
if err == nil && int64(len(data)) <= setting.Avatar.MaxFileSize {
_ = user_service.UploadAvatar(ctx, u, data)
}
}
var oauth2AvatarHTTPClient = &http.Client{Timeout: 30 * time.Second}
func oauth2UpdateAvatarIfNeed(ctx *context.Context, avatarURL string, u *user_model.User) {
if !setting.OAuth2Client.UpdateAvatar || len(avatarURL) == 0 {
return
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, avatarURL, nil)
if err != nil {
log.Warn("invalid avatar URL %q: %v", avatarURL, err)
return
}
// Some hosts (e.g. Wikimedia) reject Go's default User-Agent.
req.Header.Set("User-Agent", "Gitea "+setting.AppVer)
resp, err := oauth2AvatarHTTPClient.Do(req)
if err != nil {
log.Warn("fetch %q failed: %v", avatarURL, err)
return
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
log.Warn("fetch %q returned status %d", avatarURL, resp.StatusCode)
return
}
data, err := io.ReadAll(io.LimitReader(resp.Body, setting.Avatar.MaxFileSize+1))
if err != nil {
log.Warn("read body from %q failed: %v", avatarURL, err)
return
}
if int64(len(data)) > setting.Avatar.MaxFileSize {
log.Warn("avatar from %q exceeds max size %d", avatarURL, setting.Avatar.MaxFileSize)
return
}
if err := user_service.UploadAvatar(ctx, u, data); err != nil {
log.Warn("UploadAvatar for user %q failed: %v", u.Name, err)
}
}
+12 -3
View File
@@ -48,9 +48,18 @@ func Home(ctx *context.Context) {
}
return
// Check non-logged users landing page.
} else if setting.LandingPageURL != setting.LandingPageHome {
ctx.Redirect(setting.AppSubURL + string(setting.LandingPageURL))
return
} else {
// Dynamic landing page from admin config takes priority.
landingPage := setting.Config().Instance.LandingPage.Value(ctx)
if landingPage.Mode != "" && landingPage.Mode != "home" {
ctx.Redirect(setting.AppSubURL + landingPage.URL())
return
}
// Fall back to static app.ini setting.
if setting.LandingPageURL != setting.LandingPageHome {
ctx.Redirect(setting.AppSubURL + string(setting.LandingPageURL))
return
}
}
// Check auto-login.
+1 -2
View File
@@ -138,8 +138,7 @@ func resolveCurrentRunForView(ctx *context_module.Context) *actions_model.Action
var runByID, runByIndex *actions_model.ActionRun
var targetJobByIndex *actions_model.ActionRunJob
// Each run must have at least one job, so a valid job ID in the same run cannot be smaller than the run ID.
if !byIndex && jobNum >= runNum {
if !byIndex {
// Probe the repo-scoped job ID first and only accept it when the job exists and belongs to the same runNum.
job, err := actions_model.GetRunJobByRepoAndID(ctx, ctx.Repo.Repository.ID, jobNum)
if err != nil && !errors.Is(err, util.ErrNotExist) {
+4 -4
View File
@@ -147,11 +147,11 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
if !ctx.IsSigned {
// TODO: support digit auth - which would be Authorization header with digit
if setting.OAuth2.Enabled {
// `Basic realm="Gitea"` tells the GCM to use builtin OAuth2 application: https://github.com/git-ecosystem/git-credential-manager/pull/1442
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea"`)
// `Basic realm="<AppName>"` tells the GCM to use builtin OAuth2 application: https://github.com/git-ecosystem/git-credential-manager/pull/1442
ctx.Resp.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, setting.AppName))
} else {
// If OAuth2 is disabled, then use another realm to avoid GCM OAuth2 attempt
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea (Basic Auth)"`)
ctx.Resp.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s (Basic Auth)"`, setting.AppName))
}
ctx.HTTPError(http.StatusUnauthorized)
return nil
@@ -162,7 +162,7 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
return nil
}
if ctx.IsBasicAuth && ctx.Data["IsApiToken"] != true && !ctx.Doer.IsGiteaActions() {
if ctx.IsBasicAuth && ctx.Data["IsApiToken"] != true && !ctx.Doer.IsActions() {
_, err = auth_model.GetTwoFactorByUID(ctx, ctx.Doer.ID)
if err == nil {
// TODO: This response should be changed to "invalid credentials" for security reasons once the expectation behind it (creating an app token to authenticate) is properly documented
@@ -168,10 +168,12 @@ func SettingsProtectedBranchPost(ctx *context.Context) {
protectBranch.CanPush = true
protectBranch.EnableWhitelist = false
protectBranch.WhitelistDeployKeys = false
protectBranch.WhitelistActionsUser = false
case "whitelist":
protectBranch.CanPush = true
protectBranch.EnableWhitelist = true
protectBranch.WhitelistDeployKeys = f.WhitelistDeployKeys
protectBranch.WhitelistActionsUser = f.WhitelistActionsUser
if strings.TrimSpace(f.WhitelistUsers) != "" {
whitelistUsers, _ = base.StringsToInt64s(strings.Split(f.WhitelistUsers, ","))
}
@@ -182,6 +184,7 @@ func SettingsProtectedBranchPost(ctx *context.Context) {
protectBranch.CanPush = false
protectBranch.EnableWhitelist = false
protectBranch.WhitelistDeployKeys = false
protectBranch.WhitelistActionsUser = false
}
switch f.EnableForcePush {
@@ -189,10 +192,12 @@ func SettingsProtectedBranchPost(ctx *context.Context) {
protectBranch.CanForcePush = true
protectBranch.EnableForcePushAllowlist = false
protectBranch.ForcePushAllowlistDeployKeys = false
protectBranch.ForcePushAllowlistActionsUser = false
case "whitelist":
protectBranch.CanForcePush = true
protectBranch.EnableForcePushAllowlist = true
protectBranch.ForcePushAllowlistDeployKeys = f.ForcePushAllowlistDeployKeys
protectBranch.ForcePushAllowlistActionsUser = f.ForcePushAllowlistActionsUser
if strings.TrimSpace(f.ForcePushAllowlistUsers) != "" {
forcePushAllowlistUsers, _ = base.StringsToInt64s(strings.Split(f.ForcePushAllowlistUsers, ","))
}
@@ -203,9 +208,11 @@ func SettingsProtectedBranchPost(ctx *context.Context) {
protectBranch.CanForcePush = false
protectBranch.EnableForcePushAllowlist = false
protectBranch.ForcePushAllowlistDeployKeys = false
protectBranch.ForcePushAllowlistActionsUser = false
}
protectBranch.EnableMergeWhitelist = f.EnableMergeWhitelist
protectBranch.MergeWhitelistActionsUser = f.EnableMergeWhitelist && f.MergeWhitelistActionsUser
if f.EnableMergeWhitelist {
if strings.TrimSpace(f.MergeWhitelistUsers) != "" {
mergeWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistUsers, ","))
+25
View File
@@ -0,0 +1,25 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package repo
import (
"net/http"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/updateserver"
)
// ServeUpdatesXML generates and serves a Joomla-compatible updates.xml
// from the repository's releases.
func ServeUpdatesXML(ctx *context.Context) {
xmlData, err := updateserver.GenerateJoomlaXML(ctx, ctx.Repo.Repository)
if err != nil {
ctx.ServerError("GenerateJoomlaXML", err)
return
}
ctx.Resp.Header().Set("Content-Type", "application/xml; charset=utf-8")
ctx.Resp.WriteHeader(http.StatusOK)
_, _ = ctx.Resp.Write(xmlData)
}
+2
View File
@@ -13,6 +13,7 @@ import (
// SwaggerV1Json render swagger v1 json
func SwaggerV1Json(ctx *context.Context) {
ctx.Data["SwaggerAppVer"] = template.HTML(template.JSEscapeString(setting.AppVer))
ctx.Data["SwaggerAppName"] = template.HTML(template.JSEscapeString(setting.AppName))
ctx.Data["SwaggerAppSubUrl"] = setting.AppSubURL // it is JS-safe
ctx.JSONTemplate("swagger/v1_json")
}
@@ -20,6 +21,7 @@ func SwaggerV1Json(ctx *context.Context) {
// OpenAPI3Json render OpenAPI 3.0 json (auto-converted from Swagger 2.0)
func OpenAPI3Json(ctx *context.Context) {
ctx.Data["SwaggerAppVer"] = template.HTML(template.JSEscapeString(setting.AppVer))
ctx.Data["SwaggerAppName"] = template.HTML(template.JSEscapeString(setting.AppName))
ctx.Data["SwaggerAppSubUrl"] = setting.AppSubURL // it is JS-safe
ctx.JSONTemplate("swagger/v1_openapi3_json")
}
+6
View File
@@ -1494,6 +1494,12 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
}, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoReleaseReader)
// end "/{username}/{reponame}": repo releases
// "/{username}/{reponame}": update server (Joomla-compatible updates.xml)
m.Group("/{username}/{reponame}", func() {
m.Get("/updates.xml", repo.ServeUpdatesXML)
}, optSignIn, context.RepoAssignment)
// end "/{username}/{reponame}": update server
m.Group("/{username}/{reponame}", func() { // to maintain compatibility with old attachments
m.Get("/attachments/{uuid}", webAuth.AllowBasic, webAuth.AllowOAuth2, repo.GetAttachment)
}, optSignIn, context.RepoAssignment)
+1 -1
View File
@@ -118,7 +118,7 @@ func (input *notifyInput) Notify(ctx context.Context) {
func notify(ctx context.Context, input *notifyInput) error {
shouldDetectSchedules := input.Event == webhook_module.HookEventPush && input.Ref.BranchName() == input.Repo.DefaultBranch
if input.Doer.IsGiteaActions() {
if input.Doer.IsActions() {
// avoiding triggering cyclically, for example:
// a comment of an issue will trigger the runner to add a new comment as reply,
// and the new comment will trigger the runner again.
+1 -1
View File
@@ -68,7 +68,7 @@ func (b *Basic) parseAuthBasic(req *http.Request) (ret struct{ authToken, uname,
// VerifyAuthToken only the access token provided as parameter, used by other auth methods that want to reuse access token verification logic
func (b *Basic) VerifyAuthToken(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore, authToken string) (*user_model.User, error) {
// get oauth2 token's user's ID
// get oauth2 token's user's ID and access scope
accessTokenScope, uid := GetOAuthAccessTokenScopeAndUserID(req.Context(), authToken)
if uid != 0 {
log.Trace("Basic Authorization: Valid OAuthAccessToken for user[%d]", uid)
+3
View File
@@ -173,14 +173,17 @@ func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch, repo
PushWhitelistUsernames: pushWhitelistUsernames,
PushWhitelistTeams: pushWhitelistTeams,
PushWhitelistDeployKeys: bp.WhitelistDeployKeys,
PushWhitelistActionsUser: bp.WhitelistActionsUser,
EnableForcePush: bp.CanForcePush,
EnableForcePushAllowlist: bp.EnableForcePushAllowlist,
ForcePushAllowlistUsernames: forcePushAllowlistUsernames,
ForcePushAllowlistTeams: forcePushAllowlistTeams,
ForcePushAllowlistDeployKeys: bp.ForcePushAllowlistDeployKeys,
ForcePushAllowlistActionsUser: bp.ForcePushAllowlistActionsUser,
EnableMergeWhitelist: bp.EnableMergeWhitelist,
MergeWhitelistUsernames: mergeWhitelistUsernames,
MergeWhitelistTeams: mergeWhitelistTeams,
MergeWhitelistActionsUser: bp.MergeWhitelistActionsUser,
EnableStatusCheck: bp.EnableStatusCheck,
StatusCheckContexts: bp.StatusCheckContexts,
RequiredApprovals: bp.RequiredApprovals,
+3
View File
@@ -172,13 +172,16 @@ type ProtectBranchForm struct {
WhitelistUsers string
WhitelistTeams string
WhitelistDeployKeys bool
WhitelistActionsUser bool
EnableForcePush string
ForcePushAllowlistUsers string
ForcePushAllowlistTeams string
ForcePushAllowlistDeployKeys bool
ForcePushAllowlistActionsUser bool
EnableMergeWhitelist bool
MergeWhitelistUsers string
MergeWhitelistTeams string
MergeWhitelistActionsUser bool
EnableStatusCheck bool
StatusCheckContexts string
RequiredApprovals int64
+224
View File
@@ -0,0 +1,224 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package updateserver
import (
"context"
"encoding/xml"
"fmt"
"strings"
"time"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
repo_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
)
// Joomla-compatible updates.xml structures for XML marshaling.
type xmlUpdates struct {
XMLName xml.Name `xml:"updates"`
Updates []xmlUpdate `xml:"update"`
}
type xmlUpdate struct {
Name string `xml:"name"`
Description string `xml:"description"`
Element string `xml:"element"`
Type string `xml:"type"`
Client string `xml:"client"`
Version string `xml:"version"`
CreationDate string `xml:"creationDate"`
InfoURL xmlInfoURL `xml:"infourl"`
Downloads xmlDownloads `xml:"downloads"`
SHA256 string `xml:"sha256,omitempty"`
Tags xmlTags `xml:"tags"`
ChangelogURL string `xml:"changelogurl,omitempty"`
Maintainer string `xml:"maintainer,omitempty"`
MaintainerURL string `xml:"maintainerurl,omitempty"`
TargetPlatform xmlTargetPlat `xml:"targetplatform"`
}
type xmlInfoURL struct {
Title string `xml:"title,attr"`
URL string `xml:",chardata"`
}
type xmlDownloads struct {
DownloadURL []xmlDownloadURL `xml:"downloadurl"`
}
type xmlDownloadURL struct {
Type string `xml:"type,attr"`
Format string `xml:"format,attr"`
URL string `xml:",chardata"`
}
type xmlTags struct {
Tag string `xml:"tag"`
}
type xmlTargetPlat struct {
Name string `xml:"name,attr"`
Version string `xml:"version,attr"`
}
// channelFromTag maps a release tag name to a Joomla update channel.
func channelFromTag(tagName string, isPrerelease bool) string {
lower := strings.ToLower(tagName)
switch {
case strings.Contains(lower, "-dev") || strings.Contains(lower, "development"):
return "dev"
case strings.Contains(lower, "-alpha") || strings.Contains(lower, "alpha"):
return "alpha"
case strings.Contains(lower, "-beta") || strings.Contains(lower, "beta"):
return "beta"
case strings.Contains(lower, "-rc") || strings.Contains(lower, "release-candidate"):
return "rc"
case isPrerelease:
return "rc"
default:
return "stable"
}
}
// GenerateJoomlaXML builds a Joomla-compatible updates.xml from repository releases.
// It returns the raw XML bytes. The element, maintainer, and target platform
// are derived from the repo name and owner.
func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository) ([]byte, error) {
releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
RepoID: repo.ID,
ListOptions: db.ListOptionsAll,
IncludeDrafts: false,
IncludeTags: false,
})
if err != nil {
return nil, fmt.Errorf("GetReleasesByRepoID: %w", err)
}
if err := repo.LoadOwner(ctx); err != nil {
return nil, fmt.Errorf("LoadOwner: %w", err)
}
baseURL := setting.AppURL
if strings.HasSuffix(baseURL, "/") {
baseURL = baseURL[:len(baseURL)-1]
}
repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name)
element := strings.ToLower(repo.Name)
// Track best (latest) release per channel to emit one entry per channel.
bestByChannel := make(map[string]*repo_model.Release)
for _, rel := range releases {
if rel.IsDraft || rel.IsTag {
continue
}
ch := channelFromTag(rel.TagName, rel.IsPrerelease)
existing, ok := bestByChannel[ch]
if !ok || rel.CreatedUnix > existing.CreatedUnix {
bestByChannel[ch] = rel
}
}
var updates xmlUpdates
for _, ch := range []string{"stable", "rc", "beta", "alpha", "dev"} {
rel, ok := bestByChannel[ch]
if !ok {
continue
}
// Load attachments for download URLs.
if err := rel.LoadAttributes(ctx); err != nil {
continue
}
// Find the first .zip attachment as the download URL.
var downloadURL string
for _, att := range rel.Attachments {
if strings.HasSuffix(strings.ToLower(att.Name), ".zip") {
downloadURL = fmt.Sprintf("%s/releases/download/%s/%s", repoLink, rel.TagName, att.Name)
break
}
}
// Fall back to the release tag archive if no zip attachment.
if downloadURL == "" {
downloadURL = fmt.Sprintf("%s/archive/%s.zip", repoLink, rel.TagName)
}
version := extractVersion(rel.TagName)
suffix := channelSuffix(ch)
if suffix != "" {
version = version + suffix
}
u := xmlUpdate{
Name: fmt.Sprintf("%s - %s", repo.Owner.Name, repo.Name),
Description: fmt.Sprintf("%s - %s %s build.", repo.Owner.Name, repo.Name, ch),
Element: element,
Type: "component",
Client: "site",
Version: version,
CreationDate: time.Unix(int64(rel.CreatedUnix), 0).Format("2006-01-02"),
InfoURL: xmlInfoURL{
Title: fmt.Sprintf("%s - %s", repo.Owner.Name, repo.Name),
URL: fmt.Sprintf("%s/releases/tag/%s", repoLink, rel.TagName),
},
Downloads: xmlDownloads{
DownloadURL: []xmlDownloadURL{
{Type: "full", Format: "zip", URL: downloadURL},
},
},
Tags: xmlTags{Tag: ch},
ChangelogURL: fmt.Sprintf("%s/raw/branch/%s/CHANGELOG.md", repoLink, repo.DefaultBranch),
Maintainer: repo.Owner.Name,
MaintainerURL: fmt.Sprintf("%s/%s", baseURL, repo.Owner.Name),
TargetPlatform: xmlTargetPlat{
Name: "joomla",
Version: ".*",
},
}
updates.Updates = append(updates.Updates, u)
}
output, err := xml.MarshalIndent(updates, "", " ")
if err != nil {
return nil, fmt.Errorf("xml.MarshalIndent: %w", err)
}
return append([]byte(xml.Header), output...), nil
}
// extractVersion strips common tag prefixes (v, release-, etc.) to get the version.
func extractVersion(tagName string) string {
v := tagName
v = strings.TrimPrefix(v, "v")
v = strings.TrimPrefix(v, "release-")
v = strings.TrimPrefix(v, "release/")
// Strip channel suffixes to get base version.
for _, suffix := range []string{"-dev", "-alpha", "-beta", "-rc", "-development", "-release-candidate"} {
if idx := strings.Index(strings.ToLower(v), suffix); idx > 0 {
v = v[:idx]
break
}
}
return v
}
// channelSuffix returns the version suffix for a channel.
func channelSuffix(channel string) string {
switch channel {
case "dev":
return "-dev"
case "alpha":
return "-alpha"
case "beta":
return "-beta"
case "rc":
return "-rc"
default:
return ""
}
}
@@ -2,6 +2,7 @@
{{template "admin/config_settings/avatars" .}}
{{template "admin/config_settings/repository" .}}
{{template "admin/config_settings/landing_page" .}}
{{template "admin/config_settings/instance" .}}
{{template "admin/layout_footer" .}}
@@ -0,0 +1,49 @@
<h4 class="ui top attached header">{{ctx.Locale.Tr "admin.config.instance_landing_page"}}</h4>
<div class="ui attached segment">
<form class="ui form ignore-dirty system-config-form" method="post" action="{{AppSubUrl}}/-/admin/config">
{{$cfgOpt := $.SystemConfig.Instance.LandingPage}}
{{$cfgKey := $cfgOpt.DynKey}}
{{$landingPage := $cfgOpt.Value ctx}}
<input type="hidden" data-config-dyn-key="{{$cfgKey}}" data-config-value-json="{{JsonUtils.EncodeToString $landingPage}}">
<div class="grouped fields">
<div class="field">
<div class="ui radio checkbox">
<input name="{{$cfgKey}}.Mode" type="radio" value="home" {{if or (eq $landingPage.Mode "") (eq $landingPage.Mode "home")}}checked{{end}}>
<label>{{ctx.Locale.Tr "admin.config.landing_page.home"}}</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input name="{{$cfgKey}}.Mode" type="radio" value="explore" {{if eq $landingPage.Mode "explore"}}checked{{end}}>
<label>{{ctx.Locale.Tr "admin.config.landing_page.explore"}}</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input name="{{$cfgKey}}.Mode" type="radio" value="organizations" {{if eq $landingPage.Mode "organizations"}}checked{{end}}>
<label>{{ctx.Locale.Tr "admin.config.landing_page.organizations"}}</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input name="{{$cfgKey}}.Mode" type="radio" value="login" {{if eq $landingPage.Mode "login"}}checked{{end}}>
<label>{{ctx.Locale.Tr "admin.config.landing_page.login"}}</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input name="{{$cfgKey}}.Mode" type="radio" value="custom" {{if eq $landingPage.Mode "custom"}}checked{{end}}>
<label>{{ctx.Locale.Tr "admin.config.landing_page.custom"}}</label>
</div>
</div>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "admin.config.landing_page.custom_path"}}</label>
<input type="text" name="{{$cfgKey}}.CustomPath" value="{{$landingPage.CustomPath}}" placeholder="/MokoConsulting">
<div class="help">{{ctx.Locale.Tr "admin.config.landing_page.custom_path_help"}}</div>
</div>
<div class="field">
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
</div>
</form>
</div>
+1 -1
View File
@@ -2,7 +2,7 @@
<div class="admin-setting-content">
{{if .NeedUpdate}}
<div class="ui positive message">
<div class="header">{{svg "octicon-info"}} MokoGitea Update Available</div>
<div class="header">{{svg "octicon-info"}} {{AppName}} Update Available</div>
<p>A new version <strong>{{.LatestVersion}}</strong> is available{{if .UpdateChannel}} ({{.UpdateChannel}} channel){{end}}.
{{if .ReleaseURL}}<a href="{{.ReleaseURL}}" target="_blank" rel="noopener noreferrer">View release notes</a>{{end}}</p>
{{if .DockerImage}}<p><code>docker pull {{.DockerImage}}</code></p>{{end}}
+1 -1
View File
@@ -1,7 +1,7 @@
<footer class="page-footer" role="group" aria-label="{{ctx.Locale.Tr "aria.footer"}}">
<div class="left-links" role="contentinfo" aria-label="{{ctx.Locale.Tr "aria.footer.software"}}">
{{if ShowFooterPoweredBy}}
<a target="_blank" href="https://git.mokoconsulting.tech/MokoConsulting/MokoGitea">{{ctx.Locale.Tr "powered_by" "MokoGitea"}}</a>
<a target="_blank" href="https://git.mokoconsulting.tech/MokoConsulting/MokoGitea">{{ctx.Locale.Tr "powered_by" AppName}}</a>
{{end}}
{{if (or .ShowFooterVersion .PageIsAdmin)}}
<span>
@@ -88,6 +88,12 @@
<label>{{ctx.Locale.Tr "repo.settings.protect_whitelist_deploy_keys"}}</label>
</div>
</div>
<div class="checkbox-sub-item field">
<div class="ui checkbox">
<input type="checkbox" name="whitelist_actions_user" {{if .Rule.WhitelistActionsUser}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.protect_whitelist_actions_user"}}</label>
</div>
</div>
</div>
</div>
<div class="field">
@@ -158,6 +164,12 @@
<label>{{ctx.Locale.Tr "repo.settings.protect_force_push_allowlist_deploy_keys"}}</label>
</div>
</div>
<div class="checkbox-sub-item field">
<div class="ui checkbox">
<input type="checkbox" name="force_push_allowlist_actions_user" {{if .Rule.ForcePushAllowlistActionsUser}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.protect_force_push_allowlist_actions_user"}}</label>
</div>
</div>
</div>
</div>
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.event_pull_request_approvals"}}</h5>
@@ -303,6 +315,12 @@
</div>
</div>
{{end}}
<div class="checkbox-sub-item field">
<div class="ui checkbox">
<input type="checkbox" name="merge_whitelist_actions_user" {{if .Rule.MergeWhitelistActionsUser}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.protect_merge_whitelist_actions_user"}}</label>
</div>
</div>
</div>
</div>
<div class="field">
+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="en">
<head>
{{ctx.HeadMetaContentSecurityPolicy}}
<title>Gitea API</title>
<title>{{AppName}} API</title>
<link rel="stylesheet" href="{{ctx.CurrentWebTheme.PublicAssetURI}}">
{{/* HINT: SWAGGER-CSS-IMPORT: import swagger styles ahead to avoid UI flicker (e.g.: the swagger-back-link element) */}}
<link rel="stylesheet" href="{{AssetURI "css/swagger.css"}}">
+2 -2
View File
@@ -11,8 +11,8 @@
],
"swagger": "2.0",
"info": {
"description": "This documentation describes the Gitea API.",
"title": "Gitea API",
"description": "This documentation describes the {{.SwaggerAppName}} API.",
"title": "{{.SwaggerAppName}} API",
"license": {
"name": "MIT",
"url": "http://opensource.org/licenses/MIT"
+2 -2
View File
@@ -10588,12 +10588,12 @@
}
},
"info": {
"description": "This documentation describes the Gitea API.",
"description": "This documentation describes the {{.SwaggerAppName}} API.",
"license": {
"name": "MIT",
"url": "http://opensource.org/licenses/MIT"
},
"title": "Gitea API",
"title": "{{.SwaggerAppName}} API",
"version": "{{.SwaggerAppVer}}"
},
"openapi": "3.0.3",
+4 -4
View File
@@ -149,7 +149,7 @@ func TestActionsJobTokenPermissiveAccess(t *testing.T) {
require.NoError(t, actions_model.UpdateRun(t.Context(), task.Job.Run, "is_fork_pull_request"))
testURL := *u
testURL.User = url.UserPassword("gitea-actions", task.Token)
testURL.User = url.UserPassword("mokogitea-actions", task.Token)
t.Run("ReadGitContent", func(t *testing.T) {
testURL.Path = "/user5/repo4.git/HEAD"
@@ -443,7 +443,7 @@ jobs:
// but it should not have write access
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s/%s.git/info/lfs/objects/batch", user2.Name, repo2.Name), lfs.BatchRequest{Operation: "upload"}).
SetHeader("Accept", lfs.MediaType).
AddBasicAuth("gitea-actions", task1Token)
AddBasicAuth("mokogitea-actions", task1Token)
MakeRequest(t, req, http.StatusUnauthorized)
// set repo1&repo2 max permission to "write" so that the actions token can access code
@@ -465,11 +465,11 @@ jobs:
// now task1 has write access to repo1, but still only read access to repo2 (different repo)
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s/%s.git/info/lfs/objects/batch", user2.Name, repo1.Name), lfs.BatchRequest{Operation: "upload"}).
SetHeader("Accept", lfs.MediaType).
AddBasicAuth("gitea-actions", task1Token)
AddBasicAuth("mokogitea-actions", task1Token)
MakeRequest(t, req, http.StatusOK)
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s/%s.git/info/lfs/objects/batch", user2.Name, repo2.Name), lfs.BatchRequest{Operation: "upload"}).
SetHeader("Accept", lfs.MediaType).
AddBasicAuth("gitea-actions", task1Token)
AddBasicAuth("mokogitea-actions", task1Token)
MakeRequest(t, req, http.StatusUnauthorized)
})
}
+9 -1
View File
@@ -160,6 +160,10 @@ func testActionsRouteForLegacyIndexBasedURL(t *testing.T) {
collisionJobIdx0 := mkJob(2600, collisionRun.ID, "legacy-collision-job-1", collisionRun.CommitSHA)
collisionJobIdx1 := mkJob(2601, collisionRun.ID, "legacy-collision-job-2", collisionRun.CommitSHA)
// A run whose job has a smaller ID than the run itself (job_id < run_id)
jobSmallerThanRunRun := mkRun(5000, 5500, "legacy route job before run", "aaa007")
jobSmallerThanRunJob := mkJob(4500, jobSmallerThanRunRun.ID, "legacy-job-before-run-job", jobSmallerThanRunRun.CommitSHA)
// A small ID-based run/job pair that collides with a different legacy run/job index pair.
ambiguousIDRun := mkRun(3, 1, "legacy route ambiguous id", "aaa005")
ambiguousIDJob := mkJob(4, ambiguousIDRun.ID, "legacy-ambiguous-id-job", ambiguousIDRun.CommitSHA)
@@ -182,11 +186,12 @@ func testActionsRouteForLegacyIndexBasedURL(t *testing.T) {
targetAmbiguousLegacyJob := ambiguousLegacyJobs[int(ambiguousIDJob.ID)]
insertBeansWithExplicitIDs(t, "action_run",
smallIDRun, otherSmallRun, normalRun, ambiguousIDRun, ambiguousLegacyRun, collisionRun,
smallIDRun, otherSmallRun, normalRun, ambiguousIDRun, ambiguousLegacyRun, collisionRun, jobSmallerThanRunRun,
)
insertBeansWithExplicitIDs(t, "action_run_job",
smallIDJob, otherSmallJob, normalRunJob, ambiguousIDJob, collisionJobIdx0, collisionJobIdx1,
ambiguousLegacyJobIdx0, ambiguousLegacyJobIdx1, ambiguousLegacyJobIdx2, ambiguousLegacyJobIdx3, ambiguousLegacyJobIdx4, ambiguousLegacyJobIdx5,
jobSmallerThanRunJob,
)
t.Run("OnlyRunID", func(t *testing.T) {
@@ -220,6 +225,9 @@ func testActionsRouteForLegacyIndexBasedURL(t *testing.T) {
user2Session.MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo.Name, normalRun.ID, normalRunJob.ID))
user2Session.MakeRequest(t, req, http.StatusOK)
// URL must resolve even when job_id < run_id.
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo.Name, jobSmallerThanRunRun.ID, jobSmallerThanRunJob.ID))
user2Session.MakeRequest(t, req, http.StatusOK)
})
t.Run("RunIndexAndJobIndex", func(t *testing.T) {
+1 -1
View File
@@ -57,7 +57,7 @@ jobs:
// prepare for clone
dstPath := t.TempDir()
u.Path = fmt.Sprintf("%s/%s.git", "user2", "reusable_workflow")
u.User = url.UserPassword("gitea-actions", taskToken)
u.User = url.UserPassword("mokogitea-actions", taskToken)
// the git clone will fail
doGitCloneFail(u)(t)
@@ -21,7 +21,7 @@ func testActionUserSignIn(t *testing.T) {
resp := MakeRequest(t, req, http.StatusOK)
u := DecodeJSON(t, resp, &api.User{})
assert.Equal(t, "gitea-actions", u.UserName)
assert.Equal(t, "mokogitea-actions", u.UserName)
}
func testActionUserAccessPublicRepo(t *testing.T) {
+40
View File
@@ -9,6 +9,9 @@ import (
"net/url"
"testing"
auth_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/auth"
repo_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unittest"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/test"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/util"
@@ -20,6 +23,7 @@ import (
func TestGitSmartHTTP(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
testGitSmartHTTP(t, u)
testGitSmartHTTPTokenScopes(t)
testRenamedRepoRedirect(t)
testGitArchiveRemote(t, u)
})
@@ -80,6 +84,42 @@ func testGitSmartHTTP(t *testing.T, u *url.URL) {
}
}
func testGitSmartHTTPTokenScopes(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2, OwnerName: "user2", Name: "repo2"})
require.True(t, repo.IsPrivate)
session := loginUser(t, "user2")
badToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadNotification)
readToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
writeToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
publicOnlyToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopePublicOnly, auth_model.AccessTokenScopeReadRepository)
t.Run("upload-pack requires read repository scope", func(t *testing.T) {
path := "/user2/repo2/info/refs?service=git-upload-pack"
MakeRequest(t, NewRequest(t, "GET", path).AddBasicAuth(badToken, "x-oauth-basic"), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", path).AddTokenAuth(badToken), http.StatusForbidden)
resp := MakeRequest(t, NewRequest(t, "GET", path).AddTokenAuth(readToken), http.StatusOK)
assert.Contains(t, resp.Body.String(), "refs/heads/master")
})
t.Run("receive-pack requires write repository scope", func(t *testing.T) {
path := "/user2/repo2/info/refs?service=git-receive-pack"
MakeRequest(t, NewRequest(t, "GET", path).AddBasicAuth(readToken, "x-oauth-basic"), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", path).AddTokenAuth(readToken), http.StatusForbidden)
resp := MakeRequest(t, NewRequest(t, "GET", path).AddTokenAuth(writeToken), http.StatusOK)
assert.Contains(t, resp.Body.String(), "refs/heads/master")
})
t.Run("public-only scope rejects private repo", func(t *testing.T) {
path := "/user2/repo2/info/refs?service=git-upload-pack"
MakeRequest(t, NewRequest(t, "GET", path).AddTokenAuth(publicOnlyToken), http.StatusForbidden)
})
}
func testRenamedRepoRedirect(t *testing.T) {
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
+92
View File
@@ -0,0 +1,92 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/http"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/web/auth"
"code.gitea.io/gitea/services/auth/source/oauth2"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/tests"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestOAuth2AvatarFromPicture(t *testing.T) {
defer tests.PrepareTestEnv(t)()
defer test.MockVariableValue(&setting.OAuth2Client.UpdateAvatar, true)()
mockServer := createOAuth2MockProvider()
defer mockServer.Close()
addOAuth2Source(t, "test-oidc-avatar", oauth2.Source{
Provider: "openidConnect",
ClientID: "test-client-id",
OpenIDConnectAutoDiscoveryURL: mockServer.URL + "/.well-known/openid-configuration",
})
authSource, err := auth_model.GetActiveOAuth2SourceByAuthName(t.Context(), "test-oidc-avatar")
require.NoError(t, err)
providerName := authSource.Cfg.(*oauth2.Source).Provider
t.Run("AutoRegister", func(t *testing.T) {
defer test.MockVariableValue(&setting.OAuth2Client.Username, "")()
defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)()
defer test.MockVariableValue(&gothic.CompleteUserAuth, func(res http.ResponseWriter, req *http.Request) (goth.User, error) {
return goth.User{
Provider: providerName,
UserID: "oidc-user-ua-pic",
Email: "oidc-user-ua-pic@example.com",
Name: "OIDC UA Pic",
AvatarURL: mockServer.URL + "/avatar.png",
}, nil
})()
req := NewRequest(t, "GET", "/user/oauth2/test-oidc-avatar/callback?code=XYZ&state=XYZ")
emptyTestSession(t).MakeRequest(t, req, http.StatusSeeOther)
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: "oidc-user-ua-pic"})
assert.True(t, user.UseCustomAvatar, "avatar must sync (requires Gitea UA)")
assert.NotEmpty(t, user.Avatar)
})
t.Run("LinkAccountRegister", func(t *testing.T) {
const newUserName = "oidc-link-register"
defer web.RouteMockReset()
web.RouteMock(web.MockAfterMiddlewares, func(ctx *context.Context) {
require.NoError(t, auth.Oauth2SetLinkAccountData(ctx, auth.LinkAccountData{
AuthSourceID: authSource.ID,
GothUser: goth.User{
Provider: providerName,
UserID: "oidc-link-register-sub",
Email: "oidc-link-register-a@example.com",
Name: "OIDC Link Register",
AvatarURL: mockServer.URL + "/avatar.png",
},
}))
})
req := NewRequestWithValues(t, "POST", "/user/link_account_signup", map[string]string{
"user_name": newUserName,
"email": "oidc-link-register-b@example.com",
"password": "AVeryStrongPassword!1",
"retype": "AVeryStrongPassword!1",
})
emptyTestSession(t).MakeRequest(t, req, http.StatusSeeOther)
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: newUserName})
require.Equal(t, auth_model.OAuth2, user.LoginType)
assert.True(t, user.UseCustomAvatar, "register-link flow must sync avatar from `picture` claim")
assert.NotEmpty(t, user.Avatar)
})
}
+12 -3
View File
@@ -8,6 +8,8 @@ import (
"crypto/sha256"
"encoding/base64"
"fmt"
"image"
"image/png"
"io"
"net/http"
"net/http/httptest"
@@ -1131,10 +1133,17 @@ func addOAuth2Source(t *testing.T, authName string, cfg oauth2.Source) {
require.NoError(t, err)
}
func createMockServer() *httptest.Server {
func createOAuth2MockProvider() *httptest.Server {
var mockServer *httptest.Server
mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/avatar.png":
if !strings.HasPrefix(r.Header.Get("User-Agent"), "Gitea ") {
http.Error(w, "user agent doesn't match", http.StatusForbidden)
return
}
w.Header().Set("Content-Type", "image/png")
_ = png.Encode(w, image.NewRGBA(image.Rect(0, 0, 8, 8)))
case "/.well-known/openid-configuration":
_, _ = w.Write([]byte(`{
"issuer": "` + mockServer.URL + `",
@@ -1153,7 +1162,7 @@ func createMockServer() *httptest.Server {
func TestSignInOauthCallbackSyncSSHKeys(t *testing.T) {
defer tests.PrepareTestEnv(t)()
mockServer := createMockServer()
mockServer := createOAuth2MockProvider()
defer mockServer.Close()
ctx := t.Context()
@@ -1233,7 +1242,7 @@ func TestSignInOauthCallbackSyncSSHKeys(t *testing.T) {
// Checks if an OAuth provider with spaces within the name does work,
// with the encoding of its names in the URL (PR#37327)
func testOAuthSourceSpecialChars(t *testing.T) {
mockServer := createMockServer()
mockServer := createOAuth2MockProvider()
defer mockServer.Close()
addOAuth2Source(t, "test space", oauth2.Source{
+5 -5
View File
@@ -1,7 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later
VERSION: 05.14.00
VERSION: 05.05.00
-->
<updates>
@@ -87,13 +87,13 @@
<element>mokogitea</element>
<type>application</type>
<client>site</client>
<version>05.14.00</version>
<creationDate>2026-05-31</creationDate>
<version>05.05.00</version>
<creationDate>2026-05-30</creationDate>
<infourl title='MokoGitea'>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/stable</infourl>
<downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/stable/mokogitea-05.14.00.zip</downloadurl>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/stable/mokogitea-05.05.00.zip</downloadurl>
</downloads>
<sha256>bec4bf5a1a841f8e72d9826451004db5d8afc70144231dfedc7fb01a6695955c</sha256>
<sha256>4fee9eb03e4b819a63bce2ceb54fdce0d3eb8bf5b31460fcc42e5ecd75cc856e</sha256>
<tags><tag>stable</tag></tags>
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md</changelogurl>
<maintainer>Moko Consulting</maintainer>
+3 -3
View File
@@ -5,7 +5,6 @@ import {showErrorToast} from '../modules/toast.ts';
import {sleep} from '../utils.ts';
import RepoActivityTopAuthors from '../components/RepoActivityTopAuthors.vue';
import {createApp} from 'vue';
import {toOriginUrl} from '../utils/url.ts';
import {createTippy} from '../modules/tippy.ts';
import {localUserSettings} from '../modules/user-settings.ts';
@@ -79,7 +78,8 @@ function initCloneSchemeUrlSelection(parent: Element) {
const isTea = scheme === 'tea';
if (tabHttps) {
tabHttps.textContent = window.origin.split(':')[0].toUpperCase(); // show "HTTP" or "HTTPS"
const link = tabHttps.getAttribute('data-link')!;
tabHttps.textContent = link.split(':')[0].toUpperCase(); // show "HTTP" or "HTTPS"
tabHttps.classList.toggle('active', isHttps);
}
if (tabSsh) {
@@ -99,7 +99,7 @@ function initCloneSchemeUrlSelection(parent: Element) {
}
if (!tab) return;
const link = toOriginUrl(tab.getAttribute('data-link')!);
const link = tab.getAttribute('data-link')!;
for (const el of document.querySelectorAll('.js-clone-url')) {
if (el.nodeName === 'INPUT') {
+1 -17
View File
@@ -1,4 +1,4 @@
import {linkifyURLs, pathEscape, pathEscapeSegments, toOriginUrl, urlQueryEscape} from './url.ts';
import {linkifyURLs, pathEscape, pathEscapeSegments, urlQueryEscape} from './url.ts';
describe('escape', () => {
const queryNonAscii = " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
@@ -45,19 +45,3 @@ test('linkifyURLs', () => {
expect(linkifyURLs('https://evil.com/\nonclick=alert(1)')).toEqual(`${link('https://evil.com/')}\nonclick=alert(1)`);
expect(linkifyURLs('https://evil.com/&#34;onmouseover=alert(1)')).toEqual(`${link('https://evil.com/&#34;onmouseover=alert')}(1)`);
});
test('toOriginUrl', () => {
const oldLocation = String(window.location);
for (const origin of ['https://example.com', 'https://example.com:3000']) {
window.location.assign(`${origin}/`);
expect(toOriginUrl('/')).toEqual(`${origin}/`);
expect(toOriginUrl('/org/repo.git')).toEqual(`${origin}/org/repo.git`);
expect(toOriginUrl('https://another.com')).toEqual(`${origin}/`);
expect(toOriginUrl('https://another.com/')).toEqual(`${origin}/`);
expect(toOriginUrl('https://another.com/org/repo.git')).toEqual(`${origin}/org/repo.git`);
expect(toOriginUrl('https://another.com:4000')).toEqual(`${origin}/`);
expect(toOriginUrl('https://another.com:4000/')).toEqual(`${origin}/`);
expect(toOriginUrl('https://another.com:4000/org/repo.git')).toEqual(`${origin}/org/repo.git`);
}
window.location.assign(oldLocation);
});
-16
View File
@@ -57,19 +57,3 @@ export function linkifyURLs(html: string): string {
return `<a href="${cleanUrl}" target="_blank">${cleanUrl}</a>${trailing}`; // eslint-disable-line github/unescaped-html-literal
});
}
/** Convert an absolute or relative URL to an absolute URL with the current origin. It only
* processes absolute HTTP/HTTPS URLs or relative URLs like '/xxx' or '//host/xxx'. */
export function toOriginUrl(urlStr: string) {
try {
if (urlStr.startsWith('http://') || urlStr.startsWith('https://') || urlStr.startsWith('/')) {
const {origin, protocol, hostname, port} = window.location;
const url = new URL(urlStr, origin);
url.protocol = protocol;
url.hostname = hostname;
url.port = port || (protocol === 'https:' ? '443' : '80');
return url.toString();
}
} catch {}
return urlStr;
}