Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c74173bb86 | |||
| b2447da3dd | |||
| 7be712571c | |||
| 6634b049ab | |||
| 5c62cefcd3 | |||
| f2ca569906 | |||
| b9301a8f31 | |||
| cac06c2ac7 | |||
| 6c0c2c3f1f | |||
| 4db6f03efd | |||
| bdac121b70 | |||
| 80a4c68063 | |||
| 5c0bf5f214 | |||
| e2f8d5ce9b | |||
| 1d5427f2c2 | |||
| 9d949b6294 | |||
| 515e8fcdea | |||
| 519966015b | |||
| 2677caafd6 | |||
| 66148f76fa | |||
| 7a4be6ab63 | |||
| c26ceaea87 | |||
| 8ba2dbf3e3 | |||
| 44b25099ac | |||
| 838d1f0d28 | |||
| 6e0db30b3e | |||
| e2343c8fb4 | |||
| 8e045c60fc | |||
| d3a0c7534b | |||
| 7f0da28988 | |||
| 6cd1d10617 | |||
| 5c8056f15e | |||
| ffaa3662e6 | |||
| 1c462f9d49 | |||
| 8fbf97ac7e | |||
| b3448f4d62 | |||
| f72300a03f | |||
| 08a43ec718 | |||
| bf2b299f87 | |||
| 89aa805967 | |||
| 3efbab985b | |||
| 98b1ed2f7b | |||
| ccfc9a604b | |||
| 2f119fbd95 | |||
| 7f229ba01c | |||
| e98fca780e | |||
| 7a4dc5e809 | |||
| 8c63b00953 | |||
| 6b81922c47 | |||
| 93365cdd95 | |||
| 81ea2fcb05 | |||
| 2713c49aec | |||
| 3917bf6a29 | |||
| 89ed32e961 | |||
| 948e7bcd21 | |||
| 5d797431f0 | |||
| 63f773aa56 | |||
| 125eefc650 | |||
| 056eb7d3c4 | |||
| 0492448ab5 |
@@ -3,9 +3,9 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# DEFGROUP: MokoGitea.Workflow
|
||||
# INGROUP: mokocli.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||
# PATH: /.mokogitea/workflows/auto-bump.yml
|
||||
# VERSION: 09.02.00
|
||||
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
||||
@@ -34,7 +34,8 @@ jobs:
|
||||
if: >-
|
||||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||
!contains(github.event.head_commit.message, '[skip bump]') &&
|
||||
!startsWith(github.event.head_commit.message, 'Merge pull request')
|
||||
!startsWith(github.event.head_commit.message, 'Merge pull request') &&
|
||||
!startsWith(github.event.repository.name, 'Template-')
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -52,7 +53,7 @@ jobs:
|
||||
echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV"
|
||||
else
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \
|
||||
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/MokoCLI.git" \
|
||||
/tmp/mokocli
|
||||
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
||||
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV"
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# DEFGROUP: MokoGitea.Workflow
|
||||
# INGROUP: mokocli.Release
|
||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
|
||||
# PATH: /templates/workflows/universal/auto-release.yml.template
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||
# PATH: /.mokogitea/workflows/auto-release.yml
|
||||
# VERSION: 05.01.00
|
||||
# BRIEF: Universal build & release � detects platform from manifest.xml
|
||||
#
|
||||
@@ -142,8 +142,8 @@ jobs:
|
||||
run: |
|
||||
git fetch origin rc
|
||||
git checkout rc
|
||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "gitea-actions[bot]"
|
||||
git config --local user.email "mokogitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "mokogitea-actions[bot]"
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
|
||||
- name: Publish RC release
|
||||
@@ -214,8 +214,8 @@ jobs:
|
||||
|
||||
- name: Configure git for bot pushes
|
||||
run: |
|
||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "gitea-actions[bot]"
|
||||
git config --local user.email "mokogitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "mokogitea-actions[bot]"
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
|
||||
- name: Check for merge conflict markers
|
||||
@@ -426,7 +426,7 @@ jobs:
|
||||
&& echo "main branch pushed to GitHub mirror" \
|
||||
|| echo "WARNING: GitHub mirror push failed"
|
||||
|
||||
- name: "Step 11: Delete rc branch and recreate dev from main"
|
||||
- name: "Step 11: Delete rc branch (dev reset moved to cascade-dev.yml)"
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
@@ -438,17 +438,9 @@ jobs:
|
||||
"${API_BASE}/branches/rc" 2>/dev/null \
|
||||
&& echo "Deleted rc branch" || echo "rc branch not found"
|
||||
|
||||
# Delete dev branch
|
||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
|
||||
|
||||
# Recreate dev from main (now includes version bump + changelog promotion)
|
||||
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${API_BASE}/branches" \
|
||||
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
|
||||
|
||||
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
|
||||
# dev is reset from main by the dedicated "Cascade Main -> Dev" workflow
|
||||
# (cascade-dev.yml), which runs after this release completes.
|
||||
echo "rc cleaned; dev reset handled by cascade-dev.yml" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: "Step 12: Create version branch from main"
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# DEFGROUP: MokoGitea.Workflow
|
||||
# INGROUP: MokoStandards.Universal
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||
# PATH: /.mokogitea/workflows/branch-cleanup.yml
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Delete feature branches after PR merge
|
||||
|
||||
@@ -1,10 +1,106 @@
|
||||
# DISABLED — auto-release Step 11 recreates dev from main after every release.
|
||||
# Cascade-dev is redundant and causes version conflicts when both main and dev
|
||||
# have different version numbers in templateDetails.xml / manifest.xml.
|
||||
name: "Cascade Main → Dev (DISABLED)"
|
||||
on: workflow_dispatch
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: MokoGitea.Workflow
|
||||
# INGROUP: MokoStandards.Cascade
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||
# PATH: /.mokogitea/workflows/cascade-dev.yml
|
||||
# VERSION: 02.00.00
|
||||
# BRIEF: Cascade main -> dev via PR; auto-merge only if conflict-free, else notify
|
||||
|
||||
name: "Cascade Main -> Dev"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
# ntfy destination is configured via repo or org variables (org vars are inherited).
|
||||
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
||||
NTFY_TOPIC: ${{ vars.CASCADE_NTFY_TOPIC || vars.NTFY_TOPIC || 'gitea-releases' }}
|
||||
|
||||
jobs:
|
||||
noop:
|
||||
cascade:
|
||||
name: Cascade main -> dev
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "Cascade disabled — auto-release handles dev recreation"
|
||||
- name: Open main -> dev PR (auto-merge if clean, else notify)
|
||||
env:
|
||||
TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
set -uo pipefail
|
||||
API="${MOKOGITEA_URL}/api/v1/repos/${REPO}"
|
||||
AUTH="Authorization: token ${TOKEN}"
|
||||
jqnum() { python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$1',''))" 2>/dev/null; }
|
||||
|
||||
# 0. dev must exist
|
||||
if ! curl -sf -H "$AUTH" "${API}/branches/dev" >/dev/null 2>&1; then
|
||||
echo "No dev branch - nothing to cascade."; exit 0
|
||||
fi
|
||||
|
||||
# 1. is main ahead of dev?
|
||||
AHEAD=$(curl -sf -H "$AUTH" "${API}/compare/dev...main" \
|
||||
| python3 -c "import sys,json; print(json.load(sys.stdin).get('total_commits',0))" 2>/dev/null || echo 0)
|
||||
if [ "${AHEAD:-0}" -eq 0 ]; then
|
||||
echo "dev already up to date with main."; exit 0
|
||||
fi
|
||||
echo "main is ${AHEAD} commit(s) ahead of dev."
|
||||
|
||||
# 2. reuse an open main->dev PR, else create one
|
||||
PR=$(curl -sf -H "$AUTH" "${API}/pulls?state=open&base=dev" \
|
||||
| python3 -c "import sys,json; d=json.load(sys.stdin); print(next((str(p['number']) for p in d if p.get('head',{}).get('ref')=='main'), ''))" 2>/dev/null || echo "")
|
||||
if [ -z "$PR" ]; then
|
||||
RESP=$(curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST "${API}/pulls" \
|
||||
-d '{"head":"main","base":"dev","title":"chore(sync): cascade main -> dev","body":"Automated cascade of main into dev. Auto-merges only if conflict-free; otherwise left open for manual resolution."}')
|
||||
PR=$(printf '%s' "$RESP" | jqnum number)
|
||||
if [ -z "$PR" ]; then
|
||||
echo "::warning::Could not open cascade PR: $RESP"; exit 0
|
||||
fi
|
||||
echo "Opened cascade PR #${PR}"
|
||||
else
|
||||
echo "Reusing open cascade PR #${PR}"
|
||||
fi
|
||||
|
||||
# 3. wait for MokoGitea to compute mergeability (conflict detection)
|
||||
MERGEABLE=""
|
||||
for _ in 1 2 3 4 5 6; do
|
||||
MERGEABLE=$(curl -sf -H "$AUTH" "${API}/pulls/${PR}" | jqnum mergeable)
|
||||
case "$MERGEABLE" in True|False) break ;; esac
|
||||
sleep 3
|
||||
done
|
||||
echo "mergeable=${MERGEABLE}"
|
||||
|
||||
notify() {
|
||||
curl -sS \
|
||||
-H "Title: ${REPO}: dev cascade needs manual merge" \
|
||||
-H "Tags: warning,twisted_rightwards_arrows" \
|
||||
-H "Priority: high" \
|
||||
-H "Click: ${MOKOGITEA_URL}/${REPO}/pulls/${PR}" \
|
||||
-d "main -> dev cascade PR #${PR} $1 It was NOT auto-merged; resolve it manually." \
|
||||
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
||||
}
|
||||
|
||||
# 4. auto-merge only if conflict-free; otherwise notify
|
||||
if [ "$MERGEABLE" = "True" ]; then
|
||||
CODE=$(curl -s -o /tmp/merge.json -w "%{http_code}" -H "$AUTH" -H "Content-Type: application/json" \
|
||||
-X POST "${API}/pulls/${PR}/merge" -d '{"Do":"merge","merge_when_checks_succeed":true}')
|
||||
if [ "$CODE" -ge 200 ] && [ "$CODE" -lt 300 ]; then
|
||||
echo "Cascade PR #${PR} merged (or scheduled to merge when checks pass)."
|
||||
else
|
||||
echo "::warning::Auto-merge returned HTTP ${CODE}: $(cat /tmp/merge.json)"
|
||||
notify "could not be auto-merged (HTTP ${CODE})."
|
||||
fi
|
||||
else
|
||||
echo "::warning::Cascade PR #${PR} has conflicts (mergeable=${MERGEABLE}); sending notification."
|
||||
notify "has conflicts and cannot be merged automatically."
|
||||
fi
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# DEFGROUP: MokoGitea.Workflow
|
||||
# INGROUP: mokocli.Universal
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||
# PATH: /.mokogitea/workflows/ci-issue-reporter.yml
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Reusable workflow — creates/updates a Gitea issue when a CI gate fails.
|
||||
# BRIEF: Reusable workflow — creates/updates a MokoGitea issue when a CI gate fails.
|
||||
# Clones MokoCLI and runs cli/ci_issue_reporter.sh.
|
||||
|
||||
name: "Universal: CI Issue Reporter"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# DEFGROUP: MokoGitea.Workflow
|
||||
# INGROUP: MokoStandards.Maintenance
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||
# PATH: /.mokogitea/workflows/cleanup.yml
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
for BRANCH in $BRANCHES; do
|
||||
# Skip protected branches
|
||||
case "$BRANCH" in
|
||||
main|master|develop|release/*|hotfix/*) continue ;;
|
||||
main|master|dev|develop|rc|beta|alpha|release|release/*|production|stable|staging|hotfix/*|version/*) continue ;;
|
||||
esac
|
||||
|
||||
# Check if branch is merged into main
|
||||
|
||||
@@ -52,51 +52,69 @@ jobs:
|
||||
REGISTRY_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
TAG: ${{ steps.config.outputs.tag }}
|
||||
run: |
|
||||
HEALTH_FMT='${{ '{{' }}.State.Health.Status${{ '}}' }}'
|
||||
|
||||
# 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.
|
||||
ssh -i ~/.ssh/deploy_key -p ${{ env.DEPLOY_PORT }} \
|
||||
-o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||
-o ServerAliveInterval=30 -o ServerAliveCountMax=10 \
|
||||
${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} bash -s <<DEPLOY_EOF
|
||||
${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} \
|
||||
"TAG='$TAG' REGISTRY_TOKEN='$REGISTRY_TOKEN' 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...'
|
||||
echo "Building Docker image: ${{ env.REGISTRY }}/${{ env.IMAGE }}:$TAG"
|
||||
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
|
||||
sed -i "s|${{ env.IMAGE }}:[^ ]*|${{ env.IMAGE }}:\$TAG|" docker-compose.yml
|
||||
docker compose up -d mokogitea-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
|
||||
|
||||
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
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
# 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: Gitea.Workflow
|
||||
# DEFGROUP: MokoGitea.Workflow
|
||||
# INGROUP: MokoStandards.Security
|
||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||
# PATH: /templates/workflows/gitleaks.yml.template
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
|
||||
# PATH: /.mokogitea/workflows/gitleaks.yml
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
||||
#
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# DEFGROUP: MokoGitea.Workflow
|
||||
# INGROUP: mokocli.Automation
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# DEFGROUP: MokoGitea.Workflow
|
||||
# INGROUP: MokoStandards.Notifications
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||
# PATH: /.mokogitea/workflows/notify.yml
|
||||
@@ -15,9 +15,9 @@ name: "Universal: Notifications"
|
||||
on:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- "Joomla Build & Release"
|
||||
- "Joomla Extension CI"
|
||||
- "Deploy"
|
||||
- "Universal: Build & Release"
|
||||
- "Joomla: Extension CI"
|
||||
- "Generic: Project CI"
|
||||
types:
|
||||
- completed
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# DEFGROUP: MokoGitea.Workflow
|
||||
# INGROUP: mokocli.CI
|
||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
|
||||
# PATH: /templates/workflows/universal/pr-check.yml.template
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||
# PATH: /.mokogitea/workflows/pr-check.yml
|
||||
# VERSION: 09.23.00
|
||||
# BRIEF: PR gate — branch policy + code validation before merge
|
||||
|
||||
@@ -97,6 +97,80 @@ jobs:
|
||||
echo "Branch policy: OK (${HEAD} → ${BASE})"
|
||||
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# ── Docs Update Gate (main PRs) ─────────────────────────────────────────
|
||||
require-docs:
|
||||
name: Require Docs Update
|
||||
runs-on: ubuntu-latest
|
||||
# Enforce only on PRs merging into main: README.md + CHANGELOG.md must both be updated.
|
||||
if: ${{ github.base_ref == 'main' }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Require README.md and CHANGELOG.md in the PR diff
|
||||
run: |
|
||||
BASE="${{ github.event.pull_request.base.sha }}"
|
||||
HEAD="${{ github.event.pull_request.head.sha }}"
|
||||
CHANGED="$(git diff --name-only "$BASE" "$HEAD" 2>/dev/null || true)"
|
||||
if [ -z "$CHANGED" ]; then
|
||||
git fetch -q origin "${{ github.base_ref }}" 2>/dev/null || true
|
||||
CHANGED="$(git diff --name-only "origin/${{ github.base_ref }}...HEAD" 2>/dev/null || true)"
|
||||
fi
|
||||
echo "Changed files in PR:"
|
||||
echo "$CHANGED"
|
||||
MISSING=""
|
||||
echo "$CHANGED" | grep -qxE 'README\.md' || MISSING="README.md"
|
||||
echo "$CHANGED" | grep -qxE 'CHANGELOG\.md' || MISSING="${MISSING:+$MISSING, }CHANGELOG.md"
|
||||
if [ -n "$MISSING" ]; then
|
||||
echo "::error::PRs into main must update: ${MISSING}"
|
||||
{
|
||||
echo "## Docs Update Required"
|
||||
echo ""
|
||||
echo "PRs merging into \`main\` must update both **README.md** and **CHANGELOG.md**."
|
||||
echo ""
|
||||
echo "Not updated in this PR: **${MISSING}**"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 1
|
||||
fi
|
||||
echo "Docs update present (README.md + CHANGELOG.md)"
|
||||
echo "## Docs Update: Passed" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
# ── Wiki Update Reminder (main PRs, non-blocking) ───────────────────────
|
||||
wiki-reminder:
|
||||
name: Wiki Update Reminder
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.base_ref == 'main' }}
|
||||
steps:
|
||||
- name: Remind to update the wiki
|
||||
env:
|
||||
TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
SERVER: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
REPO: ${{ github.repository }}
|
||||
PR: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
set -uo pipefail
|
||||
{
|
||||
echo "## Wiki Update Reminder"
|
||||
echo ""
|
||||
echo "Docs are **wiki-first** at MokoConsulting. If this change affects behavior, usage, configuration, or standards, update the repo wiki:"
|
||||
echo ""
|
||||
echo "- ${SERVER}/${REPO}/wiki"
|
||||
echo ""
|
||||
echo "_Non-blocking reminder._"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
# Post a single PR comment (idempotent via hidden marker); best-effort, never fails.
|
||||
API="${SERVER}/api/v1/repos/${REPO}/issues/${PR}/comments"
|
||||
if [ -n "${TOKEN:-}" ] && [ -n "${PR:-}" ]; then
|
||||
existing="$(curl -sf -H "Authorization: token ${TOKEN}" "$API" 2>/dev/null | grep -c 'wiki-reminder' || true)"
|
||||
if [ "${existing:-0}" -eq 0 ]; then
|
||||
curl -sf -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" -X POST "$API" \
|
||||
-d '{"body":"<!-- wiki-reminder -->\n\n**Wiki reminder:** docs are wiki-first -- if this PR changes behavior, usage, config, or standards, please update the repo wiki before/after merge. _(non-blocking)_"}' >/dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
echo "Wiki reminder emitted (non-blocking)."
|
||||
|
||||
# ── Secret Scanning ──────────────────────────────────────────────────
|
||||
gitleaks:
|
||||
name: Secret Scan
|
||||
@@ -276,7 +350,7 @@ jobs:
|
||||
joomla)
|
||||
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||
if [ -z "$MANIFEST" ]; then
|
||||
echo "::warning::No Joomla manifest found (WaaS site)"
|
||||
echo "::warning::No Joomla manifest found (MokoSuite site)"
|
||||
exit 0
|
||||
fi
|
||||
echo "Manifest: ${MANIFEST}"
|
||||
@@ -289,7 +363,7 @@ jobs:
|
||||
# Block legacy raw/branch update server URLs on MokoGitea
|
||||
RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true)
|
||||
if [ -n "$RAW_URLS" ]; then
|
||||
echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the Gitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)"
|
||||
echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the MokoGitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)"
|
||||
echo "$RAW_URLS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# DEFGROUP: MokoGitea.Workflow
|
||||
# INGROUP: mokocli.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||
# PATH: /.mokogitea/workflows/pre-release.yml
|
||||
# VERSION: 05.02.00
|
||||
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
|
||||
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
||||
fi
|
||||
rm -rf /tmp/mokocli
|
||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
|
||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoCLI.git
|
||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
||||
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
||||
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
||||
@@ -156,8 +156,8 @@ jobs:
|
||||
fi
|
||||
|
||||
# Commit version bump
|
||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "gitea-actions[bot]"
|
||||
git config --local user.email "mokogitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "mokogitea-actions[bot]"
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
git add -A
|
||||
git diff --cached --quiet || {
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# DEFGROUP: MokoGitea.Workflow
|
||||
# INGROUP: mokocli.Universal
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||
# PATH: /.mokogitea/workflows/rc-revert.yml
|
||||
# VERSION: 09.23.00
|
||||
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
|
||||
@@ -25,7 +25,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.pull_request.merged == false &&
|
||||
startsWith(github.event.pull_request.head.ref, 'rc/')
|
||||
startsWith(github.event.pull_request.head.ref, 'rc/') &&
|
||||
!startsWith(github.event.repository.name, 'Template-')
|
||||
|
||||
steps:
|
||||
- name: Rename branch
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# DEFGROUP: MokoGitea.Workflow
|
||||
# INGROUP: mokocli.Validation
|
||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
|
||||
# PATH: /templates/workflows/joomla/repo_health.yml.template
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||
# PATH: /.mokogitea/workflows/repo-health.yml
|
||||
# VERSION: 09.23.00
|
||||
# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts.
|
||||
# ============================================================================
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
|
||||
# Hardcoded authorized users — always allowed
|
||||
case "$ACTOR" in
|
||||
jmiller|gitea-actions[bot])
|
||||
jmiller|mokogitea-actions[bot])
|
||||
ALLOWED=true
|
||||
PERMISSION=admin
|
||||
METHOD="hardcoded allowlist"
|
||||
|
||||
@@ -10,11 +10,12 @@ on:
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ startsWith(github.event.repository.name, 'Template-') }}
|
||||
steps:
|
||||
- name: Checkout mokocli
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: MokoConsulting/mokocli
|
||||
repository: MokoConsulting/MokoCLI
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
|
||||
- name: Setup PHP
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow.Template
|
||||
# DEFGROUP: MokoGitea.Workflow.Template
|
||||
# INGROUP: MokoStandards.CI
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
|
||||
# PATH: /.mokogitea/workflows/version-set.yml
|
||||
@@ -34,6 +34,7 @@ jobs:
|
||||
set-version:
|
||||
name: Set Version to ${{ inputs.version }}
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
|
||||
|
||||
steps:
|
||||
- name: Validate version format
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# DEFGROUP: MokoGitea.Workflow
|
||||
# INGROUP: mokocli.Universal
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||
# PATH: /.mokogitea/workflows/workflow-sync-trigger.yml
|
||||
# VERSION: 01.01.00
|
||||
# BRIEF: Trigger workflow sync to live repos when a PR is merged to main
|
||||
@@ -27,9 +27,10 @@ jobs:
|
||||
name: Sync workflows to live repos
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
startsWith(github.event.repository.name, 'Template-') &&
|
||||
(github.event_name == 'workflow_dispatch' ||
|
||||
(github.event.pull_request.merged == true &&
|
||||
!contains(github.event.pull_request.title, '[skip sync]'))
|
||||
!contains(github.event.pull_request.title, '[skip sync]')))
|
||||
|
||||
steps:
|
||||
- name: Determine platform from repo name
|
||||
@@ -52,7 +53,7 @@ jobs:
|
||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
run: |
|
||||
MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
|
||||
git clone --depth 1 "${MOKOGITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
|
||||
git clone --depth 1 "${MOKOGITEA_URL}/MokoConsulting/MokoCLI.git" /tmp/mokocli
|
||||
|
||||
- name: Install PHP
|
||||
run: |
|
||||
|
||||
@@ -63,6 +63,10 @@
|
||||
- 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)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MokoGitea
|
||||
|
||||
Custom Gitea fork with enhanced wiki system, DLID licensing, issue statuses, cascade merge, security scanning, org metadata, CI standardization, and project board API.
|
||||
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.
|
||||
|
||||
 
|
||||
|
||||
@@ -17,6 +17,7 @@ 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
|
||||
|
||||
@@ -28,19 +28,28 @@ type OrgProtectedBranch struct {
|
||||
CanPush bool `xorm:"NOT NULL DEFAULT false"`
|
||||
EnableWhitelist bool `xorm:"NOT NULL DEFAULT false"`
|
||||
WhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
|
||||
WhitelistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||
WhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
|
||||
EnableMergeWhitelist bool `xorm:"NOT NULL DEFAULT false"`
|
||||
MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
|
||||
MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||
MergeWhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
|
||||
CanForcePush bool `xorm:"NOT NULL DEFAULT false"`
|
||||
EnableForcePushAllowlist bool `xorm:"NOT NULL DEFAULT false"`
|
||||
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"`
|
||||
EnableApprovalsWhitelist bool `xorm:"NOT NULL DEFAULT false"`
|
||||
ApprovalsWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
|
||||
ApprovalsWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||
BlockOnRejectedReviews bool `xorm:"NOT NULL DEFAULT false"`
|
||||
BlockOnOfficialReviewRequests bool `xorm:"NOT NULL DEFAULT false"`
|
||||
BlockOnOutdatedBranch bool `xorm:"NOT NULL DEFAULT false"`
|
||||
@@ -84,8 +93,9 @@ func (o *OrgProtectedBranch) Match(branchName string) bool {
|
||||
}
|
||||
|
||||
// ToProtectedBranch converts an org-level rule to a ProtectedBranch for use
|
||||
// in the standard enforcement path. Fields that are user-scoped (WhitelistUserIDs etc.)
|
||||
// are left empty because org rules only reference teams.
|
||||
// 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.
|
||||
func (o *OrgProtectedBranch) ToProtectedBranch() *ProtectedBranch {
|
||||
return &ProtectedBranch{
|
||||
ID: o.ID,
|
||||
@@ -94,19 +104,28 @@ func (o *OrgProtectedBranch) ToProtectedBranch() *ProtectedBranch {
|
||||
CanPush: o.CanPush,
|
||||
EnableWhitelist: o.EnableWhitelist,
|
||||
WhitelistTeamIDs: o.WhitelistTeamIDs,
|
||||
WhitelistUserIDs: o.WhitelistUserIDs,
|
||||
WhitelistActionsUser: o.WhitelistActionsUser,
|
||||
EnableMergeWhitelist: o.EnableMergeWhitelist,
|
||||
MergeWhitelistTeamIDs: o.MergeWhitelistTeamIDs,
|
||||
MergeWhitelistUserIDs: o.MergeWhitelistUserIDs,
|
||||
MergeWhitelistActionsUser: o.MergeWhitelistActionsUser,
|
||||
CanForcePush: o.CanForcePush,
|
||||
EnableForcePushAllowlist: o.EnableForcePushAllowlist,
|
||||
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,
|
||||
EnableApprovalsWhitelist: o.EnableApprovalsWhitelist,
|
||||
ApprovalsWhitelistTeamIDs: o.ApprovalsWhitelistTeamIDs,
|
||||
ApprovalsWhitelistUserIDs: o.ApprovalsWhitelistUserIDs,
|
||||
BlockOnRejectedReviews: o.BlockOnRejectedReviews,
|
||||
BlockOnOfficialReviewRequests: o.BlockOnOfficialReviewRequests,
|
||||
BlockOnOutdatedBranch: o.BlockOnOutdatedBranch,
|
||||
|
||||
@@ -133,8 +133,10 @@ 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, nil
|
||||
return nil, err
|
||||
}
|
||||
if orgRule == nil {
|
||||
return nil, nil
|
||||
|
||||
@@ -26,27 +26,32 @@ func mergeMostRestrictive(repoRule, orgRule *ProtectedBranch) *ProtectedBranch {
|
||||
eff.CanPush = repoRule.CanPush && orgRule.CanPush
|
||||
eff.EnableWhitelist, eff.WhitelistUserIDs = mergeAllowlist(repoRule.EnableWhitelist, repoRule.WhitelistUserIDs, orgRule.EnableWhitelist, orgRule.WhitelistUserIDs)
|
||||
_, eff.WhitelistTeamIDs = mergeAllowlist(repoRule.EnableWhitelist, repoRule.WhitelistTeamIDs, orgRule.EnableWhitelist, orgRule.WhitelistTeamIDs)
|
||||
eff.WhitelistDeployKeys = repoRule.WhitelistDeployKeys && orgRule.WhitelistDeployKeys
|
||||
eff.WhitelistActionsUser = repoRule.WhitelistActionsUser && orgRule.WhitelistActionsUser
|
||||
// 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 && orgRule.ForcePushAllowlistDeployKeys
|
||||
eff.ForcePushAllowlistActionsUser = repoRule.ForcePushAllowlistActionsUser && orgRule.ForcePushAllowlistActionsUser
|
||||
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 && orgRule.DeleteAllowlistDeployKeys
|
||||
eff.DeleteAllowlistActionsUser = repoRule.DeleteAllowlistActionsUser && orgRule.DeleteAllowlistActionsUser
|
||||
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 = repoRule.MergeWhitelistActionsUser && orgRule.MergeWhitelistActionsUser
|
||||
eff.MergeWhitelistActionsUser = mergeAllowFlag(repoRule.EnableMergeWhitelist, repoRule.MergeWhitelistActionsUser, orgRule.EnableMergeWhitelist, orgRule.MergeWhitelistActionsUser)
|
||||
|
||||
// Status checks.
|
||||
eff.EnableStatusCheck = repoRule.EnableStatusCheck || orgRule.EnableStatusCheck
|
||||
@@ -89,6 +94,25 @@ func mergeAllowlist(aEnabled bool, aIDs []int64, bEnabled bool, bIDs []int64) (b
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
// 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")
|
||||
}
|
||||
@@ -444,6 +444,7 @@ func prepareMigrationTasks() []*migration {
|
||||
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),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// 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))
|
||||
}
|
||||
@@ -14,19 +14,28 @@ type OrgBranchProtection struct {
|
||||
EnablePush bool `json:"enable_push"`
|
||||
EnablePushWhitelist bool `json:"enable_push_whitelist"`
|
||||
PushWhitelistTeams []string `json:"push_whitelist_teams"`
|
||||
PushWhitelistUsernames []string `json:"push_whitelist_usernames"`
|
||||
PushWhitelistActionsUser bool `json:"push_whitelist_actions_user"`
|
||||
EnableForcePush bool `json:"enable_force_push"`
|
||||
EnableForcePushAllowlist bool `json:"enable_force_push_allowlist"`
|
||||
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"`
|
||||
MergeWhitelistActionsUser bool `json:"merge_whitelist_actions_user"`
|
||||
EnableStatusCheck bool `json:"enable_status_check"`
|
||||
StatusCheckContexts []string `json:"status_check_contexts"`
|
||||
RequiredApprovals int64 `json:"required_approvals"`
|
||||
EnableApprovalsWhitelist bool `json:"enable_approvals_whitelist"`
|
||||
ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams"`
|
||||
ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username"`
|
||||
BlockOnRejectedReviews bool `json:"block_on_rejected_reviews"`
|
||||
BlockOnOfficialReviewRequests bool `json:"block_on_official_review_requests"`
|
||||
BlockOnOutdatedBranch bool `json:"block_on_outdated_branch"`
|
||||
@@ -49,19 +58,28 @@ type CreateOrgBranchProtectionOption struct {
|
||||
EnablePush bool `json:"enable_push"`
|
||||
EnablePushWhitelist bool `json:"enable_push_whitelist"`
|
||||
PushWhitelistTeams []string `json:"push_whitelist_teams"`
|
||||
PushWhitelistUsernames []string `json:"push_whitelist_usernames"`
|
||||
PushWhitelistActionsUser bool `json:"push_whitelist_actions_user"`
|
||||
EnableForcePush bool `json:"enable_force_push"`
|
||||
EnableForcePushAllowlist bool `json:"enable_force_push_allowlist"`
|
||||
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"`
|
||||
MergeWhitelistActionsUser bool `json:"merge_whitelist_actions_user"`
|
||||
EnableStatusCheck bool `json:"enable_status_check"`
|
||||
StatusCheckContexts []string `json:"status_check_contexts"`
|
||||
RequiredApprovals int64 `json:"required_approvals"`
|
||||
EnableApprovalsWhitelist bool `json:"enable_approvals_whitelist"`
|
||||
ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams"`
|
||||
ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username"`
|
||||
BlockOnRejectedReviews bool `json:"block_on_rejected_reviews"`
|
||||
BlockOnOfficialReviewRequests bool `json:"block_on_official_review_requests"`
|
||||
BlockOnOutdatedBranch bool `json:"block_on_outdated_branch"`
|
||||
@@ -79,19 +97,28 @@ type EditOrgBranchProtectionOption struct {
|
||||
EnablePush *bool `json:"enable_push"`
|
||||
EnablePushWhitelist *bool `json:"enable_push_whitelist"`
|
||||
PushWhitelistTeams []string `json:"push_whitelist_teams"`
|
||||
PushWhitelistUsernames []string `json:"push_whitelist_usernames"`
|
||||
PushWhitelistActionsUser *bool `json:"push_whitelist_actions_user"`
|
||||
EnableForcePush *bool `json:"enable_force_push"`
|
||||
EnableForcePushAllowlist *bool `json:"enable_force_push_allowlist"`
|
||||
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"`
|
||||
MergeWhitelistActionsUser *bool `json:"merge_whitelist_actions_user"`
|
||||
EnableStatusCheck *bool `json:"enable_status_check"`
|
||||
StatusCheckContexts []string `json:"status_check_contexts"`
|
||||
RequiredApprovals *int64 `json:"required_approvals"`
|
||||
EnableApprovalsWhitelist *bool `json:"enable_approvals_whitelist"`
|
||||
ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams"`
|
||||
ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username"`
|
||||
BlockOnRejectedReviews *bool `json:"block_on_rejected_reviews"`
|
||||
BlockOnOfficialReviewRequests *bool `json:"block_on_official_review_requests"`
|
||||
BlockOnOutdatedBranch *bool `json:"block_on_outdated_branch"`
|
||||
|
||||
+18
-10
@@ -86,31 +86,35 @@ func Test_NormalizeEOL(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_RandomInt(t *testing.T) {
|
||||
randInt := CryptoRandomInt(255)
|
||||
randInt, err := CryptoRandomInt(255)
|
||||
assert.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, randInt, int64(0))
|
||||
assert.LessOrEqual(t, randInt, int64(255))
|
||||
}
|
||||
|
||||
func Test_RandomString(t *testing.T) {
|
||||
str1 := CryptoRandomString(32)
|
||||
var err error
|
||||
str1, err := CryptoRandomString(32)
|
||||
assert.NoError(t, err)
|
||||
matches, err := regexp.MatchString(`^[a-zA-Z0-9]{32}$`, str1)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, matches)
|
||||
|
||||
str2 := CryptoRandomString(32)
|
||||
str2, err := CryptoRandomString(32)
|
||||
assert.NoError(t, err)
|
||||
matches, err = regexp.MatchString(`^[a-zA-Z0-9]{32}$`, str1)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, matches)
|
||||
|
||||
assert.NotEqual(t, str1, str2)
|
||||
|
||||
str3 := CryptoRandomString(256)
|
||||
str3, err := CryptoRandomString(256)
|
||||
assert.NoError(t, err)
|
||||
matches, err = regexp.MatchString(`^[a-zA-Z0-9]{256}$`, str3)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, matches)
|
||||
|
||||
str4 := CryptoRandomString(256)
|
||||
str4, err := CryptoRandomString(256)
|
||||
assert.NoError(t, err)
|
||||
matches, err = regexp.MatchString(`^[a-zA-Z0-9]{256}$`, str4)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, matches)
|
||||
@@ -119,15 +123,19 @@ func Test_RandomString(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_RandomBytes(t *testing.T) {
|
||||
bytes1 := CryptoRandomBytes(32)
|
||||
bytes1, err := CryptoRandomBytes(32)
|
||||
assert.NoError(t, err)
|
||||
|
||||
bytes2 := CryptoRandomBytes(32)
|
||||
bytes2, err := CryptoRandomBytes(32)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NotEqual(t, bytes1, bytes2)
|
||||
|
||||
bytes3 := CryptoRandomBytes(256)
|
||||
bytes3, err := CryptoRandomBytes(256)
|
||||
assert.NoError(t, err)
|
||||
|
||||
bytes4 := CryptoRandomBytes(256)
|
||||
bytes4, err := CryptoRandomBytes(256)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NotEqual(t, bytes3, bytes4)
|
||||
}
|
||||
|
||||
@@ -2464,7 +2464,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.event_delete": "Branch Deletion",
|
||||
"repo.settings.protect_branch_deletion": "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",
|
||||
|
||||
+24
-24
@@ -516,7 +516,7 @@ func reqOrgVisible() func(ctx *context.APIContext) {
|
||||
ctx.APIErrorInternal(errors.New("reqOrgVisible: unprepared context"))
|
||||
return
|
||||
}
|
||||
if !organization.HasOrgOrUserVisible(ctx, ctx.Org.Organization.AsUser(), ctx.Doer) {
|
||||
if !organization.IsOwnerVisibleToDoer(ctx, ctx.Org.Organization.AsUser(), ctx.Doer) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
@@ -1698,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)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/organization"
|
||||
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
|
||||
api "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/web"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
@@ -36,6 +37,18 @@ func toAPIOrgBranchProtection(ctx *context.APIContext, rule *git_model.OrgProtec
|
||||
return names
|
||||
}
|
||||
|
||||
resolveUserNames := func(ids []int64) []string {
|
||||
names := make([]string, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
u, err := user_model.GetUserByID(ctx, id)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
names = append(names, u.Name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
return &api.OrgBranchProtection{
|
||||
ID: rule.ID,
|
||||
OrgID: rule.OrgID,
|
||||
@@ -44,19 +57,28 @@ func toAPIOrgBranchProtection(ctx *context.APIContext, rule *git_model.OrgProtec
|
||||
EnablePush: rule.CanPush,
|
||||
EnablePushWhitelist: rule.EnableWhitelist,
|
||||
PushWhitelistTeams: resolveTeamNames(rule.WhitelistTeamIDs),
|
||||
PushWhitelistUsernames: resolveUserNames(rule.WhitelistUserIDs),
|
||||
PushWhitelistActionsUser: rule.WhitelistActionsUser,
|
||||
EnableForcePush: rule.CanForcePush,
|
||||
EnableForcePushAllowlist: rule.EnableForcePushAllowlist,
|
||||
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),
|
||||
MergeWhitelistActionsUser: rule.MergeWhitelistActionsUser,
|
||||
EnableStatusCheck: rule.EnableStatusCheck,
|
||||
StatusCheckContexts: rule.StatusCheckContexts,
|
||||
RequiredApprovals: rule.RequiredApprovals,
|
||||
EnableApprovalsWhitelist: rule.EnableApprovalsWhitelist,
|
||||
ApprovalsWhitelistTeams: resolveTeamNames(rule.ApprovalsWhitelistTeamIDs),
|
||||
ApprovalsWhitelistUsernames: resolveUserNames(rule.ApprovalsWhitelistUserIDs),
|
||||
BlockOnRejectedReviews: rule.BlockOnRejectedReviews,
|
||||
BlockOnOfficialReviewRequests: rule.BlockOnOfficialReviewRequests,
|
||||
BlockOnOutdatedBranch: rule.BlockOnOutdatedBranch,
|
||||
@@ -88,6 +110,41 @@ 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.
|
||||
func resolveUserIDs(ctx *context.APIContext, names []string) ([]int64, bool) {
|
||||
if len(names) == 0 {
|
||||
return nil, true
|
||||
}
|
||||
seen := make(map[int64]struct{}, len(names))
|
||||
ids := make([]int64, 0, len(names))
|
||||
for _, name := range names {
|
||||
u, err := user_model.GetUserByName(ctx, name)
|
||||
if err != nil {
|
||||
if !user_model.IsErrUserNotExist(err) {
|
||||
ctx.APIErrorInternal(err)
|
||||
return nil, false
|
||||
}
|
||||
u, err = user_model.GetUserByEmail(ctx, name)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "unknown user: "+name)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
if _, dup := seen[u.ID]; dup {
|
||||
continue
|
||||
}
|
||||
seen[u.ID] = struct{}{}
|
||||
ids = append(ids, u.ID)
|
||||
}
|
||||
return ids, true
|
||||
}
|
||||
|
||||
// ListOrgBranchProtections list org-level branch protection rules
|
||||
func ListOrgBranchProtections(ctx *context.APIContext) {
|
||||
// swagger:operation GET /orgs/{org}/branch_protections organization orgListBranchProtections
|
||||
@@ -218,6 +275,26 @@ func CreateOrgBranchProtection(ctx *context.APIContext) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
pushUsers, ok := resolveUserIDs(ctx, form.PushWhitelistUsernames)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
forcePushUsers, ok := resolveUserIDs(ctx, form.ForcePushAllowlistUsernames)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
mergeUsers, ok := resolveUserIDs(ctx, form.MergeWhitelistUsernames)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
approvalsUsers, ok := resolveUserIDs(ctx, form.ApprovalsWhitelistUsernames)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
deleteUsers, ok := resolveUserIDs(ctx, form.DeleteAllowlistUsernames)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
rule := &git_model.OrgProtectedBranch{
|
||||
OrgID: orgID,
|
||||
@@ -226,19 +303,28 @@ func CreateOrgBranchProtection(ctx *context.APIContext) {
|
||||
CanPush: form.EnablePush,
|
||||
EnableWhitelist: form.EnablePush && form.EnablePushWhitelist,
|
||||
WhitelistTeamIDs: pushTeams,
|
||||
WhitelistUserIDs: pushUsers,
|
||||
WhitelistActionsUser: form.EnablePush && form.EnablePushWhitelist && form.PushWhitelistActionsUser,
|
||||
CanForcePush: form.EnablePush && form.EnableForcePush,
|
||||
EnableForcePushAllowlist: form.EnablePush && form.EnableForcePush && form.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,
|
||||
EnableMergeWhitelist: form.EnableMergeWhitelist,
|
||||
MergeWhitelistTeamIDs: mergeTeams,
|
||||
MergeWhitelistUserIDs: mergeUsers,
|
||||
MergeWhitelistActionsUser: form.EnableMergeWhitelist && form.MergeWhitelistActionsUser,
|
||||
EnableStatusCheck: form.EnableStatusCheck,
|
||||
StatusCheckContexts: form.StatusCheckContexts,
|
||||
RequiredApprovals: form.RequiredApprovals,
|
||||
EnableApprovalsWhitelist: form.EnableApprovalsWhitelist,
|
||||
ApprovalsWhitelistTeamIDs: approvalsTeams,
|
||||
ApprovalsWhitelistUserIDs: approvalsUsers,
|
||||
BlockOnRejectedReviews: form.BlockOnRejectedReviews,
|
||||
BlockOnOfficialReviewRequests: form.BlockOnOfficialReviewRequests,
|
||||
BlockOnOutdatedBranch: form.BlockOnOutdatedBranch,
|
||||
@@ -320,6 +406,16 @@ func EditOrgBranchProtection(ctx *context.APIContext) {
|
||||
}
|
||||
rule.WhitelistTeamIDs = ids
|
||||
}
|
||||
if form.PushWhitelistUsernames != nil {
|
||||
ids, ok := resolveUserIDs(ctx, form.PushWhitelistUsernames)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
rule.WhitelistUserIDs = ids
|
||||
}
|
||||
if form.PushWhitelistActionsUser != nil {
|
||||
rule.WhitelistActionsUser = *form.PushWhitelistActionsUser
|
||||
}
|
||||
if form.EnableForcePush != nil {
|
||||
rule.CanForcePush = *form.EnableForcePush
|
||||
}
|
||||
@@ -333,6 +429,16 @@ func EditOrgBranchProtection(ctx *context.APIContext) {
|
||||
}
|
||||
rule.ForcePushAllowlistTeamIDs = ids
|
||||
}
|
||||
if form.ForcePushAllowlistUsernames != nil {
|
||||
ids, ok := resolveUserIDs(ctx, form.ForcePushAllowlistUsernames)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
rule.ForcePushAllowlistUserIDs = ids
|
||||
}
|
||||
if form.ForcePushAllowlistActionsUser != nil {
|
||||
rule.ForcePushAllowlistActionsUser = *form.ForcePushAllowlistActionsUser
|
||||
}
|
||||
if form.EnableDelete != nil {
|
||||
rule.CanDelete = *form.EnableDelete
|
||||
}
|
||||
@@ -346,6 +452,16 @@ func EditOrgBranchProtection(ctx *context.APIContext) {
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -356,6 +472,16 @@ func EditOrgBranchProtection(ctx *context.APIContext) {
|
||||
}
|
||||
rule.MergeWhitelistTeamIDs = ids
|
||||
}
|
||||
if form.MergeWhitelistUsernames != nil {
|
||||
ids, ok := resolveUserIDs(ctx, form.MergeWhitelistUsernames)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
rule.MergeWhitelistUserIDs = ids
|
||||
}
|
||||
if form.MergeWhitelistActionsUser != nil {
|
||||
rule.MergeWhitelistActionsUser = *form.MergeWhitelistActionsUser
|
||||
}
|
||||
if form.EnableStatusCheck != nil {
|
||||
rule.EnableStatusCheck = *form.EnableStatusCheck
|
||||
}
|
||||
@@ -375,6 +501,13 @@ func EditOrgBranchProtection(ctx *context.APIContext) {
|
||||
}
|
||||
rule.ApprovalsWhitelistTeamIDs = ids
|
||||
}
|
||||
if form.ApprovalsWhitelistUsernames != nil {
|
||||
ids, ok := resolveUserIDs(ctx, form.ApprovalsWhitelistUsernames)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
rule.ApprovalsWhitelistUserIDs = ids
|
||||
}
|
||||
if form.BlockOnRejectedReviews != nil {
|
||||
rule.BlockOnRejectedReviews = *form.BlockOnRejectedReviews
|
||||
}
|
||||
|
||||
@@ -24,6 +24,23 @@ func toAPIOrgEmailDomainPolicy(policy *git_model.OrgEmailDomainPolicy, orgID int
|
||||
|
||||
// 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 {
|
||||
@@ -35,6 +52,31 @@ func GetOrgEmailDomainPolicy(ctx *context.APIContext) {
|
||||
|
||||
// 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
|
||||
|
||||
@@ -59,6 +101,21 @@ func EditOrgEmailDomainPolicy(ctx *context.APIContext) {
|
||||
|
||||
// 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
|
||||
|
||||
@@ -32,6 +32,23 @@ func toAPIOrgPushPolicy(policy *git_model.OrgPushPolicy, orgID int64) *api.OrgPu
|
||||
|
||||
// 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 {
|
||||
@@ -43,6 +60,31 @@ func GetOrgPushPolicy(ctx *context.APIContext) {
|
||||
|
||||
// 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
|
||||
|
||||
@@ -80,6 +122,21 @@ func EditOrgPushPolicy(ctx *context.APIContext) {
|
||||
|
||||
// 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
|
||||
|
||||
@@ -42,6 +42,23 @@ func toAPIOrgRepoDefaults(d *git_model.OrgRepoDefaults, orgID int64) *api.OrgRep
|
||||
|
||||
// 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 {
|
||||
@@ -53,6 +70,31 @@ func GetOrgRepoDefaults(ctx *context.APIContext) {
|
||||
|
||||
// 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
|
||||
|
||||
@@ -109,6 +151,21 @@ func EditOrgRepoDefaults(ctx *context.APIContext) {
|
||||
|
||||
// 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
|
||||
|
||||
@@ -41,6 +41,23 @@ func toAPIOrgTagProtection(ctx *context.APIContext, rule *git_model.OrgProtected
|
||||
|
||||
// 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)
|
||||
@@ -55,6 +72,29 @@ func ListOrgTagProtections(ctx *context.APIContext) {
|
||||
|
||||
// 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)
|
||||
@@ -69,6 +109,33 @@ func GetOrgTagProtection(ctx *context.APIContext) {
|
||||
|
||||
// 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
|
||||
|
||||
@@ -101,6 +168,37 @@ func CreateOrgTagProtection(ctx *context.APIContext) {
|
||||
|
||||
// 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
|
||||
|
||||
@@ -134,6 +232,27 @@ func EditOrgTagProtection(ctx *context.APIContext) {
|
||||
|
||||
// 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 {
|
||||
|
||||
@@ -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,6 +44,13 @@ 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
|
||||
@@ -51,6 +58,17 @@ 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"
|
||||
@@ -71,9 +89,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,
|
||||
@@ -89,7 +107,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,
|
||||
@@ -111,9 +129,21 @@ 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 {
|
||||
@@ -173,9 +203,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,
|
||||
@@ -191,7 +221,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,
|
||||
|
||||
@@ -159,6 +159,44 @@ type swaggerParameterBodies struct {
|
||||
// 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
|
||||
|
||||
// in:body
|
||||
CreateOAuth2ApplicationOptions api.CreateOAuth2ApplicationOptions
|
||||
|
||||
|
||||
@@ -41,3 +41,52 @@ type swaggerResponseOrganizationPermissions struct {
|
||||
// in:body
|
||||
Body api.OrganizationPermissions `json:"body"`
|
||||
}
|
||||
|
||||
// OrgBranchProtection
|
||||
// swagger:response OrgBranchProtection
|
||||
type swaggerResponseOrgBranchProtection struct {
|
||||
// in:body
|
||||
Body api.OrgBranchProtection `json:"body"`
|
||||
}
|
||||
|
||||
// OrgBranchProtectionList
|
||||
// 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"`
|
||||
}
|
||||
|
||||
@@ -646,7 +646,7 @@ func (ctx *preReceiveContext) checkOrgPushPolicyBranch(oldCommitID, newCommitID,
|
||||
}
|
||||
|
||||
if policy.MaxFileSize > 0 {
|
||||
if path, size := ctx.largestBlobOverLimit(newCommitID, policy.MaxFileSize); path != "" {
|
||||
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),
|
||||
})
|
||||
@@ -673,34 +673,90 @@ func (ctx *preReceiveContext) checkOrgPushPolicyTag(tagName string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// largestBlobOverLimit returns the first file (and its size) in the pushed tip tree
|
||||
// that exceeds limit bytes, or ("", 0) if none — or on any error (fail open).
|
||||
func (ctx *preReceiveContext) largestBlobOverLimit(commitID string, limit int64) (string, int64) {
|
||||
output, _, err := gitrepo.RunCmdString(ctx,
|
||||
ctx.Repo.Repository,
|
||||
gitcmd.NewCommand("ls-tree", "-r", "--long").
|
||||
AddDynamicArguments(commitID).
|
||||
WithEnv(ctx.env),
|
||||
)
|
||||
if err != nil {
|
||||
log.Error("org push policy ls-tree for %-v: %v", ctx.Repo.Repository, err)
|
||||
// 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]) // mode, type, hash, size
|
||||
if len(fields) < 4 || fields[1] != "blob" {
|
||||
fields := strings.Fields(line[:tab]) // ":oldmode newmode oldsha newsha status"
|
||||
if len(fields) < 5 {
|
||||
continue
|
||||
}
|
||||
size, perr := strconv.ParseInt(fields[3], 10, 64)
|
||||
if perr != nil {
|
||||
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 size > limit {
|
||||
return line[tab+1:], size
|
||||
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 != "" && statusIDStr != "" {
|
||||
if statusIDStr := ctx.Req.FormValue("status_id"); statusIDStr != "" {
|
||||
if statusIDStr == "reopen" {
|
||||
// Reopen via dropdown
|
||||
if issue.IsClosed {
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
|
||||
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
|
||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
|
||||
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/container"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
|
||||
|
||||
@@ -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(`yaml\.load\s*\([^)]*(?:Loader\s*=\s*yaml\.(?:Unsafe|Full)Loader|[^)]*\)(?!\s*#))`),
|
||||
Pattern: regexp.MustCompile(`(?i)yaml\.load\s*\(`),
|
||||
Description: "yaml.load() without SafeLoader allows arbitrary code execution — use yaml.safe_load()",
|
||||
Languages: []string{".py"},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,7 +172,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.event_delete"}}</h5>
|
||||
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.protect_branch_deletion"}}</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}}>
|
||||
|
||||
Generated
+2709
-2
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ package integration
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
auth_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/auth"
|
||||
@@ -36,7 +37,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", []byte(body)).
|
||||
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", strings.NewReader(body)).
|
||||
AddTokenAuth(token).
|
||||
SetHeader("Content-Type", "application/json")
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
@@ -51,7 +52,7 @@ func TestAPILicensePackages(t *testing.T) {
|
||||
|
||||
t.Run("CreatePackageNoName", func(t *testing.T) {
|
||||
body := `{"description":"Missing name"}`
|
||||
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", []byte(body)).
|
||||
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", strings.NewReader(body)).
|
||||
AddTokenAuth(token).
|
||||
SetHeader("Content-Type", "application/json")
|
||||
MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||
@@ -68,7 +69,7 @@ func TestAPILicenseKeys(t *testing.T) {
|
||||
|
||||
// Create a package first.
|
||||
body := `{"name":"Test Package","duration_days":30}`
|
||||
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", []byte(body)).
|
||||
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", strings.NewReader(body)).
|
||||
AddTokenAuth(token).
|
||||
SetHeader("Content-Type", "application/json")
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
@@ -80,7 +81,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", []byte(body)).
|
||||
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys", strings.NewReader(body)).
|
||||
AddTokenAuth(token).
|
||||
SetHeader("Content-Type", "application/json")
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
@@ -104,7 +105,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), []byte(body)).
|
||||
req := NewRequestWithBody(t, "PATCH", fmt.Sprintf("%s/license-keys/%d", urlPrefix, createdKeyID), strings.NewReader(body)).
|
||||
AddTokenAuth(token).
|
||||
SetHeader("Content-Type", "application/json")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
@@ -124,7 +125,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", []byte(body)).
|
||||
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys/validate", strings.NewReader(body)).
|
||||
SetHeader("Content-Type", "application/json")
|
||||
// Note: no token — this is a public endpoint.
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
@@ -136,7 +137,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", []byte(body)).
|
||||
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys/validate", strings.NewReader(body)).
|
||||
SetHeader("Content-Type", "application/json")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
var result api.ValidateLicenseKeyResponse
|
||||
@@ -161,7 +162,7 @@ func TestAPILicensePurchaseWebhook(t *testing.T) {
|
||||
|
||||
// Create a package.
|
||||
body := `{"name":"Purchase Test","duration_days":90}`
|
||||
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", []byte(body)).
|
||||
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", strings.NewReader(body)).
|
||||
AddTokenAuth(token).
|
||||
SetHeader("Content-Type", "application/json")
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
@@ -170,7 +171,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", []byte(body)).
|
||||
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys/purchase", strings.NewReader(body)).
|
||||
AddTokenAuth(token).
|
||||
SetHeader("Content-Type", "application/json")
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
@@ -183,7 +184,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", []byte(body)).
|
||||
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys/purchase", strings.NewReader(body)).
|
||||
AddTokenAuth(token).
|
||||
SetHeader("Content-Type", "application/json")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
@@ -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 := newFakeOIDCServer(t, FakeOIDCConfig{Sub: "test-sub", Email: "test@example.com", Name: "Test User"})
|
||||
srv := newFakeOIDCServerWithConfig(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 newFakeOIDCServer(t *testing.T, cfg FakeOIDCConfig) *httptest.Server {
|
||||
func newFakeOIDCServerWithConfig(t *testing.T, cfg FakeOIDCConfig) *httptest.Server {
|
||||
t.Helper()
|
||||
|
||||
var srv *httptest.Server
|
||||
|
||||
+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) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||
*Repo: [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) · [mokocli](https://git.mokoconsulting.tech/MokoConsulting/mokocli/wiki/Home)*
|
||||
|
||||
| Revision | Date | Author | Description |
|
||||
|---|---|---|---|
|
||||
|
||||
@@ -126,7 +126,7 @@ When a field definition is deleted, all associated values in `custom_field_value
|
||||
| System | Relationship |
|
||||
|--------|-------------|
|
||||
| Update Server | Repo-scoped custom fields with specific names (Extension Name, Display Name, etc.) are read by the update feed generators as the highest-priority metadata source. |
|
||||
| Manifest Settings | Manifest fields follow the moko-platform schema and are separate from custom fields. Custom fields are user-defined; manifest fields are standardized. |
|
||||
| Manifest Settings | Manifest fields follow the mokocli schema and are separate from custom fields. Custom fields are user-defined; manifest fields are standardized. |
|
||||
| Issue Statuses | Custom statuses are a separate feature with their own dedicated table and UI, not implemented as custom fields. |
|
||||
|
||||
---
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ Located at `/var/lib/gitea/custom/`:
|
||||
|
||||
---
|
||||
|
||||
*Repo: [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||
*Repo: [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) · [mokocli](https://git.mokoconsulting.tech/MokoConsulting/mokocli/wiki/Home)*
|
||||
|
||||
| Revision | Date | Author | Description |
|
||||
|---|---|---|---|
|
||||
|
||||
@@ -309,8 +309,8 @@ Packages can be **archived** instead of permanently deleted. This is the recomme
|
||||
|
||||
In **Organization Settings → Licensing & Update Streams**, under **Extension Metadata**:
|
||||
1. Set the **Platform** (Joomla, Dolibarr, WordPress, etc.)
|
||||
2. Set the **Extension Name** (e.g., `pkg_mokowaas`) — this becomes the `<element>` in the XML feed
|
||||
3. Set the **Display Name** (e.g., "Package - MokoWaaS") — shown in Joomla update manager
|
||||
2. Set the **Extension Name** (e.g., `pkg_mokosuite`) — this becomes the `<element>` in the XML feed
|
||||
3. Set the **Display Name** (e.g., "Package - MokoSuite") — shown in Joomla update manager
|
||||
4. Set the **Extension Type** (component, module, plugin, package, template, library)
|
||||
5. Set the **Target Version** regex (e.g., `(5|6)\..*` for Joomla 5 and 6)
|
||||
6. Set the **PHP Minimum** if applicable (e.g., `8.1`)
|
||||
|
||||
@@ -7,7 +7,7 @@ The manifest settings feature provides a centralized way to store and manage pro
|
||||
Each repository can have a manifest that describes:
|
||||
|
||||
- **Identity** — project name, organization, description, version, and license
|
||||
- **Governance** — platform type, moko-platform standards version, and standards source URL
|
||||
- **Governance** — platform type, mokocli standards version, and standards source URL
|
||||
- **Build** — language, package type, and entry point
|
||||
|
||||
These settings replace the legacy `.mokogitea/manifest.xml` file-based approach.
|
||||
@@ -35,7 +35,7 @@ If a field already has a value in the database (e.g., from org-level custom fiel
|
||||
|
||||
## REST API
|
||||
|
||||
The manifest API allows Actions workflows and the moko-platform CLI to read and write manifest settings programmatically.
|
||||
The manifest API allows Actions workflows and the mokocli CLI to read and write manifest settings programmatically.
|
||||
|
||||
### Get Manifest
|
||||
|
||||
@@ -57,7 +57,7 @@ Returns the current manifest settings. If no manifest has been saved, returns de
|
||||
"license_name": "GNU General Public License v3",
|
||||
"platform": "go",
|
||||
"standards_version": "05.00.00",
|
||||
"standards_source": "https://code.mokoconsulting.tech/MokoConsulting/moko-platform",
|
||||
"standards_source": "https://code.mokoconsulting.tech/MokoConsulting/mokocli",
|
||||
"language": "Go",
|
||||
"package_type": "application",
|
||||
"entry_point": "./"
|
||||
@@ -101,8 +101,8 @@ Manifest settings are stored in the `repo_manifest` table (migration v347). One
|
||||
| System | Relationship |
|
||||
|--------|-------------|
|
||||
| Update Server | The update server generators read from both manifest settings and update_stream_config. Manifest provides identity metadata; update_stream_config provides feed-specific settings. |
|
||||
| Custom Fields | Repo-scoped custom fields (org settings) are separate from manifest fields. Custom fields are user-defined; manifest fields follow the moko-platform schema. |
|
||||
| moko-platform CLI | The CLI reads manifest settings via the API for version bumping, build decisions, and cross-repo syncing (see issue #505). |
|
||||
| Custom Fields | Repo-scoped custom fields (org settings) are separate from manifest fields. Custom fields are user-defined; manifest fields follow the mokocli schema. |
|
||||
| mokocli CLI | The CLI reads manifest settings via the API for version bumping, build decisions, and cross-repo syncing (see issue #505). |
|
||||
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -195,7 +195,7 @@ curl -X POST -H "Authorization: token TOKEN" \
|
||||
|
||||
---
|
||||
|
||||
*Repo: [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||
*Repo: [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) · [mokocli](https://git.mokoconsulting.tech/MokoConsulting/mokocli/wiki/Home)*
|
||||
|
||||
| Revision | Date | Author | Description |
|
||||
|---|---|---|---|
|
||||
|
||||
+2
-2
@@ -23,9 +23,9 @@
|
||||
|
||||
## In Progress
|
||||
|
||||
- Rename moko-platform to MokoPlatform
|
||||
- Rename mokocli to MokoCLI
|
||||
- Granular role-based permissions for all features (#9)
|
||||
- Wire moko-platform CLI to manifest API (#505)
|
||||
- Wire mokocli CLI to manifest API (#505)
|
||||
- Bulk migrate remaining 41 flat wikis to folders
|
||||
|
||||
## Planned
|
||||
|
||||
Reference in New Issue
Block a user