Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dee280a98d | |||
| 7174fd0007 | |||
| 46f1a88543 | |||
| cd026ad481 | |||
| dbb83fd76a | |||
| 2aef425eda | |||
| 3d118bbc1f | |||
| 74a865f671 | |||
| d362d7a997 | |||
| 98b7b00750 | |||
| 3cef1d542a | |||
| ab4b89508d | |||
| c688c3d5c4 | |||
| 81d065b9bb | |||
| c5b15a8ea9 | |||
| 3267fa42b1 | |||
| 8684ce0e3e | |||
| dbcbff79ee | |||
| 42ea214a64 | |||
| d16b417a24 | |||
| fdde590491 | |||
| 2708f61a53 | |||
| 55639a6453 | |||
| afc3fc6f16 | |||
| 0cb94ef37d | |||
| 6f482e7fdb | |||
| 1be5a89286 | |||
| 675a14615a | |||
| 2d14619461 | |||
| ca37d9728e | |||
| 232e7bcf83 | |||
| 5de6ace021 | |||
| dd864c2268 | |||
| 245a74024b | |||
| ec913d7993 | |||
| 8ff637684a | |||
| d0e39c027b | |||
| acefbe6e44 | |||
| f430a4c10d | |||
| db1d97072a | |||
| d37da15aa7 | |||
| f31a60cf53 | |||
| 871ede1450 | |||
| e16ea6f934 | |||
| c610c2388d | |||
| cb2ff768e5 | |||
| 78f1d8fe15 | |||
| 8c121c08c1 | |||
| 423c3c0f83 | |||
| 411bed0d05 | |||
| 7a71ebd0b1 | |||
| 6dd0f1fb6d | |||
| 1f73a14cb0 | |||
| 2c0d71a27f | |||
| 584c50a4be |
+6
-1
@@ -63,7 +63,12 @@ cpu.out
|
||||
/public/assets/fonts
|
||||
/public/assets/img/avatar
|
||||
/vendor
|
||||
/VERSION
|
||||
# MokoGIT: keep the committed VERSION file in the Docker build context. The Makefile
|
||||
# reads it as STORED_VERSION, which takes precedence over `git describe`, so the built
|
||||
# binary reports the MokoOrgStandards xx.xx.xx version (e.g. 01.00.00) instead of the
|
||||
# leaked upstream Gitea `git describe` version. (Upstream excludes it because VERSION is
|
||||
# normally a generated release-tarball artifact; here it is intentionally committed.)
|
||||
# /VERSION
|
||||
/.air
|
||||
/.go-licenses
|
||||
/Dockerfile
|
||||
|
||||
@@ -115,6 +115,13 @@ prime/
|
||||
/.github/copilot-instructions.md
|
||||
/llms.txt
|
||||
|
||||
# Real deploy env files -- NEVER commit secrets. Keep the *.env.example templates.
|
||||
.env
|
||||
*.env
|
||||
!*.env.example
|
||||
!.env.example
|
||||
deploy/**/.env
|
||||
|
||||
# Ignore worktrees when working on multiple branches
|
||||
.worktrees/
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# DEFGROUP: MokoGIT.Workflow
|
||||
# INGROUP: mokocli.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
# PATH: /.mokogitea/workflows/auto-bump.yml
|
||||
# PATH: /.mokogit/workflows/auto-bump.yml
|
||||
# VERSION: 09.02.00
|
||||
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
||||
|
||||
@@ -22,7 +22,7 @@ on:
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
token: ${{ secrets.MOKOGIT_TOKEN }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup mokocli tools
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV"
|
||||
else
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \
|
||||
"https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \
|
||||
/tmp/mokocli
|
||||
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
||||
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV"
|
||||
@@ -62,5 +62,5 @@ jobs:
|
||||
run: |
|
||||
php ${MOKO_CLI}/version_auto_bump.php \
|
||||
--path . --branch "${GITHUB_REF_NAME}" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
--token "${{ secrets.MOKOGIT_TOKEN }}" \
|
||||
--repo-url "https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
@@ -31,7 +31,7 @@ on:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- '.mokogitea/workflows/**'
|
||||
- '.mokogit/workflows/**'
|
||||
- '*.md'
|
||||
- 'wiki/**'
|
||||
- '.editorconfig'
|
||||
@@ -52,9 +52,9 @@ on:
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||
MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
|
||||
GIT_ORG: ${{ vars.MOKOGIT_ORG || github.repository_owner }}
|
||||
GIT_REPO: ${{ vars.MOKOGIT_REPO || github.event.repository.name }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -73,12 +73,12 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
token: ${{ secrets.MOKOGIT_TOKEN }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup mokocli tools
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
run: |
|
||||
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
|
||||
@@ -99,8 +99,8 @@ jobs:
|
||||
|
||||
- name: Rename branch to rc
|
||||
run: |
|
||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
AUTH="Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
||||
AUTH="Authorization: token ${{ secrets.MOKOGIT_TOKEN }}"
|
||||
FROM="${{ github.event.pull_request.head.ref || 'dev' }}"
|
||||
PR="${{ github.event.pull_request.number }}"
|
||||
|
||||
@@ -110,11 +110,11 @@ jobs:
|
||||
SRC_SHA=$(printf '%s' "$SRC_JSON" | python3 -c "import sys, json; print(json.load(sys.stdin)['commit']['id'])" 2>/dev/null || true)
|
||||
[ -n "$SRC_SHA" ] || { echo "::error::Could not resolve HEAD of ${FROM}"; exit 1; }
|
||||
|
||||
# Point rc at the source commit via git push. Gitea's git/refs PATCH API
|
||||
# Point rc at the source commit via git push. Git's git/refs PATCH API
|
||||
# returns HTTP 405 on ANY protected branch (force or not, even for a user in
|
||||
# the force-push allowlist), so it cannot move a protected rc. git push honors
|
||||
# the push + force-push allowlists and creates rc if it is absent.
|
||||
PUSH_URL="https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@${MOKOGITEA_URL#https://}/${GITEA_ORG}/${GITEA_REPO}.git"
|
||||
PUSH_URL="https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@${MOKOGIT_URL#https://}/${GIT_ORG}/${GIT_REPO}.git"
|
||||
git config --global user.name "mokogit-actions[bot]"
|
||||
git config --global user.email "actions@mokoconsulting.tech"
|
||||
git fetch --no-tags "$PUSH_URL" "${FROM}"
|
||||
@@ -139,7 +139,7 @@ jobs:
|
||||
- name: Trigger RC deploy
|
||||
run: |
|
||||
# Workflow-token pushes do NOT wake downstream workflows; dispatch deploy-rc explicitly.
|
||||
curl -sf -X POST -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \n -H "Content-Type: application/json" \n "${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}/actions/workflows/deploy-rc.yml/dispatches" \n -d '{"ref":"rc"}' \n && echo "Dispatched deploy-rc on rc" \n || echo "::warning::deploy-rc dispatch failed (no deploy-rc.yml on rc?)"
|
||||
curl -sf -X POST -H "Authorization: token ${{ secrets.MOKOGIT_TOKEN }}" \n -H "Content-Type: application/json" \n "${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}/actions/workflows/deploy-rc.yml/dispatches" \n -d '{"ref":"rc"}' \n && echo "Dispatched deploy-rc on rc" \n || echo "::warning::deploy-rc dispatch failed (no deploy-rc.yml on rc?)"
|
||||
|
||||
- name: Checkout rc and configure git
|
||||
run: |
|
||||
@@ -147,20 +147,20 @@ jobs:
|
||||
git checkout rc
|
||||
git config --local user.email "mokogit-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "mokogit-actions[bot]"
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
|
||||
- name: Publish RC release
|
||||
continue-on-error: true
|
||||
run: |
|
||||
php ${MOKO_CLI}/release_publish.php \
|
||||
--path . --stability rc --bump minor --branch rc \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
--token "${{ secrets.MOKOGIT_TOKEN }}"
|
||||
|
||||
- name: Update RC release notes from CHANGELOG.md
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGIT_TOKEN }}"
|
||||
|
||||
# Extract [Unreleased] section from changelog
|
||||
NOTES=""
|
||||
@@ -209,14 +209,14 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
token: ${{ secrets.MOKOGIT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure git for bot pushes
|
||||
run: |
|
||||
git config --local user.email "mokogit-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "mokogit-actions[bot]"
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
|
||||
- name: Check for merge conflict markers
|
||||
run: |
|
||||
@@ -233,7 +233,7 @@ jobs:
|
||||
|
||||
- name: Setup mokocli tools
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
||||
run: |
|
||||
@@ -280,7 +280,7 @@ jobs:
|
||||
fi
|
||||
php ${MOKO_CLI}/release_publish.php \
|
||||
--path . --stability stable ${BUMP_FLAG} --branch main \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
--token "${{ secrets.MOKOGIT_TOKEN }}"
|
||||
|
||||
- name: "Read published version"
|
||||
id: version
|
||||
@@ -307,8 +307,8 @@ jobs:
|
||||
!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 }}"
|
||||
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGIT_TOKEN }}"
|
||||
SEMVER_TAG="v${VERSION}"
|
||||
|
||||
echo "Creating semver tag: ${SEMVER_TAG}"
|
||||
@@ -332,8 +332,8 @@ jobs:
|
||||
|
||||
- name: Update release notes and promote changelog
|
||||
run: |
|
||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGIT_TOKEN }}"
|
||||
|
||||
# Get the stable release info (version and ID)
|
||||
RELEASE_JSON=$(curl -sf -H "Authorization: token ${TOKEN}" \
|
||||
@@ -401,10 +401,10 @@ jobs:
|
||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
||||
php ${MOKO_CLI}/release_mirror.php \
|
||||
--version "$VERSION" --tag "$RELEASE_TAG" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||
--token "${{ secrets.MOKOGIT_TOKEN }}" --api-base "$API_BASE" \
|
||||
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
||||
--branch main 2>&1 || true
|
||||
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -430,8 +430,8 @@ jobs:
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGIT_TOKEN }}"
|
||||
|
||||
# Delete rc branch (ephemeral — created by promote-rc)
|
||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||
@@ -454,8 +454,8 @@ jobs:
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGIT_TOKEN }}"
|
||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||
BRANCH_NAME="version/${VERSION}"
|
||||
MAIN_SHA=$(git rev-parse HEAD)
|
||||
@@ -475,9 +475,9 @@ jobs:
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
||||
php ${MOKO_CLI}/version_reset_dev.php \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
||||
--token "${{ secrets.MOKOGIT_TOKEN }}" --api-base "${API_BASE}" \
|
||||
--branch dev --path . 2>&1 || true
|
||||
|
||||
# -- Summary --------------------------------------------------------------
|
||||
@@ -501,5 +501,5 @@ jobs:
|
||||
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $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](${MOKOGIT_URL}/${GIT_ORG}/${GIT_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
@@ -4,9 +4,9 @@
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: MokoGIT.Workflow
|
||||
# INGROUP: MokoStandards.Universal
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
# PATH: /.mokogitea/workflows/branch-cleanup.yml
|
||||
# INGROUP: MokoCLI.Universal
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||
# PATH: /.mokogit/workflows/branch-cleanup.yml
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Delete feature branches after PR merge
|
||||
|
||||
@@ -30,13 +30,25 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Delete source branch
|
||||
# SECURITY: the PR head ref is attacker-controllable. Pass it (and the
|
||||
# repo/token) through env so the Actions engine cannot splice it into the
|
||||
# shell source, and validate it to a safe charset before use.
|
||||
env:
|
||||
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
API_BASE: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
|
||||
BRANCH: ${{ github.event.pull_request.head.ref }}
|
||||
run: |
|
||||
BRANCH="${{ github.event.pull_request.head.ref }}"
|
||||
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
|
||||
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
|
||||
set -euo pipefail
|
||||
if ! printf '%s' "${BRANCH}" | grep -Eq '^[A-Za-z0-9._/-]+$'; then
|
||||
echo "::error::unsafe branch name; refusing to proceed"; exit 1
|
||||
fi
|
||||
API="${API_BASE}/api/v1/repos/${REPO}/branches"
|
||||
# URL-encode the branch name's slashes (no PHP dependency on the runner)
|
||||
ENCODED=$(printf '%s' "${BRANCH}" | sed 's|/|%2F|g')
|
||||
|
||||
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
|
||||
-H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
-H "Authorization: token ${MOKOGIT_TOKEN}" \
|
||||
"${API}/${ENCODED}" 2>/dev/null || true)
|
||||
|
||||
if [ "$STATUS" = "204" ]; then
|
||||
@@ -6,7 +6,7 @@
|
||||
# DEFGROUP: MokoGIT.Workflow
|
||||
# INGROUP: MokoStandards.CI
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||
# PATH: /.gitea/workflows/ci-generic.yml
|
||||
# PATH: /.mokogit/workflows/ci-generic.yml
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: CI pipeline — lint, validate, and test for generic projects (PHP + Node.js)
|
||||
|
||||
+21
-14
@@ -4,11 +4,11 @@
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: MokoGIT.Workflow
|
||||
# INGROUP: mokocli.Universal
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
# PATH: /.mokogitea/workflows/ci-issue-reporter.yml
|
||||
# INGROUP: MokoCLI.Universal
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||
# PATH: /.mokogit/workflows/ci-issue-reporter.yml
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Reusable workflow — creates/updates a MokoGIT issue when a CI gate fails.
|
||||
# BRIEF: Reusable workflow � creates/updates a MokoGIT issue when a CI gate fails.
|
||||
# Clones MokoCLI and runs cli/ci_issue_reporter.sh.
|
||||
|
||||
name: "Universal: CI Issue Reporter"
|
||||
@@ -35,7 +35,7 @@ on:
|
||||
type: string
|
||||
default: ""
|
||||
secrets:
|
||||
MOKOGITEA_TOKEN:
|
||||
MOKOGIT_TOKEN:
|
||||
required: true
|
||||
|
||||
env:
|
||||
@@ -49,20 +49,27 @@ jobs:
|
||||
steps:
|
||||
- name: Clone MokoCLI
|
||||
env:
|
||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
||||
MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
|
||||
run: |
|
||||
MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
|
||||
git clone --depth 1 --filter=blob:none --sparse "${MOKOGITEA_URL}/MokoConsulting/MokoCLI.git" /tmp/mokocli
|
||||
git clone --depth 1 --filter=blob:none --sparse "${MOKOGIT_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
|
||||
cd /tmp/mokocli && git sparse-checkout set cli/ci_issue_reporter.sh
|
||||
|
||||
- name: Report CI failure
|
||||
env:
|
||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
||||
MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
|
||||
# Route all workflow_call inputs through env vars — referencing ${{ inputs.* }}
|
||||
# directly in a run: block interpolates at YAML-parse time and is a shell-injection
|
||||
# vector. Shell variables are not re-parsed, so a crafted input can't break out.
|
||||
INPUT_GATE: ${{ inputs.gate }}
|
||||
INPUT_DETAILS: ${{ inputs.details }}
|
||||
INPUT_SEVERITY: ${{ inputs.severity }}
|
||||
INPUT_WORKFLOW: ${{ inputs.workflow }}
|
||||
run: |
|
||||
chmod +x /tmp/mokocli/cli/ci_issue_reporter.sh
|
||||
/tmp/mokocli/cli/ci_issue_reporter.sh \
|
||||
--gate "${{ inputs.gate }}" \
|
||||
--details "${{ inputs.details }}" \
|
||||
--severity "${{ inputs.severity }}" \
|
||||
--workflow "${{ inputs.workflow }}"
|
||||
--gate "$INPUT_GATE" \
|
||||
--details "$INPUT_DETAILS" \
|
||||
--severity "$INPUT_SEVERITY" \
|
||||
--workflow "$INPUT_WORKFLOW"
|
||||
@@ -6,7 +6,7 @@
|
||||
# DEFGROUP: MokoGIT.Workflow
|
||||
# INGROUP: MokoStandards.Maintenance
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||
# PATH: /.gitea/workflows/cleanup.yml
|
||||
# PATH: /.mokogit/workflows/cleanup.yml
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
||||
|
||||
@@ -21,7 +21,7 @@ permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
@@ -33,17 +33,17 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
token: ${{ secrets.MOKOGIT_TOKEN }}
|
||||
|
||||
- name: Delete merged branches
|
||||
env:
|
||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
||||
run: |
|
||||
echo "=== Merged Branch Cleanup ==="
|
||||
API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||
API="${MOKOGIT_URL}/api/v1/repos/${{ github.repository }}"
|
||||
|
||||
# List branches via API
|
||||
BRANCHES=$(curl -sS -H "Authorization: token ${MOKOGITEA_TOKEN}" \
|
||||
BRANCHES=$(curl -sS -H "Authorization: token ${MOKOGIT_TOKEN}" \
|
||||
"${API}/branches?limit=50" | jq -r '.[].name')
|
||||
|
||||
DELETED=0
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
# Check if branch is merged into main
|
||||
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
|
||||
echo " Deleting merged branch: ${BRANCH}"
|
||||
curl -sS -X DELETE -H "Authorization: token ${MOKOGITEA_TOKEN}" \
|
||||
curl -sS -X DELETE -H "Authorization: token ${MOKOGIT_TOKEN}" \
|
||||
"${API}/branches/${BRANCH}" 2>/dev/null || true
|
||||
DELETED=$((DELETED + 1))
|
||||
fi
|
||||
@@ -66,20 +66,20 @@ jobs:
|
||||
|
||||
- name: Clean old workflow runs
|
||||
env:
|
||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
||||
run: |
|
||||
echo "=== Workflow Run Cleanup ==="
|
||||
API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||
API="${MOKOGIT_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)
|
||||
|
||||
# Get old completed runs
|
||||
RUNS=$(curl -sS -H "Authorization: token ${MOKOGITEA_TOKEN}" \
|
||||
RUNS=$(curl -sS -H "Authorization: token ${MOKOGIT_TOKEN}" \
|
||||
"${API}/actions/runs?status=completed&limit=50" | \
|
||||
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
|
||||
|
||||
DELETED=0
|
||||
for RUN_ID in $RUNS; do
|
||||
curl -sS -X DELETE -H "Authorization: token ${MOKOGITEA_TOKEN}" \
|
||||
curl -sS -X DELETE -H "Authorization: token ${MOKOGIT_TOKEN}" \
|
||||
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
|
||||
DELETED=$((DELETED + 1))
|
||||
done
|
||||
@@ -0,0 +1,83 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: MokoGIT.Workflow
|
||||
# INGROUP: mokocli.Universal
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
# PATH: /.mokogit/workflows/workflow-sync-trigger.yml
|
||||
# VERSION: 01.01.00
|
||||
# BRIEF: Trigger workflow sync to live repos when a PR is merged to main
|
||||
|
||||
name: "Universal: Workflow Sync Trigger"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
name: Sync workflows to live repos
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
startsWith(github.event.repository.name, 'Template-') &&
|
||||
(github.event_name == 'workflow_dispatch' ||
|
||||
(github.event.pull_request.merged == true &&
|
||||
!contains(github.event.pull_request.title, '[skip sync]')))
|
||||
|
||||
steps:
|
||||
- name: Determine platform from repo name
|
||||
id: platform
|
||||
run: |
|
||||
REPO="${{ github.event.repository.name }}"
|
||||
case "$REPO" in
|
||||
Template-Joomla) PLATFORM="joomla" ;;
|
||||
Template-Dolibarr) PLATFORM="dolibarr" ;;
|
||||
Template-Go) PLATFORM="go" ;;
|
||||
Template-NPM) PLATFORM="npm" ;;
|
||||
Template-Generic) PLATFORM="" ;;
|
||||
*) PLATFORM="" ;;
|
||||
esac
|
||||
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
||||
echo "Platform: ${PLATFORM:-all}"
|
||||
|
||||
- name: Clone mokocli
|
||||
env:
|
||||
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
||||
run: |
|
||||
MOKOGIT_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
|
||||
git clone --depth 1 "${MOKOGIT_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
|
||||
run: |
|
||||
cd /tmp/mokocli
|
||||
composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
||||
|
||||
- name: Run workflow sync
|
||||
env:
|
||||
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
||||
run: |
|
||||
ARGS="--token ${MOKOGIT_TOKEN}"
|
||||
ARGS="${ARGS} --org ${{ vars.GITEA_ORG || github.repository_owner }}"
|
||||
ARGS="${ARGS} --phase repos"
|
||||
ARGS="${ARGS} --delete-orphans"
|
||||
|
||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||
if [ -n "$PLATFORM" ]; then
|
||||
ARGS="${ARGS} --platform-filter ${PLATFORM}"
|
||||
fi
|
||||
|
||||
php /tmp/mokocli/cli/workflow_sync.php ${ARGS}
|
||||
@@ -18,7 +18,7 @@
|
||||
# DEV_HEALTH_URL - external URL to verify after deploy
|
||||
# Required SECRETS (already configured org-wide; reused, not re-set):
|
||||
# DEPLOY_SSH_KEY - deploy private key (repo/org secret)
|
||||
# MOKOGITEA_TOKEN - registry/API token (org secret)
|
||||
# MOKOGIT_TOKEN - registry/API token (org secret)
|
||||
|
||||
name: Deploy (Dev)
|
||||
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
|
||||
- name: Build and deploy to RC via SSH
|
||||
env:
|
||||
REGISTRY_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
REGISTRY_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
||||
TAG: ${{ steps.config.outputs.tag }}
|
||||
run: |
|
||||
# Runner-side values (TAG, REGISTRY_TOKEN) are injected into the remote shell
|
||||
@@ -18,7 +18,7 @@
|
||||
# PROD_HEALTH_URL - external URL to verify after deploy
|
||||
# Required SECRETS (already configured org-wide; reused, not re-set):
|
||||
# DEPLOY_SSH_KEY - deploy private key (repo/org secret)
|
||||
# MOKOGITEA_TOKEN - registry/API token (org secret)
|
||||
# MOKOGIT_TOKEN - registry/API token (org secret)
|
||||
|
||||
name: Deploy (Prod)
|
||||
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
|
||||
- name: Build and deploy to RC via SSH
|
||||
env:
|
||||
REGISTRY_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
REGISTRY_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
||||
TAG: ${{ steps.config.outputs.tag }}
|
||||
run: |
|
||||
# Runner-side values (TAG, REGISTRY_TOKEN) are injected into the remote shell
|
||||
@@ -18,7 +18,7 @@
|
||||
# RC_HEALTH_URL - external URL to verify after deploy
|
||||
# Required SECRETS (already configured org-wide; reused, not re-set):
|
||||
# DEPLOY_SSH_KEY - deploy private key (repo/org secret)
|
||||
# MOKOGITEA_TOKEN - registry/API token (org secret)
|
||||
# MOKOGIT_TOKEN - registry/API token (org secret)
|
||||
|
||||
name: Deploy (RC)
|
||||
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
|
||||
- name: Build and deploy to RC via SSH
|
||||
env:
|
||||
REGISTRY_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
REGISTRY_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
||||
TAG: ${{ steps.config.outputs.tag }}
|
||||
run: |
|
||||
# Runner-side values (TAG, REGISTRY_TOKEN) are injected into the remote shell
|
||||
@@ -34,7 +34,7 @@ permissions:
|
||||
|
||||
env:
|
||||
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
||||
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
|
||||
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'git-security' }}
|
||||
|
||||
jobs:
|
||||
gitleaks:
|
||||
@@ -4,7 +4,7 @@
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: MokoGIT.Workflow
|
||||
# INGROUP: mokocli.Automation
|
||||
# INGROUP: MokoCLI.Automation
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
@@ -19,7 +19,7 @@ permissions:
|
||||
issues: write
|
||||
|
||||
env:
|
||||
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
|
||||
|
||||
jobs:
|
||||
create-branch:
|
||||
@@ -27,14 +27,22 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Create branch and comment
|
||||
# SECURITY: never interpolate github.event.* into a run: script — the
|
||||
# Actions engine splices the raw value into the shell source (command
|
||||
# injection via a crafted issue title). Pass everything through env so
|
||||
# the values arrive as ordinary shell variables that are not re-parsed.
|
||||
env:
|
||||
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
ISSUE_NUM: ${{ github.event.issue.number }}
|
||||
ISSUE_TITLE: ${{ github.event.issue.title }}
|
||||
run: |
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||
ISSUE_NUM="${{ github.event.issue.number }}"
|
||||
ISSUE_TITLE="${{ github.event.issue.title }}"
|
||||
TOKEN="${MOKOGIT_TOKEN}"
|
||||
API="${MOKOGIT_URL}/api/v1/repos/${REPO}"
|
||||
|
||||
# Build slug from title: lowercase, replace non-alnum with dash, trim
|
||||
SLUG=$(echo "${ISSUE_TITLE}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//' | cut -c1-40)
|
||||
# Build slug from title: lowercase, replace non-alnum with dash, trim.
|
||||
# printf (not echo) so a title beginning with "-" is not read as flags.
|
||||
SLUG=$(printf '%s' "${ISSUE_TITLE}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//' | cut -c1-40)
|
||||
BRANCH="feature/${ISSUE_NUM}-${SLUG}"
|
||||
|
||||
# Check dev branch exists
|
||||
@@ -58,7 +66,7 @@ jobs:
|
||||
echo "Created branch: ${BRANCH}"
|
||||
|
||||
# Comment on issue with branch link
|
||||
REPO_URL="${MOKOGITEA_URL}/${{ github.repository }}"
|
||||
REPO_URL="${MOKOGIT_URL}/${REPO}"
|
||||
BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`"
|
||||
|
||||
curl -sf -X POST \
|
||||
@@ -6,7 +6,7 @@
|
||||
# DEFGROUP: MokoGIT.Workflow
|
||||
# INGROUP: MokoStandards.Notifications
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||
# PATH: /.gitea/workflows/notify.yml
|
||||
# PATH: /.mokogit/workflows/notify.yml
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
||||
|
||||
@@ -26,7 +26,7 @@ permissions:
|
||||
|
||||
env:
|
||||
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
||||
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-releases' }}
|
||||
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'git-releases' }}
|
||||
|
||||
jobs:
|
||||
notify:
|
||||
@@ -497,12 +497,12 @@ jobs:
|
||||
steps:
|
||||
- name: Trigger RC pre-release
|
||||
env:
|
||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
BRANCH: ${{ github.head_ref }}
|
||||
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
|
||||
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 "${MOKOGIT_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${MOKOGIT_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
|
||||
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
@@ -513,7 +513,7 @@ jobs:
|
||||
if: >-
|
||||
always() &&
|
||||
needs.validate.result == 'failure'
|
||||
uses: ./.mokogitea/workflows/ci-issue-reporter.yml
|
||||
uses: ./.mokogit/workflows/ci-issue-reporter.yml
|
||||
with:
|
||||
gate: "PR Validation"
|
||||
workflow: "PR Check"
|
||||
@@ -40,9 +40,9 @@ permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||
GIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
|
||||
GIT_ORG: ${{ vars.MOKOGIT_ORG || github.repository_owner }}
|
||||
GIT_REPO: ${{ vars.MOKOGIT_REPO || github.event.repository.name }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
token: ${{ secrets.MOKOGIT_TOKEN }}
|
||||
ref: ${{ github.ref_name }}
|
||||
submodules: recursive
|
||||
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
|
||||
- name: Setup mokocli tools
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
run: |
|
||||
# Use pre-installed /opt/mokocli if available (updated by cron every 6h)
|
||||
@@ -154,7 +154,7 @@ jobs:
|
||||
# Commit version bump
|
||||
git config --local user.email "mokogit-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "mokogit-actions[bot]"
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
git add -A
|
||||
git diff --cached --quiet || {
|
||||
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
|
||||
@@ -164,12 +164,12 @@ jobs:
|
||||
# Auto-detect element via manifest_element.php
|
||||
php ${MOKO_CLI}/manifest_element.php \
|
||||
--path . --version "$VERSION" --stability "$STABILITY" \
|
||||
--repo "${GITEA_REPO}" --github-output
|
||||
--repo "${GIT_REPO}" --github-output
|
||||
|
||||
# Read back element outputs
|
||||
EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
|
||||
ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
|
||||
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
||||
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GIT_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
||||
[ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
|
||||
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
@@ -187,18 +187,18 @@ jobs:
|
||||
run: |
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
API_BASE="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
||||
php ${MOKO_CLI}/release_create.php \
|
||||
--path . --version "$VERSION" --tag "$TAG" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
|
||||
--token "${{ secrets.MOKOGIT_TOKEN }}" --api-base "$API_BASE" \
|
||||
--repo "${GIT_REPO}" --branch "${{ github.ref_name }}" --prerelease
|
||||
|
||||
- name: Update release notes from CHANGELOG.md
|
||||
if: steps.eligibility.outputs.proceed == 'true'
|
||||
run: |
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
API_BASE="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
||||
|
||||
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
|
||||
if [ -f "CHANGELOG.md" ]; then
|
||||
@@ -209,7 +209,7 @@ jobs:
|
||||
fi
|
||||
|
||||
# Update release body via API
|
||||
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGIT_TOKEN }}" \
|
||||
"${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||
|
||||
if [ -n "$RELEASE_ID" ]; then
|
||||
@@ -221,7 +221,7 @@ jobs:
|
||||
'${API_BASE}/releases/${RELEASE_ID}',
|
||||
data=payload, method='PATCH',
|
||||
headers={
|
||||
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
|
||||
'Authorization': 'token ${{ secrets.MOKOGIT_TOKEN }}',
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
urllib.request.urlopen(req)
|
||||
@@ -235,11 +235,11 @@ jobs:
|
||||
run: |
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
API_BASE="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
||||
php ${MOKO_CLI}/release_package.php \
|
||||
--path . --version "$VERSION" --tag "$TAG" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||
--repo "${GITEA_REPO}" --output /tmp || true
|
||||
--token "${{ secrets.MOKOGIT_TOKEN }}" --api-base "$API_BASE" \
|
||||
--repo "${GIT_REPO}" --output /tmp || true
|
||||
|
||||
# updates.xml is generated dynamically by MokoGIT license server
|
||||
# No need to build, commit, or sync updates.xml from workflows
|
||||
@@ -248,8 +248,8 @@ jobs:
|
||||
if: steps.eligibility.outputs.proceed == 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
API_BASE="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGIT_TOKEN }}"
|
||||
|
||||
php ${MOKO_CLI}/release_cascade.php \
|
||||
--stability "${{ steps.meta.outputs.stability }}" \
|
||||
@@ -6,7 +6,7 @@
|
||||
# DEFGROUP: MokoGIT.Workflow
|
||||
# INGROUP: mokocli.Universal
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
# PATH: /.mokogitea/workflows/rc-revert.yml
|
||||
# PATH: /.mokogit/workflows/rc-revert.yml
|
||||
# VERSION: 09.23.00
|
||||
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
|
||||
|
||||
@@ -32,8 +32,8 @@ jobs:
|
||||
env:
|
||||
BRANCH: ${{ github.event.pull_request.head.ref }}
|
||||
REPO: ${{ github.repository }}
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
GIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
|
||||
TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# BRANCH is attacker-controlled (PR head ref). Strict allowlist before ANY use.
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
fi
|
||||
SUFFIX="${BRANCH#rc/}"
|
||||
DEV_BRANCH="dev/${SUFFIX}"
|
||||
API="${GITEA_URL}/api/v1/repos/${REPO}/branches"
|
||||
API="${GIT_URL}/api/v1/repos/${REPO}/branches"
|
||||
|
||||
# Create dev/ branch from rc/ branch
|
||||
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \
|
||||
@@ -45,7 +45,7 @@ env:
|
||||
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
|
||||
|
||||
# Repo health policy
|
||||
REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.mokogitea/workflows/
|
||||
REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.mokogit/workflows/
|
||||
REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/
|
||||
REPO_DISALLOWED_DIRS:
|
||||
REPO_DISALLOWED_FILES: TODO.md,todo.md
|
||||
@@ -56,7 +56,7 @@ env:
|
||||
# File / directory variables
|
||||
DOCS_INDEX: docs/docs-index.md
|
||||
SCRIPT_DIR: scripts
|
||||
WORKFLOWS_DIR: .mokogitea/workflows
|
||||
WORKFLOWS_DIR: .mokogit/workflows
|
||||
SHELLCHECK_PATTERN: '*.sh'
|
||||
SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml'
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
- name: Check actor permission (admin only)
|
||||
id: perm
|
||||
env:
|
||||
TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }}
|
||||
TOKEN: ${{ secrets.MOKOGIT_TOKEN || github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
ACTOR: ${{ github.actor }}
|
||||
run: |
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
;;
|
||||
*)
|
||||
# Detect platform and check permissions via API
|
||||
API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}"
|
||||
API_BASE="${GITHUB_API_URL:-${GIT_API_URL:-https://api.github.com}}"
|
||||
RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \
|
||||
"${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}')
|
||||
PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown")
|
||||
@@ -677,7 +677,7 @@ jobs:
|
||||
if: >-
|
||||
always() &&
|
||||
needs.scripts_governance.result == 'failure'
|
||||
uses: ./.mokogitea/workflows/ci-issue-reporter.yml
|
||||
uses: ./.mokogit/workflows/ci-issue-reporter.yml
|
||||
with:
|
||||
gate: "Scripts Governance"
|
||||
workflow: "Repo Health"
|
||||
@@ -691,7 +691,7 @@ jobs:
|
||||
if: >-
|
||||
always() &&
|
||||
needs.repo_health.result == 'failure'
|
||||
uses: ./.mokogitea/workflows/ci-issue-reporter.yml
|
||||
uses: ./.mokogit/workflows/ci-issue-reporter.yml
|
||||
with:
|
||||
gate: "Repository Health"
|
||||
workflow: "Repo Health"
|
||||
+11
-11
@@ -4,7 +4,7 @@
|
||||
# DEFGROUP: GitHub.Workflow
|
||||
# INGROUP: MokoCLI.Compliance
|
||||
# REPO: https://github.com/mokoconsulting-tech/mokocli
|
||||
# PATH: /.mokogitea/workflows/standards-compliance.yml
|
||||
# PATH: /.mokogit/workflows/standards-compliance.yml
|
||||
# VERSION: 04.06.00
|
||||
# BRIEF: MokoCLI compliance validation workflow
|
||||
# NOTE: Validates repository structure, documentation, and coding standards
|
||||
@@ -320,7 +320,7 @@ jobs:
|
||||
echo "### 🔴 Critical Issues: $MISSING" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Remediation Steps:**" >> $GITHUB_STEP_SUMMARY
|
||||
[ ! -d ".mokogit" ] && echo "- Create .mokogit directory: \`mkdir -p .mokogitea/workflows\`" >> $GITHUB_STEP_SUMMARY
|
||||
[ ! -d ".mokogit" ] && echo "- Create .mokogit directory: \`mkdir -p .mokogit/workflows\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "📚 Reference: [MokoCLI Repository Structure](https://github.com/mokoconsulting-tech/mokocli/tree/main/docs/policy/core-structure.md)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -563,17 +563,17 @@ jobs:
|
||||
set -x
|
||||
echo "### GitHub Actions Workflows" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
WORKFLOWS_DIR=".mokogitea/workflows"
|
||||
WORKFLOWS_DIR=".mokogit/workflows"
|
||||
|
||||
if [ ! -d "$WORKFLOWS_DIR" ]; then
|
||||
echo "❌ No workflows directory found" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### ❌ Validation Failed: Workflows Directory Missing" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Error:** .mokogitea/workflows directory is required for CI/CD automation" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Action Required:** Create .mokogitea/workflows directory and add GitHub Actions workflows" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Error:** .mokogit/workflows directory is required for CI/CD automation" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Action Required:** Create .mokogit/workflows directory and add GitHub Actions workflows" >> $GITHUB_STEP_SUMMARY
|
||||
echo ""
|
||||
echo "❌ ERROR: .mokogitea/workflows directory not found"
|
||||
echo "❌ ERROR: .mokogit/workflows directory not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -612,7 +612,7 @@ jobs:
|
||||
echo "### Workflow YAML Syntax" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
INVALID=0
|
||||
for workflow in $(find .mokogitea/workflows -maxdepth 1 -type f \( -name "*.yml" -o -name "*.yaml" \) 2>/dev/null); do
|
||||
for workflow in $(find .mokogit/workflows -maxdepth 1 -type f \( -name "*.yml" -o -name "*.yaml" \) 2>/dev/null); do
|
||||
if [ -f "$workflow" ]; then
|
||||
if python3 -c "import yaml, sys; yaml.safe_load(open(sys.argv[1]))" "$workflow" 2>/dev/null; then
|
||||
echo "✅ $(basename $workflow)" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -629,7 +629,7 @@ jobs:
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Error:** $INVALID workflow file(s) have invalid YAML syntax" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Action Required:** Fix YAML syntax errors in the marked workflow files" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Tool:** Run \`python3 -c \"import yaml; yaml.safe_load(open('.mokogitea/workflows/FILE.yml'))\"\` locally" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Tool:** Run \`python3 -c \"import yaml; yaml.safe_load(open('.mokogit/workflows/FILE.yml'))\"\` locally" >> $GITHUB_STEP_SUMMARY
|
||||
echo ""
|
||||
echo "❌ ERROR: $INVALID workflow file(s) with invalid YAML syntax"
|
||||
exit 1
|
||||
@@ -641,7 +641,7 @@ jobs:
|
||||
echo "✅ SUCCESS: All workflow files passed YAML validation"
|
||||
|
||||
- name: Validate CodeQL Configuration
|
||||
if: hashFiles('.mokogitea/workflows/codeql-analysis.yml') != ''
|
||||
if: hashFiles('.mokogit/workflows/codeql-analysis.yml') != ''
|
||||
run: |
|
||||
set -e
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -649,7 +649,7 @@ jobs:
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# Inline validation (rewritten from Python to bash for PHP-only architecture)
|
||||
CODEQL_FILE=".mokogitea/workflows/codeql-analysis.yml"
|
||||
CODEQL_FILE=".mokogit/workflows/codeql-analysis.yml"
|
||||
|
||||
if [ ! -f "$CODEQL_FILE" ]; then
|
||||
echo "⚠️ CodeQL workflow file not found" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -1186,7 +1186,7 @@ jobs:
|
||||
fi
|
||||
|
||||
# Run yamllint and count line-length warnings
|
||||
YAML_OUTPUT=$(yamllint .mokogitea/workflows/*.yml 2>&1 | grep "line too long" || true)
|
||||
YAML_OUTPUT=$(yamllint .mokogit/workflows/*.yml 2>&1 | grep "line too long" || true)
|
||||
if [ -n "$YAML_OUTPUT" ]; then
|
||||
YAML_VIOLATIONS=$(echo "$YAML_OUTPUT" | wc -l)
|
||||
echo "⚠️ Found $YAML_VIOLATIONS lines exceeding 180 characters in YAML files" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -6,7 +6,7 @@
|
||||
# DEFGROUP: MokoGIT.Workflow.Template
|
||||
# INGROUP: MokoStandards.CI
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
|
||||
# PATH: /.mokogitea/workflows/version-set.yml
|
||||
# PATH: /.mokogit/workflows/version-set.yml
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Set or reset the extension version across all version-bearing files
|
||||
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN || github.token }}
|
||||
token: ${{ secrets.MOKOGIT_TOKEN || github.token }}
|
||||
ref: ${{ inputs.branch || github.ref }}
|
||||
fetch-depth: 1
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# DISABLED — auto-release Step 11 recreates dev from main after every release.
|
||||
# Cascade-dev is redundant and causes version conflicts when both main and dev
|
||||
# have different version numbers in templateDetails.xml / manifest.xml.
|
||||
name: "Cascade Main → Dev (DISABLED)"
|
||||
on: workflow_dispatch
|
||||
jobs:
|
||||
noop:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "Cascade disabled — auto-release handles dev recreation"
|
||||
@@ -1,179 +0,0 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# BRIEF: Auto-build RC release on PR to main, update RC update stream
|
||||
|
||||
name: "PR RC Release"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
REGISTRY: code.mokoconsulting.tech
|
||||
IMAGE: mokoconsulting/mokogit
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
rc-release:
|
||||
name: Build RC Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check target branch
|
||||
id: guard
|
||||
env:
|
||||
BASE_BRANCH: ${{ github.base_ref }}
|
||||
run: |
|
||||
echo "PR target: ${BASE_BRANCH}"
|
||||
if [ "$BASE_BRANCH" != "main" ]; then
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
echo "Skipping RC — only for PRs targeting main"
|
||||
else
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Checkout PR branch
|
||||
if: steps.guard.outputs.skip != 'true'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Determine RC version
|
||||
if: steps.guard.outputs.skip != 'true'
|
||||
id: version
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
# This RC flow drives a Joomla-style update stream (updates.xml). Repos that don't ship
|
||||
# one (e.g. generic Go/TS) have nothing to package here, so no-op cleanly instead of
|
||||
# aborting under `set -e` when the file is absent.
|
||||
if [ ! -f updates.xml ]; then
|
||||
echo "has_updates=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No updates.xml in this repo — skipping RC update-stream packaging"
|
||||
exit 0
|
||||
fi
|
||||
echo "has_updates=true" >> "$GITHUB_OUTPUT"
|
||||
BASE_VERSION=$(sed -n 's/.*<version>\(.*\)<\/version>.*/\1/p' updates.xml | head -1)
|
||||
[ -z "$BASE_VERSION" ] && BASE_VERSION="04.00.00"
|
||||
RC_VERSION="${BASE_VERSION}-rc.${PR_NUMBER}"
|
||||
RC_TAG="v1.26.1-moko.${RC_VERSION}"
|
||||
echo "version=$RC_VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=$RC_TAG" >> "$GITHUB_OUTPUT"
|
||||
echo "RC version: $RC_VERSION (tag: $RC_TAG)"
|
||||
|
||||
- name: Update updates.xml RC channel
|
||||
if: steps.guard.outputs.skip != 'true' && steps.version.outputs.has_updates == 'true'
|
||||
env:
|
||||
RC_VERSION: ${{ steps.version.outputs.version }}
|
||||
RC_TAG: ${{ steps.version.outputs.tag }}
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
PR_NUM: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
DOCKER_TAG="${REGISTRY}/${IMAGE}:${RC_TAG}"
|
||||
|
||||
python3 << 'PYEOF'
|
||||
import os, re
|
||||
|
||||
rc_version = os.environ["RC_VERSION"]
|
||||
rc_tag = os.environ["RC_TAG"]
|
||||
pr_url = os.environ["PR_URL"]
|
||||
pr_num = os.environ["PR_NUM"]
|
||||
docker_tag = os.environ["REGISTRY"] + "/" + os.environ["IMAGE"] + ":" + rc_tag
|
||||
|
||||
entry = f""" <update>
|
||||
<name>MokoGIT</name>
|
||||
<description>MokoGIT RC from PR #{pr_num}</description>
|
||||
<element>mokogit</element>
|
||||
<type>application</type>
|
||||
<version>{rc_version}</version>
|
||||
<client>server</client>
|
||||
<tags><tag>rc</tag></tags>
|
||||
<infourl title="MokoGIT RC">{pr_url}</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="docker">{docker_tag}</downloadurl>
|
||||
</downloads>
|
||||
<sha256></sha256>
|
||||
<targetplatform name="mokogit" version="((1\\.25\\.)|(1\\.26\\.))" />
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||
</update>"""
|
||||
|
||||
content = open("updates.xml").read()
|
||||
# Remove existing RC entry
|
||||
content = re.sub(
|
||||
r"\s*<update>[\s\S]*?<tag>rc</tag>[\s\S]*?</update>",
|
||||
"",
|
||||
content,
|
||||
)
|
||||
# Insert before </updates>
|
||||
content = content.replace("</updates>", entry + "\n</updates>")
|
||||
open("updates.xml", "w").write(content)
|
||||
print(f"Updated updates.xml with RC entry: {rc_version}")
|
||||
PYEOF
|
||||
|
||||
- name: Create RC release
|
||||
if: steps.guard.outputs.skip != 'true' && steps.version.outputs.has_updates == 'true'
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
RC_TAG: ${{ steps.version.outputs.tag }}
|
||||
RC_VERSION: ${{ steps.version.outputs.version }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
API_BASE: https://${{ env.REGISTRY }}/api/v1/repos/${{ github.repository }}
|
||||
run: |
|
||||
# Delete existing RC release/tag if present
|
||||
curl -s -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API_BASE}/releases/tags/${RC_TAG}" 2>/dev/null || true
|
||||
curl -s -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API_BASE}/tags/${RC_TAG}" 2>/dev/null || true
|
||||
|
||||
# Create prerelease
|
||||
python3 << PYEOF
|
||||
import json, os, urllib.request
|
||||
|
||||
api = os.environ["API_BASE"]
|
||||
token = os.environ["GITEA_TOKEN"]
|
||||
payload = json.dumps({
|
||||
"tag_name": os.environ["RC_TAG"],
|
||||
"target_commitish": os.environ["HEAD_SHA"],
|
||||
"name": f"RC: {os.environ['PR_TITLE']}",
|
||||
"body": f"Release candidate from PR #{os.environ['PR_NUMBER']}\n\nPR: {os.environ['PR_URL']}\nDocker: docker pull {os.environ['REGISTRY']}/{os.environ['IMAGE']}:{os.environ['RC_TAG']}",
|
||||
"draft": False,
|
||||
"prerelease": True,
|
||||
}).encode()
|
||||
|
||||
req = urllib.request.Request(
|
||||
f"{api}/releases",
|
||||
data=payload,
|
||||
headers={
|
||||
"Authorization": f"token {token}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
result = json.loads(resp.read())
|
||||
print(f"Created RC release: {result.get('tag_name')}")
|
||||
PYEOF
|
||||
|
||||
- name: Commit updates.xml
|
||||
if: steps.guard.outputs.skip != 'true' && steps.version.outputs.has_updates == 'true'
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
||||
PR_NUM: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
git config user.name "MokoGIT Bot"
|
||||
git config user.email "deploy@mokoconsulting.tech"
|
||||
git add updates.xml
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to updates.xml"
|
||||
else
|
||||
git commit -m "chore(ci): update RC stream for PR #${PR_NUM}"
|
||||
git push origin "HEAD:${HEAD_REF}" || echo "Push failed"
|
||||
fi
|
||||
@@ -1,12 +0,0 @@
|
||||
# Test workflow to verify .mokogit/ directory is discovered
|
||||
name: Test .mokogit workflows
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Verify .mokogit
|
||||
run: echo "This workflow ran from .mokogitea/workflows/ — feature works!"
|
||||
@@ -1,167 +0,0 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# BRIEF: Sync upstream Gitea bug fixes into MokoGIT issue tracker
|
||||
|
||||
name: Upstream Bug Sync
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 8 * * *' # daily at 08:00 UTC
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
days_back:
|
||||
description: 'How many days back to scan (default: 7)'
|
||||
required: false
|
||||
default: '7'
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Sync upstream bugs
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_MIRROR_TOKEN }}
|
||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKOGITEA_URL: https://code.mokoconsulting.tech
|
||||
MOKOGITEA_REPO: MokoConsulting/MokoGIT
|
||||
UPSTREAM_BRANCH: release/v1.26
|
||||
DAYS_BACK: ${{ github.event.inputs.days_back || '7' }}
|
||||
run: |
|
||||
python3 << 'PYEOF'
|
||||
import json, os, re, sys, urllib.parse, urllib.request
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
GH_TOKEN = os.environ["GH_TOKEN"]
|
||||
MOKO_TOKEN = os.environ["MOKOGITEA_TOKEN"]
|
||||
MOKO_URL = os.environ["MOKOGITEA_URL"]
|
||||
MOKO_REPO = os.environ["MOKOGITEA_REPO"]
|
||||
BRANCH = os.environ["UPSTREAM_BRANCH"]
|
||||
DAYS = int(os.environ.get("DAYS_BACK", "7"))
|
||||
|
||||
# Label IDs in MokoGIT
|
||||
LABELS = {
|
||||
"type_bug": 5757, "upstream": 5758, "security": 5032,
|
||||
"critical": 5018, "high": 5019, "medium": 5020, "low": 5021,
|
||||
}
|
||||
|
||||
def gh_get(url):
|
||||
req = urllib.request.Request(url, headers={
|
||||
"Authorization": f"token {GH_TOKEN}",
|
||||
"Accept": "application/vnd.github.v3+json",
|
||||
})
|
||||
with urllib.request.urlopen(req) as r:
|
||||
return json.loads(r.read())
|
||||
|
||||
def moko_get(path):
|
||||
req = urllib.request.Request(f"{MOKO_URL}/api/v1/{path}", headers={
|
||||
"Authorization": f"token {MOKO_TOKEN}",
|
||||
})
|
||||
with urllib.request.urlopen(req) as r:
|
||||
return json.loads(r.read())
|
||||
|
||||
def moko_post(path, data):
|
||||
payload = json.dumps(data).encode()
|
||||
req = urllib.request.Request(f"{MOKO_URL}/api/v1/{path}",
|
||||
data=payload, method="POST", headers={
|
||||
"Authorization": f"token {MOKO_TOKEN}",
|
||||
"Content-Type": "application/json",
|
||||
})
|
||||
with urllib.request.urlopen(req) as r:
|
||||
return json.loads(r.read())
|
||||
|
||||
# ── Step 1: Find recently merged upstream PRs ──
|
||||
since = (datetime.now(timezone.utc) - timedelta(days=DAYS)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
query = f"repo:go-gitea/gitea is:pr is:merged base:{BRANCH} merged:>={since}"
|
||||
encoded = urllib.parse.quote(query)
|
||||
print(f"Scanning: {query}")
|
||||
|
||||
result = gh_get(f"https://api.github.com/search/issues?q={encoded}&per_page=100&sort=updated&order=desc")
|
||||
total = result["total_count"]
|
||||
print(f"Found {total} merged PRs in the last {DAYS} days")
|
||||
|
||||
if total == 0:
|
||||
print("Nothing to sync.")
|
||||
sys.exit(0)
|
||||
|
||||
# ── Step 2: Filter for bug/security fixes ──
|
||||
bugs = []
|
||||
for pr in result["items"]:
|
||||
title = pr["title"]
|
||||
label_names = [l["name"].lower() for l in pr.get("labels", [])]
|
||||
is_fix = title.lower().startswith("fix")
|
||||
is_security = any("security" in l for l in label_names) or "[security]" in title.lower()
|
||||
is_bug = any("bug" in l for l in label_names)
|
||||
|
||||
if not (is_fix or is_security or is_bug):
|
||||
continue
|
||||
|
||||
refs = re.findall(r"#(\d+)", title)
|
||||
severity = "critical" if is_security and "[security]" in title.lower() else \
|
||||
"high" if is_security else "medium"
|
||||
|
||||
bugs.append({
|
||||
"number": pr["number"], "title": title, "url": pr["html_url"],
|
||||
"severity": severity, "is_security": is_security, "refs": refs,
|
||||
"merged": pr.get("pull_request", {}).get("merged_at", "")[:10],
|
||||
})
|
||||
|
||||
print(f"Filtered to {len(bugs)} bug/security fixes")
|
||||
if not bugs:
|
||||
sys.exit(0)
|
||||
|
||||
# ── Step 3: Collect already-tracked PR numbers ──
|
||||
tracked = set()
|
||||
for state in ["open", "closed"]:
|
||||
try:
|
||||
issues = moko_get(f"repos/{MOKO_REPO}/issues?state={state}&type=issues&limit=50&labels=upstream")
|
||||
for iss in issues:
|
||||
text = (iss.get("body") or "") + " " + (iss.get("title") or "")
|
||||
tracked.update(re.findall(r"(?:#|/pull/)(\d{4,})", text))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
print(f"Already tracked: {len(tracked)} upstream PRs")
|
||||
|
||||
# ── Step 4: Create issues for new bugs ──
|
||||
created = skipped = errors = 0
|
||||
for bug in bugs:
|
||||
if any(r in tracked for r in bug["refs"]):
|
||||
print(f" SKIP #{bug['number']}: {bug['title'][:55]} (tracked)")
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
labels = [LABELS["type_bug"], LABELS["upstream"], LABELS[bug["severity"]]]
|
||||
if bug["is_security"]:
|
||||
labels.append(LABELS["security"])
|
||||
|
||||
body = (
|
||||
f"## Summary\n\n"
|
||||
f"Upstream bug fix merged into `{BRANCH}`.\n\n"
|
||||
f"## Upstream Reference\n\n"
|
||||
f"- PR: {bug['url']}\n"
|
||||
f"- Merged: {bug['merged']}\n"
|
||||
f"- Branch: {BRANCH}\n\n"
|
||||
f"## Severity: {bug['severity'].title()}"
|
||||
f"{' (security)' if bug['is_security'] else ''}\n\n"
|
||||
f"## Action\n\n"
|
||||
f"Cherry-pick from upstream `{BRANCH}` branch.\n\n"
|
||||
f"---\n"
|
||||
f"*Auto-created by upstream-bug-sync workflow*\n"
|
||||
f"*Authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>*"
|
||||
)
|
||||
|
||||
try:
|
||||
iss = moko_post(f"repos/{MOKO_REPO}/issues", {
|
||||
"title": bug["title"], "body": body, "labels": labels,
|
||||
})
|
||||
print(f" CREATED #{iss['number']}: {bug['title'][:55]}")
|
||||
created += 1
|
||||
except Exception as e:
|
||||
print(f" ERROR #{bug['number']}: {e}")
|
||||
errors += 1
|
||||
|
||||
print(f"\n=== Done: {created} created, {skipped} skipped, {errors} errors ===")
|
||||
PYEOF
|
||||
@@ -2,10 +2,18 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
- **Actions: `.mokogit/workflows` custom-path detection restored** — `WorkflowDirs` now scans `.mokogit/workflows`; the legacy `.mokogitea/workflows` is fully retired. Workflow indexing, push-triggered CI/deploys, and `workflow_dispatch` work again (#798)
|
||||
- **Issue custom-status dropdown shows its options again** — the status `<select>` carried the `ui compact dropdown` class, so fomantic turned it into an overlay menu that painted behind the page content (options were present in the DOM but invisible); it is now a plain native `<select>` with its inline styles moved to a stylesheet (`web_src/css/repo/issue-status.css`)
|
||||
- Restored the repo's `.mokogit/workflows/` (deploy + org CI, ~20 workflows) from Template-Go after the workflow sync had dropped them
|
||||
|
||||
### Changed
|
||||
- Adopt MokoOrgStandards `xx.xx.xx` versioning: the app version now comes from a committed `VERSION` file (`01.00.00`), so the binary reports the MokoGIT version instead of the leaked upstream Gitea `1.24.0+dev-N-ghash`; the upstream update-checker is disabled by default
|
||||
- **Rebrand: MokoGitea → MokoGIT (complete retirement of the MokoGitea name).** Go module path `code.mokoconsulting.tech/MokoConsulting/MokoGitea` → `.../MokoGIT` (all imports + `go.mod`); default app name `MokoGitea` → `MokoGIT`; the in-repo config directory and the special org profile/wiki repo names `.mokogitea` / `.mokogitea-private` → `.mokogit` / `.mokogit-private`; the Actions system user `mokogitea-actions` → `mokogit-actions` (DB migration #369); Docker image, ntfy topic, mail tags, and remaining lowercase `mokogitea` references → `mokogit`; bundled the Moko `favicon.svg` as the app icon and PWA manifest icon. Shared `MOKOGITEA_*` CI/compose env-var and org-secret names are renamed in the coordinated server cutover (not in this repo change) to avoid breaking cross-repo CI mid-transition.
|
||||
|
||||
### Added
|
||||
- **Org internal wiki merge** — the organization Overview renders the org's internal wiki landing page (from the `.mokogit` public / `.mokogit-private` members-only repo wiki, resolved `home` → `index` → `readme` → `profile`) instead of a separate profile README, with a safe fallback to the legacy README; the per-repo Wiki tab is hidden for the `.mokogit` / `.mokogit-private` profile repos since their wiki now surfaces as the Overview; the org-settings "Internal wiki" control is relabeled to `.mokogit` / `.mokogit-private`
|
||||
- Staged `.vault` git-stack deploy compose under `deploy/git/{prod,rc,dev}/` (per-tier `docker-compose.yml` + `VERSION` + `.env.example`; every secret externalized as `${VAR}`), ready to drop into the `.vault` hub
|
||||
- Metadata platform options are now **admin-configurable** instead of hardcoded: a new **Admin → Metadata** page edits the allowed `platform` values (persisted to `app.ini` `[metadata] PLATFORM_OPTIONS`, default `joomla,dolibarr,go,npm,generic`), and the repo Settings → Metadata platform dropdown reads from it. Changing the taxonomy no longer needs a code change/redeploy. A repo's existing platform value stays selectable even if later removed from the list (#777)
|
||||
- Org branch protection: repositories now show the inherited organization rules read-only in their Branch Protection settings, with an expandable detail (direct push, force-push, branch deletion, merge restrictions, required approvals, status checks, protected files, and whitelisted teams) — like GitHub surfaces org rulesets in a repo (#727)
|
||||
- Org branch protection: org-level rules can now also protect against branch deletion (`enable_delete` + delete allowlist teams), mirroring the per-repo delete allowlist (#727)
|
||||
|
||||
@@ -37,6 +37,17 @@ See the [org wiki](https://git.mokoconsulting.tech/MokoConsulting/.mokogit/wiki/
|
||||
|
||||
This project is licensed under the GNU General Public License v3.0 or later -- see the [LICENSE](LICENSE) file.
|
||||
|
||||
## Third-Party & Vendored Assets
|
||||
|
||||
MokoGIT bundles third-party assets under their own licenses. Full attribution and
|
||||
license texts are in [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md) and travel with
|
||||
every distribution and white-labeled build.
|
||||
|
||||
- **[Font Awesome Free 7.1.0](https://fontawesome.com)** -- (c) 2025 Fonticons, Inc.
|
||||
Icons under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), fonts under
|
||||
[SIL OFL 1.1](https://scripts.sil.org/OFL), code under [MIT](https://opensource.org/licenses/MIT).
|
||||
Vendored unmodified at `web_src/css/vendor/fontawesome/` (see its `LICENSE.txt`).
|
||||
|
||||
---
|
||||
|
||||
*[Moko Consulting](https://mokoconsulting.tech)*
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<!--
|
||||
SPDX-License-Identifier: MIT
|
||||
This file lists third-party assets vendored into MokoGIT and the attribution
|
||||
required by their licenses. It is intended to travel with every distribution and
|
||||
white-labeled build. Do not remove third-party attribution when rebranding.
|
||||
-->
|
||||
|
||||
# Third-Party Notices
|
||||
|
||||
MokoGIT bundles the third-party assets listed below. Each is redistributed under
|
||||
its own license; the required attribution and license texts are retained here and
|
||||
alongside the vendored files. Rebranding / white-labeling MokoGIT does **not**
|
||||
remove these obligations — this file (and the referenced license files) must be
|
||||
distributed with the product.
|
||||
|
||||
## Font Awesome Free 7.1.0
|
||||
|
||||
- **Project:** Font Awesome Free
|
||||
- **Version:** 7.1.0
|
||||
- **Author / Copyright:** © 2025 Fonticons, Inc. — https://fontawesome.com
|
||||
- **Homepage / License:** https://fontawesome.com/license/free
|
||||
- **Vendored at:** `web_src/css/vendor/fontawesome/` (CSS + webfonts, unmodified)
|
||||
- **Full license text:** `web_src/css/vendor/fontawesome/LICENSE.txt`
|
||||
|
||||
Font Awesome Free is multi-licensed by file type:
|
||||
|
||||
| Component | License | Notes |
|
||||
| --- | --- | --- |
|
||||
| Icons (SVG/JS glyph designs) | **CC BY 4.0** (https://creativecommons.org/licenses/by/4.0/) | Attribution to Fonticons, Inc. required. Satisfied by this notice + the retained CSS header. |
|
||||
| Fonts (`.woff2` webfonts) | **SIL OFL 1.1** (https://scripts.sil.org/OFL) | Distributed with the OFL text (in `LICENSE.txt`). Font files are shipped **unmodified**; the reserved font name "Font Awesome" is retained. |
|
||||
| Code (CSS) | **MIT** (https://opensource.org/licenses/MIT) | Copyright header retained in `all.min.css`. |
|
||||
|
||||
**Compliance notes for distribution / white-labeling:**
|
||||
|
||||
- The vendored `.woff2` font files are byte-for-byte unmodified. Do not subset,
|
||||
re-generate, or rename them — doing so triggers the SIL OFL Reserved Font Name
|
||||
restriction.
|
||||
- The license header comment in `all.min.css` must be preserved.
|
||||
- Attribution may live in this notices file and the licenses page rather than in
|
||||
the visible (white-labeled) UI; it must remain accessible to end users.
|
||||
@@ -0,0 +1,42 @@
|
||||
<!-- SPDX-License-Identifier: GPL-3.0-or-later -->
|
||||
|
||||
# MokoGIT Versioning
|
||||
|
||||
MokoGIT follows the **MokoOrgStandards `xx.xx.xx`** scheme: `MAJOR.MINOR.PATCH`, **two digits
|
||||
per segment** (e.g. `01.00.00`, `01.04.12`). This is MokoGIT's own version line — it deliberately
|
||||
**replaces the inherited upstream Gitea version** (which previously leaked through as
|
||||
`1.24.0+dev-<N>-g<hash>`).
|
||||
|
||||
## Where the version comes from
|
||||
|
||||
The binary version is injected at build time into `main.Version` → `setting.AppVer` (drives the
|
||||
footer, `/api/v1/version`, and the PWA manifest). The Makefile resolves it in this order:
|
||||
|
||||
| Build type | Source | Result |
|
||||
| --- | --- | --- |
|
||||
| **Release (git tag)** | the tag name (`$GITHUB_REF_NAME`, `v` stripped) | e.g. tag `01.00.00` → `01.00.00` |
|
||||
| **Branch / dev** | the repo-root **`VERSION`** file (`STORED_VERSION`) | `01.00.00` |
|
||||
| _(fallback only if `VERSION` is absent)_ | `git describe --tags` | would leak the Gitea base — avoided by shipping `VERSION` |
|
||||
|
||||
The committed `VERSION` file (currently `01.00.00`) is what strips the Gitea base on ordinary
|
||||
branch builds. **Do not delete it.**
|
||||
|
||||
## Release tagging — MUST use `xx.xx.xx`
|
||||
|
||||
Release/tag automation MUST create tags in the `xx.xx.xx` form (e.g. `01.00.00`). Because the
|
||||
deploy workflows derive the container image tag from `git describe --tags`, tagging with
|
||||
`01.00.00` makes the deployed image report MokoGIT's version rather than Gitea's. Tiers append a
|
||||
suffix: dev → `-dev`, rc → `-rc`.
|
||||
|
||||
## Update checker
|
||||
|
||||
The upstream update-checker defaults to **disabled** (`modules/setting/setting.go`) — a fork must
|
||||
not compare its `AppVer` against Gitea's release feed. Re-enable only against a MokoGIT endpoint.
|
||||
|
||||
## Scope of a bump
|
||||
|
||||
- **PATCH** — fixes, no behavior change.
|
||||
- **MINOR** — backward-compatible features (default bump for feature/dev → stable releases).
|
||||
- **MAJOR** — breaking changes.
|
||||
|
||||
Per-stack and per-tier `VERSION` files also live under `deploy/git/` for the `.vault` git stack.
|
||||
+3
-3
@@ -86,9 +86,9 @@ type AppVersion struct {
|
||||
|
||||
func NewMainApp(appVer AppVersion) *cli.Command {
|
||||
app := &cli.Command{}
|
||||
app.Name = "gitea" // must be lower-cased because it appears in the "USAGE" section like "gitea doctor [command [command options]]"
|
||||
app.Name = "mokogit" // must be lower-cased because it appears in the "USAGE" section like "mokogit doctor [command [command options]]" and in `mokogit --version`
|
||||
app.Usage = "A painless self-hosted Git service"
|
||||
app.Description = `Gitea program contains "web" and other subcommands. If no subcommand is given, it starts the web server by default. Use "web" subcommand for more web server arguments, use other subcommands for other purposes.`
|
||||
app.Description = `MokoGIT program contains "web" and other subcommands. If no subcommand is given, it starts the web server by default. Use "web" subcommand for more web server arguments, use other subcommands for other purposes.`
|
||||
app.Version = appVer.Version + appVer.Extra
|
||||
app.EnableShellCompletion = true
|
||||
app.Flags = []cli.Flag{
|
||||
@@ -96,7 +96,7 @@ func NewMainApp(appVer AppVersion) *cli.Command {
|
||||
Name: "work-path",
|
||||
Aliases: []string{"w"},
|
||||
TakesFile: true,
|
||||
Usage: "Set Gitea's working path (defaults to the Gitea's binary directory)",
|
||||
Usage: "Set MokoGIT's working path (defaults to the MokoGIT's binary directory)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "config",
|
||||
|
||||
@@ -1346,8 +1346,8 @@ LEVEL = Info
|
||||
;; Whether the email of the user should be shown in the Explore Users page
|
||||
;SHOW_USER_EMAIL = true
|
||||
;;
|
||||
;; Set the default theme for the Gitea install
|
||||
;DEFAULT_THEME = gitea-auto
|
||||
;; Set the default theme for the MokoGIT install
|
||||
;DEFAULT_THEME = mokogit-auto
|
||||
;;
|
||||
;; All available themes. Allow users select personalized themes regardless of the value of `DEFAULT_THEME`.
|
||||
;; Leave it empty to allow users to select any theme from "{CustomPath}/public/assets/css/theme-*.css"
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<!-- SPDX-License-Identifier: GPL-3.0-or-later -->
|
||||
|
||||
# MokoGIT git stack (staged for `.vault`)
|
||||
|
||||
This directory is the **staged server compose** for the MokoGIT git service, laid out to match the
|
||||
target `.vault/stacks/git/{prod,rc,dev}/` structure so it can be dropped into `.vault` when the
|
||||
restructure resumes.
|
||||
|
||||
## Ownership split
|
||||
|
||||
- **This repo** (the Gitea fork) builds + pushes the **image** (`mokoconsulting/mokogit`, tiers
|
||||
`mokogit` / `-rc` / `-dev`) via its own CI.
|
||||
- **`.vault`** (`MokoConsulting/.vault`, `/opt/.vault` on the Beelink) owns the **server compose +
|
||||
config** that RUNS those containers. `.vault` is the single source of truth for the box.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
deploy/git/
|
||||
VERSION # per-stack version (xx.xx.xx)
|
||||
prod/ { docker-compose.yml, VERSION, .env.example } -> container mokogit, /opt/mokogit/prod, db mokogit
|
||||
rc/ { docker-compose.yml, VERSION, .env.example } -> container mokogit-rc, /opt/mokogit/rc, db mokogit_rc
|
||||
dev/ { docker-compose.yml, VERSION, .env.example } -> container mokogit-dev, /opt/mokogit/dev, db mokogit_dev
|
||||
```
|
||||
|
||||
Versioning follows MokoOrgStandards `xx.xx.xx` (see repo-root `VERSIONING.md`). Each tier and the
|
||||
stack carry a `VERSION` file.
|
||||
|
||||
## ⚠️ Not yet live — reconcile first
|
||||
|
||||
These composes are **modeled on the documented live config**, not copied from the running box, so
|
||||
they are **NOT guaranteed byte-accurate**. Before dropping into `.vault`:
|
||||
|
||||
1. **DIFF against the live** `/opt/mokogitea/docker-compose.yml` (run with `docker compose -p
|
||||
gitea-dev`). Reconcile every line tagged `RECONCILE` in the compose: exact volume/conf layout
|
||||
(live app.ini is at `${GIT_DATA_DIR}/conf/app.ini`), host ports, the DB host reachability (DB is
|
||||
**host MySQL**; app DB user is `mokogitea`), the docker network name, and any `act_runner` /
|
||||
`depends_on` wiring (note: `depends_on` uses the prod service key — mismatches caused a past
|
||||
outage).
|
||||
2. **Secrets stay external** — every host/secret value is `${VAR}`; real values go in a **gitignored
|
||||
`.env`** or Gitea Actions secrets, never committed. `.env.example` files carry only placeholders.
|
||||
|
||||
## Do NOT relocate yet
|
||||
|
||||
The `.vault` restructure is **PAUSED** on server drift (partly from the mokogitea→mokogit rebrand
|
||||
leaving the box tree dirty) and an **open critical secrets-in-git issue** in `.vault` history. Moving
|
||||
the git compose onto the box now risks breaking the `--ff-only` pull. Relocation happens when the
|
||||
restructure resumes — coordinate first. Until then git keeps running from `/opt/mokogitea` +
|
||||
`/opt/gitea-dev` (the old locations).
|
||||
@@ -0,0 +1 @@
|
||||
01.00.00
|
||||
@@ -0,0 +1,27 @@
|
||||
# MokoGIT git stack -- DEV tier env (EXAMPLE -- copy to .env and fill; .env is gitignored)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# No real secrets here. RECONCILE all values against the live box before use.
|
||||
|
||||
GIT_REGISTRY=git.mokoconsulting.tech
|
||||
GIT_IMAGE=mokoconsulting/mokogit
|
||||
MOKOGIT_DEV_TAG=01.00.00-dev # dev image tag. Deploy CI pins this per build.
|
||||
|
||||
GIT_UID=1000
|
||||
GIT_GID=1000
|
||||
|
||||
GIT_DOMAIN=dev.git.mokoconsulting.tech
|
||||
GIT_ROOT_URL=https://dev.git.mokoconsulting.tech/
|
||||
GIT_SSH_DOMAIN=dev.git.mokoconsulting.tech
|
||||
GIT_SSH_PUBLIC_PORT=2918
|
||||
|
||||
GIT_HTTP_PORT=3002
|
||||
GIT_SSH_PORT=2224
|
||||
|
||||
GIT_DATA_DIR=/opt/mokogit/dev
|
||||
|
||||
GIT_DB_HOST=host.docker.internal:3306
|
||||
GIT_DB_NAME=mokogit_dev
|
||||
GIT_DB_USER=mokogitea
|
||||
GIT_DB_PASSWD=__SET_IN_REAL_ENV__
|
||||
|
||||
GIT_NETWORK=gitea-dev_default
|
||||
@@ -0,0 +1 @@
|
||||
01.00.00
|
||||
@@ -0,0 +1,47 @@
|
||||
# MokoGIT git stack -- DEV tier
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# STAGED for .vault/stacks/git/dev/ -- see ../README.md. Modeled on the documented live config;
|
||||
# DIFF against the live /opt/mokogitea/docker-compose.yml before dropping into .vault.
|
||||
# Every host/secret value is ${VAR} (define real values in a gitignored .env; see .env.example).
|
||||
|
||||
services:
|
||||
git:
|
||||
image: "${GIT_REGISTRY}/${GIT_IMAGE}:${MOKOGIT_DEV_TAG}"
|
||||
container_name: mokogit-dev
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- USER_UID=${GIT_UID}
|
||||
- USER_GID=${GIT_GID}
|
||||
- GITEA__server__DOMAIN=${GIT_DOMAIN}
|
||||
- GITEA__server__ROOT_URL=${GIT_ROOT_URL}
|
||||
- GITEA__server__SSH_DOMAIN=${GIT_SSH_DOMAIN}
|
||||
- GITEA__server__SSH_PORT=${GIT_SSH_PUBLIC_PORT}
|
||||
- GITEA__database__DB_TYPE=mysql
|
||||
- GITEA__database__HOST=${GIT_DB_HOST}
|
||||
- GITEA__database__NAME=${GIT_DB_NAME}
|
||||
- GITEA__database__USER=${GIT_DB_USER}
|
||||
- GITEA__database__PASSWD=${GIT_DB_PASSWD}
|
||||
volumes:
|
||||
# RECONCILE: live app.ini at ${GIT_DATA_DIR}/conf/app.ini (/opt/mokogit/dev/conf).
|
||||
- "${GIT_DATA_DIR}:/data"
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
ports:
|
||||
- "${GIT_HTTP_PORT}:3000"
|
||||
- "${GIT_SSH_PORT}:22"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fsS", "http://localhost:3000/api/healthz"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
networks:
|
||||
- git
|
||||
|
||||
networks:
|
||||
git:
|
||||
name: ${GIT_NETWORK}
|
||||
external: true
|
||||
@@ -0,0 +1,34 @@
|
||||
# MokoGIT git stack -- PROD tier env (EXAMPLE -- copy to .env and fill real values; .env is gitignored)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# No real secrets in this file. RECONCILE all values against the live box before use.
|
||||
|
||||
# --- image ---
|
||||
GIT_REGISTRY=git.mokoconsulting.tech
|
||||
GIT_IMAGE=mokoconsulting/mokogit
|
||||
MOKOGIT_TAG=01.00.00 # prod image tag (release xx.xx.xx). Deploy CI pins this per build.
|
||||
|
||||
# --- runtime user ---
|
||||
GIT_UID=1000
|
||||
GIT_GID=1000
|
||||
|
||||
# --- server / URLs ---
|
||||
GIT_DOMAIN=git.mokoconsulting.tech
|
||||
GIT_ROOT_URL=https://git.mokoconsulting.tech/
|
||||
GIT_SSH_DOMAIN=git.mokoconsulting.tech
|
||||
GIT_SSH_PUBLIC_PORT=2918 # public SSH port advertised to clients
|
||||
|
||||
# --- host ports (container 3000/22 mapped to host) ---
|
||||
GIT_HTTP_PORT=3000
|
||||
GIT_SSH_PORT=2222
|
||||
|
||||
# --- data ---
|
||||
GIT_DATA_DIR=/opt/mokogit/prod
|
||||
|
||||
# --- database (HOST MySQL; app DB user is 'mokogitea') ---
|
||||
GIT_DB_HOST=host.docker.internal:3306
|
||||
GIT_DB_NAME=mokogit
|
||||
GIT_DB_USER=mokogitea
|
||||
GIT_DB_PASSWD=__SET_IN_REAL_ENV__ # real value ONLY in gitignored .env / Actions secret
|
||||
|
||||
# --- docker network (external; RECONCILE actual name) ---
|
||||
GIT_NETWORK=gitea-dev_default
|
||||
@@ -0,0 +1 @@
|
||||
01.00.00
|
||||
@@ -0,0 +1,52 @@
|
||||
# MokoGIT git stack -- PROD tier
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# STAGED for .vault/stacks/git/prod/ -- see ../README.md. Modeled on the documented live
|
||||
# config; DIFF against the live /opt/mokogitea/docker-compose.yml before dropping into .vault.
|
||||
# Ownership: THIS repo builds+pushes the image; .vault runs it. Every host/secret value is ${VAR}
|
||||
# (define real values in a gitignored .env; see .env.example). Do NOT commit a real .env.
|
||||
#
|
||||
# RECONCILE before use: exact volume/conf layout, ports, DB host reachability (host MySQL),
|
||||
# network name, and any act_runner/depends_on wiring are modeled -- verify against the box.
|
||||
|
||||
services:
|
||||
git:
|
||||
image: "${GIT_REGISTRY}/${GIT_IMAGE}:${MOKOGIT_TAG}"
|
||||
container_name: mokogit
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- USER_UID=${GIT_UID}
|
||||
- USER_GID=${GIT_GID}
|
||||
- GITEA__server__DOMAIN=${GIT_DOMAIN}
|
||||
- GITEA__server__ROOT_URL=${GIT_ROOT_URL}
|
||||
- GITEA__server__SSH_DOMAIN=${GIT_SSH_DOMAIN}
|
||||
- GITEA__server__SSH_PORT=${GIT_SSH_PUBLIC_PORT}
|
||||
# DB is HOST MySQL (RECONCILE host reachability: host-gateway / socket / LAN IP).
|
||||
- GITEA__database__DB_TYPE=mysql
|
||||
- GITEA__database__HOST=${GIT_DB_HOST}
|
||||
- GITEA__database__NAME=${GIT_DB_NAME}
|
||||
- GITEA__database__USER=${GIT_DB_USER}
|
||||
- GITEA__database__PASSWD=${GIT_DB_PASSWD}
|
||||
volumes:
|
||||
# RECONCILE: live app.ini is at ${GIT_DATA_DIR}/conf/app.ini (/opt/mokogit/prod/conf).
|
||||
- "${GIT_DATA_DIR}:/data"
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
ports:
|
||||
- "${GIT_HTTP_PORT}:3000"
|
||||
- "${GIT_SSH_PORT}:22"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fsS", "http://localhost:3000/api/healthz"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
networks:
|
||||
- git
|
||||
|
||||
networks:
|
||||
git:
|
||||
name: ${GIT_NETWORK}
|
||||
external: true
|
||||
@@ -0,0 +1,27 @@
|
||||
# MokoGIT git stack -- RC tier env (EXAMPLE -- copy to .env and fill; .env is gitignored)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# No real secrets here. RECONCILE all values against the live box before use.
|
||||
|
||||
GIT_REGISTRY=git.mokoconsulting.tech
|
||||
GIT_IMAGE=mokoconsulting/mokogit
|
||||
MOKOGIT_RC_TAG=01.00.00-rc # rc image tag. Deploy CI pins this per build.
|
||||
|
||||
GIT_UID=1000
|
||||
GIT_GID=1000
|
||||
|
||||
GIT_DOMAIN=rc.git.mokoconsulting.tech
|
||||
GIT_ROOT_URL=https://rc.git.mokoconsulting.tech/
|
||||
GIT_SSH_DOMAIN=rc.git.mokoconsulting.tech
|
||||
GIT_SSH_PUBLIC_PORT=2918
|
||||
|
||||
GIT_HTTP_PORT=3001
|
||||
GIT_SSH_PORT=2223
|
||||
|
||||
GIT_DATA_DIR=/opt/mokogit/rc
|
||||
|
||||
GIT_DB_HOST=host.docker.internal:3306
|
||||
GIT_DB_NAME=mokogit_rc
|
||||
GIT_DB_USER=mokogitea
|
||||
GIT_DB_PASSWD=__SET_IN_REAL_ENV__
|
||||
|
||||
GIT_NETWORK=gitea-dev_default
|
||||
@@ -0,0 +1 @@
|
||||
01.00.00
|
||||
@@ -0,0 +1,47 @@
|
||||
# MokoGIT git stack -- RC tier
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# STAGED for .vault/stacks/git/rc/ -- see ../README.md. Modeled on the documented live config;
|
||||
# DIFF against the live /opt/mokogitea/docker-compose.yml before dropping into .vault.
|
||||
# Every host/secret value is ${VAR} (define real values in a gitignored .env; see .env.example).
|
||||
|
||||
services:
|
||||
git:
|
||||
image: "${GIT_REGISTRY}/${GIT_IMAGE}:${MOKOGIT_RC_TAG}"
|
||||
container_name: mokogit-rc
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- USER_UID=${GIT_UID}
|
||||
- USER_GID=${GIT_GID}
|
||||
- GITEA__server__DOMAIN=${GIT_DOMAIN}
|
||||
- GITEA__server__ROOT_URL=${GIT_ROOT_URL}
|
||||
- GITEA__server__SSH_DOMAIN=${GIT_SSH_DOMAIN}
|
||||
- GITEA__server__SSH_PORT=${GIT_SSH_PUBLIC_PORT}
|
||||
- GITEA__database__DB_TYPE=mysql
|
||||
- GITEA__database__HOST=${GIT_DB_HOST}
|
||||
- GITEA__database__NAME=${GIT_DB_NAME}
|
||||
- GITEA__database__USER=${GIT_DB_USER}
|
||||
- GITEA__database__PASSWD=${GIT_DB_PASSWD}
|
||||
volumes:
|
||||
# RECONCILE: live app.ini at ${GIT_DATA_DIR}/conf/app.ini (/opt/mokogit/rc/conf).
|
||||
- "${GIT_DATA_DIR}:/data"
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
ports:
|
||||
- "${GIT_HTTP_PORT}:3000"
|
||||
- "${GIT_SSH_PORT}:22"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fsS", "http://localhost:3000/api/healthz"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
networks:
|
||||
- git
|
||||
|
||||
networks:
|
||||
git:
|
||||
name: ${GIT_NETWORK}
|
||||
external: true
|
||||
@@ -447,6 +447,7 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(367, "Add user allowlists to org protected branch", v1_27.AddUserAllowlistsToOrgProtectedBranch),
|
||||
newMigration(368, "Drop org from repo manifest (derived from org profile)", v1_27.DropOrgFromRepoManifest),
|
||||
newMigration(369, "Rename mokogitea-actions system user to mokogit-actions", v1_27.RenameActionsUserToMokoGit),
|
||||
newMigration(370, "Remap saved gitea-* user themes to mokogit-*", v1_27.RemapGiteaThemesToMokogit),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package v1_27
|
||||
|
||||
import "xorm.io/xorm"
|
||||
|
||||
// RemapGiteaThemesToMokogit rewrites each user's saved theme from the retired
|
||||
// upstream "gitea-*" theme names to their "mokogit-*" equivalents, part of the
|
||||
// MokoGitea -> MokoGIT rebrand. The branding rebrand deleted theme-gitea-*.css,
|
||||
// so any user still pointing at a gitea-* theme would load a now-missing
|
||||
// stylesheet (a blank/unstyled page). Each user's light/dark/auto preference is
|
||||
// preserved; the colorblind variants (which MokoGIT does not ship) collapse to
|
||||
// the matching base scheme. Idempotent: rows already on a mokogit-* value are
|
||||
// left untouched by the WHERE clause on the old name.
|
||||
func RemapGiteaThemesToMokogit(x *xorm.Engine) error {
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
remap := []struct{ old, new string }{
|
||||
{"gitea-auto", "mokogit-auto"},
|
||||
{"gitea-auto-colorblind", "mokogit-auto"},
|
||||
{"gitea-light", "mokogit-light"},
|
||||
{"gitea-light-colorblind", "mokogit-light"},
|
||||
{"gitea-dark", "mokogit-dark"},
|
||||
{"gitea-dark-colorblind", "mokogit-dark"},
|
||||
}
|
||||
for _, m := range remap {
|
||||
if _, err := sess.Exec("UPDATE `user` SET `theme` = ? WHERE `theme` = ?", m.new, m.old); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return sess.Commit()
|
||||
}
|
||||
@@ -34,11 +34,11 @@ var (
|
||||
Enabled: true,
|
||||
DefaultActionsURL: defaultActionsURLGitHub,
|
||||
SkipWorkflowStrings: []string{"[skip ci]", "[ci skip]", "[no ci]", "[skip actions]", "[actions skip]"},
|
||||
// TODO(MokoGIT rebrand): workflow discovery is temporarily kept at
|
||||
// .mokogitea/workflows so the currently-deployed (old) binary can discover
|
||||
// and run the deploy workflows during the cutover. Once the MokoGIT image is
|
||||
// live, switch this and the workflow directory to .mokogit/workflows.
|
||||
WorkflowDirs: []string{".mokogitea/workflows", ".gitea/workflows", ".github/workflows"},
|
||||
// MokoGIT: custom workflow-discovery path is .mokogit/workflows (the legacy
|
||||
// .mokogitea/workflows name is fully retired — do not re-add it). Fixes #798,
|
||||
// where the deployed binary scanned only .mokogitea/workflows so the org's
|
||||
// .mokogit/workflows CI was invisible. Override via [actions] WORKFLOW_DIRS.
|
||||
WorkflowDirs: []string{".mokogit/workflows", ".gitea/workflows", ".github/workflows"},
|
||||
MaxRerunAttempts: defaultMaxRerunAttempts,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -112,7 +112,7 @@ func Test_WorkflowDirs(t *testing.T) {
|
||||
{
|
||||
name: "default",
|
||||
iniStr: `[actions]`,
|
||||
wantDirs: []string{".mokogitea/workflows", ".gitea/workflows", ".github/workflows"},
|
||||
wantDirs: []string{".mokogit/workflows", ".gitea/workflows", ".github/workflows"},
|
||||
},
|
||||
{
|
||||
name: "single dir",
|
||||
|
||||
@@ -187,7 +187,9 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error {
|
||||
|
||||
func loadUpdateCheckerFrom(cfg ConfigProvider) {
|
||||
sec := cfg.Section("update_checker")
|
||||
UpdateChecker.Enabled = sec.Key("ENABLED").MustBool(true)
|
||||
// MokoGIT is a fork with its own xx.xx.xx version scheme (see repo VERSION / VERSIONING.md);
|
||||
// default the upstream update-checker OFF so we never compare AppVer against Gitea's release feed.
|
||||
UpdateChecker.Enabled = sec.Key("ENABLED").MustBool(false)
|
||||
UpdateChecker.Endpoint = sec.Key("ENDPOINT").MustString(UpdateChecker.Endpoint)
|
||||
UpdateChecker.Channel = sec.Key("CHANNEL").MustString(UpdateChecker.Channel)
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ var UI = struct {
|
||||
CodeCommentLines: 4,
|
||||
ReactionMaxUserNum: 10,
|
||||
MaxDisplayFileSize: 8388608,
|
||||
DefaultTheme: `gitea-auto`,
|
||||
DefaultTheme: `mokogit-auto`,
|
||||
FileIconTheme: `material`,
|
||||
FolderIconTheme: `basic`,
|
||||
Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`},
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
"dashboard": "Dashboard",
|
||||
"explore_title": "Explore",
|
||||
"help": "Help",
|
||||
"mokogit.theme_toggle": "Toggle light / dark theme",
|
||||
"mokogit.a11y_title": "Accessibility",
|
||||
"mokogit.a11y_text_size": "Text size",
|
||||
"mokogit.a11y_decrease": "Decrease text size",
|
||||
"mokogit.a11y_increase": "Increase text size",
|
||||
"mokogit.a11y_reset": "Reset",
|
||||
"mokogit.a11y_invert": "Invert colors",
|
||||
"mokogit.a11y_contrast": "High contrast",
|
||||
"mokogit.a11y_links": "Highlight links",
|
||||
"mokogit.a11y_readable": "Readable font",
|
||||
"mokogit.a11y_pause": "Pause animations",
|
||||
"mokogit.a11y_reset_all": "Reset all",
|
||||
"logo": "Logo",
|
||||
"sign_in": "Sign In",
|
||||
"sign_in_with_provider": "Sign in with %s",
|
||||
|
||||
+122
-6
@@ -31,20 +31,136 @@ func SiteManifest(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
ctx := req.Context()
|
||||
absoluteAssetURL := strings.TrimSuffix(httplib.MakeAbsoluteURL(ctx, setting.StaticURLPrefix), "/")
|
||||
appURL := strings.TrimSuffix(httplib.GuessCurrentAppURL(ctx), "/")
|
||||
// Icons resolve to admin-uploaded branding (nav-icon writes logo.png, favicon
|
||||
// upload writes favicon.*) when set, otherwise to the bundled MokoGIT branded
|
||||
// defaults at the same /assets/img/ paths. See routers/web/admin/branding.go.
|
||||
manifest := map[string]any{
|
||||
"name": setting.AppName,
|
||||
"short_name": setting.AppName,
|
||||
"start_url": httplib.GuessCurrentAppURL(ctx),
|
||||
"name": setting.AppName,
|
||||
"short_name": setting.AppName,
|
||||
"description": setting.AppName + " — self-hosted Git service",
|
||||
"id": appURL + "/",
|
||||
"start_url": appURL + "/",
|
||||
"scope": appURL + "/",
|
||||
"display": "standalone",
|
||||
"display_override": []string{"standalone", "minimal-ui", "browser"},
|
||||
"orientation": "any",
|
||||
"theme_color": "#112855", // MokoGIT signature navy
|
||||
"background_color": "#0e1318",
|
||||
"categories": []string{"developer", "productivity"},
|
||||
"icons": []map[string]string{
|
||||
{"src": absoluteAssetURL + "/assets/img/favicon.svg", "type": "image/svg+xml", "sizes": "any", "purpose": "any"},
|
||||
{"src": absoluteAssetURL + "/assets/img/favicon.png", "type": "image/png", "sizes": "256x256"},
|
||||
{"src": absoluteAssetURL + "/assets/img/logo-small.png", "type": "image/png", "sizes": "512x512"},
|
||||
{"src": absoluteAssetURL + "/assets/img/logo.png", "type": "image/png", "sizes": "512x512"},
|
||||
{"src": absoluteAssetURL + "/assets/img/favicon.png", "type": "image/png", "sizes": "256x256", "purpose": "any"},
|
||||
{"src": absoluteAssetURL + "/assets/img/logo-small.png", "type": "image/png", "sizes": "512x512", "purpose": "any"},
|
||||
{"src": absoluteAssetURL + "/assets/img/logo-small.png", "type": "image/png", "sizes": "512x512", "purpose": "maskable"},
|
||||
{"src": absoluteAssetURL + "/assets/img/logo.png", "type": "image/png", "sizes": "512x512", "purpose": "any"},
|
||||
},
|
||||
"shortcuts": []map[string]any{
|
||||
{"name": "Dashboard", "url": appURL + "/", "icons": []map[string]string{{"src": absoluteAssetURL + "/assets/img/logo-small.png", "sizes": "512x512"}}},
|
||||
{"name": "Issues", "url": appURL + "/issues"},
|
||||
{"name": "Pull Requests", "url": appURL + "/pulls"},
|
||||
{"name": "Explore", "url": appURL + "/explore/repos"},
|
||||
},
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(manifest)
|
||||
}
|
||||
|
||||
// serviceWorkerJS is the MokoGIT PWA service worker. Tokens are replaced per
|
||||
// request so the scope and offline/asset URLs honor AppSubURL. Strategy:
|
||||
// - navigation requests: network-first, fall back to the cached offline page
|
||||
// - same-origin static assets under the asset prefix: cache-first
|
||||
// - everything else: passthrough
|
||||
const serviceWorkerJS = `
|
||||
const CACHE = '__CACHE__';
|
||||
const OFFLINE_URL = '__OFFLINE__';
|
||||
const ASSET_PREFIX = '__ASSETS__';
|
||||
self.addEventListener('install', (e) => {
|
||||
e.waitUntil(caches.open(CACHE).then((c) => c.add(new Request(OFFLINE_URL, {cache: 'reload'}))).then(() => self.skipWaiting()));
|
||||
});
|
||||
self.addEventListener('activate', (e) => {
|
||||
e.waitUntil(caches.keys().then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))).then(() => self.clients.claim()));
|
||||
});
|
||||
self.addEventListener('fetch', (e) => {
|
||||
const req = e.request;
|
||||
if (req.method !== 'GET') return;
|
||||
const url = new URL(req.url);
|
||||
if (url.origin !== self.location.origin) return;
|
||||
if (req.mode === 'navigate') {
|
||||
e.respondWith(fetch(req).catch(() => caches.match(OFFLINE_URL)));
|
||||
return;
|
||||
}
|
||||
if (ASSET_PREFIX && url.pathname.startsWith(ASSET_PREFIX)) {
|
||||
e.respondWith(caches.match(req).then((cached) => cached || fetch(req).then((resp) => {
|
||||
if (resp && resp.ok && resp.type === 'basic') { const clone = resp.clone(); caches.open(CACHE).then((c) => c.put(req, clone)); }
|
||||
return resp;
|
||||
}).catch(() => cached)));
|
||||
}
|
||||
});
|
||||
`
|
||||
|
||||
// ServiceWorker serves the PWA service worker script with a root-scoped
|
||||
// Service-Worker-Allowed header so it can control navigations under AppSubURL.
|
||||
func ServiceWorker(w http.ResponseWriter, req *http.Request) {
|
||||
scope := setting.AppSubURL + "/"
|
||||
assetPrefix := strings.TrimSuffix(setting.StaticURLPrefix, "/") + "/assets/"
|
||||
js := serviceWorkerJS
|
||||
js = strings.ReplaceAll(js, "__CACHE__", "mokogit-pwa-"+setting.AppStartTime.Format("20060102150405"))
|
||||
js = strings.ReplaceAll(js, "__OFFLINE__", setting.AppSubURL+"/-/offline")
|
||||
js = strings.ReplaceAll(js, "__ASSETS__", assetPrefix)
|
||||
w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
|
||||
w.Header().Set("Service-Worker-Allowed", scope)
|
||||
if httpcache.HandleGenericETagPublicCache(req, w, "", &setting.AppStartTime) {
|
||||
return
|
||||
}
|
||||
if req.Method == http.MethodHead {
|
||||
return
|
||||
}
|
||||
_, _ = w.Write([]byte(js))
|
||||
}
|
||||
|
||||
// Offline serves a self-contained, branded offline fallback page. It is
|
||||
// precached by the service worker and shown when a navigation fails offline.
|
||||
func Offline(w http.ResponseWriter, req *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if req.Method == http.MethodHead {
|
||||
return
|
||||
}
|
||||
page := strings.ReplaceAll(offlineHTML, "__NAME__", setting.AppName)
|
||||
page = strings.ReplaceAll(page, "__ICON__", strings.TrimSuffix(setting.StaticURLPrefix, "/")+"/assets/img/logo.png")
|
||||
_, _ = w.Write([]byte(page))
|
||||
}
|
||||
|
||||
// offlineHTML is a dependency-light offline page (inline styles, MokoGIT navy).
|
||||
const offlineHTML = `<!DOCTYPE html>
|
||||
<html lang="en" data-theme="mokogit-auto">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Offline · __NAME__</title>
|
||||
<style>
|
||||
:root { color-scheme: light dark; }
|
||||
html,body { height:100%; margin:0; }
|
||||
body { display:flex; align-items:center; justify-content:center; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif; background:#0e1318; color:#e6ebf1; text-align:center; padding:2rem; }
|
||||
.card { max-width:28rem; }
|
||||
img { width:72px; height:72px; object-fit:contain; margin-bottom:1.25rem; }
|
||||
h1 { font-size:1.5rem; margin:0 0 .5rem; color:#f1f5f9; }
|
||||
p { color:#c7ccd4; line-height:1.5; margin:0 0 1.5rem; }
|
||||
button { background:#010156; color:#fff; border:0; border-radius:.25rem; padding:.6rem 1.25rem; font-size:1rem; cursor:pointer; }
|
||||
button:hover { background:#010149; }
|
||||
.bar { height:4px; background:#112855; position:fixed; top:0; left:0; right:0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bar"></div>
|
||||
<div class="card">
|
||||
<img src="__ICON__" alt="" onerror="this.style.display='none'">
|
||||
<h1>You're offline</h1>
|
||||
<p>__NAME__ can't be reached right now. Check your connection and try again — recently visited pages may still be available.</p>
|
||||
<button onclick="location.reload()">Retry</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
func SSHInfo(rw http.ResponseWriter, req *http.Request) {
|
||||
if !git.DefaultFeatures().SupportProcReceive {
|
||||
rw.WriteHeader(http.StatusNotFound)
|
||||
|
||||
+60
-2
@@ -14,6 +14,7 @@ import (
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/models/renderhelper"
|
||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGIT/models/repo"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/modules/git"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/modules/gitrepo"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/modules/log"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/modules/markup/markdown"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/modules/setting"
|
||||
@@ -170,23 +171,37 @@ func prepareOrgProfileReadme(ctx *context.Context, prepareResult *shared_user.Pr
|
||||
viewAs := ctx.FormString("view_as", util.Iif(ctx.Org.IsMember, "member", "public"))
|
||||
viewAsMember := viewAs == "member"
|
||||
|
||||
// Prefer the repo that exists for the chosen view; the internal wiki landing
|
||||
// (below) is rendered even when the legacy root README.md has been removed.
|
||||
var profileRepo *repo_model.Repository
|
||||
var readmeBlob *git.Blob
|
||||
if viewAsMember {
|
||||
if prepareResult.ProfilePrivateReadmeBlob != nil {
|
||||
if prepareResult.ProfilePrivateRepo != nil {
|
||||
profileRepo, readmeBlob = prepareResult.ProfilePrivateRepo, prepareResult.ProfilePrivateReadmeBlob
|
||||
} else {
|
||||
profileRepo, readmeBlob = prepareResult.ProfilePublicRepo, prepareResult.ProfilePublicReadmeBlob
|
||||
viewAsMember = false
|
||||
}
|
||||
} else {
|
||||
if prepareResult.ProfilePublicReadmeBlob != nil {
|
||||
if prepareResult.ProfilePublicRepo != nil {
|
||||
profileRepo, readmeBlob = prepareResult.ProfilePublicRepo, prepareResult.ProfilePublicReadmeBlob
|
||||
} else {
|
||||
profileRepo, readmeBlob = prepareResult.ProfilePrivateRepo, prepareResult.ProfilePrivateReadmeBlob
|
||||
viewAsMember = true
|
||||
}
|
||||
}
|
||||
if profileRepo == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Merged profile+wiki: render the internal wiki's landing page as the org
|
||||
// Overview (resolution: home -> index -> readme -> profile). Falls back to the
|
||||
// legacy root README.md below if the wiki has no landing page or errors.
|
||||
if renderOrgWikiLanding(ctx, profileRepo) {
|
||||
ctx.Data["IsViewingOrgAsMember"] = viewAsMember
|
||||
return true
|
||||
}
|
||||
|
||||
if readmeBlob == nil {
|
||||
return false
|
||||
}
|
||||
@@ -208,3 +223,46 @@ func prepareOrgProfileReadme(ctx *context.Context, prepareResult *shared_user.Pr
|
||||
ctx.Data["IsViewingOrgAsMember"] = viewAsMember
|
||||
return true
|
||||
}
|
||||
|
||||
// renderOrgWikiLanding renders the profile repo's internal wiki landing page as
|
||||
// the org Overview content. It looks for the first existing landing page in the
|
||||
// order home -> index -> readme -> profile (.md) in the repo's attached wiki and,
|
||||
// if found, renders it into ctx.Data["ProfileReadmeContent"]. Returns false (with
|
||||
// no error surfaced) when the wiki or a landing page is absent, so the caller can
|
||||
// safely fall back to the legacy root README.md.
|
||||
func renderOrgWikiLanding(ctx *context.Context, profileRepo *repo_model.Repository) bool {
|
||||
wikiGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, profileRepo.WikiStorageRepo())
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
branch := profileRepo.DefaultWikiBranch
|
||||
if branch == "" {
|
||||
branch = setting.Repository.DefaultBranch
|
||||
}
|
||||
commit, err := wikiGitRepo.GetBranchCommit(branch)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Wiki pages are stored as "<Name>.md"; try common casings for each landing name.
|
||||
for _, name := range []string{"Home", "home", "Index", "index", "README", "Readme", "readme", "Profile", "profile"} {
|
||||
blob, err := commit.GetBlobByPath(name + ".md")
|
||||
if err != nil || blob == nil {
|
||||
continue
|
||||
}
|
||||
content, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
rctx := renderhelper.NewRenderContextRepoFile(ctx, profileRepo, renderhelper.RepoFileOptions{
|
||||
CurrentRefPath: path.Join("branch", util.PathEscapeSegments(branch)),
|
||||
})
|
||||
ctx.Data["ProfileReadmeContent"], err = markdown.RenderString(rctx, content)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -276,6 +276,8 @@ func Routes() *web.Router {
|
||||
|
||||
routes.Head("/", misc.DummyOK) // for health check - doesn't need to be passed through gzip handler
|
||||
routes.Methods("GET, HEAD", "/assets/site-manifest.json", misc.SiteManifest)
|
||||
routes.Methods("GET, HEAD", "/serviceworker.js", misc.ServiceWorker) // PWA service worker (root-scoped)
|
||||
routes.Methods("GET, HEAD", "/-/offline", misc.Offline) // PWA offline fallback page
|
||||
routes.Methods("GET, HEAD, OPTIONS", "/assets/*", routing.MarkLogLevelTrace, public.AssetsCors(), public.FileHandlerFunc())
|
||||
routes.Methods("GET, HEAD", "/avatars/*", avatarStorageHandler(setting.Avatar.Storage, "avatars", storage.Avatars))
|
||||
routes.Methods("GET, HEAD", "/repo-avatars/*", avatarStorageHandler(setting.RepoAvatar.Storage, "repo-avatars", storage.RepoAvatars))
|
||||
|
||||
+2
-2
@@ -3,8 +3,8 @@ import type {Config} from 'stylelint';
|
||||
|
||||
const cssVarFiles = [
|
||||
fileURLToPath(new URL('web_src/css/base.css', import.meta.url)),
|
||||
fileURLToPath(new URL('web_src/css/themes/theme-gitea-light.css', import.meta.url)),
|
||||
fileURLToPath(new URL('web_src/css/themes/theme-gitea-dark.css', import.meta.url)),
|
||||
fileURLToPath(new URL('web_src/css/themes/theme-mokogit-light.css', import.meta.url)),
|
||||
fileURLToPath(new URL('web_src/css/themes/theme-mokogit-dark.css', import.meta.url)),
|
||||
];
|
||||
|
||||
export default {
|
||||
|
||||
+2
-2
@@ -18,8 +18,8 @@ function extractRootVars(css: string) {
|
||||
}
|
||||
|
||||
const vars = extractRootVars([
|
||||
readFileSync(new URL('web_src/css/themes/theme-gitea-light.css', import.meta.url), 'utf8'),
|
||||
readFileSync(new URL('web_src/css/themes/theme-gitea-dark.css', import.meta.url), 'utf8'),
|
||||
readFileSync(new URL('web_src/css/themes/theme-mokogit-light.css', import.meta.url), 'utf8'),
|
||||
readFileSync(new URL('web_src/css/themes/theme-mokogit-dark.css', import.meta.url), 'utf8'),
|
||||
].join('\n'));
|
||||
|
||||
export default {
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{if .Title}}{{.Title}} - {{end}}{{.PageTitleCommon}}</title>
|
||||
<link rel="manifest" href="{{AssetUrlPrefix}}/site-manifest.json">
|
||||
<meta name="theme-color" content="#112855">{{/* MokoGIT navy — installed PWA / mobile browser chrome */}}
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="{{AppName}}">
|
||||
<meta name="author" content="{{if .Repository}}{{.Owner.Name}}{{else}}{{MetaAuthor}}{{end}}">
|
||||
<meta name="description" content="{{if .Repository}}{{.Repository.Name}}{{if .Repository.Description}} - {{.Repository.Description}}{{end}}{{else}}{{MetaDescription}}{{end}}">
|
||||
<meta name="keywords" content="{{MetaKeywords}}">
|
||||
@@ -23,6 +28,13 @@
|
||||
{{template "base/head_style" .}}
|
||||
{{template "base/head_script" .}}
|
||||
{{template "custom/header" .}}
|
||||
<script nonce="{{ctx.CspScriptNonce}}">
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', function () {
|
||||
navigator.serviceWorker.register('{{AppSubUrl}}/serviceworker.js', {scope: '{{AppSubUrl}}/'}).catch(function () {});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
{{template "custom/body_outer_pre" .}}
|
||||
|
||||
@@ -40,6 +40,36 @@
|
||||
|
||||
<!-- the full dropdown menus -->
|
||||
<div class="navbar-right">
|
||||
{{/* MokoGIT: accessibility menu toggle (client-side, all users) */}}
|
||||
<button type="button" class="item mokogit-a11y-toggle" id="mokogit-a11y-toggle"
|
||||
aria-haspopup="dialog" aria-expanded="false" aria-controls="mokogit-a11y-panel"
|
||||
data-tooltip-content="{{ctx.Locale.Tr "mokogit.a11y_title"}}"
|
||||
aria-label="{{ctx.Locale.Tr "mokogit.a11y_title"}}"
|
||||
data-label-title="{{ctx.Locale.Tr "mokogit.a11y_title"}}"
|
||||
data-label-textsize="{{ctx.Locale.Tr "mokogit.a11y_text_size"}}"
|
||||
data-label-decrease="{{ctx.Locale.Tr "mokogit.a11y_decrease"}}"
|
||||
data-label-increase="{{ctx.Locale.Tr "mokogit.a11y_increase"}}"
|
||||
data-label-reset="{{ctx.Locale.Tr "mokogit.a11y_reset"}}"
|
||||
data-label-invert="{{ctx.Locale.Tr "mokogit.a11y_invert"}}"
|
||||
data-label-contrast="{{ctx.Locale.Tr "mokogit.a11y_contrast"}}"
|
||||
data-label-links="{{ctx.Locale.Tr "mokogit.a11y_links"}}"
|
||||
data-label-readable="{{ctx.Locale.Tr "mokogit.a11y_readable"}}"
|
||||
data-label-pause="{{ctx.Locale.Tr "mokogit.a11y_pause"}}"
|
||||
data-label-resetall="{{ctx.Locale.Tr "mokogit.a11y_reset_all"}}">
|
||||
<span class="flex-text-block"><i class="fa-solid fa-universal-access"></i></span>
|
||||
</button>
|
||||
{{/* MokoGIT: light/dark theme toggle */}}
|
||||
<button type="button" class="item mokogit-theme-toggle" id="mokogit-theme-toggle"
|
||||
data-tooltip-content="{{ctx.Locale.Tr "mokogit.theme_toggle"}}"
|
||||
aria-label="{{ctx.Locale.Tr "mokogit.theme_toggle"}}">
|
||||
<span class="flex-text-block"><i class="fa-solid fa-sun"></i><i class="fa-solid fa-moon"></i></span>
|
||||
</button>
|
||||
{{if .IsSigned}}
|
||||
<form id="mokogit-theme-form" method="post" action="{{AppSubUrl}}/user/settings/appearance/theme" class="tw-hidden">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="theme" value="">
|
||||
</form>
|
||||
{{end}}
|
||||
{{if and .IsSigned .MustChangePassword}}
|
||||
<div class="ui dropdown jump item" data-tooltip-content="{{ctx.Locale.Tr "user_profile_and_more"}}">
|
||||
<span class="text">
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{{/* MokoGIT brand override hook (ships by default).
|
||||
Drop a stylesheet at custom/public/assets/css/mokogit-custom.css on any instance to re-skin the
|
||||
MokoGIT brand tokens LIVE, with no rebuild -- Gitea serves /assets/ with custom/ taking precedence.
|
||||
If no override file is present the link is a harmless 404 (no visual/functional impact).
|
||||
Instance admins may still override this whole partial via custom/templates/custom/header.tmpl. */}}
|
||||
<link rel="stylesheet" href="{{AppSubUrl}}/assets/css/mokogit-custom.css">
|
||||
|
||||
@@ -68,11 +68,11 @@
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
<input class="enable-system-radio" name="wiki_mode" type="radio" value="" data-context="#external_wiki_box" data-target="#internal_wiki_box" {{if eq .Org.WikiMode ""}}checked{{end}}>
|
||||
<label>Internal wiki (uses <code>.profile</code> / <code>.profile-private</code> repo wikis)</label>
|
||||
<label>Internal wiki (uses <code>.mokogit</code> / <code>.mokogit-private</code> repo wikis)</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="internal_wiki_box" class="field tw-pl-4 {{if ne .Org.WikiMode ""}}disabled{{end}}">
|
||||
<p class="help">Enable the wiki on <code>.profile</code> (public) and/or <code>.profile-private</code> (members-only) repos.</p>
|
||||
<p class="help">Enable the wiki on <code>.mokogit</code> (public) and/or <code>.mokogit-private</code> (members-only) repos.</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
|
||||
@@ -166,7 +166,8 @@
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
{{if .Permission.CanRead ctx.Consts.RepoUnitTypeWiki}}
|
||||
{{/* MokoGIT: the internal-wiki (.mokogit/.mokogit-private) surfaces as the org Overview, so hide its per-repo Wiki tab. */}}
|
||||
{{if and (.Permission.CanRead ctx.Consts.RepoUnitTypeWiki) (not (or (eq .Repository.Name ".mokogit") (eq .Repository.Name ".mokogit-private")))}}
|
||||
<a class="{{if .PageIsWiki}}active {{end}}item" href="{{.RepoLink}}/wiki">
|
||||
{{svg "octicon-book"}} {{ctx.Locale.Tr "repo.wiki"}}
|
||||
</a>
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
<span class="status-button-text">{{ctx.Locale.Tr "repo.issues.reopen_issue"}}</span>
|
||||
</button>
|
||||
{{else}}
|
||||
<select name="status_id" class="ui compact dropdown" style="min-width:140px;padding:7px 10px;border:1px solid var(--color-secondary);border-radius:4px;background:var(--color-body);">
|
||||
<select name="status_id" class="issue-status-select">
|
||||
<option value="">-- {{ctx.Locale.Tr "repo.issues.status"}} --</option>
|
||||
{{range .IssueStatusDefs}}
|
||||
{{if not .ClosesIssue}}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
/* MokoGIT accessibility menu — panel UI + the effect classes toggled on <html>.
|
||||
Ported from the mokoconsulting.tech / MokoOnyx accessibility widget. */
|
||||
|
||||
/* ---- navbar toggle button ---- */
|
||||
.mokogit-a11y-toggle,
|
||||
.mokogit-theme-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* theme toggle: show sun in dark mode (click -> go light), moon in light mode */
|
||||
.mokogit-theme-toggle .fa-moon { display: inline-block; }
|
||||
.mokogit-theme-toggle .fa-sun { display: none; }
|
||||
.mokogit-theme-toggle.is-dark .fa-moon { display: none; }
|
||||
.mokogit-theme-toggle.is-dark .fa-sun { display: inline-block; }
|
||||
|
||||
/* ---- panel ---- */
|
||||
.a11y-panel {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
min-width: 260px;
|
||||
max-width: 320px;
|
||||
padding: 12px;
|
||||
background: var(--color-body);
|
||||
color: var(--color-text);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: var(--border-radius, 4px);
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.a11y-panel-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
}
|
||||
|
||||
.a11y-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.a11y-row-label { font-size: 0.95em; }
|
||||
|
||||
.a11y-size-controls { display: flex; align-items: center; gap: 4px; }
|
||||
|
||||
.a11y-size-btn {
|
||||
min-width: 28px;
|
||||
height: 28px;
|
||||
padding: 0 8px;
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: var(--border-radius, 4px);
|
||||
background: var(--color-secondary-bg, var(--color-box-body));
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.a11y-size-value { min-width: 44px; text-align: center; font-variant-numeric: tabular-nums; }
|
||||
|
||||
/* toggle switch */
|
||||
.a11y-switch {
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 22px;
|
||||
border-radius: 11px;
|
||||
border: 1px solid var(--color-secondary);
|
||||
background: var(--color-secondary);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.a11y-switch::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
.a11y-switch.is-on { background: var(--color-primary); border-color: var(--color-primary); }
|
||||
.a11y-switch.is-on::after { transform: translateX(18px); }
|
||||
|
||||
.a11y-reset-all {
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: var(--border-radius, 4px);
|
||||
background: var(--color-secondary-bg, var(--color-box-body));
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ==== effect classes toggled on <html> ==== */
|
||||
|
||||
/* invert colors (keep media upright) */
|
||||
html.a11y-inverted { filter: invert(1) hue-rotate(180deg); }
|
||||
html.a11y-inverted img,
|
||||
html.a11y-inverted video,
|
||||
html.a11y-inverted picture,
|
||||
html.a11y-inverted canvas,
|
||||
html.a11y-inverted [style*="background-image"] { filter: invert(1) hue-rotate(180deg); }
|
||||
|
||||
/* high contrast */
|
||||
html.a11y-high-contrast { filter: contrast(1.3); }
|
||||
html.a11y-high-contrast a { text-decoration: underline; }
|
||||
|
||||
/* highlight links */
|
||||
html.a11y-highlight-links a {
|
||||
text-decoration: underline !important;
|
||||
outline: 1px solid currentcolor;
|
||||
outline-offset: 1px;
|
||||
background: rgba(255, 235, 59, 0.25);
|
||||
}
|
||||
|
||||
/* readable font */
|
||||
html.a11y-readable-font,
|
||||
html.a11y-readable-font * {
|
||||
font-family: Verdana, Tahoma, "Segoe UI", Arial, sans-serif !important;
|
||||
letter-spacing: 0.02em;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
/* pause animations */
|
||||
html.a11y-pause-animations *,
|
||||
html.a11y-pause-animations *::before,
|
||||
html.a11y-pause-animations *::after {
|
||||
animation-duration: 0s !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0s !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
@import "./modules/normalize.css";
|
||||
@import "./modules/animations.css";
|
||||
|
||||
/* Vendored Font Awesome 7 Free (unmodified). Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT.
|
||||
License + attribution: web_src/css/vendor/fontawesome/LICENSE.txt and THIRD-PARTY-NOTICES.md. */
|
||||
@import "./vendor/fontawesome/css/all.min.css";
|
||||
|
||||
/* fomantic replacements */
|
||||
@import "./modules/button.css";
|
||||
@import "./modules/container.css";
|
||||
@@ -49,6 +53,7 @@
|
||||
@import "./features/cropper.css";
|
||||
@import "./features/console.css";
|
||||
@import "./features/captcha.css";
|
||||
@import "./features/a11y-menu.css";
|
||||
|
||||
@import "./markup/content.css";
|
||||
@import "./markup/codeblock.css";
|
||||
@@ -65,6 +70,7 @@
|
||||
@import "./repo/issue-card.css";
|
||||
@import "./repo/issue-label.css";
|
||||
@import "./repo/issue-list.css";
|
||||
@import "./repo/issue-status.css";
|
||||
@import "./repo/list-header.css";
|
||||
@import "./repo/file-view.css";
|
||||
@import "./repo/wiki.css";
|
||||
@@ -87,4 +93,7 @@
|
||||
|
||||
@import "./helpers.css";
|
||||
|
||||
/* MokoGIT brand reskin — last so it overrides component modules (Tailwind utilities still win). */
|
||||
@import "./mokogit-brand.css";
|
||||
|
||||
@tailwind utilities;
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/* MokoGIT brand reskin layer
|
||||
*
|
||||
* Applies the mokoconsulting.tech design language on top of Gitea's component
|
||||
* modules, using theme tokens (theme-mokogit-{light,dark}.css) wherever a
|
||||
* variable exists so light/dark and runtime overrides "just work".
|
||||
*
|
||||
* Sourced from A:\MokoOnyx (the mokoconsulting.tech template):
|
||||
* - fonts: system stack + Roboto (Gitea's --fonts-proportional already matches)
|
||||
* - radius: .25rem base (Gitea --border-radius is already 4px)
|
||||
* - soft elevation on cards/dropdowns/modals
|
||||
* - buttons: weight 400, .6rem/1rem padding
|
||||
* - deep-navy nav + footer (set via --color-nav-bg in the theme files)
|
||||
*
|
||||
* Load order: imported last in index.css so it overrides component modules;
|
||||
* Tailwind utilities still win (declared after this).
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* Exact mokoconsulting.tech font stack for parity (Gitea falls back to this). */
|
||||
--fonts-override: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans";
|
||||
|
||||
/* mokoconsulting soft-elevation shadow scale (referenced by the rules below
|
||||
* and available for custom overrides). */
|
||||
--mokogit-shadow-sm: 0 .125rem .25rem rgba(0, 0, 0, .075);
|
||||
--mokogit-shadow: 0 .5rem 1rem rgba(0, 0, 0, .15);
|
||||
--mokogit-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, .175);
|
||||
}
|
||||
|
||||
/* Heavier elevation on dark surfaces — explicit dark theme + auto-following-system. */
|
||||
:root[data-theme="mokogit-dark"] {
|
||||
--mokogit-shadow-sm: 0 .125rem .25rem rgba(0, 0, 0, .25);
|
||||
--mokogit-shadow: 0 .5rem 1rem rgba(0, 0, 0, .5);
|
||||
--mokogit-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, .6);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root[data-theme="mokogit-auto"] {
|
||||
--mokogit-shadow-sm: 0 .125rem .25rem rgba(0, 0, 0, .25);
|
||||
--mokogit-shadow: 0 .5rem 1rem rgba(0, 0, 0, .5);
|
||||
--mokogit-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, .6);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Buttons: mokoconsulting weight + padding ---- */
|
||||
.ui.button,
|
||||
.ui.buttons .button {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* ---- Soft elevation on floating surfaces ---- */
|
||||
.ui.card,
|
||||
.ui.cards > .card,
|
||||
.ui.segment {
|
||||
box-shadow: var(--mokogit-shadow-sm);
|
||||
}
|
||||
|
||||
.ui.dropdown .menu,
|
||||
.ui.menu .dropdown.item .menu {
|
||||
box-shadow: var(--mokogit-shadow);
|
||||
}
|
||||
|
||||
.ui.modal {
|
||||
box-shadow: var(--mokogit-shadow-lg);
|
||||
}
|
||||
|
||||
/* ---- Navbar: brand weight for the site title ---- */
|
||||
#navbar .navbar-brand,
|
||||
#navbar .brand {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ---- Footer: navy brand band (color already via --color-footer) ---- */
|
||||
.page-footer,
|
||||
footer.page-footer {
|
||||
color: var(--color-nav-text);
|
||||
}
|
||||
.page-footer a {
|
||||
color: var(--color-nav-text);
|
||||
opacity: .85;
|
||||
}
|
||||
.page-footer a:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ---- Links: mokoconsulting underline affordance in prose ---- */
|
||||
.markup a:not(.btn):not([class^="ui "]) {
|
||||
text-decoration-thickness: from-font;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/* Custom issue-status control.
|
||||
|
||||
Kept as a plain native <select> (NOT a fomantic ".ui dropdown"): when this
|
||||
element carried the "ui compact dropdown" class, fomantic enhanced it into an
|
||||
overlay menu that rendered mispositioned and behind .issue-content, so the
|
||||
status options were present in the DOM but invisible. A native <select>
|
||||
renders its option list via the browser, so the options always show.
|
||||
|
||||
Styles moved here from an inline style= attribute on the element. */
|
||||
.issue-status-select {
|
||||
min-width: 140px;
|
||||
padding: 7px 10px;
|
||||
color: var(--color-text);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 4px;
|
||||
background: var(--color-body);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
@import "./theme-gitea-light-protanopia-deuteranopia.css" (prefers-color-scheme: light);
|
||||
@import "./theme-gitea-dark-protanopia-deuteranopia.css" (prefers-color-scheme: dark);
|
||||
|
||||
gitea-theme-meta-info {
|
||||
--theme-display-name: "Auto";
|
||||
--theme-colorblind-type: "red-green";
|
||||
--theme-color-scheme: "auto";
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
@import "./theme-gitea-light-tritanopia.css" (prefers-color-scheme: light);
|
||||
@import "./theme-gitea-dark-tritanopia.css" (prefers-color-scheme: dark);
|
||||
|
||||
gitea-theme-meta-info {
|
||||
--theme-display-name: "Auto";
|
||||
--theme-colorblind-type: "blue-yellow";
|
||||
--theme-color-scheme: "auto";
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
@import "./theme-gitea-light.css" (prefers-color-scheme: light);
|
||||
@import "./theme-gitea-dark.css" (prefers-color-scheme: dark);
|
||||
|
||||
gitea-theme-meta-info {
|
||||
--theme-display-name: "Auto";
|
||||
--theme-color-scheme: "auto";
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
@import "./theme-gitea-dark.css";
|
||||
|
||||
gitea-theme-meta-info {
|
||||
--theme-display-name: "Dark";
|
||||
--theme-colorblind-type: "red-green";
|
||||
--theme-color-scheme: "dark";
|
||||
}
|
||||
|
||||
/* red/green colorblind-friendly colors */
|
||||
:root {
|
||||
--color-diff-added-fg: #58a6ff;
|
||||
--color-diff-added-linenum-bg: #243d5d;
|
||||
--color-diff-added-row-bg: #132339;
|
||||
--color-diff-added-word-bg: #214d87;
|
||||
--color-diff-removed-fg: #f0883e;
|
||||
--color-diff-removed-linenum-bg: #5b361c;
|
||||
--color-diff-removed-row-bg: #3c2419;
|
||||
--color-diff-removed-word-bg: #824e1f;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
@import "./theme-gitea-dark.css";
|
||||
|
||||
gitea-theme-meta-info {
|
||||
--theme-display-name: "Dark";
|
||||
--theme-colorblind-type: "blue-yellow";
|
||||
--theme-color-scheme: "dark";
|
||||
}
|
||||
|
||||
/* blue/yellow colorblind-friendly colors */
|
||||
:root {
|
||||
--color-diff-added-fg: #58a6ff;
|
||||
--color-diff-added-linenum-bg: #243d5d;
|
||||
--color-diff-added-row-bg: #132339;
|
||||
--color-diff-added-word-bg: #214d87;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
@import "./theme-gitea-light.css";
|
||||
|
||||
gitea-theme-meta-info {
|
||||
--theme-display-name: "Light";
|
||||
--theme-colorblind-type: "red-green";
|
||||
--theme-color-scheme: "light";
|
||||
}
|
||||
|
||||
/* red/green colorblind-friendly colors */
|
||||
:root {
|
||||
--color-diff-added-fg: #2185d0;
|
||||
--color-diff-added-linenum-bg: #b6e3ff;
|
||||
--color-diff-added-row-bg: #ddf4ff;
|
||||
--color-diff-added-word-bg: #b6e3ff;
|
||||
--color-diff-removed-fg: #fc6500;
|
||||
--color-diff-removed-linenum-bg: #ffd8b5;
|
||||
--color-diff-removed-row-bg: #fff1e5;
|
||||
--color-diff-removed-word-bg: #ffd8b5;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
@import "./theme-gitea-light.css";
|
||||
|
||||
gitea-theme-meta-info {
|
||||
--theme-display-name: "Light";
|
||||
--theme-colorblind-type: "blue-yellow";
|
||||
--theme-color-scheme: "light";
|
||||
}
|
||||
|
||||
/* blue/yellow colorblind-friendly colors */
|
||||
:root {
|
||||
--color-diff-added-fg: #2185d0;
|
||||
--color-diff-added-linenum-bg: #b6e3ff;
|
||||
--color-diff-added-row-bg: #ddf4ff;
|
||||
--color-diff-added-word-bg: #b6e3ff;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
@import "./theme-mokogit-light.css" (prefers-color-scheme: light);
|
||||
@import "./theme-mokogit-dark.css" (prefers-color-scheme: dark);
|
||||
|
||||
gitea-theme-meta-info {
|
||||
--theme-display-name: "MokoGIT Auto";
|
||||
--theme-color-scheme: "auto";
|
||||
}
|
||||
+59
-43
@@ -1,37 +1,38 @@
|
||||
gitea-theme-meta-info {
|
||||
--theme-display-name: "Dark";
|
||||
--theme-display-name: "MokoGIT Dark";
|
||||
--theme-color-scheme: "dark";
|
||||
}
|
||||
|
||||
:root {
|
||||
--is-dark-theme: true;
|
||||
--color-primary: #4183c4;
|
||||
/* MokoGIT brand primary (dark): bright sky-blue accent (mokoconsulting.tech --accent-color-primary #3f8ff0), readable on dark bg. Nav uses #112855 below. */
|
||||
--color-primary: #3f8ff0;
|
||||
--color-primary-contrast: #ffffff;
|
||||
--color-primary-dark-1: #548fca;
|
||||
--color-primary-dark-2: #679cd0;
|
||||
--color-primary-dark-3: #7aa8d6;
|
||||
--color-primary-dark-4: #8db5dc;
|
||||
--color-primary-dark-5: #b3cde7;
|
||||
--color-primary-dark-6: #d9e6f3;
|
||||
--color-primary-dark-7: #f4f8fb;
|
||||
--color-primary-light-1: #3876b3;
|
||||
--color-primary-light-2: #31699f;
|
||||
--color-primary-light-3: #2b5c8b;
|
||||
--color-primary-light-4: #254f77;
|
||||
--color-primary-light-5: #193450;
|
||||
--color-primary-light-6: #0c1a28;
|
||||
--color-primary-light-7: #04080c;
|
||||
--color-primary-alpha-10: #4183c419;
|
||||
--color-primary-alpha-20: #4183c433;
|
||||
--color-primary-alpha-30: #4183c44b;
|
||||
--color-primary-alpha-40: #4183c466;
|
||||
--color-primary-alpha-50: #4183c480;
|
||||
--color-primary-alpha-60: #4183c499;
|
||||
--color-primary-alpha-70: #4183c4b3;
|
||||
--color-primary-alpha-80: #4183c4cc;
|
||||
--color-primary-alpha-90: #4183c4e1;
|
||||
--color-primary-hover: var(--color-primary-light-1);
|
||||
--color-primary-active: var(--color-primary-light-2);
|
||||
--color-primary-dark-1: #58a0f5;
|
||||
--color-primary-dark-2: #71b0f8;
|
||||
--color-primary-dark-3: #8bbffa;
|
||||
--color-primary-dark-4: #a5cffb;
|
||||
--color-primary-dark-5: #c5e0fd;
|
||||
--color-primary-dark-6: #e0eefe;
|
||||
--color-primary-dark-7: #f3f8ff;
|
||||
--color-primary-light-1: #3579cf;
|
||||
--color-primary-light-2: #2c66b0;
|
||||
--color-primary-light-3: #234f88;
|
||||
--color-primary-light-4: #1a3a63;
|
||||
--color-primary-light-5: #14304f;
|
||||
--color-primary-light-6: #0c1c30;
|
||||
--color-primary-light-7: #060e18;
|
||||
--color-primary-alpha-10: #3f8ff019;
|
||||
--color-primary-alpha-20: #3f8ff033;
|
||||
--color-primary-alpha-30: #3f8ff04b;
|
||||
--color-primary-alpha-40: #3f8ff066;
|
||||
--color-primary-alpha-50: #3f8ff080;
|
||||
--color-primary-alpha-60: #3f8ff099;
|
||||
--color-primary-alpha-70: #3f8ff0b3;
|
||||
--color-primary-alpha-80: #3f8ff0cc;
|
||||
--color-primary-alpha-90: #3f8ff0e1;
|
||||
--color-primary-hover: var(--color-primary-dark-1);
|
||||
--color-primary-active: var(--color-primary-dark-2);
|
||||
--color-secondary: #3f4248;
|
||||
--color-secondary-dark-1: #46494f;
|
||||
--color-secondary-dark-2: #4f5259;
|
||||
@@ -72,14 +73,14 @@ gitea-theme-meta-info {
|
||||
--color-console-menu-bg: #292b2e;
|
||||
--color-console-menu-border: #46494f;
|
||||
--color-console-link: #969aa1;
|
||||
/* named colors */
|
||||
--color-red: #cc4848;
|
||||
--color-orange: #cc580c;
|
||||
--color-yellow: #cc9903;
|
||||
/* named colors - MokoGIT/mokoconsulting.tech brand palette (dark tuned) */
|
||||
--color-red: #ff7a73;
|
||||
--color-orange: #ff9c4d;
|
||||
--color-yellow: #ffd166;
|
||||
--color-olive: #91a313;
|
||||
--color-green: #87ab63;
|
||||
--color-teal: #00918a;
|
||||
--color-blue: #3a8ac6;
|
||||
--color-green: #78d694;
|
||||
--color-teal: #76e3ff;
|
||||
--color-blue: #91a4ff;
|
||||
--color-violet: #906ae1;
|
||||
--color-purple: #b259d0;
|
||||
--color-pink: #d22e8b;
|
||||
@@ -207,11 +208,11 @@ gitea-theme-meta-info {
|
||||
--color-orange-badge-hover-bg: #f2711c4d;
|
||||
--color-git: #f05133;
|
||||
--color-logo: #609926;
|
||||
/* target-based colors */
|
||||
--color-body: #1e1f20;
|
||||
--color-box-header: #1b1c1e;
|
||||
--color-box-body: #161718;
|
||||
--color-box-body-highlight: #202124;
|
||||
/* target-based colors - MokoGIT navy-dark surfaces (mokoconsulting.tech) */
|
||||
--color-body: #0e1318;
|
||||
--color-box-header: #151b22;
|
||||
--color-box-body: #10151b;
|
||||
--color-box-body-highlight: #151b22;
|
||||
--color-text-dark: #f8f8f8;
|
||||
--color-text: #d2d4d8;
|
||||
--color-text-light: #c0c2c7;
|
||||
@@ -238,7 +239,7 @@ gitea-theme-meta-info {
|
||||
--color-code-bg: #161718;
|
||||
--color-shadow: #0b0b0c58;
|
||||
--color-shadow-opaque: #0b0b0c;
|
||||
--color-secondary-bg: #2e3033;
|
||||
--color-secondary-bg: #151b22;
|
||||
--color-expand-button: #333539;
|
||||
--color-placeholder-text: var(--color-text-light-3);
|
||||
--color-editor-line-highlight: var(--color-secondary-alpha-40);
|
||||
@@ -250,9 +251,10 @@ gitea-theme-meta-info {
|
||||
--color-reaction-active-bg: var(--color-primary-light-5);
|
||||
--color-tooltip-text: #fafafa;
|
||||
--color-tooltip-bg: #0b0b0cf0;
|
||||
--color-nav-bg: #18191b;
|
||||
--color-nav-hover-bg: var(--color-secondary-light-1);
|
||||
--color-nav-text: var(--color-text);
|
||||
/* MokoGIT brand: signature deep-navy top nav + footer (matches mokoconsulting.tech) */
|
||||
--color-nav-bg: #112855;
|
||||
--color-nav-hover-bg: rgba(255, 255, 255, 0.12);
|
||||
--color-nav-text: #ffffff;
|
||||
--color-secondary-nav-bg: #1a1b1e;
|
||||
--color-label-text: var(--color-text);
|
||||
--color-label-bg: #7a7f8a4b;
|
||||
@@ -308,6 +310,20 @@ gitea-theme-meta-info {
|
||||
--color-syntax-traceback: #ff8686;
|
||||
--color-syntax-matching-bracket-bg: #00918a48;
|
||||
--color-syntax-nonmatching-bracket-bg: #cc484848;
|
||||
/* ===== MokoOnyx surface separation (navy-tinted elevation) =====
|
||||
Establish a clear 3-step elevation so panels separate from the page:
|
||||
page (darkest) -> box body -> box header/secondary (lightest). All tones
|
||||
are navy-tinted (blue channel raised) to stay cohesive with the #112855
|
||||
brand nav, mirroring mokoconsulting.tech's dark surface hierarchy. */
|
||||
--color-body: #0b1017; /* page: deepest navy-black */
|
||||
--color-box-body: #121a24; /* cards/boxes: lifted, navy-tinted */
|
||||
--color-box-header: #17212e; /* box headers: lightest surface */
|
||||
--color-box-body-highlight: #17212e;
|
||||
--color-secondary-bg: #17212e;
|
||||
--color-card: #141d28;
|
||||
--color-menu: #141d28;
|
||||
--color-button: #141d28;
|
||||
--color-light-border: #ffffff24; /* more visible panel edges on dark */
|
||||
accent-color: var(--color-accent);
|
||||
color-scheme: dark;
|
||||
}
|
||||
+50
-36
@@ -1,35 +1,36 @@
|
||||
gitea-theme-meta-info {
|
||||
--theme-display-name: "Light";
|
||||
--theme-display-name: "MokoGIT Light";
|
||||
--theme-color-scheme: "light";
|
||||
}
|
||||
|
||||
:root {
|
||||
--is-dark-theme: false;
|
||||
--color-primary: #4183c4;
|
||||
/* MokoGIT brand primary: deep navy (mokoconsulting.tech --primary #010156). Nav uses #112855 below. */
|
||||
--color-primary: #010156;
|
||||
--color-primary-contrast: #ffffff;
|
||||
--color-primary-dark-1: #3876b3;
|
||||
--color-primary-dark-2: #31699f;
|
||||
--color-primary-dark-3: #2b5c8b;
|
||||
--color-primary-dark-4: #254f77;
|
||||
--color-primary-dark-5: #193450;
|
||||
--color-primary-dark-6: #0c1a28;
|
||||
--color-primary-dark-7: #04080c;
|
||||
--color-primary-light-1: #548fca;
|
||||
--color-primary-light-2: #679cd0;
|
||||
--color-primary-light-3: #7aa8d6;
|
||||
--color-primary-light-4: #8db5dc;
|
||||
--color-primary-light-5: #b3cde7;
|
||||
--color-primary-light-6: #d9e6f3;
|
||||
--color-primary-light-7: #f4f8fb;
|
||||
--color-primary-alpha-10: #4183c419;
|
||||
--color-primary-alpha-20: #4183c433;
|
||||
--color-primary-alpha-30: #4183c44b;
|
||||
--color-primary-alpha-40: #4183c466;
|
||||
--color-primary-alpha-50: #4183c480;
|
||||
--color-primary-alpha-60: #4183c499;
|
||||
--color-primary-alpha-70: #4183c4b3;
|
||||
--color-primary-alpha-80: #4183c4cc;
|
||||
--color-primary-alpha-90: #4183c4e1;
|
||||
--color-primary-dark-1: #010149;
|
||||
--color-primary-dark-2: #010141;
|
||||
--color-primary-dark-3: #010139;
|
||||
--color-primary-dark-4: #010130;
|
||||
--color-primary-dark-5: #010122;
|
||||
--color-primary-dark-6: #010114;
|
||||
--color-primary-dark-7: #01010a;
|
||||
--color-primary-light-1: #12137a;
|
||||
--color-primary-light-2: #2a2c99;
|
||||
--color-primary-light-3: #4548b8;
|
||||
--color-primary-light-4: #6b6ecf;
|
||||
--color-primary-light-5: #b3b4de;
|
||||
--color-primary-light-6: #d9daf0;
|
||||
--color-primary-light-7: #f3f3fb;
|
||||
--color-primary-alpha-10: #01015619;
|
||||
--color-primary-alpha-20: #01015633;
|
||||
--color-primary-alpha-30: #0101564b;
|
||||
--color-primary-alpha-40: #01015666;
|
||||
--color-primary-alpha-50: #01015680;
|
||||
--color-primary-alpha-60: #01015699;
|
||||
--color-primary-alpha-70: #010156b3;
|
||||
--color-primary-alpha-80: #010156cc;
|
||||
--color-primary-alpha-90: #010156e1;
|
||||
--color-primary-hover: var(--color-primary-dark-1);
|
||||
--color-primary-active: var(--color-primary-dark-2);
|
||||
--color-secondary: #d0d7de;
|
||||
@@ -72,14 +73,14 @@ gitea-theme-meta-info {
|
||||
--color-console-menu-bg: #f8f9fb;
|
||||
--color-console-menu-border: #d0d7de;
|
||||
--color-console-link: #5c656d;
|
||||
/* named colors */
|
||||
--color-red: #db2828;
|
||||
--color-orange: #f2711c;
|
||||
--color-yellow: #fbbd08;
|
||||
/* named colors - MokoGIT/mokoconsulting.tech brand palette */
|
||||
--color-red: #a51f18;
|
||||
--color-orange: #fd7e17;
|
||||
--color-yellow: #ad6200;
|
||||
--color-olive: #b5cc18;
|
||||
--color-green: #21ba45;
|
||||
--color-teal: #00b5ad;
|
||||
--color-blue: #2185d0;
|
||||
--color-green: #448344;
|
||||
--color-teal: #5abfdd;
|
||||
--color-blue: #010156;
|
||||
--color-violet: #6435c9;
|
||||
--color-purple: #a333c8;
|
||||
--color-pink: #e03997;
|
||||
@@ -238,7 +239,7 @@ gitea-theme-meta-info {
|
||||
--color-code-bg: #fafdff;
|
||||
--color-shadow: #00001726;
|
||||
--color-shadow-opaque: #c7ced5;
|
||||
--color-secondary-bg: #f2f5f8;
|
||||
--color-secondary-bg: #eaedf0;
|
||||
--color-expand-button: #cfe8fa;
|
||||
--color-placeholder-text: var(--color-text-light-3);
|
||||
--color-editor-line-highlight: var(--color-secondary-alpha-30);
|
||||
@@ -250,9 +251,10 @@ gitea-theme-meta-info {
|
||||
--color-reaction-active-bg: var(--color-primary-light-6);
|
||||
--color-tooltip-text: #fbfdff;
|
||||
--color-tooltip-bg: #000017f0;
|
||||
--color-nav-bg: #f6f7fa;
|
||||
--color-nav-hover-bg: var(--color-secondary-light-1);
|
||||
--color-nav-text: var(--color-text);
|
||||
/* MokoGIT brand: signature deep-navy top nav + footer (matches mokoconsulting.tech) */
|
||||
--color-nav-bg: #112855;
|
||||
--color-nav-hover-bg: rgba(255, 255, 255, 0.12);
|
||||
--color-nav-text: #ffffff;
|
||||
--color-secondary-nav-bg: #f9fafb;
|
||||
--color-label-text: var(--color-text);
|
||||
--color-label-bg: #949da64b;
|
||||
@@ -308,6 +310,18 @@ gitea-theme-meta-info {
|
||||
--color-syntax-traceback: #c00000;
|
||||
--color-syntax-matching-bracket-bg: #00b5ad38;
|
||||
--color-syntax-nonmatching-bracket-bg: #db282838;
|
||||
/* ===== MokoOnyx surface separation (mokoconsulting.tech gray scale) =====
|
||||
Give the page a soft gray base so white cards/boxes visibly lift off it,
|
||||
with a defined gray-300 border tone for clear panel edges. Mirrors the
|
||||
surface hierarchy of mokoconsulting.tech (MokoOnyx --gray-100/200/300). */
|
||||
--color-body: #f0f3f6; /* page: soft gray (was #fff, blended into cards) */
|
||||
--color-box-body: #ffffff; /* cards/boxes: white, now lift off the gray page */
|
||||
--color-box-header: #e9edf1; /* box headers: gray-200 tone */
|
||||
--color-secondary-bg: #e7ebef; /* MokoOnyx gray-200 */
|
||||
--color-card: #ffffff;
|
||||
--color-menu: #ffffff;
|
||||
--color-button: #ffffff;
|
||||
--color-light-border: #0000172e; /* stronger gray-300 border definition */
|
||||
accent-color: var(--color-accent);
|
||||
color-scheme: light;
|
||||
}
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
Fonticons, Inc. (https://fontawesome.com)
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Font Awesome Free License
|
||||
|
||||
Font Awesome Free is free, open source, and GPL friendly. You can use it for
|
||||
commercial projects, open source projects, or really almost whatever you want.
|
||||
Full Font Awesome Free license: https://fontawesome.com/license/free.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/)
|
||||
|
||||
The Font Awesome Free download is licensed under a Creative Commons
|
||||
Attribution 4.0 International License and applies to all icons packaged
|
||||
as SVG and JS file types.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
# Fonts: SIL OFL 1.1 License
|
||||
|
||||
In the Font Awesome Free download, the SIL OFL license applies to all icons
|
||||
packaged as web and desktop font files.
|
||||
|
||||
Copyright (c) 2025 Fonticons, Inc. (https://fontawesome.com)
|
||||
with Reserved Font Name: "Font Awesome".
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
SIL OPEN FONT LICENSE
|
||||
Version 1.1 - 26 February 2007
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting — in part or in whole — any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
# Code: MIT License (https://opensource.org/licenses/MIT)
|
||||
|
||||
In the Font Awesome Free download, the MIT license applies to all non-font and
|
||||
non-icon files.
|
||||
|
||||
Copyright 2025 Fonticons, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without limitation the rights to use, copy,
|
||||
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
and to permit persons to whom the Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
# Attribution
|
||||
|
||||
Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font
|
||||
Awesome Free files already contain embedded comments with sufficient
|
||||
attribution, so you shouldn't need to do anything additional when using these
|
||||
files normally.
|
||||
|
||||
We've kept attribution comments terse, so we ask that you do not actively work
|
||||
to remove them from files, especially code. They're a great way for folks to
|
||||
learn about Font Awesome.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
# Brand Icons
|
||||
|
||||
All brand icons are trademarks of their respective owners. The use of these
|
||||
trademarks does not indicate endorsement of the trademark holder by Font
|
||||
Awesome, nor vice versa. **Please do not use brand logos for any purpose except
|
||||
to represent the company, product, or service to which they refer.**
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,183 @@
|
||||
// MokoGIT accessibility menu (ported from MokoOnyx / mokoconsulting.tech).
|
||||
// Client-side, persisted in localStorage["moko-a11y"]. Each option toggles a
|
||||
// class on <html> (except text size, which sets root font-size). Labels are read
|
||||
// from data-* attributes on the toggle button so they stay localized.
|
||||
|
||||
type A11yState = {
|
||||
fontStep: number; // index into FONT_STEPS
|
||||
invert: boolean;
|
||||
contrast: boolean;
|
||||
links: boolean;
|
||||
readable: boolean;
|
||||
pauseAnim: boolean;
|
||||
};
|
||||
|
||||
const LS_KEY = 'moko-a11y';
|
||||
const FONT_STEPS = [85, 90, 100, 110, 120, 130];
|
||||
const DEFAULT_STEP = 2; // 100%
|
||||
|
||||
const DEFAULT_STATE: A11yState = {
|
||||
fontStep: DEFAULT_STEP, invert: false, contrast: false, links: false, readable: false, pauseAnim: false,
|
||||
};
|
||||
|
||||
function load(): A11yState {
|
||||
try {
|
||||
return {...DEFAULT_STATE, ...JSON.parse(localStorage.getItem(LS_KEY) || '{}')};
|
||||
} catch {
|
||||
return {...DEFAULT_STATE};
|
||||
}
|
||||
}
|
||||
|
||||
function save(s: A11yState): void {
|
||||
try {
|
||||
localStorage.setItem(LS_KEY, JSON.stringify(s));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function apply(s: A11yState): void {
|
||||
const el = document.documentElement;
|
||||
el.style.fontSize = `${FONT_STEPS[s.fontStep] ?? 100}%`;
|
||||
el.classList.toggle('a11y-inverted', s.invert);
|
||||
el.classList.toggle('a11y-high-contrast', s.contrast);
|
||||
el.classList.toggle('a11y-highlight-links', s.links);
|
||||
el.classList.toggle('a11y-readable-font', s.readable);
|
||||
el.classList.toggle('a11y-pause-animations', s.pauseAnim);
|
||||
}
|
||||
|
||||
// Apply persisted prefs as early as this module runs, to minimize flash.
|
||||
apply(load());
|
||||
|
||||
function makeSwitch(labelText: string, checked: boolean, onChange: (v: boolean) => void): HTMLElement {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'a11y-row';
|
||||
const label = document.createElement('span');
|
||||
label.className = 'a11y-row-label';
|
||||
label.textContent = labelText;
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'a11y-switch';
|
||||
btn.setAttribute('role', 'switch');
|
||||
btn.setAttribute('aria-checked', String(checked));
|
||||
btn.setAttribute('aria-label', labelText);
|
||||
const setVisual = (v: boolean) => {
|
||||
btn.setAttribute('aria-checked', String(v));
|
||||
btn.classList.toggle('is-on', v);
|
||||
};
|
||||
setVisual(checked);
|
||||
btn.addEventListener('click', () => {
|
||||
const v = btn.getAttribute('aria-checked') !== 'true';
|
||||
setVisual(v);
|
||||
onChange(v);
|
||||
});
|
||||
row.append(label, btn);
|
||||
return row;
|
||||
}
|
||||
|
||||
function buildPanel(btn: HTMLElement, state: A11yState): HTMLElement {
|
||||
const d = btn.dataset;
|
||||
const panel = document.createElement('div');
|
||||
panel.className = 'a11y-panel';
|
||||
panel.id = 'mokogit-a11y-panel';
|
||||
panel.setAttribute('role', 'dialog');
|
||||
panel.setAttribute('aria-label', d.labelTitle || 'Accessibility');
|
||||
panel.hidden = true;
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.className = 'a11y-panel-title';
|
||||
title.textContent = d.labelTitle || 'Accessibility';
|
||||
panel.append(title);
|
||||
|
||||
// Text size group: - [nnn%] + reset
|
||||
const sizeRow = document.createElement('div');
|
||||
sizeRow.className = 'a11y-row a11y-size-row';
|
||||
const sizeLabel = document.createElement('span');
|
||||
sizeLabel.className = 'a11y-row-label';
|
||||
sizeLabel.textContent = d.labelTextsize || 'Text size';
|
||||
const controls = document.createElement('div');
|
||||
controls.className = 'a11y-size-controls';
|
||||
const dec = document.createElement('button');
|
||||
dec.type = 'button'; dec.className = 'a11y-size-btn'; dec.textContent = '−';
|
||||
dec.setAttribute('aria-label', d.labelDecrease || 'Decrease text size');
|
||||
const pct = document.createElement('span');
|
||||
pct.className = 'a11y-size-value';
|
||||
const inc = document.createElement('button');
|
||||
inc.type = 'button'; inc.className = 'a11y-size-btn'; inc.textContent = '+';
|
||||
inc.setAttribute('aria-label', d.labelIncrease || 'Increase text size');
|
||||
const rst = document.createElement('button');
|
||||
rst.type = 'button'; rst.className = 'a11y-size-btn a11y-size-reset';
|
||||
rst.textContent = d.labelReset || 'Reset';
|
||||
const renderPct = () => { pct.textContent = `${FONT_STEPS[state.fontStep]}%`; };
|
||||
renderPct();
|
||||
dec.addEventListener('click', () => { state.fontStep = Math.max(0, state.fontStep - 1); renderPct(); apply(state); save(state); });
|
||||
inc.addEventListener('click', () => { state.fontStep = Math.min(FONT_STEPS.length - 1, state.fontStep + 1); renderPct(); apply(state); save(state); });
|
||||
rst.addEventListener('click', () => { state.fontStep = DEFAULT_STEP; renderPct(); apply(state); save(state); });
|
||||
controls.append(dec, pct, inc, rst);
|
||||
sizeRow.append(sizeLabel, controls);
|
||||
panel.append(sizeRow);
|
||||
|
||||
const commit = (k: 'invert' | 'contrast' | 'links' | 'readable' | 'pauseAnim', v: boolean) => { state[k] = v; apply(state); save(state); };
|
||||
panel.append(makeSwitch(d.labelInvert || 'Invert colors', state.invert, (v) => commit('invert', v)));
|
||||
panel.append(makeSwitch(d.labelContrast || 'High contrast', state.contrast, (v) => commit('contrast', v)));
|
||||
panel.append(makeSwitch(d.labelLinks || 'Highlight links', state.links, (v) => commit('links', v)));
|
||||
panel.append(makeSwitch(d.labelReadable || 'Readable font', state.readable, (v) => commit('readable', v)));
|
||||
panel.append(makeSwitch(d.labelPause || 'Pause animations', state.pauseAnim, (v) => commit('pauseAnim', v)));
|
||||
|
||||
const resetAll = document.createElement('button');
|
||||
resetAll.type = 'button';
|
||||
resetAll.className = 'a11y-reset-all';
|
||||
resetAll.textContent = d.labelResetall || 'Reset all';
|
||||
resetAll.addEventListener('click', () => {
|
||||
Object.assign(state, DEFAULT_STATE);
|
||||
apply(state); save(state);
|
||||
// rebuild to reflect reset state
|
||||
panel.replaceWith(buildPanel(btn, state));
|
||||
(document.querySelector('#mokogit-a11y-panel') as HTMLElement).hidden = false;
|
||||
});
|
||||
panel.append(resetAll);
|
||||
return panel;
|
||||
}
|
||||
|
||||
export function initA11yMenu(): void {
|
||||
const btn = document.querySelector<HTMLButtonElement>('#mokogit-a11y-toggle');
|
||||
if (!btn) return;
|
||||
|
||||
const state = load();
|
||||
apply(state);
|
||||
let panel = buildPanel(btn, state);
|
||||
document.body.append(panel);
|
||||
|
||||
const positionPanel = () => {
|
||||
const r = btn.getBoundingClientRect();
|
||||
panel.style.top = `${Math.round(r.bottom + 6)}px`;
|
||||
panel.style.right = `${Math.round(window.innerWidth - r.right)}px`;
|
||||
};
|
||||
|
||||
const open = () => {
|
||||
panel = document.querySelector('#mokogit-a11y-panel') as HTMLElement;
|
||||
positionPanel();
|
||||
panel.hidden = false;
|
||||
btn.setAttribute('aria-expanded', 'true');
|
||||
document.addEventListener('click', onDocClick, true);
|
||||
document.addEventListener('keydown', onKey, true);
|
||||
};
|
||||
const close = () => {
|
||||
(document.querySelector('#mokogit-a11y-panel') as HTMLElement).hidden = true;
|
||||
btn.setAttribute('aria-expanded', 'false');
|
||||
document.removeEventListener('click', onDocClick, true);
|
||||
document.removeEventListener('keydown', onKey, true);
|
||||
};
|
||||
const onDocClick = (e: MouseEvent) => {
|
||||
const p = document.querySelector('#mokogit-a11y-panel') as HTMLElement;
|
||||
if (p.contains(e.target as Node) || btn.contains(e.target as Node)) return;
|
||||
close();
|
||||
};
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') { close(); btn.focus(); }
|
||||
};
|
||||
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const p = document.querySelector('#mokogit-a11y-panel') as HTMLElement;
|
||||
p.hidden ? open() : close();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// MokoGIT navbar light/dark toggle.
|
||||
// Signed-in users: persists to the account via the existing
|
||||
// /user/settings/appearance/theme endpoint (a hidden CSRF-protected form is
|
||||
// rendered in the navbar), so the choice syncs across devices; the page reloads
|
||||
// with the new theme. Anonymous users: best-effort data-theme flip + localStorage
|
||||
// (only the server-served theme CSS is loaded for anon, so effect is limited).
|
||||
|
||||
const LIGHT = 'mokogit-light';
|
||||
const DARK = 'mokogit-dark';
|
||||
const LS_KEY = 'mokogit-theme';
|
||||
|
||||
function currentIsDark(): boolean {
|
||||
const t = document.documentElement.getAttribute('data-theme') || '';
|
||||
if (t.includes('dark')) return true;
|
||||
if (t.includes('light')) return false;
|
||||
// auto / unknown -> follow the OS preference
|
||||
return window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
|
||||
}
|
||||
|
||||
export function initThemeToggle(): void {
|
||||
const btn = document.querySelector<HTMLButtonElement>('#mokogit-theme-toggle');
|
||||
if (!btn) return;
|
||||
|
||||
const sync = () => btn.classList.toggle('is-dark', currentIsDark());
|
||||
sync();
|
||||
|
||||
btn.addEventListener('click', () => {
|
||||
const target = currentIsDark() ? LIGHT : DARK;
|
||||
const form = document.querySelector<HTMLFormElement>('#mokogit-theme-form');
|
||||
if (form) {
|
||||
const input = form.querySelector<HTMLInputElement>('input[name="theme"]');
|
||||
if (input) input.value = target;
|
||||
form.submit(); // server persists + redirects back, re-rendering with the new theme
|
||||
return;
|
||||
}
|
||||
// anonymous best-effort
|
||||
document.documentElement.setAttribute('data-theme', target);
|
||||
try {
|
||||
localStorage.setItem(LS_KEY, target);
|
||||
} catch {}
|
||||
sync();
|
||||
});
|
||||
}
|
||||
@@ -39,6 +39,8 @@ import {initInstall} from './features/install.ts';
|
||||
import {initCompWebHookEditor} from './features/comp/WebHookEditor.ts';
|
||||
import {initRepoBranchButton} from './features/repo-branch.ts';
|
||||
import {initCommonOrganization} from './features/common-organization.ts';
|
||||
import {initThemeToggle} from './features/theme-toggle.ts';
|
||||
import {initA11yMenu} from './features/a11y-menu.ts';
|
||||
import {initRepoWikiForm} from './features/repo-wiki.ts';
|
||||
import {initRepository, initBranchSelectorTabs} from './features/repo-legacy.ts';
|
||||
import {initCopyContent} from './features/copycontent.ts';
|
||||
@@ -163,6 +165,9 @@ const initPerformanceTracer = callInitFunctions([
|
||||
initActionsPermissionsForm,
|
||||
|
||||
initDevtest,
|
||||
|
||||
initThemeToggle,
|
||||
initA11yMenu,
|
||||
]);
|
||||
|
||||
// it must be the last one, then the "querySelectorAll" only needs to be executed once for global init functions.
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# Third-Party & Vendored Assets
|
||||
|
||||
MokoGIT bundles a small number of third-party assets. Each is redistributed under
|
||||
its own license, with attribution retained here, in the repository
|
||||
[`THIRD-PARTY-NOTICES.md`](https://git.mokoconsulting.tech/MokoConsulting/MokoGIT/src/branch/main/THIRD-PARTY-NOTICES.md),
|
||||
and alongside the vendored files.
|
||||
|
||||
> **White-labeling note:** these attribution obligations survive rebranding. When
|
||||
> producing a white-labeled build, keep this page (or the `THIRD-PARTY-NOTICES.md`
|
||||
> file) accessible to end users. You may keep attribution out of the visible UI, but
|
||||
> it must remain reachable in the product's documentation or licenses listing.
|
||||
|
||||
## Font Awesome Free 7.1.0
|
||||
|
||||
- **Author / Copyright:** (c) 2025 Fonticons, Inc. -- <https://fontawesome.com>
|
||||
- **Upstream license:** <https://fontawesome.com/license/free>
|
||||
- **Vendored at:** `web_src/css/vendor/fontawesome/` (CSS + `.woff2` webfonts, unmodified)
|
||||
- **License text shipped at:** `web_src/css/vendor/fontawesome/LICENSE.txt`
|
||||
|
||||
Font Awesome Free is multi-licensed by file type:
|
||||
|
||||
| Component | License |
|
||||
| --- | --- |
|
||||
| Icons (glyph designs) | CC BY 4.0 -- attribution to Fonticons, Inc. required |
|
||||
| Fonts (`.woff2`) | SIL OFL 1.1 -- shipped with the OFL text; font files unmodified; reserved font name "Font Awesome" retained |
|
||||
| Code (CSS) | MIT -- copyright header retained in `all.min.css` |
|
||||
|
||||
**Do not** subset, regenerate, or rename the vendored font files -- modifying them
|
||||
triggers the SIL OFL Reserved Font Name restriction. Keep the license header comment
|
||||
in `all.min.css` intact.
|
||||
Reference in New Issue
Block a user