Compare commits

..

30 Commits

Author SHA1 Message Date
jmiller bdac121b70 docs(wiki): normalize in-repo wiki branding (MokoGitea/mokocli/MokoStandards->org wiki/MokoSuite)
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 1m50s
2026-07-05 22:01:47 +00:00
jmiller 80a4c68063 chore: sync workflow-sync-trigger.yml from Template-Generic [skip ci] 2026-07-05 21:46:31 +00:00
jmiller 5c0bf5f214 chore: sync version-set.yml from Template-Generic [skip ci] 2026-07-05 21:46:18 +00:00
jmiller e2f8d5ce9b chore: sync repo-health.yml from Template-Generic [skip ci] 2026-07-05 21:46:08 +00:00
jmiller 1d5427f2c2 chore: sync rc-revert.yml from Template-Generic [skip ci] 2026-07-05 21:45:58 +00:00
jmiller 9d949b6294 chore: sync pre-release.yml from Template-Generic [skip ci] 2026-07-05 21:45:45 +00:00
jmiller 515e8fcdea chore: sync pr-check.yml from Template-Generic [skip ci] 2026-07-05 21:45:34 +00:00
jmiller 519966015b chore: sync notify.yml from Template-Generic [skip ci] 2026-07-05 21:45:24 +00:00
jmiller 2677caafd6 chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-07-05 21:45:14 +00:00
jmiller 66148f76fa chore: sync gitleaks.yml from Template-Generic [skip ci] 2026-07-05 21:45:05 +00:00
jmiller 7a4be6ab63 chore: sync cleanup.yml from Template-Generic [skip ci] 2026-07-05 21:44:56 +00:00
jmiller c26ceaea87 chore: sync ci-issue-reporter.yml from Template-Generic [skip ci] 2026-07-05 21:44:50 +00:00
jmiller 8ba2dbf3e3 chore: sync cascade-dev.yml from Template-Generic [skip ci] 2026-07-05 21:42:28 +00:00
jmiller 44b25099ac chore: sync branch-cleanup.yml from Template-Generic [skip ci] 2026-07-05 21:42:21 +00:00
jmiller 838d1f0d28 chore: sync auto-release.yml from Template-Generic [skip ci] 2026-07-05 21:42:15 +00:00
jmiller 6e0db30b3e chore: sync auto-bump.yml from Template-Generic [skip ci] 2026-07-05 21:42:09 +00:00
jmiller e2343c8fb4 chore: sync workflow-sync-trigger.yml from Template-Generic [skip ci] 2026-07-05 21:09:03 +00:00
jmiller 8e045c60fc chore: sync pr-check.yml from Template-Generic [skip ci] 2026-07-05 21:08:49 +00:00
jmiller d3a0c7534b chore: sync version-set.yml from Template-Generic [skip ci] 2026-07-05 20:43:30 +00:00
jmiller 7f0da28988 chore: sync sync-on-merge.yml from Template-Generic [skip ci] 2026-07-05 20:43:14 +00:00
jmiller 6cd1d10617 chore: sync repo-health.yml from Template-Generic [skip ci] 2026-07-05 20:42:59 +00:00
jmiller 5c8056f15e chore: sync rc-revert.yml from Template-Generic [skip ci] 2026-07-05 20:42:42 +00:00
jmiller ffaa3662e6 chore: sync pre-release.yml from Template-Generic [skip ci] 2026-07-05 20:42:24 +00:00
jmiller 1c462f9d49 chore: sync pr-check.yml from Template-Generic [skip ci] 2026-07-05 20:42:05 +00:00
jmiller 8fbf97ac7e chore: sync gitleaks.yml from Template-Generic [skip ci] 2026-07-05 20:41:46 +00:00
jmiller b3448f4d62 chore: sync ci-issue-reporter.yml from Template-Generic [skip ci] 2026-07-05 20:41:21 +00:00
jmiller f72300a03f chore: sync cascade-dev.yml from Template-Generic [skip ci] 2026-07-05 20:40:53 +00:00
jmiller 08a43ec718 chore: sync branch-cleanup.yml from Template-Generic [skip ci] 2026-07-05 20:40:04 +00:00
jmiller bf2b299f87 chore: sync auto-release.yml from Template-Generic [skip ci] 2026-07-05 20:39:44 +00:00
jmiller 89aa805967 chore: sync auto-bump.yml from Template-Generic [skip ci] 2026-07-05 20:39:24 +00:00
62 changed files with 258 additions and 2156 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: Feature Request
about: Suggest a new feature or enhancement
title: '(feat) '
title: '[FEATURE] '
labels: 'enhancement'
assignees: ''
+5 -4
View File
@@ -3,9 +3,9 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# DEFGROUP: MokoGitea.Workflow
# INGROUP: mokocli.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.mokogitea/workflows/auto-bump.yml
# VERSION: 09.02.00
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
@@ -34,7 +34,8 @@ jobs:
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')
!startsWith(github.event.head_commit.message, 'Merge pull request') &&
!startsWith(github.event.repository.name, 'Template-')
steps:
- name: Checkout
@@ -52,7 +53,7 @@ jobs:
echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV"
else
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/MokoCLI.git" \
/tmp/mokocli
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV"
+11 -19
View File
@@ -3,10 +3,10 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# DEFGROUP: MokoGitea.Workflow
# INGROUP: mokocli.Release
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
# PATH: /templates/workflows/universal/auto-release.yml.template
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.mokogitea/workflows/auto-release.yml
# VERSION: 05.01.00
# BRIEF: Universal build & release detects platform from manifest.xml
#
@@ -142,8 +142,8 @@ jobs:
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 config --local user.email "mokogitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "mokogitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- name: Publish RC release
@@ -214,8 +214,8 @@ jobs:
- 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 config --local user.email "mokogitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "mokogitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- name: Check for merge conflict markers
@@ -426,7 +426,7 @@ jobs:
&& echo "main branch pushed to GitHub mirror" \
|| echo "WARNING: GitHub mirror push failed"
- name: "Step 11: Delete rc branch and recreate dev from main"
- name: "Step 11: Delete rc branch (dev reset moved to cascade-dev.yml)"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
@@ -438,17 +438,9 @@ jobs:
"${API_BASE}/branches/rc" 2>/dev/null \
&& echo "Deleted rc branch" || echo "rc branch not found"
# Delete dev branch
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
# Recreate dev from main (now includes version bump + changelog promotion)
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API_BASE}/branches" \
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
# dev is reset from main by the dedicated "Cascade Main -> Dev" workflow
# (cascade-dev.yml), which runs after this release completes.
echo "rc cleaned; dev reset handled by cascade-dev.yml" >> $GITHUB_STEP_SUMMARY
- name: "Step 12: Create version branch from main"
if: steps.version.outputs.skip != 'true'
+2 -2
View File
@@ -3,9 +3,9 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# DEFGROUP: MokoGitea.Workflow
# INGROUP: MokoStandards.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.mokogitea/workflows/branch-cleanup.yml
# VERSION: 01.00.00
# BRIEF: Delete feature branches after PR merge
+103 -7
View File
@@ -1,10 +1,106 @@
# DISABLED — auto-release Step 11 recreates dev from main after every release.
# Cascade-dev is redundant and causes version conflicts when both main and dev
# have different version numbers in templateDetails.xml / manifest.xml.
name: "Cascade Main → Dev (DISABLED)"
on: workflow_dispatch
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow
# INGROUP: MokoStandards.Cascade
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.mokogitea/workflows/cascade-dev.yml
# VERSION: 02.00.00
# BRIEF: Cascade main -> dev via PR; auto-merge only if conflict-free, else notify
name: "Cascade Main -> Dev"
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: write
pull-requests: write
env:
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
# ntfy destination is configured via repo or org variables (org vars are inherited).
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
NTFY_TOPIC: ${{ vars.CASCADE_NTFY_TOPIC || vars.NTFY_TOPIC || 'gitea-releases' }}
jobs:
noop:
cascade:
name: Cascade main -> dev
runs-on: ubuntu-latest
steps:
- run: echo "Cascade disabled — auto-release handles dev recreation"
- name: Open main -> dev PR (auto-merge if clean, else notify)
env:
TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
REPO: ${{ github.repository }}
run: |
set -uo pipefail
API="${MOKOGITEA_URL}/api/v1/repos/${REPO}"
AUTH="Authorization: token ${TOKEN}"
jqnum() { python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$1',''))" 2>/dev/null; }
# 0. dev must exist
if ! curl -sf -H "$AUTH" "${API}/branches/dev" >/dev/null 2>&1; then
echo "No dev branch - nothing to cascade."; exit 0
fi
# 1. is main ahead of dev?
AHEAD=$(curl -sf -H "$AUTH" "${API}/compare/dev...main" \
| python3 -c "import sys,json; print(json.load(sys.stdin).get('total_commits',0))" 2>/dev/null || echo 0)
if [ "${AHEAD:-0}" -eq 0 ]; then
echo "dev already up to date with main."; exit 0
fi
echo "main is ${AHEAD} commit(s) ahead of dev."
# 2. reuse an open main->dev PR, else create one
PR=$(curl -sf -H "$AUTH" "${API}/pulls?state=open&base=dev" \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(next((str(p['number']) for p in d if p.get('head',{}).get('ref')=='main'), ''))" 2>/dev/null || echo "")
if [ -z "$PR" ]; then
RESP=$(curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST "${API}/pulls" \
-d '{"head":"main","base":"dev","title":"chore(sync): cascade main -> dev","body":"Automated cascade of main into dev. Auto-merges only if conflict-free; otherwise left open for manual resolution."}')
PR=$(printf '%s' "$RESP" | jqnum number)
if [ -z "$PR" ]; then
echo "::warning::Could not open cascade PR: $RESP"; exit 0
fi
echo "Opened cascade PR #${PR}"
else
echo "Reusing open cascade PR #${PR}"
fi
# 3. wait for MokoGitea to compute mergeability (conflict detection)
MERGEABLE=""
for _ in 1 2 3 4 5 6; do
MERGEABLE=$(curl -sf -H "$AUTH" "${API}/pulls/${PR}" | jqnum mergeable)
case "$MERGEABLE" in True|False) break ;; esac
sleep 3
done
echo "mergeable=${MERGEABLE}"
notify() {
curl -sS \
-H "Title: ${REPO}: dev cascade needs manual merge" \
-H "Tags: warning,twisted_rightwards_arrows" \
-H "Priority: high" \
-H "Click: ${MOKOGITEA_URL}/${REPO}/pulls/${PR}" \
-d "main -> dev cascade PR #${PR} $1 It was NOT auto-merged; resolve it manually." \
"${NTFY_URL}/${NTFY_TOPIC}" || true
}
# 4. auto-merge only if conflict-free; otherwise notify
if [ "$MERGEABLE" = "True" ]; then
CODE=$(curl -s -o /tmp/merge.json -w "%{http_code}" -H "$AUTH" -H "Content-Type: application/json" \
-X POST "${API}/pulls/${PR}/merge" -d '{"Do":"merge","merge_when_checks_succeed":true}')
if [ "$CODE" -ge 200 ] && [ "$CODE" -lt 300 ]; then
echo "Cascade PR #${PR} merged (or scheduled to merge when checks pass)."
else
echo "::warning::Auto-merge returned HTTP ${CODE}: $(cat /tmp/merge.json)"
notify "could not be auto-merged (HTTP ${CODE})."
fi
else
echo "::warning::Cascade PR #${PR} has conflicts (mergeable=${MERGEABLE}); sending notification."
notify "has conflicts and cannot be merged automatically."
fi
+3 -3
View File
@@ -3,12 +3,12 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# DEFGROUP: MokoGitea.Workflow
# INGROUP: mokocli.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.mokogitea/workflows/ci-issue-reporter.yml
# VERSION: 01.00.00
# BRIEF: Reusable workflow — creates/updates a Gitea issue when a CI gate fails.
# BRIEF: Reusable workflow — creates/updates a MokoGitea issue when a CI gate fails.
# Clones MokoCLI and runs cli/ci_issue_reporter.sh.
name: "Universal: CI Issue Reporter"
+1 -1
View File
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# DEFGROUP: MokoGitea.Workflow
# INGROUP: MokoStandards.Maintenance
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.mokogitea/workflows/cleanup.yml
+3 -3
View File
@@ -3,10 +3,10 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# DEFGROUP: MokoGitea.Workflow
# INGROUP: MokoStandards.Security
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /templates/workflows/gitleaks.yml.template
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
# PATH: /.mokogitea/workflows/gitleaks.yml
# VERSION: 01.00.00
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
#
+1 -1
View File
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# DEFGROUP: MokoGitea.Workflow
# INGROUP: mokocli.Automation
# VERSION: 01.00.00
# BRIEF: Auto-create feature branch when an issue is opened
+1 -1
View File
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# DEFGROUP: MokoGitea.Workflow
# INGROUP: MokoStandards.Notifications
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.mokogitea/workflows/notify.yml
+79 -5
View File
@@ -3,10 +3,10 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# DEFGROUP: MokoGitea.Workflow
# INGROUP: mokocli.CI
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
# PATH: /templates/workflows/universal/pr-check.yml.template
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.mokogitea/workflows/pr-check.yml
# VERSION: 09.23.00
# BRIEF: PR gate — branch policy + code validation before merge
@@ -97,6 +97,80 @@ jobs:
echo "Branch policy: OK (${HEAD} → ${BASE})"
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
# ── Docs Update Gate (main PRs) ─────────────────────────────────────────
require-docs:
name: Require Docs Update
runs-on: ubuntu-latest
# Enforce only on PRs merging into main: README.md + CHANGELOG.md must both be updated.
if: ${{ github.base_ref == 'main' }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Require README.md and CHANGELOG.md in the PR diff
run: |
BASE="${{ github.event.pull_request.base.sha }}"
HEAD="${{ github.event.pull_request.head.sha }}"
CHANGED="$(git diff --name-only "$BASE" "$HEAD" 2>/dev/null || true)"
if [ -z "$CHANGED" ]; then
git fetch -q origin "${{ github.base_ref }}" 2>/dev/null || true
CHANGED="$(git diff --name-only "origin/${{ github.base_ref }}...HEAD" 2>/dev/null || true)"
fi
echo "Changed files in PR:"
echo "$CHANGED"
MISSING=""
echo "$CHANGED" | grep -qxE 'README\.md' || MISSING="README.md"
echo "$CHANGED" | grep -qxE 'CHANGELOG\.md' || MISSING="${MISSING:+$MISSING, }CHANGELOG.md"
if [ -n "$MISSING" ]; then
echo "::error::PRs into main must update: ${MISSING}"
{
echo "## Docs Update Required"
echo ""
echo "PRs merging into \`main\` must update both **README.md** and **CHANGELOG.md**."
echo ""
echo "Not updated in this PR: **${MISSING}**"
} >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
echo "Docs update present (README.md + CHANGELOG.md)"
echo "## Docs Update: Passed" >> "$GITHUB_STEP_SUMMARY"
# ── Wiki Update Reminder (main PRs, non-blocking) ───────────────────────
wiki-reminder:
name: Wiki Update Reminder
runs-on: ubuntu-latest
if: ${{ github.base_ref == 'main' }}
steps:
- name: Remind to update the wiki
env:
TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
SERVER: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
REPO: ${{ github.repository }}
PR: ${{ github.event.pull_request.number }}
run: |
set -uo pipefail
{
echo "## Wiki Update Reminder"
echo ""
echo "Docs are **wiki-first** at MokoConsulting. If this change affects behavior, usage, configuration, or standards, update the repo wiki:"
echo ""
echo "- ${SERVER}/${REPO}/wiki"
echo ""
echo "_Non-blocking reminder._"
} >> "$GITHUB_STEP_SUMMARY"
# Post a single PR comment (idempotent via hidden marker); best-effort, never fails.
API="${SERVER}/api/v1/repos/${REPO}/issues/${PR}/comments"
if [ -n "${TOKEN:-}" ] && [ -n "${PR:-}" ]; then
existing="$(curl -sf -H "Authorization: token ${TOKEN}" "$API" 2>/dev/null | grep -c 'wiki-reminder' || true)"
if [ "${existing:-0}" -eq 0 ]; then
curl -sf -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" -X POST "$API" \
-d '{"body":"<!-- wiki-reminder -->\n\n**Wiki reminder:** docs are wiki-first -- if this PR changes behavior, usage, config, or standards, please update the repo wiki before/after merge. _(non-blocking)_"}' >/dev/null 2>&1 || true
fi
fi
echo "Wiki reminder emitted (non-blocking)."
# ── Secret Scanning ──────────────────────────────────────────────────
gitleaks:
name: Secret Scan
@@ -276,7 +350,7 @@ jobs:
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)"
echo "::warning::No Joomla manifest found (MokoSuite site)"
exit 0
fi
echo "Manifest: ${MANIFEST}"
@@ -289,7 +363,7 @@ jobs:
# Block legacy raw/branch update server URLs on MokoGitea
RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true)
if [ -n "$RAW_URLS" ]; then
echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the Gitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)"
echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the MokoGitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)"
echo "$RAW_URLS"
exit 1
fi
+6 -6
View File
@@ -3,10 +3,10 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# DEFGROUP: MokoGitea.Workflow
# INGROUP: mokocli.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /templates/workflows/universal/pre-release.yml.template
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.mokogitea/workflows/pre-release.yml
# VERSION: 05.02.00
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
@@ -84,7 +84,7 @@ jobs:
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
fi
rm -rf /tmp/mokocli
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoCLI.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
@@ -156,8 +156,8 @@ jobs:
fi
# Commit version bump
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git config --local user.email "mokogitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "mokogitea-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 || {
+4 -3
View File
@@ -3,9 +3,9 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# DEFGROUP: MokoGitea.Workflow
# INGROUP: mokocli.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.mokogitea/workflows/rc-revert.yml
# VERSION: 09.23.00
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
@@ -25,7 +25,8 @@ jobs:
runs-on: ubuntu-latest
if: >-
github.event.pull_request.merged == false &&
startsWith(github.event.pull_request.head.ref, 'rc/')
startsWith(github.event.pull_request.head.ref, 'rc/') &&
!startsWith(github.event.repository.name, 'Template-')
steps:
- name: Rename branch
+4 -4
View File
@@ -6,10 +6,10 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# DEFGROUP: MokoGitea.Workflow
# INGROUP: mokocli.Validation
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
# PATH: /templates/workflows/joomla/repo_health.yml.template
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.mokogitea/workflows/repo-health.yml
# VERSION: 09.23.00
# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts.
# ============================================================================
@@ -88,7 +88,7 @@ jobs:
# Hardcoded authorized users — always allowed
case "$ACTOR" in
jmiller|gitea-actions[bot])
jmiller|mokogitea-actions[bot])
ALLOWED=true
PERMISSION=admin
METHOD="hardcoded allowlist"
+2 -1
View File
@@ -10,11 +10,12 @@ on:
jobs:
sync:
runs-on: ubuntu-latest
if: ${{ startsWith(github.event.repository.name, 'Template-') }}
steps:
- name: Checkout mokocli
uses: actions/checkout@v4
with:
repository: MokoConsulting/mokocli
repository: MokoConsulting/MokoCLI
token: ${{ secrets.MOKOGITEA_TOKEN }}
- name: Setup PHP
+2 -1
View File
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow.Template
# DEFGROUP: MokoGitea.Workflow.Template
# INGROUP: MokoStandards.CI
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
# PATH: /.mokogitea/workflows/version-set.yml
@@ -34,6 +34,7 @@ jobs:
set-version:
name: Set Version to ${{ inputs.version }}
runs-on: ubuntu-latest
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
steps:
- name: Validate version format
@@ -3,9 +3,9 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# DEFGROUP: MokoGitea.Workflow
# INGROUP: mokocli.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.mokogitea/workflows/workflow-sync-trigger.yml
# VERSION: 01.01.00
# BRIEF: Trigger workflow sync to live repos when a PR is merged to main
@@ -27,9 +27,10 @@ jobs:
name: Sync workflows to live repos
runs-on: ubuntu-latest
if: >-
github.event_name == 'workflow_dispatch' ||
startsWith(github.event.repository.name, 'Template-') &&
(github.event_name == 'workflow_dispatch' ||
(github.event.pull_request.merged == true &&
!contains(github.event.pull_request.title, '[skip sync]'))
!contains(github.event.pull_request.title, '[skip sync]')))
steps:
- name: Determine platform from repo name
@@ -52,7 +53,7 @@ jobs:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: |
MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
git clone --depth 1 "${MOKOGITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
git clone --depth 1 "${MOKOGITEA_URL}/MokoConsulting/MokoCLI.git" /tmp/mokocli
- name: Install PHP
run: |
-7
View File
@@ -3,12 +3,6 @@
## [Unreleased]
### Added
- Org branch protection: repositories now show the inherited organization rules read-only in their Branch Protection settings, with an expandable detail (direct push, force-push, branch deletion, merge restrictions, required approvals, status checks, protected files, and whitelisted teams) — like GitHub surfaces org rulesets in a repo (#727)
- Org branch protection: org-level rules can now also protect against branch deletion (`enable_delete` + delete allowlist teams), mirroring the per-repo delete allowlist (#727)
- Org-level tag protection: protect tag patterns org-wide (e.g. `v*`) with a team allowlist, layered on top of each repo's own protected tags — a tag is controllable only if allowed at both levels (fail-closed). API at `/orgs/{org}/tag_protections`; enforced at the git push/delete hook and the release create/delete paths; shown read-only in the repo Tag settings (#727)
- Org-level push policy: one policy per org, enforced in the pre-receive hook across all its repositories — branch/tag name conventions (glob), a mandatory secret-scanning block-on-push that repos cannot disable, a max pushed-file size, and blocked file-path patterns. API at `/orgs/{org}/push_policy`. Naming is fail-closed; the content checks (blocked paths, max size) fail open on error so a policy bug can never block every push (#727)
- Org-level repository defaults: an org can force new/transferred repositories private and set default pull-request settings (allowed merge styles, default merge style, auto-delete branch after merge), applied via a notifier when a repo is created in or transferred into the org (best-effort — never blocks repo creation). API at `/orgs/{org}/repo_defaults` (#727)
- Org-level email domain policy: restrict which email domains an organization's members may have — a user can only be added to the org (via any team) if their primary email matches one of the allowed domain globs. Enforced at the single membership-add choke point (`AddTeamMember`); API at `/orgs/{org}/email_domain_policy` (#727)
- Code security scanner: pattern-based detection of SQL injection, XSS, command injection, path traversal, insecure deserialization, hardcoded credentials, and weak cryptography across Go/PHP/Python/JS/TS (#552)
- Cascade merge: auto-create PRs to downstream branches after merge with configurable rules per repo (#460)
- Issue status presets: 4 built-in templates (default, software-development, support-tickets, bug-tracking) with API + web UI (#507)
@@ -63,7 +57,6 @@
- Cherry-pick upstream v1.26.4: walk git log context error handling — regression fix (#38185)
### Fixed
- Org-level branch protection now **layers** with per-repo rules instead of being ignored whenever a repo rule exists. When both an org rule and a repo rule match a branch, the effective rule is the most-restrictive (fail-closed) combination — the org rule is a mandatory floor a repo cannot weaken: allow flags AND'd, gate/require/block flags OR'd, required approvals max'd, status checks and protected-file patterns unioned, whitelists intersected. Previously a repo rule shadowed the org rule entirely at the enforcement choke point (`GetFirstMatchProtectedBranchRule`), letting a repo opt out of org protection (#727)
- Org Teams page: list now renders — the handler wrote `ctx.Data["OrgListTeams"]` but the template reads `.Teams`, so the page showed header/nav but no teams (#720)
- Issue type: now editable after creation for users with issue write permission — the sidebar gated editing on a `FieldEditFlags` data key that was never populated (always read-only); now uses `HasIssuesOrPullsWritePermission` like the priority field (#721)
- Admin config form: radio inputs (e.g. instance landing page Mode) no longer throw "Unsupported config form value mapping", which had aborted all JS init on the admin settings page
-134
View File
@@ -1,134 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package git
import (
"context"
"fmt"
"strings"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/glob"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
"xorm.io/builder"
)
// OrgEmailDomainPolicy restricts which email domains an organization's members may
// have. When configured, a user can only be added to the org if their primary email
// matches one of the allowed domain globs. At most one row per org. See issue #727.
type OrgEmailDomainPolicy struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"UNIQUE NOT NULL"`
AllowedDomains string `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
func init() {
db.RegisterModel(new(OrgEmailDomainPolicy))
}
// ErrEmailDomainNotAllowed is returned when a user's email domain is not permitted
// by the organization's email domain policy.
type ErrEmailDomainNotAllowed struct {
Email string
OrgID int64
}
func (e ErrEmailDomainNotAllowed) Error() string {
return fmt.Sprintf("email %q is not in an allowed domain for organization %d", e.Email, e.OrgID)
}
// IsErrEmailDomainNotAllowed reports whether err is an ErrEmailDomainNotAllowed.
func IsErrEmailDomainNotAllowed(err error) bool {
_, ok := err.(ErrEmailDomainNotAllowed)
return ok
}
func (p *OrgEmailDomainPolicy) domainGlobs() []glob.Glob {
var out []glob.Glob
for _, d := range strings.Split(p.AllowedDomains, ";") {
d = strings.TrimSpace(strings.ToLower(d))
if d == "" {
continue
}
if g, err := glob.Compile(d); err == nil {
out = append(out, g)
} else {
log.Warn("Invalid org email domain glob %q: %v", d, err)
}
}
return out
}
// EmailAllowed reports whether email's domain satisfies the policy. An empty policy
// (no configured domains) allows any email.
func (p *OrgEmailDomainPolicy) EmailAllowed(email string) bool {
globs := p.domainGlobs()
if len(globs) == 0 {
return true
}
at := strings.LastIndexByte(email, '@')
if at < 0 {
return false
}
domain := strings.ToLower(email[at+1:])
for _, g := range globs {
if g.Match(domain) {
return true
}
}
return false
}
// GetOrgEmailDomainPolicy returns the org's email domain policy, or nil if none.
func GetOrgEmailDomainPolicy(ctx context.Context, orgID int64) (*OrgEmailDomainPolicy, error) {
policy, exist, err := db.Get[OrgEmailDomainPolicy](ctx, builder.Eq{"org_id": orgID})
if err != nil {
return nil, err
} else if !exist {
return nil, nil //nolint:nilnil
}
return policy, nil
}
// OrgEmailDomainAllowed reports whether email is permitted for the org. It returns
// true when the org has no policy configured.
func OrgEmailDomainAllowed(ctx context.Context, orgID int64, email string) (bool, error) {
policy, err := GetOrgEmailDomainPolicy(ctx, orgID)
if err != nil {
return false, err
}
if policy == nil {
return true, nil
}
return policy.EmailAllowed(email), nil
}
// UpsertOrgEmailDomainPolicy creates or updates the single policy row for an org.
func UpsertOrgEmailDomainPolicy(ctx context.Context, policy *OrgEmailDomainPolicy) error {
existing, err := GetOrgEmailDomainPolicy(ctx, policy.OrgID)
if err != nil {
return err
}
if existing == nil {
if _, err := db.GetEngine(ctx).Insert(policy); err != nil {
return fmt.Errorf("Insert OrgEmailDomainPolicy: %v", err)
}
return nil
}
policy.ID = existing.ID
if _, err := db.GetEngine(ctx).ID(existing.ID).AllCols().Update(policy); err != nil {
return fmt.Errorf("Update OrgEmailDomainPolicy: %v", err)
}
return nil
}
// DeleteOrgEmailDomainPolicy removes an org's email domain policy.
func DeleteOrgEmailDomainPolicy(ctx context.Context, orgID int64) error {
_, err := db.GetEngine(ctx).Where("org_id = ?", orgID).Delete(new(OrgEmailDomainPolicy))
return err
}
-6
View File
@@ -33,9 +33,6 @@ type OrgProtectedBranch struct {
CanForcePush bool `xorm:"NOT NULL DEFAULT false"`
EnableForcePushAllowlist bool `xorm:"NOT NULL DEFAULT false"`
ForcePushAllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
CanDelete bool `xorm:"NOT NULL DEFAULT false"`
EnableDeleteAllowlist bool `xorm:"NOT NULL DEFAULT false"`
DeleteAllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"`
StatusCheckContexts []string `xorm:"JSON TEXT"`
RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"`
@@ -99,9 +96,6 @@ func (o *OrgProtectedBranch) ToProtectedBranch() *ProtectedBranch {
CanForcePush: o.CanForcePush,
EnableForcePushAllowlist: o.EnableForcePushAllowlist,
ForcePushAllowlistTeamIDs: o.ForcePushAllowlistTeamIDs,
CanDelete: o.CanDelete,
EnableDeleteAllowlist: o.EnableDeleteAllowlist,
DeleteAllowlistTeamIDs: o.DeleteAllowlistTeamIDs,
EnableStatusCheck: o.EnableStatusCheck,
StatusCheckContexts: o.StatusCheckContexts,
RequiredApprovals: o.RequiredApprovals,
-133
View File
@@ -1,133 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package git
import (
"context"
"fmt"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
"xorm.io/builder"
)
// OrgProtectedTag represents an org-level tag protection rule. It cascades to all
// repositories in the organization and layers on top of each repo's own protected
// tags (a tag is controllable only if allowed at both levels). Org rules reference
// teams only (like OrgProtectedBranch). See issue #727.
type OrgProtectedTag struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"UNIQUE(s) index"`
NamePattern string `xorm:"UNIQUE(s)"`
AllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
func init() {
db.RegisterModel(new(OrgProtectedTag))
}
// ToProtectedTag converts an org-level tag rule into a repo-scoped ProtectedTag so
// the standard name-matching and allowlist logic can be reused. Org rules are
// team-only, so the user allowlist is left empty.
func (o *OrgProtectedTag) ToProtectedTag() *ProtectedTag {
return &ProtectedTag{
NamePattern: o.NamePattern,
AllowlistTeamIDs: o.AllowlistTeamIDs,
}
}
// GetOrgProtectedTagByID retrieves a single org tag rule by org ID and rule ID.
func GetOrgProtectedTagByID(ctx context.Context, orgID, id int64) (*OrgProtectedTag, error) {
rule, exist, err := db.Get[OrgProtectedTag](ctx, builder.Eq{"org_id": orgID, "id": id})
if err != nil {
return nil, err
} else if !exist {
return nil, nil //nolint:nilnil
}
return rule, nil
}
// GetOrgProtectedTagByNamePattern retrieves a single org tag rule by its pattern.
func GetOrgProtectedTagByNamePattern(ctx context.Context, orgID int64, pattern string) (*OrgProtectedTag, error) {
rule, exist, err := db.Get[OrgProtectedTag](ctx, builder.Eq{"org_id": orgID, "name_pattern": pattern})
if err != nil {
return nil, err
} else if !exist {
return nil, nil //nolint:nilnil
}
return rule, nil
}
// FindOrgProtectedTags loads all org-level tag protection rules for an organization.
func FindOrgProtectedTags(ctx context.Context, orgID int64) ([]*OrgProtectedTag, error) {
var rules []*OrgProtectedTag
err := db.GetEngine(ctx).Where("org_id = ?", orgID).Asc("created_unix").Find(&rules)
return rules, err
}
// CreateOrgProtectedTag creates a new org-level tag protection rule.
func CreateOrgProtectedTag(ctx context.Context, rule *OrgProtectedTag) error {
if _, err := db.GetEngine(ctx).Insert(rule); err != nil {
return fmt.Errorf("Insert OrgProtectedTag: %v", err)
}
return nil
}
// UpdateOrgProtectedTag updates an existing org-level tag protection rule.
func UpdateOrgProtectedTag(ctx context.Context, rule *OrgProtectedTag) error {
if _, err := db.GetEngine(ctx).ID(rule.ID).AllCols().Update(rule); err != nil {
return fmt.Errorf("Update OrgProtectedTag: %v", err)
}
return nil
}
// DeleteOrgProtectedTag deletes an org-level tag protection rule.
func DeleteOrgProtectedTag(ctx context.Context, orgID, id int64) error {
affected, err := db.GetEngine(ctx).Where("org_id = ? AND id = ?", orgID, id).Delete(new(OrgProtectedTag))
if err != nil {
return err
}
if affected == 0 {
return fmt.Errorf("org tag protection rule ID(%d) not found", id)
}
return nil
}
// IsUserAllowedToControlTagInRepo layers org-level tag rules on top of a repo's own
// protected tags: the user must be allowed by BOTH levels (most-restrictive). The
// repo decision is evaluated first (using the already-loaded repoTags); if it
// allows and the owner is an organization, the org-level rules must also allow.
func IsUserAllowedToControlTagInRepo(ctx context.Context, repoTags []*ProtectedTag, repo *repo_model.Repository, tagName string, userID int64) (bool, error) {
allowed, err := IsUserAllowedToControlTag(ctx, repoTags, tagName, userID)
if err != nil || !allowed {
return allowed, err
}
owner, err := user_model.GetUserByID(ctx, repo.OwnerID)
if err != nil {
return false, err
}
if !owner.IsOrganization() {
return true, nil
}
orgRules, err := FindOrgProtectedTags(ctx, owner.ID)
if err != nil {
return false, err
}
if len(orgRules) == 0 {
return true, nil
}
orgTags := make([]*ProtectedTag, len(orgRules))
for i, r := range orgRules {
orgTags[i] = r.ToProtectedTag()
}
return IsUserAllowedToControlTag(ctx, orgTags, tagName, userID)
}
-130
View File
@@ -1,130 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package git
import (
"context"
"fmt"
"strings"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/glob"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
"xorm.io/builder"
)
// OrgPushPolicy is a single org-wide policy enforced in the pre-receive hook on
// every repository of the organization. Unlike the branch/tag rulesets there is at
// most one policy per org. Empty pattern / zero fields mean "no constraint". See #727.
type OrgPushPolicy struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"UNIQUE NOT NULL"`
BranchNamePattern string `xorm:"TEXT"`
TagNamePattern string `xorm:"TEXT"`
RequireSecretBlock bool `xorm:"NOT NULL DEFAULT false"`
MaxFileSize int64 `xorm:"NOT NULL DEFAULT 0"`
BlockedFilePatterns string `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
func init() {
db.RegisterModel(new(OrgPushPolicy))
}
// nameMatchesPattern reports whether name satisfies a glob pattern. An empty pattern
// imposes no constraint; an invalid pattern fails open (no constraint) so a
// misconfigured policy never blocks all pushes.
func nameMatchesPattern(pattern, name string) bool {
pattern = strings.TrimSpace(pattern)
if pattern == "" {
return true
}
g, err := glob.Compile(pattern, '/')
if err != nil {
log.Warn("Invalid org push policy name pattern %q: %v", pattern, err)
return true
}
return g.Match(name)
}
// BranchNameAllowed reports whether a branch name satisfies the naming policy.
func (p *OrgPushPolicy) BranchNameAllowed(name string) bool {
return nameMatchesPattern(p.BranchNamePattern, name)
}
// TagNameAllowed reports whether a tag name satisfies the naming policy.
func (p *OrgPushPolicy) TagNameAllowed(name string) bool {
return nameMatchesPattern(p.TagNamePattern, name)
}
// BlockedFileGlobs parses the ';'-separated blocked file pattern list.
func (p *OrgPushPolicy) BlockedFileGlobs() []glob.Glob {
var out []glob.Glob
for _, expr := range strings.Split(p.BlockedFilePatterns, ";") {
expr = strings.TrimSpace(strings.ToLower(expr))
if expr == "" {
continue
}
if g, err := glob.Compile(expr, '.', '/'); err == nil {
out = append(out, g)
} else {
log.Warn("Invalid org push policy blocked file pattern %q: %v", expr, err)
}
}
return out
}
// GetOrgPushPolicy returns the org's push policy, or nil if none is configured.
func GetOrgPushPolicy(ctx context.Context, orgID int64) (*OrgPushPolicy, error) {
policy, exist, err := db.Get[OrgPushPolicy](ctx, builder.Eq{"org_id": orgID})
if err != nil {
return nil, err
} else if !exist {
return nil, nil //nolint:nilnil
}
return policy, nil
}
// GetOrgPushPolicyForRepo returns the push policy of the repo's owning organization,
// or nil if the owner is not an organization or has no policy.
func GetOrgPushPolicyForRepo(ctx context.Context, repo *repo_model.Repository) (*OrgPushPolicy, error) {
owner, err := user_model.GetUserByID(ctx, repo.OwnerID)
if err != nil {
return nil, err
}
if !owner.IsOrganization() {
return nil, nil //nolint:nilnil
}
return GetOrgPushPolicy(ctx, owner.ID)
}
// UpsertOrgPushPolicy creates or updates the single push policy for an org.
func UpsertOrgPushPolicy(ctx context.Context, policy *OrgPushPolicy) error {
existing, err := GetOrgPushPolicy(ctx, policy.OrgID)
if err != nil {
return err
}
if existing == nil {
if _, err := db.GetEngine(ctx).Insert(policy); err != nil {
return fmt.Errorf("Insert OrgPushPolicy: %v", err)
}
return nil
}
policy.ID = existing.ID
if _, err := db.GetEngine(ctx).ID(existing.ID).AllCols().Update(policy); err != nil {
return fmt.Errorf("Update OrgPushPolicy: %v", err)
}
return nil
}
// DeleteOrgPushPolicy removes an org's push policy.
func DeleteOrgPushPolicy(ctx context.Context, orgID int64) error {
_, err := db.GetEngine(ctx).Where("org_id = ?", orgID).Delete(new(OrgPushPolicy))
return err
}
-88
View File
@@ -1,88 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package git
import (
"context"
"fmt"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
"xorm.io/builder"
)
// OrgRepoDefaults holds an organization's default repository settings, applied to a
// repository when it is created in or transferred into the org (via a notifier).
// There is at most one row per org. See issue #727.
type OrgRepoDefaults struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"UNIQUE NOT NULL"`
ForcePrivate bool `xorm:"NOT NULL DEFAULT false"`
ApplyPRDefaults bool `xorm:"NOT NULL DEFAULT false"`
AllowMerge bool `xorm:"NOT NULL DEFAULT true"`
AllowRebase bool `xorm:"NOT NULL DEFAULT true"`
AllowRebaseMerge bool `xorm:"NOT NULL DEFAULT true"`
AllowSquash bool `xorm:"NOT NULL DEFAULT true"`
AllowFastForwardOnly bool `xorm:"NOT NULL DEFAULT true"`
DefaultMergeStyle string `xorm:"TEXT"`
DeleteBranchAfterMerge bool `xorm:"NOT NULL DEFAULT false"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
func init() {
db.RegisterModel(new(OrgRepoDefaults))
}
// GetOrgRepoDefaults returns the org's repo defaults, or nil if none are configured.
func GetOrgRepoDefaults(ctx context.Context, orgID int64) (*OrgRepoDefaults, error) {
defaults, exist, err := db.Get[OrgRepoDefaults](ctx, builder.Eq{"org_id": orgID})
if err != nil {
return nil, err
} else if !exist {
return nil, nil //nolint:nilnil
}
return defaults, nil
}
// GetOrgRepoDefaultsForRepo returns the repo-defaults of the repo's owning org, or
// nil if the owner is not an organization or has none configured.
func GetOrgRepoDefaultsForRepo(ctx context.Context, repo *repo_model.Repository) (*OrgRepoDefaults, error) {
owner, err := user_model.GetUserByID(ctx, repo.OwnerID)
if err != nil {
return nil, err
}
if !owner.IsOrganization() {
return nil, nil //nolint:nilnil
}
return GetOrgRepoDefaults(ctx, owner.ID)
}
// UpsertOrgRepoDefaults creates or updates the single repo-defaults row for an org.
func UpsertOrgRepoDefaults(ctx context.Context, defaults *OrgRepoDefaults) error {
existing, err := GetOrgRepoDefaults(ctx, defaults.OrgID)
if err != nil {
return err
}
if existing == nil {
if _, err := db.GetEngine(ctx).Insert(defaults); err != nil {
return fmt.Errorf("Insert OrgRepoDefaults: %v", err)
}
return nil
}
defaults.ID = existing.ID
if _, err := db.GetEngine(ctx).ID(existing.ID).AllCols().Update(defaults); err != nil {
return fmt.Errorf("Update OrgRepoDefaults: %v", err)
}
return nil
}
// DeleteOrgRepoDefaults removes an org's repo defaults.
func DeleteOrgRepoDefaults(ctx context.Context, orgID int64) error {
_, err := db.GetEngine(ctx).Where("org_id = ?", orgID).Delete(new(OrgRepoDefaults))
return err
}
+7 -28
View File
@@ -85,40 +85,19 @@ func FindAllMatchedBranches(ctx context.Context, repoID int64, ruleName string)
return results, nil
}
// GetFirstMatchProtectedBranchRule returns the effective protected-branch rule for a
// branch. It combines the matching repo-level rule with the matching org-level rule
// (when the repo belongs to an organization): if both match they are layered with
// mergeMostRestrictive so the org rule acts as a floor the repo cannot weaken; if
// only one matches that one is returned; if neither matches, nil.
// GetFirstMatchProtectedBranchRule returns the first matched rule.
// It checks repo-level rules first; if none match, it falls back to org-level rules
// (if the repo belongs to an organization).
func GetFirstMatchProtectedBranchRule(ctx context.Context, repoID int64, branchName string) (*ProtectedBranch, error) {
rules, err := FindRepoProtectedBranchRules(ctx, repoID)
if err != nil {
return nil, err
}
repoRule := rules.GetFirstMatched(branchName)
orgRule, err := getFirstMatchOrgProtectedBranchRule(ctx, repoID, branchName)
if err != nil {
return nil, err
if matched := rules.GetFirstMatched(branchName); matched != nil {
return matched, nil
}
switch {
case repoRule == nil && orgRule == nil:
return nil, nil
case orgRule == nil:
return repoRule, nil
case repoRule == nil:
return orgRule, nil
default:
return mergeMostRestrictive(repoRule, orgRule), nil
}
}
// getFirstMatchOrgProtectedBranchRule returns the matching org-level rule for a
// branch expressed as a repo-scoped ProtectedBranch (RepoID set so downstream
// permission checks work), or nil if the repo's owner is not an organization or no
// org rule matches.
func getFirstMatchOrgProtectedBranchRule(ctx context.Context, repoID int64, branchName string) (*ProtectedBranch, error) {
// Fall back to org-level rules
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
if err != nil {
return nil, err
@@ -140,7 +119,7 @@ func getFirstMatchOrgProtectedBranchRule(ctx context.Context, repoID int64, bran
return nil, nil
}
// Convert org rule to a ProtectedBranch with RepoID set so callers work correctly.
// Convert org rule to a ProtectedBranch with RepoID set so callers work correctly
pb := orgRule.ToProtectedBranch()
pb.RepoID = repoID
return pb, nil
-178
View File
@@ -1,178 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package git
import "strings"
// mergeMostRestrictive combines a repo-level and an org-level protected-branch rule
// that both match the same branch into a single effective rule, always applying the
// STRICTER constraint from each side (fail-closed). This makes an org-level rule a
// mandatory floor that a repo rule can only tighten, never weaken. See issue #727.
//
// Combination directions:
// - "Can*" / allow booleans -> AND (an action is allowed only if both allow it)
// - "Enable*/Block*/Require*" -> OR (a gate is on if either side turns it on)
// - RequiredApprovals -> max
// - required-set lists -> union (status contexts, protected files)
// - allow-set lists -> intersection (whitelists, unprotected files)
//
// Identity (ID, RepoID, RuleName, Priority) is taken from the repo rule so that
// downstream permission checks (which LoadRepo via RepoID) keep working.
func mergeMostRestrictive(repoRule, orgRule *ProtectedBranch) *ProtectedBranch {
eff := *repoRule
// Direct push.
eff.CanPush = repoRule.CanPush && orgRule.CanPush
eff.EnableWhitelist, eff.WhitelistUserIDs = mergeAllowlist(repoRule.EnableWhitelist, repoRule.WhitelistUserIDs, orgRule.EnableWhitelist, orgRule.WhitelistUserIDs)
_, eff.WhitelistTeamIDs = mergeAllowlist(repoRule.EnableWhitelist, repoRule.WhitelistTeamIDs, orgRule.EnableWhitelist, orgRule.WhitelistTeamIDs)
eff.WhitelistDeployKeys = repoRule.WhitelistDeployKeys && orgRule.WhitelistDeployKeys
eff.WhitelistActionsUser = repoRule.WhitelistActionsUser && orgRule.WhitelistActionsUser
// Force push.
eff.CanForcePush = repoRule.CanForcePush && orgRule.CanForcePush
eff.EnableForcePushAllowlist, eff.ForcePushAllowlistUserIDs = mergeAllowlist(repoRule.EnableForcePushAllowlist, repoRule.ForcePushAllowlistUserIDs, orgRule.EnableForcePushAllowlist, orgRule.ForcePushAllowlistUserIDs)
_, eff.ForcePushAllowlistTeamIDs = mergeAllowlist(repoRule.EnableForcePushAllowlist, repoRule.ForcePushAllowlistTeamIDs, orgRule.EnableForcePushAllowlist, orgRule.ForcePushAllowlistTeamIDs)
eff.ForcePushAllowlistDeployKeys = repoRule.ForcePushAllowlistDeployKeys && orgRule.ForcePushAllowlistDeployKeys
eff.ForcePushAllowlistActionsUser = repoRule.ForcePushAllowlistActionsUser && orgRule.ForcePushAllowlistActionsUser
// Delete.
eff.CanDelete = repoRule.CanDelete && orgRule.CanDelete
eff.EnableDeleteAllowlist, eff.DeleteAllowlistUserIDs = mergeAllowlist(repoRule.EnableDeleteAllowlist, repoRule.DeleteAllowlistUserIDs, orgRule.EnableDeleteAllowlist, orgRule.DeleteAllowlistUserIDs)
_, eff.DeleteAllowlistTeamIDs = mergeAllowlist(repoRule.EnableDeleteAllowlist, repoRule.DeleteAllowlistTeamIDs, orgRule.EnableDeleteAllowlist, orgRule.DeleteAllowlistTeamIDs)
eff.DeleteAllowlistDeployKeys = repoRule.DeleteAllowlistDeployKeys && orgRule.DeleteAllowlistDeployKeys
eff.DeleteAllowlistActionsUser = repoRule.DeleteAllowlistActionsUser && orgRule.DeleteAllowlistActionsUser
// Merge whitelist.
eff.EnableMergeWhitelist, eff.MergeWhitelistUserIDs = mergeAllowlist(repoRule.EnableMergeWhitelist, repoRule.MergeWhitelistUserIDs, orgRule.EnableMergeWhitelist, orgRule.MergeWhitelistUserIDs)
_, eff.MergeWhitelistTeamIDs = mergeAllowlist(repoRule.EnableMergeWhitelist, repoRule.MergeWhitelistTeamIDs, orgRule.EnableMergeWhitelist, orgRule.MergeWhitelistTeamIDs)
eff.MergeWhitelistActionsUser = repoRule.MergeWhitelistActionsUser && orgRule.MergeWhitelistActionsUser
// Status checks.
eff.EnableStatusCheck = repoRule.EnableStatusCheck || orgRule.EnableStatusCheck
eff.StatusCheckContexts = unionStrings(repoRule.StatusCheckContexts, orgRule.StatusCheckContexts)
// Approvals and reviews.
eff.RequiredApprovals = maxInt64(repoRule.RequiredApprovals, orgRule.RequiredApprovals)
eff.EnableApprovalsWhitelist, eff.ApprovalsWhitelistUserIDs = mergeAllowlist(repoRule.EnableApprovalsWhitelist, repoRule.ApprovalsWhitelistUserIDs, orgRule.EnableApprovalsWhitelist, orgRule.ApprovalsWhitelistUserIDs)
_, eff.ApprovalsWhitelistTeamIDs = mergeAllowlist(repoRule.EnableApprovalsWhitelist, repoRule.ApprovalsWhitelistTeamIDs, orgRule.EnableApprovalsWhitelist, orgRule.ApprovalsWhitelistTeamIDs)
eff.BlockOnRejectedReviews = repoRule.BlockOnRejectedReviews || orgRule.BlockOnRejectedReviews
eff.BlockOnOfficialReviewRequests = repoRule.BlockOnOfficialReviewRequests || orgRule.BlockOnOfficialReviewRequests
eff.BlockOnOutdatedBranch = repoRule.BlockOnOutdatedBranch || orgRule.BlockOnOutdatedBranch
eff.DismissStaleApprovals = repoRule.DismissStaleApprovals || orgRule.DismissStaleApprovals
eff.IgnoreStaleApprovals = repoRule.IgnoreStaleApprovals || orgRule.IgnoreStaleApprovals
// Commits, files, admin override.
eff.RequireSignedCommits = repoRule.RequireSignedCommits || orgRule.RequireSignedCommits
eff.ProtectedFilePatterns = unionPatterns(repoRule.ProtectedFilePatterns, orgRule.ProtectedFilePatterns)
eff.UnprotectedFilePatterns = intersectPatterns(repoRule.UnprotectedFilePatterns, orgRule.UnprotectedFilePatterns)
eff.BlockAdminMergeOverride = repoRule.BlockAdminMergeOverride || orgRule.BlockAdminMergeOverride
return &eff
}
// mergeAllowlist combines two allow-lists under most-restrictive semantics. An
// allow-list only narrows access when its Enable flag is set; a disabled list means
// "everyone", so it imposes no constraint. Therefore: if both are enabled the result
// is the intersection (a principal must be allowed by both); if only one is enabled
// its list is used as-is; if neither is enabled the list is irrelevant.
func mergeAllowlist(aEnabled bool, aIDs []int64, bEnabled bool, bIDs []int64) (bool, []int64) {
switch {
case aEnabled && bEnabled:
return true, intersectInt64(aIDs, bIDs)
case aEnabled:
return true, aIDs
case bEnabled:
return true, bIDs
default:
return false, nil
}
}
func intersectInt64(a, b []int64) []int64 {
if len(a) == 0 || len(b) == 0 {
return nil
}
set := make(map[int64]struct{}, len(a))
for _, x := range a {
set[x] = struct{}{}
}
var out []int64
for _, x := range b {
if _, ok := set[x]; ok {
out = append(out, x)
}
}
return out
}
func unionStrings(a, b []string) []string {
if len(a) == 0 {
return b
}
if len(b) == 0 {
return a
}
seen := make(map[string]struct{}, len(a)+len(b))
out := make([]string, 0, len(a)+len(b))
for _, s := range a {
if _, ok := seen[s]; !ok {
seen[s] = struct{}{}
out = append(out, s)
}
}
for _, s := range b {
if _, ok := seen[s]; !ok {
seen[s] = struct{}{}
out = append(out, s)
}
}
return out
}
// unionPatterns unions two ';'-separated file-pattern lists (more patterns protected
// = more restrictive).
func unionPatterns(a, b string) string {
return strings.Join(unionStrings(splitPatterns(a), splitPatterns(b)), ";")
}
// intersectPatterns intersects two ';'-separated file-pattern lists. Unprotected
// patterns are carve-outs that REDUCE protection, so the restrictive combination
// keeps only the exemptions present in both.
func intersectPatterns(a, b string) string {
as, bs := splitPatterns(a), splitPatterns(b)
set := make(map[string]struct{}, len(as))
for _, s := range as {
set[s] = struct{}{}
}
seen := make(map[string]struct{}, len(bs))
var out []string
for _, s := range bs {
if _, ok := set[s]; !ok {
continue
}
if _, dup := seen[s]; dup {
continue
}
seen[s] = struct{}{}
out = append(out, s)
}
return strings.Join(out, ";")
}
func splitPatterns(s string) []string {
var out []string
for _, p := range strings.Split(s, ";") {
if p = strings.TrimSpace(p); p != "" {
out = append(out, p)
}
}
return out
}
func maxInt64(a, b int64) int64 {
if a > b {
return a
}
return b
}
-5
View File
@@ -439,11 +439,6 @@ func prepareMigrationTasks() []*migration {
newMigration(359, "Add deploy fields to repo manifest", v1_27.AddDeployFieldsToRepoManifest),
newMigration(360, "Add delete allowlist to protected branch", v1_27.AddDeleteAllowlistToProtectedBranch),
newMigration(361, "Add cascade merge rule table", v1_27.AddCascadeMergeRuleTable),
newMigration(362, "Add delete allowlist to org protected branch", v1_27.AddDeleteAllowlistToOrgProtectedBranch),
newMigration(363, "Add org protected tag table", v1_27.AddOrgProtectedTagTable),
newMigration(364, "Add org push policy table", v1_27.AddOrgPushPolicyTable),
newMigration(365, "Add org repo defaults table", v1_27.AddOrgRepoDefaultsTable),
newMigration(366, "Add org email domain policy table", v1_27.AddOrgEmailDomainPolicyTable),
}
return preparedMigrations
}
-18
View File
@@ -1,18 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import "xorm.io/xorm"
// AddDeleteAllowlistToOrgProtectedBranch adds branch-deletion protection columns to
// org-level branch protection rules, mirroring the per-repo delete allowlist so an
// org rule can also protect branches from deletion. See issue #727.
func AddDeleteAllowlistToOrgProtectedBranch(x *xorm.Engine) error {
type OrgProtectedBranch struct {
CanDelete bool `xorm:"NOT NULL DEFAULT false"`
EnableDeleteAllowlist bool `xorm:"NOT NULL DEFAULT false"`
DeleteAllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
}
return x.Sync(new(OrgProtectedBranch))
}
-25
View File
@@ -1,25 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
"xorm.io/xorm"
)
// AddOrgProtectedTagTable creates the org-level tag protection table. Org tag rules
// cascade to all repositories in the organization and layer on top of each repo's
// own protected tags. See issue #727.
func AddOrgProtectedTagTable(x *xorm.Engine) error {
type OrgProtectedTag struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"UNIQUE(s) index"`
NamePattern string `xorm:"UNIQUE(s)"`
AllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
return x.Sync(new(OrgProtectedTag))
}
-27
View File
@@ -1,27 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
"xorm.io/xorm"
)
// AddOrgPushPolicyTable creates the org-level push policy table (one row per org),
// enforced in the pre-receive hook across all repositories of the org. See #727.
func AddOrgPushPolicyTable(x *xorm.Engine) error {
type OrgPushPolicy struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"UNIQUE NOT NULL"`
BranchNamePattern string `xorm:"TEXT"`
TagNamePattern string `xorm:"TEXT"`
RequireSecretBlock bool `xorm:"NOT NULL DEFAULT false"`
MaxFileSize int64 `xorm:"NOT NULL DEFAULT 0"`
BlockedFilePatterns string `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
return x.Sync(new(OrgPushPolicy))
}
-31
View File
@@ -1,31 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
"xorm.io/xorm"
)
// AddOrgRepoDefaultsTable creates the org repository-defaults table (one row per
// org), applied to repositories created in or transferred into the org. See #727.
func AddOrgRepoDefaultsTable(x *xorm.Engine) error {
type OrgRepoDefaults struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"UNIQUE NOT NULL"`
ForcePrivate bool `xorm:"NOT NULL DEFAULT false"`
ApplyPRDefaults bool `xorm:"NOT NULL DEFAULT false"`
AllowMerge bool `xorm:"NOT NULL DEFAULT true"`
AllowRebase bool `xorm:"NOT NULL DEFAULT true"`
AllowRebaseMerge bool `xorm:"NOT NULL DEFAULT true"`
AllowSquash bool `xorm:"NOT NULL DEFAULT true"`
AllowFastForwardOnly bool `xorm:"NOT NULL DEFAULT true"`
DefaultMergeStyle string `xorm:"TEXT"`
DeleteBranchAfterMerge bool `xorm:"NOT NULL DEFAULT false"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
return x.Sync(new(OrgRepoDefaults))
}
-23
View File
@@ -1,23 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
"xorm.io/xorm"
)
// AddOrgEmailDomainPolicyTable creates the org email-domain policy table (one row
// per org) restricting the email domains of members added to the org. See #727.
func AddOrgEmailDomainPolicyTable(x *xorm.Engine) error {
type OrgEmailDomainPolicy struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"UNIQUE NOT NULL"`
AllowedDomains string `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
return x.Sync(new(OrgEmailDomainPolicy))
}
-9
View File
@@ -17,9 +17,6 @@ type OrgBranchProtection struct {
EnableForcePush bool `json:"enable_force_push"`
EnableForcePushAllowlist bool `json:"enable_force_push_allowlist"`
ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"`
EnableDelete bool `json:"enable_delete"`
EnableDeleteAllowlist bool `json:"enable_delete_allowlist"`
DeleteAllowlistTeams []string `json:"delete_allowlist_teams"`
EnableMergeWhitelist bool `json:"enable_merge_whitelist"`
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
EnableStatusCheck bool `json:"enable_status_check"`
@@ -52,9 +49,6 @@ type CreateOrgBranchProtectionOption struct {
EnableForcePush bool `json:"enable_force_push"`
EnableForcePushAllowlist bool `json:"enable_force_push_allowlist"`
ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"`
EnableDelete bool `json:"enable_delete"`
EnableDeleteAllowlist bool `json:"enable_delete_allowlist"`
DeleteAllowlistTeams []string `json:"delete_allowlist_teams"`
EnableMergeWhitelist bool `json:"enable_merge_whitelist"`
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
EnableStatusCheck bool `json:"enable_status_check"`
@@ -82,9 +76,6 @@ type EditOrgBranchProtectionOption struct {
EnableForcePush *bool `json:"enable_force_push"`
EnableForcePushAllowlist *bool `json:"enable_force_push_allowlist"`
ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"`
EnableDelete *bool `json:"enable_delete"`
EnableDeleteAllowlist *bool `json:"enable_delete_allowlist"`
DeleteAllowlistTeams []string `json:"delete_allowlist_teams"`
EnableMergeWhitelist *bool `json:"enable_merge_whitelist"`
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
EnableStatusCheck *bool `json:"enable_status_check"`
-15
View File
@@ -1,15 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package structs
// OrgEmailDomainPolicy represents an organization's email domain policy
type OrgEmailDomainPolicy struct {
OrgID int64 `json:"org_id"`
AllowedDomains string `json:"allowed_domains"`
}
// EditOrgEmailDomainPolicyOption options for editing an org's email domain policy
type EditOrgEmailDomainPolicyOption struct {
AllowedDomains *string `json:"allowed_domains"`
}
-30
View File
@@ -1,30 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package structs
import "time"
// OrgPushPolicy represents an organization's push policy (one per org)
type OrgPushPolicy struct {
OrgID int64 `json:"org_id"`
BranchNamePattern string `json:"branch_name_pattern"`
TagNamePattern string `json:"tag_name_pattern"`
RequireSecretBlock bool `json:"require_secret_block"`
MaxFileSize int64 `json:"max_file_size"`
BlockedFilePatterns string `json:"blocked_file_patterns"`
// swagger:strfmt date-time
Created time.Time `json:"created_at"`
// swagger:strfmt date-time
Updated time.Time `json:"updated_at"`
}
// EditOrgPushPolicyOption options for editing an organization's push policy. Only
// fields that are set will be changed.
type EditOrgPushPolicyOption struct {
BranchNamePattern *string `json:"branch_name_pattern"`
TagNamePattern *string `json:"tag_name_pattern"`
RequireSecretBlock *bool `json:"require_secret_block"`
MaxFileSize *int64 `json:"max_file_size"`
BlockedFilePatterns *string `json:"blocked_file_patterns"`
}
-32
View File
@@ -1,32 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package structs
// OrgRepoDefaults represents an organization's default repository settings
type OrgRepoDefaults struct {
OrgID int64 `json:"org_id"`
ForcePrivate bool `json:"force_private"`
ApplyPRDefaults bool `json:"apply_pr_defaults"`
AllowMerge bool `json:"allow_merge"`
AllowRebase bool `json:"allow_rebase"`
AllowRebaseMerge bool `json:"allow_rebase_merge"`
AllowSquash bool `json:"allow_squash"`
AllowFastForwardOnly bool `json:"allow_fast_forward_only"`
DefaultMergeStyle string `json:"default_merge_style"`
DeleteBranchAfterMerge bool `json:"delete_branch_after_merge"`
}
// EditOrgRepoDefaultsOption options for editing an org's repo defaults. Only fields
// that are set will be changed.
type EditOrgRepoDefaultsOption struct {
ForcePrivate *bool `json:"force_private"`
ApplyPRDefaults *bool `json:"apply_pr_defaults"`
AllowMerge *bool `json:"allow_merge"`
AllowRebase *bool `json:"allow_rebase"`
AllowRebaseMerge *bool `json:"allow_rebase_merge"`
AllowSquash *bool `json:"allow_squash"`
AllowFastForwardOnly *bool `json:"allow_fast_forward_only"`
DefaultMergeStyle *string `json:"default_merge_style"`
DeleteBranchAfterMerge *bool `json:"delete_branch_after_merge"`
}
-30
View File
@@ -1,30 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package structs
import "time"
// OrgTagProtection represents an org-level tag protection rule
type OrgTagProtection struct {
ID int64 `json:"id"`
OrgID int64 `json:"org_id"`
NamePattern string `json:"name_pattern"`
WhitelistTeams []string `json:"whitelist_teams"`
// swagger:strfmt date-time
Created time.Time `json:"created_at"`
// swagger:strfmt date-time
Updated time.Time `json:"updated_at"`
}
// CreateOrgTagProtectionOption options for creating an org-level tag protection
type CreateOrgTagProtectionOption struct {
NamePattern string `json:"name_pattern" binding:"Required"`
WhitelistTeams []string `json:"whitelist_teams"`
}
// EditOrgTagProtectionOption options for editing an org-level tag protection
type EditOrgTagProtectionOption struct {
NamePattern *string `json:"name_pattern"`
WhitelistTeams []string `json:"whitelist_teams"`
}
-50
View File
@@ -2411,56 +2411,6 @@
"repo.settings.protected_branch": "Branch Protection",
"repo.settings.protected_branch.save_rule": "Save Rule",
"repo.settings.protected_branch.delete_rule": "Delete Rule",
"repo.settings.org_protected_branch": "Organization Branch Protection",
"repo.settings.org_protected_branch_desc": "These rules are defined by the organization and are enforced on top of this repository's own rules — the stricter of the two applies. They cannot be edited here.",
"repo.settings.org_protected_branch.inherited": "Organization",
"repo.settings.org_protected_branch.read_only": "Read-only",
"repo.settings.org_protected_branch.approvals": "Required approvals",
"repo.settings.org_protected_branch.signed": "Signed commits",
"repo.settings.org_protected_branch.status_check": "Required status checks",
"repo.settings.org_protected_branch.direct_push": "Direct push",
"repo.settings.org_protected_branch.force_push": "Force push",
"repo.settings.org_protected_branch.deletion": "Branch deletion",
"repo.settings.org_protected_branch.merge": "Merge restricted to",
"repo.settings.org_protected_branch.protected_files": "Protected files",
"repo.settings.org_protected_branch.also": "Also enforces",
"repo.settings.org_protected_branch.blocked": "Blocked",
"repo.settings.org_protected_branch.allowed": "Allowed",
"repo.settings.org_protected_branch.restricted": "Restricted to specific teams",
"repo.settings.org_protected_branch.write_access": "Anyone with write access",
"repo.settings.org_protected_branch.teams": "Teams: %s",
"repo.settings.org_protected_branch.any": "Any configured checks",
"repo.settings.org_protected_branch.block_outdated": "Block on outdated branch",
"repo.settings.org_protected_branch.block_rejected": "Block on rejected reviews",
"repo.settings.org_protected_branch.block_admin": "Block admin merge override",
"repo.settings.org_protected_tag": "Organization Tag Protection",
"repo.settings.org_protected_tag_desc": "These tag protection rules are defined by the organization and are enforced on top of this repository's own rules. They cannot be edited here.",
"repo.settings.org_protected_tag.read_only": "Read-only",
"repo.settings.org_push_policy": "Organization Push Policy",
"repo.settings.org_push_policy_desc": "This push policy is defined by the organization and enforced on every push to this repository. It cannot be edited here.",
"repo.settings.org_push_policy.inherited": "Organization",
"repo.settings.org_push_policy.read_only": "Read-only",
"repo.settings.org_push_policy.branch_pattern": "Allowed branch names",
"repo.settings.org_push_policy.tag_pattern": "Allowed tag names",
"repo.settings.org_push_policy.require_secret_block": "Block pushes containing secrets",
"repo.settings.org_push_policy.max_file_size": "Maximum file size",
"repo.settings.org_push_policy.blocked_files": "Blocked file paths",
"repo.settings.org_push_policy.enabled": "Enabled",
"repo.settings.org_repo_defaults": "Organization Repository Defaults",
"repo.settings.org_repo_defaults_desc": "These defaults are defined by the organization and were applied to this repository when it was created or transferred into the organization. They are shown for reference and cannot be edited here.",
"repo.settings.org_repo_defaults.inherited": "Organization",
"repo.settings.org_repo_defaults.read_only": "Read-only",
"repo.settings.org_repo_defaults.force_private": "Force private repositories",
"repo.settings.org_repo_defaults.merge_styles": "Allowed merge styles",
"repo.settings.org_repo_defaults.default_merge_style": "Default merge style",
"repo.settings.org_repo_defaults.delete_branch_after_merge": "Delete branch after merge",
"repo.settings.org_repo_defaults.style_merge": "Merge commits",
"repo.settings.org_repo_defaults.style_rebase": "Rebase",
"repo.settings.org_repo_defaults.style_rebase_merge": "Rebase with merge commit",
"repo.settings.org_repo_defaults.style_squash": "Squash",
"repo.settings.org_repo_defaults.style_fast_forward_only": "Fast-forward only",
"repo.settings.org_repo_defaults.enabled": "Enabled",
"repo.settings.org_repo_defaults.disabled": "Disabled",
"repo.settings.protected_branch_can_push": "Allow push?",
"repo.settings.protected_branch_can_push_yes": "You can push",
"repo.settings.protected_branch_can_push_no": "You cannot push",
-28
View File
@@ -1823,34 +1823,6 @@ func Routes() *web.Router {
})
}, reqToken(), reqOrgOwnership())
m.Group("/tag_protections", func() {
m.Combo("").Get(org.ListOrgTagProtections).
Post(bind(api.CreateOrgTagProtectionOption{}), org.CreateOrgTagProtection)
m.Group("/{id}", func() {
m.Get("", org.GetOrgTagProtection)
m.Patch("", bind(api.EditOrgTagProtectionOption{}), org.EditOrgTagProtection)
m.Delete("", org.DeleteOrgTagProtection)
})
}, reqToken(), reqOrgOwnership())
m.Group("/push_policy", func() {
m.Combo("").Get(org.GetOrgPushPolicy).
Patch(bind(api.EditOrgPushPolicyOption{}), org.EditOrgPushPolicy).
Delete(org.DeleteOrgPushPolicy)
}, reqToken(), reqOrgOwnership())
m.Group("/repo_defaults", func() {
m.Combo("").Get(org.GetOrgRepoDefaults).
Patch(bind(api.EditOrgRepoDefaultsOption{}), org.EditOrgRepoDefaults).
Delete(org.DeleteOrgRepoDefaults)
}, reqToken(), reqOrgOwnership())
m.Group("/email_domain_policy", func() {
m.Combo("").Get(org.GetOrgEmailDomainPolicy).
Patch(bind(api.EditOrgEmailDomainPolicyOption{}), org.EditOrgEmailDomainPolicy).
Delete(org.DeleteOrgEmailDomainPolicy)
}, reqToken(), reqOrgOwnership())
m.Group("/blocks", func() {
m.Get("", org.ListBlocks)
m.Group("/{username}", func() {
-23
View File
@@ -47,9 +47,6 @@ func toAPIOrgBranchProtection(ctx *context.APIContext, rule *git_model.OrgProtec
EnableForcePush: rule.CanForcePush,
EnableForcePushAllowlist: rule.EnableForcePushAllowlist,
ForcePushAllowlistTeams: resolveTeamNames(rule.ForcePushAllowlistTeamIDs),
EnableDelete: rule.CanDelete,
EnableDeleteAllowlist: rule.EnableDeleteAllowlist,
DeleteAllowlistTeams: resolveTeamNames(rule.DeleteAllowlistTeamIDs),
EnableMergeWhitelist: rule.EnableMergeWhitelist,
MergeWhitelistTeams: resolveTeamNames(rule.MergeWhitelistTeamIDs),
EnableStatusCheck: rule.EnableStatusCheck,
@@ -214,10 +211,6 @@ func CreateOrgBranchProtection(ctx *context.APIContext) {
if !ok {
return
}
deleteTeams, ok := resolveTeamIDs(ctx, orgID, form.DeleteAllowlistTeams)
if !ok {
return
}
rule := &git_model.OrgProtectedBranch{
OrgID: orgID,
@@ -229,9 +222,6 @@ func CreateOrgBranchProtection(ctx *context.APIContext) {
CanForcePush: form.EnablePush && form.EnableForcePush,
EnableForcePushAllowlist: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist,
ForcePushAllowlistTeamIDs: forcePushTeams,
CanDelete: form.EnableDelete,
EnableDeleteAllowlist: form.EnableDelete && form.EnableDeleteAllowlist,
DeleteAllowlistTeamIDs: deleteTeams,
EnableMergeWhitelist: form.EnableMergeWhitelist,
MergeWhitelistTeamIDs: mergeTeams,
EnableStatusCheck: form.EnableStatusCheck,
@@ -333,19 +323,6 @@ func EditOrgBranchProtection(ctx *context.APIContext) {
}
rule.ForcePushAllowlistTeamIDs = ids
}
if form.EnableDelete != nil {
rule.CanDelete = *form.EnableDelete
}
if form.EnableDeleteAllowlist != nil {
rule.EnableDeleteAllowlist = *form.EnableDeleteAllowlist
}
if form.DeleteAllowlistTeams != nil {
ids, ok := resolveTeamIDs(ctx, orgID, form.DeleteAllowlistTeams)
if !ok {
return
}
rule.DeleteAllowlistTeamIDs = ids
}
if form.EnableMergeWhitelist != nil {
rule.EnableMergeWhitelist = *form.EnableMergeWhitelist
}
-67
View File
@@ -1,67 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package org
import (
"net/http"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
api "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/web"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
func toAPIOrgEmailDomainPolicy(policy *git_model.OrgEmailDomainPolicy, orgID int64) *api.OrgEmailDomainPolicy {
if policy == nil {
return &api.OrgEmailDomainPolicy{OrgID: orgID}
}
return &api.OrgEmailDomainPolicy{
OrgID: policy.OrgID,
AllowedDomains: policy.AllowedDomains,
}
}
// GetOrgEmailDomainPolicy get the organization's email domain policy
func GetOrgEmailDomainPolicy(ctx *context.APIContext) {
orgID := ctx.Org.Organization.ID
policy, err := git_model.GetOrgEmailDomainPolicy(ctx, orgID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, toAPIOrgEmailDomainPolicy(policy, orgID))
}
// EditOrgEmailDomainPolicy create or update the organization's email domain policy
func EditOrgEmailDomainPolicy(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.EditOrgEmailDomainPolicyOption)
orgID := ctx.Org.Organization.ID
policy, err := git_model.GetOrgEmailDomainPolicy(ctx, orgID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if policy == nil {
policy = &git_model.OrgEmailDomainPolicy{OrgID: orgID}
}
if form.AllowedDomains != nil {
policy.AllowedDomains = *form.AllowedDomains
}
if err := git_model.UpsertOrgEmailDomainPolicy(ctx, policy); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, toAPIOrgEmailDomainPolicy(policy, orgID))
}
// DeleteOrgEmailDomainPolicy remove the organization's email domain policy
func DeleteOrgEmailDomainPolicy(ctx *context.APIContext) {
if err := git_model.DeleteOrgEmailDomainPolicy(ctx, ctx.Org.Organization.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
-88
View File
@@ -1,88 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package org
import (
"net/http"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
api "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/web"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
// toAPIOrgPushPolicy converts the model to its API representation. A nil policy is
// rendered as an all-empty policy so clients always get a consistent shape.
func toAPIOrgPushPolicy(policy *git_model.OrgPushPolicy, orgID int64) *api.OrgPushPolicy {
if policy == nil {
return &api.OrgPushPolicy{OrgID: orgID}
}
return &api.OrgPushPolicy{
OrgID: policy.OrgID,
BranchNamePattern: policy.BranchNamePattern,
TagNamePattern: policy.TagNamePattern,
RequireSecretBlock: policy.RequireSecretBlock,
MaxFileSize: policy.MaxFileSize,
BlockedFilePatterns: policy.BlockedFilePatterns,
Created: policy.CreatedUnix.AsTime(),
Updated: policy.UpdatedUnix.AsTime(),
}
}
// GetOrgPushPolicy get the organization's push policy
func GetOrgPushPolicy(ctx *context.APIContext) {
orgID := ctx.Org.Organization.ID
policy, err := git_model.GetOrgPushPolicy(ctx, orgID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, toAPIOrgPushPolicy(policy, orgID))
}
// EditOrgPushPolicy create or update the organization's push policy
func EditOrgPushPolicy(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.EditOrgPushPolicyOption)
orgID := ctx.Org.Organization.ID
policy, err := git_model.GetOrgPushPolicy(ctx, orgID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if policy == nil {
policy = &git_model.OrgPushPolicy{OrgID: orgID}
}
if form.BranchNamePattern != nil {
policy.BranchNamePattern = *form.BranchNamePattern
}
if form.TagNamePattern != nil {
policy.TagNamePattern = *form.TagNamePattern
}
if form.RequireSecretBlock != nil {
policy.RequireSecretBlock = *form.RequireSecretBlock
}
if form.MaxFileSize != nil {
policy.MaxFileSize = *form.MaxFileSize
}
if form.BlockedFilePatterns != nil {
policy.BlockedFilePatterns = *form.BlockedFilePatterns
}
if err := git_model.UpsertOrgPushPolicy(ctx, policy); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, toAPIOrgPushPolicy(policy, orgID))
}
// DeleteOrgPushPolicy remove the organization's push policy
func DeleteOrgPushPolicy(ctx *context.APIContext) {
if err := git_model.DeleteOrgPushPolicy(ctx, ctx.Org.Organization.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
-117
View File
@@ -1,117 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package org
import (
"net/http"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
api "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/web"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
// toAPIOrgRepoDefaults converts the model to its API representation. A nil value is
// rendered as the effective defaults (all merge styles allowed) so clients always
// get a consistent shape.
func toAPIOrgRepoDefaults(d *git_model.OrgRepoDefaults, orgID int64) *api.OrgRepoDefaults {
if d == nil {
return &api.OrgRepoDefaults{
OrgID: orgID,
AllowMerge: true,
AllowRebase: true,
AllowRebaseMerge: true,
AllowSquash: true,
AllowFastForwardOnly: true,
}
}
return &api.OrgRepoDefaults{
OrgID: d.OrgID,
ForcePrivate: d.ForcePrivate,
ApplyPRDefaults: d.ApplyPRDefaults,
AllowMerge: d.AllowMerge,
AllowRebase: d.AllowRebase,
AllowRebaseMerge: d.AllowRebaseMerge,
AllowSquash: d.AllowSquash,
AllowFastForwardOnly: d.AllowFastForwardOnly,
DefaultMergeStyle: d.DefaultMergeStyle,
DeleteBranchAfterMerge: d.DeleteBranchAfterMerge,
}
}
// GetOrgRepoDefaults get the organization's default repository settings
func GetOrgRepoDefaults(ctx *context.APIContext) {
orgID := ctx.Org.Organization.ID
defaults, err := git_model.GetOrgRepoDefaults(ctx, orgID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, toAPIOrgRepoDefaults(defaults, orgID))
}
// EditOrgRepoDefaults create or update the organization's default repository settings
func EditOrgRepoDefaults(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.EditOrgRepoDefaultsOption)
orgID := ctx.Org.Organization.ID
defaults, err := git_model.GetOrgRepoDefaults(ctx, orgID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if defaults == nil {
defaults = &git_model.OrgRepoDefaults{
OrgID: orgID,
AllowMerge: true,
AllowRebase: true,
AllowRebaseMerge: true,
AllowSquash: true,
AllowFastForwardOnly: true,
}
}
if form.ForcePrivate != nil {
defaults.ForcePrivate = *form.ForcePrivate
}
if form.ApplyPRDefaults != nil {
defaults.ApplyPRDefaults = *form.ApplyPRDefaults
}
if form.AllowMerge != nil {
defaults.AllowMerge = *form.AllowMerge
}
if form.AllowRebase != nil {
defaults.AllowRebase = *form.AllowRebase
}
if form.AllowRebaseMerge != nil {
defaults.AllowRebaseMerge = *form.AllowRebaseMerge
}
if form.AllowSquash != nil {
defaults.AllowSquash = *form.AllowSquash
}
if form.AllowFastForwardOnly != nil {
defaults.AllowFastForwardOnly = *form.AllowFastForwardOnly
}
if form.DefaultMergeStyle != nil {
defaults.DefaultMergeStyle = *form.DefaultMergeStyle
}
if form.DeleteBranchAfterMerge != nil {
defaults.DeleteBranchAfterMerge = *form.DeleteBranchAfterMerge
}
if err := git_model.UpsertOrgRepoDefaults(ctx, defaults); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, toAPIOrgRepoDefaults(defaults, orgID))
}
// DeleteOrgRepoDefaults remove the organization's default repository settings
func DeleteOrgRepoDefaults(ctx *context.APIContext) {
if err := git_model.DeleteOrgRepoDefaults(ctx, ctx.Org.Organization.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
-152
View File
@@ -1,152 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package org
import (
"net/http"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/organization"
api "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/web"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
// toAPIOrgTagProtection converts an org tag rule to its API representation.
func toAPIOrgTagProtection(ctx *context.APIContext, rule *git_model.OrgProtectedTag) *api.OrgTagProtection {
teams, err := organization.FindOrgTeams(ctx, rule.OrgID)
if err != nil {
teams = nil
}
teamNamesByID := make(map[int64]string, len(teams))
for _, t := range teams {
teamNamesByID[t.ID] = t.Name
}
names := make([]string, 0, len(rule.AllowlistTeamIDs))
for _, id := range rule.AllowlistTeamIDs {
if name, ok := teamNamesByID[id]; ok {
names = append(names, name)
}
}
return &api.OrgTagProtection{
ID: rule.ID,
OrgID: rule.OrgID,
NamePattern: rule.NamePattern,
WhitelistTeams: names,
Created: rule.CreatedUnix.AsTime(),
Updated: rule.UpdatedUnix.AsTime(),
}
}
// ListOrgTagProtections list org-level tag protection rules
func ListOrgTagProtections(ctx *context.APIContext) {
rules, err := git_model.FindOrgProtectedTags(ctx, ctx.Org.Organization.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiRules := make([]*api.OrgTagProtection, len(rules))
for i, rule := range rules {
apiRules[i] = toAPIOrgTagProtection(ctx, rule)
}
ctx.JSON(http.StatusOK, apiRules)
}
// GetOrgTagProtection get a specific org-level tag protection rule
func GetOrgTagProtection(ctx *context.APIContext) {
rule, err := git_model.GetOrgProtectedTagByID(ctx, ctx.Org.Organization.ID, ctx.PathParamInt64("id"))
if err != nil {
ctx.APIErrorInternal(err)
return
}
if rule == nil {
ctx.APIErrorNotFound()
return
}
ctx.JSON(http.StatusOK, toAPIOrgTagProtection(ctx, rule))
}
// CreateOrgTagProtection create an org-level tag protection rule
func CreateOrgTagProtection(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.CreateOrgTagProtectionOption)
orgID := ctx.Org.Organization.ID
existing, err := git_model.GetOrgProtectedTagByNamePattern(ctx, orgID, form.NamePattern)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if existing != nil {
ctx.APIError(http.StatusForbidden, "org tag protection rule already exists for this pattern")
return
}
teams, ok := resolveTeamIDs(ctx, orgID, form.WhitelistTeams)
if !ok {
return
}
rule := &git_model.OrgProtectedTag{
OrgID: orgID,
NamePattern: form.NamePattern,
AllowlistTeamIDs: teams,
}
if err := git_model.CreateOrgProtectedTag(ctx, rule); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusCreated, toAPIOrgTagProtection(ctx, rule))
}
// EditOrgTagProtection edit an org-level tag protection rule
func EditOrgTagProtection(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.EditOrgTagProtectionOption)
orgID := ctx.Org.Organization.ID
rule, err := git_model.GetOrgProtectedTagByID(ctx, orgID, ctx.PathParamInt64("id"))
if err != nil {
ctx.APIErrorInternal(err)
return
}
if rule == nil {
ctx.APIErrorNotFound()
return
}
if form.NamePattern != nil {
rule.NamePattern = *form.NamePattern
}
if form.WhitelistTeams != nil {
ids, ok := resolveTeamIDs(ctx, orgID, form.WhitelistTeams)
if !ok {
return
}
rule.AllowlistTeamIDs = ids
}
if err := git_model.UpdateOrgProtectedTag(ctx, rule); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, toAPIOrgTagProtection(ctx, rule))
}
// DeleteOrgTagProtection delete an org-level tag protection rule
func DeleteOrgTagProtection(ctx *context.APIContext) {
orgID := ctx.Org.Organization.ID
rule, err := git_model.GetOrgProtectedTagByID(ctx, orgID, ctx.PathParamInt64("id"))
if err != nil {
ctx.APIErrorInternal(err)
return
}
if rule == nil {
ctx.APIErrorNotFound()
return
}
if err := git_model.DeleteOrgProtectedTag(ctx, orgID, rule.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
-3
View File
@@ -9,7 +9,6 @@ import (
"net/http"
activities_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/activities"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/organization"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/perm"
access_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/perm/access"
@@ -492,8 +491,6 @@ func AddTeamMember(ctx *context.APIContext) {
if err := org_service.AddTeamMember(ctx, ctx.Org.Team, u); err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err)
} else if git_model.IsErrEmailDomainNotAllowed(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else {
ctx.APIErrorInternal(err)
}
-2
View File
@@ -48,7 +48,6 @@ import (
repo_migrations "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/migrations"
mirror_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/mirror"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/oauth2_provider"
org_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/org"
packages_spec "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/packages/pkgspec"
pull_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/pull"
release_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/release"
@@ -156,7 +155,6 @@ func InitWebInstalled(ctx context.Context) {
mustInit(pull_service.Init)
mustInit(automerge.Init)
cascade.Init()
org_service.Init()
mustInit(task.Init)
mustInit(repo_migrations.Init)
eventsource.GetManager().Init()
+1 -110
View File
@@ -8,8 +8,6 @@ import (
"fmt"
"net/http"
"os"
"strconv"
"strings"
asymkey_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/asymkey"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
@@ -162,10 +160,6 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
gitRepo := ctx.Repo.GitRepo
objectFormat := ctx.Repo.GetObjectFormat()
if ctx.checkOrgPushPolicyBranch(oldCommitID, newCommitID, branchName) {
return
}
if newCommitID != objectFormat.EmptyObjectID().String() {
newCommit, err := gitRepo.GetCommit(newCommitID)
if err != nil {
@@ -461,10 +455,6 @@ func preReceiveTag(ctx *preReceiveContext, refFullName git.RefName) {
tagName := refFullName.TagName()
if ctx.checkOrgPushPolicyTag(tagName) {
return
}
if !ctx.gotProtectedTags {
var err error
ctx.protectedTags, err = git_model.GetProtectedTags(ctx, ctx.Repo.Repository.ID)
@@ -478,7 +468,7 @@ func preReceiveTag(ctx *preReceiveContext, refFullName git.RefName) {
ctx.gotProtectedTags = true
}
isAllowed, err := git_model.IsUserAllowedToControlTagInRepo(ctx, ctx.protectedTags, ctx.Repo.Repository, tagName, ctx.opts.UserID)
isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, ctx.protectedTags, tagName, ctx.opts.UserID)
if err != nil {
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: err.Error(),
@@ -606,102 +596,3 @@ func (ctx *preReceiveContext) loadPusherAndPermission() bool {
ctx.loadedPusher = true
return true
}
// checkOrgPushPolicyBranch enforces the owning organization's push policy on a
// branch push. It writes a 403 response and returns true when the push is rejected.
// Content checks (blocked paths, max file size) fail open on unexpected errors so a
// policy or parsing bug can never block every push in the organization.
func (ctx *preReceiveContext) checkOrgPushPolicyBranch(oldCommitID, newCommitID, branchName string) bool {
policy, err := git_model.GetOrgPushPolicyForRepo(ctx, ctx.Repo.Repository)
if err != nil {
log.Error("GetOrgPushPolicyForRepo for %-v: %v", ctx.Repo.Repository, err)
return false
}
if policy == nil {
return false
}
if !policy.BranchNameAllowed(branchName) {
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: fmt.Sprintf("Branch name %q is not allowed by the organization push policy (pattern: %s)", branchName, policy.BranchNamePattern),
})
return true
}
// Deletions have no content to inspect.
if newCommitID == ctx.Repo.GetObjectFormat().EmptyObjectID().String() {
return false
}
if globs := policy.BlockedFileGlobs(); len(globs) > 0 {
if _, err := pull_service.CheckFileProtection(ctx.Repo.GitRepo, branchName, oldCommitID, newCommitID, globs, 10, ctx.env); err != nil {
if pull_service.IsErrFilePathProtected(err) {
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: "Push rejected by the organization push policy: a changed file matches a blocked path pattern",
})
return true
}
log.Error("org push policy blocked-path check for %-v: %v", ctx.Repo.Repository, err) // fail open
}
}
if policy.MaxFileSize > 0 {
if path, size := ctx.largestBlobOverLimit(newCommitID, policy.MaxFileSize); path != "" {
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: fmt.Sprintf("Push rejected by the organization push policy: %q is %d bytes, over the %d-byte limit", path, size, policy.MaxFileSize),
})
return true
}
}
return false
}
// checkOrgPushPolicyTag enforces the organization tag naming policy. Returns true
// (with a 403 written) when the tag name is rejected.
func (ctx *preReceiveContext) checkOrgPushPolicyTag(tagName string) bool {
policy, err := git_model.GetOrgPushPolicyForRepo(ctx, ctx.Repo.Repository)
if err != nil {
log.Error("GetOrgPushPolicyForRepo for %-v: %v", ctx.Repo.Repository, err)
return false
}
if policy == nil || policy.TagNameAllowed(tagName) {
return false
}
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: fmt.Sprintf("Tag name %q is not allowed by the organization push policy (pattern: %s)", tagName, policy.TagNamePattern),
})
return true
}
// largestBlobOverLimit returns the first file (and its size) in the pushed tip tree
// that exceeds limit bytes, or ("", 0) if none — or on any error (fail open).
func (ctx *preReceiveContext) largestBlobOverLimit(commitID string, limit int64) (string, int64) {
output, _, err := gitrepo.RunCmdString(ctx,
ctx.Repo.Repository,
gitcmd.NewCommand("ls-tree", "-r", "--long").
AddDynamicArguments(commitID).
WithEnv(ctx.env),
)
if err != nil {
log.Error("org push policy ls-tree for %-v: %v", ctx.Repo.Repository, err)
return "", 0
}
for _, line := range strings.Split(output, "\n") {
tab := strings.IndexByte(line, '\t')
if tab < 0 {
continue
}
fields := strings.Fields(line[:tab]) // mode, type, hash, size
if len(fields) < 4 || fields[1] != "blob" {
continue
}
size, perr := strconv.ParseInt(fields[3], 10, 64)
if perr != nil {
continue
}
if size > limit {
return line[tab+1:], size
}
}
return "", 0
}
@@ -1,51 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package setting
import (
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
// prepareOrgPushPolicyView surfaces the owning organization's push policy (if any)
// as a read-only informational section on the repo's branch protection settings page.
// The policy is enforced org-wide in the pre-receive hook and cannot be edited from
// the repository. Template data is only populated when the repo is owned by an
// organization that has a push policy configured (nil policy renders nothing).
// See issue #738.
func prepareOrgPushPolicyView(ctx *context.Context) error {
if !ctx.Repo.Owner.IsOrganization() {
return nil
}
policy, err := git_model.GetOrgPushPolicyForRepo(ctx, ctx.Repo.Repository)
if err != nil {
return err
}
if policy == nil {
return nil
}
ctx.Data["OrgPushPolicy"] = policy
return nil
}
// prepareOrgRepoDefaultsView surfaces the owning organization's repository defaults
// (if any) as a read-only informational section. These defaults are applied when a
// repository is created in, or transferred into, the organization; they are shown
// for reference only and are not editable here. Template data is only populated when
// the repo is owned by an organization that has repo defaults configured (nil renders
// nothing). See issue #738.
func prepareOrgRepoDefaultsView(ctx *context.Context) error {
if !ctx.Repo.Owner.IsOrganization() {
return nil
}
defaults, err := git_model.GetOrgRepoDefaultsForRepo(ctx, ctx.Repo.Repository)
if err != nil {
return err
}
if defaults == nil {
return nil
}
ctx.Data["OrgRepoDefaults"] = defaults
return nil
}
@@ -34,64 +34,6 @@ const (
tplProtectedBranch templates.TplName = "repo/settings/protected_branch"
)
// orgBranchProtectionView is a read-only presentation of an org-level branch
// protection rule for the repo settings page, with team IDs resolved to names.
type orgBranchProtectionView struct {
Rule *git_model.OrgProtectedBranch
PushTeams string
ForcePushTeams string
DeleteTeams string
MergeTeams string
ApprovalTeams string
StatusContexts string
}
// prepareOrgProtectedBranches loads the owning organization's branch protection
// rules and exposes them (with team IDs resolved to names) as read-only view models
// under the "OrgProtectedBranches" template key.
func prepareOrgProtectedBranches(ctx *context.Context) error {
orgRules, err := git_model.FindOrgProtectedBranchRules(ctx, ctx.Repo.Owner.ID)
if err != nil {
return err
}
if len(orgRules) == 0 {
return nil
}
teams, err := organization.FindOrgTeams(ctx, ctx.Repo.Owner.ID)
if err != nil {
return err
}
teamNames := make(map[int64]string, len(teams))
for _, t := range teams {
teamNames[t.ID] = t.Name
}
join := func(ids []int64) string {
names := make([]string, 0, len(ids))
for _, id := range ids {
if n, ok := teamNames[id]; ok {
names = append(names, n)
}
}
return strings.Join(names, ", ")
}
views := make([]*orgBranchProtectionView, len(orgRules))
for i, r := range orgRules {
views[i] = &orgBranchProtectionView{
Rule: r,
PushTeams: join(r.WhitelistTeamIDs),
ForcePushTeams: join(r.ForcePushAllowlistTeamIDs),
DeleteTeams: join(r.DeleteAllowlistTeamIDs),
MergeTeams: join(r.MergeWhitelistTeamIDs),
ApprovalTeams: join(r.ApprovalsWhitelistTeamIDs),
StatusContexts: strings.Join(r.StatusCheckContexts, ", "),
}
}
ctx.Data["OrgProtectedBranches"] = views
return nil
}
// ProtectedBranchRules render the page to protect the repository
func ProtectedBranchRules(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.branches")
@@ -104,26 +46,6 @@ func ProtectedBranchRules(ctx *context.Context) {
}
ctx.Data["ProtectedBranches"] = rules
// Surface the organization-level rules that also apply to this repo (read-only),
// so admins can see the org "floor" that is layered on top of the repo's own
// rules at enforcement time. See issue #727.
if ctx.Repo.Owner.IsOrganization() {
if err := prepareOrgProtectedBranches(ctx); err != nil {
ctx.ServerError("prepareOrgProtectedBranches", err)
return
}
// Surface the org-wide push policy and the org repo defaults that apply to
// this repo (read-only, informational). See issue #738.
if err := prepareOrgPushPolicyView(ctx); err != nil {
ctx.ServerError("prepareOrgPushPolicyView", err)
return
}
if err := prepareOrgRepoDefaultsView(ctx); err != nil {
ctx.ServerError("prepareOrgRepoDefaultsView", err)
return
}
}
repo.PrepareBranchList(ctx)
if ctx.Written() {
return
-37
View File
@@ -138,13 +138,6 @@ func DeleteProtectedTagPost(ctx *context.Context) {
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/tags")
}
// orgProtectedTagView is a read-only presentation of an org-level tag rule for the
// repo settings page, with allowlist team IDs resolved to names.
type orgProtectedTagView struct {
Rule *git_model.OrgProtectedTag
Teams string
}
func setTagsContext(ctx *context.Context) error {
ctx.Data["Title"] = ctx.Tr("repo.settings.tags")
ctx.Data["PageIsSettingsTags"] = true
@@ -170,36 +163,6 @@ func setTagsContext(ctx *context.Context) error {
return err
}
ctx.Data["Teams"] = teams
// Surface the organization's tag protection rules read-only, so admins can see
// the org "floor" layered on top of this repo's own protected tags (#727).
orgRules, err := git_model.FindOrgProtectedTags(ctx, ctx.Repo.Owner.ID)
if err != nil {
ctx.ServerError("FindOrgProtectedTags", err)
return err
}
if len(orgRules) > 0 {
allTeams, err := organization.FindOrgTeams(ctx, ctx.Repo.Owner.ID)
if err != nil {
ctx.ServerError("FindOrgTeams", err)
return err
}
teamNames := make(map[int64]string, len(allTeams))
for _, t := range allTeams {
teamNames[t.ID] = t.Name
}
views := make([]*orgProtectedTagView, len(orgRules))
for i, r := range orgRules {
names := make([]string, 0, len(r.AllowlistTeamIDs))
for _, id := range r.AllowlistTeamIDs {
if n, ok := teamNames[id]; ok {
names = append(names, n)
}
}
views[i] = &orgProtectedTagView{Rule: r, Teams: strings.Join(names, ", ")}
}
ctx.Data["OrgProtectedTags"] = views
}
}
return nil
-92
View File
@@ -1,92 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package org
import (
"context"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
notify_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/notify"
repo_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/repository"
)
// Init registers the org notifier so organization repository defaults are applied to
// repositories as they are created in or transferred into the org. The notifier
// pattern avoids the services/repository -> services/org import cycle.
func Init() {
notify_service.RegisterNotifier(&repoDefaultsNotifier{})
}
type repoDefaultsNotifier struct {
notify_service.NullNotifier
}
func (n *repoDefaultsNotifier) CreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) {
applyOrgRepoDefaults(ctx, u, repo)
}
func (n *repoDefaultsNotifier) TransferRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldOwnerName string) {
applyOrgRepoDefaults(ctx, nil, repo)
}
// applyOrgRepoDefaults applies the owning organization's default repository settings
// to a repo that has just joined the org. Best-effort: errors are logged and never
// propagated, so a defaults bug can never break repository creation or transfer.
func applyOrgRepoDefaults(ctx context.Context, owner *user_model.User, repo *repo_model.Repository) {
if owner == nil {
if err := repo.LoadOwner(ctx); err != nil {
log.Error("org repo defaults: load owner of repo %d: %v", repo.ID, err)
return
}
owner = repo.Owner
}
if owner == nil || !owner.IsOrganization() {
return
}
defaults, err := git_model.GetOrgRepoDefaults(ctx, owner.ID)
if err != nil {
log.Error("org repo defaults: load for org %d: %v", owner.ID, err)
return
}
if defaults == nil {
return
}
if defaults.ForcePrivate && !repo.IsPrivate {
repo.IsPrivate = true
if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private"); err != nil {
log.Error("org repo defaults: force private on repo %d: %v", repo.ID, err)
}
}
if defaults.ApplyPRDefaults {
prUnit, err := repo.GetUnit(ctx, unit.TypePullRequests)
if err != nil {
// The repository may not have pull requests enabled; nothing to apply.
return
}
cfg := prUnit.PullRequestsConfig()
cfg.AllowMerge = defaults.AllowMerge
cfg.AllowRebase = defaults.AllowRebase
cfg.AllowRebaseMerge = defaults.AllowRebaseMerge
cfg.AllowSquash = defaults.AllowSquash
cfg.AllowFastForwardOnly = defaults.AllowFastForwardOnly
cfg.DefaultDeleteBranchAfterMerge = defaults.DeleteBranchAfterMerge
if defaults.DefaultMergeStyle != "" {
cfg.DefaultMergeStyle = repo_model.MergeStyle(defaults.DefaultMergeStyle)
}
if err := repo_service.UpdateRepositoryUnits(ctx, repo, []repo_model.RepoUnit{{
RepoID: repo.ID,
Type: unit.TypePullRequests,
Config: cfg,
}}, nil); err != nil {
log.Error("org repo defaults: update PR unit on repo %d: %v", repo.ID, err)
}
}
}
-7
View File
@@ -220,13 +220,6 @@ func AddTeamMember(ctx context.Context, team *organization.Team, user *user_mode
return err
}
// Enforce the organization email domain policy for new members.
if allowed, err := git_model.OrgEmailDomainAllowed(ctx, team.OrgID, user.Email); err != nil {
return err
} else if !allowed {
return git_model.ErrEmailDomainNotAllowed{Email: user.Email, OrgID: team.OrgID}
}
if err := organization.AddOrgUser(ctx, team.OrgID, user.ID); err != nil {
return err
}
+2 -2
View File
@@ -92,7 +92,7 @@ func createTag(ctx context.Context, gitRepo *git.Repository, rel *repo_model.Rel
// Trim '--' prefix to prevent command line argument vulnerability.
rel.TagName = strings.TrimPrefix(rel.TagName, "--")
isAllowed, err := git_model.IsUserAllowedToControlTagInRepo(ctx, protectedTags, rel.Repo, rel.TagName, rel.PublisherID)
isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, protectedTags, rel.TagName, rel.PublisherID)
if err != nil {
return false, err
}
@@ -439,7 +439,7 @@ func DeleteReleaseByID(ctx context.Context, repo *repo_model.Repository, rel *re
if err != nil {
return fmt.Errorf("GetProtectedTags: %w", err)
}
isAllowed, err := git_model.IsUserAllowedToControlTagInRepo(ctx, protectedTags, repo, rel.TagName, doer.ID)
isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, protectedTags, rel.TagName, doer.ID)
if err != nil {
return err
}
+1 -22
View File
@@ -6,7 +6,6 @@ package security
import (
"context"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
security_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/security"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
@@ -22,11 +21,7 @@ func ScanPushForSecrets(ctx context.Context, repoID int64, commit *git.Commit) [
return nil
}
if !cfg.Enabled || !cfg.BlockOnPush || !cfg.SecretScanner {
// The owning organization may mandate secret blocking regardless of the
// repository's own scanner config (org push policy).
if !orgRequiresSecretBlock(ctx, repoID) {
return nil
}
return nil
}
scanner := NewSecretScanner()
@@ -38,22 +33,6 @@ func ScanPushForSecrets(ctx context.Context, repoID int64, commit *git.Commit) [
return findings
}
// orgRequiresSecretBlock reports whether the repo's owning organization mandates
// secret blocking on push via its org push policy.
func orgRequiresSecretBlock(ctx context.Context, repoID int64) bool {
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
if err != nil {
log.Error("orgRequiresSecretBlock: GetRepositoryByID: %v", err)
return false
}
policy, err := git_model.GetOrgPushPolicyForRepo(ctx, repo)
if err != nil {
log.Error("orgRequiresSecretBlock: GetOrgPushPolicyForRepo: %v", err)
return false
}
return policy != nil && policy.RequireSecretBlock
}
// ScanOnPush runs enabled scanners against a commit pushed to the default branch.
// Called from services/repository/push.go on default branch pushes.
func ScanOnPush(ctx context.Context, repo *repo_model.Repository, commit *git.Commit) {
-174
View File
@@ -62,180 +62,6 @@
{{end}}
</div>
</div>
{{if .OrgProtectedBranches}}
<h4 class="ui top attached header">
{{ctx.Locale.Tr "repo.settings.org_protected_branch"}}
</h4>
<div class="ui attached segment">
<p class="tw-mb-3">{{ctx.Locale.Tr "repo.settings.org_protected_branch_desc"}}</p>
<div class="flex-divided-list items-with-main">
{{range .OrgProtectedBranches}}
<div class="item">
<div class="item-main tw-w-full">
<details class="tw-w-full">
<summary class="tw-flex tw-items-center tw-gap-2 tw-flex-wrap tw-cursor-pointer">
<div class="ui basic label">{{.Rule.RuleName}}</div>
<span class="ui tiny label">{{svg "octicon-organization" 12}} {{ctx.Locale.Tr "repo.settings.org_protected_branch.inherited"}}</span>
<span class="text grey tw-text-sm">{{svg "octicon-lock" 12}} {{ctx.Locale.Tr "repo.settings.org_protected_branch.read_only"}}</span>
</summary>
<table class="ui very basic compact table tw-mt-2 tw-w-auto tw-ml-4">
<tbody>
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_protected_branch.direct_push"}}</td>
<td>{{if not .Rule.CanPush}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.blocked"}}{{else if .Rule.EnableWhitelist}}{{if .PushTeams}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.teams" .PushTeams}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.restricted"}}{{end}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.write_access"}}{{end}}</td>
</tr>
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_protected_branch.force_push"}}</td>
<td>{{if not .Rule.CanForcePush}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.blocked"}}{{else if .Rule.EnableForcePushAllowlist}}{{if .ForcePushTeams}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.teams" .ForcePushTeams}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.restricted"}}{{end}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.allowed"}}{{end}}</td>
</tr>
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_protected_branch.deletion"}}</td>
<td>{{if not .Rule.CanDelete}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.blocked"}}{{else if .Rule.EnableDeleteAllowlist}}{{if .DeleteTeams}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.teams" .DeleteTeams}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.restricted"}}{{end}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.allowed"}}{{end}}</td>
</tr>
{{if gt .Rule.RequiredApprovals 0}}
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_protected_branch.approvals"}}</td>
<td>{{.Rule.RequiredApprovals}}{{if .ApprovalTeams}} ({{.ApprovalTeams}}){{end}}</td>
</tr>
{{end}}
{{if .Rule.EnableMergeWhitelist}}
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_protected_branch.merge"}}</td>
<td>{{if .MergeTeams}}{{.MergeTeams}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.restricted"}}{{end}}</td>
</tr>
{{end}}
{{if .Rule.EnableStatusCheck}}
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_protected_branch.status_check"}}</td>
<td>{{if .StatusContexts}}{{.StatusContexts}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.any"}}{{end}}</td>
</tr>
{{end}}
{{if .Rule.RequireSignedCommits}}
<tr>
<td class="tw-font-semibold">{{ctx.Locale.Tr "repo.settings.org_protected_branch.signed"}}</td>
<td>{{svg "octicon-check" 14}}</td>
</tr>
{{end}}
{{if .Rule.ProtectedFilePatterns}}
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_protected_branch.protected_files"}}</td>
<td><code>{{.Rule.ProtectedFilePatterns}}</code></td>
</tr>
{{end}}
{{if or .Rule.BlockOnOutdatedBranch .Rule.BlockOnRejectedReviews .Rule.BlockAdminMergeOverride}}
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_protected_branch.also"}}</td>
<td>
<div class="tw-flex tw-gap-1 tw-flex-wrap">
{{if .Rule.BlockOnOutdatedBranch}}<span class="ui tiny basic label">{{ctx.Locale.Tr "repo.settings.org_protected_branch.block_outdated"}}</span>{{end}}
{{if .Rule.BlockOnRejectedReviews}}<span class="ui tiny basic label">{{ctx.Locale.Tr "repo.settings.org_protected_branch.block_rejected"}}</span>{{end}}
{{if .Rule.BlockAdminMergeOverride}}<span class="ui tiny basic label">{{ctx.Locale.Tr "repo.settings.org_protected_branch.block_admin"}}</span>{{end}}
</div>
</td>
</tr>
{{end}}
</tbody>
</table>
</details>
</div>
</div>
{{end}}
</div>
</div>
{{end}}
{{if .OrgPushPolicy}}
<h4 class="ui top attached header">
{{ctx.Locale.Tr "repo.settings.org_push_policy"}}
</h4>
<div class="ui attached segment">
<p class="tw-mb-3">
{{svg "octicon-organization" 12}} {{ctx.Locale.Tr "repo.settings.org_push_policy.inherited"}}
<span class="text grey tw-text-sm tw-ml-2">{{svg "octicon-lock" 12}} {{ctx.Locale.Tr "repo.settings.org_push_policy.read_only"}}</span>
</p>
<p class="tw-mb-3">{{ctx.Locale.Tr "repo.settings.org_push_policy_desc"}}</p>
<table class="ui very basic compact table tw-w-auto">
<tbody>
{{if .OrgPushPolicy.BranchNamePattern}}
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_push_policy.branch_pattern"}}</td>
<td><code>{{.OrgPushPolicy.BranchNamePattern}}</code></td>
</tr>
{{end}}
{{if .OrgPushPolicy.TagNamePattern}}
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_push_policy.tag_pattern"}}</td>
<td><code>{{.OrgPushPolicy.TagNamePattern}}</code></td>
</tr>
{{end}}
{{if .OrgPushPolicy.RequireSecretBlock}}
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_push_policy.require_secret_block"}}</td>
<td>{{svg "octicon-check" 14}} {{ctx.Locale.Tr "repo.settings.org_push_policy.enabled"}}</td>
</tr>
{{end}}
{{if gt .OrgPushPolicy.MaxFileSize 0}}
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_push_policy.max_file_size"}}</td>
<td>{{FileSize .OrgPushPolicy.MaxFileSize}}</td>
</tr>
{{end}}
{{if .OrgPushPolicy.BlockedFilePatterns}}
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_push_policy.blocked_files"}}</td>
<td><code>{{.OrgPushPolicy.BlockedFilePatterns}}</code></td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
{{if .OrgRepoDefaults}}
<h4 class="ui top attached header">
{{ctx.Locale.Tr "repo.settings.org_repo_defaults"}}
</h4>
<div class="ui attached segment">
<p class="tw-mb-3">
{{svg "octicon-organization" 12}} {{ctx.Locale.Tr "repo.settings.org_repo_defaults.inherited"}}
<span class="text grey tw-text-sm tw-ml-2">{{svg "octicon-lock" 12}} {{ctx.Locale.Tr "repo.settings.org_repo_defaults.read_only"}}</span>
</p>
<p class="tw-mb-3">{{ctx.Locale.Tr "repo.settings.org_repo_defaults_desc"}}</p>
<table class="ui very basic compact table tw-w-auto">
<tbody>
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_repo_defaults.force_private"}}</td>
<td>{{if .OrgRepoDefaults.ForcePrivate}}{{ctx.Locale.Tr "repo.settings.org_repo_defaults.enabled"}}{{else}}{{ctx.Locale.Tr "repo.settings.org_repo_defaults.disabled"}}{{end}}</td>
</tr>
{{if .OrgRepoDefaults.ApplyPRDefaults}}
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_repo_defaults.merge_styles"}}</td>
<td>
<div class="tw-flex tw-gap-1 tw-flex-wrap">
{{if .OrgRepoDefaults.AllowMerge}}<span class="ui tiny basic label">{{ctx.Locale.Tr "repo.settings.org_repo_defaults.style_merge"}}</span>{{end}}
{{if .OrgRepoDefaults.AllowRebase}}<span class="ui tiny basic label">{{ctx.Locale.Tr "repo.settings.org_repo_defaults.style_rebase"}}</span>{{end}}
{{if .OrgRepoDefaults.AllowRebaseMerge}}<span class="ui tiny basic label">{{ctx.Locale.Tr "repo.settings.org_repo_defaults.style_rebase_merge"}}</span>{{end}}
{{if .OrgRepoDefaults.AllowSquash}}<span class="ui tiny basic label">{{ctx.Locale.Tr "repo.settings.org_repo_defaults.style_squash"}}</span>{{end}}
{{if .OrgRepoDefaults.AllowFastForwardOnly}}<span class="ui tiny basic label">{{ctx.Locale.Tr "repo.settings.org_repo_defaults.style_fast_forward_only"}}</span>{{end}}
</div>
</td>
</tr>
{{if .OrgRepoDefaults.DefaultMergeStyle}}
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_repo_defaults.default_merge_style"}}</td>
<td><code>{{.OrgRepoDefaults.DefaultMergeStyle}}</code></td>
</tr>
{{end}}
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_repo_defaults.delete_branch_after_merge"}}</td>
<td>{{if .OrgRepoDefaults.DeleteBranchAfterMerge}}{{ctx.Locale.Tr "repo.settings.org_repo_defaults.enabled"}}{{else}}{{ctx.Locale.Tr "repo.settings.org_repo_defaults.disabled"}}{{end}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
{{end}}
</div>
-24
View File
@@ -116,30 +116,6 @@
{{end}}
</tbody>
</table>
{{if .OrgProtectedTags}}
<h5 class="ui top attached header tw-mt-4">
{{svg "octicon-organization" 14}} {{ctx.Locale.Tr "repo.settings.org_protected_tag"}}
</h5>
<div class="ui attached segment">
<p class="tw-mb-3">{{ctx.Locale.Tr "repo.settings.org_protected_tag_desc"}}</p>
<table class="ui single line table">
<thead>
<th>{{ctx.Locale.Tr "repo.settings.tags.protection.pattern"}}</th>
<th>{{ctx.Locale.Tr "repo.settings.tags.protection.allowed"}}</th>
<th class="tw-text-right">{{ctx.Locale.Tr "repo.settings.org_protected_tag.read_only"}}</th>
</thead>
<tbody>
{{range .OrgProtectedTags}}
<tr>
<td><pre>{{.Rule.NamePattern}}</pre></td>
<td>{{if .Teams}}{{.Teams}}{{else}}{{ctx.Locale.Tr "repo.settings.tags.protection.allowed.noone"}}{{end}}</td>
<td class="tw-text-right"><span class="text grey">{{svg "octicon-lock" 14}}</span></td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
</div>
</div>
</div>
+1 -1
View File
@@ -25,7 +25,7 @@ Custom CSS themes at `/var/lib/gitea/custom/public/assets/css/`:
---
*Repo: [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
*Repo: [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) · [mokocli](https://git.mokoconsulting.tech/MokoConsulting/mokocli/wiki/Home)*
| Revision | Date | Author | Description |
|---|---|---|---|
+1 -1
View File
@@ -126,7 +126,7 @@ When a field definition is deleted, all associated values in `custom_field_value
| System | Relationship |
|--------|-------------|
| Update Server | Repo-scoped custom fields with specific names (Extension Name, Display Name, etc.) are read by the update feed generators as the highest-priority metadata source. |
| Manifest Settings | Manifest fields follow the moko-platform schema and are separate from custom fields. Custom fields are user-defined; manifest fields are standardized. |
| Manifest Settings | Manifest fields follow the mokocli schema and are separate from custom fields. Custom fields are user-defined; manifest fields are standardized. |
| Issue Statuses | Custom statuses are a separate feature with their own dedicated table and UI, not implemented as custom fields. |
---
+1 -1
View File
@@ -41,7 +41,7 @@ Located at `/var/lib/gitea/custom/`:
---
*Repo: [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
*Repo: [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) · [mokocli](https://git.mokoconsulting.tech/MokoConsulting/mokocli/wiki/Home)*
| Revision | Date | Author | Description |
|---|---|---|---|
+2 -2
View File
@@ -309,8 +309,8 @@ Packages can be **archived** instead of permanently deleted. This is the recomme
In **Organization Settings → Licensing & Update Streams**, under **Extension Metadata**:
1. Set the **Platform** (Joomla, Dolibarr, WordPress, etc.)
2. Set the **Extension Name** (e.g., `pkg_mokowaas`) — this becomes the `<element>` in the XML feed
3. Set the **Display Name** (e.g., "Package - MokoWaaS") — shown in Joomla update manager
2. Set the **Extension Name** (e.g., `pkg_mokosuite`) — this becomes the `<element>` in the XML feed
3. Set the **Display Name** (e.g., "Package - MokoSuite") — shown in Joomla update manager
4. Set the **Extension Type** (component, module, plugin, package, template, library)
5. Set the **Target Version** regex (e.g., `(5|6)\..*` for Joomla 5 and 6)
6. Set the **PHP Minimum** if applicable (e.g., `8.1`)
+5 -5
View File
@@ -7,7 +7,7 @@ The manifest settings feature provides a centralized way to store and manage pro
Each repository can have a manifest that describes:
- **Identity** — project name, organization, description, version, and license
- **Governance** — platform type, moko-platform standards version, and standards source URL
- **Governance** — platform type, mokocli standards version, and standards source URL
- **Build** — language, package type, and entry point
These settings replace the legacy `.mokogitea/manifest.xml` file-based approach.
@@ -35,7 +35,7 @@ If a field already has a value in the database (e.g., from org-level custom fiel
## REST API
The manifest API allows Actions workflows and the moko-platform CLI to read and write manifest settings programmatically.
The manifest API allows Actions workflows and the mokocli CLI to read and write manifest settings programmatically.
### Get Manifest
@@ -57,7 +57,7 @@ Returns the current manifest settings. If no manifest has been saved, returns de
"license_name": "GNU General Public License v3",
"platform": "go",
"standards_version": "05.00.00",
"standards_source": "https://code.mokoconsulting.tech/MokoConsulting/moko-platform",
"standards_source": "https://code.mokoconsulting.tech/MokoConsulting/mokocli",
"language": "Go",
"package_type": "application",
"entry_point": "./"
@@ -101,8 +101,8 @@ Manifest settings are stored in the `repo_manifest` table (migration v347). One
| System | Relationship |
|--------|-------------|
| Update Server | The update server generators read from both manifest settings and update_stream_config. Manifest provides identity metadata; update_stream_config provides feed-specific settings. |
| Custom Fields | Repo-scoped custom fields (org settings) are separate from manifest fields. Custom fields are user-defined; manifest fields follow the moko-platform schema. |
| moko-platform CLI | The CLI reads manifest settings via the API for version bumping, build decisions, and cross-repo syncing (see issue #505). |
| Custom Fields | Repo-scoped custom fields (org settings) are separate from manifest fields. Custom fields are user-defined; manifest fields follow the mokocli schema. |
| mokocli CLI | The CLI reads manifest settings via the API for version bumping, build decisions, and cross-repo syncing (see issue #505). |
---
+1 -1
View File
@@ -195,7 +195,7 @@ curl -X POST -H "Authorization: token TOKEN" \
---
*Repo: [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
*Repo: [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) · [mokocli](https://git.mokoconsulting.tech/MokoConsulting/mokocli/wiki/Home)*
| Revision | Date | Author | Description |
|---|---|---|---|
+2 -2
View File
@@ -23,9 +23,9 @@
## In Progress
- Rename moko-platform to MokoPlatform
- Rename mokocli to MokoCLI
- Granular role-based permissions for all features (#9)
- Wire moko-platform CLI to manifest API (#505)
- Wire mokocli CLI to manifest API (#505)
- Bulk migrate remaining 41 flat wikis to folders
## Planned