Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 10760463f8 | |||
| bc3bb0f778 | |||
| d955bac72b | |||
| afc470f513 | |||
| 6505840839 | |||
| 7d98098a87 | |||
| 28b9d94658 | |||
| 8d8ecf176a | |||
| 7203628004 | |||
| 36a824be1d | |||
| fcf33d35df | |||
| 358606e235 | |||
| 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
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: mokocli.Release
|
# INGROUP: mokocli.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||||
# PATH: /.mokogitea/workflows/auto-bump.yml
|
# PATH: /.mokogitea/workflows/auto-bump.yml
|
||||||
# VERSION: 09.02.00
|
# VERSION: 09.02.00
|
||||||
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
||||||
@@ -34,7 +34,8 @@ jobs:
|
|||||||
if: >-
|
if: >-
|
||||||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||||
!contains(github.event.head_commit.message, '[skip bump]') &&
|
!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:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -52,7 +53,7 @@ jobs:
|
|||||||
echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV"
|
echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV"
|
||||||
else
|
else
|
||||||
git clone --depth 1 --branch main --quiet \
|
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
|
/tmp/mokocli
|
||||||
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
||||||
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV"
|
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV"
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: mokocli.Release
|
# INGROUP: mokocli.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||||
# PATH: /templates/workflows/universal/auto-release.yml.template
|
# PATH: /.mokogitea/workflows/auto-release.yml
|
||||||
# VERSION: 05.01.00
|
# VERSION: 05.01.00
|
||||||
# BRIEF: Universal build & release � detects platform from manifest.xml
|
# BRIEF: Universal build & release � detects platform from manifest.xml
|
||||||
#
|
#
|
||||||
@@ -142,8 +142,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
git fetch origin rc
|
git fetch origin rc
|
||||||
git checkout rc
|
git checkout rc
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
git config --local user.email "mokogitea-actions[bot]@mokoconsulting.tech"
|
||||||
git config --local user.name "gitea-actions[bot]"
|
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 remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
|
||||||
- name: Publish RC release
|
- name: Publish RC release
|
||||||
@@ -214,8 +214,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Configure git for bot pushes
|
- name: Configure git for bot pushes
|
||||||
run: |
|
run: |
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
git config --local user.email "mokogitea-actions[bot]@mokoconsulting.tech"
|
||||||
git config --local user.name "gitea-actions[bot]"
|
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 remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
|
||||||
- name: Check for merge conflict markers
|
- name: Check for merge conflict markers
|
||||||
@@ -382,7 +382,7 @@ jobs:
|
|||||||
content = open('CHANGELOG.md').read()
|
content = open('CHANGELOG.md').read()
|
||||||
old = '## [Unreleased]'
|
old = '## [Unreleased]'
|
||||||
new = f'## [Unreleased]\n\n## [{version}] --- {date}'
|
new = f'## [Unreleased]\n\n## [{version}] --- {date}'
|
||||||
content = content.replace(old, new, 1)
|
content = content if ('## [' + version + ']') in content else content.replace(old, new, 1)
|
||||||
open('CHANGELOG.md', 'w').write(content)
|
open('CHANGELOG.md', 'w').write(content)
|
||||||
" "$VERSION" "$DATE"
|
" "$VERSION" "$DATE"
|
||||||
git add CHANGELOG.md
|
git add CHANGELOG.md
|
||||||
@@ -426,7 +426,7 @@ jobs:
|
|||||||
&& echo "main branch pushed to GitHub mirror" \
|
&& echo "main branch pushed to GitHub mirror" \
|
||||||
|| echo "WARNING: GitHub mirror push failed"
|
|| 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'
|
if: steps.version.outputs.skip != 'true'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
@@ -438,17 +438,9 @@ jobs:
|
|||||||
"${API_BASE}/branches/rc" 2>/dev/null \
|
"${API_BASE}/branches/rc" 2>/dev/null \
|
||||||
&& echo "Deleted rc branch" || echo "rc branch not found"
|
&& echo "Deleted rc branch" || echo "rc branch not found"
|
||||||
|
|
||||||
# Delete dev branch
|
# dev is reset from main by the dedicated "Cascade Main -> Dev" workflow
|
||||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
# (cascade-dev.yml), which runs after this release completes.
|
||||||
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
|
echo "rc cleaned; dev reset handled by cascade-dev.yml" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
# Recreate dev from main (now includes version bump + changelog promotion)
|
|
||||||
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${API_BASE}/branches" \
|
|
||||||
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
|
|
||||||
|
|
||||||
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
- name: "Step 12: Create version branch from main"
|
- name: "Step 12: Create version branch from main"
|
||||||
if: steps.version.outputs.skip != 'true'
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: MokoStandards.Universal
|
# INGROUP: MokoStandards.Universal
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||||
# PATH: /.mokogitea/workflows/branch-cleanup.yml
|
# PATH: /.mokogitea/workflows/branch-cleanup.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Delete feature branches after PR merge
|
# BRIEF: Delete feature branches after PR merge
|
||||||
|
|||||||
@@ -1,10 +1,106 @@
|
|||||||
# DISABLED — auto-release Step 11 recreates dev from main after every release.
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
# Cascade-dev is redundant and causes version conflicts when both main and dev
|
#
|
||||||
# have different version numbers in templateDetails.xml / manifest.xml.
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
name: "Cascade Main → Dev (DISABLED)"
|
#
|
||||||
on: workflow_dispatch
|
# 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:
|
jobs:
|
||||||
noop:
|
cascade:
|
||||||
|
name: Cascade main -> dev
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
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,7 +3,7 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: MokoStandards.CI
|
# INGROUP: MokoStandards.CI
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||||
# PATH: /.mokogitea/workflows/ci-generic.yml
|
# PATH: /.mokogitea/workflows/ci-generic.yml
|
||||||
@@ -131,10 +131,11 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
name: Tests
|
name: Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: lint
|
# Independent job (no `needs: lint`): the Gitea Actions scheduler does not
|
||||||
# Run only when lint succeeded; always() forces evaluation so a skipped
|
# offer the dependent 2nd job of a needs-chain to runners, so it stalls in
|
||||||
# lint (e.g. template repos) skips this job cleanly instead of hanging.
|
# "waiting" and is reaped by ABANDONED_JOB_TIMEOUT. Guard template repos
|
||||||
if: ${{ always() && needs.lint.result == 'success' }}
|
# directly (same condition lint uses) instead of gating on lint's result.
|
||||||
|
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: mokocli.Universal
|
# 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
|
# PATH: /.mokogitea/workflows/ci-issue-reporter.yml
|
||||||
# VERSION: 01.00.00
|
# 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.
|
# Clones MokoCLI and runs cli/ci_issue_reporter.sh.
|
||||||
|
|
||||||
name: "Universal: CI Issue Reporter"
|
name: "Universal: CI Issue Reporter"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: MokoStandards.Maintenance
|
# INGROUP: MokoStandards.Maintenance
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||||
# PATH: /.mokogitea/workflows/cleanup.yml
|
# PATH: /.mokogitea/workflows/cleanup.yml
|
||||||
@@ -50,7 +50,7 @@ jobs:
|
|||||||
for BRANCH in $BRANCHES; do
|
for BRANCH in $BRANCHES; do
|
||||||
# Skip protected branches
|
# Skip protected branches
|
||||||
case "$BRANCH" in
|
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
|
esac
|
||||||
|
|
||||||
# Check if branch is merged into main
|
# Check if branch is merged into main
|
||||||
|
|||||||
@@ -52,51 +52,69 @@ jobs:
|
|||||||
REGISTRY_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
REGISTRY_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
TAG: ${{ steps.config.outputs.tag }}
|
TAG: ${{ steps.config.outputs.tag }}
|
||||||
run: |
|
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 }} \
|
ssh -i ~/.ssh/deploy_key -p ${{ env.DEPLOY_PORT }} \
|
||||||
-o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
-o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||||
-o ServerAliveInterval=30 -o ServerAliveCountMax=10 \
|
-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
|
set -e
|
||||||
echo 'SSH connected to dev environment'
|
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...'
|
echo 'Cleaning Docker build cache...'
|
||||||
docker builder prune -af 2>/dev/null || true
|
docker builder prune -af 2>/dev/null || true
|
||||||
docker image prune -af 2>/dev/null || true
|
docker image prune -af 2>/dev/null || true
|
||||||
|
|
||||||
echo 'Pulling source...'
|
echo 'Pulling source...'
|
||||||
SOURCE_DIR=/opt/gitea-dev/source
|
SOURCE_DIR=/opt/gitea-dev/source
|
||||||
if [ ! -d \$SOURCE_DIR/.git ]; then
|
if [ ! -d "$SOURCE_DIR/.git" ]; then
|
||||||
git clone -b dev https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-Fork.git \$SOURCE_DIR
|
git clone -b dev https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-Fork.git "$SOURCE_DIR"
|
||||||
fi
|
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 remote set-url origin https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-Fork.git 2>/dev/null || true
|
||||||
git fetch origin dev
|
git fetch origin dev
|
||||||
git reset --hard 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' \
|
docker build --no-cache --build-arg GOFLAGS='-p 1' \
|
||||||
--tag ${{ env.REGISTRY }}/${{ env.IMAGE }}:\$TAG \
|
--tag "${{ env.REGISTRY }}/${{ env.IMAGE }}:$TAG" \
|
||||||
-f Dockerfile .
|
-f Dockerfile .
|
||||||
|
|
||||||
echo 'Pushing to registry...'
|
echo 'Pushing to registry...'
|
||||||
echo '\$REGISTRY_TOKEN' | docker login ${{ env.REGISTRY }} -u ${{ env.DEPLOY_USER }} --password-stdin
|
echo "$REGISTRY_TOKEN" | docker login ${{ env.REGISTRY }} -u ${{ env.DEPLOY_USER }} --password-stdin
|
||||||
docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:\$TAG
|
docker push "${{ env.REGISTRY }}/${{ env.IMAGE }}:$TAG"
|
||||||
|
|
||||||
echo 'Restarting dev container...'
|
echo 'Restarting dev container...'
|
||||||
cd /opt/gitea-dev
|
cd /opt/gitea-dev
|
||||||
sed -i "s|${{ env.IMAGE }}:[^ ]*|${{ env.IMAGE }}:\$TAG|" docker-compose.yml
|
# The dev service in the SHARED compose file reads its image tag from
|
||||||
docker compose up -d mokogitea-dev
|
# ${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...'
|
echo 'Health check...'
|
||||||
for i in 1 2 3 4 5 6 7 8; do
|
for i in 1 2 3 4 5 6 7 8; do
|
||||||
sleep 15
|
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!'
|
echo 'Dev container healthy!'
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
echo "Waiting... (attempt \$i/8)"
|
echo "Waiting... (attempt $i/8)"
|
||||||
done
|
done
|
||||||
echo 'Health check failed'
|
echo 'Health check failed'
|
||||||
docker logs mokogitea-dev --tail 20
|
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
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: MokoStandards.Security
|
# INGROUP: MokoStandards.Security
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
|
||||||
# PATH: /templates/workflows/gitleaks.yml.template
|
# PATH: /.mokogitea/workflows/gitleaks.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: mokocli.Automation
|
# INGROUP: mokocli.Automation
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Auto-create feature branch when an issue is opened
|
# BRIEF: Auto-create feature branch when an issue is opened
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: MokoStandards.Notifications
|
# INGROUP: MokoStandards.Notifications
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||||
# PATH: /.mokogitea/workflows/notify.yml
|
# PATH: /.mokogitea/workflows/notify.yml
|
||||||
@@ -15,9 +15,9 @@ name: "Universal: Notifications"
|
|||||||
on:
|
on:
|
||||||
workflow_run:
|
workflow_run:
|
||||||
workflows:
|
workflows:
|
||||||
- "Joomla Build & Release"
|
- "Universal: Build & Release"
|
||||||
- "Joomla Extension CI"
|
- "Joomla: Extension CI"
|
||||||
- "Deploy"
|
- "Generic: Project CI"
|
||||||
types:
|
types:
|
||||||
- completed
|
- completed
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: mokocli.CI
|
# INGROUP: mokocli.CI
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||||
# PATH: /templates/workflows/universal/pr-check.yml.template
|
# PATH: /.mokogitea/workflows/pr-check.yml
|
||||||
# VERSION: 09.23.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: PR gate — branch policy + code validation before merge
|
# BRIEF: PR gate — branch policy + code validation before merge
|
||||||
|
|
||||||
@@ -97,6 +97,80 @@ jobs:
|
|||||||
echo "Branch policy: OK (${HEAD} → ${BASE})"
|
echo "Branch policy: OK (${HEAD} → ${BASE})"
|
||||||
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
|
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 ──────────────────────────────────────────────────
|
# ── Secret Scanning ──────────────────────────────────────────────────
|
||||||
gitleaks:
|
gitleaks:
|
||||||
name: Secret Scan
|
name: Secret Scan
|
||||||
@@ -136,7 +210,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Check for merge conflict markers
|
- name: Check for merge conflict markers
|
||||||
run: |
|
run: |
|
||||||
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --exclude-dir='.git' --exclude-dir='.mokogitea' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
||||||
if [ -n "$CONFLICTS" ]; then
|
if [ -n "$CONFLICTS" ]; then
|
||||||
echo "::error::Merge conflict markers found in source files"
|
echo "::error::Merge conflict markers found in source files"
|
||||||
echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
|
echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -276,7 +350,7 @@ jobs:
|
|||||||
joomla)
|
joomla)
|
||||||
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||||
if [ -z "$MANIFEST" ]; then
|
if [ -z "$MANIFEST" ]; then
|
||||||
echo "::warning::No Joomla manifest found (WaaS site)"
|
echo "::warning::No Joomla manifest found (MokoSuite site)"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
echo "Manifest: ${MANIFEST}"
|
echo "Manifest: ${MANIFEST}"
|
||||||
@@ -289,7 +363,7 @@ jobs:
|
|||||||
# Block legacy raw/branch update server URLs on MokoGitea
|
# Block legacy raw/branch update server URLs on MokoGitea
|
||||||
RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true)
|
RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true)
|
||||||
if [ -n "$RAW_URLS" ]; then
|
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"
|
echo "$RAW_URLS"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: mokocli.Release
|
# INGROUP: mokocli.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
# PATH: /.mokogitea/workflows/pre-release.yml
|
||||||
# VERSION: 05.02.00
|
# VERSION: 05.02.00
|
||||||
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
|
# 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
|
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
|
fi
|
||||||
rm -rf /tmp/mokocli
|
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
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
||||||
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
||||||
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
||||||
@@ -156,8 +156,8 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Commit version bump
|
# Commit version bump
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
git config --local user.email "mokogitea-actions[bot]@mokoconsulting.tech"
|
||||||
git config --local user.name "gitea-actions[bot]"
|
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 remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
git add -A
|
git add -A
|
||||||
git diff --cached --quiet || {
|
git diff --cached --quiet || {
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: mokocli.Universal
|
# INGROUP: mokocli.Universal
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||||
# PATH: /.mokogitea/workflows/rc-revert.yml
|
# PATH: /.mokogitea/workflows/rc-revert.yml
|
||||||
# VERSION: 09.23.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
|
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
|
||||||
@@ -25,7 +25,8 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: >-
|
if: >-
|
||||||
github.event.pull_request.merged == false &&
|
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:
|
steps:
|
||||||
- name: Rename branch
|
- name: Rename branch
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: mokocli.Validation
|
# INGROUP: mokocli.Validation
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||||
# PATH: /templates/workflows/joomla/repo_health.yml.template
|
# PATH: /.mokogitea/workflows/repo-health.yml
|
||||||
# VERSION: 09.23.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts.
|
# 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
|
# Hardcoded authorized users — always allowed
|
||||||
case "$ACTOR" in
|
case "$ACTOR" in
|
||||||
jmiller|gitea-actions[bot])
|
jmiller|mokogitea-actions[bot])
|
||||||
ALLOWED=true
|
ALLOWED=true
|
||||||
PERMISSION=admin
|
PERMISSION=admin
|
||||||
METHOD="hardcoded allowlist"
|
METHOD="hardcoded allowlist"
|
||||||
|
|||||||
@@ -10,11 +10,12 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
sync:
|
sync:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ startsWith(github.event.repository.name, 'Template-') }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout mokocli
|
- name: Checkout mokocli
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: MokoConsulting/mokocli
|
repository: MokoConsulting/MokoCLI
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
|
||||||
- name: Setup PHP
|
- name: Setup PHP
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow.Template
|
# DEFGROUP: MokoGitea.Workflow.Template
|
||||||
# INGROUP: MokoStandards.CI
|
# INGROUP: MokoStandards.CI
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
|
||||||
# PATH: /.mokogitea/workflows/version-set.yml
|
# PATH: /.mokogitea/workflows/version-set.yml
|
||||||
@@ -34,6 +34,7 @@ jobs:
|
|||||||
set-version:
|
set-version:
|
||||||
name: Set Version to ${{ inputs.version }}
|
name: Set Version to ${{ inputs.version }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Validate version format
|
- name: Validate version format
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: MokoGitea.Workflow
|
||||||
# INGROUP: mokocli.Universal
|
# 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
|
# PATH: /.mokogitea/workflows/workflow-sync-trigger.yml
|
||||||
# VERSION: 01.01.00
|
# VERSION: 01.01.00
|
||||||
# BRIEF: Trigger workflow sync to live repos when a PR is merged to main
|
# 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
|
name: Sync workflows to live repos
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: >-
|
if: >-
|
||||||
github.event_name == 'workflow_dispatch' ||
|
startsWith(github.event.repository.name, 'Template-') &&
|
||||||
|
(github.event_name == 'workflow_dispatch' ||
|
||||||
(github.event.pull_request.merged == true &&
|
(github.event.pull_request.merged == true &&
|
||||||
!contains(github.event.pull_request.title, '[skip sync]'))
|
!contains(github.event.pull_request.title, '[skip sync]')))
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Determine platform from repo name
|
- name: Determine platform from repo name
|
||||||
@@ -52,7 +53,7 @@ jobs:
|
|||||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
|
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
|
- name: Install PHP
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -63,6 +63,10 @@
|
|||||||
- Cherry-pick upstream v1.26.4: walk git log context error handling — regression fix (#38185)
|
- Cherry-pick upstream v1.26.4: walk git log context error handling — regression fix (#38185)
|
||||||
|
|
||||||
### Fixed
|
### 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-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)
|
- 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)
|
- 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
|
# 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
|
- **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
|
- **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)
|
- **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
|
- **Project Board API** -- REST endpoints for project columns and cards
|
||||||
- **CI Infrastructure** -- reusable workflows, centralized ci-issue-reporter, standardized MOKOGITEA_TOKEN naming
|
- **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
|
- **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"`
|
CanPush bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
EnableWhitelist bool `xorm:"NOT NULL DEFAULT false"`
|
EnableWhitelist bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
WhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
|
WhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
|
||||||
|
WhitelistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||||
|
WhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
EnableMergeWhitelist bool `xorm:"NOT NULL DEFAULT false"`
|
EnableMergeWhitelist bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
|
MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
|
||||||
|
MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||||
|
MergeWhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
CanForcePush bool `xorm:"NOT NULL DEFAULT false"`
|
CanForcePush bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
EnableForcePushAllowlist bool `xorm:"NOT NULL DEFAULT false"`
|
EnableForcePushAllowlist bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
ForcePushAllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
|
ForcePushAllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
|
||||||
|
ForcePushAllowlistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||||
|
ForcePushAllowlistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
CanDelete bool `xorm:"NOT NULL DEFAULT false"`
|
CanDelete bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
EnableDeleteAllowlist bool `xorm:"NOT NULL DEFAULT false"`
|
EnableDeleteAllowlist bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
DeleteAllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
|
DeleteAllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
|
||||||
|
DeleteAllowlistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||||
|
DeleteAllowlistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"`
|
EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
StatusCheckContexts []string `xorm:"JSON TEXT"`
|
StatusCheckContexts []string `xorm:"JSON TEXT"`
|
||||||
RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"`
|
RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||||
EnableApprovalsWhitelist bool `xorm:"NOT NULL DEFAULT false"`
|
EnableApprovalsWhitelist bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
ApprovalsWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
|
ApprovalsWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
|
||||||
|
ApprovalsWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||||
BlockOnRejectedReviews bool `xorm:"NOT NULL DEFAULT false"`
|
BlockOnRejectedReviews bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
BlockOnOfficialReviewRequests bool `xorm:"NOT NULL DEFAULT false"`
|
BlockOnOfficialReviewRequests bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
BlockOnOutdatedBranch 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
|
// ToProtectedBranch converts an org-level rule to a ProtectedBranch for use
|
||||||
// in the standard enforcement path. Fields that are user-scoped (WhitelistUserIDs etc.)
|
// in the standard enforcement path. Both team-scoped and user-scoped allowlists
|
||||||
// are left empty because org rules only reference teams.
|
// (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 {
|
func (o *OrgProtectedBranch) ToProtectedBranch() *ProtectedBranch {
|
||||||
return &ProtectedBranch{
|
return &ProtectedBranch{
|
||||||
ID: o.ID,
|
ID: o.ID,
|
||||||
@@ -94,19 +104,28 @@ func (o *OrgProtectedBranch) ToProtectedBranch() *ProtectedBranch {
|
|||||||
CanPush: o.CanPush,
|
CanPush: o.CanPush,
|
||||||
EnableWhitelist: o.EnableWhitelist,
|
EnableWhitelist: o.EnableWhitelist,
|
||||||
WhitelistTeamIDs: o.WhitelistTeamIDs,
|
WhitelistTeamIDs: o.WhitelistTeamIDs,
|
||||||
|
WhitelistUserIDs: o.WhitelistUserIDs,
|
||||||
|
WhitelistActionsUser: o.WhitelistActionsUser,
|
||||||
EnableMergeWhitelist: o.EnableMergeWhitelist,
|
EnableMergeWhitelist: o.EnableMergeWhitelist,
|
||||||
MergeWhitelistTeamIDs: o.MergeWhitelistTeamIDs,
|
MergeWhitelistTeamIDs: o.MergeWhitelistTeamIDs,
|
||||||
|
MergeWhitelistUserIDs: o.MergeWhitelistUserIDs,
|
||||||
|
MergeWhitelistActionsUser: o.MergeWhitelistActionsUser,
|
||||||
CanForcePush: o.CanForcePush,
|
CanForcePush: o.CanForcePush,
|
||||||
EnableForcePushAllowlist: o.EnableForcePushAllowlist,
|
EnableForcePushAllowlist: o.EnableForcePushAllowlist,
|
||||||
ForcePushAllowlistTeamIDs: o.ForcePushAllowlistTeamIDs,
|
ForcePushAllowlistTeamIDs: o.ForcePushAllowlistTeamIDs,
|
||||||
|
ForcePushAllowlistUserIDs: o.ForcePushAllowlistUserIDs,
|
||||||
|
ForcePushAllowlistActionsUser: o.ForcePushAllowlistActionsUser,
|
||||||
CanDelete: o.CanDelete,
|
CanDelete: o.CanDelete,
|
||||||
EnableDeleteAllowlist: o.EnableDeleteAllowlist,
|
EnableDeleteAllowlist: o.EnableDeleteAllowlist,
|
||||||
DeleteAllowlistTeamIDs: o.DeleteAllowlistTeamIDs,
|
DeleteAllowlistTeamIDs: o.DeleteAllowlistTeamIDs,
|
||||||
|
DeleteAllowlistUserIDs: o.DeleteAllowlistUserIDs,
|
||||||
|
DeleteAllowlistActionsUser: o.DeleteAllowlistActionsUser,
|
||||||
EnableStatusCheck: o.EnableStatusCheck,
|
EnableStatusCheck: o.EnableStatusCheck,
|
||||||
StatusCheckContexts: o.StatusCheckContexts,
|
StatusCheckContexts: o.StatusCheckContexts,
|
||||||
RequiredApprovals: o.RequiredApprovals,
|
RequiredApprovals: o.RequiredApprovals,
|
||||||
EnableApprovalsWhitelist: o.EnableApprovalsWhitelist,
|
EnableApprovalsWhitelist: o.EnableApprovalsWhitelist,
|
||||||
ApprovalsWhitelistTeamIDs: o.ApprovalsWhitelistTeamIDs,
|
ApprovalsWhitelistTeamIDs: o.ApprovalsWhitelistTeamIDs,
|
||||||
|
ApprovalsWhitelistUserIDs: o.ApprovalsWhitelistUserIDs,
|
||||||
BlockOnRejectedReviews: o.BlockOnRejectedReviews,
|
BlockOnRejectedReviews: o.BlockOnRejectedReviews,
|
||||||
BlockOnOfficialReviewRequests: o.BlockOnOfficialReviewRequests,
|
BlockOnOfficialReviewRequests: o.BlockOnOfficialReviewRequests,
|
||||||
BlockOnOutdatedBranch: o.BlockOnOutdatedBranch,
|
BlockOnOutdatedBranch: o.BlockOnOutdatedBranch,
|
||||||
|
|||||||
@@ -133,8 +133,10 @@ func getFirstMatchOrgProtectedBranchRule(ctx context.Context, repoID int64, bran
|
|||||||
|
|
||||||
orgRule, err := FindOrgBranchRuleForBranch(ctx, owner.ID, branchName)
|
orgRule, err := FindOrgBranchRuleForBranch(ctx, owner.ID, branchName)
|
||||||
if err != nil {
|
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)
|
log.Error("FindOrgBranchRuleForBranch: %v", err)
|
||||||
return nil, nil
|
return nil, err
|
||||||
}
|
}
|
||||||
if orgRule == nil {
|
if orgRule == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|||||||
@@ -26,27 +26,32 @@ func mergeMostRestrictive(repoRule, orgRule *ProtectedBranch) *ProtectedBranch {
|
|||||||
eff.CanPush = repoRule.CanPush && orgRule.CanPush
|
eff.CanPush = repoRule.CanPush && orgRule.CanPush
|
||||||
eff.EnableWhitelist, eff.WhitelistUserIDs = mergeAllowlist(repoRule.EnableWhitelist, repoRule.WhitelistUserIDs, orgRule.EnableWhitelist, orgRule.WhitelistUserIDs)
|
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.WhitelistTeamIDs = mergeAllowlist(repoRule.EnableWhitelist, repoRule.WhitelistTeamIDs, orgRule.EnableWhitelist, orgRule.WhitelistTeamIDs)
|
||||||
eff.WhitelistDeployKeys = repoRule.WhitelistDeployKeys && orgRule.WhitelistDeployKeys
|
// Deploy-key allowances are still not expressible in an OrgProtectedBranch, so the
|
||||||
eff.WhitelistActionsUser = repoRule.WhitelistActionsUser && orgRule.WhitelistActionsUser
|
// 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.
|
// Force push.
|
||||||
eff.CanForcePush = repoRule.CanForcePush && orgRule.CanForcePush
|
eff.CanForcePush = repoRule.CanForcePush && orgRule.CanForcePush
|
||||||
eff.EnableForcePushAllowlist, eff.ForcePushAllowlistUserIDs = mergeAllowlist(repoRule.EnableForcePushAllowlist, repoRule.ForcePushAllowlistUserIDs, orgRule.EnableForcePushAllowlist, orgRule.ForcePushAllowlistUserIDs)
|
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.ForcePushAllowlistTeamIDs = mergeAllowlist(repoRule.EnableForcePushAllowlist, repoRule.ForcePushAllowlistTeamIDs, orgRule.EnableForcePushAllowlist, orgRule.ForcePushAllowlistTeamIDs)
|
||||||
eff.ForcePushAllowlistDeployKeys = repoRule.ForcePushAllowlistDeployKeys && orgRule.ForcePushAllowlistDeployKeys
|
eff.ForcePushAllowlistDeployKeys = repoRule.ForcePushAllowlistDeployKeys
|
||||||
eff.ForcePushAllowlistActionsUser = repoRule.ForcePushAllowlistActionsUser && orgRule.ForcePushAllowlistActionsUser
|
eff.ForcePushAllowlistActionsUser = mergeAllowFlag(repoRule.EnableForcePushAllowlist, repoRule.ForcePushAllowlistActionsUser, orgRule.EnableForcePushAllowlist, orgRule.ForcePushAllowlistActionsUser)
|
||||||
|
|
||||||
// Delete.
|
// Delete.
|
||||||
eff.CanDelete = repoRule.CanDelete && orgRule.CanDelete
|
eff.CanDelete = repoRule.CanDelete && orgRule.CanDelete
|
||||||
eff.EnableDeleteAllowlist, eff.DeleteAllowlistUserIDs = mergeAllowlist(repoRule.EnableDeleteAllowlist, repoRule.DeleteAllowlistUserIDs, orgRule.EnableDeleteAllowlist, orgRule.DeleteAllowlistUserIDs)
|
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.DeleteAllowlistTeamIDs = mergeAllowlist(repoRule.EnableDeleteAllowlist, repoRule.DeleteAllowlistTeamIDs, orgRule.EnableDeleteAllowlist, orgRule.DeleteAllowlistTeamIDs)
|
||||||
eff.DeleteAllowlistDeployKeys = repoRule.DeleteAllowlistDeployKeys && orgRule.DeleteAllowlistDeployKeys
|
eff.DeleteAllowlistDeployKeys = repoRule.DeleteAllowlistDeployKeys
|
||||||
eff.DeleteAllowlistActionsUser = repoRule.DeleteAllowlistActionsUser && orgRule.DeleteAllowlistActionsUser
|
eff.DeleteAllowlistActionsUser = mergeAllowFlag(repoRule.EnableDeleteAllowlist, repoRule.DeleteAllowlistActionsUser, orgRule.EnableDeleteAllowlist, orgRule.DeleteAllowlistActionsUser)
|
||||||
|
|
||||||
// Merge whitelist.
|
// Merge whitelist.
|
||||||
eff.EnableMergeWhitelist, eff.MergeWhitelistUserIDs = mergeAllowlist(repoRule.EnableMergeWhitelist, repoRule.MergeWhitelistUserIDs, orgRule.EnableMergeWhitelist, orgRule.MergeWhitelistUserIDs)
|
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.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.
|
// Status checks.
|
||||||
eff.EnableStatusCheck = repoRule.EnableStatusCheck || orgRule.EnableStatusCheck
|
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 {
|
func intersectInt64(a, b []int64) []int64 {
|
||||||
if len(a) == 0 || len(b) == 0 {
|
if len(a) == 0 || len(b) == 0 {
|
||||||
return nil
|
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(364, "Add org push policy table", v1_27.AddOrgPushPolicyTable),
|
||||||
newMigration(365, "Add org repo defaults table", v1_27.AddOrgRepoDefaultsTable),
|
newMigration(365, "Add org repo defaults table", v1_27.AddOrgRepoDefaultsTable),
|
||||||
newMigration(366, "Add org email domain policy table", v1_27.AddOrgEmailDomainPolicyTable),
|
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
|
return preparedMigrations
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// AddRepoManifestTable creates the repo_manifest table for storing
|
// AddRepoManifestTable creates the repo_manifest table for storing
|
||||||
// mokoplatform manifest settings per repository.
|
// mokocli manifest settings per repository.
|
||||||
func AddRepoManifestTable(x *xorm.Engine) error {
|
func AddRepoManifestTable(x *xorm.Engine) error {
|
||||||
type RepoManifest struct {
|
type RepoManifest struct {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
@@ -15,50 +15,50 @@ func init() {
|
|||||||
db.RegisterModel(new(RepoMetadata))
|
db.RegisterModel(new(RepoMetadata))
|
||||||
}
|
}
|
||||||
|
|
||||||
// RepoMetadata stores mokoplatform metadata settings for a repository.
|
// RepoMetadata stores mokocli metadata settings for a repository.
|
||||||
// These fields correspond to the .mokogitea/manifest.xml schema and are
|
// These first-class fields are the authoritative repository metadata,
|
||||||
// exposed via API for use by Actions workflows and the mokoplatform CLI.
|
// exposed via API for use by Actions workflows and mokocli.
|
||||||
type RepoMetadata struct {
|
type RepoMetadata struct {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
RepoID int64 `xorm:"UNIQUE INDEX NOT NULL 'repo_id'"`
|
RepoID int64 `xorm:"UNIQUE INDEX NOT NULL 'repo_id'"`
|
||||||
|
|
||||||
// identity section
|
// identity section
|
||||||
Name string `xorm:"TEXT 'name'"` // project name
|
Name string `xorm:"TEXT 'name'"` // project name
|
||||||
Org string `xorm:"TEXT 'org'"` // organization name
|
Org string `xorm:"TEXT 'org'"` // organization name
|
||||||
Description string `xorm:"TEXT 'description'"` // project description
|
Description string `xorm:"TEXT 'description'"` // project description
|
||||||
LicenseSPDX string `xorm:"VARCHAR(50) 'license_spdx'"` // SPDX identifier, e.g. "GPL-3.0-or-later"
|
LicenseSPDX string `xorm:"VARCHAR(50) 'license_spdx'"` // SPDX identifier, e.g. "GPL-3.0-or-later"
|
||||||
LicenseName string `xorm:"TEXT 'license_name'"` // human-readable license name
|
LicenseName string `xorm:"TEXT 'license_name'"` // human-readable license name
|
||||||
|
|
||||||
// governance section
|
// governance section
|
||||||
Platform string `xorm:"VARCHAR(50) 'platform'"` // go, php, node, python, etc.
|
Platform string `xorm:"VARCHAR(50) 'platform'"` // go, php, node, python, etc.
|
||||||
StandardsVersion string `xorm:"VARCHAR(20) 'standards_version'"` // mokoplatform standards version
|
StandardsVersion string `xorm:"VARCHAR(20) 'standards_version'"` // mokocli standards version
|
||||||
StandardsSource string `xorm:"TEXT 'standards_source'"` // URL to standards repo
|
StandardsSource string `xorm:"TEXT 'standards_source'"` // URL to standards repo
|
||||||
|
|
||||||
// versioning
|
// versioning
|
||||||
VersionPrefix string `xorm:"TEXT 'version_prefix'"` // tag prefix stripped for version display, e.g. "v1.26.1-moko."
|
VersionPrefix string `xorm:"TEXT 'version_prefix'"` // tag prefix stripped for version display, e.g. "v1.26.1-moko."
|
||||||
ElementName string `xorm:"TEXT 'element_name'"` // full element name override, e.g. "pkg_mokowaas" (auto-constructed if empty)
|
ElementName string `xorm:"TEXT 'element_name'"` // full element name override, e.g. "pkg_mokowaas" (auto-constructed if empty)
|
||||||
|
|
||||||
// distribution metadata (used by update server feed generation)
|
// distribution metadata (used by update server feed generation)
|
||||||
Maintainer string `xorm:"TEXT 'maintainer'"` // maintainer/author name
|
Maintainer string `xorm:"TEXT 'maintainer'"` // maintainer/author name
|
||||||
MaintainerURL string `xorm:"TEXT 'maintainer_url'"` // maintainer website
|
MaintainerURL string `xorm:"TEXT 'maintainer_url'"` // maintainer website
|
||||||
InfoURL string `xorm:"TEXT 'info_url'"` // extension info/product page URL
|
InfoURL string `xorm:"TEXT 'info_url'"` // extension info/product page URL
|
||||||
TargetVersion string `xorm:"TEXT 'target_version'"` // target platform version regex, e.g. "(5|6)\..*"
|
TargetVersion string `xorm:"TEXT 'target_version'"` // target platform version regex, e.g. "(5|6)\..*"
|
||||||
PHPMinimum string `xorm:"VARCHAR(20) 'php_minimum'"` // minimum PHP version, e.g. "8.1"
|
PHPMinimum string `xorm:"VARCHAR(20) 'php_minimum'"` // minimum PHP version, e.g. "8.1"
|
||||||
|
|
||||||
// build section
|
// build section
|
||||||
Language string `xorm:"VARCHAR(50) 'language'"` // Go, PHP, TypeScript, etc.
|
Language string `xorm:"VARCHAR(50) 'language'"` // Go, PHP, TypeScript, etc.
|
||||||
ExtensionType string `xorm:"VARCHAR(50) 'extension_type'"` // component, module, plugin, package, template, library, file
|
ExtensionType string `xorm:"VARCHAR(50) 'extension_type'"` // component, module, plugin, package, template, library, file
|
||||||
EntryPoint string `xorm:"TEXT 'entry_point'"` // build entry point path
|
EntryPoint string `xorm:"TEXT 'entry_point'"` // build entry point path
|
||||||
|
|
||||||
// deploy section
|
// deploy section
|
||||||
DeployHost string `xorm:"VARCHAR(255) 'deploy_host'"` // SSH host for deploy
|
DeployHost string `xorm:"VARCHAR(255) 'deploy_host'"` // SSH host for deploy
|
||||||
DeployPort string `xorm:"VARCHAR(10) 'deploy_port'"` // SSH port (default 2918)
|
DeployPort string `xorm:"VARCHAR(10) 'deploy_port'"` // SSH port (default 2918)
|
||||||
DeployUser string `xorm:"VARCHAR(100) 'deploy_user'"` // SSH user
|
DeployUser string `xorm:"VARCHAR(100) 'deploy_user'"` // SSH user
|
||||||
DeployPath string `xorm:"TEXT 'deploy_path'"` // remote path for source/compose
|
DeployPath string `xorm:"TEXT 'deploy_path'"` // remote path for source/compose
|
||||||
DockerImage string `xorm:"VARCHAR(255) 'docker_image'"` // e.g. mokoconsulting/mokogitea
|
DockerImage string `xorm:"VARCHAR(255) 'docker_image'"` // e.g. mokoconsulting/mokogitea
|
||||||
DockerRegistry string `xorm:"VARCHAR(255) 'docker_registry'"` // e.g. git.mokoconsulting.tech
|
DockerRegistry string `xorm:"VARCHAR(255) 'docker_registry'"` // e.g. git.mokoconsulting.tech
|
||||||
ContainerName string `xorm:"VARCHAR(100) 'container_name'"` // Docker container name
|
ContainerName string `xorm:"VARCHAR(100) 'container_name'"` // Docker container name
|
||||||
HealthURL string `xorm:"TEXT 'health_url'"` // health check URL after deploy
|
HealthURL string `xorm:"TEXT 'health_url'"` // health check URL after deploy
|
||||||
|
|
||||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
|
||||||
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"`
|
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"`
|
||||||
|
|||||||
@@ -14,19 +14,28 @@ type OrgBranchProtection struct {
|
|||||||
EnablePush bool `json:"enable_push"`
|
EnablePush bool `json:"enable_push"`
|
||||||
EnablePushWhitelist bool `json:"enable_push_whitelist"`
|
EnablePushWhitelist bool `json:"enable_push_whitelist"`
|
||||||
PushWhitelistTeams []string `json:"push_whitelist_teams"`
|
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"`
|
EnableForcePush bool `json:"enable_force_push"`
|
||||||
EnableForcePushAllowlist bool `json:"enable_force_push_allowlist"`
|
EnableForcePushAllowlist bool `json:"enable_force_push_allowlist"`
|
||||||
ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"`
|
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"`
|
EnableDelete bool `json:"enable_delete"`
|
||||||
EnableDeleteAllowlist bool `json:"enable_delete_allowlist"`
|
EnableDeleteAllowlist bool `json:"enable_delete_allowlist"`
|
||||||
DeleteAllowlistTeams []string `json:"delete_allowlist_teams"`
|
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"`
|
EnableMergeWhitelist bool `json:"enable_merge_whitelist"`
|
||||||
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
|
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"`
|
EnableStatusCheck bool `json:"enable_status_check"`
|
||||||
StatusCheckContexts []string `json:"status_check_contexts"`
|
StatusCheckContexts []string `json:"status_check_contexts"`
|
||||||
RequiredApprovals int64 `json:"required_approvals"`
|
RequiredApprovals int64 `json:"required_approvals"`
|
||||||
EnableApprovalsWhitelist bool `json:"enable_approvals_whitelist"`
|
EnableApprovalsWhitelist bool `json:"enable_approvals_whitelist"`
|
||||||
ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams"`
|
ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams"`
|
||||||
|
ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username"`
|
||||||
BlockOnRejectedReviews bool `json:"block_on_rejected_reviews"`
|
BlockOnRejectedReviews bool `json:"block_on_rejected_reviews"`
|
||||||
BlockOnOfficialReviewRequests bool `json:"block_on_official_review_requests"`
|
BlockOnOfficialReviewRequests bool `json:"block_on_official_review_requests"`
|
||||||
BlockOnOutdatedBranch bool `json:"block_on_outdated_branch"`
|
BlockOnOutdatedBranch bool `json:"block_on_outdated_branch"`
|
||||||
@@ -49,19 +58,28 @@ type CreateOrgBranchProtectionOption struct {
|
|||||||
EnablePush bool `json:"enable_push"`
|
EnablePush bool `json:"enable_push"`
|
||||||
EnablePushWhitelist bool `json:"enable_push_whitelist"`
|
EnablePushWhitelist bool `json:"enable_push_whitelist"`
|
||||||
PushWhitelistTeams []string `json:"push_whitelist_teams"`
|
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"`
|
EnableForcePush bool `json:"enable_force_push"`
|
||||||
EnableForcePushAllowlist bool `json:"enable_force_push_allowlist"`
|
EnableForcePushAllowlist bool `json:"enable_force_push_allowlist"`
|
||||||
ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"`
|
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"`
|
EnableDelete bool `json:"enable_delete"`
|
||||||
EnableDeleteAllowlist bool `json:"enable_delete_allowlist"`
|
EnableDeleteAllowlist bool `json:"enable_delete_allowlist"`
|
||||||
DeleteAllowlistTeams []string `json:"delete_allowlist_teams"`
|
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"`
|
EnableMergeWhitelist bool `json:"enable_merge_whitelist"`
|
||||||
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
|
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"`
|
EnableStatusCheck bool `json:"enable_status_check"`
|
||||||
StatusCheckContexts []string `json:"status_check_contexts"`
|
StatusCheckContexts []string `json:"status_check_contexts"`
|
||||||
RequiredApprovals int64 `json:"required_approvals"`
|
RequiredApprovals int64 `json:"required_approvals"`
|
||||||
EnableApprovalsWhitelist bool `json:"enable_approvals_whitelist"`
|
EnableApprovalsWhitelist bool `json:"enable_approvals_whitelist"`
|
||||||
ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams"`
|
ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams"`
|
||||||
|
ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username"`
|
||||||
BlockOnRejectedReviews bool `json:"block_on_rejected_reviews"`
|
BlockOnRejectedReviews bool `json:"block_on_rejected_reviews"`
|
||||||
BlockOnOfficialReviewRequests bool `json:"block_on_official_review_requests"`
|
BlockOnOfficialReviewRequests bool `json:"block_on_official_review_requests"`
|
||||||
BlockOnOutdatedBranch bool `json:"block_on_outdated_branch"`
|
BlockOnOutdatedBranch bool `json:"block_on_outdated_branch"`
|
||||||
@@ -79,19 +97,28 @@ type EditOrgBranchProtectionOption struct {
|
|||||||
EnablePush *bool `json:"enable_push"`
|
EnablePush *bool `json:"enable_push"`
|
||||||
EnablePushWhitelist *bool `json:"enable_push_whitelist"`
|
EnablePushWhitelist *bool `json:"enable_push_whitelist"`
|
||||||
PushWhitelistTeams []string `json:"push_whitelist_teams"`
|
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"`
|
EnableForcePush *bool `json:"enable_force_push"`
|
||||||
EnableForcePushAllowlist *bool `json:"enable_force_push_allowlist"`
|
EnableForcePushAllowlist *bool `json:"enable_force_push_allowlist"`
|
||||||
ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"`
|
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"`
|
EnableDelete *bool `json:"enable_delete"`
|
||||||
EnableDeleteAllowlist *bool `json:"enable_delete_allowlist"`
|
EnableDeleteAllowlist *bool `json:"enable_delete_allowlist"`
|
||||||
DeleteAllowlistTeams []string `json:"delete_allowlist_teams"`
|
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"`
|
EnableMergeWhitelist *bool `json:"enable_merge_whitelist"`
|
||||||
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
|
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"`
|
EnableStatusCheck *bool `json:"enable_status_check"`
|
||||||
StatusCheckContexts []string `json:"status_check_contexts"`
|
StatusCheckContexts []string `json:"status_check_contexts"`
|
||||||
RequiredApprovals *int64 `json:"required_approvals"`
|
RequiredApprovals *int64 `json:"required_approvals"`
|
||||||
EnableApprovalsWhitelist *bool `json:"enable_approvals_whitelist"`
|
EnableApprovalsWhitelist *bool `json:"enable_approvals_whitelist"`
|
||||||
ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams"`
|
ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams"`
|
||||||
|
ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username"`
|
||||||
BlockOnRejectedReviews *bool `json:"block_on_rejected_reviews"`
|
BlockOnRejectedReviews *bool `json:"block_on_rejected_reviews"`
|
||||||
BlockOnOfficialReviewRequests *bool `json:"block_on_official_review_requests"`
|
BlockOnOfficialReviewRequests *bool `json:"block_on_official_review_requests"`
|
||||||
BlockOnOutdatedBranch *bool `json:"block_on_outdated_branch"`
|
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) {
|
func Test_RandomInt(t *testing.T) {
|
||||||
randInt := CryptoRandomInt(255)
|
randInt, err := CryptoRandomInt(255)
|
||||||
|
assert.NoError(t, err)
|
||||||
assert.GreaterOrEqual(t, randInt, int64(0))
|
assert.GreaterOrEqual(t, randInt, int64(0))
|
||||||
assert.LessOrEqual(t, randInt, int64(255))
|
assert.LessOrEqual(t, randInt, int64(255))
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_RandomString(t *testing.T) {
|
func Test_RandomString(t *testing.T) {
|
||||||
str1 := CryptoRandomString(32)
|
str1, err := CryptoRandomString(32)
|
||||||
var err error
|
assert.NoError(t, err)
|
||||||
matches, err := regexp.MatchString(`^[a-zA-Z0-9]{32}$`, str1)
|
matches, err := regexp.MatchString(`^[a-zA-Z0-9]{32}$`, str1)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.True(t, matches)
|
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)
|
matches, err = regexp.MatchString(`^[a-zA-Z0-9]{32}$`, str1)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.True(t, matches)
|
assert.True(t, matches)
|
||||||
|
|
||||||
assert.NotEqual(t, str1, str2)
|
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)
|
matches, err = regexp.MatchString(`^[a-zA-Z0-9]{256}$`, str3)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.True(t, matches)
|
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)
|
matches, err = regexp.MatchString(`^[a-zA-Z0-9]{256}$`, str4)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.True(t, matches)
|
assert.True(t, matches)
|
||||||
@@ -119,15 +123,19 @@ func Test_RandomString(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Test_RandomBytes(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)
|
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)
|
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_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_deploy_keys": "Allowlist deploy keys with push access to force push.",
|
||||||
"repo.settings.protect_force_push_allowlist_actions_user": "Allowlist actions bot user to force push.",
|
"repo.settings.protect_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": "Disable Deletion",
|
||||||
"repo.settings.protect_disable_delete_desc": "This branch cannot be deleted.",
|
"repo.settings.protect_disable_delete_desc": "This branch cannot be deleted.",
|
||||||
"repo.settings.protect_enable_delete_all": "Enable Deletion",
|
"repo.settings.protect_enable_delete_all": "Enable Deletion",
|
||||||
@@ -2772,7 +2772,7 @@
|
|||||||
"repo.settings.support_url_help": "Shown when downloads are gated. Can point to your wiki, product page, or external support site.",
|
"repo.settings.support_url_help": "Shown when downloads are gated. Can point to your wiki, product page, or external support site.",
|
||||||
"repo.settings.custom_fields": "Custom Fields",
|
"repo.settings.custom_fields": "Custom Fields",
|
||||||
"repo.settings.manifest": "Manifest",
|
"repo.settings.manifest": "Manifest",
|
||||||
"repo.settings.manifest_desc": "Project identity, governance, and build settings from the MokoPlatform manifest. These are accessible via API for Actions workflows and the MokoPlatform CLI.",
|
"repo.settings.manifest_desc": "Project identity, governance, and build settings for the repository. These are accessible via API for Actions workflows and MokoCLI.",
|
||||||
"repo.settings.manifest_identity": "Identity",
|
"repo.settings.manifest_identity": "Identity",
|
||||||
"repo.settings.manifest_name": "Project Name",
|
"repo.settings.manifest_name": "Project Name",
|
||||||
"repo.settings.manifest_element_name": "Element Name",
|
"repo.settings.manifest_element_name": "Element Name",
|
||||||
|
|||||||
+24
-26
@@ -516,7 +516,7 @@ func reqOrgVisible() func(ctx *context.APIContext) {
|
|||||||
ctx.APIErrorInternal(errors.New("reqOrgVisible: unprepared context"))
|
ctx.APIErrorInternal(errors.New("reqOrgVisible: unprepared context"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !organization.HasOrgOrUserVisible(ctx, ctx.Org.Organization.AsUser(), ctx.Doer) {
|
if !organization.IsOwnerVisibleToDoer(ctx, ctx.Org.Organization.AsUser(), ctx.Doer) {
|
||||||
ctx.APIErrorNotFound()
|
ctx.APIErrorNotFound()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1543,8 +1543,6 @@ func Routes() *web.Router {
|
|||||||
}, reqAnyRepoReader())
|
}, reqAnyRepoReader())
|
||||||
m.Get("/metadata", repo.GetRepoMetadata)
|
m.Get("/metadata", repo.GetRepoMetadata)
|
||||||
m.Put("/metadata", reqToken(), reqAdmin(), repo.UpdateRepoMetadata)
|
m.Put("/metadata", reqToken(), reqAdmin(), repo.UpdateRepoMetadata)
|
||||||
m.Get("/manifest", repo.GetRepoMetadata) // backward compat
|
|
||||||
m.Put("/manifest", reqToken(), reqAdmin(), repo.UpdateRepoMetadata)
|
|
||||||
// MokoGitea badge engine
|
// MokoGitea badge engine
|
||||||
m.Get("/badge/{type}.svg", repo.GetRepoBadge)
|
m.Get("/badge/{type}.svg", repo.GetRepoBadge)
|
||||||
m.Get("/issue_templates", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(), repo.GetIssueTemplates)
|
m.Get("/issue_templates", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(), repo.GetIssueTemplates)
|
||||||
@@ -1698,29 +1696,29 @@ func Routes() *web.Router {
|
|||||||
Patch(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditMilestoneOption{}), repo.EditMilestone).
|
Patch(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditMilestoneOption{}), repo.EditMilestone).
|
||||||
Delete(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteMilestone)
|
Delete(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteMilestone)
|
||||||
})
|
})
|
||||||
// m.Group("/projects", func() {
|
// m.Group("/projects", func() {
|
||||||
// m.Combo("").Get(repo.ListProjects).
|
// m.Combo("").Get(repo.ListProjects).
|
||||||
// Post(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.CreateProjectOption{}), repo.CreateProject)
|
// Post(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.CreateProjectOption{}), repo.CreateProject)
|
||||||
// m.Group("/{id}", func() {
|
// m.Group("/{id}", func() {
|
||||||
// m.Combo("").Get(repo.GetProject).
|
// m.Combo("").Get(repo.GetProject).
|
||||||
// Patch(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.EditProjectOption{}), repo.EditProject).
|
// Patch(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.EditProjectOption{}), repo.EditProject).
|
||||||
// Delete(reqToken(), reqRepoWriter(unit.TypeProjects), repo.DeleteProject)
|
// Delete(reqToken(), reqRepoWriter(unit.TypeProjects), repo.DeleteProject)
|
||||||
// m.Post("/{action}", reqToken(), reqRepoWriter(unit.TypeProjects), repo.ChangeProjectStatus)
|
// m.Post("/{action}", reqToken(), reqRepoWriter(unit.TypeProjects), repo.ChangeProjectStatus)
|
||||||
// m.Group("/columns", func() {
|
// m.Group("/columns", func() {
|
||||||
// m.Combo("").Get(repo.ListProjectColumns).
|
// m.Combo("").Get(repo.ListProjectColumns).
|
||||||
// Post(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.CreateProjectColumnOption{}), repo.CreateProjectColumn)
|
// Post(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.CreateProjectColumnOption{}), repo.CreateProjectColumn)
|
||||||
// m.Group("/{columnId}", func() {
|
// m.Group("/{columnId}", func() {
|
||||||
// m.Delete("", reqToken(), reqRepoWriter(unit.TypeProjects), repo.DeleteProjectColumn)
|
// m.Delete("", reqToken(), reqRepoWriter(unit.TypeProjects), repo.DeleteProjectColumn)
|
||||||
// m.Combo("/issues").Get(repo.ListProjectColumnIssues).
|
// m.Combo("/issues").Get(repo.ListProjectColumnIssues).
|
||||||
// Post(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.AddProjectColumnIssueOption{}), repo.AddIssueToColumn)
|
// Post(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.AddProjectColumnIssueOption{}), repo.AddIssueToColumn)
|
||||||
// })
|
// })
|
||||||
// })
|
// })
|
||||||
// m.Group("/issues/{issueId}", func() {
|
// m.Group("/issues/{issueId}", func() {
|
||||||
// m.Patch("/move", reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.MoveProjectColumnIssueOption{}), repo.MoveIssueOnColumn)
|
// m.Patch("/move", reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.MoveProjectColumnIssueOption{}), repo.MoveIssueOnColumn)
|
||||||
// m.Delete("", reqToken(), reqRepoWriter(unit.TypeProjects), repo.RemoveIssueFromProject)
|
// m.Delete("", reqToken(), reqRepoWriter(unit.TypeProjects), repo.RemoveIssueFromProject)
|
||||||
// })
|
// })
|
||||||
// })
|
// })
|
||||||
// })
|
// })
|
||||||
// Repo custom fields (repo-scoped key-value metadata)
|
// Repo custom fields (repo-scoped key-value metadata)
|
||||||
m.Group("/custom-fields", func() {
|
m.Group("/custom-fields", func() {
|
||||||
m.Get("", repo.GetRepoCustomFields)
|
m.Get("", repo.GetRepoCustomFields)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
|
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/organization"
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/organization"
|
||||||
|
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
|
||||||
api "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
|
api "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/web"
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/web"
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||||
@@ -36,6 +37,18 @@ func toAPIOrgBranchProtection(ctx *context.APIContext, rule *git_model.OrgProtec
|
|||||||
return names
|
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{
|
return &api.OrgBranchProtection{
|
||||||
ID: rule.ID,
|
ID: rule.ID,
|
||||||
OrgID: rule.OrgID,
|
OrgID: rule.OrgID,
|
||||||
@@ -44,19 +57,28 @@ func toAPIOrgBranchProtection(ctx *context.APIContext, rule *git_model.OrgProtec
|
|||||||
EnablePush: rule.CanPush,
|
EnablePush: rule.CanPush,
|
||||||
EnablePushWhitelist: rule.EnableWhitelist,
|
EnablePushWhitelist: rule.EnableWhitelist,
|
||||||
PushWhitelistTeams: resolveTeamNames(rule.WhitelistTeamIDs),
|
PushWhitelistTeams: resolveTeamNames(rule.WhitelistTeamIDs),
|
||||||
|
PushWhitelistUsernames: resolveUserNames(rule.WhitelistUserIDs),
|
||||||
|
PushWhitelistActionsUser: rule.WhitelistActionsUser,
|
||||||
EnableForcePush: rule.CanForcePush,
|
EnableForcePush: rule.CanForcePush,
|
||||||
EnableForcePushAllowlist: rule.EnableForcePushAllowlist,
|
EnableForcePushAllowlist: rule.EnableForcePushAllowlist,
|
||||||
ForcePushAllowlistTeams: resolveTeamNames(rule.ForcePushAllowlistTeamIDs),
|
ForcePushAllowlistTeams: resolveTeamNames(rule.ForcePushAllowlistTeamIDs),
|
||||||
|
ForcePushAllowlistUsernames: resolveUserNames(rule.ForcePushAllowlistUserIDs),
|
||||||
|
ForcePushAllowlistActionsUser: rule.ForcePushAllowlistActionsUser,
|
||||||
EnableDelete: rule.CanDelete,
|
EnableDelete: rule.CanDelete,
|
||||||
EnableDeleteAllowlist: rule.EnableDeleteAllowlist,
|
EnableDeleteAllowlist: rule.EnableDeleteAllowlist,
|
||||||
DeleteAllowlistTeams: resolveTeamNames(rule.DeleteAllowlistTeamIDs),
|
DeleteAllowlistTeams: resolveTeamNames(rule.DeleteAllowlistTeamIDs),
|
||||||
|
DeleteAllowlistUsernames: resolveUserNames(rule.DeleteAllowlistUserIDs),
|
||||||
|
DeleteAllowlistActionsUser: rule.DeleteAllowlistActionsUser,
|
||||||
EnableMergeWhitelist: rule.EnableMergeWhitelist,
|
EnableMergeWhitelist: rule.EnableMergeWhitelist,
|
||||||
MergeWhitelistTeams: resolveTeamNames(rule.MergeWhitelistTeamIDs),
|
MergeWhitelistTeams: resolveTeamNames(rule.MergeWhitelistTeamIDs),
|
||||||
|
MergeWhitelistUsernames: resolveUserNames(rule.MergeWhitelistUserIDs),
|
||||||
|
MergeWhitelistActionsUser: rule.MergeWhitelistActionsUser,
|
||||||
EnableStatusCheck: rule.EnableStatusCheck,
|
EnableStatusCheck: rule.EnableStatusCheck,
|
||||||
StatusCheckContexts: rule.StatusCheckContexts,
|
StatusCheckContexts: rule.StatusCheckContexts,
|
||||||
RequiredApprovals: rule.RequiredApprovals,
|
RequiredApprovals: rule.RequiredApprovals,
|
||||||
EnableApprovalsWhitelist: rule.EnableApprovalsWhitelist,
|
EnableApprovalsWhitelist: rule.EnableApprovalsWhitelist,
|
||||||
ApprovalsWhitelistTeams: resolveTeamNames(rule.ApprovalsWhitelistTeamIDs),
|
ApprovalsWhitelistTeams: resolveTeamNames(rule.ApprovalsWhitelistTeamIDs),
|
||||||
|
ApprovalsWhitelistUsernames: resolveUserNames(rule.ApprovalsWhitelistUserIDs),
|
||||||
BlockOnRejectedReviews: rule.BlockOnRejectedReviews,
|
BlockOnRejectedReviews: rule.BlockOnRejectedReviews,
|
||||||
BlockOnOfficialReviewRequests: rule.BlockOnOfficialReviewRequests,
|
BlockOnOfficialReviewRequests: rule.BlockOnOfficialReviewRequests,
|
||||||
BlockOnOutdatedBranch: rule.BlockOnOutdatedBranch,
|
BlockOnOutdatedBranch: rule.BlockOnOutdatedBranch,
|
||||||
@@ -88,6 +110,41 @@ func resolveTeamIDs(ctx *context.APIContext, orgID int64, names []string) ([]int
|
|||||||
return ids, true
|
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
|
// ListOrgBranchProtections list org-level branch protection rules
|
||||||
func ListOrgBranchProtections(ctx *context.APIContext) {
|
func ListOrgBranchProtections(ctx *context.APIContext) {
|
||||||
// swagger:operation GET /orgs/{org}/branch_protections organization orgListBranchProtections
|
// swagger:operation GET /orgs/{org}/branch_protections organization orgListBranchProtections
|
||||||
@@ -218,6 +275,26 @@ func CreateOrgBranchProtection(ctx *context.APIContext) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return
|
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{
|
rule := &git_model.OrgProtectedBranch{
|
||||||
OrgID: orgID,
|
OrgID: orgID,
|
||||||
@@ -226,19 +303,28 @@ func CreateOrgBranchProtection(ctx *context.APIContext) {
|
|||||||
CanPush: form.EnablePush,
|
CanPush: form.EnablePush,
|
||||||
EnableWhitelist: form.EnablePush && form.EnablePushWhitelist,
|
EnableWhitelist: form.EnablePush && form.EnablePushWhitelist,
|
||||||
WhitelistTeamIDs: pushTeams,
|
WhitelistTeamIDs: pushTeams,
|
||||||
|
WhitelistUserIDs: pushUsers,
|
||||||
|
WhitelistActionsUser: form.EnablePush && form.EnablePushWhitelist && form.PushWhitelistActionsUser,
|
||||||
CanForcePush: form.EnablePush && form.EnableForcePush,
|
CanForcePush: form.EnablePush && form.EnableForcePush,
|
||||||
EnableForcePushAllowlist: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist,
|
EnableForcePushAllowlist: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist,
|
||||||
ForcePushAllowlistTeamIDs: forcePushTeams,
|
ForcePushAllowlistTeamIDs: forcePushTeams,
|
||||||
|
ForcePushAllowlistUserIDs: forcePushUsers,
|
||||||
|
ForcePushAllowlistActionsUser: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist && form.ForcePushAllowlistActionsUser,
|
||||||
CanDelete: form.EnableDelete,
|
CanDelete: form.EnableDelete,
|
||||||
EnableDeleteAllowlist: form.EnableDelete && form.EnableDeleteAllowlist,
|
EnableDeleteAllowlist: form.EnableDelete && form.EnableDeleteAllowlist,
|
||||||
DeleteAllowlistTeamIDs: deleteTeams,
|
DeleteAllowlistTeamIDs: deleteTeams,
|
||||||
|
DeleteAllowlistUserIDs: deleteUsers,
|
||||||
|
DeleteAllowlistActionsUser: form.EnableDelete && form.EnableDeleteAllowlist && form.DeleteAllowlistActionsUser,
|
||||||
EnableMergeWhitelist: form.EnableMergeWhitelist,
|
EnableMergeWhitelist: form.EnableMergeWhitelist,
|
||||||
MergeWhitelistTeamIDs: mergeTeams,
|
MergeWhitelistTeamIDs: mergeTeams,
|
||||||
|
MergeWhitelistUserIDs: mergeUsers,
|
||||||
|
MergeWhitelistActionsUser: form.EnableMergeWhitelist && form.MergeWhitelistActionsUser,
|
||||||
EnableStatusCheck: form.EnableStatusCheck,
|
EnableStatusCheck: form.EnableStatusCheck,
|
||||||
StatusCheckContexts: form.StatusCheckContexts,
|
StatusCheckContexts: form.StatusCheckContexts,
|
||||||
RequiredApprovals: form.RequiredApprovals,
|
RequiredApprovals: form.RequiredApprovals,
|
||||||
EnableApprovalsWhitelist: form.EnableApprovalsWhitelist,
|
EnableApprovalsWhitelist: form.EnableApprovalsWhitelist,
|
||||||
ApprovalsWhitelistTeamIDs: approvalsTeams,
|
ApprovalsWhitelistTeamIDs: approvalsTeams,
|
||||||
|
ApprovalsWhitelistUserIDs: approvalsUsers,
|
||||||
BlockOnRejectedReviews: form.BlockOnRejectedReviews,
|
BlockOnRejectedReviews: form.BlockOnRejectedReviews,
|
||||||
BlockOnOfficialReviewRequests: form.BlockOnOfficialReviewRequests,
|
BlockOnOfficialReviewRequests: form.BlockOnOfficialReviewRequests,
|
||||||
BlockOnOutdatedBranch: form.BlockOnOutdatedBranch,
|
BlockOnOutdatedBranch: form.BlockOnOutdatedBranch,
|
||||||
@@ -320,6 +406,16 @@ func EditOrgBranchProtection(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
rule.WhitelistTeamIDs = ids
|
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 {
|
if form.EnableForcePush != nil {
|
||||||
rule.CanForcePush = *form.EnableForcePush
|
rule.CanForcePush = *form.EnableForcePush
|
||||||
}
|
}
|
||||||
@@ -333,6 +429,16 @@ func EditOrgBranchProtection(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
rule.ForcePushAllowlistTeamIDs = ids
|
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 {
|
if form.EnableDelete != nil {
|
||||||
rule.CanDelete = *form.EnableDelete
|
rule.CanDelete = *form.EnableDelete
|
||||||
}
|
}
|
||||||
@@ -346,6 +452,16 @@ func EditOrgBranchProtection(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
rule.DeleteAllowlistTeamIDs = ids
|
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 {
|
if form.EnableMergeWhitelist != nil {
|
||||||
rule.EnableMergeWhitelist = *form.EnableMergeWhitelist
|
rule.EnableMergeWhitelist = *form.EnableMergeWhitelist
|
||||||
}
|
}
|
||||||
@@ -356,6 +472,16 @@ func EditOrgBranchProtection(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
rule.MergeWhitelistTeamIDs = ids
|
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 {
|
if form.EnableStatusCheck != nil {
|
||||||
rule.EnableStatusCheck = *form.EnableStatusCheck
|
rule.EnableStatusCheck = *form.EnableStatusCheck
|
||||||
}
|
}
|
||||||
@@ -375,6 +501,13 @@ func EditOrgBranchProtection(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
rule.ApprovalsWhitelistTeamIDs = ids
|
rule.ApprovalsWhitelistTeamIDs = ids
|
||||||
}
|
}
|
||||||
|
if form.ApprovalsWhitelistUsernames != nil {
|
||||||
|
ids, ok := resolveUserIDs(ctx, form.ApprovalsWhitelistUsernames)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rule.ApprovalsWhitelistUserIDs = ids
|
||||||
|
}
|
||||||
if form.BlockOnRejectedReviews != nil {
|
if form.BlockOnRejectedReviews != nil {
|
||||||
rule.BlockOnRejectedReviews = *form.BlockOnRejectedReviews
|
rule.BlockOnRejectedReviews = *form.BlockOnRejectedReviews
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,23 @@ func toAPIOrgEmailDomainPolicy(policy *git_model.OrgEmailDomainPolicy, orgID int
|
|||||||
|
|
||||||
// GetOrgEmailDomainPolicy get the organization's email domain policy
|
// GetOrgEmailDomainPolicy get the organization's email domain policy
|
||||||
func GetOrgEmailDomainPolicy(ctx *context.APIContext) {
|
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
|
orgID := ctx.Org.Organization.ID
|
||||||
policy, err := git_model.GetOrgEmailDomainPolicy(ctx, orgID)
|
policy, err := git_model.GetOrgEmailDomainPolicy(ctx, orgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -35,6 +52,31 @@ func GetOrgEmailDomainPolicy(ctx *context.APIContext) {
|
|||||||
|
|
||||||
// EditOrgEmailDomainPolicy create or update the organization's email domain policy
|
// EditOrgEmailDomainPolicy create or update the organization's email domain policy
|
||||||
func EditOrgEmailDomainPolicy(ctx *context.APIContext) {
|
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)
|
form := web.GetForm(ctx).(*api.EditOrgEmailDomainPolicyOption)
|
||||||
orgID := ctx.Org.Organization.ID
|
orgID := ctx.Org.Organization.ID
|
||||||
|
|
||||||
@@ -59,6 +101,21 @@ func EditOrgEmailDomainPolicy(ctx *context.APIContext) {
|
|||||||
|
|
||||||
// DeleteOrgEmailDomainPolicy remove the organization's email domain policy
|
// DeleteOrgEmailDomainPolicy remove the organization's email domain policy
|
||||||
func DeleteOrgEmailDomainPolicy(ctx *context.APIContext) {
|
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 {
|
if err := git_model.DeleteOrgEmailDomainPolicy(ctx, ctx.Org.Organization.ID); err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -32,6 +32,23 @@ func toAPIOrgPushPolicy(policy *git_model.OrgPushPolicy, orgID int64) *api.OrgPu
|
|||||||
|
|
||||||
// GetOrgPushPolicy get the organization's push policy
|
// GetOrgPushPolicy get the organization's push policy
|
||||||
func GetOrgPushPolicy(ctx *context.APIContext) {
|
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
|
orgID := ctx.Org.Organization.ID
|
||||||
policy, err := git_model.GetOrgPushPolicy(ctx, orgID)
|
policy, err := git_model.GetOrgPushPolicy(ctx, orgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -43,6 +60,31 @@ func GetOrgPushPolicy(ctx *context.APIContext) {
|
|||||||
|
|
||||||
// EditOrgPushPolicy create or update the organization's push policy
|
// EditOrgPushPolicy create or update the organization's push policy
|
||||||
func EditOrgPushPolicy(ctx *context.APIContext) {
|
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)
|
form := web.GetForm(ctx).(*api.EditOrgPushPolicyOption)
|
||||||
orgID := ctx.Org.Organization.ID
|
orgID := ctx.Org.Organization.ID
|
||||||
|
|
||||||
@@ -80,6 +122,21 @@ func EditOrgPushPolicy(ctx *context.APIContext) {
|
|||||||
|
|
||||||
// DeleteOrgPushPolicy remove the organization's push policy
|
// DeleteOrgPushPolicy remove the organization's push policy
|
||||||
func DeleteOrgPushPolicy(ctx *context.APIContext) {
|
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 {
|
if err := git_model.DeleteOrgPushPolicy(ctx, ctx.Org.Organization.ID); err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -42,6 +42,23 @@ func toAPIOrgRepoDefaults(d *git_model.OrgRepoDefaults, orgID int64) *api.OrgRep
|
|||||||
|
|
||||||
// GetOrgRepoDefaults get the organization's default repository settings
|
// GetOrgRepoDefaults get the organization's default repository settings
|
||||||
func GetOrgRepoDefaults(ctx *context.APIContext) {
|
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
|
orgID := ctx.Org.Organization.ID
|
||||||
defaults, err := git_model.GetOrgRepoDefaults(ctx, orgID)
|
defaults, err := git_model.GetOrgRepoDefaults(ctx, orgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -53,6 +70,31 @@ func GetOrgRepoDefaults(ctx *context.APIContext) {
|
|||||||
|
|
||||||
// EditOrgRepoDefaults create or update the organization's default repository settings
|
// EditOrgRepoDefaults create or update the organization's default repository settings
|
||||||
func EditOrgRepoDefaults(ctx *context.APIContext) {
|
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)
|
form := web.GetForm(ctx).(*api.EditOrgRepoDefaultsOption)
|
||||||
orgID := ctx.Org.Organization.ID
|
orgID := ctx.Org.Organization.ID
|
||||||
|
|
||||||
@@ -109,6 +151,21 @@ func EditOrgRepoDefaults(ctx *context.APIContext) {
|
|||||||
|
|
||||||
// DeleteOrgRepoDefaults remove the organization's default repository settings
|
// DeleteOrgRepoDefaults remove the organization's default repository settings
|
||||||
func DeleteOrgRepoDefaults(ctx *context.APIContext) {
|
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 {
|
if err := git_model.DeleteOrgRepoDefaults(ctx, ctx.Org.Organization.ID); err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -41,6 +41,23 @@ func toAPIOrgTagProtection(ctx *context.APIContext, rule *git_model.OrgProtected
|
|||||||
|
|
||||||
// ListOrgTagProtections list org-level tag protection rules
|
// ListOrgTagProtections list org-level tag protection rules
|
||||||
func ListOrgTagProtections(ctx *context.APIContext) {
|
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)
|
rules, err := git_model.FindOrgProtectedTags(ctx, ctx.Org.Organization.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
@@ -55,6 +72,29 @@ func ListOrgTagProtections(ctx *context.APIContext) {
|
|||||||
|
|
||||||
// GetOrgTagProtection get a specific org-level tag protection rule
|
// GetOrgTagProtection get a specific org-level tag protection rule
|
||||||
func GetOrgTagProtection(ctx *context.APIContext) {
|
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"))
|
rule, err := git_model.GetOrgProtectedTagByID(ctx, ctx.Org.Organization.ID, ctx.PathParamInt64("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
@@ -69,6 +109,33 @@ func GetOrgTagProtection(ctx *context.APIContext) {
|
|||||||
|
|
||||||
// CreateOrgTagProtection create an org-level tag protection rule
|
// CreateOrgTagProtection create an org-level tag protection rule
|
||||||
func CreateOrgTagProtection(ctx *context.APIContext) {
|
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)
|
form := web.GetForm(ctx).(*api.CreateOrgTagProtectionOption)
|
||||||
orgID := ctx.Org.Organization.ID
|
orgID := ctx.Org.Organization.ID
|
||||||
|
|
||||||
@@ -101,6 +168,37 @@ func CreateOrgTagProtection(ctx *context.APIContext) {
|
|||||||
|
|
||||||
// EditOrgTagProtection edit an org-level tag protection rule
|
// EditOrgTagProtection edit an org-level tag protection rule
|
||||||
func EditOrgTagProtection(ctx *context.APIContext) {
|
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)
|
form := web.GetForm(ctx).(*api.EditOrgTagProtectionOption)
|
||||||
orgID := ctx.Org.Organization.ID
|
orgID := ctx.Org.Organization.ID
|
||||||
|
|
||||||
@@ -134,6 +232,27 @@ func EditOrgTagProtection(ctx *context.APIContext) {
|
|||||||
|
|
||||||
// DeleteOrgTagProtection delete an org-level tag protection rule
|
// DeleteOrgTagProtection delete an org-level tag protection rule
|
||||||
func DeleteOrgTagProtection(ctx *context.APIContext) {
|
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
|
orgID := ctx.Org.Organization.ID
|
||||||
rule, err := git_model.GetOrgProtectedTagByID(ctx, orgID, ctx.PathParamInt64("id"))
|
rule, err := git_model.GetOrgProtectedTagByID(ctx, orgID, ctx.PathParamInt64("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ type apiMetadata struct {
|
|||||||
TargetVersion string `json:"target_version"`
|
TargetVersion string `json:"target_version"`
|
||||||
PHPMinimum string `json:"php_minimum"`
|
PHPMinimum string `json:"php_minimum"`
|
||||||
Language string `json:"language"`
|
Language string `json:"language"`
|
||||||
ExtensionType string `json:"extension_type"`
|
ExtensionType string `json:"extension_type"`
|
||||||
EntryPoint string `json:"entry_point"`
|
EntryPoint string `json:"entry_point"`
|
||||||
|
|
||||||
// deploy
|
// deploy
|
||||||
@@ -44,6 +44,13 @@ type apiMetadata struct {
|
|||||||
HealthURL string `json:"health_url,omitempty"`
|
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.
|
// GetRepoMetadata returns the manifest settings for a repository.
|
||||||
func GetRepoMetadata(ctx *context.APIContext) {
|
func GetRepoMetadata(ctx *context.APIContext) {
|
||||||
// swagger:operation GET /repos/{owner}/{repo}/manifest repository repoGetManifest
|
// swagger:operation GET /repos/{owner}/{repo}/manifest repository repoGetManifest
|
||||||
@@ -51,6 +58,17 @@ func GetRepoMetadata(ctx *context.APIContext) {
|
|||||||
// summary: Get repo manifest settings
|
// summary: Get repo manifest settings
|
||||||
// produces:
|
// produces:
|
||||||
// - application/json
|
// - 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:
|
// responses:
|
||||||
// "200":
|
// "200":
|
||||||
// "$ref": "#/responses/Manifest"
|
// "$ref": "#/responses/Manifest"
|
||||||
@@ -71,9 +89,9 @@ func GetRepoMetadata(ctx *context.APIContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.JSON(http.StatusOK, &apiMetadata{
|
ctx.JSON(http.StatusOK, &apiMetadata{
|
||||||
Name: m.Name,
|
Name: m.Name,
|
||||||
Org: m.Org,
|
Org: m.Org,
|
||||||
Description: m.Description,
|
Description: m.Description,
|
||||||
|
|
||||||
LicenseSPDX: m.LicenseSPDX,
|
LicenseSPDX: m.LicenseSPDX,
|
||||||
LicenseName: m.LicenseName,
|
LicenseName: m.LicenseName,
|
||||||
@@ -89,7 +107,7 @@ func GetRepoMetadata(ctx *context.APIContext) {
|
|||||||
TargetVersion: m.TargetVersion,
|
TargetVersion: m.TargetVersion,
|
||||||
PHPMinimum: m.PHPMinimum,
|
PHPMinimum: m.PHPMinimum,
|
||||||
Language: m.Language,
|
Language: m.Language,
|
||||||
ExtensionType: m.ExtensionType,
|
ExtensionType: m.ExtensionType,
|
||||||
EntryPoint: m.EntryPoint,
|
EntryPoint: m.EntryPoint,
|
||||||
DeployHost: m.DeployHost,
|
DeployHost: m.DeployHost,
|
||||||
DeployPort: m.DeployPort,
|
DeployPort: m.DeployPort,
|
||||||
@@ -111,9 +129,21 @@ func UpdateRepoMetadata(ctx *context.APIContext) {
|
|||||||
// - application/json
|
// - application/json
|
||||||
// produces:
|
// produces:
|
||||||
// - application/json
|
// - 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:
|
// responses:
|
||||||
// "200":
|
// "200":
|
||||||
// "$ref": "#/responses/Manifest"
|
// "$ref": "#/responses/Manifest"
|
||||||
|
|
||||||
// Decode into a map to detect which fields were actually sent.
|
// Decode into a map to detect which fields were actually sent.
|
||||||
var raw map[string]any
|
var raw map[string]any
|
||||||
if err := json.NewDecoder(ctx.Req.Body).Decode(&raw); err != nil {
|
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{
|
ctx.JSON(http.StatusOK, &apiMetadata{
|
||||||
Name: m.Name,
|
Name: m.Name,
|
||||||
Org: m.Org,
|
Org: m.Org,
|
||||||
Description: m.Description,
|
Description: m.Description,
|
||||||
|
|
||||||
LicenseSPDX: m.LicenseSPDX,
|
LicenseSPDX: m.LicenseSPDX,
|
||||||
LicenseName: m.LicenseName,
|
LicenseName: m.LicenseName,
|
||||||
@@ -191,7 +221,7 @@ func UpdateRepoMetadata(ctx *context.APIContext) {
|
|||||||
TargetVersion: m.TargetVersion,
|
TargetVersion: m.TargetVersion,
|
||||||
PHPMinimum: m.PHPMinimum,
|
PHPMinimum: m.PHPMinimum,
|
||||||
Language: m.Language,
|
Language: m.Language,
|
||||||
ExtensionType: m.ExtensionType,
|
ExtensionType: m.ExtensionType,
|
||||||
EntryPoint: m.EntryPoint,
|
EntryPoint: m.EntryPoint,
|
||||||
DeployHost: m.DeployHost,
|
DeployHost: m.DeployHost,
|
||||||
DeployPort: m.DeployPort,
|
DeployPort: m.DeployPort,
|
||||||
|
|||||||
@@ -159,6 +159,44 @@ type swaggerParameterBodies struct {
|
|||||||
// in:body
|
// in:body
|
||||||
UpdateBranchProtectionPriories api.UpdateBranchProtectionPriories
|
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
|
// in:body
|
||||||
CreateOAuth2ApplicationOptions api.CreateOAuth2ApplicationOptions
|
CreateOAuth2ApplicationOptions api.CreateOAuth2ApplicationOptions
|
||||||
|
|
||||||
|
|||||||
@@ -41,3 +41,52 @@ type swaggerResponseOrganizationPermissions struct {
|
|||||||
// in:body
|
// in:body
|
||||||
Body api.OrganizationPermissions `json:"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 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{
|
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),
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// largestBlobOverLimit returns the first file (and its size) in the pushed tip tree
|
// largestBlobOverLimit returns the first file (and its size) added or modified by the
|
||||||
// that exceeds limit bytes, or ("", 0) if none — or on any error (fail open).
|
// push (oldCommitID..newCommitID) whose blob exceeds limit bytes, or ("", 0) if none —
|
||||||
func (ctx *preReceiveContext) largestBlobOverLimit(commitID string, limit int64) (string, int64) {
|
// or on any error (fail open). Only the pushed delta is inspected so a pre-existing
|
||||||
output, _, err := gitrepo.RunCmdString(ctx,
|
// oversized file committed before the policy existed cannot permanently block unrelated
|
||||||
ctx.Repo.Repository,
|
// pushes. For a new branch (no old commit) the full new tree is scanned, since every
|
||||||
gitcmd.NewCommand("ls-tree", "-r", "--long").
|
// blob is effectively introduced by the push.
|
||||||
AddDynamicArguments(commitID).
|
func (ctx *preReceiveContext) largestBlobOverLimit(oldCommitID, newCommitID string, limit int64) (string, int64) {
|
||||||
WithEnv(ctx.env),
|
emptyID := ctx.Repo.GetObjectFormat().EmptyObjectID().String()
|
||||||
)
|
|
||||||
if err != nil {
|
// New branch: no base commit to diff against, so scan the full tip tree. ls-tree
|
||||||
log.Error("org push policy ls-tree for %-v: %v", ctx.Repo.Repository, err)
|
// --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
|
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") {
|
for _, line := range strings.Split(output, "\n") {
|
||||||
tab := strings.IndexByte(line, '\t')
|
tab := strings.IndexByte(line, '\t')
|
||||||
if tab < 0 {
|
if tab < 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fields := strings.Fields(line[:tab]) // mode, type, hash, size
|
fields := strings.Fields(line[:tab]) // ":oldmode newmode oldsha newsha status"
|
||||||
if len(fields) < 4 || fields[1] != "blob" {
|
if len(fields) < 5 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
size, perr := strconv.ParseInt(fields[3], 10, 64)
|
newMode, newSha, status := fields[1], fields[3], fields[4]
|
||||||
if perr != nil {
|
// Only regular, executable, or symlink blobs; skip submodule gitlinks (160000).
|
||||||
|
if newMode != "100644" && newMode != "100755" && newMode != "120000" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if size > limit {
|
if status == "D" || newSha == emptyID {
|
||||||
return line[tab+1:], size
|
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
|
return "", 0
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ func NewComment(ctx *context.Context) {
|
|||||||
} // end if: handle close or reopen
|
} // end if: handle close or reopen
|
||||||
|
|
||||||
// Handle custom status from the status dropdown (replaces close button for issues with org statuses).
|
// 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" {
|
if statusIDStr == "reopen" {
|
||||||
// Reopen via dropdown
|
// Reopen via dropdown
|
||||||
if issue.IsClosed {
|
if issue.IsClosed {
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import (
|
|||||||
|
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||||
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
|
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"
|
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"
|
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/container"
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/container"
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package repository
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/xml"
|
|
||||||
|
|
||||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
|
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// manifestXML mirrors the .mokogitea/manifest.xml schema for XML parsing.
|
|
||||||
type manifestXML struct {
|
|
||||||
XMLName xml.Name `xml:"mokoplatform"`
|
|
||||||
Identity manifestIdentity `xml:"identity"`
|
|
||||||
Governance manifestGovernance `xml:"governance"`
|
|
||||||
Distribution manifestDistribution `xml:"distribution"`
|
|
||||||
Build manifestBuild `xml:"build"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type manifestDistribution struct {
|
|
||||||
DisplayName string `xml:"display-name"`
|
|
||||||
Maintainer string `xml:"maintainer"`
|
|
||||||
MaintainerURL string `xml:"maintainer-url"`
|
|
||||||
InfoURL string `xml:"info-url"`
|
|
||||||
TargetVersion string `xml:"target-version"`
|
|
||||||
PHPMinimum string `xml:"php-minimum"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type manifestIdentity struct {
|
|
||||||
Name string `xml:"name"`
|
|
||||||
Org string `xml:"org"`
|
|
||||||
Description string `xml:"description"`
|
|
||||||
VersionPrefix string `xml:"version-prefix"`
|
|
||||||
ElementName string `xml:"element-name"`
|
|
||||||
License manifestLicense `xml:"license"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type manifestLicense struct {
|
|
||||||
SPDX string `xml:"spdx,attr"`
|
|
||||||
Name string `xml:",chardata"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type manifestGovernance struct {
|
|
||||||
Platform string `xml:"platform"`
|
|
||||||
StandardsVersion string `xml:"standards-version"`
|
|
||||||
StandardsSource string `xml:"standards-source"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type manifestBuild struct {
|
|
||||||
Language string `xml:"language"`
|
|
||||||
ExtensionType string `xml:"package-type"`
|
|
||||||
EntryPoint string `xml:"entry-point"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SyncMetadataFromCommit reads .mokogitea/manifest.xml from the given commit
|
|
||||||
// and upserts the values into the repo_manifest database table.
|
|
||||||
// This is called on push to the default branch to keep the database in sync
|
|
||||||
// with the XML file. If no manifest.xml exists, this is a no-op.
|
|
||||||
func SyncMetadataFromCommit(ctx context.Context, repo *repo_model.Repository, commit *git.Commit) {
|
|
||||||
if commit == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
entry, err := commit.GetTreeEntryByPath(".mokogitea/manifest.xml")
|
|
||||||
if err != nil || entry == nil {
|
|
||||||
return // no manifest.xml — not an error
|
|
||||||
}
|
|
||||||
|
|
||||||
reader, err := entry.Blob().DataAsync()
|
|
||||||
if err != nil {
|
|
||||||
log.Error("SyncMetadata: read blob for %s: %v", repo.FullName(), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer reader.Close()
|
|
||||||
|
|
||||||
var mxml manifestXML
|
|
||||||
decoder := xml.NewDecoder(reader)
|
|
||||||
if err := decoder.Decode(&mxml); err != nil {
|
|
||||||
log.Error("SyncMetadata: parse XML for %s: %v", repo.FullName(), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
manifest := &repo_model.RepoMetadata{
|
|
||||||
RepoID: repo.ID,
|
|
||||||
Name: mxml.Identity.Name,
|
|
||||||
Org: mxml.Identity.Org,
|
|
||||||
Description: mxml.Identity.Description,
|
|
||||||
VersionPrefix: mxml.Identity.VersionPrefix,
|
|
||||||
ElementName: mxml.Identity.ElementName,
|
|
||||||
LicenseSPDX: mxml.Identity.License.SPDX,
|
|
||||||
LicenseName: mxml.Identity.License.Name,
|
|
||||||
Platform: mxml.Governance.Platform,
|
|
||||||
StandardsVersion: mxml.Governance.StandardsVersion,
|
|
||||||
StandardsSource: mxml.Governance.StandardsSource,
|
|
||||||
Maintainer: mxml.Distribution.Maintainer,
|
|
||||||
MaintainerURL: mxml.Distribution.MaintainerURL,
|
|
||||||
InfoURL: mxml.Distribution.InfoURL,
|
|
||||||
TargetVersion: mxml.Distribution.TargetVersion,
|
|
||||||
PHPMinimum: mxml.Distribution.PHPMinimum,
|
|
||||||
Language: mxml.Build.Language,
|
|
||||||
ExtensionType: mxml.Build.ExtensionType,
|
|
||||||
EntryPoint: mxml.Build.EntryPoint,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := repo_model.CreateOrUpdateRepoMetadata(ctx, manifest); err != nil {
|
|
||||||
log.Error("SyncMetadata: save for %s: %v", repo.FullName(), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("SyncMetadata: synced .mokogitea/manifest.xml for %s", repo.FullName())
|
|
||||||
}
|
|
||||||
@@ -194,8 +194,6 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
|
|||||||
if err := DelRepoDivergenceFromCache(ctx, repo.ID); err != nil {
|
if err := DelRepoDivergenceFromCache(ctx, repo.ID); err != nil {
|
||||||
log.Error("DelRepoDivergenceFromCache: %v", err)
|
log.Error("DelRepoDivergenceFromCache: %v", err)
|
||||||
}
|
}
|
||||||
// Auto-sync .mokogitea/manifest.xml to database on default branch push
|
|
||||||
SyncMetadataFromCommit(ctx, repo, newCommit)
|
|
||||||
// Run security scanners on default branch push
|
// Run security scanners on default branch push
|
||||||
security_service.ScanOnPush(ctx, repo, newCommit)
|
security_service.ScanOnPush(ctx, repo, newCommit)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ var DefaultCodeRules = []CodeRule{
|
|||||||
{
|
{
|
||||||
ID: "deserialize-yaml-py", Title: "Insecure Deserialization: yaml.load() (Python)",
|
ID: "deserialize-yaml-py", Title: "Insecure Deserialization: yaml.load() (Python)",
|
||||||
Severity: security_model.SeverityHigh, CWE: "CWE-502",
|
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()",
|
Description: "yaml.load() without SafeLoader allows arbitrary code execution — use yaml.safe_load()",
|
||||||
Languages: []string{".py"},
|
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>
|
</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="field">
|
||||||
<div class="ui radio checkbox">
|
<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}}>
|
<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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
auth_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/auth"
|
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) {
|
t.Run("CreatePackage", func(t *testing.T) {
|
||||||
body := `{"name":"Test Pro Annual","description":"Annual pro subscription","duration_days":365,"max_sites":5}`
|
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).
|
AddTokenAuth(token).
|
||||||
SetHeader("Content-Type", "application/json")
|
SetHeader("Content-Type", "application/json")
|
||||||
resp := MakeRequest(t, req, http.StatusCreated)
|
resp := MakeRequest(t, req, http.StatusCreated)
|
||||||
@@ -51,7 +52,7 @@ func TestAPILicensePackages(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("CreatePackageNoName", func(t *testing.T) {
|
t.Run("CreatePackageNoName", func(t *testing.T) {
|
||||||
body := `{"description":"Missing name"}`
|
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).
|
AddTokenAuth(token).
|
||||||
SetHeader("Content-Type", "application/json")
|
SetHeader("Content-Type", "application/json")
|
||||||
MakeRequest(t, req, http.StatusUnprocessableEntity)
|
MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||||
@@ -68,7 +69,7 @@ func TestAPILicenseKeys(t *testing.T) {
|
|||||||
|
|
||||||
// Create a package first.
|
// Create a package first.
|
||||||
body := `{"name":"Test Package","duration_days":30}`
|
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).
|
AddTokenAuth(token).
|
||||||
SetHeader("Content-Type", "application/json")
|
SetHeader("Content-Type", "application/json")
|
||||||
resp := MakeRequest(t, req, http.StatusCreated)
|
resp := MakeRequest(t, req, http.StatusCreated)
|
||||||
@@ -80,7 +81,7 @@ func TestAPILicenseKeys(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("CreateKey", func(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)
|
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).
|
AddTokenAuth(token).
|
||||||
SetHeader("Content-Type", "application/json")
|
SetHeader("Content-Type", "application/json")
|
||||||
resp := MakeRequest(t, req, http.StatusCreated)
|
resp := MakeRequest(t, req, http.StatusCreated)
|
||||||
@@ -104,7 +105,7 @@ func TestAPILicenseKeys(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("EditKey", func(t *testing.T) {
|
t.Run("EditKey", func(t *testing.T) {
|
||||||
body := `{"licensee_name":"Jane Doe","domain_restriction":"example.com,test.com"}`
|
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).
|
AddTokenAuth(token).
|
||||||
SetHeader("Content-Type", "application/json")
|
SetHeader("Content-Type", "application/json")
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
@@ -124,7 +125,7 @@ func TestAPILicenseKeys(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("ValidateKey", func(t *testing.T) {
|
t.Run("ValidateKey", func(t *testing.T) {
|
||||||
body := fmt.Sprintf(`{"key":"%s","domain":"example.com"}`, rawKey)
|
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")
|
SetHeader("Content-Type", "application/json")
|
||||||
// Note: no token — this is a public endpoint.
|
// Note: no token — this is a public endpoint.
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
@@ -136,7 +137,7 @@ func TestAPILicenseKeys(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("ValidateInvalidKey", func(t *testing.T) {
|
t.Run("ValidateInvalidKey", func(t *testing.T) {
|
||||||
body := `{"key":"MOKO-XXXX-XXXX-XXXX-XXXX","domain":"example.com"}`
|
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")
|
SetHeader("Content-Type", "application/json")
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
var result api.ValidateLicenseKeyResponse
|
var result api.ValidateLicenseKeyResponse
|
||||||
@@ -161,7 +162,7 @@ func TestAPILicensePurchaseWebhook(t *testing.T) {
|
|||||||
|
|
||||||
// Create a package.
|
// Create a package.
|
||||||
body := `{"name":"Purchase Test","duration_days":90}`
|
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).
|
AddTokenAuth(token).
|
||||||
SetHeader("Content-Type", "application/json")
|
SetHeader("Content-Type", "application/json")
|
||||||
resp := MakeRequest(t, req, http.StatusCreated)
|
resp := MakeRequest(t, req, http.StatusCreated)
|
||||||
@@ -170,7 +171,7 @@ func TestAPILicensePurchaseWebhook(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("PurchaseNewKey", func(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)
|
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).
|
AddTokenAuth(token).
|
||||||
SetHeader("Content-Type", "application/json")
|
SetHeader("Content-Type", "application/json")
|
||||||
resp := MakeRequest(t, req, http.StatusCreated)
|
resp := MakeRequest(t, req, http.StatusCreated)
|
||||||
@@ -183,7 +184,7 @@ func TestAPILicensePurchaseWebhook(t *testing.T) {
|
|||||||
t.Run("PurchaseIdempotent", func(t *testing.T) {
|
t.Run("PurchaseIdempotent", func(t *testing.T) {
|
||||||
// Same payment_ref should return existing key without raw_key.
|
// 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)
|
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).
|
AddTokenAuth(token).
|
||||||
SetHeader("Content-Type", "application/json")
|
SetHeader("Content-Type", "application/json")
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ func TestPackageComposer(t *testing.T) {
|
|||||||
AddBasicAuth(user.Name)
|
AddBasicAuth(user.Name)
|
||||||
resp = MakeRequest(t, req, http.StatusOK)
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
result = composer.PackageMetadataResponse{}
|
result = &composer.PackageMetadataResponse{}
|
||||||
DecodeJSON(t, resp, &result)
|
DecodeJSON(t, resp, &result)
|
||||||
pkgs = result.Packages[packageName]
|
pkgs = result.Packages[packageName]
|
||||||
assert.Len(t, pkgs, 1)
|
assert.Len(t, pkgs, 1)
|
||||||
@@ -268,7 +268,7 @@ func TestPackageComposer(t *testing.T) {
|
|||||||
AddBasicAuth(otherUser.Name)
|
AddBasicAuth(otherUser.Name)
|
||||||
resp = MakeRequest(t, req, http.StatusOK)
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
result = composer.PackageMetadataResponse{}
|
result = &composer.PackageMetadataResponse{}
|
||||||
DecodeJSON(t, resp, &result)
|
DecodeJSON(t, resp, &result)
|
||||||
pkgs = result.Packages[packageName]
|
pkgs = result.Packages[packageName]
|
||||||
assert.Len(t, pkgs, 1)
|
assert.Len(t, pkgs, 1)
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ func TestOAuth2CallbackReactivationGating(t *testing.T) {
|
|||||||
defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)()
|
defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)()
|
||||||
defer test.MockVariableValue(&setting.OAuth2Client.Username, setting.OAuth2UsernameUserid)()
|
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{
|
addOAuth2Source(t, "test-oauth-source", oauth2.Source{
|
||||||
Provider: "openidConnect",
|
Provider: "openidConnect",
|
||||||
ClientID: "test-client-id",
|
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
|
// 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()
|
t.Helper()
|
||||||
|
|
||||||
var srv *httptest.Server
|
var srv *httptest.Server
|
||||||
|
|||||||
@@ -7,16 +7,16 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
auth_model "code.gitea.io/gitea/models/auth"
|
auth_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/auth"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unittest"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/test"
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/test"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/web"
|
||||||
"code.gitea.io/gitea/routers/web/auth"
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/routers/web/auth"
|
||||||
"code.gitea.io/gitea/services/auth/source/oauth2"
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/auth/source/oauth2"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||||
"code.gitea.io/gitea/tests"
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/tests"
|
||||||
|
|
||||||
"github.com/markbates/goth"
|
"github.com/markbates/goth"
|
||||||
"github.com/markbates/goth/gothic"
|
"github.com/markbates/goth/gothic"
|
||||||
|
|||||||
+1
-1
@@ -25,7 +25,7 @@ Custom CSS themes at `/var/lib/gitea/custom/public/assets/css/`:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Repo: [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) · [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 |
|
| Revision | Date | Author | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ When a field definition is deleted, all associated values in `custom_field_value
|
|||||||
| System | Relationship |
|
| 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. |
|
| 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. |
|
| 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 |
|
| 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**:
|
In **Organization Settings → Licensing & Update Streams**, under **Extension Metadata**:
|
||||||
1. Set the **Platform** (Joomla, Dolibarr, WordPress, etc.)
|
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
|
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 - MokoWaaS") — shown in Joomla update manager
|
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)
|
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)
|
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`)
|
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:
|
Each repository can have a manifest that describes:
|
||||||
|
|
||||||
- **Identity** — project name, organization, description, version, and license
|
- **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
|
- **Build** — language, package type, and entry point
|
||||||
|
|
||||||
These settings replace the legacy `.mokogitea/manifest.xml` file-based approach.
|
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
|
## 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
|
### 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",
|
"license_name": "GNU General Public License v3",
|
||||||
"platform": "go",
|
"platform": "go",
|
||||||
"standards_version": "05.00.00",
|
"standards_version": "05.00.00",
|
||||||
"standards_source": "https://code.mokoconsulting.tech/MokoConsulting/moko-platform",
|
"standards_source": "https://code.mokoconsulting.tech/MokoConsulting/mokocli",
|
||||||
"language": "Go",
|
"language": "Go",
|
||||||
"package_type": "application",
|
"package_type": "application",
|
||||||
"entry_point": "./"
|
"entry_point": "./"
|
||||||
@@ -101,8 +101,8 @@ Manifest settings are stored in the `repo_manifest` table (migration v347). One
|
|||||||
| System | Relationship |
|
| 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. |
|
| 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. |
|
| Custom Fields | Repo-scoped custom fields (org settings) are separate from manifest fields. Custom fields are user-defined; manifest fields follow the mokocli schema. |
|
||||||
| moko-platform CLI | The CLI reads manifest settings via the API for version bumping, build decisions, and cross-repo syncing (see issue #505). |
|
| 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 |
|
| Revision | Date | Author | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
|||||||
+2
-2
@@ -23,9 +23,9 @@
|
|||||||
|
|
||||||
## In Progress
|
## In Progress
|
||||||
|
|
||||||
- Rename moko-platform to MokoPlatform
|
- Rename mokocli to MokoCLI
|
||||||
- Granular role-based permissions for all features (#9)
|
- 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
|
- Bulk migrate remaining 41 flat wikis to folders
|
||||||
|
|
||||||
## Planned
|
## Planned
|
||||||
|
|||||||
Reference in New Issue
Block a user