Compare commits

..

1 Commits

Author SHA1 Message Date
Jonathan Miller add7c0da4d feat: make metadata/manifest GET endpoint publicly accessible (#676)
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Successful in 11s
PR RC Release / Build RC Release (pull_request) Failing after 54s
Universal: PR Check / Secret Scan (pull_request) Successful in 54s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 42s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Publish to Composer / Publish Package (release) Failing after 5s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
Remove reqRepoReader auth requirement from GET /repos/{owner}/{repo}/metadata
and /manifest endpoints. PUT (update) still requires token + admin.
2026-06-21 10:18:07 -05:00
52 changed files with 1979 additions and 3907 deletions
+5 -5
View File
@@ -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")
+1 -1
View File
@@ -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
+17 -63
View File
@@ -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
+10 -10
View File
@@ -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
+76
View File
@@ -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
-109
View File
@@ -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"
+4 -4
View File
@@ -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" \
-16
View File
@@ -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)
+4 -4
View File
@@ -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 \
+51
View File
@@ -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 }}
+23 -10
View File
@@ -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."
+5 -21
View File
@@ -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 \
+37 -25
View File
@@ -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."
+82
View File
@@ -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
-130
View File
@@ -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
+4 -12
View File
@@ -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: |
-44
View File
@@ -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
+23 -15
View File
@@ -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
![Language](https://img.shields.io/badge/Go-00ADD8?style=flat-square&logo=go&logoColor=white) ![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green?style=flat-square) ![Language](https://img.shields.io/badge/Go-00ADD8?style=flat-square&logo=go&logoColor=white) ![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green?style=flat-square) ![Wiki](https://img.shields.io/badge/wiki-MokoGitea-blue?style=flat-square)
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)*
+237
View File
@@ -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
+2 -16
View File
@@ -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
+3 -3
View File
@@ -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() {
+6 -32
View File
@@ -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
} }
-1
View File
@@ -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
} }
-23
View File
@@ -1,23 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import (
"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))
}
-10
View File
@@ -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'"`
} }
-1
View File
@@ -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"`
} }
-10
View File
@@ -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
-2
View File
@@ -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)",
+2 -8
View File
@@ -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
+3 -12
View File
@@ -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)
} }
-26
View File
@@ -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)
+22 -72
View File
@@ -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
View File
@@ -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 {
-100
View File
@@ -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
-5
View File
@@ -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
View File
@@ -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.
-8
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
-53
View File
@@ -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 {
-1
View File
@@ -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}}
+12 -37
View File
@@ -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>
+5 -1
View File
@@ -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.">
-42
View File
@@ -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" .}}
-36
View File
@@ -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" .}}
-51
View File
@@ -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>
&middot;
<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> &mdash; {{.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" .}}
-40
View File
@@ -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 &middot; {{.Title}}
</p>
</body>
</html>
-72
View File
@@ -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" .}}
-52
View File
@@ -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" .}}
+2 -33
View File
@@ -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>
-80
View File
@@ -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"}}
-31
View File
@@ -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;
}