Compare commits
1 Commits
dev
..
development
| Author | SHA1 | Date | |
|---|---|---|---|
| add7c0da4d |
@@ -57,7 +57,7 @@ jobs:
|
|||||||
- name: Determine target repos
|
- name: Determine target repos
|
||||||
id: repos
|
id: repos
|
||||||
env:
|
env:
|
||||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
API="${GITEA_URL}/api/v1"
|
API="${GITEA_URL}/api/v1"
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ jobs:
|
|||||||
REPOS=""
|
REPOS=""
|
||||||
while true; do
|
while true; do
|
||||||
BATCH=$(curl -sS \
|
BATCH=$(curl -sS \
|
||||||
-H "Authorization: token ${MOKOGITEA_TOKEN}" \
|
-H "Authorization: token ${GA_TOKEN}" \
|
||||||
"${API}/orgs/${GITEA_ORG}/repos?page=${PAGE}&limit=50" \
|
"${API}/orgs/${GITEA_ORG}/repos?page=${PAGE}&limit=50" \
|
||||||
| jq -r '.[].name // empty')
|
| jq -r '.[].name // empty')
|
||||||
[ -z "$BATCH" ] && break
|
[ -z "$BATCH" ] && break
|
||||||
@@ -105,7 +105,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Apply protection rules
|
- name: Apply protection rules
|
||||||
env:
|
env:
|
||||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
DRY_RUN: ${{ inputs.dry_run || 'false' }}
|
DRY_RUN: ${{ inputs.dry_run || 'false' }}
|
||||||
run: |
|
run: |
|
||||||
API="${GITEA_URL}/api/v1"
|
API="${GITEA_URL}/api/v1"
|
||||||
@@ -214,13 +214,13 @@ jobs:
|
|||||||
ENCODED_NAME=$(echo "$NAME" | sed 's|/|%2F|g')
|
ENCODED_NAME=$(echo "$NAME" | sed 's|/|%2F|g')
|
||||||
curl -sS -o /dev/null -w "" \
|
curl -sS -o /dev/null -w "" \
|
||||||
-X DELETE \
|
-X DELETE \
|
||||||
-H "Authorization: token ${MOKOGITEA_TOKEN}" \
|
-H "Authorization: token ${GA_TOKEN}" \
|
||||||
"${API}/repos/${GITEA_ORG}/${REPO}/branch_protections/${ENCODED_NAME}" 2>/dev/null || true
|
"${API}/repos/${GITEA_ORG}/${REPO}/branch_protections/${ENCODED_NAME}" 2>/dev/null || true
|
||||||
|
|
||||||
# Create rule
|
# Create rule
|
||||||
RESPONSE=$(curl -sS -w "\n%{http_code}" \
|
RESPONSE=$(curl -sS -w "\n%{http_code}" \
|
||||||
-X POST \
|
-X POST \
|
||||||
-H "Authorization: token ${MOKOGITEA_TOKEN}" \
|
-H "Authorization: token ${GA_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "$RULE" \
|
-d "$RULE" \
|
||||||
"${API}/repos/${GITEA_ORG}/${REPO}/branch_protections")
|
"${API}/repos/${GITEA_ORG}/${REPO}/branch_protections")
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|||||||
@@ -10,9 +10,9 @@
|
|||||||
# VERSION: 05.00.00
|
# VERSION: 05.00.00
|
||||||
# BRIEF: Universal build & release � detects platform from manifest.xml
|
# BRIEF: Universal build & release � detects platform from manifest.xml
|
||||||
#
|
#
|
||||||
# +=======================================================================+
|
# +========================================================================+
|
||||||
# | UNIVERSAL BUILD & RELEASE PIPELINE |
|
# | UNIVERSAL BUILD & RELEASE PIPELINE |
|
||||||
# +=======================================================================+
|
# +========================================================================+
|
||||||
# | |
|
# | |
|
||||||
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
||||||
# | |
|
# | |
|
||||||
@@ -21,24 +21,15 @@
|
|||||||
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
||||||
# | generic: README-only, no update stream |
|
# | generic: README-only, no update stream |
|
||||||
# | |
|
# | |
|
||||||
# +=======================================================================+
|
# +========================================================================+
|
||||||
|
|
||||||
name: "Universal: Build & Release"
|
name: "Universal: Build & Release"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, synchronize, closed]
|
types: [opened, closed]
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths-ignore:
|
|
||||||
- '.mokogitea/workflows/**'
|
|
||||||
- '*.md'
|
|
||||||
- 'wiki/**'
|
|
||||||
- '.editorconfig'
|
|
||||||
- '.gitignore'
|
|
||||||
- '.gitattributes'
|
|
||||||
- '.gitmessage'
|
|
||||||
- 'LICENSE'
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
action:
|
action:
|
||||||
@@ -52,7 +43,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||||
|
|
||||||
@@ -60,13 +51,12 @@ permissions:
|
|||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────────
|
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────
|
||||||
promote-rc:
|
promote-rc:
|
||||||
name: Promote to RC
|
name: Promote to RC
|
||||||
runs-on: release
|
runs-on: release
|
||||||
if: >-
|
if: >-
|
||||||
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
|
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
|
||||||
(github.event.action == 'synchronize' && github.event.pull_request.merged != true) ||
|
|
||||||
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
|
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -102,7 +92,7 @@ jobs:
|
|||||||
php ${MOKO_CLI}/branch_rename.php \
|
php ${MOKO_CLI}/branch_rename.php \
|
||||||
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
--api-base "${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
||||||
--pr "${{ github.event.pull_request.number }}"
|
--pr "${{ github.event.pull_request.number }}"
|
||||||
|
|
||||||
- name: Checkout rc and configure git
|
- name: Checkout rc and configure git
|
||||||
@@ -121,7 +111,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Update RC release notes from CHANGELOG.md
|
- name: Update RC release notes from CHANGELOG.md
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
# Extract [Unreleased] section from changelog
|
# Extract [Unreleased] section from changelog
|
||||||
@@ -159,7 +149,7 @@ jobs:
|
|||||||
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
|
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
# ── Merged PR → Build & Release (or promote RC to stable) ─────────────────────────
|
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
||||||
release:
|
release:
|
||||||
name: Build & Release Pipeline
|
name: Build & Release Pipeline
|
||||||
runs-on: release
|
runs-on: release
|
||||||
@@ -251,50 +241,14 @@ jobs:
|
|||||||
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||||
[ -z "$VERSION" ] && VERSION="00.00.00" && echo "skip=true" >> "$GITHUB_OUTPUT"
|
[ -z "$VERSION" ] && VERSION="00.00.00" && echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
echo "tag=stable" >> "$GITHUB_OUTPUT"
|
||||||
if [[ "$PLATFORM" == joomla* ]]; then
|
echo "release_tag=stable" >> "$GITHUB_OUTPUT"
|
||||||
echo "tag=stable" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "release_tag=stable" >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
|
||||||
echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "release_tag=v${VERSION}" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
echo "branch=main" >> "$GITHUB_OUTPUT"
|
echo "branch=main" >> "$GITHUB_OUTPUT"
|
||||||
echo "Published version: ${VERSION}"
|
echo "Published version: ${VERSION}"
|
||||||
|
|
||||||
- name: "Create semver tag for non-Joomla repos"
|
|
||||||
id: semver
|
|
||||||
if: |
|
|
||||||
steps.version.outputs.skip != 'true' &&
|
|
||||||
!startsWith(steps.platform.outputs.platform, 'joomla')
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
|
||||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
|
||||||
SEMVER_TAG="v${VERSION}"
|
|
||||||
|
|
||||||
echo "Creating semver tag: ${SEMVER_TAG}"
|
|
||||||
|
|
||||||
# Create the git tag via API
|
|
||||||
HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" \
|
|
||||||
-X POST -H "Authorization: token ${TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${API_BASE}/tags" \
|
|
||||||
-d "{\"tag_name\":\"${SEMVER_TAG}\",\"target\":\"main\",\"message\":\"Release ${VERSION}\"}" 2>/dev/null || echo "000")
|
|
||||||
|
|
||||||
if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then
|
|
||||||
echo "Created semver tag: ${SEMVER_TAG}"
|
|
||||||
elif [ "$HTTP_CODE" = "409" ]; then
|
|
||||||
echo "Semver tag ${SEMVER_TAG} already exists (skipped)"
|
|
||||||
else
|
|
||||||
echo "::warning::Failed to create semver tag ${SEMVER_TAG} (HTTP ${HTTP_CODE})"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "semver_tag=${SEMVER_TAG}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Update release notes and promote changelog
|
- name: Update release notes and promote changelog
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
# Get the stable release info (version and ID)
|
# Get the stable release info (version and ID)
|
||||||
@@ -363,7 +317,7 @@ jobs:
|
|||||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
php ${MOKO_CLI}/release_mirror.php \
|
php ${MOKO_CLI}/release_mirror.php \
|
||||||
--version "$VERSION" --tag "$RELEASE_TAG" \
|
--version "$VERSION" --tag "$RELEASE_TAG" \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
@@ -392,7 +346,7 @@ jobs:
|
|||||||
if: steps.version.outputs.skip != 'true'
|
if: steps.version.outputs.skip != 'true'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
# Delete rc branch (ephemeral — created by promote-rc)
|
# Delete rc branch (ephemeral — created by promote-rc)
|
||||||
@@ -416,7 +370,7 @@ jobs:
|
|||||||
if: steps.version.outputs.skip != 'true'
|
if: steps.version.outputs.skip != 'true'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
BRANCH_NAME="version/${VERSION}"
|
BRANCH_NAME="version/${VERSION}"
|
||||||
@@ -437,7 +391,7 @@ jobs:
|
|||||||
if: steps.version.outputs.skip != 'true'
|
if: steps.version.outputs.skip != 'true'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
php ${MOKO_CLI}/version_reset_dev.php \
|
php ${MOKO_CLI}/version_reset_dev.php \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
||||||
--branch dev --path . 2>&1 || true
|
--branch dev --path . 2>&1 || true
|
||||||
@@ -463,5 +417,5 @@ jobs:
|
|||||||
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
|
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
|
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "| Release | [View](${MOKOGITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
|
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ permissions:
|
|||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cleanup:
|
cleanup:
|
||||||
@@ -33,17 +33,17 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.GA_TOKEN }}
|
||||||
|
|
||||||
- name: Delete merged branches
|
- name: Delete merged branches
|
||||||
env:
|
env:
|
||||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
echo "=== Merged Branch Cleanup ==="
|
echo "=== Merged Branch Cleanup ==="
|
||||||
API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}"
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
|
|
||||||
# List branches via API
|
# List branches via API
|
||||||
BRANCHES=$(curl -sS -H "Authorization: token ${MOKOGITEA_TOKEN}" \
|
BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
|
||||||
"${API}/branches?limit=50" | jq -r '.[].name')
|
"${API}/branches?limit=50" | jq -r '.[].name')
|
||||||
|
|
||||||
DELETED=0
|
DELETED=0
|
||||||
@@ -56,7 +56,7 @@ jobs:
|
|||||||
# Check if branch is merged into main
|
# Check if branch is merged into main
|
||||||
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
|
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
|
||||||
echo " Deleting merged branch: ${BRANCH}"
|
echo " Deleting merged branch: ${BRANCH}"
|
||||||
curl -sS -X DELETE -H "Authorization: token ${MOKOGITEA_TOKEN}" \
|
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
|
||||||
"${API}/branches/${BRANCH}" 2>/dev/null || true
|
"${API}/branches/${BRANCH}" 2>/dev/null || true
|
||||||
DELETED=$((DELETED + 1))
|
DELETED=$((DELETED + 1))
|
||||||
fi
|
fi
|
||||||
@@ -66,20 +66,20 @@ jobs:
|
|||||||
|
|
||||||
- name: Clean old workflow runs
|
- name: Clean old workflow runs
|
||||||
env:
|
env:
|
||||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
echo "=== Workflow Run Cleanup ==="
|
echo "=== Workflow Run Cleanup ==="
|
||||||
API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}"
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
|
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
|
||||||
|
|
||||||
# Get old completed runs
|
# Get old completed runs
|
||||||
RUNS=$(curl -sS -H "Authorization: token ${MOKOGITEA_TOKEN}" \
|
RUNS=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
|
||||||
"${API}/actions/runs?status=completed&limit=50" | \
|
"${API}/actions/runs?status=completed&limit=50" | \
|
||||||
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
|
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
|
||||||
|
|
||||||
DELETED=0
|
DELETED=0
|
||||||
for RUN_ID in $RUNS; do
|
for RUN_ID in $RUNS; do
|
||||||
curl -sS -X DELETE -H "Authorization: token ${MOKOGITEA_TOKEN}" \
|
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
|
||||||
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
|
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
|
||||||
DELETED=$((DELETED + 1))
|
DELETED=$((DELETED + 1))
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
name: "Publish to Composer"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
- '[0-9]*.[0-9]*.[0-9]*'
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
name: Publish Package
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >-
|
||||||
|
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||||
|
!contains(github.event.head_commit.message, '[skip publish]')
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
run: |
|
||||||
|
if ! command -v php &> /dev/null; then
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer install --no-dev --no-interaction --prefer-dist --quiet
|
||||||
|
|
||||||
|
- name: Determine version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
VERSION=$(php -r "echo json_decode(file_get_contents('composer.json'))->version;")
|
||||||
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Package version: ${VERSION}"
|
||||||
|
|
||||||
|
# Gitea Composer Registry — auto-publishes from tags
|
||||||
|
# The tag push itself registers the package at:
|
||||||
|
# https://git.mokoconsulting.tech/api/packages/MokoConsulting/composer
|
||||||
|
- name: Verify Gitea registry
|
||||||
|
run: |
|
||||||
|
echo "Gitea Composer registry auto-publishes from tags."
|
||||||
|
echo "Package available at: ${GITEA_URL}/api/packages/MokoConsulting/composer"
|
||||||
|
echo "Install: composer require mokoconsulting/mokocli"
|
||||||
|
|
||||||
|
# Packagist — notify of new version
|
||||||
|
- name: Notify Packagist
|
||||||
|
if: secrets.PACKAGIST_TOKEN != ''
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
echo "Notifying Packagist of version ${VERSION}..."
|
||||||
|
curl -sf -X POST \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"repository":{"url":"https://git.mokoconsulting.tech/MokoConsulting/mokocli"}}' \
|
||||||
|
"https://packagist.org/api/update-package?username=mokoconsulting&apiToken=${{ secrets.PACKAGIST_TOKEN }}" \
|
||||||
|
&& echo "Packagist notified" \
|
||||||
|
|| echo "::warning::Packagist notification failed (package may not be registered yet)"
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
echo "## Composer Package Published" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Registry | Status |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|----------|--------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Gitea | \`composer require mokoconsulting/mokocli:${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Packagist | \`composer require mokoconsulting/mokocli\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
# BRIEF: Build and deploy MokoGitea to dev environment on push to dev branch.
|
|
||||||
# Production deploy (deploy-mokogitea.yml) only succeeds if dev is healthy.
|
|
||||||
|
|
||||||
name: Deploy MokoGitea (Dev)
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: deploy-mokogitea-dev
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
env:
|
|
||||||
REGISTRY: git.mokoconsulting.tech
|
|
||||||
IMAGE: mokoconsulting/mokogitea
|
|
||||||
DEPLOY_HOST: git.mokoconsulting.tech
|
|
||||||
DEPLOY_PORT: 2918
|
|
||||||
DEPLOY_USER: mokoconsulting
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy-dev:
|
|
||||||
name: "Build & Deploy to Dev"
|
|
||||||
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 "dev-$(git rev-parse --short HEAD)")
|
|
||||||
echo "tag=${VERSION}-dev" >> $GITHUB_OUTPUT
|
|
||||||
echo "Version: ${VERSION}-dev"
|
|
||||||
|
|
||||||
- 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 dev via SSH
|
|
||||||
env:
|
|
||||||
REGISTRY_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
TAG: ${{ steps.config.outputs.tag }}
|
|
||||||
run: |
|
|
||||||
HEALTH_FMT='${{ '{{' }}.State.Health.Status${{ '}}' }}'
|
|
||||||
|
|
||||||
ssh -i ~/.ssh/deploy_key -p ${{ env.DEPLOY_PORT }} \
|
|
||||||
-o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
|
||||||
-o ServerAliveInterval=30 -o ServerAliveCountMax=10 \
|
|
||||||
${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} bash -s <<DEPLOY_EOF
|
|
||||||
set -e
|
|
||||||
echo 'SSH connected to dev environment'
|
|
||||||
|
|
||||||
echo 'Cleaning Docker build cache...'
|
|
||||||
docker builder prune -af 2>/dev/null || true
|
|
||||||
docker image prune -af 2>/dev/null || true
|
|
||||||
|
|
||||||
echo 'Pulling source...'
|
|
||||||
SOURCE_DIR=/opt/gitea-dev/source
|
|
||||||
if [ ! -d \$SOURCE_DIR/.git ]; then
|
|
||||||
git clone -b dev https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-Fork.git \$SOURCE_DIR
|
|
||||||
fi
|
|
||||||
cd \$SOURCE_DIR
|
|
||||||
git remote set-url origin https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-Fork.git 2>/dev/null || true
|
|
||||||
git fetch origin dev
|
|
||||||
git reset --hard origin/dev
|
|
||||||
|
|
||||||
echo 'Building Docker image...'
|
|
||||||
docker build --no-cache --build-arg GOFLAGS='-p 1' \
|
|
||||||
--tag ${{ env.REGISTRY }}/${{ env.IMAGE }}:\$TAG \
|
|
||||||
-f Dockerfile .
|
|
||||||
|
|
||||||
echo 'Pushing to registry...'
|
|
||||||
echo '\$REGISTRY_TOKEN' | docker login ${{ env.REGISTRY }} -u ${{ env.DEPLOY_USER }} --password-stdin
|
|
||||||
docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:\$TAG
|
|
||||||
|
|
||||||
echo 'Restarting dev container...'
|
|
||||||
cd /opt/gitea-dev
|
|
||||||
sed -i "s|${{ env.IMAGE }}:[^ ]*|${{ env.IMAGE }}:\$TAG|" docker-compose.yml
|
|
||||||
docker compose up -d mokogitea-dev
|
|
||||||
|
|
||||||
echo 'Health check...'
|
|
||||||
for i in 1 2 3 4 5 6 7 8; do
|
|
||||||
sleep 15
|
|
||||||
if docker inspect --format='\$HEALTH_FMT' mokogitea-dev 2>/dev/null | grep -q healthy; then
|
|
||||||
echo 'Dev container healthy!'
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
echo "Waiting... (attempt \$i/8)"
|
|
||||||
done
|
|
||||||
echo 'Health check failed'
|
|
||||||
docker logs mokogitea-dev --tail 20
|
|
||||||
exit 1
|
|
||||||
DEPLOY_EOF
|
|
||||||
|
|
||||||
- name: Verify dev instance
|
|
||||||
run: |
|
|
||||||
sleep 5
|
|
||||||
curl -sf https://git.dev.mokoconsulting.tech/api/healthz && echo " Dev API healthy"
|
|
||||||
@@ -42,10 +42,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup MokoStandards tools
|
- name: Setup MokoStandards tools
|
||||||
env:
|
env:
|
||||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }}
|
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }}
|
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
||||||
MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
|
||||||
run: |
|
run: |
|
||||||
git clone --depth 1 --branch main --quiet \
|
git clone --depth 1 --branch main --quiet \
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
||||||
|
|||||||
@@ -36,23 +36,7 @@ env:
|
|||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-dev:
|
|
||||||
name: "Verify dev environment is healthy"
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check dev health
|
|
||||||
run: |
|
|
||||||
echo "Checking git.dev.mokoconsulting.tech health..."
|
|
||||||
if curl -sf --max-time 10 https://git.dev.mokoconsulting.tech/api/healthz; then
|
|
||||||
echo " Dev environment is healthy — proceeding with production deploy"
|
|
||||||
else
|
|
||||||
echo "::error::Dev environment is NOT healthy — blocking production deploy"
|
|
||||||
echo "Deploy to dev first (push to dev branch) and verify it passes before merging to main."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
needs: check-dev
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source (for version detection)
|
- name: Checkout source (for version detection)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ permissions:
|
|||||||
issues: write
|
issues: write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
create-branch:
|
create-branch:
|
||||||
@@ -28,8 +28,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Create branch and comment
|
- name: Create branch and comment
|
||||||
run: |
|
run: |
|
||||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
TOKEN="${{ secrets.GA_TOKEN }}"
|
||||||
API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}"
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
ISSUE_NUM="${{ github.event.issue.number }}"
|
ISSUE_NUM="${{ github.event.issue.number }}"
|
||||||
ISSUE_TITLE="${{ github.event.issue.title }}"
|
ISSUE_TITLE="${{ github.event.issue.title }}"
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
echo "Created branch: ${BRANCH}"
|
echo "Created branch: ${BRANCH}"
|
||||||
|
|
||||||
# Comment on issue with branch link
|
# Comment on issue with branch link
|
||||||
REPO_URL="${MOKOGITEA_URL}/${{ github.repository }}"
|
REPO_URL="${GITEA_URL}/${{ github.repository }}"
|
||||||
BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`"
|
BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`"
|
||||||
|
|
||||||
curl -sf -X POST \
|
curl -sf -X POST \
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
name: Publish MCP to npm
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- '.mokogitea/mcp/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|
||||||
|
- name: Install and build
|
||||||
|
working-directory: .mokogitea/mcp
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
npx tsc
|
||||||
|
|
||||||
|
- name: Check version change
|
||||||
|
id: version
|
||||||
|
working-directory: .mokogitea/mcp
|
||||||
|
run: |
|
||||||
|
LOCAL_VERSION=$(node -p "require('./package.json').version")
|
||||||
|
NPM_VERSION=$(npm view @mokoconsulting/mokogitea-mcp version 2>/dev/null || echo "0.0.0")
|
||||||
|
if [ "$LOCAL_VERSION" != "$NPM_VERSION" ]; then
|
||||||
|
echo "changed=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Version changed: $NPM_VERSION -> $LOCAL_VERSION"
|
||||||
|
else
|
||||||
|
echo "changed=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "Version unchanged: $LOCAL_VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Publish to npm
|
||||||
|
if: steps.version.outputs.changed == 'true'
|
||||||
|
working-directory: .mokogitea/mcp
|
||||||
|
run: npm publish --access public
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
|
- name: Publish to Gitea registry
|
||||||
|
if: steps.version.outputs.changed == 'true'
|
||||||
|
working-directory: .mokogitea/mcp
|
||||||
|
run: |
|
||||||
|
npm publish --registry ${{ github.server_url }}/api/packages/${{ github.repository_owner }}/npm/ \
|
||||||
|
--//$(echo "${{ github.server_url }}" | sed 's|https://||')/api/packages/${{ github.repository_owner }}/npm/:_authToken=${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
@@ -496,26 +496,39 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Trigger RC pre-release
|
- name: Trigger RC pre-release
|
||||||
env:
|
env:
|
||||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
REPO: ${{ github.repository }}
|
REPO: ${{ github.repository }}
|
||||||
BRANCH: ${{ github.head_ref }}
|
BRANCH: ${{ github.head_ref }}
|
||||||
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
run: |
|
run: |
|
||||||
curl -s -X POST "${MOKOGITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${MOKOGITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
|
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
|
||||||
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
|
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
|
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
# ── Issue Reporter ──────────────────────────────────────────────────────
|
# ── Issue Reporter ──────────────────────────────────────────────────────
|
||||||
report-issues:
|
report-issues:
|
||||||
name: Report Issues
|
name: Report Issues
|
||||||
|
runs-on: ubuntu-latest
|
||||||
needs: [branch-policy, validate]
|
needs: [branch-policy, validate]
|
||||||
if: >-
|
if: >-
|
||||||
always() &&
|
always() &&
|
||||||
needs.validate.result == 'failure'
|
needs.validate.result == 'failure'
|
||||||
uses: ./.mokogitea/workflows/ci-issue-reporter.yml
|
|
||||||
with:
|
steps:
|
||||||
gate: "PR Validation"
|
- name: Checkout
|
||||||
workflow: "PR Check"
|
uses: actions/checkout@v4
|
||||||
severity: error
|
with:
|
||||||
details: "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed."
|
sparse-checkout: automation/ci-issue-reporter.sh
|
||||||
secrets: inherit
|
sparse-checkout-cone-mode: false
|
||||||
|
|
||||||
|
- name: "File issue for PR validation failure"
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
run: |
|
||||||
|
chmod +x automation/ci-issue-reporter.sh
|
||||||
|
./automation/ci-issue-reporter.sh \
|
||||||
|
--gate "PR Validation" \
|
||||||
|
--workflow "PR Check" \
|
||||||
|
--severity error \
|
||||||
|
--details "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed."
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ permissions:
|
|||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||||
|
|
||||||
@@ -88,20 +88,8 @@ jobs:
|
|||||||
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
|
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
|
||||||
php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
||||||
|
|
||||||
- name: Check platform eligibility (Joomla only)
|
|
||||||
id: eligibility
|
|
||||||
run: |
|
|
||||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
|
||||||
if [[ "$PLATFORM" == joomla* ]] || [[ "$PLATFORM" == "joomla" ]]; then
|
|
||||||
echo "proceed=true" >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
|
||||||
echo "proceed=false" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "::notice::Platform '$PLATFORM' — non-Joomla, skipping pre-release auto-bump"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Resolve metadata and bump version
|
- name: Resolve metadata and bump version
|
||||||
id: meta
|
id: meta
|
||||||
if: steps.eligibility.outputs.proceed == 'true'
|
|
||||||
run: |
|
run: |
|
||||||
# Auto-detect stability from branch name on push, or use input on dispatch
|
# Auto-detect stability from branch name on push, or use input on dispatch
|
||||||
if [ "${{ github.event_name }}" = "push" ]; then
|
if [ "${{ github.event_name }}" = "push" ]; then
|
||||||
@@ -178,22 +166,20 @@ jobs:
|
|||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
id: release
|
id: release
|
||||||
if: steps.eligibility.outputs.proceed == 'true'
|
|
||||||
run: |
|
run: |
|
||||||
TAG="${{ steps.meta.outputs.tag }}"
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
php ${MOKO_CLI}/release_create.php \
|
php ${MOKO_CLI}/release_create.php \
|
||||||
--path . --version "$VERSION" --tag "$TAG" \
|
--path . --version "$VERSION" --tag "$TAG" \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
|
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
|
||||||
|
|
||||||
- name: Update release notes from CHANGELOG.md
|
- name: Update release notes from CHANGELOG.md
|
||||||
if: steps.eligibility.outputs.proceed == 'true'
|
|
||||||
run: |
|
run: |
|
||||||
TAG="${{ steps.meta.outputs.tag }}"
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
|
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
|
||||||
if [ -f "CHANGELOG.md" ]; then
|
if [ -f "CHANGELOG.md" ]; then
|
||||||
@@ -226,11 +212,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Build package and upload
|
- name: Build package and upload
|
||||||
id: package
|
id: package
|
||||||
if: steps.eligibility.outputs.proceed == 'true'
|
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
TAG="${{ steps.meta.outputs.tag }}"
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
php ${MOKO_CLI}/release_package.php \
|
php ${MOKO_CLI}/release_package.php \
|
||||||
--path . --version "$VERSION" --tag "$TAG" \
|
--path . --version "$VERSION" --tag "$TAG" \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
@@ -240,10 +225,9 @@ jobs:
|
|||||||
# No need to build, commit, or sync updates.xml from workflows
|
# No need to build, commit, or sync updates.xml from workflows
|
||||||
|
|
||||||
- name: "Delete lesser pre-release channels (cascade)"
|
- name: "Delete lesser pre-release channels (cascade)"
|
||||||
if: steps.eligibility.outputs.proceed == 'true'
|
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
php ${MOKO_CLI}/release_cascade.php \
|
php ${MOKO_CLI}/release_cascade.php \
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ jobs:
|
|||||||
- name: Check actor permission (admin only)
|
- name: Check actor permission (admin only)
|
||||||
id: perm
|
id: perm
|
||||||
env:
|
env:
|
||||||
TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }}
|
TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
|
||||||
REPO: ${{ github.repository }}
|
REPO: ${{ github.repository }}
|
||||||
ACTOR: ${{ github.actor }}
|
ACTOR: ${{ github.actor }}
|
||||||
run: |
|
run: |
|
||||||
@@ -671,30 +671,42 @@ jobs:
|
|||||||
# ═══════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════
|
||||||
# Issue Reporter — file issues for failed gates
|
# Issue Reporter — file issues for failed gates
|
||||||
# ═══════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════
|
||||||
report-scripts:
|
report-issues:
|
||||||
name: "Report: Scripts Governance"
|
name: "Report Issues"
|
||||||
needs: [access_check, scripts_governance]
|
runs-on: ubuntu-latest
|
||||||
|
needs: [access_check, scripts_governance, repo_health]
|
||||||
if: >-
|
if: >-
|
||||||
always() &&
|
always() &&
|
||||||
needs.scripts_governance.result == 'failure'
|
(needs.scripts_governance.result == 'failure' ||
|
||||||
uses: ./.mokogitea/workflows/ci-issue-reporter.yml
|
needs.repo_health.result == 'failure')
|
||||||
with:
|
|
||||||
gate: "Scripts Governance"
|
|
||||||
workflow: "Repo Health"
|
|
||||||
severity: error
|
|
||||||
details: "Scripts directory policy violations detected. Review required and allowed directories."
|
|
||||||
secrets: inherit
|
|
||||||
|
|
||||||
report-health:
|
steps:
|
||||||
name: "Report: Repository Health"
|
- name: Checkout
|
||||||
needs: [access_check, repo_health]
|
uses: actions/checkout@v4
|
||||||
if: >-
|
with:
|
||||||
always() &&
|
sparse-checkout: automation/ci-issue-reporter.sh
|
||||||
needs.repo_health.result == 'failure'
|
sparse-checkout-cone-mode: false
|
||||||
uses: ./.mokogitea/workflows/ci-issue-reporter.yml
|
|
||||||
with:
|
- name: "File issues for failed gates"
|
||||||
gate: "Repository Health"
|
env:
|
||||||
workflow: "Repo Health"
|
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
severity: error
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
details: "Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary."
|
run: |
|
||||||
secrets: inherit
|
chmod +x automation/ci-issue-reporter.sh
|
||||||
|
REPORTER="./automation/ci-issue-reporter.sh"
|
||||||
|
WF="Repo Health"
|
||||||
|
|
||||||
|
report_gate() {
|
||||||
|
local gate="$1" result="$2" details="$3"
|
||||||
|
if [ "$result" = "failure" ]; then
|
||||||
|
"$REPORTER" --gate "$gate" --details "$details" --workflow "$WF" --severity error
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
report_gate "Scripts Governance" \
|
||||||
|
"${{ needs.scripts_governance.result }}" \
|
||||||
|
"Scripts directory policy violations detected. Review required and allowed directories."
|
||||||
|
|
||||||
|
report_gate "Repository Health" \
|
||||||
|
"${{ needs.repo_health.result }}" \
|
||||||
|
"Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary."
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: MokoStandards.Security
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||||
|
# PATH: /.gitea/workflows/security-audit.yml
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: Dependency vulnerability scanning for composer and npm packages
|
||||||
|
|
||||||
|
name: "Universal: Security Audit"
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 6 * * 1' # Weekly on Monday at 06:00 UTC
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'composer.json'
|
||||||
|
- 'composer.lock'
|
||||||
|
- 'package.json'
|
||||||
|
- 'package-lock.json'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
||||||
|
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
audit:
|
||||||
|
name: Dependency Audit
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Composer audit
|
||||||
|
if: hashFiles('composer.lock') != ''
|
||||||
|
run: |
|
||||||
|
echo "=== Composer Security Audit ==="
|
||||||
|
if ! command -v composer &> /dev/null; then
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y -qq php-cli composer >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
composer audit --format=plain 2>&1 | tee /tmp/composer-audit.txt
|
||||||
|
RESULT=$?
|
||||||
|
if [ $RESULT -ne 0 ]; then
|
||||||
|
echo "::warning::Composer vulnerabilities found"
|
||||||
|
echo "composer_vulnerable=true" >> "$GITHUB_ENV"
|
||||||
|
else
|
||||||
|
echo "No known vulnerabilities in composer dependencies"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: NPM audit
|
||||||
|
if: hashFiles('package-lock.json') != ''
|
||||||
|
run: |
|
||||||
|
echo "=== NPM Security Audit ==="
|
||||||
|
npm audit --production 2>&1 | tee /tmp/npm-audit.txt || true
|
||||||
|
if npm audit --production 2>&1 | grep -q "found 0 vulnerabilities"; then
|
||||||
|
echo "No known vulnerabilities in npm dependencies"
|
||||||
|
else
|
||||||
|
echo "::warning::NPM vulnerabilities found"
|
||||||
|
echo "npm_vulnerable=true" >> "$GITHUB_ENV"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Notify on vulnerabilities
|
||||||
|
if: env.composer_vulnerable == 'true' || env.npm_vulnerable == 'true'
|
||||||
|
run: |
|
||||||
|
REPO="${{ github.event.repository.name }}"
|
||||||
|
curl -sS \
|
||||||
|
-H "Title: ${REPO} has vulnerable dependencies" \
|
||||||
|
-H "Tags: lock,warning" \
|
||||||
|
-H "Priority: high" \
|
||||||
|
-d "Security audit found vulnerabilities. Review dependency updates." \
|
||||||
|
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow.Template
|
|
||||||
# INGROUP: MokoStandards.CI
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
|
|
||||||
# PATH: /.mokogitea/workflows/version-set.yml
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Set or reset the extension version across all version-bearing files
|
|
||||||
|
|
||||||
name: "Joomla: Set Version"
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
version:
|
|
||||||
description: "Version number (e.g. 01.00.00)"
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
branch:
|
|
||||||
description: "Branch to update (default: current)"
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
set-version:
|
|
||||||
name: Set Version to ${{ inputs.version }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Validate version format
|
|
||||||
run: |
|
|
||||||
VERSION="${{ inputs.version }}"
|
|
||||||
if ! echo "$VERSION" | grep -qP '^\d{2}\.\d{2}\.\d{2}$'; then
|
|
||||||
echo "::error::Invalid version format '${VERSION}' — expected XX.YY.ZZ (e.g. 01.00.00)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "VERSION=${VERSION}" >> "$GITHUB_ENV"
|
|
||||||
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN || github.token }}
|
|
||||||
ref: ${{ inputs.branch || github.ref }}
|
|
||||||
fetch-depth: 1
|
|
||||||
|
|
||||||
- name: Update manifest version
|
|
||||||
run: |
|
|
||||||
MANIFEST=""
|
|
||||||
for XML_FILE in $(find . -maxdepth 3 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
|
|
||||||
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
|
|
||||||
MANIFEST="$XML_FILE"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ -z "$MANIFEST" ]; then
|
|
||||||
echo "::warning::No Joomla extension manifest found — skipping manifest update"
|
|
||||||
else
|
|
||||||
OLD_VER=$(grep -oP '<version>\K[^<]+' "$MANIFEST" | head -1)
|
|
||||||
sed -i "s|<version>${OLD_VER}</version>|<version>${VERSION}</version>|" "$MANIFEST"
|
|
||||||
echo "Manifest: ${OLD_VER} → ${VERSION} (${MANIFEST})"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Update README.md version
|
|
||||||
run: |
|
|
||||||
if [ -f "README.md" ]; then
|
|
||||||
if grep -qP '^\s*VERSION:\s*\d' README.md; then
|
|
||||||
sed -i -E "s/(VERSION:\s*)[0-9]{2}\.[0-9]{2}\.[0-9]{2}/\1${VERSION}/" README.md
|
|
||||||
echo "README.md version updated to ${VERSION}"
|
|
||||||
else
|
|
||||||
echo "::warning::No VERSION line found in README.md — skipping"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Update CHANGELOG.md
|
|
||||||
run: |
|
|
||||||
if [ -f "CHANGELOG.md" ]; then
|
|
||||||
DATE=$(date +%Y-%m-%d)
|
|
||||||
# Check if this version already has an entry
|
|
||||||
if grep -q "^\#\# \[${VERSION}\]" CHANGELOG.md; then
|
|
||||||
echo "CHANGELOG.md already has entry for ${VERSION} — skipping"
|
|
||||||
else
|
|
||||||
# Insert new version entry after [Unreleased] or at the top after header
|
|
||||||
if grep -q '^\#\# \[Unreleased\]' CHANGELOG.md; then
|
|
||||||
sed -i "/^\#\# \[Unreleased\]/a\\\\n## [${VERSION}] --- ${DATE}" CHANGELOG.md
|
|
||||||
else
|
|
||||||
sed -i "/^\# Changelog/a\\\\n## [Unreleased]\n\n## [${VERSION}] --- ${DATE}" CHANGELOG.md
|
|
||||||
fi
|
|
||||||
echo "CHANGELOG.md: added entry for ${VERSION}"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "::warning::No CHANGELOG.md found — skipping"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Update FILE INFORMATION blocks
|
|
||||||
run: |
|
|
||||||
# Update VERSION in file header blocks (# VERSION: XX.YY.ZZ)
|
|
||||||
find . -maxdepth 1 -type f \( -name "*.yml" -o -name "*.yaml" -o -name "*.php" -o -name "*.md" \) \
|
|
||||||
-not -path "./.git/*" -not -path "./vendor/*" -print0 2>/dev/null | \
|
|
||||||
while IFS= read -r -d '' FILE; do
|
|
||||||
if head -20 "$FILE" | grep -qP '^\s*#?\s*VERSION:\s*\d{2}\.\d{2}\.\d{2}'; then
|
|
||||||
sed -i -E "s/(#?\s*VERSION:\s*)[0-9]{2}\.[0-9]{2}\.[0-9]{2}/\1${VERSION}/" "$FILE"
|
|
||||||
echo "Updated FILE INFORMATION VERSION in ${FILE}"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Commit and push
|
|
||||||
run: |
|
|
||||||
git config user.name "Moko Consulting [bot]"
|
|
||||||
git config user.email "hello@mokoconsulting.tech"
|
|
||||||
git add -A
|
|
||||||
if git diff --cached --quiet; then
|
|
||||||
echo "No version changes detected — nothing to commit"
|
|
||||||
else
|
|
||||||
git commit -m "chore: set version to ${VERSION} [skip bump]
|
|
||||||
|
|
||||||
Authored-by: Moko Consulting"
|
|
||||||
git push
|
|
||||||
echo "### Version Set" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "Version updated to \`${VERSION}\` on branch \`${GITHUB_REF_NAME}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
@@ -13,7 +13,6 @@
|
|||||||
name: "Universal: Workflow Sync Trigger"
|
name: "Universal: Workflow Sync Trigger"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [closed]
|
types: [closed]
|
||||||
branches:
|
branches:
|
||||||
@@ -27,9 +26,8 @@ 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' ||
|
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
|
||||||
@@ -51,14 +49,8 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
|
GITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
|
||||||
git clone --depth 1 "${MOKOGITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
|
git clone --depth 1 "${GITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
|
||||||
|
|
||||||
- name: Install PHP
|
|
||||||
run: |
|
|
||||||
if ! command -v php &> /dev/null; then
|
|
||||||
apt-get update -qq && apt-get install -y -qq php-cli php-json php-curl > /dev/null 2>&1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -3,19 +3,6 @@
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- API token scope `read:licensing` / `write:licensing` for licensing endpoints (#697)
|
|
||||||
- Edit API token scopes: PATCH /users/{username}/tokens/{id} API endpoint + web UI edit button (#697)
|
|
||||||
- Wiki full-text search: case-insensitive search across all wiki page titles and content (#550)
|
|
||||||
- Wiki search API: GET /wiki/search?q=term with paginated JSON results (#550)
|
|
||||||
- Metadata deploy fields: deploy_host, deploy_port, deploy_user, deploy_path, docker_image, docker_registry, container_name, health_url (#692)
|
|
||||||
- Metadata API partial updates: PUT /metadata now merges only sent fields instead of replacing all
|
|
||||||
- Wiki revision diff: line-by-line diff view per commit in wiki page history (#667)
|
|
||||||
- Wiki categories: YAML frontmatter `categories:` with category index page (#668)
|
|
||||||
- Wiki template transclusion: `{{template:Name|key=val}}` with `_Template/` folder (#671)
|
|
||||||
- Wiki enhanced ToC: collapsible, inline via frontmatter, sticky sidebar (#673)
|
|
||||||
- Wiki folder ACL: `_access.yml` per-folder write protection (#674)
|
|
||||||
- Wiki print view and ZIP export of all wiki pages (#675)
|
|
||||||
- Wiki features documentation page in org wiki (standards/Wiki-Features)
|
|
||||||
- DLID licensing system: license, entitlement, activation, product_tier, audit_log tables (v359 migration)
|
- DLID licensing system: license, entitlement, activation, product_tier, audit_log tables (v359 migration)
|
||||||
- License CRUD with CRC32-checksummed DLID generation and format validation
|
- License CRUD with CRC32-checksummed DLID generation and format validation
|
||||||
- Entitlement model with tier-based rebuild and custom entitlement preservation
|
- Entitlement model with tier-based rebuild and custom entitlement preservation
|
||||||
@@ -23,37 +10,6 @@
|
|||||||
- 13 seeded product tiers from base to enterprise
|
- 13 seeded product tiers from base to enterprise
|
||||||
- DLID-gated update XML endpoint: GET /api/v1/licensing/updates/{product}.xml
|
- DLID-gated update XML endpoint: GET /api/v1/licensing/updates/{product}.xml
|
||||||
- Profile repo fallback chain: .mokogitea > .profile > .github
|
- Profile repo fallback chain: .mokogitea > .profile > .github
|
||||||
- Metadata/manifest GET endpoint publicly accessible without auth (#676)
|
|
||||||
- Org wiki: folder-based collapsible tree sidebar, _Sidebar.md overrides (#680)
|
|
||||||
- Wiki backlinks: "What links here" page showing all pages referencing current page (#669)
|
|
||||||
- Wiki wikilinks: [[Page Name]] and [[Page|Display Text]] syntax with red links for missing pages (#666)
|
|
||||||
- Required baseline issue statuses: Open and Closed are indestructible (is_required flag) (#681)
|
|
||||||
- Issue status API response includes is_required field
|
|
||||||
- Wiki recent changes page: cross-page edit activity with pagination (#670)
|
|
||||||
- Wiki page rename with automatic redirects via YAML frontmatter (#672)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- API token edit: reject empty scope update requests with 400 instead of silently succeeding
|
|
||||||
- Workflow token auth: pr-check.yml pre-release dispatch was silently failing due to env var / curl reference mismatch
|
|
||||||
- Workflow tokens: standardize all GA_TOKEN/GITEA_TOKEN/GITEA_URL env vars to MOKOGITEA_TOKEN/MOKOGITEA_URL across all workflow files in 5 template repos + MokoCLI (65+ files)
|
|
||||||
- CI issue reporter: rename GITEA_TOKEN/GITEA_URL to MOKOGITEA_TOKEN/MOKOGITEA_URL in automation/ci-issue-reporter.sh
|
|
||||||
- Workflow sync trigger: add workflow_dispatch event, fix if-condition to allow manual dispatch, add PHP install step for non-PHP runners
|
|
||||||
- Licensing API: handle DB write errors in UpdateLicense, UpdateTier, DeleteTier instead of silently discarding
|
|
||||||
- Wiki API: fix findEntryForFile URL-decode fallback for non-ASCII page names
|
|
||||||
- Metadata settings template 500 error: removed reference to deleted Version field
|
|
||||||
- Wiki recent changes: use commit.MessageTitle() instead of commit.Message()
|
|
||||||
- Wiki backlinks: proper URL encoding for subdirectory pages
|
|
||||||
- Wiki wikilinks: page existence lookup normalizes spaces and hyphens
|
|
||||||
- Issue statuses template: garbled em-dash character replaced
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Issue status seed defaults: Open, In Progress, Waiting, In Review, Closed, Won't Fix
|
|
||||||
- Pre-release workflow: auto-bump skipped for non-Joomla repos (platform check)
|
|
||||||
- CI issue reporter: moved to MokoCLI (cli/ci_issue_reporter.sh), pr-check and repo-health now use ci-issue-reporter.yml reusable workflow
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
- Workflows: gitleaks.yml, npm-publish.yml, notify.yml, workflow-sync-trigger.yml, composer-publish.yml, deploy-manual.yml, security-audit.yml (not applicable to Go repo)
|
|
||||||
- automation/ci-issue-reporter.sh: moved to MokoCLI as centralized CLI tool
|
|
||||||
|
|
||||||
## [06.19.00] --- 2026-06-20
|
## [06.19.00] --- 2026-06-20
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,38 @@
|
|||||||
# MokoGitea
|
# MokoGitea
|
||||||
|
|
||||||
Custom Gitea fork with enhanced wiki system, DLID licensing, issue statuses, org metadata, and project board API.
|
Moko fork of Gitea — adding project board REST API endpoints and custom enhancements
|
||||||
|
|
||||||
 
|
  
|
||||||
|
|
||||||
|
|
||||||
|
Custom Gitea fork with Project Board API
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Key Features
|
## Pages
|
||||||
|
|
||||||
- **Wiki System** -- wikilinks, categories, backlinks, template transclusion, revision diffs, rename redirects, folder ACL, enhanced ToC, print view, ZIP export ([details](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki/standards/Wiki-Features))
|
- [Branding](https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/wiki/Branding)
|
||||||
- **DLID Licensing** -- license management, entitlements, domain activations, ed25519-signed downloads
|
- [Deployment](https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/wiki/Deployment)
|
||||||
- **API Token Scope Editing** -- edit token scopes via API (PATCH) or web UI after creation
|
- [Project API](Project API)
|
||||||
- **Issue Statuses** -- custom workflow statuses per org with required baseline protection
|
- [roadmap](https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/wiki/roadmap)
|
||||||
- **Org Metadata** -- per-repo metadata API (public GET, admin PUT), platform detection for versioning
|
|
||||||
- **Project Board API** -- REST endpoints for project columns and cards
|
---
|
||||||
- **Dev Deploy Gate** -- builds deploy to dev environment first, production checks dev health
|
|
||||||
|
**Category:** Infrastructure | **Platform:** [MokoPlatform wiki](https://code.mokoconsulting.tech/MokoConsulting/MokoPlatform/wiki)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- [Org Wiki](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki/) -- standards, CLI reference, API docs
|
Full documentation is available on the [Wiki](https://code.mokoconsulting.tech/MokoConsulting/MokoGitea/wiki).
|
||||||
- [Wiki Features](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki/standards/Wiki-Features) -- all 10 wiki enhancements
|
|
||||||
- [Licensing API](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki/api/Licensing-API)
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
See the [org wiki](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki/) for development guidelines, coding standards, and contribution instructions.
|
See the wiki for development guidelines and contribution instructions.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
@@ -32,4 +40,4 @@ This project is licensed under the GNU General Public License v3.0 or later -- s
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*[Moko Consulting](https://mokoconsulting.tech)*
|
*[Moko Consulting](https://mokoconsulting.tech) -- [MokoStandards](https://code.mokoconsulting.tech/MokoConsulting/MokoPlatform/wiki/Home)*
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ============================================================================
|
||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Automation.CI
|
||||||
|
# INGROUP: moko-platform.Automation
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
|
# PATH: /automation/ci-issue-reporter.sh
|
||||||
|
# VERSION: 09.23.00
|
||||||
|
# BRIEF: Creates or updates a Gitea issue when a CI gate fails.
|
||||||
|
# Deduplicates by searching open issues with the "ci-auto" label
|
||||||
|
# whose title matches the gate. If a matching issue exists, a comment
|
||||||
|
# is appended instead of opening a duplicate.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ── Defaults ────────────────────────────────────────────────────────────────
|
||||||
|
GITEA_URL="${GITEA_URL:-https://git.mokoconsulting.tech}"
|
||||||
|
GITEA_TOKEN="${GITEA_TOKEN:-}"
|
||||||
|
REPO="${GITHUB_REPOSITORY:-}"
|
||||||
|
RUN_URL="${GITHUB_SERVER_URL:-${GITEA_URL}}/${REPO}/actions/runs/${GITHUB_RUN_ID:-0}"
|
||||||
|
LABEL_NAME="ci-auto"
|
||||||
|
LABEL_COLOR="#e11d48"
|
||||||
|
|
||||||
|
GATE=""
|
||||||
|
DETAILS=""
|
||||||
|
SEVERITY="error"
|
||||||
|
WORKFLOW=""
|
||||||
|
|
||||||
|
# ── Parse arguments ─────────────────────────────────────────────────────────
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: ci-issue-reporter.sh --gate NAME --details TEXT [OPTIONS]
|
||||||
|
|
||||||
|
Required:
|
||||||
|
--gate CI gate name (e.g. "Code Quality", "Self-Health")
|
||||||
|
--details Human-readable failure description
|
||||||
|
|
||||||
|
Optional:
|
||||||
|
--severity "error" (default) or "warning"
|
||||||
|
--workflow Workflow name for the issue title
|
||||||
|
--repo owner/repo (default: \$GITHUB_REPOSITORY)
|
||||||
|
--run-url URL to the CI run (auto-detected from env)
|
||||||
|
--token Gitea API token (default: \$GITEA_TOKEN)
|
||||||
|
--url Gitea base URL (default: \$GITEA_URL)
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--gate) GATE="$2"; shift 2 ;;
|
||||||
|
--details) DETAILS="$2"; shift 2 ;;
|
||||||
|
--severity) SEVERITY="$2"; shift 2 ;;
|
||||||
|
--workflow) WORKFLOW="$2"; shift 2 ;;
|
||||||
|
--repo) REPO="$2"; shift 2 ;;
|
||||||
|
--run-url) RUN_URL="$2"; shift 2 ;;
|
||||||
|
--token) GITEA_TOKEN="$2"; shift 2 ;;
|
||||||
|
--url) GITEA_URL="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage ;;
|
||||||
|
*) echo "Unknown option: $1"; usage ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -z "$GATE" ]] && { echo "ERROR: --gate is required"; usage; }
|
||||||
|
[[ -z "$DETAILS" ]] && { echo "ERROR: --details is required"; usage; }
|
||||||
|
[[ -z "$GITEA_TOKEN" ]] && { echo "ERROR: GITEA_TOKEN not set"; exit 1; }
|
||||||
|
[[ -z "$REPO" ]] && { echo "ERROR: GITHUB_REPOSITORY not set"; exit 1; }
|
||||||
|
|
||||||
|
API="${GITEA_URL}/api/v1/repos/${REPO}"
|
||||||
|
|
||||||
|
# ── Build title ─────────────────────────────────────────────────────────────
|
||||||
|
if [[ -n "$WORKFLOW" ]]; then
|
||||||
|
TITLE="[CI] ${WORKFLOW}: ${GATE} failed"
|
||||||
|
else
|
||||||
|
TITLE="[CI] ${GATE} failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Ensure label exists ─────────────────────────────────────────────────────
|
||||||
|
ensure_label() {
|
||||||
|
local exists
|
||||||
|
exists=$(curl -sf -o /dev/null -w '%{http_code}' \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API}/labels" 2>/dev/null || echo "000")
|
||||||
|
|
||||||
|
if [[ "$exists" == "200" ]]; then
|
||||||
|
# Check if label already exists
|
||||||
|
local found
|
||||||
|
found=$(curl -sf \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API}/labels" 2>/dev/null \
|
||||||
|
| grep -o "\"name\":\"${LABEL_NAME}\"" || true)
|
||||||
|
|
||||||
|
if [[ -z "$found" ]]; then
|
||||||
|
curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/labels" \
|
||||||
|
-d "{\"name\":\"${LABEL_NAME}\",\"color\":\"${LABEL_COLOR}\",\"description\":\"Auto-created by CI issue reporter\"}" \
|
||||||
|
> /dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Search for existing open issue ──────────────────────────────────────────
|
||||||
|
find_existing_issue() {
|
||||||
|
# URL-encode the gate name for the query
|
||||||
|
local query
|
||||||
|
query=$(printf '%s' "[CI] ${GATE}" | sed 's/ /%20/g; s/\[/%5B/g; s/\]/%5D/g')
|
||||||
|
|
||||||
|
local response
|
||||||
|
response=$(curl -sf \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API}/issues?type=issues&state=open&labels=${LABEL_NAME}&q=${query}&limit=5" \
|
||||||
|
2>/dev/null || echo "[]")
|
||||||
|
|
||||||
|
# Extract the first matching issue number
|
||||||
|
echo "$response" \
|
||||||
|
| grep -oP '"number":\s*\K[0-9]+' \
|
||||||
|
| head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Build issue body ────────────────────────────────────────────────────────
|
||||||
|
build_body() {
|
||||||
|
local severity_badge
|
||||||
|
if [[ "$SEVERITY" == "error" ]]; then
|
||||||
|
severity_badge="**Severity:** Error"
|
||||||
|
else
|
||||||
|
severity_badge="**Severity:** Warning"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<BODY
|
||||||
|
## CI Gate Failure: ${GATE}
|
||||||
|
|
||||||
|
${severity_badge}
|
||||||
|
**Workflow:** ${WORKFLOW:-unknown}
|
||||||
|
**Branch:** ${GITHUB_REF_NAME:-unknown}
|
||||||
|
**Commit:** \`${GITHUB_SHA:0:8}\`
|
||||||
|
**Run:** [View CI run](${RUN_URL})
|
||||||
|
|
||||||
|
### Details
|
||||||
|
|
||||||
|
${DETAILS}
|
||||||
|
|
||||||
|
### Resolution
|
||||||
|
|
||||||
|
Fix the issue described above and push a new commit. This issue will be closed automatically when the gate passes, or can be closed manually.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Auto-created by [ci-issue-reporter](${GITEA_URL}/${REPO}/src/branch/main/automation/ci-issue-reporter.sh)*
|
||||||
|
BODY
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Build comment body (for existing issues) ────────────────────────────────
|
||||||
|
build_comment() {
|
||||||
|
cat <<COMMENT
|
||||||
|
### CI failure recurrence
|
||||||
|
|
||||||
|
**Branch:** ${GITHUB_REF_NAME:-unknown}
|
||||||
|
**Commit:** \`${GITHUB_SHA:0:8}\`
|
||||||
|
**Run:** [View CI run](${RUN_URL})
|
||||||
|
|
||||||
|
${DETAILS}
|
||||||
|
COMMENT
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Main ────────────────────────────────────────────────────────────────────
|
||||||
|
ensure_label
|
||||||
|
|
||||||
|
EXISTING=$(find_existing_issue)
|
||||||
|
|
||||||
|
if [[ -n "$EXISTING" ]]; then
|
||||||
|
# Append comment to existing issue
|
||||||
|
COMMENT_BODY=$(build_comment)
|
||||||
|
COMMENT_JSON=$(printf '%s' "$COMMENT_BODY" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
print(json.dumps({'body': sys.stdin.read()}))" 2>/dev/null)
|
||||||
|
|
||||||
|
HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/issues/${EXISTING}/comments" \
|
||||||
|
-d "${COMMENT_JSON}" 2>/dev/null || echo "000")
|
||||||
|
|
||||||
|
if [[ "$HTTP" == "201" ]]; then
|
||||||
|
echo "Commented on existing issue #${EXISTING}"
|
||||||
|
else
|
||||||
|
echo "WARNING: Failed to comment on issue #${EXISTING} (HTTP ${HTTP})"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Create new issue
|
||||||
|
ISSUE_BODY=$(build_body)
|
||||||
|
ISSUE_JSON=$(python3 -c "
|
||||||
|
import sys, json
|
||||||
|
body = sys.stdin.read()
|
||||||
|
print(json.dumps({
|
||||||
|
'title': sys.argv[1],
|
||||||
|
'body': body,
|
||||||
|
'labels': []
|
||||||
|
}))" "$TITLE" <<< "$ISSUE_BODY" 2>/dev/null)
|
||||||
|
|
||||||
|
# Create the issue
|
||||||
|
RESPONSE=$(curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/issues" \
|
||||||
|
-d "${ISSUE_JSON}" 2>/dev/null || echo "{}")
|
||||||
|
|
||||||
|
ISSUE_NUM=$(echo "$RESPONSE" | grep -oP '"number":\s*\K[0-9]+' | head -1)
|
||||||
|
|
||||||
|
if [[ -n "$ISSUE_NUM" ]]; then
|
||||||
|
# Apply label (separate call — more reliable across Gitea versions)
|
||||||
|
LABEL_ID=$(curl -sf \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API}/labels" 2>/dev/null \
|
||||||
|
| grep -oP "\"id\":\s*\K[0-9]+(?=[^}]*\"name\":\s*\"${LABEL_NAME}\")" \
|
||||||
|
| head -1 || true)
|
||||||
|
|
||||||
|
if [[ -n "$LABEL_ID" ]]; then
|
||||||
|
curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/issues/${ISSUE_NUM}/labels" \
|
||||||
|
-d "{\"labels\":[${LABEL_ID}]}" \
|
||||||
|
> /dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Created issue #${ISSUE_NUM}: ${TITLE}"
|
||||||
|
else
|
||||||
|
echo "WARNING: Failed to create issue"
|
||||||
|
echo "Response: ${RESPONSE}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
@@ -24,7 +24,6 @@ const (
|
|||||||
AccessTokenScopeCategoryIssue
|
AccessTokenScopeCategoryIssue
|
||||||
AccessTokenScopeCategoryRepository
|
AccessTokenScopeCategoryRepository
|
||||||
AccessTokenScopeCategoryUser
|
AccessTokenScopeCategoryUser
|
||||||
AccessTokenScopeCategoryLicensing
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// AllAccessTokenScopeCategories contains all access token scope categories
|
// AllAccessTokenScopeCategories contains all access token scope categories
|
||||||
@@ -38,7 +37,6 @@ var AllAccessTokenScopeCategories = []AccessTokenScopeCategory{
|
|||||||
AccessTokenScopeCategoryIssue,
|
AccessTokenScopeCategoryIssue,
|
||||||
AccessTokenScopeCategoryRepository,
|
AccessTokenScopeCategoryRepository,
|
||||||
AccessTokenScopeCategoryUser,
|
AccessTokenScopeCategoryUser,
|
||||||
AccessTokenScopeCategoryLicensing,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccessTokenScopeLevel represents the access levels without a given scope category
|
// AccessTokenScopeLevel represents the access levels without a given scope category
|
||||||
@@ -84,9 +82,6 @@ const (
|
|||||||
|
|
||||||
AccessTokenScopeReadUser AccessTokenScope = "read:user"
|
AccessTokenScopeReadUser AccessTokenScope = "read:user"
|
||||||
AccessTokenScopeWriteUser AccessTokenScope = "write:user"
|
AccessTokenScopeWriteUser AccessTokenScope = "write:user"
|
||||||
|
|
||||||
AccessTokenScopeReadLicensing AccessTokenScope = "read:licensing"
|
|
||||||
AccessTokenScopeWriteLicensing AccessTokenScope = "write:licensing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// accessTokenScopeBitmap represents a bitmap of access token scopes.
|
// accessTokenScopeBitmap represents a bitmap of access token scopes.
|
||||||
@@ -98,8 +93,7 @@ const (
|
|||||||
accessTokenScopeAllBits accessTokenScopeBitmap = accessTokenScopeWriteActivityPubBits |
|
accessTokenScopeAllBits accessTokenScopeBitmap = accessTokenScopeWriteActivityPubBits |
|
||||||
accessTokenScopeWriteAdminBits | accessTokenScopeWriteMiscBits | accessTokenScopeWriteNotificationBits |
|
accessTokenScopeWriteAdminBits | accessTokenScopeWriteMiscBits | accessTokenScopeWriteNotificationBits |
|
||||||
accessTokenScopeWriteOrganizationBits | accessTokenScopeWritePackageBits | accessTokenScopeWriteIssueBits |
|
accessTokenScopeWriteOrganizationBits | accessTokenScopeWritePackageBits | accessTokenScopeWriteIssueBits |
|
||||||
accessTokenScopeWriteRepositoryBits | accessTokenScopeWriteUserBits |
|
accessTokenScopeWriteRepositoryBits | accessTokenScopeWriteUserBits
|
||||||
accessTokenScopeWriteLicensingBits
|
|
||||||
|
|
||||||
accessTokenScopePublicOnlyBits accessTokenScopeBitmap = 1 << iota
|
accessTokenScopePublicOnlyBits accessTokenScopeBitmap = 1 << iota
|
||||||
|
|
||||||
@@ -130,9 +124,6 @@ const (
|
|||||||
accessTokenScopeReadUserBits accessTokenScopeBitmap = 1 << iota
|
accessTokenScopeReadUserBits accessTokenScopeBitmap = 1 << iota
|
||||||
accessTokenScopeWriteUserBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadUserBits
|
accessTokenScopeWriteUserBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadUserBits
|
||||||
|
|
||||||
accessTokenScopeReadLicensingBits accessTokenScopeBitmap = 1 << iota
|
|
||||||
accessTokenScopeWriteLicensingBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadLicensingBits
|
|
||||||
|
|
||||||
// The current implementation only supports up to 64 token scopes.
|
// The current implementation only supports up to 64 token scopes.
|
||||||
// If we need to support > 64 scopes,
|
// If we need to support > 64 scopes,
|
||||||
// refactoring the whole implementation in this file (and only this file) is needed.
|
// refactoring the whole implementation in this file (and only this file) is needed.
|
||||||
@@ -151,7 +142,6 @@ var allAccessTokenScopes = []AccessTokenScope{
|
|||||||
AccessTokenScopeWriteIssue, AccessTokenScopeReadIssue,
|
AccessTokenScopeWriteIssue, AccessTokenScopeReadIssue,
|
||||||
AccessTokenScopeWriteRepository, AccessTokenScopeReadRepository,
|
AccessTokenScopeWriteRepository, AccessTokenScopeReadRepository,
|
||||||
AccessTokenScopeWriteUser, AccessTokenScopeReadUser,
|
AccessTokenScopeWriteUser, AccessTokenScopeReadUser,
|
||||||
AccessTokenScopeWriteLicensing, AccessTokenScopeReadLicensing,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// allAccessTokenScopeBits contains all access token scopes.
|
// allAccessTokenScopeBits contains all access token scopes.
|
||||||
@@ -176,8 +166,6 @@ var allAccessTokenScopeBits = map[AccessTokenScope]accessTokenScopeBitmap{
|
|||||||
AccessTokenScopeWriteRepository: accessTokenScopeWriteRepositoryBits,
|
AccessTokenScopeWriteRepository: accessTokenScopeWriteRepositoryBits,
|
||||||
AccessTokenScopeReadUser: accessTokenScopeReadUserBits,
|
AccessTokenScopeReadUser: accessTokenScopeReadUserBits,
|
||||||
AccessTokenScopeWriteUser: accessTokenScopeWriteUserBits,
|
AccessTokenScopeWriteUser: accessTokenScopeWriteUserBits,
|
||||||
AccessTokenScopeReadLicensing: accessTokenScopeReadLicensingBits,
|
|
||||||
AccessTokenScopeWriteLicensing: accessTokenScopeWriteLicensingBits,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// readAccessTokenScopes maps a scope category to the read permission scope
|
// readAccessTokenScopes maps a scope category to the read permission scope
|
||||||
@@ -192,7 +180,6 @@ var accessTokenScopes = map[AccessTokenScopeLevel]map[AccessTokenScopeCategory]A
|
|||||||
AccessTokenScopeCategoryIssue: AccessTokenScopeReadIssue,
|
AccessTokenScopeCategoryIssue: AccessTokenScopeReadIssue,
|
||||||
AccessTokenScopeCategoryRepository: AccessTokenScopeReadRepository,
|
AccessTokenScopeCategoryRepository: AccessTokenScopeReadRepository,
|
||||||
AccessTokenScopeCategoryUser: AccessTokenScopeReadUser,
|
AccessTokenScopeCategoryUser: AccessTokenScopeReadUser,
|
||||||
AccessTokenScopeCategoryLicensing: AccessTokenScopeReadLicensing,
|
|
||||||
},
|
},
|
||||||
Write: {
|
Write: {
|
||||||
AccessTokenScopeCategoryActivityPub: AccessTokenScopeWriteActivityPub,
|
AccessTokenScopeCategoryActivityPub: AccessTokenScopeWriteActivityPub,
|
||||||
@@ -204,7 +191,6 @@ var accessTokenScopes = map[AccessTokenScopeLevel]map[AccessTokenScopeCategory]A
|
|||||||
AccessTokenScopeCategoryIssue: AccessTokenScopeWriteIssue,
|
AccessTokenScopeCategoryIssue: AccessTokenScopeWriteIssue,
|
||||||
AccessTokenScopeCategoryRepository: AccessTokenScopeWriteRepository,
|
AccessTokenScopeCategoryRepository: AccessTokenScopeWriteRepository,
|
||||||
AccessTokenScopeCategoryUser: AccessTokenScopeWriteUser,
|
AccessTokenScopeCategoryUser: AccessTokenScopeWriteUser,
|
||||||
AccessTokenScopeCategoryLicensing: AccessTokenScopeWriteLicensing,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,7 +370,7 @@ func (bitmap accessTokenScopeBitmap) toScope() AccessTokenScope {
|
|||||||
scope := AccessTokenScope(strings.Join(scopes, ","))
|
scope := AccessTokenScope(strings.Join(scopes, ","))
|
||||||
scope = AccessTokenScope(strings.ReplaceAll(
|
scope = AccessTokenScope(strings.ReplaceAll(
|
||||||
string(scope),
|
string(scope),
|
||||||
"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user,write:licensing",
|
"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user",
|
||||||
"all",
|
"all",
|
||||||
))
|
))
|
||||||
return scope
|
return scope
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ type scopeTestNormalize struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAccessTokenScope_Normalize(t *testing.T) {
|
func TestAccessTokenScope_Normalize(t *testing.T) {
|
||||||
assert.Equal(t, []string{"activitypub", "admin", "issue", "licensing", "misc", "notification", "organization", "package", "repository", "user"}, GetAccessTokenCategories())
|
assert.Equal(t, []string{"activitypub", "admin", "issue", "misc", "notification", "organization", "package", "repository", "user"}, GetAccessTokenCategories())
|
||||||
tests := []scopeTestNormalize{
|
tests := []scopeTestNormalize{
|
||||||
{"", "", nil},
|
{"", "", nil},
|
||||||
{"write:misc,write:notification,read:package,write:notification,public-only", "public-only,write:misc,write:notification,read:package", nil},
|
{"write:misc,write:notification,read:package,write:notification,public-only", "public-only,write:misc,write:notification,read:package", nil},
|
||||||
{"all", "all", nil},
|
{"all", "all", nil},
|
||||||
{"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user,write:licensing", "all", nil},
|
{"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user", "all", nil},
|
||||||
{"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user,write:licensing,public-only", "public-only,all", nil},
|
{"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user,public-only", "public-only,all", nil},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, scope := range GetAccessTokenCategories() {
|
for _, scope := range GetAccessTokenCategories() {
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ type IssueStatusDef struct {
|
|||||||
Color string `xorm:"VARCHAR(7)"` // hex color, e.g. "#e11d48"
|
Color string `xorm:"VARCHAR(7)"` // hex color, e.g. "#e11d48"
|
||||||
Description string `xorm:"TEXT"`
|
Description string `xorm:"TEXT"`
|
||||||
ClosesIssue bool `xorm:"NOT NULL DEFAULT false 'closes_issue'"`
|
ClosesIssue bool `xorm:"NOT NULL DEFAULT false 'closes_issue'"`
|
||||||
IsRequired bool `xorm:"NOT NULL DEFAULT false 'is_required'"` // cannot be deleted
|
|
||||||
SortOrder int `xorm:"NOT NULL DEFAULT 0 'sort_order'"`
|
SortOrder int `xorm:"NOT NULL DEFAULT 0 'sort_order'"`
|
||||||
IsActive bool `xorm:"NOT NULL DEFAULT true 'is_active'"`
|
IsActive bool `xorm:"NOT NULL DEFAULT true 'is_active'"`
|
||||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
|
||||||
@@ -57,15 +56,14 @@ func GetIssueStatusDefsByOrg(ctx context.Context, orgID int64) ([]*IssueStatusDe
|
|||||||
}
|
}
|
||||||
|
|
||||||
// seedDefaultIssueStatuses creates the standard status presets for an org.
|
// seedDefaultIssueStatuses creates the standard status presets for an org.
|
||||||
// Open and Closed are required (is_required=true) and cannot be deleted.
|
|
||||||
func seedDefaultIssueStatuses(ctx context.Context, orgID int64) error {
|
func seedDefaultIssueStatuses(ctx context.Context, orgID int64) error {
|
||||||
defaults := []*IssueStatusDef{
|
defaults := []*IssueStatusDef{
|
||||||
{OrgID: orgID, Name: "Open", Color: "#2563eb", Description: "New or active issue", ClosesIssue: false, IsRequired: true, SortOrder: 0, IsActive: true},
|
{OrgID: orgID, Name: "In Progress", Color: "#2563eb", Description: "Work is actively being done", SortOrder: 1, IsActive: true},
|
||||||
{OrgID: orgID, Name: "In Progress", Color: "#7c3aed", Description: "Work is actively being done", SortOrder: 1, IsActive: true},
|
{OrgID: orgID, Name: "Needs Info", Color: "#f59e0b", Description: "Waiting for more information", SortOrder: 2, IsActive: true},
|
||||||
{OrgID: orgID, Name: "Waiting", Color: "#f59e0b", Description: "Blocked or waiting for input", SortOrder: 2, IsActive: true},
|
{OrgID: orgID, Name: "Blocked", Color: "#dc2626", Description: "Cannot proceed due to dependency", SortOrder: 3, IsActive: true},
|
||||||
{OrgID: orgID, Name: "In Review", Color: "#0891b2", Description: "PR submitted, awaiting review", SortOrder: 3, IsActive: true},
|
{OrgID: orgID, Name: "Resolved", Color: "#16a34a", Description: "Fix implemented and verified", ClosesIssue: true, SortOrder: 4, IsActive: true},
|
||||||
{OrgID: orgID, Name: "Closed", Color: "#16a34a", Description: "Completed or resolved", ClosesIssue: true, IsRequired: true, SortOrder: 4, IsActive: true},
|
|
||||||
{OrgID: orgID, Name: "Won't Fix", Color: "#6b7280", Description: "Decided not to address", ClosesIssue: true, SortOrder: 5, IsActive: true},
|
{OrgID: orgID, Name: "Won't Fix", Color: "#6b7280", Description: "Decided not to address", ClosesIssue: true, SortOrder: 5, IsActive: true},
|
||||||
|
{OrgID: orgID, Name: "Duplicate", Color: "#8b5cf6", Description: "Already tracked elsewhere", ClosesIssue: true, SortOrder: 6, IsActive: true},
|
||||||
}
|
}
|
||||||
for _, d := range defaults {
|
for _, d := range defaults {
|
||||||
if _, err := db.GetEngine(ctx).Insert(d); err != nil {
|
if _, err := db.GetEngine(ctx).Insert(d); err != nil {
|
||||||
@@ -113,37 +111,13 @@ func UpdateIssueStatusDef(ctx context.Context, def *IssueStatusDef) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrStatusRequired is returned when trying to delete a required status.
|
|
||||||
type ErrStatusRequired struct {
|
|
||||||
ID int64
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e ErrStatusRequired) Error() string {
|
|
||||||
return "status is required and cannot be deleted"
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsErrStatusRequired checks if an error is ErrStatusRequired.
|
|
||||||
func IsErrStatusRequired(err error) bool {
|
|
||||||
_, ok := err.(ErrStatusRequired)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteIssueStatusDef deletes a status definition and clears references on issues.
|
// DeleteIssueStatusDef deletes a status definition and clears references on issues.
|
||||||
// Returns ErrStatusRequired if the status is marked as required.
|
|
||||||
func DeleteIssueStatusDef(ctx context.Context, id int64) error {
|
func DeleteIssueStatusDef(ctx context.Context, id int64) error {
|
||||||
def, err := GetIssueStatusDefByID(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if def.IsRequired {
|
|
||||||
return ErrStatusRequired{ID: def.ID, Name: def.Name}
|
|
||||||
}
|
|
||||||
// Clear status_id on all issues that reference this definition
|
// Clear status_id on all issues that reference this definition
|
||||||
if _, err := db.GetEngine(ctx).Exec("UPDATE issue SET status_id = 0 WHERE status_id = ?", id); err != nil {
|
if _, err := db.GetEngine(ctx).Exec("UPDATE issue SET status_id = 0 WHERE status_id = ?", id); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = db.GetEngine(ctx).ID(id).Delete(new(IssueStatusDef))
|
_, err := db.GetEngine(ctx).ID(id).Delete(new(IssueStatusDef))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -436,7 +436,6 @@ func prepareMigrationTasks() []*migration {
|
|||||||
newMigration(356, "Rename package_type to extension_type in repo manifest", v1_27.RenamePackageTypeToExtensionType),
|
newMigration(356, "Rename package_type to extension_type in repo manifest", v1_27.RenamePackageTypeToExtensionType),
|
||||||
newMigration(357, "Drop display_name from repo manifest and update stream config", v1_27.DropDisplayNameColumns),
|
newMigration(357, "Drop display_name from repo manifest and update stream config", v1_27.DropDisplayNameColumns),
|
||||||
newMigration(358, "Add licensing tables (license, entitlement, activation, product_tier)", v1_27.AddLicensingTables),
|
newMigration(358, "Add licensing tables (license, entitlement, activation, product_tier)", v1_27.AddLicensingTables),
|
||||||
newMigration(359, "Add deploy fields to repo manifest", v1_27.AddDeployFieldsToRepoManifest),
|
|
||||||
}
|
}
|
||||||
return preparedMigrations
|
return preparedMigrations
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package v1_27
|
|
||||||
|
|
||||||
import (
|
|
||||||
"xorm.io/xorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AddDeployFieldsToRepoManifest adds deploy configuration columns to repo_manifest.
|
|
||||||
func AddDeployFieldsToRepoManifest(x *xorm.Engine) error {
|
|
||||||
type RepoManifest struct {
|
|
||||||
DeployHost string `xorm:"VARCHAR(255) 'deploy_host'"`
|
|
||||||
DeployPort string `xorm:"VARCHAR(10) 'deploy_port'"`
|
|
||||||
DeployUser string `xorm:"VARCHAR(100) 'deploy_user'"`
|
|
||||||
DeployPath string `xorm:"TEXT 'deploy_path'"`
|
|
||||||
DockerImage string `xorm:"VARCHAR(255) 'docker_image'"`
|
|
||||||
DockerRegistry string `xorm:"VARCHAR(255) 'docker_registry'"`
|
|
||||||
ContainerName string `xorm:"VARCHAR(100) 'container_name'"`
|
|
||||||
HealthURL string `xorm:"TEXT 'health_url'"`
|
|
||||||
}
|
|
||||||
return x.Sync(new(RepoManifest))
|
|
||||||
}
|
|
||||||
@@ -50,16 +50,6 @@ type RepoMetadata struct {
|
|||||||
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
|
|
||||||
DeployHost string `xorm:"VARCHAR(255) 'deploy_host'"` // SSH host for deploy
|
|
||||||
DeployPort string `xorm:"VARCHAR(10) 'deploy_port'"` // SSH port (default 2918)
|
|
||||||
DeployUser string `xorm:"VARCHAR(100) 'deploy_user'"` // SSH user
|
|
||||||
DeployPath string `xorm:"TEXT 'deploy_path'"` // remote path for source/compose
|
|
||||||
DockerImage string `xorm:"VARCHAR(255) 'docker_image'"` // e.g. mokoconsulting/mokogitea
|
|
||||||
DockerRegistry string `xorm:"VARCHAR(255) 'docker_registry'"` // e.g. git.mokoconsulting.tech
|
|
||||||
ContainerName string `xorm:"VARCHAR(100) 'container_name'"` // Docker container name
|
|
||||||
HealthURL string `xorm:"TEXT 'health_url'"` // health check URL after deploy
|
|
||||||
|
|
||||||
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'"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,7 +165,6 @@ type IssueStatusDef struct {
|
|||||||
Color string `json:"color"`
|
Color string `json:"color"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
ClosesIssue bool `json:"closes_issue"`
|
ClosesIssue bool `json:"closes_issue"`
|
||||||
IsRequired bool `json:"is_required"`
|
|
||||||
SortOrder int `json:"sort_order"`
|
SortOrder int `json:"sort_order"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,16 +40,6 @@ type CreateAccessTokenOption struct {
|
|||||||
Scopes []string `json:"scopes"`
|
Scopes []string `json:"scopes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// EditAccessTokenOption options when editing access token scopes
|
|
||||||
// swagger:model EditAccessTokenOption
|
|
||||||
type EditAccessTokenOption struct {
|
|
||||||
// The new name for the token (optional)
|
|
||||||
Name string `json:"name"`
|
|
||||||
// The new scopes for the token
|
|
||||||
// example: ["read:repository", "write:issue"]
|
|
||||||
Scopes []string `json:"scopes"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateOAuth2ApplicationOptions holds options to create an oauth2 application
|
// CreateOAuth2ApplicationOptions holds options to create an oauth2 application
|
||||||
type CreateOAuth2ApplicationOptions struct {
|
type CreateOAuth2ApplicationOptions struct {
|
||||||
// The name of the OAuth2 application
|
// The name of the OAuth2 application
|
||||||
|
|||||||
@@ -855,8 +855,6 @@
|
|||||||
"settings.access_token_deletion_confirm_action": "Delete",
|
"settings.access_token_deletion_confirm_action": "Delete",
|
||||||
"settings.access_token_deletion_desc": "Deleting a token will revoke access to your account for applications using it. This cannot be undone. Continue?",
|
"settings.access_token_deletion_desc": "Deleting a token will revoke access to your account for applications using it. This cannot be undone. Continue?",
|
||||||
"settings.delete_token_success": "The token has been deleted. Applications using it no longer have access to your account.",
|
"settings.delete_token_success": "The token has been deleted. Applications using it no longer have access to your account.",
|
||||||
"settings.edit_token_scopes": "Edit Token Scopes",
|
|
||||||
"settings.update_token_success": "Token scopes have been updated successfully.",
|
|
||||||
"settings.repo_and_org_access": "Repository and Organization Access",
|
"settings.repo_and_org_access": "Repository and Organization Access",
|
||||||
"settings.permissions_public_only": "Public only",
|
"settings.permissions_public_only": "Public only",
|
||||||
"settings.permissions_access_all": "All (public, private, and limited)",
|
"settings.permissions_access_all": "All (public, private, and limited)",
|
||||||
|
|||||||
@@ -294,9 +294,6 @@ func checkTokenPublicOnly() func(ctx *context.APIContext) {
|
|||||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public packages")
|
ctx.APIError(http.StatusForbidden, "token scope is limited to public packages")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case auth_model.AccessTokenScopeCategoryLicensing:
|
|
||||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public resources, licensing is not available")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1007,9 +1004,7 @@ func Routes() *web.Router {
|
|||||||
m.Group("/tokens", func() {
|
m.Group("/tokens", func() {
|
||||||
m.Combo("").Get(user.ListAccessTokens).
|
m.Combo("").Get(user.ListAccessTokens).
|
||||||
Post(bind(api.CreateAccessTokenOption{}), reqToken(), user.CreateAccessToken)
|
Post(bind(api.CreateAccessTokenOption{}), reqToken(), user.CreateAccessToken)
|
||||||
m.Combo("/{id}").
|
m.Combo("/{id}").Delete(reqToken(), user.DeleteAccessToken)
|
||||||
Patch(bind(api.EditAccessTokenOption{}), reqToken(), user.UpdateAccessToken).
|
|
||||||
Delete(reqToken(), user.DeleteAccessToken)
|
|
||||||
}, reqSelfOrAdmin(), reqBasicOrRevProxyAuth())
|
}, reqSelfOrAdmin(), reqBasicOrRevProxyAuth())
|
||||||
|
|
||||||
m.Get("/activities/feeds", user.ListUserActivityFeeds)
|
m.Get("/activities/feeds", user.ListUserActivityFeeds)
|
||||||
@@ -1319,7 +1314,6 @@ func Routes() *web.Router {
|
|||||||
m.Get("/revisions/*", repo.ListPageRevisions)
|
m.Get("/revisions/*", repo.ListPageRevisions)
|
||||||
m.Post("/new", reqToken(), mustNotBeArchived, reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.NewWikiPage)
|
m.Post("/new", reqToken(), mustNotBeArchived, reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.NewWikiPage)
|
||||||
m.Get("/pages", repo.ListWikiPages)
|
m.Get("/pages", repo.ListWikiPages)
|
||||||
m.Get("/search", repo.SearchWikiPages)
|
|
||||||
}, mustEnableWiki)
|
}, mustEnableWiki)
|
||||||
m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup)
|
m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup)
|
||||||
m.Post("/markdown", reqToken(), bind(api.MarkdownOption{}), misc.Markdown)
|
m.Post("/markdown", reqToken(), bind(api.MarkdownOption{}), misc.Markdown)
|
||||||
@@ -1897,7 +1891,7 @@ func Routes() *web.Router {
|
|||||||
|
|
||||||
// Authenticated license detail
|
// Authenticated license detail
|
||||||
m.Get("/{dlid}/status", reqToken(), licensing.Status)
|
m.Get("/{dlid}/status", reqToken(), licensing.Status)
|
||||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryLicensing))
|
})
|
||||||
}, sudo())
|
}, sudo())
|
||||||
|
|
||||||
return m
|
return m
|
||||||
|
|||||||
@@ -207,10 +207,7 @@ func UpdateLicense(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
if len(cols) > 0 {
|
if len(cols) > 0 {
|
||||||
cols = append(cols, "updated_at")
|
cols = append(cols, "updated_at")
|
||||||
if _, err := db.GetEngine(ctx).ID(id).Cols(cols...).Update(license); err != nil {
|
db.GetEngine(ctx).ID(id).Cols(cols...).Update(license)
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.JSON(http.StatusOK, licenseToJSON(ctx, license))
|
ctx.JSON(http.StatusOK, licenseToJSON(ctx, license))
|
||||||
@@ -402,10 +399,7 @@ func UpdateTier(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(cols) > 0 {
|
if len(cols) > 0 {
|
||||||
if _, err := db.GetEngine(ctx).ID(id).Cols(cols...).Update(tier); err != nil {
|
db.GetEngine(ctx).ID(id).Cols(cols...).Update(tier)
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.JSON(http.StatusOK, tierToJSON(tier))
|
ctx.JSON(http.StatusOK, tierToJSON(tier))
|
||||||
@@ -433,10 +427,7 @@ func DeleteTier(ctx *context.APIContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := db.GetEngine(ctx).ID(id).Delete(new(licensing_model.ProductTier)); err != nil {
|
db.GetEngine(ctx).ID(id).Delete(new(licensing_model.ProductTier))
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.Status(http.StatusNoContent)
|
ctx.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,19 +11,6 @@ import (
|
|||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||||
)
|
)
|
||||||
|
|
||||||
// checkOrgVisibility returns true if the current user can view org metadata.
|
|
||||||
// Public orgs are visible to everyone. Private/limited orgs require authentication.
|
|
||||||
func checkOrgVisibility(ctx *context.APIContext) bool {
|
|
||||||
if ctx.Org.Organization.Visibility == api.VisibleTypePublic {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if ctx.Doer == nil {
|
|
||||||
ctx.APIErrorNotFound()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListIssueStatuses returns active issue status definitions for an org.
|
// ListIssueStatuses returns active issue status definitions for an org.
|
||||||
func ListIssueStatuses(ctx *context.APIContext) {
|
func ListIssueStatuses(ctx *context.APIContext) {
|
||||||
// swagger:operation GET /orgs/{org}/issue-statuses organization orgListIssueStatuses
|
// swagger:operation GET /orgs/{org}/issue-statuses organization orgListIssueStatuses
|
||||||
@@ -47,10 +34,6 @@ func ListIssueStatuses(ctx *context.APIContext) {
|
|||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
if !checkOrgVisibility(ctx) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defs, err := issues_model.GetIssueStatusDefsByOrg(ctx, ctx.Org.Organization.ID)
|
defs, err := issues_model.GetIssueStatusDefsByOrg(ctx, ctx.Org.Organization.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
@@ -64,7 +47,6 @@ func ListIssueStatuses(ctx *context.APIContext) {
|
|||||||
Color: d.Color,
|
Color: d.Color,
|
||||||
Description: d.Description,
|
Description: d.Description,
|
||||||
ClosesIssue: d.ClosesIssue,
|
ClosesIssue: d.ClosesIssue,
|
||||||
IsRequired: d.IsRequired,
|
|
||||||
SortOrder: d.SortOrder,
|
SortOrder: d.SortOrder,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -94,10 +76,6 @@ func ListIssuePriorities(ctx *context.APIContext) {
|
|||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
if !checkOrgVisibility(ctx) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defs, err := issues_model.GetIssuePriorityDefsByOrg(ctx, ctx.Org.Organization.ID)
|
defs, err := issues_model.GetIssuePriorityDefsByOrg(ctx, ctx.Org.Organization.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
@@ -140,10 +118,6 @@ func ListIssueTypes(ctx *context.APIContext) {
|
|||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
if !checkOrgVisibility(ctx) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defs, err := issues_model.GetIssueTypeDefsByOrg(ctx, ctx.Org.Organization.ID)
|
defs, err := issues_model.GetIssueTypeDefsByOrg(ctx, ctx.Org.Organization.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
|
|||||||
@@ -32,16 +32,6 @@ type apiMetadata struct {
|
|||||||
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
|
|
||||||
DeployHost string `json:"deploy_host,omitempty"`
|
|
||||||
DeployPort string `json:"deploy_port,omitempty"`
|
|
||||||
DeployUser string `json:"deploy_user,omitempty"`
|
|
||||||
DeployPath string `json:"deploy_path,omitempty"`
|
|
||||||
DockerImage string `json:"docker_image,omitempty"`
|
|
||||||
DockerRegistry string `json:"docker_registry,omitempty"`
|
|
||||||
ContainerName string `json:"container_name,omitempty"`
|
|
||||||
HealthURL string `json:"health_url,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRepoMetadata returns the manifest settings for a repository.
|
// GetRepoMetadata returns the manifest settings for a repository.
|
||||||
@@ -91,14 +81,6 @@ func GetRepoMetadata(ctx *context.APIContext) {
|
|||||||
Language: m.Language,
|
Language: m.Language,
|
||||||
ExtensionType: m.ExtensionType,
|
ExtensionType: m.ExtensionType,
|
||||||
EntryPoint: m.EntryPoint,
|
EntryPoint: m.EntryPoint,
|
||||||
DeployHost: m.DeployHost,
|
|
||||||
DeployPort: m.DeployPort,
|
|
||||||
DeployUser: m.DeployUser,
|
|
||||||
DeployPath: m.DeployPath,
|
|
||||||
DockerImage: m.DockerImage,
|
|
||||||
DockerRegistry: m.DockerRegistry,
|
|
||||||
ContainerName: m.ContainerName,
|
|
||||||
HealthURL: m.HealthURL,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,58 +96,34 @@ func UpdateRepoMetadata(ctx *context.APIContext) {
|
|||||||
// responses:
|
// responses:
|
||||||
// "200":
|
// "200":
|
||||||
// "$ref": "#/responses/Manifest"
|
// "$ref": "#/responses/Manifest"
|
||||||
// Decode into a map to detect which fields were actually sent.
|
var req apiMetadata
|
||||||
var raw map[string]any
|
if err := json.NewDecoder(ctx.Req.Body).Decode(&req); err != nil {
|
||||||
if err := json.NewDecoder(ctx.Req.Body).Decode(&raw); err != nil {
|
|
||||||
ctx.APIError(http.StatusBadRequest, err)
|
ctx.APIError(http.StatusBadRequest, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load existing metadata (or create defaults).
|
m := &repo_model.RepoMetadata{
|
||||||
m, _ := repo_model.GetRepoMetadata(ctx, ctx.Repo.Repository.ID)
|
RepoID: ctx.Repo.Repository.ID,
|
||||||
if m == nil {
|
Name: req.Name,
|
||||||
m = &repo_model.RepoMetadata{
|
Org: req.Org,
|
||||||
RepoID: ctx.Repo.Repository.ID,
|
Description: req.Description,
|
||||||
Name: ctx.Repo.Repository.Name,
|
|
||||||
Org: ctx.Repo.Repository.OwnerName,
|
|
||||||
Description: ctx.Repo.Repository.Description,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply only the fields present in the request.
|
LicenseSPDX: req.LicenseSPDX,
|
||||||
setStr := func(key string, target *string) {
|
LicenseName: req.LicenseName,
|
||||||
if v, ok := raw[key]; ok {
|
VersionPrefix: req.VersionPrefix,
|
||||||
if s, ok := v.(string); ok {
|
ElementName: req.ElementName,
|
||||||
*target = s
|
Platform: req.Platform,
|
||||||
}
|
StandardsVersion: req.StandardsVersion,
|
||||||
}
|
StandardsSource: req.StandardsSource,
|
||||||
|
Maintainer: req.Maintainer,
|
||||||
|
MaintainerURL: req.MaintainerURL,
|
||||||
|
InfoURL: req.InfoURL,
|
||||||
|
TargetVersion: req.TargetVersion,
|
||||||
|
PHPMinimum: req.PHPMinimum,
|
||||||
|
Language: req.Language,
|
||||||
|
ExtensionType: req.ExtensionType,
|
||||||
|
EntryPoint: req.EntryPoint,
|
||||||
}
|
}
|
||||||
setStr("name", &m.Name)
|
|
||||||
setStr("org", &m.Org)
|
|
||||||
setStr("description", &m.Description)
|
|
||||||
setStr("license_spdx", &m.LicenseSPDX)
|
|
||||||
setStr("license_name", &m.LicenseName)
|
|
||||||
setStr("version_prefix", &m.VersionPrefix)
|
|
||||||
setStr("element_name", &m.ElementName)
|
|
||||||
setStr("platform", &m.Platform)
|
|
||||||
setStr("standards_version", &m.StandardsVersion)
|
|
||||||
setStr("standards_source", &m.StandardsSource)
|
|
||||||
setStr("maintainer", &m.Maintainer)
|
|
||||||
setStr("maintainer_url", &m.MaintainerURL)
|
|
||||||
setStr("info_url", &m.InfoURL)
|
|
||||||
setStr("target_version", &m.TargetVersion)
|
|
||||||
setStr("php_minimum", &m.PHPMinimum)
|
|
||||||
setStr("language", &m.Language)
|
|
||||||
setStr("extension_type", &m.ExtensionType)
|
|
||||||
setStr("entry_point", &m.EntryPoint)
|
|
||||||
setStr("deploy_host", &m.DeployHost)
|
|
||||||
setStr("deploy_port", &m.DeployPort)
|
|
||||||
setStr("deploy_user", &m.DeployUser)
|
|
||||||
setStr("deploy_path", &m.DeployPath)
|
|
||||||
setStr("docker_image", &m.DockerImage)
|
|
||||||
setStr("docker_registry", &m.DockerRegistry)
|
|
||||||
setStr("container_name", &m.ContainerName)
|
|
||||||
setStr("health_url", &m.HealthURL)
|
|
||||||
|
|
||||||
if err := repo_model.CreateOrUpdateRepoMetadata(ctx, m); err != nil {
|
if err := repo_model.CreateOrUpdateRepoMetadata(ctx, m); err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
@@ -193,13 +151,5 @@ func UpdateRepoMetadata(ctx *context.APIContext) {
|
|||||||
Language: m.Language,
|
Language: m.Language,
|
||||||
ExtensionType: m.ExtensionType,
|
ExtensionType: m.ExtensionType,
|
||||||
EntryPoint: m.EntryPoint,
|
EntryPoint: m.EntryPoint,
|
||||||
DeployHost: m.DeployHost,
|
|
||||||
DeployPort: m.DeployPort,
|
|
||||||
DeployUser: m.DeployUser,
|
|
||||||
DeployPath: m.DeployPath,
|
|
||||||
DockerImage: m.DockerImage,
|
|
||||||
DockerRegistry: m.DockerRegistry,
|
|
||||||
ContainerName: m.ContainerName,
|
|
||||||
HealthURL: m.HealthURL,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-140
@@ -8,7 +8,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
|
||||||
|
|
||||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
|
||||||
@@ -462,148 +461,10 @@ func ListPageRevisions(ctx *context.APIContext) {
|
|||||||
ctx.JSON(http.StatusOK, convert.ToWikiCommitList(commitsHistory, commitsCount))
|
ctx.JSON(http.StatusOK, convert.ToWikiCommitList(commitsHistory, commitsCount))
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchWikiPages searches wiki page titles and content.
|
|
||||||
func SearchWikiPages(ctx *context.APIContext) {
|
|
||||||
// swagger:operation GET /repos/{owner}/{repo}/wiki/search repository repoSearchWikiPages
|
|
||||||
// ---
|
|
||||||
// summary: Search wiki pages
|
|
||||||
// produces:
|
|
||||||
// - application/json
|
|
||||||
// parameters:
|
|
||||||
// - name: owner
|
|
||||||
// in: path
|
|
||||||
// description: owner of the repo
|
|
||||||
// type: string
|
|
||||||
// required: true
|
|
||||||
// - name: repo
|
|
||||||
// in: path
|
|
||||||
// description: name of the repo
|
|
||||||
// type: string
|
|
||||||
// required: true
|
|
||||||
// - name: q
|
|
||||||
// in: query
|
|
||||||
// description: search query
|
|
||||||
// type: string
|
|
||||||
// required: true
|
|
||||||
// - name: page
|
|
||||||
// in: query
|
|
||||||
// description: page number of results to return (1-based)
|
|
||||||
// type: integer
|
|
||||||
// - name: limit
|
|
||||||
// in: query
|
|
||||||
// description: page size of results
|
|
||||||
// type: integer
|
|
||||||
// responses:
|
|
||||||
// "200":
|
|
||||||
// description: "SearchResults"
|
|
||||||
// "404":
|
|
||||||
// "$ref": "#/responses/notFound"
|
|
||||||
|
|
||||||
query := strings.TrimSpace(ctx.FormString("q"))
|
|
||||||
if query == "" {
|
|
||||||
ctx.JSON(http.StatusOK, []interface{}{})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
wikiRepo, commit := findWikiRepoCommit(ctx)
|
|
||||||
if wikiRepo != nil {
|
|
||||||
defer wikiRepo.Close()
|
|
||||||
}
|
|
||||||
if ctx.Written() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
queryLower := strings.ToLower(query)
|
|
||||||
|
|
||||||
type WikiSearchResult struct {
|
|
||||||
PageName string `json:"page_name"`
|
|
||||||
PageURL string `json:"page_url"`
|
|
||||||
Context string `json:"context,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
entries, err := commit.ListEntriesRecursiveFast()
|
|
||||||
if err != nil {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var results []WikiSearchResult
|
|
||||||
for _, entry := range entries {
|
|
||||||
if !entry.IsRegular() || !strings.HasSuffix(entry.Name(), ".md") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
baseName := strings.TrimSuffix(entry.Name(), ".md")
|
|
||||||
// Extract just the filename without path for special file check
|
|
||||||
parts := strings.Split(baseName, "/")
|
|
||||||
shortName := parts[len(parts)-1]
|
|
||||||
if shortName == "_Sidebar" || shortName == "_Footer" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
blob := entry.Blob()
|
|
||||||
content, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
titleMatch := strings.Contains(strings.ToLower(baseName), queryLower)
|
|
||||||
contentMatch := strings.Contains(strings.ToLower(content), queryLower)
|
|
||||||
|
|
||||||
if !titleMatch && !contentMatch {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
contextLine := ""
|
|
||||||
if contentMatch {
|
|
||||||
for _, line := range strings.Split(content, "\n") {
|
|
||||||
if strings.Contains(strings.ToLower(line), queryLower) {
|
|
||||||
contextLine = strings.TrimSpace(line)
|
|
||||||
if len(contextLine) > 200 {
|
|
||||||
contextLine = contextLine[:200] + "..."
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wikiName, err := wiki_service.GitPathToWebPath(entry.Name())
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
_, displayName := wiki_service.WebPathToUserTitle(wikiName)
|
|
||||||
|
|
||||||
results = append(results, WikiSearchResult{
|
|
||||||
PageName: displayName,
|
|
||||||
PageURL: string(wikiName),
|
|
||||||
Context: contextLine,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pagination
|
|
||||||
page := max(ctx.FormInt("page"), 1)
|
|
||||||
limit := ctx.FormInt("limit")
|
|
||||||
if limit <= 0 {
|
|
||||||
limit = setting.API.DefaultPagingNum
|
|
||||||
}
|
|
||||||
total := len(results)
|
|
||||||
start := (page - 1) * limit
|
|
||||||
end := start + limit
|
|
||||||
if start > total {
|
|
||||||
start = total
|
|
||||||
}
|
|
||||||
if end > total {
|
|
||||||
end = total
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.SetLinkHeader(int64(total), limit)
|
|
||||||
ctx.SetTotalCountHeader(int64(total))
|
|
||||||
ctx.JSON(http.StatusOK, results[start:end])
|
|
||||||
}
|
|
||||||
|
|
||||||
// findEntryForFile finds the tree entry for a target filepath.
|
// findEntryForFile finds the tree entry for a target filepath.
|
||||||
func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) {
|
func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) {
|
||||||
entry, err := commit.GetTreeEntryByPath(target)
|
entry, err := commit.GetTreeEntryByPath(target)
|
||||||
if err != nil && !git.IsErrNotExist(err) {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if entry != nil {
|
if entry != nil {
|
||||||
|
|||||||
@@ -209,106 +209,6 @@ func DeleteAccessToken(ctx *context.APIContext) {
|
|||||||
ctx.Status(http.StatusNoContent)
|
ctx.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateAccessToken update access token scopes
|
|
||||||
func UpdateAccessToken(ctx *context.APIContext) {
|
|
||||||
// swagger:operation PATCH /users/{username}/tokens/{id} user userUpdateAccessToken
|
|
||||||
// ---
|
|
||||||
// summary: Update an access token's scopes
|
|
||||||
// consumes:
|
|
||||||
// - application/json
|
|
||||||
// produces:
|
|
||||||
// - application/json
|
|
||||||
// parameters:
|
|
||||||
// - name: username
|
|
||||||
// in: path
|
|
||||||
// description: username of the user whose token is to be updated
|
|
||||||
// type: string
|
|
||||||
// required: true
|
|
||||||
// - name: id
|
|
||||||
// in: path
|
|
||||||
// description: id of the token to update
|
|
||||||
// type: integer
|
|
||||||
// format: int64
|
|
||||||
// required: true
|
|
||||||
// - name: body
|
|
||||||
// in: body
|
|
||||||
// schema:
|
|
||||||
// "$ref": "#/definitions/EditAccessTokenOption"
|
|
||||||
// responses:
|
|
||||||
// "200":
|
|
||||||
// "$ref": "#/responses/AccessToken"
|
|
||||||
// "400":
|
|
||||||
// "$ref": "#/responses/error"
|
|
||||||
// "403":
|
|
||||||
// "$ref": "#/responses/forbidden"
|
|
||||||
// "404":
|
|
||||||
// "$ref": "#/responses/notFound"
|
|
||||||
|
|
||||||
tokenID, _ := strconv.ParseInt(ctx.PathParam("id"), 0, 64)
|
|
||||||
if tokenID == 0 {
|
|
||||||
ctx.APIErrorNotFound()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tokens, err := db.Find[auth_model.AccessToken](ctx, auth_model.ListAccessTokensOptions{
|
|
||||||
UserID: ctx.ContextUser.ID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var token *auth_model.AccessToken
|
|
||||||
for _, t := range tokens {
|
|
||||||
if t.ID == tokenID {
|
|
||||||
token = t
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if token == nil {
|
|
||||||
ctx.APIErrorNotFound()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
form := web.GetForm(ctx).(*api.EditAccessTokenOption)
|
|
||||||
|
|
||||||
if form.Name == "" && len(form.Scopes) == 0 {
|
|
||||||
ctx.APIError(http.StatusBadRequest, "must provide name or scopes to update")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if form.Name != "" {
|
|
||||||
token.Name = form.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(form.Scopes) > 0 {
|
|
||||||
scope, err := auth_model.AccessTokenScope(strings.Join(form.Scopes, ",")).Normalize()
|
|
||||||
if err != nil {
|
|
||||||
ctx.APIError(http.StatusBadRequest, fmt.Errorf("invalid access token scope: %w", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if scope == "" {
|
|
||||||
ctx.APIError(http.StatusBadRequest, "access token must have a scope")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
token.Scope = scope
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := auth_model.UpdateAccessToken(ctx, token); err != nil {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.JSON(http.StatusOK, &api.AccessToken{
|
|
||||||
ID: token.ID,
|
|
||||||
Name: token.Name,
|
|
||||||
TokenLastEight: token.TokenLastEight,
|
|
||||||
Scopes: token.Scope.StringSlice(),
|
|
||||||
Created: token.CreatedUnix.AsTime(),
|
|
||||||
Updated: token.UpdatedUnix.AsTime(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateOauth2Application is the handler to create a new OAuth2 Application for the authenticated user
|
// CreateOauth2Application is the handler to create a new OAuth2 Application for the authenticated user
|
||||||
func CreateOauth2Application(ctx *context.APIContext) {
|
func CreateOauth2Application(ctx *context.APIContext) {
|
||||||
// swagger:operation POST /user/applications/oauth2 user userCreateOAuth2Application
|
// swagger:operation POST /user/applications/oauth2 user userCreateOAuth2Application
|
||||||
|
|||||||
@@ -103,11 +103,6 @@ func SettingsIssueStatusesDeletePost(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := issues_model.DeleteIssueStatusDef(ctx, id); err != nil {
|
if err := issues_model.DeleteIssueStatusDef(ctx, id); err != nil {
|
||||||
if issues_model.IsErrStatusRequired(err) {
|
|
||||||
ctx.Flash.Error("Cannot delete required status: " + def.Name)
|
|
||||||
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-statuses")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.ServerError("DeleteIssueStatusDef", err)
|
ctx.ServerError("DeleteIssueStatusDef", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-73
@@ -29,14 +29,6 @@ type OrgWikiPage struct {
|
|||||||
SubURL string
|
SubURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
// OrgWikiTreeNode represents a node in the org wiki folder tree for sidebar navigation.
|
|
||||||
type OrgWikiTreeNode struct {
|
|
||||||
Name string
|
|
||||||
SubURL string
|
|
||||||
IsDir bool
|
|
||||||
Children []*OrgWikiTreeNode
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wiki renders the org wiki tab.
|
// Wiki renders the org wiki tab.
|
||||||
func Wiki(ctx *context.Context) {
|
func Wiki(ctx *context.Context) {
|
||||||
org := ctx.Org.Organization
|
org := ctx.Org.Organization
|
||||||
@@ -79,9 +71,31 @@ func Wiki(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
ctx.Data["WikiRepoLink"] = wikiRepo.Link()
|
ctx.Data["WikiRepoLink"] = wikiRepo.Link()
|
||||||
|
|
||||||
// Build folder tree for sidebar navigation.
|
// Build page list from repo root.
|
||||||
wikiTree := buildOrgWikiTree(commit)
|
entries, err := commit.ListEntries()
|
||||||
ctx.Data["WikiTree"] = wikiTree
|
if err != nil {
|
||||||
|
ctx.ServerError("ListEntries", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pages := make([]OrgWikiPage, 0, len(entries))
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !entry.IsRegular() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := entry.Name()
|
||||||
|
if !isMarkdownFile(name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
displayName := strings.TrimSuffix(name, path.Ext(name))
|
||||||
|
if strings.EqualFold(displayName, "_sidebar") || strings.EqualFold(displayName, "_footer") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pages = append(pages, OrgWikiPage{
|
||||||
|
Name: displayName,
|
||||||
|
SubURL: displayName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ctx.Data["Pages"] = pages
|
||||||
|
|
||||||
// Determine which page to render.
|
// Determine which page to render.
|
||||||
pageName := ctx.PathParamRaw("*")
|
pageName := ctx.PathParamRaw("*")
|
||||||
@@ -143,68 +157,6 @@ func Wiki(ctx *context.Context) {
|
|||||||
ctx.HTML(http.StatusOK, tplOrgWiki)
|
ctx.HTML(http.StatusOK, tplOrgWiki)
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildOrgWikiTree builds a hierarchical folder tree from the org wiki git repo.
|
|
||||||
// Shows up to 2 levels deep (folders and their immediate children).
|
|
||||||
func buildOrgWikiTree(commit *git.Commit) []*OrgWikiTreeNode {
|
|
||||||
if commit == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
entries, err := commit.ListEntries()
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var topLevel []*OrgWikiTreeNode
|
|
||||||
|
|
||||||
for _, entry := range entries {
|
|
||||||
name := entry.Name()
|
|
||||||
if entry.IsDir() {
|
|
||||||
node := &OrgWikiTreeNode{
|
|
||||||
Name: name,
|
|
||||||
SubURL: name,
|
|
||||||
IsDir: true,
|
|
||||||
}
|
|
||||||
// List children of this directory (1 level deep).
|
|
||||||
subTree := entry.Tree()
|
|
||||||
if subTree != nil {
|
|
||||||
children, _ := subTree.ListEntries()
|
|
||||||
for _, child := range children {
|
|
||||||
childName := child.Name()
|
|
||||||
if child.IsDir() {
|
|
||||||
node.Children = append(node.Children, &OrgWikiTreeNode{
|
|
||||||
Name: childName,
|
|
||||||
SubURL: name + "/" + childName,
|
|
||||||
IsDir: true,
|
|
||||||
})
|
|
||||||
} else if isMarkdownFile(childName) {
|
|
||||||
displayName := strings.TrimSuffix(childName, path.Ext(childName))
|
|
||||||
if strings.EqualFold(displayName, "_sidebar") || strings.EqualFold(displayName, "_footer") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
node.Children = append(node.Children, &OrgWikiTreeNode{
|
|
||||||
Name: displayName,
|
|
||||||
SubURL: name + "/" + displayName,
|
|
||||||
IsDir: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
topLevel = append(topLevel, node)
|
|
||||||
} else if isMarkdownFile(name) {
|
|
||||||
displayName := strings.TrimSuffix(name, path.Ext(name))
|
|
||||||
if strings.EqualFold(displayName, "_sidebar") || strings.EqualFold(displayName, "_footer") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
topLevel = append(topLevel, &OrgWikiTreeNode{
|
|
||||||
Name: displayName,
|
|
||||||
SubURL: displayName,
|
|
||||||
IsDir: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return topLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
// findOrgWikiCommit locates the profile repo's wiki and returns its HEAD commit.
|
// findOrgWikiCommit locates the profile repo's wiki and returns its HEAD commit.
|
||||||
// The org wiki lives in the .wiki.git sidecar of the profile repo (e.g. .mokogitea.wiki.git).
|
// The org wiki lives in the .wiki.git sidecar of the profile repo (e.g. .mokogitea.wiki.git).
|
||||||
// Tries fallback repo names (.profile, .github) if the primary doesn't exist.
|
// Tries fallback repo names (.profile, .github) if the primary doesn't exist.
|
||||||
|
|||||||
@@ -123,14 +123,6 @@ func saveMetadata(ctx *context.Context) {
|
|||||||
manifest.Maintainer = existing.Maintainer
|
manifest.Maintainer = existing.Maintainer
|
||||||
manifest.MaintainerURL = existing.MaintainerURL
|
manifest.MaintainerURL = existing.MaintainerURL
|
||||||
manifest.Language = existing.Language
|
manifest.Language = existing.Language
|
||||||
manifest.DeployHost = existing.DeployHost
|
|
||||||
manifest.DeployPort = existing.DeployPort
|
|
||||||
manifest.DeployUser = existing.DeployUser
|
|
||||||
manifest.DeployPath = existing.DeployPath
|
|
||||||
manifest.DockerImage = existing.DockerImage
|
|
||||||
manifest.DockerRegistry = existing.DockerRegistry
|
|
||||||
manifest.ContainerName = existing.ContainerName
|
|
||||||
manifest.HealthURL = existing.HealthURL
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := repo_model.CreateOrUpdateRepoMetadata(ctx, manifest); err != nil {
|
if err := repo_model.CreateOrUpdateRepoMetadata(ctx, manifest); err != nil {
|
||||||
|
|||||||
+66
-1111
File diff suppressed because it is too large
Load Diff
@@ -90,59 +90,6 @@ func ApplicationsPost(ctx *context.Context) {
|
|||||||
ctx.Redirect(setting.AppSubURL + "/user/settings/applications")
|
ctx.Redirect(setting.AppSubURL + "/user/settings/applications")
|
||||||
}
|
}
|
||||||
|
|
||||||
// EditApplication response for editing user access token scopes
|
|
||||||
func EditApplication(ctx *context.Context) {
|
|
||||||
tokenID := ctx.FormInt64("id")
|
|
||||||
|
|
||||||
tokens, err := db.Find[auth_model.AccessToken](ctx, auth_model.ListAccessTokensOptions{UserID: ctx.Doer.ID})
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("ListAccessTokens", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var token *auth_model.AccessToken
|
|
||||||
for _, t := range tokens {
|
|
||||||
if t.ID == tokenID {
|
|
||||||
token = t
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if token == nil {
|
|
||||||
ctx.Flash.Error("Token not found")
|
|
||||||
ctx.JSONRedirect(setting.AppSubURL + "/user/settings/applications")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = ctx.Req.ParseForm()
|
|
||||||
var scopeNames []string
|
|
||||||
const accessTokenScopePrefix = "scope-"
|
|
||||||
for k, v := range ctx.Req.Form {
|
|
||||||
if strings.HasPrefix(k, accessTokenScopePrefix) {
|
|
||||||
scopeNames = append(scopeNames, v...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scope, err := auth_model.AccessTokenScope(strings.Join(scopeNames, ",")).Normalize()
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("GetScope", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !scope.HasPermissionScope() {
|
|
||||||
ctx.Flash.Error(ctx.Tr("settings.at_least_one_permission"))
|
|
||||||
ctx.JSONRedirect(setting.AppSubURL + "/user/settings/applications")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
token.Scope = scope
|
|
||||||
if err := auth_model.UpdateAccessToken(ctx, token); err != nil {
|
|
||||||
ctx.ServerError("UpdateAccessToken", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Flash.Success(ctx.Tr("settings.update_token_success"))
|
|
||||||
ctx.JSONRedirect(setting.AppSubURL + "/user/settings/applications")
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteApplication response for delete user access token
|
// DeleteApplication response for delete user access token
|
||||||
func DeleteApplication(ctx *context.Context) {
|
func DeleteApplication(ctx *context.Context) {
|
||||||
if err := auth_model.DeleteAccessTokenByID(ctx, ctx.FormInt64("id"), ctx.Doer.ID); err != nil {
|
if err := auth_model.DeleteAccessTokenByID(ctx, ctx.FormInt64("id"), ctx.Doer.ID); err != nil {
|
||||||
|
|||||||
@@ -680,7 +680,6 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
|||||||
// access token applications
|
// access token applications
|
||||||
m.Combo("").Get(user_setting.Applications).
|
m.Combo("").Get(user_setting.Applications).
|
||||||
Post(web.Bind(forms.NewAccessTokenForm{}), user_setting.ApplicationsPost)
|
Post(web.Bind(forms.NewAccessTokenForm{}), user_setting.ApplicationsPost)
|
||||||
m.Post("/edit", user_setting.EditApplication)
|
|
||||||
m.Post("/delete", user_setting.DeleteApplication)
|
m.Post("/delete", user_setting.DeleteApplication)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,6 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<strong>{{.Name}}</strong>
|
<strong>{{.Name}}</strong>
|
||||||
{{if .IsRequired}}<span class="ui mini blue label" title="Required status - cannot be deleted">{{svg "octicon-lock" 10}} required</span>{{end}}
|
|
||||||
{{if not .IsActive}}<span class="ui mini grey label">{{ctx.Locale.Tr "org.settings.issue_status_inactive"}}</span>{{end}}
|
{{if not .IsActive}}<span class="ui mini grey label">{{ctx.Locale.Tr "org.settings.issue_status_inactive"}}</span>{{end}}
|
||||||
{{if .Description}}<br><small class="text grey">{{.Description}}</small>{{end}}
|
{{if .Description}}<br><small class="text grey">{{.Description}}</small>{{end}}
|
||||||
</td>
|
</td>
|
||||||
@@ -41,14 +40,10 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>{{.SortOrder}}</td>
|
<td>{{.SortOrder}}</td>
|
||||||
<td class="tw-text-right">
|
<td class="tw-text-right">
|
||||||
{{if .IsRequired}}
|
|
||||||
<span class="ui tiny icon button disabled" title="Required - cannot be deleted">{{svg "octicon-lock" 14}}</span>
|
|
||||||
{{else}}
|
|
||||||
<form method="post" action="{{$.OrgLink}}/settings/issue-statuses/{{.ID}}/delete" class="tw-inline">
|
<form method="post" action="{{$.OrgLink}}/settings/issue-statuses/{{.ID}}/delete" class="tw-inline">
|
||||||
{{$.CsrfTokenHtml}}
|
{{$.CsrfTokenHtml}}
|
||||||
<button class="ui tiny red icon button" type="submit" title="{{ctx.Locale.Tr "remove"}}">{{svg "octicon-trash" 14}}</button>
|
<button class="ui tiny red icon button" type="submit" title="{{ctx.Locale.Tr "remove"}}">{{svg "octicon-trash" 14}}</button>
|
||||||
</form>
|
</form>
|
||||||
{{end}}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
This organization doesn't have a wiki yet.
|
This organization doesn't have a wiki yet.
|
||||||
</div>
|
</div>
|
||||||
<p class="tw-text-center">
|
<p class="tw-text-center">
|
||||||
Enable the wiki on the <code>.mokogitea</code> (public) or <code>.mokogitea-private</code> (members-only)
|
Enable the wiki on the <code>.profile</code> (public) or <code>.profile-private</code> (members-only)
|
||||||
repository to get started.
|
repository to get started.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,59 +47,34 @@
|
|||||||
<p>The page "{{.CurrentPage}}" does not exist in this wiki.</p>
|
<p>The page "{{.CurrentPage}}" does not exist in this wiki.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{if .WikiTree}}
|
{{if .Pages}}
|
||||||
<h4>Available pages:</h4>
|
<h4>Available pages:</h4>
|
||||||
<ul>
|
<ul>
|
||||||
{{range .WikiTree}}
|
{{range .Pages}}
|
||||||
{{if .IsDir}}
|
<li><a href="{{$.Org.HomeLink}}/-/wiki/{{.SubURL}}">{{.Name}}</a></li>
|
||||||
{{range .Children}}
|
|
||||||
<li><a href="{{$.Org.HomeLink}}/-/wiki/{{.SubURL}}">{{.Name}}</a></li>
|
|
||||||
{{end}}
|
|
||||||
{{else}}
|
|
||||||
<li><a href="{{$.Org.HomeLink}}/-/wiki/{{.SubURL}}">{{.Name}}</a></li>
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="wiki-content-parts">
|
<div class="wiki-content-parts">
|
||||||
<div class="render-content markup wiki-content-main {{if or .WikiSidebarHTML .WikiTree}}with-sidebar{{end}}">
|
<div class="render-content markup wiki-content-main {{if or .WikiSidebarHTML .Pages}}with-sidebar{{end}}">
|
||||||
{{.WikiContent}}
|
{{.WikiContent}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{if or .WikiSidebarHTML .WikiTree}}
|
{{if or .WikiSidebarHTML .Pages}}
|
||||||
<div class="render-content markup wiki-content-sidebar">
|
<div class="render-content markup wiki-content-sidebar">
|
||||||
{{if .WikiSidebarHTML}}
|
{{if .WikiSidebarHTML}}
|
||||||
{{.WikiSidebarHTML}}
|
{{.WikiSidebarHTML}}
|
||||||
{{else if .WikiTree}}
|
<div class="ui divider"></div>
|
||||||
|
{{end}}
|
||||||
|
{{if .Pages}}
|
||||||
<strong>{{svg "octicon-list-unordered" 14}} Pages</strong>
|
<strong>{{svg "octicon-list-unordered" 14}} Pages</strong>
|
||||||
<ul class="wiki-tree-list">
|
<ul class="wiki-tree-list">
|
||||||
{{range .WikiTree}}
|
{{range .Pages}}
|
||||||
<li>
|
<li>
|
||||||
{{if .IsDir}}
|
{{svg "octicon-file" 14}}
|
||||||
<details open>
|
<a href="{{$.Org.HomeLink}}/-/wiki/{{.SubURL}}" {{if eq $.CurrentPage .Name}}class="active"{{end}}>{{.Name}}</a>
|
||||||
<summary>{{svg "octicon-file-directory" 14}} <strong>{{.Name}}</strong></summary>
|
|
||||||
{{if .Children}}
|
|
||||||
<ul>
|
|
||||||
{{range .Children}}
|
|
||||||
<li>
|
|
||||||
{{if .IsDir}}
|
|
||||||
{{svg "octicon-file-directory" 14}}
|
|
||||||
<a href="{{$.Org.HomeLink}}/-/wiki/{{.SubURL}}"><strong>{{.Name}}</strong></a>
|
|
||||||
{{else}}
|
|
||||||
{{svg "octicon-file" 14}}
|
|
||||||
<a href="{{$.Org.HomeLink}}/-/wiki/{{.SubURL}}" {{if eq $.CurrentPage .Name}}class="active"{{end}}>{{.Name}}</a>
|
|
||||||
{{end}}
|
|
||||||
</li>
|
|
||||||
{{end}}
|
|
||||||
</ul>
|
|
||||||
{{end}}
|
|
||||||
</details>
|
|
||||||
{{else}}
|
|
||||||
{{svg "octicon-file" 14}}
|
|
||||||
<a href="{{$.Org.HomeLink}}/-/wiki/{{.SubURL}}" {{if eq $.CurrentPage .Name}}class="active"{{end}}>{{.Name}}</a>
|
|
||||||
{{end}}
|
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -21,7 +21,11 @@
|
|||||||
<input name="org" value="{{.Manifest.Org}}" placeholder="Organization">
|
<input name="org" value="{{.Manifest.Org}}" placeholder="Organization">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="three fields">
|
<div class="four fields">
|
||||||
|
<div class="field">
|
||||||
|
<label>Version</label>
|
||||||
|
<input name="version" value="{{.Manifest.Version}}" placeholder="e.g. 06.00.00">
|
||||||
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Version Prefix</label>
|
<label>Version Prefix</label>
|
||||||
<input name="version_prefix" value="{{.Manifest.VersionPrefix}}" placeholder="e.g. v1.26.1-moko.">
|
<input name="version_prefix" value="{{.Manifest.VersionPrefix}}" placeholder="e.g. v1.26.1-moko.">
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
{{template "base/head" .}}
|
|
||||||
<div role="main" aria-label="{{.Title}}" class="page-content repository wiki">
|
|
||||||
{{template "repo/header" .}}
|
|
||||||
<div class="ui container">
|
|
||||||
<div class="repo-button-row">
|
|
||||||
<div class="tw-flex tw-items-center tw-gap-2">
|
|
||||||
<a class="ui small button" href="{{.RepoLink}}/wiki/{{.PageURL}}">
|
|
||||||
{{svg "octicon-arrow-left" 14}} Back to {{.title}}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>{{svg "octicon-cross-reference" 20}} What links here: {{.title}}</h2>
|
|
||||||
|
|
||||||
{{if .Backlinks}}
|
|
||||||
<div class="ui relaxed divided list">
|
|
||||||
{{range .Backlinks}}
|
|
||||||
<div class="item">
|
|
||||||
<div class="content">
|
|
||||||
<a class="header" href="{{$.RepoLink}}/wiki/{{.PageURL}}">{{.PageName}}</a>
|
|
||||||
{{if .Context}}
|
|
||||||
<div class="description">
|
|
||||||
<code class="tw-text-sm">{{.Context}}</code>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
<p class="tw-mt-4 text grey">{{.BacklinkCount}} {{if eq .BacklinkCount 1}}page{{else}}pages{{end}} linking here.</p>
|
|
||||||
{{else}}
|
|
||||||
<div class="ui placeholder segment">
|
|
||||||
<div class="ui icon header">
|
|
||||||
{{svg "octicon-unlink" 48}}
|
|
||||||
<br>
|
|
||||||
No pages link to "{{.title}}"
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{template "base/footer" .}}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
{{template "base/head" .}}
|
|
||||||
<div role="main" aria-label="{{.Title}}" class="page-content repository wiki">
|
|
||||||
{{template "repo/header" .}}
|
|
||||||
<div class="ui container">
|
|
||||||
<div class="repo-button-row tw-flex tw-items-center tw-gap-2 tw-mb-4">
|
|
||||||
<div class="tw-flex-1">
|
|
||||||
<a class="ui small button" href="{{.RepoLink}}/wiki/">
|
|
||||||
{{svg "octicon-arrow-left" 14}} Back to wiki
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>{{svg "octicon-tag" 20}} Category: {{.CategoryName}}</h2>
|
|
||||||
|
|
||||||
{{if .CategoryPages}}
|
|
||||||
<div class="ui relaxed divided list">
|
|
||||||
{{range .CategoryPages}}
|
|
||||||
<div class="item">
|
|
||||||
{{svg "octicon-file" 14}}
|
|
||||||
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}">{{.Name}}</a>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
<p class="tw-mt-4 text grey">{{.CategoryCount}} {{if eq .CategoryCount 1}}page{{else}}pages{{end}} in this category.</p>
|
|
||||||
{{else}}
|
|
||||||
<div class="ui placeholder segment">
|
|
||||||
<div class="ui icon header">
|
|
||||||
{{svg "octicon-tag" 48}}
|
|
||||||
<br>
|
|
||||||
No pages in category "{{.CategoryName}}"
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{template "base/footer" .}}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
{{template "base/head" .}}
|
|
||||||
<div role="main" aria-label="{{.Title}}" class="page-content repository wiki">
|
|
||||||
{{template "repo/header" .}}
|
|
||||||
<div class="ui container">
|
|
||||||
<div class="repo-button-row tw-flex tw-items-center tw-gap-2 tw-mb-4">
|
|
||||||
<div class="tw-flex-1">
|
|
||||||
<a href="{{.RepoLink}}/wiki/{{.PageURL}}">{{svg "octicon-arrow-left" 14}} {{.title}}</a>
|
|
||||||
·
|
|
||||||
<a href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_revision">Revision history</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ui segment">
|
|
||||||
<h3>{{svg "octicon-diff" 20}} Changes in <code>{{.CommitID}}</code></h3>
|
|
||||||
<p>
|
|
||||||
<strong>{{.CommitAuthor}}</strong> — {{.CommitMessage}}
|
|
||||||
<br>
|
|
||||||
<small class="text grey">{{DateUtils.TimeSince .CommitWhen}}</small>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{{if .IsNewPage}}
|
|
||||||
<div class="ui info message">New page created</div>
|
|
||||||
{{else if .IsDeletedPage}}
|
|
||||||
<div class="ui warning message">Page deleted</div>
|
|
||||||
{{else if not .HasDiff}}
|
|
||||||
<div class="ui info message">No content changes in this revision</div>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
{{if .HasDiff}}
|
|
||||||
<div class="diff-file-box" style="overflow-x: auto;">
|
|
||||||
<table class="chroma" style="width: 100%; border-collapse: collapse; font-family: monospace; font-size: 13px;">
|
|
||||||
{{range .DiffLines}}
|
|
||||||
<tr class="{{if eq .Type "add"}}diff-line-add{{else if eq .Type "del"}}diff-line-del{{else}}diff-line-context{{end}}">
|
|
||||||
<td style="width: 40px; text-align: right; padding: 0 8px; color: #999; user-select: none; {{if eq .Type "add"}}background: #e6ffec;{{else if eq .Type "del"}}background: #ffebe9;{{else}}background: #f6f8fa;{{end}}">
|
|
||||||
{{if .OldNum}}{{.OldNum}}{{end}}
|
|
||||||
</td>
|
|
||||||
<td style="width: 40px; text-align: right; padding: 0 8px; color: #999; user-select: none; {{if eq .Type "add"}}background: #e6ffec;{{else if eq .Type "del"}}background: #ffebe9;{{else}}background: #f6f8fa;{{end}}">
|
|
||||||
{{if .NewNum}}{{.NewNum}}{{end}}
|
|
||||||
</td>
|
|
||||||
<td style="padding: 0 8px; white-space: pre-wrap; word-break: break-all; {{if eq .Type "add"}}background: #e6ffec;{{else if eq .Type "del"}}background: #ffebe9;{{else}}background: #fff;{{end}}">
|
|
||||||
{{if eq .Type "add"}}+{{else if eq .Type "del"}}-{{else}} {{end}} {{.Content}}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{{end}}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{template "base/footer" .}}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>{{.Title}}</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 40px 20px;
|
|
||||||
color: #333;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
h1 { font-size: 2em; border-bottom: 1px solid #eee; padding-bottom: 0.3em; }
|
|
||||||
h2 { font-size: 1.5em; border-bottom: 1px solid #eee; padding-bottom: 0.3em; }
|
|
||||||
code { background: #f6f8fa; padding: 2px 6px; border-radius: 3px; font-size: 85%; }
|
|
||||||
pre { background: #f6f8fa; padding: 16px; border-radius: 6px; overflow-x: auto; }
|
|
||||||
pre code { background: none; padding: 0; }
|
|
||||||
table { border-collapse: collapse; width: 100%; }
|
|
||||||
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
|
|
||||||
th { background: #f6f8fa; }
|
|
||||||
img { max-width: 100%; }
|
|
||||||
blockquote { border-left: 4px solid #ddd; margin: 0; padding: 0 16px; color: #666; }
|
|
||||||
a { color: #0366d6; }
|
|
||||||
@media print {
|
|
||||||
body { padding: 0; }
|
|
||||||
a { color: inherit; text-decoration: none; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>{{.Title}}</h1>
|
|
||||||
{{.WikiContentHTML}}
|
|
||||||
<hr>
|
|
||||||
<p style="font-size: 12px; color: #999;">
|
|
||||||
Printed from wiki · {{.Title}}
|
|
||||||
</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
{{template "base/head" .}}
|
|
||||||
<div role="main" aria-label="{{.Title}}" class="page-content repository wiki">
|
|
||||||
{{template "repo/header" .}}
|
|
||||||
<div class="ui container">
|
|
||||||
<div class="repo-button-row tw-flex tw-items-center tw-gap-2 tw-mb-4">
|
|
||||||
<div class="tw-flex-1">
|
|
||||||
<h2>{{svg "octicon-history" 20}} Recent changes</h2>
|
|
||||||
</div>
|
|
||||||
<a class="ui small button" href="{{.RepoLink}}/wiki/">
|
|
||||||
{{svg "octicon-arrow-left" 14}} Back to wiki
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{if .RecentChanges}}
|
|
||||||
<table class="ui compact table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Page</th>
|
|
||||||
<th>Author</th>
|
|
||||||
<th>Edit summary</th>
|
|
||||||
<th>When</th>
|
|
||||||
<th>Commit</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{{range .RecentChanges}}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
{{if .PageURL}}
|
|
||||||
{{svg "octicon-file" 14}}
|
|
||||||
<a href="{{$.RepoLink}}/wiki/{{.PageURL}}">{{.PageName}}</a>
|
|
||||||
{{else if .PageName}}
|
|
||||||
{{svg "octicon-file" 14}} {{.PageName}}
|
|
||||||
{{else}}
|
|
||||||
<span class="text grey">—</span>
|
|
||||||
{{end}}
|
|
||||||
</td>
|
|
||||||
<td>{{.Author}}</td>
|
|
||||||
<td class="gt-ellipsis" style="max-width: 400px;">{{.Message}}</td>
|
|
||||||
<td>{{DateUtils.TimeSince .When}}</td>
|
|
||||||
<td><code class="tw-text-xs">{{.SHA}}</code></td>
|
|
||||||
</tr>
|
|
||||||
{{end}}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<div class="tw-flex tw-justify-between tw-mt-4">
|
|
||||||
{{if .HasPrevPage}}
|
|
||||||
<a class="ui small button" href="{{.RepoLink}}/wiki/?action=_recent&page={{Eval .CurrentPage "-" 1}}">
|
|
||||||
{{svg "octicon-chevron-left" 14}} Newer
|
|
||||||
</a>
|
|
||||||
{{else}}
|
|
||||||
<span></span>
|
|
||||||
{{end}}
|
|
||||||
{{if .HasNextPage}}
|
|
||||||
<a class="ui small button" href="{{.RepoLink}}/wiki/?action=_recent&page={{Eval .CurrentPage "+" 1}}">
|
|
||||||
Older {{svg "octicon-chevron-right" 14}}
|
|
||||||
</a>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
<div class="ui placeholder segment">
|
|
||||||
<div class="ui icon header">
|
|
||||||
{{svg "octicon-history" 48}}
|
|
||||||
<br>
|
|
||||||
No recent changes
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{template "base/footer" .}}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
{{template "base/head" .}}
|
|
||||||
<div role="main" aria-label="{{.Title}}" class="page-content repository wiki">
|
|
||||||
{{template "repo/header" .}}
|
|
||||||
<div class="ui container">
|
|
||||||
<div class="repo-button-row">
|
|
||||||
<div class="tw-flex tw-items-center tw-gap-2">
|
|
||||||
<a class="ui small button" href="{{.RepoLink}}/wiki/">
|
|
||||||
{{svg "octicon-arrow-left" 14}} Back to wiki
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>{{svg "octicon-search" 20}} Wiki search</h2>
|
|
||||||
|
|
||||||
<form class="ui form tw-mb-4" action="{{.RepoLink}}/wiki/" method="get">
|
|
||||||
<input type="hidden" name="action" value="_search">
|
|
||||||
<div class="ui action input tw-w-full">
|
|
||||||
<input type="text" name="q" value="{{.Query}}" placeholder="Search wiki pages..." autofocus>
|
|
||||||
<button class="ui primary button" type="submit">{{svg "octicon-search" 14}} Search</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{{if .Query}}
|
|
||||||
{{if .Results}}
|
|
||||||
<div class="ui relaxed divided list">
|
|
||||||
{{range .Results}}
|
|
||||||
<div class="item">
|
|
||||||
<div class="content">
|
|
||||||
<a class="header" href="{{$.RepoLink}}/wiki/{{.PageURL}}">{{.PageName}}</a>
|
|
||||||
{{if .Context}}
|
|
||||||
<div class="description">
|
|
||||||
<code class="tw-text-sm">{{.Context}}</code>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
<p class="tw-mt-4 text grey">{{.ResultCount}} {{if eq .ResultCount 1}}result{{else}}results{{end}} for "{{.Query}}"</p>
|
|
||||||
{{else}}
|
|
||||||
<div class="ui placeholder segment">
|
|
||||||
<div class="ui icon header">
|
|
||||||
{{svg "octicon-search" 48}}
|
|
||||||
<br>
|
|
||||||
No results for "{{.Query}}"
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{template "base/footer" .}}
|
|
||||||
@@ -20,10 +20,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="scrolling menu">
|
<div class="scrolling menu">
|
||||||
<a class="item muted" href="{{.RepoLink}}/wiki/?action=_pages">{{ctx.Locale.Tr "repo.wiki.pages"}}</a>
|
<a class="item muted" href="{{.RepoLink}}/wiki/?action=_pages">{{ctx.Locale.Tr "repo.wiki.pages"}}</a>
|
||||||
<a class="item muted" href="{{.RepoLink}}/wiki/?action=_search">{{svg "octicon-search" 14}} Search wiki</a>
|
|
||||||
<a class="item muted" href="{{.RepoLink}}/wiki/?action=_recent">{{svg "octicon-history" 14}} Recent changes</a>
|
|
||||||
t <a class="item muted" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_print" target="_blank">{{svg "octicon-browser" 14}} Print view</a>
|
|
||||||
<a class="item muted" href="{{.RepoLink}}/wiki/?action=_export&format=zip">{{svg "octicon-download" 14}} Export wiki (ZIP)</a>
|
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
{{range .Pages}}
|
{{range .Pages}}
|
||||||
<a class="item {{if eq $.Title .Name}}selected{{end}}" href="{{$.RepoLink}}/wiki/{{.SubURL}}">{{.Name}}</a>
|
<a class="item {{if eq $.Title .Name}}selected{{end}}" href="{{$.RepoLink}}/wiki/{{.SubURL}}">{{.Name}}</a>
|
||||||
@@ -38,8 +34,6 @@ t <a class="item muted" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_print
|
|||||||
<div class="flex-text-block tw-flex-wrap tw-justify-end">
|
<div class="flex-text-block tw-flex-wrap tw-justify-end">
|
||||||
<div class="flex-text-block tw-flex-1 tw-min-w-[300px]">
|
<div class="flex-text-block tw-flex-1 tw-min-w-[300px]">
|
||||||
<a class="ui basic button tw-px-3 tw-gap-3" title="{{ctx.Locale.Tr "repo.wiki.file_revision"}}" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_revision" >{{if .CommitCount}}<span>{{.CommitCount}}</span> {{end}}{{svg "octicon-history"}}</a>
|
<a class="ui basic button tw-px-3 tw-gap-3" title="{{ctx.Locale.Tr "repo.wiki.file_revision"}}" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_revision" >{{if .CommitCount}}<span>{{.CommitCount}}</span> {{end}}{{svg "octicon-history"}}</a>
|
||||||
<a class="ui basic button tw-px-3" title="What links here" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_backlinks">{{svg "octicon-cross-reference"}}</a>
|
|
||||||
{{if .LastCommitID}}<a class="ui basic button tw-px-3" title="View last change" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_diff&commit={{.LastCommitID}}">{{svg "octicon-diff"}}</a>{{end}}
|
|
||||||
<div class="tw-flex-1 gt-ellipsis">
|
<div class="tw-flex-1 gt-ellipsis">
|
||||||
{{$title}}
|
{{$title}}
|
||||||
<div class="ui sub header gt-ellipsis">
|
<div class="ui sub header gt-ellipsis">
|
||||||
@@ -53,7 +47,7 @@ t <a class="item muted" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_print
|
|||||||
<a class="ui small button unescape-button tw-hidden" data-unicode-content-selector=".wiki-content-parts">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</a>
|
<a class="ui small button unescape-button tw-hidden" data-unicode-content-selector=".wiki-content-parts">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</a>
|
||||||
<a class="ui small button escape-button" data-unicode-content-selector=".wiki-content-parts">{{ctx.Locale.Tr "repo.escape_control_characters"}}</a>
|
<a class="ui small button escape-button" data-unicode-content-selector=".wiki-content-parts">{{ctx.Locale.Tr "repo.escape_control_characters"}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if and .CanWriteWiki (not .Repository.IsMirror) (not .WikiFolderProtected)}}
|
{{if and .CanWriteWiki (not .Repository.IsMirror)}}
|
||||||
<a class="ui small button" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_edit">{{ctx.Locale.Tr "repo.wiki.edit_page_button"}}</a>
|
<a class="ui small button" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_edit">{{ctx.Locale.Tr "repo.wiki.edit_page_button"}}</a>
|
||||||
<a class="ui small primary button" href="{{.RepoLink}}/wiki?action=_new">{{ctx.Locale.Tr "repo.wiki.new_page_button"}}</a>
|
<a class="ui small primary button" href="{{.RepoLink}}/wiki?action=_new">{{ctx.Locale.Tr "repo.wiki.new_page_button"}}</a>
|
||||||
<a class="ui small red button link-action" href data-modal-confirm="#repo-wiki-delete-page-modal" data-url="{{.RepoLink}}/wiki/{{.PageURL}}?action=_delete">{{ctx.Locale.Tr "repo.wiki.delete_page_button"}}</a>
|
<a class="ui small red button link-action" href data-modal-confirm="#repo-wiki-delete-page-modal" data-url="{{.RepoLink}}/wiki/{{.PageURL}}?action=_delete">{{ctx.Locale.Tr "repo.wiki.delete_page_button"}}</a>
|
||||||
@@ -75,12 +69,6 @@ t <a class="item muted" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_print
|
|||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{if .WikiFolderProtected}}
|
|
||||||
<div class="ui warning message">
|
|
||||||
<p>{{svg "octicon-lock" 14}} This page is in a protected folder. Only users with the required role can edit it.</p>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
{{if .FormatWarning}}
|
{{if .FormatWarning}}
|
||||||
<div class="ui negative message">
|
<div class="ui negative message">
|
||||||
<p>{{.FormatWarning}}</p>
|
<p>{{.FormatWarning}}</p>
|
||||||
@@ -115,30 +103,13 @@ t <a class="item muted" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_print
|
|||||||
<div class="wiki-content-parts">
|
<div class="wiki-content-parts">
|
||||||
{{if .WikiSidebarTocHTML}}
|
{{if .WikiSidebarTocHTML}}
|
||||||
<div class="render-content markup wiki-content-sidebar wiki-content-toc">
|
<div class="render-content markup wiki-content-sidebar wiki-content-toc">
|
||||||
<details open>
|
{{.WikiSidebarTocHTML}}
|
||||||
<summary><strong>{{svg "octicon-list-unordered" 14}} Contents</strong></summary>
|
|
||||||
{{.WikiSidebarTocHTML}}
|
|
||||||
</details>
|
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<div class="render-content markup wiki-content-main {{if or .WikiSidebarTocHTML .WikiSidebarHTML .WikiTree}}with-sidebar{{end}}">
|
<div class="render-content markup wiki-content-main {{if or .WikiSidebarTocHTML .WikiSidebarHTML .WikiTree}}with-sidebar{{end}}">
|
||||||
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus}}
|
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus}}
|
||||||
{{if .WikiInlineTocHTML}}
|
|
||||||
<details open class="wiki-toc-inline tw-mb-4">
|
|
||||||
<summary><strong>{{svg "octicon-list-unordered" 14}} Contents</strong></summary>
|
|
||||||
{{.WikiInlineTocHTML}}
|
|
||||||
</details>
|
|
||||||
{{end}}
|
|
||||||
{{.WikiContentHTML}}
|
{{.WikiContentHTML}}
|
||||||
{{if .WikiCategories}}
|
|
||||||
<div class="tw-mt-4 tw-pt-2" style="border-top: 1px solid var(--color-secondary);">
|
|
||||||
{{svg "octicon-tag" 14}} Categories:
|
|
||||||
{{range .WikiCategories}}
|
|
||||||
<a class="ui small label" href="{{$.RepoLink}}/wiki/?action=_category&name={{.}}">{{.}}</a>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{if .WikiTree}}
|
{{if .WikiTree}}
|
||||||
@@ -150,7 +121,6 @@ t <a class="item muted" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_print
|
|||||||
{{if .IsDir}}
|
{{if .IsDir}}
|
||||||
{{svg "octicon-file-directory" 14}}
|
{{svg "octicon-file-directory" 14}}
|
||||||
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}"><strong>{{.Name}}</strong></a>
|
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}"><strong>{{.Name}}</strong></a>
|
||||||
{{if .Protected}}{{svg "octicon-lock" 12}}{{end}}
|
|
||||||
{{if .Children}}
|
{{if .Children}}
|
||||||
<ul>
|
<ul>
|
||||||
{{range .Children}}
|
{{range .Children}}
|
||||||
@@ -158,7 +128,6 @@ t <a class="item muted" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_print
|
|||||||
{{if .IsDir}}
|
{{if .IsDir}}
|
||||||
{{svg "octicon-file-directory" 14}}
|
{{svg "octicon-file-directory" 14}}
|
||||||
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}"><strong>{{.Name}}</strong></a>
|
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}"><strong>{{.Name}}</strong></a>
|
||||||
{{if .Protected}}{{svg "octicon-lock" 12}}{{end}}
|
|
||||||
{{else}}
|
{{else}}
|
||||||
{{svg "octicon-file" 14}}
|
{{svg "octicon-file" 14}}
|
||||||
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}" {{if eq $.PageURL .SubURL}}class="active"{{end}}>{{.Name}}</a>
|
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}" {{if eq $.PageURL .SubURL}}class="active"{{end}}>{{.Name}}</a>
|
||||||
|
|||||||
@@ -40,10 +40,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="item-trailing">
|
<div class="item-trailing">
|
||||||
<button class="ui primary tiny button edit-token-button" data-modal-id="edit-token" data-id="{{.ID}}" data-scopes="{{StringUtils.Join (.Scope.StringSlice) ","}}">
|
|
||||||
{{svg "octicon-pencil"}}
|
|
||||||
{{ctx.Locale.Tr "edit"}}
|
|
||||||
</button>
|
|
||||||
<button class="ui red tiny button delete-button" data-modal-id="delete-token" data-url="{{$.Link}}/delete" data-id="{{.ID}}">
|
<button class="ui red tiny button delete-button" data-modal-id="delete-token" data-url="{{$.Link}}/delete" data-id="{{.ID}}">
|
||||||
{{svg "octicon-trash"}}
|
{{svg "octicon-trash"}}
|
||||||
{{ctx.Locale.Tr "settings.delete_token"}}
|
{{ctx.Locale.Tr "settings.delete_token"}}
|
||||||
@@ -96,82 +92,6 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ui modal" id="edit-token">
|
|
||||||
<div class="header">
|
|
||||||
{{svg "octicon-pencil"}}
|
|
||||||
{{ctx.Locale.Tr "settings.edit_token_scopes"}}
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<form class="ui form" id="edit-token-form" action="{{.Link}}/edit" method="post">
|
|
||||||
{{.CsrfTokenHtml}}
|
|
||||||
<input type="hidden" name="id" value="">
|
|
||||||
<div class="field">
|
|
||||||
<div class="tw-my-2">{{ctx.Locale.Tr "settings.repo_and_org_access"}}</div>
|
|
||||||
<label class="gt-checkbox">
|
|
||||||
<input type="radio" name="scope-public-only" value="{{$.AccessTokenScopePublicOnly}}"> {{ctx.Locale.Tr "settings.permissions_public_only"}}
|
|
||||||
</label>
|
|
||||||
<label class="gt-checkbox">
|
|
||||||
<input type="radio" name="scope-public-only" value="" checked> {{ctx.Locale.Tr "settings.permissions_access_all"}}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="tw-my-2">{{ctx.Locale.Tr "settings.permissions_list"}}</div>
|
|
||||||
<table class="ui table unstackable tw-my-2">
|
|
||||||
{{range $category := .TokenCategories}}
|
|
||||||
<tr>
|
|
||||||
<td>{{$category}}</td>
|
|
||||||
<td><label class="gt-checkbox"><input type="radio" name="scope-{{$category}}" value="" checked> {{ctx.Locale.Tr "settings.permission_no_access"}}</label></td>
|
|
||||||
<td><label class="gt-checkbox"><input type="radio" name="scope-{{$category}}" value="read:{{$category}}"> {{ctx.Locale.Tr "settings.permission_read"}}</label></td>
|
|
||||||
<td><label class="gt-checkbox"><input type="radio" name="scope-{{$category}}" value="write:{{$category}}"> {{ctx.Locale.Tr "settings.permission_write"}}</label></td>
|
|
||||||
</tr>
|
|
||||||
{{end}}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="actions">
|
|
||||||
<button class="ui cancel button">{{ctx.Locale.Tr "cancel"}}</button>
|
|
||||||
<button class="ui primary button" id="edit-token-submit">{{ctx.Locale.Tr "save"}}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
for (const btn of document.querySelectorAll('.edit-token-button')) {
|
|
||||||
btn.addEventListener('click', (e) => {
|
|
||||||
const modal = document.getElementById('edit-token');
|
|
||||||
const form = document.getElementById('edit-token-form');
|
|
||||||
const id = e.currentTarget.getAttribute('data-id');
|
|
||||||
const scopes = (e.currentTarget.getAttribute('data-scopes') || '').split(',').filter(Boolean);
|
|
||||||
|
|
||||||
form.querySelector('input[name="id"]').value = id;
|
|
||||||
|
|
||||||
// Reset all radios to defaults
|
|
||||||
for (const radio of form.querySelectorAll('input[type="radio"]')) {
|
|
||||||
radio.checked = radio.value === '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set current scopes
|
|
||||||
for (const scope of scopes) {
|
|
||||||
if (scope === 'public-only') {
|
|
||||||
const radio = form.querySelector('input[name="scope-public-only"][value="public-only"]');
|
|
||||||
if (radio) radio.checked = true;
|
|
||||||
} else {
|
|
||||||
const radio = form.querySelector(`input[name="scope-${scope.split(':')[1]}"][value="${scope}"]`);
|
|
||||||
if (radio) radio.checked = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$(modal).modal('show');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('edit-token-submit')?.addEventListener('click', () => {
|
|
||||||
document.getElementById('edit-token-form')?.submit();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="ui g-modal-confirm delete modal" id="delete-token">
|
<div class="ui g-modal-confirm delete modal" id="delete-token">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
{{svg "octicon-trash"}}
|
{{svg "octicon-trash"}}
|
||||||
|
|||||||
@@ -86,34 +86,3 @@
|
|||||||
max-width: unset;
|
max-width: unset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Wikilinks: red links for non-existent pages */
|
|
||||||
.wiki .wiki-link-new {
|
|
||||||
color: var(--color-red);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wiki .wiki-link-new:hover {
|
|
||||||
color: var(--color-red);
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Wiki inline ToC */
|
|
||||||
.wiki .wiki-toc-inline {
|
|
||||||
border: 1px solid var(--color-secondary);
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 8px 16px;
|
|
||||||
background: var(--color-box-body);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wiki .wiki-toc-inline summary {
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sticky sidebar ToC */
|
|
||||||
.wiki .wiki-content-toc {
|
|
||||||
position: sticky;
|
|
||||||
top: 16px;
|
|
||||||
max-height: calc(100vh - 100px);
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user