Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 56e65268d7 |
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Suggest a new feature or enhancement
|
||||
title: '(feat) '
|
||||
title: '[FEATURE] '
|
||||
labels: 'enhancement'
|
||||
assignees: ''
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: MokoGitea.Workflow
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
# PATH: /.mokogitea/workflows/auto-bump.yml
|
||||
# VERSION: 09.02.00
|
||||
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
||||
@@ -34,8 +34,7 @@ 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.repository.name, 'Template-')
|
||||
!startsWith(github.event.head_commit.message, 'Merge pull request')
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -53,7 +52,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"
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: MokoGitea.Workflow
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||
# PATH: /.mokogitea/workflows/auto-release.yml
|
||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
|
||||
# PATH: /templates/workflows/universal/auto-release.yml.template
|
||||
# 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 "mokogitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "mokogitea-actions[bot]"
|
||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "gitea-actions[bot]"
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
|
||||
- name: Publish RC release
|
||||
@@ -214,8 +214,8 @@ jobs:
|
||||
|
||||
- name: Configure git for bot pushes
|
||||
run: |
|
||||
git config --local user.email "mokogitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "mokogitea-actions[bot]"
|
||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "gitea-actions[bot]"
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
|
||||
- name: Check for merge conflict markers
|
||||
@@ -382,7 +382,7 @@ jobs:
|
||||
content = open('CHANGELOG.md').read()
|
||||
old = '## [Unreleased]'
|
||||
new = f'## [Unreleased]\n\n## [{version}] --- {date}'
|
||||
content = content if ('## [' + version + ']') in content else content.replace(old, new, 1)
|
||||
content = content.replace(old, new, 1)
|
||||
open('CHANGELOG.md', 'w').write(content)
|
||||
" "$VERSION" "$DATE"
|
||||
git add CHANGELOG.md
|
||||
@@ -426,7 +426,7 @@ jobs:
|
||||
&& echo "main branch pushed to GitHub mirror" \
|
||||
|| echo "WARNING: GitHub mirror push failed"
|
||||
|
||||
- name: "Step 11: Delete rc branch (dev reset moved to cascade-dev.yml)"
|
||||
- name: "Step 11: Delete rc branch and recreate dev from main"
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
@@ -438,9 +438,17 @@ jobs:
|
||||
"${API_BASE}/branches/rc" 2>/dev/null \
|
||||
&& echo "Deleted rc branch" || echo "rc branch not found"
|
||||
|
||||
# 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
|
||||
# Delete dev branch
|
||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
|
||||
|
||||
# Recreate dev from main (now includes version bump + changelog promotion)
|
||||
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${API_BASE}/branches" \
|
||||
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
|
||||
|
||||
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: "Step 12: Create version branch from main"
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: MokoGitea.Workflow
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Universal
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
# PATH: /.mokogitea/workflows/branch-cleanup.yml
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Delete feature branches after PR merge
|
||||
|
||||
@@ -1,106 +1,10 @@
|
||||
# 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' }}
|
||||
|
||||
# DISABLED — auto-release Step 11 recreates dev from main after every release.
|
||||
# Cascade-dev is redundant and causes version conflicts when both main and dev
|
||||
# have different version numbers in templateDetails.xml / manifest.xml.
|
||||
name: "Cascade Main → Dev (DISABLED)"
|
||||
on: workflow_dispatch
|
||||
jobs:
|
||||
cascade:
|
||||
name: Cascade main -> dev
|
||||
noop:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- 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
|
||||
- run: echo "Cascade disabled — auto-release handles dev recreation"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: MokoGitea.Workflow
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.CI
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||
# PATH: /.mokogitea/workflows/ci-generic.yml
|
||||
@@ -131,11 +131,10 @@ jobs:
|
||||
test:
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
# Independent job (no `needs: lint`): the Gitea Actions scheduler does not
|
||||
# offer the dependent 2nd job of a needs-chain to runners, so it stalls in
|
||||
# "waiting" and is reaped by ABANDONED_JOB_TIMEOUT. Guard template repos
|
||||
# directly (same condition lint uses) instead of gating on lint's result.
|
||||
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
|
||||
needs: lint
|
||||
# Run only when lint succeeded; always() forces evaluation so a skipped
|
||||
# lint (e.g. template repos) skips this job cleanly instead of hanging.
|
||||
if: ${{ always() && needs.lint.result == 'success' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: MokoGitea.Workflow
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.Universal
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
# PATH: /.mokogitea/workflows/ci-issue-reporter.yml
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Reusable workflow — creates/updates a MokoGitea issue when a CI gate fails.
|
||||
# BRIEF: Reusable workflow — creates/updates a Gitea issue when a CI gate fails.
|
||||
# Clones MokoCLI and runs cli/ci_issue_reporter.sh.
|
||||
|
||||
name: "Universal: CI Issue Reporter"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: MokoGitea.Workflow
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Maintenance
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||
# PATH: /.mokogitea/workflows/cleanup.yml
|
||||
|
||||
@@ -52,69 +52,51 @@ jobs:
|
||||
REGISTRY_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
TAG: ${{ steps.config.outputs.tag }}
|
||||
run: |
|
||||
# Inject runner-side values (TAG, REGISTRY_TOKEN) into the remote shell's
|
||||
# environment via a command prefix, then use a *quoted* heredoc so every
|
||||
# $var below expands in exactly one place: the remote dev host. This avoids
|
||||
# the local-vs-remote expansion trap that previously left TAG empty.
|
||||
HEALTH_FMT='${{ '{{' }}.State.Health.Status${{ '}}' }}'
|
||||
|
||||
ssh -i ~/.ssh/deploy_key -p ${{ env.DEPLOY_PORT }} \
|
||||
-o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||
-o ServerAliveInterval=30 -o ServerAliveCountMax=10 \
|
||||
${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} \
|
||||
"TAG='$TAG' REGISTRY_TOKEN='$REGISTRY_TOKEN' bash -s" <<'DEPLOY_EOF'
|
||||
${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} bash -s <<DEPLOY_EOF
|
||||
set -e
|
||||
echo 'SSH connected to dev environment'
|
||||
|
||||
if [ -z "$TAG" ]; then
|
||||
echo 'ERROR: TAG is empty; refusing to build an untagged image' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
HEALTH_FMT='{{.State.Health.Status}}'
|
||||
|
||||
echo 'Cleaning Docker build cache...'
|
||||
docker builder prune -af 2>/dev/null || true
|
||||
docker image prune -af 2>/dev/null || true
|
||||
|
||||
echo 'Pulling source...'
|
||||
SOURCE_DIR=/opt/gitea-dev/source
|
||||
if [ ! -d "$SOURCE_DIR/.git" ]; then
|
||||
git clone -b dev https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-Fork.git "$SOURCE_DIR"
|
||||
if [ ! -d \$SOURCE_DIR/.git ]; then
|
||||
git clone -b dev https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-Fork.git \$SOURCE_DIR
|
||||
fi
|
||||
cd "$SOURCE_DIR"
|
||||
cd \$SOURCE_DIR
|
||||
git remote set-url origin https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-Fork.git 2>/dev/null || true
|
||||
git fetch origin dev
|
||||
git reset --hard origin/dev
|
||||
|
||||
echo "Building Docker image: ${{ env.REGISTRY }}/${{ env.IMAGE }}:$TAG"
|
||||
echo 'Building Docker image...'
|
||||
docker build --no-cache --build-arg GOFLAGS='-p 1' \
|
||||
--tag "${{ env.REGISTRY }}/${{ env.IMAGE }}:$TAG" \
|
||||
--tag ${{ env.REGISTRY }}/${{ env.IMAGE }}:\$TAG \
|
||||
-f Dockerfile .
|
||||
|
||||
echo 'Pushing to registry...'
|
||||
echo "$REGISTRY_TOKEN" | docker login ${{ env.REGISTRY }} -u ${{ env.DEPLOY_USER }} --password-stdin
|
||||
docker push "${{ env.REGISTRY }}/${{ env.IMAGE }}:$TAG"
|
||||
echo '\$REGISTRY_TOKEN' | docker login ${{ env.REGISTRY }} -u ${{ env.DEPLOY_USER }} --password-stdin
|
||||
docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:\$TAG
|
||||
|
||||
echo 'Restarting dev container...'
|
||||
cd /opt/gitea-dev
|
||||
# The dev service in the SHARED compose file reads its image tag from
|
||||
# ${MOKOGITEA_DEV_TAG}. Drive it from the freshly built tag instead of
|
||||
# rewriting the file with sed: the old sed pattern matched the *prod*
|
||||
# service line (container_name: mokogitea) and left the dev service pinned
|
||||
# to a stale image, so every dev deploy recreated old code while silently
|
||||
# corrupting the prod image pin. The env-var only affects the dev service.
|
||||
# Remove any lingering fixed-name container first so the recreate can't hit
|
||||
# a name conflict, pin the project name for determinism, then force-recreate.
|
||||
docker rm -f mokogitea-dev 2>/dev/null || true
|
||||
MOKOGITEA_DEV_TAG="$TAG" docker compose -p gitea-dev up -d --force-recreate mokogitea-dev
|
||||
sed -i "s|${{ env.IMAGE }}:[^ ]*|${{ env.IMAGE }}:\$TAG|" docker-compose.yml
|
||||
docker compose up -d mokogitea-dev
|
||||
|
||||
echo 'Health check...'
|
||||
for i in 1 2 3 4 5 6 7 8; do
|
||||
sleep 15
|
||||
if docker inspect --format="$HEALTH_FMT" mokogitea-dev 2>/dev/null | grep -q healthy; then
|
||||
if docker inspect --format='\$HEALTH_FMT' mokogitea-dev 2>/dev/null | grep -q healthy; then
|
||||
echo 'Dev container healthy!'
|
||||
exit 0
|
||||
fi
|
||||
echo "Waiting... (attempt $i/8)"
|
||||
echo "Waiting... (attempt \$i/8)"
|
||||
done
|
||||
echo 'Health check failed'
|
||||
docker logs mokogitea-dev --tail 20
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# BRIEF: Build and deploy to the RC environment on push to the rc branch.
|
||||
# Portable across Go server repos: ALL deployment config comes from repo
|
||||
# Actions variables (vars.*) and secrets (secrets.*), nothing hardcoded.
|
||||
# The rc branch is created by promote-rc when a PR to main opens.
|
||||
# OWNER: Template-Go (canonical source; syncs to the root workflows dir). See Template-Go#3.
|
||||
#
|
||||
# Required repo VARIABLES (all tier-scoped so each environment is independent —
|
||||
# a repo may host its rc/dev/prod tiers on different machines):
|
||||
# RC_SSH_HOST, RC_SSH_PORT, RC_SSH_USERNAME - SSH deploy target for the rc tier
|
||||
# RC_REGISTRY, RC_REGISTRY_USER, RC_IMAGE - container registry + login user + image
|
||||
# RC_CONTAINER - compose service/container to recreate
|
||||
# RC_COMPOSE_PROJECT - docker compose -p project name
|
||||
# RC_COMPOSE_DIR - dir containing docker-compose.yml on host
|
||||
# RC_SOURCE_DIR - build source checkout on host
|
||||
# RC_TAG_ENV - compose env-var name that pins the image tag
|
||||
# RC_HEALTH_URL - external URL to verify after deploy
|
||||
# Required SECRETS (already configured org-wide; reused, not re-set):
|
||||
# DEPLOY_SSH_KEY - deploy private key (repo/org secret)
|
||||
# MOKOGITEA_TOKEN - registry/API token (org secret)
|
||||
|
||||
name: Deploy (RC)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- rc
|
||||
# Manual trigger for isolated end-to-end tests without a full RC promotion.
|
||||
# Runs on the ref it is dispatched from; that ref must carry current source
|
||||
# (>= the RC database migration version) or the rebuilt image will refuse the
|
||||
# newer DB. Dispatch from `rc` once `rc` is current.
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy-rc
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
deploy-rc:
|
||||
name: "Build & Deploy to RC"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout source
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Determine version
|
||||
id: config
|
||||
run: |
|
||||
VERSION=$(git describe --tags --always 2>/dev/null || echo "rc-$(git rev-parse --short HEAD)")
|
||||
echo "tag=${VERSION}-rc" >> $GITHUB_OUTPUT
|
||||
echo "Version: ${VERSION}-rc"
|
||||
|
||||
- name: Write deploy key
|
||||
env:
|
||||
DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "$DEPLOY_KEY" > ~/.ssh/deploy_key
|
||||
chmod 600 ~/.ssh/deploy_key
|
||||
|
||||
- name: Build and deploy to RC via SSH
|
||||
env:
|
||||
REGISTRY_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
TAG: ${{ steps.config.outputs.tag }}
|
||||
run: |
|
||||
# Runner-side values (TAG, REGISTRY_TOKEN) are injected into the remote shell
|
||||
# via an env prefix; a *quoted* heredoc keeps every $var expanding once, on the
|
||||
# remote. Repo variables (vars.*) are substituted inline by Actions before ssh.
|
||||
ssh -i ~/.ssh/deploy_key -p ${{ vars.RC_SSH_PORT }} \
|
||||
-o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||
-o ServerAliveInterval=30 -o ServerAliveCountMax=10 \
|
||||
${{ vars.RC_SSH_USERNAME }}@${{ vars.RC_SSH_HOST }} \
|
||||
"TAG='$TAG' REGISTRY_TOKEN='$REGISTRY_TOKEN' bash -s" <<'DEPLOY_EOF'
|
||||
set -e
|
||||
echo 'SSH connected to RC environment'
|
||||
|
||||
if [ -z "$TAG" ]; then
|
||||
echo 'ERROR: TAG is empty; refusing to build an untagged image' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
HEALTH_FMT='{{.State.Health.Status}}'
|
||||
|
||||
echo 'Cleaning Docker build cache...'
|
||||
docker builder prune -af 2>/dev/null || true
|
||||
docker image prune -af 2>/dev/null || true
|
||||
|
||||
echo 'Pulling source...'
|
||||
SOURCE_DIR='${{ vars.RC_SOURCE_DIR }}'
|
||||
if [ ! -d "$SOURCE_DIR/.git" ]; then
|
||||
git clone -b rc ${{ github.server_url }}/${{ github.repository }}.git "$SOURCE_DIR"
|
||||
fi
|
||||
cd "$SOURCE_DIR"
|
||||
git remote set-url origin ${{ github.server_url }}/${{ github.repository }}.git 2>/dev/null || true
|
||||
git fetch origin rc
|
||||
git reset --hard origin/rc
|
||||
|
||||
echo "Building image: ${{ vars.RC_REGISTRY }}/${{ vars.RC_IMAGE }}:$TAG"
|
||||
docker build --no-cache --build-arg GOFLAGS='-p 1' \
|
||||
--tag "${{ vars.RC_REGISTRY }}/${{ vars.RC_IMAGE }}:$TAG" \
|
||||
-f Dockerfile .
|
||||
|
||||
echo 'Pushing to registry...'
|
||||
echo "$REGISTRY_TOKEN" | docker login ${{ vars.RC_REGISTRY }} -u ${{ vars.RC_REGISTRY_USER }} --password-stdin
|
||||
docker push "${{ vars.RC_REGISTRY }}/${{ vars.RC_IMAGE }}:$TAG"
|
||||
|
||||
echo 'Restarting RC container...'
|
||||
cd '${{ vars.RC_COMPOSE_DIR }}'
|
||||
# Drive the rc service image tag via its compose env-var (no sed on the shared
|
||||
# file); remove any lingering fixed-name container first, then force-recreate.
|
||||
docker rm -f '${{ vars.RC_CONTAINER }}' 2>/dev/null || true
|
||||
${{ vars.RC_TAG_ENV }}="$TAG" docker compose -p '${{ vars.RC_COMPOSE_PROJECT }}' up -d --force-recreate '${{ vars.RC_CONTAINER }}'
|
||||
|
||||
echo 'Health check...'
|
||||
for i in 1 2 3 4 5 6 7 8; do
|
||||
sleep 15
|
||||
if docker inspect --format="$HEALTH_FMT" '${{ vars.RC_CONTAINER }}' 2>/dev/null | grep -q healthy; then
|
||||
echo 'RC container healthy!'
|
||||
exit 0
|
||||
fi
|
||||
echo "Waiting... (attempt $i/8)"
|
||||
done
|
||||
echo 'Health check failed'
|
||||
docker logs '${{ vars.RC_CONTAINER }}' --tail 20
|
||||
exit 1
|
||||
DEPLOY_EOF
|
||||
|
||||
- name: Verify RC instance
|
||||
run: |
|
||||
sleep 5
|
||||
curl -sf "${{ vars.RC_HEALTH_URL }}" && echo " RC API healthy"
|
||||
@@ -3,10 +3,10 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: MokoGitea.Workflow
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Security
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
|
||||
# PATH: /.mokogitea/workflows/gitleaks.yml
|
||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||
# PATH: /templates/workflows/gitleaks.yml.template
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
||||
#
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: MokoGitea.Workflow
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.Automation
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: MokoGitea.Workflow
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Notifications
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||
# PATH: /.mokogitea/workflows/notify.yml
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: MokoGitea.Workflow
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.CI
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||
# PATH: /.mokogitea/workflows/pr-check.yml
|
||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
|
||||
# PATH: /templates/workflows/universal/pr-check.yml.template
|
||||
# VERSION: 09.23.00
|
||||
# BRIEF: PR gate — branch policy + code validation before merge
|
||||
|
||||
@@ -97,80 +97,6 @@ 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
|
||||
@@ -210,7 +136,7 @@ jobs:
|
||||
|
||||
- name: Check for merge conflict markers
|
||||
run: |
|
||||
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --exclude-dir='.git' --exclude-dir='.mokogitea' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
||||
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
||||
if [ -n "$CONFLICTS" ]; then
|
||||
echo "::error::Merge conflict markers found in source files"
|
||||
echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -350,7 +276,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 (MokoSuite site)"
|
||||
echo "::warning::No Joomla manifest found (WaaS site)"
|
||||
exit 0
|
||||
fi
|
||||
echo "Manifest: ${MANIFEST}"
|
||||
@@ -363,7 +289,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 MokoGitea 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 Gitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)"
|
||||
echo "$RAW_URLS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: MokoGitea.Workflow
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||
# PATH: /.mokogitea/workflows/pre-release.yml
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||
# 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 "mokogitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "mokogitea-actions[bot]"
|
||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "gitea-actions[bot]"
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
git add -A
|
||||
git diff --cached --quiet || {
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: MokoGitea.Workflow
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.Universal
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
# PATH: /.mokogitea/workflows/rc-revert.yml
|
||||
# VERSION: 09.23.00
|
||||
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
|
||||
@@ -25,8 +25,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.pull_request.merged == false &&
|
||||
startsWith(github.event.pull_request.head.ref, 'rc/') &&
|
||||
!startsWith(github.event.repository.name, 'Template-')
|
||||
startsWith(github.event.pull_request.head.ref, 'rc/')
|
||||
|
||||
steps:
|
||||
- name: Rename branch
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: MokoGitea.Workflow
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.Validation
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||
# PATH: /.mokogitea/workflows/repo-health.yml
|
||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
|
||||
# PATH: /templates/workflows/joomla/repo_health.yml.template
|
||||
# VERSION: 09.23.00
|
||||
# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts.
|
||||
# ============================================================================
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
|
||||
# Hardcoded authorized users — always allowed
|
||||
case "$ACTOR" in
|
||||
jmiller|mokogitea-actions[bot])
|
||||
jmiller|gitea-actions[bot])
|
||||
ALLOWED=true
|
||||
PERMISSION=admin
|
||||
METHOD="hardcoded allowlist"
|
||||
|
||||
@@ -10,12 +10,11 @@ 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
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: MokoGitea.Workflow.Template
|
||||
# DEFGROUP: Gitea.Workflow.Template
|
||||
# INGROUP: MokoStandards.CI
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
|
||||
# PATH: /.mokogitea/workflows/version-set.yml
|
||||
@@ -34,7 +34,6 @@ 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: MokoGitea.Workflow
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.Universal
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
# 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,10 +27,9 @@ jobs:
|
||||
name: Sync workflows to live repos
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
startsWith(github.event.repository.name, 'Template-') &&
|
||||
(github.event_name == 'workflow_dispatch' ||
|
||||
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
|
||||
@@ -53,7 +52,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: |
|
||||
|
||||
@@ -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,11 +57,6 @@
|
||||
- Cherry-pick upstream v1.26.4: walk git log context error handling — regression fix (#38185)
|
||||
|
||||
### Fixed
|
||||
- Fork server binary now compiles: `routers/api/v1/api.go` called `organization.HasOrgOrUserVisible`, which had been renamed to `IsOwnerVisibleToDoer`; the one missed call site broke `go build` of the entire `routers/api/v1` package (CI's Lint & Validate does not run a full build, so it went unnoticed) (#735)
|
||||
- Dev deploy workflow: the build/deploy step referenced runner-side values as `\$TAG` / `\$REGISTRY_TOKEN` inside an unquoted SSH heredoc, deferring expansion to the remote shell where those names are unset — the Docker tag collapsed to an empty `mokogitea:` and every dev deploy failed with `invalid reference format`. Runner values are now injected via an ssh env-prefix and the heredoc is quoted so each `$var` expands in exactly one place (#737)
|
||||
- Repaired unit-test compile and `go vet` failures: `CryptoRandomInt/String/Bytes` now return two values (updated `modules/util/util_test.go`), removed a redundant `&&` condition in `issue_comment.go`, and cleaned up isolated integration-test compile errors (#736)
|
||||
- Removed a stray `package-lock.json` (13.9k lines) that a `git add -A` had accidentally swept into the org-push-policy branch (#734)
|
||||
- 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MokoGitea
|
||||
|
||||
Custom Gitea fork with enhanced wiki system, DLID licensing, issue statuses, cascade merge, security scanning, org-level governance, org metadata, CI standardization, and project board API.
|
||||
Custom Gitea fork with enhanced wiki system, DLID licensing, issue statuses, cascade merge, security scanning, org metadata, CI standardization, and project board API.
|
||||
|
||||
 
|
||||
|
||||
@@ -17,7 +17,6 @@ Custom Gitea fork with enhanced wiki system, DLID licensing, issue statuses, cas
|
||||
- **Default Org Teams** -- auto-create Developers, Reviewers, and CI/CD teams on org creation
|
||||
- **Org Metadata** -- per-repo metadata API (public GET, admin PUT), platform detection for versioning
|
||||
- **Branch Protection** -- delete allowlist for protected branches (per-user/team/deploy-key)
|
||||
- **Org Governance** -- organization-wide rules that layer onto every repository: branch protection as a most-restrictive floor a repo cannot weaken, tag protection (team allowlist), push policy (branch/tag naming, mandatory secret-block, max file size, blocked paths), repository defaults (force-private, PR merge settings), and member email-domain allowlists
|
||||
- **Project Board API** -- REST endpoints for project columns and cards
|
||||
- **CI Infrastructure** -- reusable workflows, centralized ci-issue-reporter, standardized MOKOGITEA_TOKEN naming
|
||||
- **Dev Deploy Gate** -- builds deploy to dev environment first, production checks dev health
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -39,11 +39,6 @@ type OrgProtectedBranch struct {
|
||||
ForcePushAllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
|
||||
ForcePushAllowlistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||
ForcePushAllowlistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
|
||||
CanDelete bool `xorm:"NOT NULL DEFAULT false"`
|
||||
EnableDeleteAllowlist bool `xorm:"NOT NULL DEFAULT false"`
|
||||
DeleteAllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
|
||||
DeleteAllowlistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||
DeleteAllowlistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
|
||||
EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"`
|
||||
StatusCheckContexts []string `xorm:"JSON TEXT"`
|
||||
RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||
@@ -94,8 +89,9 @@ func (o *OrgProtectedBranch) Match(branchName string) bool {
|
||||
|
||||
// ToProtectedBranch converts an org-level rule to a ProtectedBranch for use
|
||||
// in the standard enforcement path. Both team-scoped and user-scoped allowlists
|
||||
// (plus the Actions-bot toggles) are carried across; deploy-key allowances remain
|
||||
// repo-only because an org rule cannot express them.
|
||||
// (plus the "allow the actions bot" toggles) are carried over so org rules can
|
||||
// grant push/merge/force-push/approval access to individual users and the actions
|
||||
// bot, in addition to teams.
|
||||
func (o *OrgProtectedBranch) ToProtectedBranch() *ProtectedBranch {
|
||||
return &ProtectedBranch{
|
||||
ID: o.ID,
|
||||
@@ -115,11 +111,6 @@ func (o *OrgProtectedBranch) ToProtectedBranch() *ProtectedBranch {
|
||||
ForcePushAllowlistTeamIDs: o.ForcePushAllowlistTeamIDs,
|
||||
ForcePushAllowlistUserIDs: o.ForcePushAllowlistUserIDs,
|
||||
ForcePushAllowlistActionsUser: o.ForcePushAllowlistActionsUser,
|
||||
CanDelete: o.CanDelete,
|
||||
EnableDeleteAllowlist: o.EnableDeleteAllowlist,
|
||||
DeleteAllowlistTeamIDs: o.DeleteAllowlistTeamIDs,
|
||||
DeleteAllowlistUserIDs: o.DeleteAllowlistUserIDs,
|
||||
DeleteAllowlistActionsUser: o.DeleteAllowlistActionsUser,
|
||||
EnableStatusCheck: o.EnableStatusCheck,
|
||||
StatusCheckContexts: o.StatusCheckContexts,
|
||||
RequiredApprovals: o.RequiredApprovals,
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestOrgProtectedBranchToProtectedBranch verifies that the org-level rule carries
|
||||
// its team allowlists, user allowlists and actions-user toggles across to the
|
||||
// standard ProtectedBranch used by the enforcement path.
|
||||
func TestOrgProtectedBranchToProtectedBranch(t *testing.T) {
|
||||
org := &OrgProtectedBranch{
|
||||
ID: 7,
|
||||
RuleName: "main",
|
||||
Priority: 3,
|
||||
|
||||
CanPush: true,
|
||||
EnableWhitelist: true,
|
||||
WhitelistTeamIDs: []int64{1, 2},
|
||||
WhitelistUserIDs: []int64{10, 11},
|
||||
WhitelistActionsUser: true,
|
||||
|
||||
EnableMergeWhitelist: true,
|
||||
MergeWhitelistTeamIDs: []int64{3},
|
||||
MergeWhitelistUserIDs: []int64{12},
|
||||
MergeWhitelistActionsUser: true,
|
||||
|
||||
CanForcePush: true,
|
||||
EnableForcePushAllowlist: true,
|
||||
ForcePushAllowlistTeamIDs: []int64{4},
|
||||
ForcePushAllowlistUserIDs: []int64{13, 14},
|
||||
ForcePushAllowlistActionsUser: true,
|
||||
|
||||
RequiredApprovals: 2,
|
||||
EnableApprovalsWhitelist: true,
|
||||
ApprovalsWhitelistTeamIDs: []int64{5},
|
||||
ApprovalsWhitelistUserIDs: []int64{15},
|
||||
}
|
||||
|
||||
pb := org.ToProtectedBranch()
|
||||
|
||||
// Push allowlist
|
||||
assert.Equal(t, []int64{1, 2}, pb.WhitelistTeamIDs)
|
||||
assert.Equal(t, []int64{10, 11}, pb.WhitelistUserIDs)
|
||||
assert.True(t, pb.WhitelistActionsUser)
|
||||
|
||||
// Merge allowlist
|
||||
assert.Equal(t, []int64{3}, pb.MergeWhitelistTeamIDs)
|
||||
assert.Equal(t, []int64{12}, pb.MergeWhitelistUserIDs)
|
||||
assert.True(t, pb.MergeWhitelistActionsUser)
|
||||
|
||||
// Force-push allowlist
|
||||
assert.Equal(t, []int64{4}, pb.ForcePushAllowlistTeamIDs)
|
||||
assert.Equal(t, []int64{13, 14}, pb.ForcePushAllowlistUserIDs)
|
||||
assert.True(t, pb.ForcePushAllowlistActionsUser)
|
||||
|
||||
// Approvals allowlist (no actions-user at the approval stage, matching repo-level)
|
||||
assert.Equal(t, []int64{5}, pb.ApprovalsWhitelistTeamIDs)
|
||||
assert.Equal(t, []int64{15}, pb.ApprovalsWhitelistUserIDs)
|
||||
|
||||
// Sanity: identity fields propagate
|
||||
assert.Equal(t, int64(7), pb.ID)
|
||||
assert.Equal(t, "main", pb.RuleName)
|
||||
assert.Equal(t, int64(3), pb.Priority)
|
||||
assert.EqualValues(t, 2, pb.RequiredApprovals)
|
||||
}
|
||||
|
||||
// TestOrgProtectedBranchToProtectedBranchEmpty verifies that when the org rule does
|
||||
// not grant actions-user access, the toggles remain false (opt-in semantics).
|
||||
func TestOrgProtectedBranchToProtectedBranchEmpty(t *testing.T) {
|
||||
pb := (&OrgProtectedBranch{RuleName: "release/*"}).ToProtectedBranch()
|
||||
|
||||
assert.False(t, pb.WhitelistActionsUser)
|
||||
assert.False(t, pb.MergeWhitelistActionsUser)
|
||||
assert.False(t, pb.ForcePushAllowlistActionsUser)
|
||||
assert.Nil(t, pb.WhitelistUserIDs)
|
||||
assert.Nil(t, pb.MergeWhitelistUserIDs)
|
||||
assert.Nil(t, pb.ForcePushAllowlistUserIDs)
|
||||
assert.Nil(t, pb.ApprovalsWhitelistUserIDs)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -133,16 +112,14 @@ func getFirstMatchOrgProtectedBranchRule(ctx context.Context, repoID int64, bran
|
||||
|
||||
orgRule, err := FindOrgBranchRuleForBranch(ctx, owner.ID, branchName)
|
||||
if err != nil {
|
||||
// Fail closed: propagate the error so callers keep the org floor in force
|
||||
// rather than silently falling back to the repo rule on a transient error.
|
||||
log.Error("FindOrgBranchRuleForBranch: %v", err)
|
||||
return nil, err
|
||||
return nil, nil
|
||||
}
|
||||
if orgRule == nil {
|
||||
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
|
||||
|
||||
@@ -1,202 +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)
|
||||
// Deploy-key allowances are still not expressible in an OrgProtectedBranch, so the
|
||||
// org rule imposes no constraint on them; carry the repo value through unchanged.
|
||||
// ANDing against the org side's always-false zero value would silently lock out
|
||||
// every deploy-key push in any org that has a matching branch rule (see #727 review).
|
||||
// The Actions-bot toggle IS now expressible org-side, so merge it most-restrictively.
|
||||
eff.WhitelistDeployKeys = repoRule.WhitelistDeployKeys
|
||||
eff.WhitelistActionsUser = mergeAllowFlag(repoRule.EnableWhitelist, repoRule.WhitelistActionsUser, orgRule.EnableWhitelist, 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
|
||||
eff.ForcePushAllowlistActionsUser = mergeAllowFlag(repoRule.EnableForcePushAllowlist, repoRule.ForcePushAllowlistActionsUser, orgRule.EnableForcePushAllowlist, 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
|
||||
eff.DeleteAllowlistActionsUser = mergeAllowFlag(repoRule.EnableDeleteAllowlist, repoRule.DeleteAllowlistActionsUser, orgRule.EnableDeleteAllowlist, 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 = mergeAllowFlag(repoRule.EnableMergeWhitelist, repoRule.MergeWhitelistActionsUser, orgRule.EnableMergeWhitelist, 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
|
||||
}
|
||||
}
|
||||
|
||||
// mergeAllowFlag combines two "allow the Actions bot" toggles under most-restrictive
|
||||
// semantics, mirroring mergeAllowlist. A toggle only grants access when its Enable flag
|
||||
// is set; a disabled allowlist means "everyone", so it imposes no constraint. Therefore:
|
||||
// if both allowlists are enabled the bot is allowed only when BOTH sides allow it; if
|
||||
// only one is enabled that side's flag governs; if neither is enabled the flag is
|
||||
// irrelevant (fall back to the repo/a value).
|
||||
func mergeAllowFlag(aEnabled, aFlag, bEnabled, bFlag bool) bool {
|
||||
switch {
|
||||
case aEnabled && bEnabled:
|
||||
return aFlag && bFlag
|
||||
case aEnabled:
|
||||
return aFlag
|
||||
case bEnabled:
|
||||
return bFlag
|
||||
default:
|
||||
return aFlag
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMergeAllowFlag(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
aEnabled bool
|
||||
aFlag bool
|
||||
bEnabled bool
|
||||
bFlag bool
|
||||
expected bool
|
||||
}{
|
||||
// Both allowlists enabled: bot allowed only when BOTH allow it.
|
||||
{"both enabled, both allow", true, true, true, true, true},
|
||||
{"both enabled, a denies", true, false, true, true, false},
|
||||
{"both enabled, b denies", true, true, true, false, false},
|
||||
{"both enabled, both deny", true, false, true, false, false},
|
||||
// Only one side enabled: that side governs.
|
||||
{"only a enabled, a allows", true, true, false, false, true},
|
||||
{"only a enabled, a denies", true, false, false, true, false},
|
||||
{"only b enabled, b allows", false, false, true, true, true},
|
||||
{"only b enabled, b denies", false, true, true, false, false},
|
||||
// Neither enabled: flag irrelevant, falls back to a's flag.
|
||||
{"neither enabled, a true", false, true, false, false, true},
|
||||
{"neither enabled, a false", false, false, false, true, false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
assert.Equal(t, c.expected, mergeAllowFlag(c.aEnabled, c.aFlag, c.bEnabled, c.bFlag))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeMostRestrictiveUserIDsIntersect(t *testing.T) {
|
||||
repoRule := &ProtectedBranch{
|
||||
EnableWhitelist: true,
|
||||
WhitelistUserIDs: []int64{1, 2, 3},
|
||||
// Deploy keys are repo-only and must always pass through unchanged.
|
||||
WhitelistDeployKeys: true,
|
||||
ForcePushAllowlistDeployKeys: true,
|
||||
DeleteAllowlistDeployKeys: true,
|
||||
}
|
||||
orgRule := &ProtectedBranch{
|
||||
EnableWhitelist: true,
|
||||
WhitelistUserIDs: []int64{2, 3, 4},
|
||||
// Org rule cannot express deploy keys; these zero values must NOT override repo.
|
||||
}
|
||||
|
||||
eff := mergeMostRestrictive(repoRule, orgRule)
|
||||
|
||||
// User IDs are intersected when both allowlists are enabled.
|
||||
assert.True(t, eff.EnableWhitelist)
|
||||
assert.ElementsMatch(t, []int64{2, 3}, eff.WhitelistUserIDs)
|
||||
|
||||
// Deploy-key allowances still pass through from the repo rule.
|
||||
assert.True(t, eff.WhitelistDeployKeys)
|
||||
assert.True(t, eff.ForcePushAllowlistDeployKeys)
|
||||
assert.True(t, eff.DeleteAllowlistDeployKeys)
|
||||
}
|
||||
|
||||
func TestMergeMostRestrictiveActionsUserFlags(t *testing.T) {
|
||||
// Both sides enable their allowlist and both allow the bot -> allowed.
|
||||
repoRule := &ProtectedBranch{
|
||||
EnableWhitelist: true,
|
||||
WhitelistActionsUser: true,
|
||||
EnableForcePushAllowlist: true,
|
||||
// force-push: org denies -> effective deny.
|
||||
ForcePushAllowlistActionsUser: true,
|
||||
EnableDeleteAllowlist: true,
|
||||
DeleteAllowlistActionsUser: true,
|
||||
// merge: repo allowlist disabled, org enabled+denies -> org governs (deny).
|
||||
EnableMergeWhitelist: false,
|
||||
MergeWhitelistActionsUser: true,
|
||||
}
|
||||
orgRule := &ProtectedBranch{
|
||||
EnableWhitelist: true,
|
||||
WhitelistActionsUser: true,
|
||||
EnableForcePushAllowlist: true,
|
||||
ForcePushAllowlistActionsUser: false,
|
||||
EnableDeleteAllowlist: true,
|
||||
DeleteAllowlistActionsUser: true,
|
||||
EnableMergeWhitelist: true,
|
||||
MergeWhitelistActionsUser: false,
|
||||
}
|
||||
|
||||
eff := mergeMostRestrictive(repoRule, orgRule)
|
||||
|
||||
assert.True(t, eff.WhitelistActionsUser, "push: both allow")
|
||||
assert.False(t, eff.ForcePushAllowlistActionsUser, "force-push: org denies")
|
||||
assert.True(t, eff.DeleteAllowlistActionsUser, "delete: both allow")
|
||||
assert.False(t, eff.MergeWhitelistActionsUser, "merge: org enabled and denies")
|
||||
}
|
||||
@@ -439,12 +439,7 @@ 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),
|
||||
newMigration(367, "Add user allowlists to org protected branch", v1_27.AddUserAllowlistsToOrgProtectedBranch),
|
||||
newMigration(362, "Add user allowlists and actions-user toggles to org protected branch", v1_27.AddUserAllowlistsToOrgProtectedBranch),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
// AddRepoManifestTable creates the repo_manifest table for storing
|
||||
// mokocli manifest settings per repository.
|
||||
// mokoplatform manifest settings per repository.
|
||||
func AddRepoManifestTable(x *xorm.Engine) error {
|
||||
type RepoManifest struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
|
||||
@@ -3,16 +3,22 @@
|
||||
|
||||
package v1_27
|
||||
|
||||
import "xorm.io/xorm"
|
||||
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 {
|
||||
// AddUserAllowlistsToOrgProtectedBranch adds per-user allowlists (by user ID) and
|
||||
// "allow the actions bot" toggles to org-level branch protection rules, alongside
|
||||
// the existing team-only allowlists. Additive columns only.
|
||||
func AddUserAllowlistsToOrgProtectedBranch(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"`
|
||||
WhitelistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||
WhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
|
||||
MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||
MergeWhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
|
||||
ForcePushAllowlistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||
ForcePushAllowlistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
|
||||
ApprovalsWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||
}
|
||||
return x.Sync(new(OrgProtectedBranch))
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package v1_27
|
||||
|
||||
import "xorm.io/xorm"
|
||||
|
||||
// AddUserAllowlistsToOrgProtectedBranch adds per-user allowlist columns (username or
|
||||
// email resolved to user IDs) plus the "allow Actions bot" toggles to org-level branch
|
||||
// protection rules for the push, merge, force-push and delete categories, mirroring the
|
||||
// per-repo user allowlists so an org rule can also grant access to individual users and
|
||||
// the Actions bot. See issue #727.
|
||||
func AddUserAllowlistsToOrgProtectedBranch(x *xorm.Engine) error {
|
||||
type OrgProtectedBranch struct {
|
||||
WhitelistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||
WhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
|
||||
MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||
MergeWhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
|
||||
ForcePushAllowlistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||
ForcePushAllowlistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
|
||||
DeleteAllowlistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||
DeleteAllowlistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
|
||||
ApprovalsWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||
}
|
||||
return x.Sync(new(OrgProtectedBranch))
|
||||
}
|
||||
@@ -15,50 +15,50 @@ func init() {
|
||||
db.RegisterModel(new(RepoMetadata))
|
||||
}
|
||||
|
||||
// RepoMetadata stores mokocli metadata settings for a repository.
|
||||
// These first-class fields are the authoritative repository metadata,
|
||||
// exposed via API for use by Actions workflows and mokocli.
|
||||
// RepoMetadata stores mokoplatform metadata settings for a repository.
|
||||
// These fields correspond to the .mokogitea/manifest.xml schema and are
|
||||
// exposed via API for use by Actions workflows and the mokoplatform CLI.
|
||||
type RepoMetadata struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"UNIQUE INDEX NOT NULL 'repo_id'"`
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"UNIQUE INDEX NOT NULL 'repo_id'"`
|
||||
|
||||
// identity section
|
||||
Name string `xorm:"TEXT 'name'"` // project name
|
||||
Org string `xorm:"TEXT 'org'"` // organization name
|
||||
Description string `xorm:"TEXT 'description'"` // project description
|
||||
Name string `xorm:"TEXT 'name'"` // project name
|
||||
Org string `xorm:"TEXT 'org'"` // organization name
|
||||
Description string `xorm:"TEXT 'description'"` // project description
|
||||
LicenseSPDX string `xorm:"VARCHAR(50) 'license_spdx'"` // SPDX identifier, e.g. "GPL-3.0-or-later"
|
||||
LicenseName string `xorm:"TEXT 'license_name'"` // human-readable license name
|
||||
LicenseName string `xorm:"TEXT 'license_name'"` // human-readable license name
|
||||
|
||||
// governance section
|
||||
Platform string `xorm:"VARCHAR(50) 'platform'"` // go, php, node, python, etc.
|
||||
StandardsVersion string `xorm:"VARCHAR(20) 'standards_version'"` // mokocli standards version
|
||||
StandardsSource string `xorm:"TEXT 'standards_source'"` // URL to standards repo
|
||||
StandardsVersion string `xorm:"VARCHAR(20) 'standards_version'"` // mokoplatform standards version
|
||||
StandardsSource string `xorm:"TEXT 'standards_source'"` // URL to standards repo
|
||||
|
||||
// versioning
|
||||
VersionPrefix string `xorm:"TEXT 'version_prefix'"` // tag prefix stripped for version display, e.g. "v1.26.1-moko."
|
||||
ElementName string `xorm:"TEXT 'element_name'"` // full element name override, e.g. "pkg_mokowaas" (auto-constructed if empty)
|
||||
VersionPrefix string `xorm:"TEXT 'version_prefix'"` // tag prefix stripped for version display, e.g. "v1.26.1-moko."
|
||||
ElementName string `xorm:"TEXT 'element_name'"` // full element name override, e.g. "pkg_mokowaas" (auto-constructed if empty)
|
||||
|
||||
// distribution metadata (used by update server feed generation)
|
||||
Maintainer string `xorm:"TEXT 'maintainer'"` // maintainer/author name
|
||||
MaintainerURL string `xorm:"TEXT 'maintainer_url'"` // maintainer website
|
||||
InfoURL string `xorm:"TEXT 'info_url'"` // extension info/product page URL
|
||||
TargetVersion string `xorm:"TEXT 'target_version'"` // target platform version regex, e.g. "(5|6)\..*"
|
||||
Maintainer string `xorm:"TEXT 'maintainer'"` // maintainer/author name
|
||||
MaintainerURL string `xorm:"TEXT 'maintainer_url'"` // maintainer website
|
||||
InfoURL string `xorm:"TEXT 'info_url'"` // extension info/product page URL
|
||||
TargetVersion string `xorm:"TEXT 'target_version'"` // target platform version regex, e.g. "(5|6)\..*"
|
||||
PHPMinimum string `xorm:"VARCHAR(20) 'php_minimum'"` // minimum PHP version, e.g. "8.1"
|
||||
|
||||
// build section
|
||||
Language string `xorm:"VARCHAR(50) 'language'"` // Go, PHP, TypeScript, etc.
|
||||
Language string `xorm:"VARCHAR(50) 'language'"` // Go, PHP, TypeScript, etc.
|
||||
ExtensionType string `xorm:"VARCHAR(50) 'extension_type'"` // component, module, plugin, package, template, library, file
|
||||
EntryPoint string `xorm:"TEXT 'entry_point'"` // build entry point path
|
||||
EntryPoint string `xorm:"TEXT 'entry_point'"` // build entry point path
|
||||
|
||||
// deploy section
|
||||
DeployHost string `xorm:"VARCHAR(255) 'deploy_host'"` // SSH host for deploy
|
||||
DeployPort string `xorm:"VARCHAR(10) 'deploy_port'"` // SSH port (default 2918)
|
||||
DeployUser string `xorm:"VARCHAR(100) 'deploy_user'"` // SSH user
|
||||
DeployPath string `xorm:"TEXT 'deploy_path'"` // remote path for source/compose
|
||||
DeployPath string `xorm:"TEXT 'deploy_path'"` // remote path for source/compose
|
||||
DockerImage string `xorm:"VARCHAR(255) 'docker_image'"` // e.g. mokoconsulting/mokogitea
|
||||
DockerRegistry string `xorm:"VARCHAR(255) 'docker_registry'"` // e.g. git.mokoconsulting.tech
|
||||
ContainerName string `xorm:"VARCHAR(100) 'container_name'"` // Docker container name
|
||||
HealthURL string `xorm:"TEXT 'health_url'"` // health check URL after deploy
|
||||
HealthURL string `xorm:"TEXT 'health_url'"` // health check URL after deploy
|
||||
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"`
|
||||
|
||||
@@ -21,11 +21,6 @@ type OrgBranchProtection struct {
|
||||
ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"`
|
||||
ForcePushAllowlistUsernames []string `json:"force_push_allowlist_usernames"`
|
||||
ForcePushAllowlistActionsUser bool `json:"force_push_allowlist_actions_user"`
|
||||
EnableDelete bool `json:"enable_delete"`
|
||||
EnableDeleteAllowlist bool `json:"enable_delete_allowlist"`
|
||||
DeleteAllowlistTeams []string `json:"delete_allowlist_teams"`
|
||||
DeleteAllowlistUsernames []string `json:"delete_allowlist_usernames"`
|
||||
DeleteAllowlistActionsUser bool `json:"delete_allowlist_actions_user"`
|
||||
EnableMergeWhitelist bool `json:"enable_merge_whitelist"`
|
||||
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
|
||||
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"`
|
||||
@@ -65,11 +60,6 @@ type CreateOrgBranchProtectionOption struct {
|
||||
ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"`
|
||||
ForcePushAllowlistUsernames []string `json:"force_push_allowlist_usernames"`
|
||||
ForcePushAllowlistActionsUser bool `json:"force_push_allowlist_actions_user"`
|
||||
EnableDelete bool `json:"enable_delete"`
|
||||
EnableDeleteAllowlist bool `json:"enable_delete_allowlist"`
|
||||
DeleteAllowlistTeams []string `json:"delete_allowlist_teams"`
|
||||
DeleteAllowlistUsernames []string `json:"delete_allowlist_usernames"`
|
||||
DeleteAllowlistActionsUser bool `json:"delete_allowlist_actions_user"`
|
||||
EnableMergeWhitelist bool `json:"enable_merge_whitelist"`
|
||||
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
|
||||
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"`
|
||||
@@ -104,11 +94,6 @@ type EditOrgBranchProtectionOption struct {
|
||||
ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"`
|
||||
ForcePushAllowlistUsernames []string `json:"force_push_allowlist_usernames"`
|
||||
ForcePushAllowlistActionsUser *bool `json:"force_push_allowlist_actions_user"`
|
||||
EnableDelete *bool `json:"enable_delete"`
|
||||
EnableDeleteAllowlist *bool `json:"enable_delete_allowlist"`
|
||||
DeleteAllowlistTeams []string `json:"delete_allowlist_teams"`
|
||||
DeleteAllowlistUsernames []string `json:"delete_allowlist_usernames"`
|
||||
DeleteAllowlistActionsUser *bool `json:"delete_allowlist_actions_user"`
|
||||
EnableMergeWhitelist *bool `json:"enable_merge_whitelist"`
|
||||
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
|
||||
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"`
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
+10
-18
@@ -86,35 +86,31 @@ func Test_NormalizeEOL(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_RandomInt(t *testing.T) {
|
||||
randInt, err := CryptoRandomInt(255)
|
||||
assert.NoError(t, err)
|
||||
randInt := CryptoRandomInt(255)
|
||||
assert.GreaterOrEqual(t, randInt, int64(0))
|
||||
assert.LessOrEqual(t, randInt, int64(255))
|
||||
}
|
||||
|
||||
func Test_RandomString(t *testing.T) {
|
||||
str1, err := CryptoRandomString(32)
|
||||
assert.NoError(t, err)
|
||||
str1 := CryptoRandomString(32)
|
||||
var err error
|
||||
matches, err := regexp.MatchString(`^[a-zA-Z0-9]{32}$`, str1)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, matches)
|
||||
|
||||
str2, err := CryptoRandomString(32)
|
||||
assert.NoError(t, err)
|
||||
str2 := CryptoRandomString(32)
|
||||
matches, err = regexp.MatchString(`^[a-zA-Z0-9]{32}$`, str1)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, matches)
|
||||
|
||||
assert.NotEqual(t, str1, str2)
|
||||
|
||||
str3, err := CryptoRandomString(256)
|
||||
assert.NoError(t, err)
|
||||
str3 := CryptoRandomString(256)
|
||||
matches, err = regexp.MatchString(`^[a-zA-Z0-9]{256}$`, str3)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, matches)
|
||||
|
||||
str4, err := CryptoRandomString(256)
|
||||
assert.NoError(t, err)
|
||||
str4 := CryptoRandomString(256)
|
||||
matches, err = regexp.MatchString(`^[a-zA-Z0-9]{256}$`, str4)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, matches)
|
||||
@@ -123,19 +119,15 @@ func Test_RandomString(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_RandomBytes(t *testing.T) {
|
||||
bytes1, err := CryptoRandomBytes(32)
|
||||
assert.NoError(t, err)
|
||||
bytes1 := CryptoRandomBytes(32)
|
||||
|
||||
bytes2, err := CryptoRandomBytes(32)
|
||||
assert.NoError(t, err)
|
||||
bytes2 := CryptoRandomBytes(32)
|
||||
|
||||
assert.NotEqual(t, bytes1, bytes2)
|
||||
|
||||
bytes3, err := CryptoRandomBytes(256)
|
||||
assert.NoError(t, err)
|
||||
bytes3 := CryptoRandomBytes(256)
|
||||
|
||||
bytes4, err := CryptoRandomBytes(256)
|
||||
assert.NoError(t, err)
|
||||
bytes4 := CryptoRandomBytes(256)
|
||||
|
||||
assert.NotEqual(t, bytes3, bytes4)
|
||||
}
|
||||
|
||||
@@ -2411,31 +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.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",
|
||||
@@ -2464,7 +2439,7 @@
|
||||
"repo.settings.protect_force_push_allowlist_teams": "Allowlisted teams for force pushing:",
|
||||
"repo.settings.protect_force_push_allowlist_deploy_keys": "Allowlist deploy keys with push access to force push.",
|
||||
"repo.settings.protect_force_push_allowlist_actions_user": "Allowlist actions bot user to force push.",
|
||||
"repo.settings.protect_branch_deletion": "Branch Deletion",
|
||||
"repo.settings.event_delete": "Branch Deletion",
|
||||
"repo.settings.protect_disable_delete": "Disable Deletion",
|
||||
"repo.settings.protect_disable_delete_desc": "This branch cannot be deleted.",
|
||||
"repo.settings.protect_enable_delete_all": "Enable Deletion",
|
||||
@@ -2772,7 +2747,7 @@
|
||||
"repo.settings.support_url_help": "Shown when downloads are gated. Can point to your wiki, product page, or external support site.",
|
||||
"repo.settings.custom_fields": "Custom Fields",
|
||||
"repo.settings.manifest": "Manifest",
|
||||
"repo.settings.manifest_desc": "Project identity, governance, and build settings for the repository. These are accessible via API for Actions workflows and MokoCLI.",
|
||||
"repo.settings.manifest_desc": "Project identity, governance, and build settings from the MokoPlatform manifest. These are accessible via API for Actions workflows and the MokoPlatform CLI.",
|
||||
"repo.settings.manifest_identity": "Identity",
|
||||
"repo.settings.manifest_name": "Project Name",
|
||||
"repo.settings.manifest_element_name": "Element Name",
|
||||
|
||||
+26
-52
@@ -516,7 +516,7 @@ func reqOrgVisible() func(ctx *context.APIContext) {
|
||||
ctx.APIErrorInternal(errors.New("reqOrgVisible: unprepared context"))
|
||||
return
|
||||
}
|
||||
if !organization.IsOwnerVisibleToDoer(ctx, ctx.Org.Organization.AsUser(), ctx.Doer) {
|
||||
if !organization.HasOrgOrUserVisible(ctx, ctx.Org.Organization.AsUser(), ctx.Doer) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
@@ -1543,6 +1543,8 @@ func Routes() *web.Router {
|
||||
}, reqAnyRepoReader())
|
||||
m.Get("/metadata", repo.GetRepoMetadata)
|
||||
m.Put("/metadata", reqToken(), reqAdmin(), repo.UpdateRepoMetadata)
|
||||
m.Get("/manifest", repo.GetRepoMetadata) // backward compat
|
||||
m.Put("/manifest", reqToken(), reqAdmin(), repo.UpdateRepoMetadata)
|
||||
// MokoGitea badge engine
|
||||
m.Get("/badge/{type}.svg", repo.GetRepoBadge)
|
||||
m.Get("/issue_templates", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(), repo.GetIssueTemplates)
|
||||
@@ -1696,29 +1698,29 @@ func Routes() *web.Router {
|
||||
Patch(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditMilestoneOption{}), repo.EditMilestone).
|
||||
Delete(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteMilestone)
|
||||
})
|
||||
// m.Group("/projects", func() {
|
||||
// m.Combo("").Get(repo.ListProjects).
|
||||
// Post(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.CreateProjectOption{}), repo.CreateProject)
|
||||
// m.Group("/{id}", func() {
|
||||
// m.Combo("").Get(repo.GetProject).
|
||||
// Patch(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.EditProjectOption{}), repo.EditProject).
|
||||
// Delete(reqToken(), reqRepoWriter(unit.TypeProjects), repo.DeleteProject)
|
||||
// m.Post("/{action}", reqToken(), reqRepoWriter(unit.TypeProjects), repo.ChangeProjectStatus)
|
||||
// m.Group("/columns", func() {
|
||||
// m.Combo("").Get(repo.ListProjectColumns).
|
||||
// Post(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.CreateProjectColumnOption{}), repo.CreateProjectColumn)
|
||||
// m.Group("/{columnId}", func() {
|
||||
// m.Delete("", reqToken(), reqRepoWriter(unit.TypeProjects), repo.DeleteProjectColumn)
|
||||
// m.Combo("/issues").Get(repo.ListProjectColumnIssues).
|
||||
// Post(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.AddProjectColumnIssueOption{}), repo.AddIssueToColumn)
|
||||
// })
|
||||
// })
|
||||
// m.Group("/issues/{issueId}", func() {
|
||||
// m.Patch("/move", reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.MoveProjectColumnIssueOption{}), repo.MoveIssueOnColumn)
|
||||
// m.Delete("", reqToken(), reqRepoWriter(unit.TypeProjects), repo.RemoveIssueFromProject)
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// m.Group("/projects", func() {
|
||||
// m.Combo("").Get(repo.ListProjects).
|
||||
// Post(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.CreateProjectOption{}), repo.CreateProject)
|
||||
// m.Group("/{id}", func() {
|
||||
// m.Combo("").Get(repo.GetProject).
|
||||
// Patch(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.EditProjectOption{}), repo.EditProject).
|
||||
// Delete(reqToken(), reqRepoWriter(unit.TypeProjects), repo.DeleteProject)
|
||||
// m.Post("/{action}", reqToken(), reqRepoWriter(unit.TypeProjects), repo.ChangeProjectStatus)
|
||||
// m.Group("/columns", func() {
|
||||
// m.Combo("").Get(repo.ListProjectColumns).
|
||||
// Post(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.CreateProjectColumnOption{}), repo.CreateProjectColumn)
|
||||
// m.Group("/{columnId}", func() {
|
||||
// m.Delete("", reqToken(), reqRepoWriter(unit.TypeProjects), repo.DeleteProjectColumn)
|
||||
// m.Combo("/issues").Get(repo.ListProjectColumnIssues).
|
||||
// Post(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.AddProjectColumnIssueOption{}), repo.AddIssueToColumn)
|
||||
// })
|
||||
// })
|
||||
// m.Group("/issues/{issueId}", func() {
|
||||
// m.Patch("/move", reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.MoveProjectColumnIssueOption{}), repo.MoveIssueOnColumn)
|
||||
// m.Delete("", reqToken(), reqRepoWriter(unit.TypeProjects), repo.RemoveIssueFromProject)
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// Repo custom fields (repo-scoped key-value metadata)
|
||||
m.Group("/custom-fields", func() {
|
||||
m.Get("", repo.GetRepoCustomFields)
|
||||
@@ -1821,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() {
|
||||
|
||||
@@ -64,11 +64,6 @@ func toAPIOrgBranchProtection(ctx *context.APIContext, rule *git_model.OrgProtec
|
||||
ForcePushAllowlistTeams: resolveTeamNames(rule.ForcePushAllowlistTeamIDs),
|
||||
ForcePushAllowlistUsernames: resolveUserNames(rule.ForcePushAllowlistUserIDs),
|
||||
ForcePushAllowlistActionsUser: rule.ForcePushAllowlistActionsUser,
|
||||
EnableDelete: rule.CanDelete,
|
||||
EnableDeleteAllowlist: rule.EnableDeleteAllowlist,
|
||||
DeleteAllowlistTeams: resolveTeamNames(rule.DeleteAllowlistTeamIDs),
|
||||
DeleteAllowlistUsernames: resolveUserNames(rule.DeleteAllowlistUserIDs),
|
||||
DeleteAllowlistActionsUser: rule.DeleteAllowlistActionsUser,
|
||||
EnableMergeWhitelist: rule.EnableMergeWhitelist,
|
||||
MergeWhitelistTeams: resolveTeamNames(rule.MergeWhitelistTeamIDs),
|
||||
MergeWhitelistUsernames: resolveUserNames(rule.MergeWhitelistUserIDs),
|
||||
@@ -110,9 +105,10 @@ func resolveTeamIDs(ctx *context.APIContext, orgID int64, names []string) ([]int
|
||||
return ids, true
|
||||
}
|
||||
|
||||
// resolveUserIDs converts a list of user identifiers (each a username or an email
|
||||
// address) to deduped user IDs, returning 422 on the first unknown identifier. Each
|
||||
// entry is looked up by username first, then falls back to an activated email address.
|
||||
// resolveUserIDs converts a list of usernames or emails to user IDs, returning 422
|
||||
// on any entry that matches no user. Each entry is first looked up by username and,
|
||||
// if that user does not exist, by email address. Empty entries are ignored and the
|
||||
// resulting IDs are deduplicated.
|
||||
func resolveUserIDs(ctx *context.APIContext, names []string) ([]int64, bool) {
|
||||
if len(names) == 0 {
|
||||
return nil, true
|
||||
@@ -120,6 +116,9 @@ func resolveUserIDs(ctx *context.APIContext, names []string) ([]int64, bool) {
|
||||
seen := make(map[int64]struct{}, len(names))
|
||||
ids := make([]int64, 0, len(names))
|
||||
for _, name := range names {
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
u, err := user_model.GetUserByName(ctx, name)
|
||||
if err != nil {
|
||||
if !user_model.IsErrUserNotExist(err) {
|
||||
@@ -129,14 +128,14 @@ func resolveUserIDs(ctx *context.APIContext, names []string) ([]int64, bool) {
|
||||
u, err = user_model.GetUserByEmail(ctx, name)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "unknown user: "+name)
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "user does not exist: "+name)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
if _, dup := seen[u.ID]; dup {
|
||||
if _, ok := seen[u.ID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[u.ID] = struct{}{}
|
||||
@@ -271,10 +270,7 @@ func CreateOrgBranchProtection(ctx *context.APIContext) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
deleteTeams, ok := resolveTeamIDs(ctx, orgID, form.DeleteAllowlistTeams)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
pushUsers, ok := resolveUserIDs(ctx, form.PushWhitelistUsernames)
|
||||
if !ok {
|
||||
return
|
||||
@@ -291,30 +287,24 @@ func CreateOrgBranchProtection(ctx *context.APIContext) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
deleteUsers, ok := resolveUserIDs(ctx, form.DeleteAllowlistUsernames)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
enablePushWhitelist := form.EnablePush && form.EnablePushWhitelist
|
||||
enableForcePushAllowlist := form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist
|
||||
|
||||
rule := &git_model.OrgProtectedBranch{
|
||||
OrgID: orgID,
|
||||
RuleName: form.RuleName,
|
||||
Priority: form.Priority,
|
||||
CanPush: form.EnablePush,
|
||||
EnableWhitelist: form.EnablePush && form.EnablePushWhitelist,
|
||||
EnableWhitelist: enablePushWhitelist,
|
||||
WhitelistTeamIDs: pushTeams,
|
||||
WhitelistUserIDs: pushUsers,
|
||||
WhitelistActionsUser: form.EnablePush && form.EnablePushWhitelist && form.PushWhitelistActionsUser,
|
||||
WhitelistActionsUser: enablePushWhitelist && form.PushWhitelistActionsUser,
|
||||
CanForcePush: form.EnablePush && form.EnableForcePush,
|
||||
EnableForcePushAllowlist: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist,
|
||||
EnableForcePushAllowlist: enableForcePushAllowlist,
|
||||
ForcePushAllowlistTeamIDs: forcePushTeams,
|
||||
ForcePushAllowlistUserIDs: forcePushUsers,
|
||||
ForcePushAllowlistActionsUser: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist && form.ForcePushAllowlistActionsUser,
|
||||
CanDelete: form.EnableDelete,
|
||||
EnableDeleteAllowlist: form.EnableDelete && form.EnableDeleteAllowlist,
|
||||
DeleteAllowlistTeamIDs: deleteTeams,
|
||||
DeleteAllowlistUserIDs: deleteUsers,
|
||||
DeleteAllowlistActionsUser: form.EnableDelete && form.EnableDeleteAllowlist && form.DeleteAllowlistActionsUser,
|
||||
ForcePushAllowlistActionsUser: enableForcePushAllowlist && form.ForcePushAllowlistActionsUser,
|
||||
EnableMergeWhitelist: form.EnableMergeWhitelist,
|
||||
MergeWhitelistTeamIDs: mergeTeams,
|
||||
MergeWhitelistUserIDs: mergeUsers,
|
||||
@@ -439,29 +429,6 @@ func EditOrgBranchProtection(ctx *context.APIContext) {
|
||||
if form.ForcePushAllowlistActionsUser != nil {
|
||||
rule.ForcePushAllowlistActionsUser = *form.ForcePushAllowlistActionsUser
|
||||
}
|
||||
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.DeleteAllowlistUsernames != nil {
|
||||
ids, ok := resolveUserIDs(ctx, form.DeleteAllowlistUsernames)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
rule.DeleteAllowlistUserIDs = ids
|
||||
}
|
||||
if form.DeleteAllowlistActionsUser != nil {
|
||||
rule.DeleteAllowlistActionsUser = *form.DeleteAllowlistActionsUser
|
||||
}
|
||||
if form.EnableMergeWhitelist != nil {
|
||||
rule.EnableMergeWhitelist = *form.EnableMergeWhitelist
|
||||
}
|
||||
|
||||
@@ -1,124 +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) {
|
||||
// swagger:operation GET /orgs/{org}/email_domain_policy organization orgGetEmailDomainPolicy
|
||||
// ---
|
||||
// summary: Get the organization's email domain policy
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/OrgEmailDomainPolicy"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
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) {
|
||||
// swagger:operation PATCH /orgs/{org}/email_domain_policy organization orgEditEmailDomainPolicy
|
||||
// ---
|
||||
// summary: Create or update the organization's email domain policy. Only fields that are set will be changed
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EditOrgEmailDomainPolicyOption"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/OrgEmailDomainPolicy"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
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) {
|
||||
// swagger:operation DELETE /orgs/{org}/email_domain_policy organization orgDeleteEmailDomainPolicy
|
||||
// ---
|
||||
// summary: Remove the organization's email domain policy
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
if err := git_model.DeleteOrgEmailDomainPolicy(ctx, ctx.Org.Organization.ID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
@@ -1,145 +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) {
|
||||
// swagger:operation GET /orgs/{org}/push_policy organization orgGetPushPolicy
|
||||
// ---
|
||||
// summary: Get the organization's push policy
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/OrgPushPolicy"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
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) {
|
||||
// swagger:operation PATCH /orgs/{org}/push_policy organization orgEditPushPolicy
|
||||
// ---
|
||||
// summary: Create or update the organization's push policy. Only fields that are set will be changed
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EditOrgPushPolicyOption"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/OrgPushPolicy"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
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) {
|
||||
// swagger:operation DELETE /orgs/{org}/push_policy organization orgDeletePushPolicy
|
||||
// ---
|
||||
// summary: Remove the organization's push policy
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
if err := git_model.DeleteOrgPushPolicy(ctx, ctx.Org.Organization.ID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
@@ -1,174 +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) {
|
||||
// swagger:operation GET /orgs/{org}/repo_defaults organization orgGetRepoDefaults
|
||||
// ---
|
||||
// summary: Get the organization's default repository settings
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/OrgRepoDefaults"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
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) {
|
||||
// swagger:operation PATCH /orgs/{org}/repo_defaults organization orgEditRepoDefaults
|
||||
// ---
|
||||
// summary: Create or update the organization's default repository settings. Only fields that are set will be changed
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EditOrgRepoDefaultsOption"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/OrgRepoDefaults"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
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) {
|
||||
// swagger:operation DELETE /orgs/{org}/repo_defaults organization orgDeleteRepoDefaults
|
||||
// ---
|
||||
// summary: Remove the organization's default repository settings
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
if err := git_model.DeleteOrgRepoDefaults(ctx, ctx.Org.Organization.ID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
@@ -1,271 +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) {
|
||||
// swagger:operation GET /orgs/{org}/tag_protections organization orgListTagProtections
|
||||
// ---
|
||||
// summary: List an organization's tag protection rules
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/OrgTagProtectionList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
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) {
|
||||
// swagger:operation GET /orgs/{org}/tag_protections/{id} organization orgGetTagProtection
|
||||
// ---
|
||||
// summary: Get a specific org-level tag protection rule
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the tag protection rule
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/OrgTagProtection"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
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) {
|
||||
// swagger:operation POST /orgs/{org}/tag_protections organization orgCreateTagProtection
|
||||
// ---
|
||||
// summary: Create an org-level tag protection rule
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/CreateOrgTagProtectionOption"
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/OrgTagProtection"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
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) {
|
||||
// swagger:operation PATCH /orgs/{org}/tag_protections/{id} organization orgEditTagProtection
|
||||
// ---
|
||||
// summary: Edit an org-level tag protection rule. Only fields that are set will be changed
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the tag protection rule
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EditOrgTagProtectionOption"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/OrgTagProtection"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
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) {
|
||||
// swagger:operation DELETE /orgs/{org}/tag_protections/{id} organization orgDeleteTagProtection
|
||||
// ---
|
||||
// summary: Delete an org-level tag protection rule
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the tag protection rule
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ type apiMetadata struct {
|
||||
TargetVersion string `json:"target_version"`
|
||||
PHPMinimum string `json:"php_minimum"`
|
||||
Language string `json:"language"`
|
||||
ExtensionType string `json:"extension_type"`
|
||||
ExtensionType string `json:"extension_type"`
|
||||
EntryPoint string `json:"entry_point"`
|
||||
|
||||
// deploy
|
||||
@@ -44,13 +44,6 @@ type apiMetadata struct {
|
||||
HealthURL string `json:"health_url,omitempty"`
|
||||
}
|
||||
|
||||
// Manifest
|
||||
// swagger:response Manifest
|
||||
type swaggerResponseManifest struct {
|
||||
// in:body
|
||||
Body apiMetadata `json:"body"`
|
||||
}
|
||||
|
||||
// GetRepoMetadata returns the manifest settings for a repository.
|
||||
func GetRepoMetadata(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/manifest repository repoGetManifest
|
||||
@@ -58,17 +51,6 @@ func GetRepoMetadata(ctx *context.APIContext) {
|
||||
// summary: Get repo manifest settings
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Manifest"
|
||||
@@ -89,9 +71,9 @@ func GetRepoMetadata(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, &apiMetadata{
|
||||
Name: m.Name,
|
||||
Org: m.Org,
|
||||
Description: m.Description,
|
||||
Name: m.Name,
|
||||
Org: m.Org,
|
||||
Description: m.Description,
|
||||
|
||||
LicenseSPDX: m.LicenseSPDX,
|
||||
LicenseName: m.LicenseName,
|
||||
@@ -107,7 +89,7 @@ func GetRepoMetadata(ctx *context.APIContext) {
|
||||
TargetVersion: m.TargetVersion,
|
||||
PHPMinimum: m.PHPMinimum,
|
||||
Language: m.Language,
|
||||
ExtensionType: m.ExtensionType,
|
||||
ExtensionType: m.ExtensionType,
|
||||
EntryPoint: m.EntryPoint,
|
||||
DeployHost: m.DeployHost,
|
||||
DeployPort: m.DeployPort,
|
||||
@@ -129,21 +111,9 @@ func UpdateRepoMetadata(ctx *context.APIContext) {
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Manifest"
|
||||
|
||||
// Decode into a map to detect which fields were actually sent.
|
||||
var raw map[string]any
|
||||
if err := json.NewDecoder(ctx.Req.Body).Decode(&raw); err != nil {
|
||||
@@ -203,9 +173,9 @@ func UpdateRepoMetadata(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, &apiMetadata{
|
||||
Name: m.Name,
|
||||
Org: m.Org,
|
||||
Description: m.Description,
|
||||
Name: m.Name,
|
||||
Org: m.Org,
|
||||
Description: m.Description,
|
||||
|
||||
LicenseSPDX: m.LicenseSPDX,
|
||||
LicenseName: m.LicenseName,
|
||||
@@ -221,7 +191,7 @@ func UpdateRepoMetadata(ctx *context.APIContext) {
|
||||
TargetVersion: m.TargetVersion,
|
||||
PHPMinimum: m.PHPMinimum,
|
||||
Language: m.Language,
|
||||
ExtensionType: m.ExtensionType,
|
||||
ExtensionType: m.ExtensionType,
|
||||
EntryPoint: m.EntryPoint,
|
||||
DeployHost: m.DeployHost,
|
||||
DeployPort: m.DeployPort,
|
||||
|
||||
@@ -156,46 +156,14 @@ type swaggerParameterBodies struct {
|
||||
// in:body
|
||||
EditBranchProtectionOption api.EditBranchProtectionOption
|
||||
|
||||
// in:body
|
||||
UpdateBranchProtectionPriories api.UpdateBranchProtectionPriories
|
||||
|
||||
// in:body
|
||||
CreateOrgBranchProtectionOption api.CreateOrgBranchProtectionOption
|
||||
|
||||
// in:body
|
||||
EditOrgBranchProtectionOption api.EditOrgBranchProtectionOption
|
||||
|
||||
// in:body
|
||||
CreateOrgTagProtectionOption api.CreateOrgTagProtectionOption
|
||||
// in:body
|
||||
EditOrgTagProtectionOption api.EditOrgTagProtectionOption
|
||||
|
||||
// in:body
|
||||
EditOrgPushPolicyOption api.EditOrgPushPolicyOption
|
||||
|
||||
// in:body
|
||||
EditOrgRepoDefaultsOption api.EditOrgRepoDefaultsOption
|
||||
|
||||
// in:body
|
||||
EditOrgEmailDomainPolicyOption api.EditOrgEmailDomainPolicyOption
|
||||
|
||||
// in:body
|
||||
EditAccessTokenOption api.EditAccessTokenOption
|
||||
|
||||
// in:body
|
||||
IssueBulkAssigneesOption api.IssueBulkAssigneesOption
|
||||
// in:body
|
||||
IssueBulkLabelsOption api.IssueBulkLabelsOption
|
||||
// in:body
|
||||
IssueBulkMilestoneOption api.IssueBulkMilestoneOption
|
||||
// in:body
|
||||
IssueBulkStateOption api.IssueBulkStateOption
|
||||
|
||||
// in:body
|
||||
IssuePriorityDef api.IssuePriorityDef
|
||||
// in:body
|
||||
IssueStatusDef api.IssueStatusDef
|
||||
// in:body
|
||||
IssueTypeDef api.IssueTypeDef
|
||||
UpdateBranchProtectionPriories api.UpdateBranchProtectionPriories
|
||||
|
||||
// in:body
|
||||
CreateOAuth2ApplicationOptions api.CreateOAuth2ApplicationOptions
|
||||
|
||||
@@ -53,40 +53,5 @@ type swaggerResponseOrgBranchProtection struct {
|
||||
// swagger:response OrgBranchProtectionList
|
||||
type swaggerResponseOrgBranchProtectionList struct {
|
||||
// in:body
|
||||
Body []*api.OrgBranchProtection `json:"body"`
|
||||
}
|
||||
|
||||
// OrgTagProtection
|
||||
// swagger:response OrgTagProtection
|
||||
type swaggerResponseOrgTagProtection struct {
|
||||
// in:body
|
||||
Body api.OrgTagProtection `json:"body"`
|
||||
}
|
||||
|
||||
// OrgTagProtectionList
|
||||
// swagger:response OrgTagProtectionList
|
||||
type swaggerResponseOrgTagProtectionList struct {
|
||||
// in:body
|
||||
Body []*api.OrgTagProtection `json:"body"`
|
||||
}
|
||||
|
||||
// OrgPushPolicy
|
||||
// swagger:response OrgPushPolicy
|
||||
type swaggerResponseOrgPushPolicy struct {
|
||||
// in:body
|
||||
Body api.OrgPushPolicy `json:"body"`
|
||||
}
|
||||
|
||||
// OrgRepoDefaults
|
||||
// swagger:response OrgRepoDefaults
|
||||
type swaggerResponseOrgRepoDefaults struct {
|
||||
// in:body
|
||||
Body api.OrgRepoDefaults `json:"body"`
|
||||
}
|
||||
|
||||
// OrgEmailDomainPolicy
|
||||
// swagger:response OrgEmailDomainPolicy
|
||||
type swaggerResponseOrgEmailDomainPolicy struct {
|
||||
// in:body
|
||||
Body api.OrgEmailDomainPolicy `json:"body"`
|
||||
Body []api.OrgBranchProtection `json:"body"`
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,158 +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(oldCommitID, 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) added or modified by the
|
||||
// push (oldCommitID..newCommitID) whose blob exceeds limit bytes, or ("", 0) if none —
|
||||
// or on any error (fail open). Only the pushed delta is inspected so a pre-existing
|
||||
// oversized file committed before the policy existed cannot permanently block unrelated
|
||||
// pushes. For a new branch (no old commit) the full new tree is scanned, since every
|
||||
// blob is effectively introduced by the push.
|
||||
func (ctx *preReceiveContext) largestBlobOverLimit(oldCommitID, newCommitID string, limit int64) (string, int64) {
|
||||
emptyID := ctx.Repo.GetObjectFormat().EmptyObjectID().String()
|
||||
|
||||
// New branch: no base commit to diff against, so scan the full tip tree. ls-tree
|
||||
// --long carries the blob size inline, so no extra sizing pass is needed.
|
||||
if oldCommitID == "" || oldCommitID == emptyID {
|
||||
output, _, err := gitrepo.RunCmdString(ctx, ctx.Repo.Repository,
|
||||
gitcmd.NewCommand("ls-tree", "-r", "--long").AddDynamicArguments(newCommitID).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
|
||||
}
|
||||
if size, perr := strconv.ParseInt(fields[3], 10, 64); perr == nil && size > limit {
|
||||
return line[tab+1:], size
|
||||
}
|
||||
}
|
||||
return "", 0
|
||||
}
|
||||
|
||||
// Existing branch: inspect only blobs added or modified by this push. diff-tree
|
||||
// gives the changed paths and their new object ids but not sizes, so collect the
|
||||
// candidate blob ids and size them in a single cat-file --batch-check pass.
|
||||
output, _, err := gitrepo.RunCmdString(ctx, ctx.Repo.Repository,
|
||||
gitcmd.NewCommand("diff-tree", "--no-commit-id", "-r").AddDynamicArguments(oldCommitID, newCommitID).WithEnv(ctx.env))
|
||||
if err != nil {
|
||||
log.Error("org push policy diff-tree for %-v: %v", ctx.Repo.Repository, err)
|
||||
return "", 0
|
||||
}
|
||||
shaToPath := make(map[string]string)
|
||||
var order []string
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
tab := strings.IndexByte(line, '\t')
|
||||
if tab < 0 {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(line[:tab]) // ":oldmode newmode oldsha newsha status"
|
||||
if len(fields) < 5 {
|
||||
continue
|
||||
}
|
||||
newMode, newSha, status := fields[1], fields[3], fields[4]
|
||||
// Only regular, executable, or symlink blobs; skip submodule gitlinks (160000).
|
||||
if newMode != "100644" && newMode != "100755" && newMode != "120000" {
|
||||
continue
|
||||
}
|
||||
if status == "D" || newSha == emptyID {
|
||||
continue
|
||||
}
|
||||
if _, seen := shaToPath[newSha]; !seen {
|
||||
order = append(order, newSha)
|
||||
}
|
||||
shaToPath[newSha] = line[tab+1:]
|
||||
}
|
||||
if len(order) == 0 {
|
||||
return "", 0
|
||||
}
|
||||
|
||||
output, _, err = gitrepo.RunCmdString(ctx, ctx.Repo.Repository,
|
||||
gitcmd.NewCommand("cat-file", "--batch-check").WithStdinBytes([]byte(strings.Join(order, "\n")+"\n")).WithEnv(ctx.env))
|
||||
if err != nil {
|
||||
log.Error("org push policy cat-file for %-v: %v", ctx.Repo.Repository, err)
|
||||
return "", 0
|
||||
}
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
fields := strings.Fields(line) // "<sha> <type> <size>" or "<sha> missing"
|
||||
if len(fields) != 3 || fields[1] != "blob" {
|
||||
continue
|
||||
}
|
||||
if size, perr := strconv.ParseInt(fields[2], 10, 64); perr == nil && size > limit {
|
||||
return shaToPath[fields[0]], size
|
||||
}
|
||||
}
|
||||
return "", 0
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ func NewComment(ctx *context.Context) {
|
||||
} // end if: handle close or reopen
|
||||
|
||||
// Handle custom status from the status dropdown (replaces close button for issues with org statuses).
|
||||
if statusIDStr := ctx.Req.FormValue("status_id"); statusIDStr != "" {
|
||||
if statusIDStr := ctx.Req.FormValue("status_id"); statusIDStr != "" && statusIDStr != "" {
|
||||
if statusIDStr == "reopen" {
|
||||
// Reopen via dropdown
|
||||
if issue.IsClosed {
|
||||
|
||||
@@ -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,16 +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
|
||||
}
|
||||
}
|
||||
|
||||
repo.PrepareBranchList(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
|
||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
|
||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/container"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
|
||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
||||
)
|
||||
|
||||
// manifestXML mirrors the .mokogitea/manifest.xml schema for XML parsing.
|
||||
type manifestXML struct {
|
||||
XMLName xml.Name `xml:"mokoplatform"`
|
||||
Identity manifestIdentity `xml:"identity"`
|
||||
Governance manifestGovernance `xml:"governance"`
|
||||
Distribution manifestDistribution `xml:"distribution"`
|
||||
Build manifestBuild `xml:"build"`
|
||||
}
|
||||
|
||||
type manifestDistribution struct {
|
||||
DisplayName string `xml:"display-name"`
|
||||
Maintainer string `xml:"maintainer"`
|
||||
MaintainerURL string `xml:"maintainer-url"`
|
||||
InfoURL string `xml:"info-url"`
|
||||
TargetVersion string `xml:"target-version"`
|
||||
PHPMinimum string `xml:"php-minimum"`
|
||||
}
|
||||
|
||||
type manifestIdentity struct {
|
||||
Name string `xml:"name"`
|
||||
Org string `xml:"org"`
|
||||
Description string `xml:"description"`
|
||||
VersionPrefix string `xml:"version-prefix"`
|
||||
ElementName string `xml:"element-name"`
|
||||
License manifestLicense `xml:"license"`
|
||||
}
|
||||
|
||||
type manifestLicense struct {
|
||||
SPDX string `xml:"spdx,attr"`
|
||||
Name string `xml:",chardata"`
|
||||
}
|
||||
|
||||
type manifestGovernance struct {
|
||||
Platform string `xml:"platform"`
|
||||
StandardsVersion string `xml:"standards-version"`
|
||||
StandardsSource string `xml:"standards-source"`
|
||||
}
|
||||
|
||||
type manifestBuild struct {
|
||||
Language string `xml:"language"`
|
||||
ExtensionType string `xml:"package-type"`
|
||||
EntryPoint string `xml:"entry-point"`
|
||||
}
|
||||
|
||||
// SyncMetadataFromCommit reads .mokogitea/manifest.xml from the given commit
|
||||
// and upserts the values into the repo_manifest database table.
|
||||
// This is called on push to the default branch to keep the database in sync
|
||||
// with the XML file. If no manifest.xml exists, this is a no-op.
|
||||
func SyncMetadataFromCommit(ctx context.Context, repo *repo_model.Repository, commit *git.Commit) {
|
||||
if commit == nil {
|
||||
return
|
||||
}
|
||||
|
||||
entry, err := commit.GetTreeEntryByPath(".mokogitea/manifest.xml")
|
||||
if err != nil || entry == nil {
|
||||
return // no manifest.xml — not an error
|
||||
}
|
||||
|
||||
reader, err := entry.Blob().DataAsync()
|
||||
if err != nil {
|
||||
log.Error("SyncMetadata: read blob for %s: %v", repo.FullName(), err)
|
||||
return
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
var mxml manifestXML
|
||||
decoder := xml.NewDecoder(reader)
|
||||
if err := decoder.Decode(&mxml); err != nil {
|
||||
log.Error("SyncMetadata: parse XML for %s: %v", repo.FullName(), err)
|
||||
return
|
||||
}
|
||||
|
||||
manifest := &repo_model.RepoMetadata{
|
||||
RepoID: repo.ID,
|
||||
Name: mxml.Identity.Name,
|
||||
Org: mxml.Identity.Org,
|
||||
Description: mxml.Identity.Description,
|
||||
VersionPrefix: mxml.Identity.VersionPrefix,
|
||||
ElementName: mxml.Identity.ElementName,
|
||||
LicenseSPDX: mxml.Identity.License.SPDX,
|
||||
LicenseName: mxml.Identity.License.Name,
|
||||
Platform: mxml.Governance.Platform,
|
||||
StandardsVersion: mxml.Governance.StandardsVersion,
|
||||
StandardsSource: mxml.Governance.StandardsSource,
|
||||
Maintainer: mxml.Distribution.Maintainer,
|
||||
MaintainerURL: mxml.Distribution.MaintainerURL,
|
||||
InfoURL: mxml.Distribution.InfoURL,
|
||||
TargetVersion: mxml.Distribution.TargetVersion,
|
||||
PHPMinimum: mxml.Distribution.PHPMinimum,
|
||||
Language: mxml.Build.Language,
|
||||
ExtensionType: mxml.Build.ExtensionType,
|
||||
EntryPoint: mxml.Build.EntryPoint,
|
||||
}
|
||||
|
||||
if err := repo_model.CreateOrUpdateRepoMetadata(ctx, manifest); err != nil {
|
||||
log.Error("SyncMetadata: save for %s: %v", repo.FullName(), err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("SyncMetadata: synced .mokogitea/manifest.xml for %s", repo.FullName())
|
||||
}
|
||||
@@ -194,6 +194,8 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
|
||||
if err := DelRepoDivergenceFromCache(ctx, repo.ID); err != nil {
|
||||
log.Error("DelRepoDivergenceFromCache: %v", err)
|
||||
}
|
||||
// Auto-sync .mokogitea/manifest.xml to database on default branch push
|
||||
SyncMetadataFromCommit(ctx, repo, newCommit)
|
||||
// Run security scanners on default branch push
|
||||
security_service.ScanOnPush(ctx, repo, newCommit)
|
||||
} else {
|
||||
|
||||
@@ -160,7 +160,7 @@ var DefaultCodeRules = []CodeRule{
|
||||
{
|
||||
ID: "deserialize-yaml-py", Title: "Insecure Deserialization: yaml.load() (Python)",
|
||||
Severity: security_model.SeverityHigh, CWE: "CWE-502",
|
||||
Pattern: regexp.MustCompile(`(?i)yaml\.load\s*\(`),
|
||||
Pattern: regexp.MustCompile(`yaml\.load\s*\([^)]*(?:Loader\s*=\s*yaml\.(?:Unsafe|Full)Loader|[^)]*\)(?!\s*#))`),
|
||||
Description: "yaml.load() without SafeLoader allows arbitrary code execution — use yaml.safe_load()",
|
||||
Languages: []string{".py"},
|
||||
},
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package security
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestDefaultCodeRulesCompile forces the package init, which builds every scanner
|
||||
// rule's regexp via regexp.MustCompile, and asserts each pattern is present. Go's
|
||||
// regexp engine is RE2 and rejects Perl-only constructs (lookahead/lookbehind/
|
||||
// backreferences) by panicking in MustCompile at init — which crash-loops the
|
||||
// server at startup. This test executes that init so such a pattern fails CI here
|
||||
// instead of on a live deploy.
|
||||
func TestDefaultCodeRulesCompile(t *testing.T) {
|
||||
if len(DefaultCodeRules) == 0 {
|
||||
t.Fatal("DefaultCodeRules is empty")
|
||||
}
|
||||
for _, r := range DefaultCodeRules {
|
||||
if r.Pattern == nil {
|
||||
t.Errorf("rule %q has a nil Pattern", r.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -62,88 +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}}
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -172,7 +172,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.protect_branch_deletion"}}</h5>
|
||||
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.event_delete"}}</h5>
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
<input type="radio" name="enable_delete" value="none" class="toggle-target-disabled" data-target="#delete_allowlist_box" {{if not .Rule.CanDelete}}checked{{end}}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Generated
+666
-2676
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,6 @@ package integration
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
auth_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/auth"
|
||||
@@ -37,7 +36,7 @@ func TestAPILicensePackages(t *testing.T) {
|
||||
|
||||
t.Run("CreatePackage", func(t *testing.T) {
|
||||
body := `{"name":"Test Pro Annual","description":"Annual pro subscription","duration_days":365,"max_sites":5}`
|
||||
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", strings.NewReader(body)).
|
||||
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", []byte(body)).
|
||||
AddTokenAuth(token).
|
||||
SetHeader("Content-Type", "application/json")
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
@@ -52,7 +51,7 @@ func TestAPILicensePackages(t *testing.T) {
|
||||
|
||||
t.Run("CreatePackageNoName", func(t *testing.T) {
|
||||
body := `{"description":"Missing name"}`
|
||||
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", strings.NewReader(body)).
|
||||
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", []byte(body)).
|
||||
AddTokenAuth(token).
|
||||
SetHeader("Content-Type", "application/json")
|
||||
MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||
@@ -69,7 +68,7 @@ func TestAPILicenseKeys(t *testing.T) {
|
||||
|
||||
// Create a package first.
|
||||
body := `{"name":"Test Package","duration_days":30}`
|
||||
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", strings.NewReader(body)).
|
||||
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", []byte(body)).
|
||||
AddTokenAuth(token).
|
||||
SetHeader("Content-Type", "application/json")
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
@@ -81,7 +80,7 @@ func TestAPILicenseKeys(t *testing.T) {
|
||||
|
||||
t.Run("CreateKey", func(t *testing.T) {
|
||||
body := fmt.Sprintf(`{"package_id":%d,"licensee_name":"John Doe","licensee_email":"john@example.com"}`, pkg.ID)
|
||||
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys", strings.NewReader(body)).
|
||||
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys", []byte(body)).
|
||||
AddTokenAuth(token).
|
||||
SetHeader("Content-Type", "application/json")
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
@@ -105,7 +104,7 @@ func TestAPILicenseKeys(t *testing.T) {
|
||||
|
||||
t.Run("EditKey", func(t *testing.T) {
|
||||
body := `{"licensee_name":"Jane Doe","domain_restriction":"example.com,test.com"}`
|
||||
req := NewRequestWithBody(t, "PATCH", fmt.Sprintf("%s/license-keys/%d", urlPrefix, createdKeyID), strings.NewReader(body)).
|
||||
req := NewRequestWithBody(t, "PATCH", fmt.Sprintf("%s/license-keys/%d", urlPrefix, createdKeyID), []byte(body)).
|
||||
AddTokenAuth(token).
|
||||
SetHeader("Content-Type", "application/json")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
@@ -125,7 +124,7 @@ func TestAPILicenseKeys(t *testing.T) {
|
||||
|
||||
t.Run("ValidateKey", func(t *testing.T) {
|
||||
body := fmt.Sprintf(`{"key":"%s","domain":"example.com"}`, rawKey)
|
||||
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys/validate", strings.NewReader(body)).
|
||||
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys/validate", []byte(body)).
|
||||
SetHeader("Content-Type", "application/json")
|
||||
// Note: no token — this is a public endpoint.
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
@@ -137,7 +136,7 @@ func TestAPILicenseKeys(t *testing.T) {
|
||||
|
||||
t.Run("ValidateInvalidKey", func(t *testing.T) {
|
||||
body := `{"key":"MOKO-XXXX-XXXX-XXXX-XXXX","domain":"example.com"}`
|
||||
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys/validate", strings.NewReader(body)).
|
||||
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys/validate", []byte(body)).
|
||||
SetHeader("Content-Type", "application/json")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
var result api.ValidateLicenseKeyResponse
|
||||
@@ -162,7 +161,7 @@ func TestAPILicensePurchaseWebhook(t *testing.T) {
|
||||
|
||||
// Create a package.
|
||||
body := `{"name":"Purchase Test","duration_days":90}`
|
||||
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", strings.NewReader(body)).
|
||||
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", []byte(body)).
|
||||
AddTokenAuth(token).
|
||||
SetHeader("Content-Type", "application/json")
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
@@ -171,7 +170,7 @@ func TestAPILicensePurchaseWebhook(t *testing.T) {
|
||||
|
||||
t.Run("PurchaseNewKey", func(t *testing.T) {
|
||||
body := fmt.Sprintf(`{"package_id":%d,"licensee_name":"Buyer","licensee_email":"buyer@shop.com","domain":"shop.com","payment_ref":"stripe_pi_test123"}`, pkg.ID)
|
||||
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys/purchase", strings.NewReader(body)).
|
||||
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys/purchase", []byte(body)).
|
||||
AddTokenAuth(token).
|
||||
SetHeader("Content-Type", "application/json")
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
@@ -184,7 +183,7 @@ func TestAPILicensePurchaseWebhook(t *testing.T) {
|
||||
t.Run("PurchaseIdempotent", func(t *testing.T) {
|
||||
// Same payment_ref should return existing key without raw_key.
|
||||
body := fmt.Sprintf(`{"package_id":%d,"licensee_name":"Buyer","payment_ref":"stripe_pi_test123"}`, pkg.ID)
|
||||
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys/purchase", strings.NewReader(body)).
|
||||
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys/purchase", []byte(body)).
|
||||
AddTokenAuth(token).
|
||||
SetHeader("Content-Type", "application/json")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
@@ -255,7 +255,7 @@ func TestPackageComposer(t *testing.T) {
|
||||
AddBasicAuth(user.Name)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
result = &composer.PackageMetadataResponse{}
|
||||
result = composer.PackageMetadataResponse{}
|
||||
DecodeJSON(t, resp, &result)
|
||||
pkgs = result.Packages[packageName]
|
||||
assert.Len(t, pkgs, 1)
|
||||
@@ -268,7 +268,7 @@ func TestPackageComposer(t *testing.T) {
|
||||
AddBasicAuth(otherUser.Name)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
result = &composer.PackageMetadataResponse{}
|
||||
result = composer.PackageMetadataResponse{}
|
||||
DecodeJSON(t, resp, &result)
|
||||
pkgs = result.Packages[packageName]
|
||||
assert.Len(t, pkgs, 1)
|
||||
|
||||
@@ -253,7 +253,7 @@ func TestOAuth2CallbackReactivationGating(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)()
|
||||
defer test.MockVariableValue(&setting.OAuth2Client.Username, setting.OAuth2UsernameUserid)()
|
||||
|
||||
srv := newFakeOIDCServerWithConfig(t, FakeOIDCConfig{Sub: "test-sub", Email: "test@example.com", Name: "Test User"})
|
||||
srv := newFakeOIDCServer(t, FakeOIDCConfig{Sub: "test-sub", Email: "test@example.com", Name: "Test User"})
|
||||
addOAuth2Source(t, "test-oauth-source", oauth2.Source{
|
||||
Provider: "openidConnect",
|
||||
ClientID: "test-client-id",
|
||||
@@ -308,7 +308,7 @@ type FakeOIDCConfig struct {
|
||||
}
|
||||
|
||||
// newFakeOIDCServer starts a httptest.Server that implements the minimum OIDC endpoints needed to complete a sign-in flow
|
||||
func newFakeOIDCServerWithConfig(t *testing.T, cfg FakeOIDCConfig) *httptest.Server {
|
||||
func newFakeOIDCServer(t *testing.T, cfg FakeOIDCConfig) *httptest.Server {
|
||||
t.Helper()
|
||||
|
||||
var srv *httptest.Server
|
||||
|
||||
@@ -7,16 +7,16 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
auth_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/auth"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unittest"
|
||||
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/test"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/web"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/routers/web/auth"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/auth/source/oauth2"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/tests"
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/web/auth"
|
||||
"code.gitea.io/gitea/services/auth/source/oauth2"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/markbates/goth"
|
||||
"github.com/markbates/goth/gothic"
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ Custom CSS themes at `/var/lib/gitea/custom/public/assets/css/`:
|
||||
|
||||
---
|
||||
|
||||
*Repo: [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) · [mokocli](https://git.mokoconsulting.tech/MokoConsulting/mokocli/wiki/Home)*
|
||||
*Repo: [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||
|
||||
| Revision | Date | Author | Description |
|
||||
|---|---|---|---|
|
||||
|
||||
@@ -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 mokocli schema and are separate from custom fields. Custom fields are user-defined; manifest fields are standardized. |
|
||||
| Manifest Settings | Manifest fields follow the moko-platform 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
@@ -41,7 +41,7 @@ Located at `/var/lib/gitea/custom/`:
|
||||
|
||||
---
|
||||
|
||||
*Repo: [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) · [mokocli](https://git.mokoconsulting.tech/MokoConsulting/mokocli/wiki/Home)*
|
||||
*Repo: [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||
|
||||
| Revision | Date | Author | Description |
|
||||
|---|---|---|---|
|
||||
|
||||
@@ -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_mokosuite`) — this becomes the `<element>` in the XML feed
|
||||
3. Set the **Display Name** (e.g., "Package - MokoSuite") — shown in Joomla update manager
|
||||
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
|
||||
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`)
|
||||
|
||||
@@ -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, mokocli standards version, and standards source URL
|
||||
- **Governance** — platform type, moko-platform 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 mokocli CLI to read and write manifest settings programmatically.
|
||||
The manifest API allows Actions workflows and the moko-platform 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/mokocli",
|
||||
"standards_source": "https://code.mokoconsulting.tech/MokoConsulting/moko-platform",
|
||||
"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 mokocli schema. |
|
||||
| mokocli 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 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). |
|
||||
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -195,7 +195,7 @@ curl -X POST -H "Authorization: token TOKEN" \
|
||||
|
||||
---
|
||||
|
||||
*Repo: [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) · [mokocli](https://git.mokoconsulting.tech/MokoConsulting/mokocli/wiki/Home)*
|
||||
*Repo: [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||
|
||||
| Revision | Date | Author | Description |
|
||||
|---|---|---|---|
|
||||
|
||||
+2
-2
@@ -23,9 +23,9 @@
|
||||
|
||||
## In Progress
|
||||
|
||||
- Rename mokocli to MokoCLI
|
||||
- Rename moko-platform to MokoPlatform
|
||||
- Granular role-based permissions for all features (#9)
|
||||
- Wire mokocli CLI to manifest API (#505)
|
||||
- Wire moko-platform CLI to manifest API (#505)
|
||||
- Bulk migrate remaining 41 flat wikis to folders
|
||||
|
||||
## Planned
|
||||
|
||||
Reference in New Issue
Block a user