Compare commits
4 Commits
dev
..
development
| Author | SHA1 | Date | |
|---|---|---|---|
| fb0b7c53d7 | |||
| 314ad5794a | |||
| 0335354f0b | |||
| bfed3e16ae |
@@ -4,7 +4,7 @@
|
|||||||
<name>MokoGitea</name>
|
<name>MokoGitea</name>
|
||||||
<org>MokoConsulting</org>
|
<org>MokoConsulting</org>
|
||||||
<description>Moko fork of Gitea - adding project board REST API endpoints and custom enhancements</description>
|
<description>Moko fork of Gitea - adding project board REST API endpoints and custom enhancements</description>
|
||||||
<version>06.12.03</version>
|
<version>06.12.04</version>
|
||||||
<version-prefix>v1.26.1+MOKO</version-prefix>
|
<version-prefix>v1.26.1+MOKO</version-prefix>
|
||||||
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||||
</identity>
|
</identity>
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
DEFGROUP: gitea-api-mcp.Documentation
|
||||||
|
REPO: https://git.mokoconsulting.tech/MokoConsulting/gitea-api-mcp
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Renamed** package from `@mokoconsulting/gitea-api-mcp` to `@mokoconsulting/mokogitea-api-mcp` to distinguish Moko's forked Gitea MCP from upstream
|
||||||
|
- **Renamed** McpServer name and bin entry to `mokogitea-api-mcp`
|
||||||
|
|
||||||
|
|
||||||
|
## [0.0] - 2026-05-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
#### User / Auth (3 tools)
|
||||||
|
- `gitea_me` -- Get the authenticated user info
|
||||||
|
- `gitea_user_orgs` -- List organizations the authenticated user belongs to
|
||||||
|
- `gitea_user_repos` -- List repositories owned by the authenticated user
|
||||||
|
|
||||||
|
#### Repositories (8 tools)
|
||||||
|
- `gitea_repo_get` -- Get repository details
|
||||||
|
- `gitea_repo_create` -- Create a new repository
|
||||||
|
- `gitea_repo_delete` -- Delete a repository
|
||||||
|
- `gitea_repo_edit` -- Edit repository settings
|
||||||
|
- `gitea_repo_fork` -- Fork a repository
|
||||||
|
- `gitea_repo_search` -- Search repositories
|
||||||
|
- `gitea_org_repos` -- List repositories in an organization
|
||||||
|
- `gitea_list_connections` -- List configured Gitea connections
|
||||||
|
|
||||||
|
#### File Contents (5 tools)
|
||||||
|
- `gitea_file_get` -- Get file contents from a repository
|
||||||
|
- `gitea_dir_get` -- Get directory contents (file listing) from a repository
|
||||||
|
- `gitea_file_create_or_update` -- Create or update a file in a repository
|
||||||
|
- `gitea_file_delete` -- Delete a file from a repository
|
||||||
|
- `gitea_tree_get` -- Get the git tree for a repository (recursive file listing)
|
||||||
|
|
||||||
|
#### Branches (4 tools)
|
||||||
|
- `gitea_branches_list` -- List branches in a repository
|
||||||
|
- `gitea_branch_get` -- Get a specific branch
|
||||||
|
- `gitea_branch_create` -- Create a new branch
|
||||||
|
- `gitea_branch_delete` -- Delete a branch
|
||||||
|
|
||||||
|
#### Commits (2 tools)
|
||||||
|
- `gitea_commits_list` -- List commits in a repository
|
||||||
|
- `gitea_commit_get` -- Get a specific commit
|
||||||
|
|
||||||
|
#### Issues (7 tools)
|
||||||
|
- `gitea_issues_list` -- List issues in a repository
|
||||||
|
- `gitea_issue_get` -- Get a single issue by number
|
||||||
|
- `gitea_issue_create` -- Create a new issue
|
||||||
|
- `gitea_issue_update` -- Update an issue
|
||||||
|
- `gitea_issue_comments_list` -- List comments on an issue
|
||||||
|
- `gitea_issue_comment_create` -- Add a comment to an issue
|
||||||
|
- `gitea_issue_search` -- Search issues across all repositories
|
||||||
|
|
||||||
|
#### Labels (2 tools)
|
||||||
|
- `gitea_labels_list` -- List labels in a repository
|
||||||
|
- `gitea_label_create` -- Create a label
|
||||||
|
|
||||||
|
#### Milestones (2 tools)
|
||||||
|
- `gitea_milestones_list` -- List milestones in a repository
|
||||||
|
- `gitea_milestone_create` -- Create a milestone
|
||||||
|
|
||||||
|
#### Pull Requests (6 tools)
|
||||||
|
- `gitea_pulls_list` -- List pull requests
|
||||||
|
- `gitea_pull_get` -- Get a single pull request
|
||||||
|
- `gitea_pull_create` -- Create a pull request
|
||||||
|
- `gitea_pull_merge` -- Merge a pull request
|
||||||
|
- `gitea_pull_files` -- List files changed in a pull request
|
||||||
|
- `gitea_pull_review_create` -- Create a pull request review
|
||||||
|
|
||||||
|
#### Releases (5 tools)
|
||||||
|
- `gitea_releases_list` -- List releases
|
||||||
|
- `gitea_release_get` -- Get a single release by ID
|
||||||
|
- `gitea_release_latest` -- Get the latest release
|
||||||
|
- `gitea_release_create` -- Create a new release
|
||||||
|
- `gitea_release_delete` -- Delete a release
|
||||||
|
|
||||||
|
#### Tags (3 tools)
|
||||||
|
- `gitea_tags_list` -- List tags
|
||||||
|
- `gitea_tag_create` -- Create a tag
|
||||||
|
- `gitea_tag_delete` -- Delete a tag
|
||||||
|
|
||||||
|
#### Actions (2 tools)
|
||||||
|
- `gitea_actions_runs_list` -- List workflow runs for a repository
|
||||||
|
- `gitea_actions_run_get` -- Get a specific workflow run
|
||||||
|
|
||||||
|
#### Organizations (3 tools)
|
||||||
|
- `gitea_org_get` -- Get organization details
|
||||||
|
- `gitea_org_teams_list` -- List teams in an organization
|
||||||
|
- `gitea_org_members_list` -- List members of an organization
|
||||||
|
|
||||||
|
#### Users (2 tools)
|
||||||
|
- `gitea_user_get` -- Get a user profile
|
||||||
|
- `gitea_users_search` -- Search users
|
||||||
|
|
||||||
|
#### Webhooks (2 tools)
|
||||||
|
- `gitea_webhooks_list` -- List webhooks for a repository
|
||||||
|
- `gitea_webhook_create` -- Create a webhook
|
||||||
|
|
||||||
|
#### Wiki (2 tools)
|
||||||
|
- `gitea_wiki_pages_list` -- List wiki pages
|
||||||
|
- `gitea_wiki_page_get` -- Get a wiki page
|
||||||
|
|
||||||
|
#### Notifications (2 tools)
|
||||||
|
- `gitea_notifications_list` -- List notifications for the authenticated user
|
||||||
|
- `gitea_notifications_read` -- Mark all notifications as read
|
||||||
|
|
||||||
|
#### Generic (2 tools)
|
||||||
|
- `gitea_api_request` -- Make a raw API request to any Gitea v1 endpoint
|
||||||
|
- `gitea_list_connections` -- List configured Gitea connections
|
||||||
|
|
||||||
|
### Infrastructure
|
||||||
|
- Multi-connection config support via `~/.gitea-api-mcp.json`
|
||||||
|
- Token-based authentication (Gitea native `Authorization: token` header)
|
||||||
|
- Built on `node:https` / `node:http` (zero HTTP dependencies)
|
||||||
|
- MCP SDK v1.12.x with stdio transport
|
||||||
|
|
||||||
|
[0.0.1]: https://git.mokoconsulting.tech/MokoConsulting/gitea-api-mcp/releases/tag/v0.0.1
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci --production=false
|
||||||
|
|
||||||
|
COPY tsconfig.json ./
|
||||||
|
COPY src/ ./src/
|
||||||
|
RUN npx tsc && npm prune --production
|
||||||
|
|
||||||
|
EXPOSE 3100
|
||||||
|
|
||||||
|
ENV PORT=3100
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
# SSE mode by default for Docker deployments
|
||||||
|
CMD ["node", "dist/sse.js"]
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# MokoGitea MCP Server
|
||||||
|
|
||||||
|
A comprehensive [Model Context Protocol](https://modelcontextprotocol.io) server for [Gitea](https://gitea.com) and [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea). 120+ tools for repos, issues, PRs, projects, releases, custom fields, statuses, priorities, and manifests.
|
||||||
|
|
||||||
|
Works with any Gitea instance. MokoGitea-specific features degrade gracefully on vanilla Gitea.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### npx (no install)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GITEA_URL=https://gitea.example.com GITEA_TOKEN=your_token npx @mokoconsulting/mokogitea-mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
### Claude Code
|
||||||
|
|
||||||
|
Add to `.claude.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"mokogitea": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["@mokoconsulting/mokogitea-mcp"],
|
||||||
|
"env": {
|
||||||
|
"GITEA_URL": "https://gitea.example.com",
|
||||||
|
"GITEA_TOKEN": "your_token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker (SSE mode)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -p 3100:3100 \
|
||||||
|
-e GITEA_URL=https://gitea.example.com \
|
||||||
|
-e GITEA_TOKEN=your_token \
|
||||||
|
mokoconsulting/mokogitea-mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
Connect MCP client to `http://localhost:3100/sse`.
|
||||||
|
|
||||||
|
### Multi-instance config
|
||||||
|
|
||||||
|
Create `~/.mcp_mokogitea.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"defaultConnection": "production",
|
||||||
|
"connections": {
|
||||||
|
"production": { "baseUrl": "https://gitea.example.com", "token": "your_token" },
|
||||||
|
"dev": { "baseUrl": "https://dev.gitea.example.com", "token": "dev_token" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
| Method | Use Case |
|
||||||
|
|--------|----------|
|
||||||
|
| `GITEA_URL` + `GITEA_TOKEN` env vars | Single instance, quick setup |
|
||||||
|
| `~/.mcp_mokogitea.json` config file | Multiple instances |
|
||||||
|
| `GITEA_API_MCP_CONFIG` env var | Custom config path |
|
||||||
|
| `GITEA_INSECURE=true` | Skip TLS verification |
|
||||||
|
|
||||||
|
## Tools (120+)
|
||||||
|
|
||||||
|
### Repositories
|
||||||
|
`gitea_repo_create` `gitea_repo_get` `gitea_repo_edit` `gitea_repo_delete` `gitea_repo_search` `gitea_repo_fork` `gitea_repo_generate` `gitea_repo_languages` `gitea_repo_contributors` `gitea_repo_topics` `gitea_repo_topics_set`
|
||||||
|
|
||||||
|
### Issues
|
||||||
|
`gitea_issue_create` (dedup by title) `gitea_issue_get` `gitea_issue_update` `gitea_issues_list` `gitea_issue_search` `gitea_issue_comment_create` `gitea_issue_comments_list` `gitea_issue_labels_set` `gitea_issue_bulk_set_status`
|
||||||
|
|
||||||
|
### Pull Requests
|
||||||
|
`gitea_pull_create` `gitea_pull_get` `gitea_pulls_list` `gitea_pull_merge` `gitea_pull_files` `gitea_pull_review_create`
|
||||||
|
|
||||||
|
### Branches and Tags
|
||||||
|
`gitea_branches_list` `gitea_branch_create` `gitea_branch_delete` `gitea_branch_get` `gitea_tags_list` `gitea_tag_create` `gitea_tag_delete`
|
||||||
|
|
||||||
|
### Releases
|
||||||
|
`gitea_releases_list` `gitea_release_create` `gitea_release_get` `gitea_release_latest` `gitea_release_delete` `gitea_release_asset_upload` `gitea_release_asset_delete`
|
||||||
|
|
||||||
|
### Files and Trees
|
||||||
|
`gitea_file_get` `gitea_file_create_or_update` `gitea_file_delete` `gitea_dir_get` `gitea_tree_get` `gitea_bulk_file_push`
|
||||||
|
|
||||||
|
### Projects
|
||||||
|
`gitea_project_list` `gitea_project_create` `gitea_project_get` `gitea_project_update` `gitea_project_delete` `gitea_project_overview` `gitea_project_columns_list` `gitea_project_column_create` `gitea_project_column_delete` `gitea_project_cards_list` `gitea_project_card_add` `gitea_project_card_move` `gitea_project_card_remove`
|
||||||
|
|
||||||
|
### Organizations
|
||||||
|
`gitea_org_get` `gitea_org_repos` `gitea_org_members_list` `gitea_org_teams_list` `gitea_org_labels_list` `gitea_org_label_create`
|
||||||
|
|
||||||
|
### Wiki
|
||||||
|
`gitea_wiki_pages_list` `gitea_wiki_page_get`
|
||||||
|
|
||||||
|
### MokoGitea Extensions
|
||||||
|
`gitea_manifest_get` `gitea_manifest_update` `gitea_org_custom_fields_list` `gitea_org_custom_field_create` `gitea_org_custom_field_delete` `gitea_issue_custom_fields_get` `gitea_issue_custom_fields_set` `gitea_org_issue_statuses_list` `gitea_issue_set_status` `gitea_org_issue_priorities_list` `gitea_issue_set_priority`
|
||||||
|
|
||||||
|
### Admin and Other
|
||||||
|
`gitea_me` `gitea_users_search` `gitea_user_get` `gitea_notifications_list` `gitea_notifications_read` `gitea_commits_list` `gitea_commit_get` `gitea_compare` `gitea_webhooks_list` `gitea_webhook_create` `gitea_admin_users_list` `gitea_admin_orgs_list` `gitea_admin_cron_list` `gitea_admin_cron_run` `gitea_list_connections`
|
||||||
|
|
||||||
|
## SSE Server
|
||||||
|
|
||||||
|
For hosted deployments:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET / Server info
|
||||||
|
GET /sse SSE connection endpoint
|
||||||
|
POST /message Tool call messages
|
||||||
|
GET /health Health check
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
GPL-3.0-or-later - [Moko Consulting](https://mokoconsulting.tech)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"defaultConnection": "moko",
|
||||||
|
"connections": {
|
||||||
|
"moko": {
|
||||||
|
"baseUrl": "https://git.mokoconsulting.tech",
|
||||||
|
"token": "your-gitea-access-token"
|
||||||
|
},
|
||||||
|
"github-mirror": {
|
||||||
|
"baseUrl": "https://gitea.example.com",
|
||||||
|
"token": "your-other-token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+1198
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"name": "@mokoconsulting/mokogitea-mcp",
|
||||||
|
"version": "1.1.0",
|
||||||
|
"description": "MCP server for Gitea and MokoGitea - 120+ tools for repos, issues, PRs, projects, releases, custom fields, statuses, priorities, and manifests",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"bin": {
|
||||||
|
"mokogitea-mcp": "dist/index.js",
|
||||||
|
"mokogitea-mcp-sse": "dist/sse.js"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"dev": "tsc --watch",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"start:sse": "node dist/sse.js",
|
||||||
|
"setup": "node scripts/setup.mjs",
|
||||||
|
"clean": "rm -rf dist/"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"mcp",
|
||||||
|
"gitea",
|
||||||
|
"mokogitea",
|
||||||
|
"model-context-protocol",
|
||||||
|
"claude",
|
||||||
|
"ai",
|
||||||
|
"git",
|
||||||
|
"self-hosted",
|
||||||
|
"api",
|
||||||
|
"devops"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||||
|
"zod": "^3.24.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.15.3",
|
||||||
|
"typescript": "^5.8.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"license": "GPL-3.0-or-later",
|
||||||
|
"author": "Moko Consulting <hello@mokoconsulting.tech>",
|
||||||
|
"homepage": "https://git.mokoconsulting.tech/MokoConsulting/mcp_mokogitea_api",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.mokoconsulting.tech/MokoConsulting/mcp_mokogitea_api.git"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/",
|
||||||
|
"config.example.json",
|
||||||
|
"README.md",
|
||||||
|
"LICENSE"
|
||||||
|
],
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# mcp_mokogitea_api PowerShell Profile
|
||||||
|
# Source this with: . ./profile.ps1
|
||||||
|
|
||||||
|
$env:MCP_ROOT = $PSScriptRoot
|
||||||
|
$env:TEMP = 'A:\temp'
|
||||||
|
$env:TMP = 'A:\temp'
|
||||||
|
|
||||||
|
function mcp { Set-Location $PSScriptRoot }
|
||||||
|
function mcp-src { Set-Location (Join-Path $PSScriptRoot 'src') }
|
||||||
|
function mcp-build { Set-Location $PSScriptRoot; npm run build }
|
||||||
|
function mcp-dev { Set-Location $PSScriptRoot; npm run dev }
|
||||||
|
|
||||||
|
Write-Host "mcp_mokogitea_api profile loaded" -ForegroundColor Cyan
|
||||||
|
Write-Host " Commands: mcp-build, mcp-dev" -ForegroundColor DarkGray
|
||||||
|
Write-Host " Navigate: mcp, mcp-src" -ForegroundColor DarkGray
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
* BRIEF: Interactive setup — prompts for Gitea connection details
|
||||||
|
*/
|
||||||
|
import { createInterface } from 'node:readline/promises';
|
||||||
|
import { readFile, writeFile } from 'node:fs/promises';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
import { homedir } from 'node:os';
|
||||||
|
|
||||||
|
const CONFIG_PATH = resolve(homedir(), '.gitea-api-mcp.json');
|
||||||
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||||
|
|
||||||
|
async function prompt(q, d) { const a = await rl.question(`${q}${d ? ` [${d}]` : ''}: `); return a.trim() || d || ''; }
|
||||||
|
async function promptRequired(q) { let a = ''; while (!a) { a = (await rl.question(`${q}: `)).trim(); if (!a) console.log(' Required.'); } return a; }
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('\n=== gitea-api-mcp Setup ===\n');
|
||||||
|
let existing = null;
|
||||||
|
try { existing = JSON.parse(await readFile(CONFIG_PATH, 'utf-8')); console.log(`Existing: ${Object.keys(existing.connections).join(', ')}\n`); } catch {}
|
||||||
|
|
||||||
|
const name = await prompt('Connection name', 'moko');
|
||||||
|
const baseUrl = await promptRequired('Gitea URL (e.g. https://git.mokoconsulting.tech)');
|
||||||
|
const token = await promptRequired('Access token (Settings > Applications > Generate Token)');
|
||||||
|
const insecure = (await prompt('Skip TLS verification? (y/N)', 'N')).toLowerCase() === 'y';
|
||||||
|
|
||||||
|
const conn = { baseUrl: baseUrl.replace(/\/+$/, ''), token };
|
||||||
|
if (insecure) conn.insecure = true;
|
||||||
|
|
||||||
|
const config = existing ?? { defaultConnection: name, connections: {} };
|
||||||
|
config.connections[name] = conn;
|
||||||
|
if (!existing) config.defaultConnection = name;
|
||||||
|
else if ((await prompt(`Set "${name}" as default? (y/N)`, 'N')).toLowerCase() === 'y') config.defaultConnection = name;
|
||||||
|
|
||||||
|
await writeFile(CONFIG_PATH, JSON.stringify(config, null, '\t') + '\n', 'utf-8');
|
||||||
|
console.log(`\nConfig written to ${CONFIG_PATH}\n`);
|
||||||
|
rl.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(e => { console.error(e.message); rl.close(); process.exit(1); });
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
*
|
||||||
|
* This file is part of a Moko Consulting project.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* FILE INFORMATION
|
||||||
|
* DEFGROUP: gitea-api-mcp.Client
|
||||||
|
* INGROUP: gitea-api-mcp
|
||||||
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/gitea-api-mcp
|
||||||
|
* PATH: /src/client.ts
|
||||||
|
* VERSION: 01.00.00
|
||||||
|
* BRIEF: HTTP client for Gitea REST API v1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as https from 'node:https';
|
||||||
|
import * as http from 'node:http';
|
||||||
|
import type { GiteaConnection, ApiResponse } from './types.js';
|
||||||
|
|
||||||
|
const API_PREFIX = '/api/v1';
|
||||||
|
const TIMEOUT_MS = 30_000;
|
||||||
|
|
||||||
|
export class GiteaClient {
|
||||||
|
private readonly base_url: string;
|
||||||
|
private readonly headers: Record<string, string>;
|
||||||
|
private readonly insecure: boolean;
|
||||||
|
|
||||||
|
constructor(conn: GiteaConnection) {
|
||||||
|
this.base_url = conn.baseUrl.replace(/\/+$/, '') + API_PREFIX;
|
||||||
|
this.headers = {
|
||||||
|
'Authorization': `token ${conn.token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
};
|
||||||
|
this.insecure = conn.insecure ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(endpoint: string, params?: Record<string, string>): Promise<ApiResponse> {
|
||||||
|
return this.request(this.buildUrl(endpoint, params), 'GET');
|
||||||
|
}
|
||||||
|
|
||||||
|
async post(endpoint: string, body?: unknown): Promise<ApiResponse> {
|
||||||
|
return this.request(this.buildUrl(endpoint), 'POST', body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async patch(endpoint: string, body: unknown): Promise<ApiResponse> {
|
||||||
|
return this.request(this.buildUrl(endpoint), 'PATCH', body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async put(endpoint: string, body: unknown): Promise<ApiResponse> {
|
||||||
|
return this.request(this.buildUrl(endpoint), 'PUT', body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(endpoint: string): Promise<ApiResponse> {
|
||||||
|
return this.request(this.buildUrl(endpoint), 'DELETE');
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildUrl(endpoint: string, params?: Record<string, string>): string {
|
||||||
|
const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
||||||
|
const url = new URL(`${this.base_url}${path}`);
|
||||||
|
if (params) {
|
||||||
|
for (const [key, value] of Object.entries(params)) {
|
||||||
|
url.searchParams.set(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private request(url: string, method: string, body?: unknown): Promise<ApiResponse> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
const is_https = parsed.protocol === 'https:';
|
||||||
|
const transport = is_https ? https : http;
|
||||||
|
|
||||||
|
const options: https.RequestOptions = {
|
||||||
|
hostname: parsed.hostname,
|
||||||
|
port: parsed.port || (is_https ? 443 : 80),
|
||||||
|
path: parsed.pathname + parsed.search,
|
||||||
|
method,
|
||||||
|
headers: { ...this.headers },
|
||||||
|
timeout: TIMEOUT_MS,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.insecure && is_https) {
|
||||||
|
options.rejectUnauthorized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = body !== undefined ? JSON.stringify(body) : undefined;
|
||||||
|
if (payload) {
|
||||||
|
(options.headers as Record<string, string>)['Content-Length'] = Buffer.byteLength(payload).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const req = transport.request(options, (res) => {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
res.on('data', (chunk: Buffer) => chunks.push(chunk));
|
||||||
|
res.on('end', () => {
|
||||||
|
const raw = Buffer.concat(chunks).toString('utf-8');
|
||||||
|
let data: unknown;
|
||||||
|
try {
|
||||||
|
data = JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
data = raw;
|
||||||
|
}
|
||||||
|
resolve({ status: res.statusCode ?? 0, data });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', (err) => reject(err));
|
||||||
|
req.on('timeout', () => {
|
||||||
|
req.destroy();
|
||||||
|
reject(new Error('Request timed out'));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (payload) {
|
||||||
|
req.write(payload);
|
||||||
|
}
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
import { homedir } from 'node:os';
|
||||||
|
import type { GiteaConfig, GiteaConnection } from './types.js';
|
||||||
|
|
||||||
|
const CONFIG_FILENAME = '.mcp_mokogitea.json';
|
||||||
|
|
||||||
|
export async function loadConfig(): Promise<GiteaConfig> {
|
||||||
|
// Priority 1: Environment variables (zero-config single instance)
|
||||||
|
if (process.env.GITEA_URL && process.env.GITEA_TOKEN) {
|
||||||
|
const conn: GiteaConnection = {
|
||||||
|
baseUrl: process.env.GITEA_URL,
|
||||||
|
token: process.env.GITEA_TOKEN,
|
||||||
|
insecure: process.env.GITEA_INSECURE === 'true',
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
connections: { default: conn },
|
||||||
|
defaultConnection: 'default',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 2: Config file
|
||||||
|
const config_path = process.env.GITEA_API_MCP_CONFIG
|
||||||
|
? resolve(process.env.GITEA_API_MCP_CONFIG)
|
||||||
|
: resolve(homedir(), CONFIG_FILENAME);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const raw = await readFile(config_path, 'utf-8');
|
||||||
|
const parsed = JSON.parse(raw) as Partial<GiteaConfig>;
|
||||||
|
|
||||||
|
if (!parsed.connections || Object.keys(parsed.connections).length === 0) {
|
||||||
|
throw new Error('No connections defined in config');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
connections: parsed.connections,
|
||||||
|
defaultConnection: parsed.defaultConnection ?? Object.keys(parsed.connections)[0],
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
throw new Error(
|
||||||
|
`Failed to load config from ${config_path}: ${message}\n` +
|
||||||
|
`Option 1: Set GITEA_URL and GITEA_TOKEN environment variables\n` +
|
||||||
|
`Option 2: Create ${config_path} - see config.example.json for format`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getConnection(config: GiteaConfig, name?: string): GiteaConnection {
|
||||||
|
const key = name ?? config.defaultConnection;
|
||||||
|
const conn = config.connections[key];
|
||||||
|
if (!conn) {
|
||||||
|
throw new Error(
|
||||||
|
`Connection "${key}" not found. Available: ${Object.keys(config.connections).join(', ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
|||||||
|
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// Creates a configured MCP server instance for use by both stdio and SSE transports.
|
||||||
|
|
||||||
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||||
|
import type { GiteaConfig } from './types.js';
|
||||||
|
|
||||||
|
// Import index.ts to register all tools on its exported `server` singleton,
|
||||||
|
// then re-export a factory that initializes config and returns the server.
|
||||||
|
import { server, initConfig } from './index.js';
|
||||||
|
|
||||||
|
export function createMcpServer(cfg: GiteaConfig): McpServer {
|
||||||
|
initConfig(cfg);
|
||||||
|
return server;
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// SSE transport entry point for MokoGitea MCP server.
|
||||||
|
// Run with: node dist/sse.js
|
||||||
|
// Or: GITEA_URL=https://gitea.example.com GITEA_TOKEN=xxx node dist/sse.js
|
||||||
|
//
|
||||||
|
// Listens on PORT (default 3100) and serves SSE at /sse with POST at /message.
|
||||||
|
|
||||||
|
import { createServer } from 'node:http';
|
||||||
|
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
||||||
|
import { createMcpServer } from './server.js';
|
||||||
|
import { loadConfig } from './config.js';
|
||||||
|
|
||||||
|
const PORT = parseInt(process.env.PORT ?? '3100', 10);
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
const config = await loadConfig();
|
||||||
|
const transports = new Map<string, SSEServerTransport>();
|
||||||
|
|
||||||
|
const httpServer = createServer(async (req, res) => {
|
||||||
|
// CORS headers for browser clients
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||||
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||||
|
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
res.writeHead(204);
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
if (req.url === '/health') {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ status: 'ok', tools: 120 }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSE endpoint - client connects here
|
||||||
|
if (req.url === '/sse' && req.method === 'GET') {
|
||||||
|
const transport = new SSEServerTransport('/message', res);
|
||||||
|
const sessionId = transport.sessionId;
|
||||||
|
transports.set(sessionId, transport);
|
||||||
|
|
||||||
|
const server = createMcpServer(config);
|
||||||
|
await server.connect(transport);
|
||||||
|
|
||||||
|
req.on('close', () => {
|
||||||
|
transports.delete(sessionId);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message endpoint - client sends tool calls here
|
||||||
|
if (req.url?.startsWith('/message') && req.method === 'POST') {
|
||||||
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||||
|
const sessionId = url.searchParams.get('sessionId');
|
||||||
|
if (!sessionId || !transports.has(sessionId)) {
|
||||||
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: 'Invalid or missing sessionId' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const transport = transports.get(sessionId)!;
|
||||||
|
await transport.handlePostMessage(req, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Root - info page
|
||||||
|
if (req.url === '/' || req.url === '') {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
name: '@mokoconsulting/mokogitea-mcp',
|
||||||
|
version: '1.1.0',
|
||||||
|
description: 'MCP server for Gitea and MokoGitea - 120+ tools',
|
||||||
|
endpoints: {
|
||||||
|
sse: '/sse',
|
||||||
|
message: '/message',
|
||||||
|
health: '/health',
|
||||||
|
},
|
||||||
|
docs: 'https://git.mokoconsulting.tech/MokoConsulting/mcp_mokogitea_api',
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(404);
|
||||||
|
res.end('Not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
httpServer.listen(PORT, () => {
|
||||||
|
process.stderr.write(`MokoGitea MCP SSE server listening on port ${PORT}\n`);
|
||||||
|
process.stderr.write(` SSE: http://localhost:${PORT}/sse\n`);
|
||||||
|
process.stderr.write(` Health: http://localhost:${PORT}/health\n`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
process.stderr.write(`Fatal: ${err}\n`);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
*
|
||||||
|
* This file is part of a Moko Consulting project.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* FILE INFORMATION
|
||||||
|
* DEFGROUP: gitea-api-mcp.Types
|
||||||
|
* INGROUP: gitea-api-mcp
|
||||||
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/gitea-api-mcp
|
||||||
|
* PATH: /src/types.ts
|
||||||
|
* VERSION: 01.00.00
|
||||||
|
* BRIEF: TypeScript type definitions for Gitea API MCP server
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface GiteaConnection {
|
||||||
|
baseUrl: string;
|
||||||
|
token: string;
|
||||||
|
/** Skip TLS certificate verification (self-signed certs) */
|
||||||
|
insecure?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GitHubBackupConfig {
|
||||||
|
token: string;
|
||||||
|
org: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GiteaConfig {
|
||||||
|
connections: Record<string, GiteaConnection>;
|
||||||
|
defaultConnection: string;
|
||||||
|
github?: GitHubBackupConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiResponse {
|
||||||
|
status: number;
|
||||||
|
data: unknown;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "Node16",
|
||||||
|
"moduleResolution": "Node16",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: moko-platform.Release
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
||||||
# PATH: /.mokogitea/workflows/auto-bump.yml
|
|
||||||
# VERSION: 09.02.00
|
|
||||||
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
|
||||||
|
|
||||||
name: "Universal: Auto Version Bump"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
- rc
|
|
||||||
- 'feature/**'
|
|
||||||
- 'patch/**'
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
bump:
|
|
||||||
name: Version Bump
|
|
||||||
runs-on: release
|
|
||||||
if: >-
|
|
||||||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
|
||||||
!contains(github.event.head_commit.message, '[skip bump]') &&
|
|
||||||
!startsWith(github.event.head_commit.message, 'Merge pull request')
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
fetch-depth: 1
|
|
||||||
|
|
||||||
- name: Setup moko-platform tools
|
|
||||||
run: |
|
|
||||||
if ! command -v composer &> /dev/null; then
|
|
||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
if [ -d "/opt/moko-platform/cli" ]; then
|
|
||||||
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
|
|
||||||
else
|
|
||||||
git clone --depth 1 --branch main --quiet \
|
|
||||||
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
|
|
||||||
/tmp/moko-platform-api
|
|
||||||
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
|
||||||
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Bump version
|
|
||||||
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"
|
|
||||||
@@ -1,341 +1 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
placeholder
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: moko-platform.Release
|
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
|
||||||
# PATH: /templates/workflows/universal/auto-release.yml.template
|
|
||||||
# VERSION: 05.00.00
|
|
||||||
# BRIEF: Universal build & release � detects platform from manifest.xml
|
|
||||||
#
|
|
||||||
# +========================================================================+
|
|
||||||
# | UNIVERSAL BUILD & RELEASE PIPELINE |
|
|
||||||
# +========================================================================+
|
|
||||||
# | |
|
|
||||||
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
|
||||||
# | |
|
|
||||||
# | Platform-specific: |
|
|
||||||
# | joomla: XML manifest, type-prefixed packages |
|
|
||||||
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
|
||||||
# | generic: README-only, no update stream |
|
|
||||||
# | |
|
|
||||||
# +========================================================================+
|
|
||||||
|
|
||||||
name: "Universal: Build & Release"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, closed]
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
action:
|
|
||||||
description: 'Action to perform'
|
|
||||||
required: false
|
|
||||||
type: choice
|
|
||||||
default: release
|
|
||||||
options:
|
|
||||||
- release
|
|
||||||
- promote-rc
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
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 }}
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────
|
|
||||||
promote-rc:
|
|
||||||
name: Promote to RC
|
|
||||||
runs-on: release
|
|
||||||
if: >-
|
|
||||||
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
|
|
||||||
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
fetch-depth: 1
|
|
||||||
|
|
||||||
- name: Setup moko-platform tools
|
|
||||||
env:
|
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
|
||||||
run: |
|
|
||||||
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
|
||||||
echo Using pre-installed /opt/moko-platform
|
|
||||||
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
|
||||||
else
|
|
||||||
echo Falling back to fresh clone
|
|
||||||
if ! command -v composer > /dev/null 2>&1; then
|
|
||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
|
||||||
fi
|
|
||||||
rm -rf /tmp/moko-platform-api
|
|
||||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
|
||||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
|
||||||
cd /tmp/moko-platform-api
|
|
||||||
composer install --no-dev --no-interaction --quiet
|
|
||||||
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Rename branch to rc
|
|
||||||
run: |
|
|
||||||
php ${MOKO_CLI}/branch_rename.php \
|
|
||||||
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
|
||||||
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
|
||||||
--pr "${{ github.event.pull_request.number }}"
|
|
||||||
|
|
||||||
- name: Checkout rc and configure git
|
|
||||||
run: |
|
|
||||||
git fetch origin rc
|
|
||||||
git checkout rc
|
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
|
||||||
git config --local user.name "gitea-actions[bot]"
|
|
||||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
|
||||||
|
|
||||||
- name: Publish RC release
|
|
||||||
run: |
|
|
||||||
php ${MOKO_CLI}/release_publish.php \
|
|
||||||
--path . --stability rc --bump minor --branch rc \
|
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
|
||||||
|
|
||||||
- name: Summary
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
|
||||||
release:
|
|
||||||
name: Build & Release Pipeline
|
|
||||||
runs-on: release
|
|
||||||
if: >-
|
|
||||||
github.event.pull_request.merged == true ||
|
|
||||||
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Configure git for bot pushes
|
|
||||||
run: |
|
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
|
||||||
git config --local user.name "gitea-actions[bot]"
|
|
||||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
|
||||||
|
|
||||||
- name: Check for merge conflict markers
|
|
||||||
run: |
|
|
||||||
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
|
||||||
if [ -n "$CONFLICTS" ]; then
|
|
||||||
echo "::error::Merge conflict markers found — aborting release"
|
|
||||||
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "No conflict markers found"
|
|
||||||
|
|
||||||
- name: Setup moko-platform tools
|
|
||||||
env:
|
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
|
||||||
run: |
|
|
||||||
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
|
||||||
echo Using pre-installed /opt/moko-platform
|
|
||||||
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
|
||||||
else
|
|
||||||
echo Falling back to fresh clone
|
|
||||||
if ! command -v composer > /dev/null 2>&1; then
|
|
||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
|
||||||
fi
|
|
||||||
rm -rf /tmp/moko-platform-api
|
|
||||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
|
||||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
|
||||||
cd /tmp/moko-platform-api
|
|
||||||
composer install --no-dev --no-interaction --quiet
|
|
||||||
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: "Determine version bump level"
|
|
||||||
id: bump
|
|
||||||
run: |
|
|
||||||
# Fix/patch branches: version was already bumped by pre-release, just strip suffix
|
|
||||||
# Feature/dev branches: bump minor for the new stable release
|
|
||||||
HEAD_REF="${{ github.event.pull_request.head.ref || 'dev' }}"
|
|
||||||
case "$HEAD_REF" in
|
|
||||||
fix/*|patch/*|hotfix/*|bugfix/*) BUMP="none" ;;
|
|
||||||
*) BUMP="minor" ;;
|
|
||||||
esac
|
|
||||||
echo "level=${BUMP}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "Bump level: ${BUMP} (from branch: ${HEAD_REF})"
|
|
||||||
|
|
||||||
- name: "Publish stable release"
|
|
||||||
run: |
|
|
||||||
BUMP_FLAG=""
|
|
||||||
if [ "${{ steps.bump.outputs.level }}" != "none" ]; then
|
|
||||||
BUMP_FLAG="--bump ${{ steps.bump.outputs.level }}"
|
|
||||||
fi
|
|
||||||
php ${MOKO_CLI}/release_publish.php \
|
|
||||||
--path . --stability stable ${BUMP_FLAG} --branch main \
|
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
|
||||||
|
|
||||||
- name: Update release notes from CHANGELOG.md
|
|
||||||
run: |
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
|
|
||||||
# Extract [Unreleased] section from changelog
|
|
||||||
if [ -f "CHANGELOG.md" ]; then
|
|
||||||
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
|
||||||
[ -z "$NOTES" ] && NOTES="Stable release"
|
|
||||||
else
|
|
||||||
NOTES="Stable release"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Update release body via API
|
|
||||||
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
|
||||||
"${API_BASE}/releases/tags/stable" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
|
||||||
|
|
||||||
if [ -n "$RELEASE_ID" ]; then
|
|
||||||
python3 -c "
|
|
||||||
import json, urllib.request
|
|
||||||
body = open('/dev/stdin').read()
|
|
||||||
payload = json.dumps({'body': body}).encode()
|
|
||||||
req = urllib.request.Request(
|
|
||||||
'${API_BASE}/releases/${RELEASE_ID}',
|
|
||||||
data=payload, method='PATCH',
|
|
||||||
headers={
|
|
||||||
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
})
|
|
||||||
urllib.request.urlopen(req)
|
|
||||||
" <<< "$NOTES"
|
|
||||||
echo "Release notes updated from CHANGELOG.md"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
|
||||||
- name: "Step 9: Mirror release to GitHub"
|
|
||||||
if: >-
|
|
||||||
steps.version.outputs.skip != 'true' &&
|
|
||||||
secrets.GH_MIRROR_TOKEN != ''
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
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="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
php ${MOKO_CLI}/release_mirror.php \
|
|
||||||
--version "$VERSION" --tag "$RELEASE_TAG" \
|
|
||||||
--token "${{ secrets.MOKOGITEA_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
|
|
||||||
|
|
||||||
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
|
|
||||||
- name: "Step 10: Push main to GitHub mirror"
|
|
||||||
if: >-
|
|
||||||
steps.version.outputs.skip != 'true' &&
|
|
||||||
secrets.GH_MIRROR_TOKEN != ''
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
|
||||||
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
|
|
||||||
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
|
|
||||||
git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
|
|
||||||
git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
|
|
||||||
git fetch origin main --depth=1
|
|
||||||
git push github origin/main:refs/heads/main --force 2>/dev/null \
|
|
||||||
&& echo "main branch pushed to GitHub mirror" \
|
|
||||||
|| echo "WARNING: GitHub mirror push failed"
|
|
||||||
|
|
||||||
- name: "Step 11: Delete rc branch and recreate dev from main"
|
|
||||||
if: steps.version.outputs.skip != 'true'
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
|
||||||
|
|
||||||
# Delete rc branch (ephemeral — created by promote-rc)
|
|
||||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
|
||||||
"${API_BASE}/branches/rc" 2>/dev/null \
|
|
||||||
&& echo "Deleted rc branch" || echo "rc branch not found"
|
|
||||||
|
|
||||||
# Delete dev branch
|
|
||||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
|
||||||
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
|
|
||||||
|
|
||||||
# Recreate dev from main (now includes version bump + changelog promotion)
|
|
||||||
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${API_BASE}/branches" \
|
|
||||||
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
|
|
||||||
|
|
||||||
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
- name: "Step 12: Create version branch from main"
|
|
||||||
if: steps.version.outputs.skip != 'true'
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
|
||||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
|
||||||
BRANCH_NAME="version/${VERSION}"
|
|
||||||
MAIN_SHA=$(git rev-parse HEAD)
|
|
||||||
|
|
||||||
# Delete old version branch if it exists (same version re-release)
|
|
||||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
|
|
||||||
|
|
||||||
# Create version/XX.YY.ZZ from main
|
|
||||||
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
|
|
||||||
|
|
||||||
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# -- Dolibarr post-release: Reset dev version -----------------------------
|
|
||||||
- name: "Post-release: Reset dev version"
|
|
||||||
if: steps.version.outputs.skip != 'true'
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
php ${MOKO_CLI}/version_reset_dev.php \
|
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
|
||||||
--branch dev --path . 2>&1 || true
|
|
||||||
|
|
||||||
# -- Summary --------------------------------------------------------------
|
|
||||||
- name: Pipeline Summary
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
|
||||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
|
||||||
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
|
|
||||||
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
|
|
||||||
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
|
|
||||||
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
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](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoStandards.Universal
|
# INGROUP: MokoStandards.Universal
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://code.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
# PATH: /.mokogitea/workflows/branch-cleanup.yml
|
# PATH: /.mokogitea/workflows/branch-cleanup.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Delete feature branches after PR merge
|
# BRIEF: Delete feature branches after PR merge
|
||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
- name: Delete source branch
|
- name: Delete source branch
|
||||||
run: |
|
run: |
|
||||||
BRANCH="${{ github.event.pull_request.head.ref }}"
|
BRANCH="${{ github.event.pull_request.head.ref }}"
|
||||||
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
|
API="${{ vars.GITEA_URL || 'https://code.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
|
||||||
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
|
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
|
||||||
|
|
||||||
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
|
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
|
||||||
|
|||||||
@@ -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,204 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: MokoStandards.CI
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
|
||||||
# PATH: /.gitea/workflows/ci-generic.yml
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: CI pipeline — lint, validate, and test for generic projects (PHP + Node.js)
|
|
||||||
|
|
||||||
name: "Generic: Project CI"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- dev
|
|
||||||
- dev/**
|
|
||||||
- rc/**
|
|
||||||
- version/**
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- dev
|
|
||||||
- dev/**
|
|
||||||
- rc/**
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
# ── Lint & Validate ───────────────────────────────────────────────────
|
|
||||||
lint:
|
|
||||||
name: Lint & Validate
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Detect toolchain
|
|
||||||
id: detect
|
|
||||||
run: |
|
|
||||||
HAS_PHP=false
|
|
||||||
HAS_NODE=false
|
|
||||||
[ -f "composer.json" ] && HAS_PHP=true
|
|
||||||
[ -f "package.json" ] && HAS_NODE=true
|
|
||||||
echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "Toolchain: PHP=$HAS_PHP Node=$HAS_NODE"
|
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
if: steps.detect.outputs.has_php == 'true'
|
|
||||||
run: |
|
|
||||||
if ! command -v php &> /dev/null; then
|
|
||||||
sudo apt-get update -qq
|
|
||||||
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
php -v
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
if: steps.detect.outputs.has_node == 'true'
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
|
|
||||||
- name: Install PHP dependencies
|
|
||||||
if: steps.detect.outputs.has_php == 'true'
|
|
||||||
run: |
|
|
||||||
if [ -f "composer.json" ]; then
|
|
||||||
composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Install Node.js dependencies
|
|
||||||
if: steps.detect.outputs.has_node == 'true'
|
|
||||||
run: |
|
|
||||||
if [ -f "package.json" ]; then
|
|
||||||
npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: PHP syntax check
|
|
||||||
if: steps.detect.outputs.has_php == 'true'
|
|
||||||
run: |
|
|
||||||
ERRORS=0
|
|
||||||
while IFS= read -r -d '' file; do
|
|
||||||
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
|
|
||||||
echo "::error file=${file}::PHP syntax error"
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
fi
|
|
||||||
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./node_modules/*" -print0)
|
|
||||||
|
|
||||||
echo "## PHP Lint" >> $GITHUB_STEP_SUMMARY
|
|
||||||
if [ "$ERRORS" -eq 0 ]; then
|
|
||||||
echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "${ERRORS} file(s) with syntax errors." >> $GITHUB_STEP_SUMMARY
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: TypeScript/JavaScript lint
|
|
||||||
if: steps.detect.outputs.has_node == 'true'
|
|
||||||
run: |
|
|
||||||
if [ -f "node_modules/.bin/eslint" ]; then
|
|
||||||
npx eslint src/ --quiet 2>&1 || { echo "::error::ESLint errors found"; exit 1; }
|
|
||||||
echo "## ESLint" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "All files passed ESLint." >> $GITHUB_STEP_SUMMARY
|
|
||||||
elif [ -f ".eslintrc.json" ] || [ -f ".eslintrc.js" ] || [ -f "eslint.config.js" ]; then
|
|
||||||
echo "::warning::ESLint config found but eslint not installed"
|
|
||||||
else
|
|
||||||
echo "No ESLint configured — skipping"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: TypeScript compile check
|
|
||||||
if: steps.detect.outputs.has_node == 'true'
|
|
||||||
run: |
|
|
||||||
if [ -f "tsconfig.json" ] && [ -f "node_modules/.bin/tsc" ]; then
|
|
||||||
npx tsc --noEmit 2>&1 || { echo "::error::TypeScript compilation errors"; exit 1; }
|
|
||||||
echo "## TypeScript" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "TypeScript compilation passed." >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: PHPStan static analysis
|
|
||||||
if: steps.detect.outputs.has_php == 'true'
|
|
||||||
run: |
|
|
||||||
if [ -f "phpstan.neon" ] && [ -f "vendor/bin/phpstan" ]; then
|
|
||||||
vendor/bin/phpstan analyse --no-progress 2>&1 || { echo "::warning::PHPStan found issues"; }
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Tests ─────────────────────────────────────────────────────────────
|
|
||||||
test:
|
|
||||||
name: Tests
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: lint
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Detect toolchain
|
|
||||||
id: detect
|
|
||||||
run: |
|
|
||||||
HAS_PHP=false
|
|
||||||
HAS_NODE=false
|
|
||||||
[ -f "composer.json" ] && HAS_PHP=true
|
|
||||||
[ -f "package.json" ] && HAS_NODE=true
|
|
||||||
echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
if: steps.detect.outputs.has_php == 'true'
|
|
||||||
run: |
|
|
||||||
if ! command -v php &> /dev/null; then
|
|
||||||
sudo apt-get update -qq
|
|
||||||
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
if: steps.detect.outputs.has_node == 'true'
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
[ -f "composer.json" ] && composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true
|
|
||||||
[ -f "package.json" ] && { npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true; }
|
|
||||||
|
|
||||||
- name: Run PHP tests
|
|
||||||
if: steps.detect.outputs.has_php == 'true'
|
|
||||||
run: |
|
|
||||||
if [ -f "vendor/bin/phpunit" ]; then
|
|
||||||
vendor/bin/phpunit --testdox 2>&1
|
|
||||||
echo "## PHPUnit" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "Tests passed." >> $GITHUB_STEP_SUMMARY
|
|
||||||
elif [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then
|
|
||||||
echo "::warning::PHPUnit config found but phpunit not installed"
|
|
||||||
else
|
|
||||||
echo "No PHPUnit configured — skipping"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Run Node.js tests
|
|
||||||
if: steps.detect.outputs.has_node == 'true'
|
|
||||||
run: |
|
|
||||||
if jq -e '.scripts.test' package.json > /dev/null 2>&1; then
|
|
||||||
npm test 2>&1
|
|
||||||
echo "## Node.js Tests" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "Tests passed." >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "No test script in package.json — skipping"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Build check
|
|
||||||
run: |
|
|
||||||
if [ -f "Makefile" ]; then
|
|
||||||
make build 2>&1 || echo "::warning::Build failed or not configured"
|
|
||||||
elif [ -f "package.json" ] && jq -e '.scripts.build' package.json > /dev/null 2>&1; then
|
|
||||||
npm run build 2>&1 || echo "::warning::Build failed"
|
|
||||||
fi
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: MokoStandards.Maintenance
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
|
||||||
# PATH: /.gitea/workflows/cleanup.yml
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
|
||||||
|
|
||||||
name: "Universal: Repository Cleanup"
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 3 * * 0' # Weekly on Sunday at 03:00 UTC
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
env:
|
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
cleanup:
|
|
||||||
name: Clean Merged Branches
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
token: ${{ secrets.GA_TOKEN }}
|
|
||||||
|
|
||||||
- name: Delete merged branches
|
|
||||||
env:
|
|
||||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
|
||||||
run: |
|
|
||||||
echo "=== Merged Branch Cleanup ==="
|
|
||||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
|
||||||
|
|
||||||
# List branches via API
|
|
||||||
BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
|
|
||||||
"${API}/branches?limit=50" | jq -r '.[].name')
|
|
||||||
|
|
||||||
DELETED=0
|
|
||||||
for BRANCH in $BRANCHES; do
|
|
||||||
# Skip protected branches
|
|
||||||
case "$BRANCH" in
|
|
||||||
main|master|develop|release/*|hotfix/*) continue ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# 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 ${GA_TOKEN}" \
|
|
||||||
"${API}/branches/${BRANCH}" 2>/dev/null || true
|
|
||||||
DELETED=$((DELETED + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Deleted ${DELETED} merged branch(es)"
|
|
||||||
|
|
||||||
- name: Clean old workflow runs
|
|
||||||
env:
|
|
||||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
|
||||||
run: |
|
|
||||||
echo "=== Workflow Run Cleanup ==="
|
|
||||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
|
||||||
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
|
|
||||||
|
|
||||||
# Get old completed runs
|
|
||||||
RUNS=$(curl -sS -H "Authorization: token ${GA_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 ${GA_TOKEN}" \
|
|
||||||
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
|
|
||||||
DELETED=$((DELETED + 1))
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Deleted ${DELETED} old workflow run(s)"
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: MokoStandards.Deploy
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
|
|
||||||
# PATH: /templates/workflows/joomla/deploy-manual.yml.template
|
|
||||||
# VERSION: 04.07.00
|
|
||||||
# BRIEF: Manual SFTP deploy to dev server for Joomla repos
|
|
||||||
|
|
||||||
name: "Universal: Deploy to Dev (Manual)"
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
clear_remote:
|
|
||||||
description: 'Delete all remote files before uploading'
|
|
||||||
required: false
|
|
||||||
default: 'false'
|
|
||||||
type: boolean
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
name: SFTP Deploy to Dev
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
run: |
|
|
||||||
php -v && composer --version
|
|
||||||
|
|
||||||
- name: Setup MokoStandards tools
|
|
||||||
env:
|
|
||||||
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
|
||||||
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
|
|
||||||
run: |
|
|
||||||
git clone --depth 1 --branch main --quiet \
|
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
|
||||||
/tmp/mokostandards-api 2>/dev/null || true
|
|
||||||
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
|
|
||||||
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Check FTP configuration
|
|
||||||
id: check
|
|
||||||
env:
|
|
||||||
HOST: ${{ vars.DEV_FTP_HOST }}
|
|
||||||
PATH_VAR: ${{ vars.DEV_FTP_PATH }}
|
|
||||||
PORT: ${{ vars.DEV_FTP_PORT }}
|
|
||||||
run: |
|
|
||||||
if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then
|
|
||||||
echo "DEV_FTP_HOST or DEV_FTP_PATH not configured -- cannot deploy"
|
|
||||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "host=$HOST" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
REMOTE="${PATH_VAR%/}"
|
|
||||||
echo "remote=$REMOTE" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
[ -z "$PORT" ] && PORT="22"
|
|
||||||
echo "port=$PORT" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Deploy via SFTP
|
|
||||||
if: steps.check.outputs.skip != 'true'
|
|
||||||
env:
|
|
||||||
SFTP_KEY: ${{ secrets.DEV_FTP_KEY }}
|
|
||||||
SFTP_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
|
|
||||||
SFTP_USER: ${{ vars.DEV_FTP_USERNAME }}
|
|
||||||
run: |
|
|
||||||
SOURCE_DIR="src"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ -- nothing to deploy"; exit 0; }
|
|
||||||
|
|
||||||
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
|
|
||||||
"${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \
|
|
||||||
> /tmp/sftp-config.json
|
|
||||||
|
|
||||||
if [ -n "$SFTP_KEY" ]; then
|
|
||||||
echo "$SFTP_KEY" > /tmp/deploy_key
|
|
||||||
chmod 600 /tmp/deploy_key
|
|
||||||
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
|
|
||||||
else
|
|
||||||
printf ',"password":"%s"}' "$SFTP_PASS" >> /tmp/sftp-config.json
|
|
||||||
fi
|
|
||||||
|
|
||||||
DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
|
|
||||||
[ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote)
|
|
||||||
|
|
||||||
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
|
|
||||||
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
|
|
||||||
php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
|
|
||||||
else
|
|
||||||
php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
|
||||||
|
|
||||||
- name: Summary
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
if [ "${{ steps.check.outputs.skip }}" = "true" ]; then
|
|
||||||
echo "### Deploy Skipped -- FTP not configured" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Host | \`${{ steps.check.outputs.host }}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Remote | \`${{ steps.check.outputs.remote }}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Clear | ${{ inputs.clear_remote }} |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: MokoStandards.Security
|
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
|
||||||
# PATH: /templates/workflows/gitleaks.yml.template
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
|
||||||
#
|
|
||||||
# +========================================================================+
|
|
||||||
# | SECRET SCANNING |
|
|
||||||
# +========================================================================+
|
|
||||||
# | |
|
|
||||||
# | Scans commits for leaked secrets using Gitleaks. |
|
|
||||||
# | |
|
|
||||||
# | - PR scan: only new commits in the PR |
|
|
||||||
# | - Scheduled: full repo scan weekly |
|
|
||||||
# | - Alerts via ntfy on findings |
|
|
||||||
# | |
|
|
||||||
# +========================================================================+
|
|
||||||
|
|
||||||
name: "Universal: Secret Scanning"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- 'dev/**'
|
|
||||||
schedule:
|
|
||||||
- cron: '0 5 * * 1' # Weekly Monday 05:00 UTC
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
env:
|
|
||||||
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
|
||||||
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
gitleaks:
|
|
||||||
name: Gitleaks Secret Scan
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Install Gitleaks
|
|
||||||
run: |
|
|
||||||
GITLEAKS_VERSION="8.21.2"
|
|
||||||
curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
|
|
||||||
| tar -xz -C /usr/local/bin gitleaks
|
|
||||||
gitleaks version
|
|
||||||
|
|
||||||
- name: Scan for secrets
|
|
||||||
id: scan
|
|
||||||
run: |
|
|
||||||
echo "### Secret Scanning" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ARGS="--source . --verbose --report-format json --report-path /tmp/gitleaks-report.json"
|
|
||||||
|
|
||||||
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
|
||||||
# Scan only PR commits
|
|
||||||
ARGS="$ARGS --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}"
|
|
||||||
echo "Scanning PR commits only" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "Full repository scan" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
if gitleaks detect $ARGS 2>&1; then
|
|
||||||
echo "result=clean" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "result=found" >> "$GITHUB_OUTPUT"
|
|
||||||
FINDINGS=$(jq length /tmp/gitleaks-report.json 2>/dev/null || echo "unknown")
|
|
||||||
echo "**${FINDINGS} potential secret(s) detected.**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "Review the findings and rotate any exposed credentials immediately." >> $GITHUB_STEP_SUMMARY
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Notify on findings
|
|
||||||
if: failure() && steps.scan.outputs.result == 'found'
|
|
||||||
run: |
|
|
||||||
REPO="${{ github.event.repository.name }}"
|
|
||||||
curl -sS \
|
|
||||||
-H "Title: ${REPO} — secrets detected in code" \
|
|
||||||
-H "Tags: rotating_light,key" \
|
|
||||||
-H "Priority: urgent" \
|
|
||||||
-d "Gitleaks found potential secrets. Review and rotate credentials immediately." \
|
|
||||||
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: mokoplatform.Automation
|
# INGROUP: mokoplatform.Automation
|
||||||
# VERSION: 06.12.03
|
# VERSION: 06.12.04
|
||||||
# BRIEF: Auto-create feature branch when an issue is opened
|
# BRIEF: Auto-create feature branch when an issue is opened
|
||||||
|
|
||||||
name: "Universal: Issue Branch"
|
name: "Universal: Issue Branch"
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: MokoStandards.Notifications
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
|
||||||
# PATH: /.gitea/workflows/notify.yml
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
|
||||||
|
|
||||||
name: "Universal: Notifications"
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows:
|
|
||||||
- "Joomla Build & Release"
|
|
||||||
- "Joomla Extension CI"
|
|
||||||
- "Deploy"
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
env:
|
|
||||||
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
|
||||||
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-releases' }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
notify:
|
|
||||||
name: Send Notification
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: >-
|
|
||||||
github.event.workflow_run.conclusion == 'success' ||
|
|
||||||
github.event.workflow_run.conclusion == 'failure'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Notify on success (releases only)
|
|
||||||
if: >-
|
|
||||||
github.event.workflow_run.conclusion == 'success' &&
|
|
||||||
contains(github.event.workflow_run.name, 'Release')
|
|
||||||
run: |
|
|
||||||
REPO="${{ github.event.repository.name }}"
|
|
||||||
WORKFLOW="${{ github.event.workflow_run.name }}"
|
|
||||||
URL="${{ github.event.workflow_run.html_url }}"
|
|
||||||
|
|
||||||
curl -sS \
|
|
||||||
-H "Title: ${REPO} released" \
|
|
||||||
-H "Tags: white_check_mark,package" \
|
|
||||||
-H "Priority: default" \
|
|
||||||
-H "Click: ${URL}" \
|
|
||||||
-d "${WORKFLOW} completed successfully." \
|
|
||||||
"${NTFY_URL}/${NTFY_TOPIC}"
|
|
||||||
|
|
||||||
- name: Notify on failure
|
|
||||||
if: github.event.workflow_run.conclusion == 'failure'
|
|
||||||
run: |
|
|
||||||
REPO="${{ github.event.repository.name }}"
|
|
||||||
WORKFLOW="${{ github.event.workflow_run.name }}"
|
|
||||||
URL="${{ github.event.workflow_run.html_url }}"
|
|
||||||
|
|
||||||
curl -sS \
|
|
||||||
-H "Title: ${REPO} workflow failed" \
|
|
||||||
-H "Tags: x,warning" \
|
|
||||||
-H "Priority: high" \
|
|
||||||
-H "Click: ${URL}" \
|
|
||||||
-d "${WORKFLOW} failed. Check the run for details." \
|
|
||||||
"${NTFY_URL}/${NTFY_TOPIC}"
|
|
||||||
+508
-508
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,242 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Release
|
# INGROUP: mokoplatform.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||||
# VERSION: 05.01.00
|
# VERSION: 05.01.00
|
||||||
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
|
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
|
||||||
|
|
||||||
|
name: "Universal: Pre-Release"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
- alpha
|
||||||
|
- beta
|
||||||
|
- rc
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
stability:
|
||||||
|
description: 'Pre-release channel'
|
||||||
|
required: true
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- development
|
||||||
|
- alpha
|
||||||
|
- beta
|
||||||
|
- release-candidate
|
||||||
|
|
||||||
|
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 }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})"
|
||||||
|
runs-on: release
|
||||||
|
if: >-
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
github.event_name == 'push'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
ref: ${{ github.ref_name }}
|
||||||
|
|
||||||
|
- name: Setup mokoplatform tools
|
||||||
|
env:
|
||||||
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
|
run: |
|
||||||
|
# Use pre-installed /opt/mokoplatform if available (updated by cron every 6h)
|
||||||
|
if [ -f /opt/mokoplatform/cli/version_bump.php ] && [ -f /opt/mokoplatform/cli/manifest_element.php ] && [ -f /opt/mokoplatform/vendor/autoload.php ]; then
|
||||||
|
echo Using pre-installed /opt/mokoplatform
|
||||||
|
echo MOKO_CLI=/opt/mokoplatform/cli >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo Falling back to fresh clone
|
||||||
|
if ! command -v composer > /dev/null 2>&1; then
|
||||||
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
||||||
|
fi
|
||||||
|
rm -rf /tmp/mokoplatform-api
|
||||||
|
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokoplatform.git
|
||||||
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokoplatform-api
|
||||||
|
cd /tmp/mokoplatform-api && composer install --no-dev --no-interaction --quiet
|
||||||
|
echo MOKO_CLI=/tmp/mokoplatform-api/cli >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Detect platform
|
||||||
|
id: platform
|
||||||
|
run: |
|
||||||
|
php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
||||||
|
|
||||||
|
- name: Resolve metadata and bump version
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
# Auto-detect stability from branch name on push, or use input on dispatch
|
||||||
|
if [ "${{ github.event_name }}" = "push" ]; then
|
||||||
|
case "${{ github.ref_name }}" in
|
||||||
|
rc) STABILITY="release-candidate" ;;
|
||||||
|
alpha) STABILITY="alpha" ;;
|
||||||
|
beta) STABILITY="beta" ;;
|
||||||
|
*) STABILITY="development" ;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
STABILITY="${{ inputs.stability || 'development' }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$STABILITY" in
|
||||||
|
development) SUFFIX="-dev"; TAG="development" ;;
|
||||||
|
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
|
||||||
|
beta) SUFFIX="-beta"; TAG="beta" ;;
|
||||||
|
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Bump version via CLI: patch for dev/alpha/beta, minor for RC
|
||||||
|
case "$STABILITY" in
|
||||||
|
release-candidate) BUMP="minor" ;;
|
||||||
|
*) BUMP="patch" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true
|
||||||
|
|
||||||
|
# Set stability suffix and verify consistency
|
||||||
|
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01")
|
||||||
|
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||||
|
|
||||||
|
php ${MOKO_CLI}/version_set_platform.php \
|
||||||
|
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
|
||||||
|
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
||||||
|
|
||||||
|
# Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml
|
||||||
|
php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true
|
||||||
|
|
||||||
|
# Append suffix for output
|
||||||
|
if [ -n "$SUFFIX" ]; then
|
||||||
|
VERSION="${VERSION}${SUFFIX}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Commit version bump
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
git add -A
|
||||||
|
git diff --cached --quiet || {
|
||||||
|
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
|
||||||
|
git push origin HEAD 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Auto-detect element via manifest_element.php
|
||||||
|
php ${MOKO_CLI}/manifest_element.php \
|
||||||
|
--path . --version "$VERSION" --stability "$STABILITY" \
|
||||||
|
--repo "${GITEA_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 "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
|
||||||
|
|
||||||
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
|
||||||
|
|
||||||
|
- name: Create release
|
||||||
|
id: release
|
||||||
|
run: |
|
||||||
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_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
|
||||||
|
|
||||||
|
- name: Update release notes from CHANGELOG.md
|
||||||
|
run: |
|
||||||
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
|
||||||
|
if [ -f "CHANGELOG.md" ]; then
|
||||||
|
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
||||||
|
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
|
||||||
|
else
|
||||||
|
NOTES="Release ${VERSION}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update release body via API
|
||||||
|
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_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
|
||||||
|
python3 -c "
|
||||||
|
import json, urllib.request
|
||||||
|
body = open('/dev/stdin').read()
|
||||||
|
payload = json.dumps({'body': body}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
'${API_BASE}/releases/${RELEASE_ID}',
|
||||||
|
data=payload, method='PATCH',
|
||||||
|
headers={
|
||||||
|
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
})
|
||||||
|
urllib.request.urlopen(req)
|
||||||
|
" <<< "$NOTES"
|
||||||
|
echo "Release notes updated from CHANGELOG.md"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build package and upload
|
||||||
|
id: package
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_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
|
||||||
|
|
||||||
|
# updates.xml is generated dynamically by MokoGitea license server
|
||||||
|
# No need to build, commit, or sync updates.xml from workflows
|
||||||
|
|
||||||
|
- name: "Delete lesser pre-release channels (cascade)"
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
|
php ${MOKO_CLI}/release_cascade.php \
|
||||||
|
--stability "${{ steps.meta.outputs.stability }}" \
|
||||||
|
--token "${TOKEN}" \
|
||||||
|
--api-base "${API_BASE}"
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
|
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
|
||||||
|
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
||||||
|
echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,82 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: MokoStandards.Security
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
|
||||||
# PATH: /.gitea/workflows/security-audit.yml
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Dependency vulnerability scanning for composer and npm packages
|
|
||||||
|
|
||||||
name: "Universal: Security Audit"
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 6 * * 1' # Weekly on Monday at 06:00 UTC
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- 'composer.json'
|
|
||||||
- 'composer.lock'
|
|
||||||
- 'package.json'
|
|
||||||
- 'package-lock.json'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
env:
|
|
||||||
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
|
||||||
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
audit:
|
|
||||||
name: Dependency Audit
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Composer audit
|
|
||||||
if: hashFiles('composer.lock') != ''
|
|
||||||
run: |
|
|
||||||
echo "=== Composer Security Audit ==="
|
|
||||||
if ! command -v composer &> /dev/null; then
|
|
||||||
sudo apt-get update -qq
|
|
||||||
sudo apt-get install -y -qq php-cli composer >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
composer audit --format=plain 2>&1 | tee /tmp/composer-audit.txt
|
|
||||||
RESULT=$?
|
|
||||||
if [ $RESULT -ne 0 ]; then
|
|
||||||
echo "::warning::Composer vulnerabilities found"
|
|
||||||
echo "composer_vulnerable=true" >> "$GITHUB_ENV"
|
|
||||||
else
|
|
||||||
echo "No known vulnerabilities in composer dependencies"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: NPM audit
|
|
||||||
if: hashFiles('package-lock.json') != ''
|
|
||||||
run: |
|
|
||||||
echo "=== NPM Security Audit ==="
|
|
||||||
npm audit --production 2>&1 | tee /tmp/npm-audit.txt || true
|
|
||||||
if npm audit --production 2>&1 | grep -q "found 0 vulnerabilities"; then
|
|
||||||
echo "No known vulnerabilities in npm dependencies"
|
|
||||||
else
|
|
||||||
echo "::warning::NPM vulnerabilities found"
|
|
||||||
echo "npm_vulnerable=true" >> "$GITHUB_ENV"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Notify on vulnerabilities
|
|
||||||
if: env.composer_vulnerable == 'true' || env.npm_vulnerable == 'true'
|
|
||||||
run: |
|
|
||||||
REPO="${{ github.event.repository.name }}"
|
|
||||||
curl -sS \
|
|
||||||
-H "Title: ${REPO} has vulnerable dependencies" \
|
|
||||||
-H "Tags: lock,warning" \
|
|
||||||
-H "Priority: high" \
|
|
||||||
-d "Security audit found vulnerabilities. Review dependency updates." \
|
|
||||||
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
|
||||||
@@ -3,21 +3,6 @@
|
|||||||
All notable changes to MokoGitea are documented here. Versions follow the format
|
All notable changes to MokoGitea are documented here. Versions follow the format
|
||||||
`v{upstream}-moko.{major}.{minor}` (e.g. `v1.26.1-moko.06.03`).
|
`v{upstream}-moko.{major}.{minor}` (e.g. `v1.26.1-moko.06.03`).
|
||||||
|
|
||||||
## [Unreleased]
|
|
||||||
|
|
||||||
* FEATURES
|
|
||||||
* feat(api): issue status/priority/type exposed in REST API - GET/PATCH on issues now includes status_id, priority_id, type_id with resolved names
|
|
||||||
* feat(api): org-level issue metadata endpoints - GET /orgs/{org}/issue-statuses, /issue-priorities, /issue-types
|
|
||||||
* feat(wiki): org wiki tab - inline wiki rendering from convention repos (wiki / wiki-private)
|
|
||||||
* feat(wiki): public/private wiki toggle dropdown (same UX as org profile README selector)
|
|
||||||
* feat(wiki): external wiki support - link to an outside URL from the org wiki tab
|
|
||||||
* feat(settings): wiki mode setting in org settings (internal repos vs external URL)
|
|
||||||
* feat(mcp): 5 new MCP tools - gitea_org_issue_statuses_list, gitea_org_issue_priorities_list, gitea_org_issue_types_list, gitea_issue_set_status, gitea_issue_set_priority
|
|
||||||
* feat(mcp): gitea_issue_create and gitea_issue_update now accept status_id, priority_id, type_id
|
|
||||||
|
|
||||||
* MIGRATIONS
|
|
||||||
* migration 354: add wiki_mode and wiki_url columns to user table for org wiki settings
|
|
||||||
|
|
||||||
## [v1.26.1-moko.06.12] - 2026-06-07
|
## [v1.26.1-moko.06.12] - 2026-06-07
|
||||||
|
|
||||||
* FEATURES
|
* FEATURES
|
||||||
|
|||||||
Submodule mcp-mokogitea-api deleted from c9eb6cfc89
@@ -431,7 +431,6 @@ func prepareMigrationTasks() []*migration {
|
|||||||
newMigration(351, "Add CDN public flag to attachments", v1_27.AddAttachmentCDNPublic),
|
newMigration(351, "Add CDN public flag to attachments", v1_27.AddAttachmentCDNPublic),
|
||||||
newMigration(352, "Add version prefix and element name to repo manifest", v1_27.AddManifestVersionPrefixAndElement),
|
newMigration(352, "Add version prefix and element name to repo manifest", v1_27.AddManifestVersionPrefixAndElement),
|
||||||
newMigration(353, "Add distribution metadata fields to repo manifest", v1_27.AddManifestDistributionFields),
|
newMigration(353, "Add distribution metadata fields to repo manifest", v1_27.AddManifestDistributionFields),
|
||||||
newMigration(354, "Add org wiki settings to user table", v1_27.AddOrgWikiSettings),
|
|
||||||
}
|
}
|
||||||
return preparedMigrations
|
return preparedMigrations
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package v1_27
|
|
||||||
|
|
||||||
import "xorm.io/xorm"
|
|
||||||
|
|
||||||
// AddOrgWikiSettings adds wiki_mode and wiki_url columns to the user table
|
|
||||||
// for configuring org-level wiki behavior (internal convention repos vs external link).
|
|
||||||
func AddOrgWikiSettings(x *xorm.Engine) error {
|
|
||||||
type User struct {
|
|
||||||
WikiMode string `xorm:"VARCHAR(20) NOT NULL DEFAULT '' 'wiki_mode'"`
|
|
||||||
WikiURL string `xorm:"TEXT 'wiki_url'"`
|
|
||||||
}
|
|
||||||
return x.Sync(new(User))
|
|
||||||
}
|
|
||||||
@@ -153,8 +153,6 @@ type User struct {
|
|||||||
Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 0"`
|
Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 0"`
|
||||||
RepoAdminChangeTeamAccess bool `xorm:"NOT NULL DEFAULT false"`
|
RepoAdminChangeTeamAccess bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
ParentOrgID int64 `xorm:"INDEX DEFAULT 0"` // 0 = no parent (top-level org)
|
ParentOrgID int64 `xorm:"INDEX DEFAULT 0"` // 0 = no parent (top-level org)
|
||||||
WikiMode string `xorm:"VARCHAR(20) NOT NULL DEFAULT '' 'wiki_mode'"` // "" = internal (convention repos), "external" = link to WikiURL
|
|
||||||
WikiURL string `xorm:"TEXT 'wiki_url'"` // external wiki URL (used when WikiMode == "external")
|
|
||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"`
|
DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"`
|
||||||
|
|||||||
@@ -84,14 +84,6 @@ type Issue struct {
|
|||||||
PinOrder int `json:"pin_order"`
|
PinOrder int `json:"pin_order"`
|
||||||
// The version of the issue content for optimistic locking
|
// The version of the issue content for optimistic locking
|
||||||
ContentVersion int `json:"content_version"`
|
ContentVersion int `json:"content_version"`
|
||||||
|
|
||||||
// Issue metadata (org-level definitions)
|
|
||||||
StatusID int64 `json:"status_id"`
|
|
||||||
StatusName string `json:"status_name"`
|
|
||||||
PriorityID int64 `json:"priority_id"`
|
|
||||||
PriorityName string `json:"priority_name"`
|
|
||||||
TypeID int64 `json:"type_id"`
|
|
||||||
TypeName string `json:"type_name"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateIssueOption options to create one issue
|
// CreateIssueOption options to create one issue
|
||||||
@@ -114,10 +106,6 @@ type CreateIssueOption struct {
|
|||||||
Closed bool `json:"closed"`
|
Closed bool `json:"closed"`
|
||||||
// custom field values keyed by field name
|
// custom field values keyed by field name
|
||||||
CustomFields map[string]string `json:"custom_fields,omitempty"`
|
CustomFields map[string]string `json:"custom_fields,omitempty"`
|
||||||
// org-level issue metadata IDs
|
|
||||||
StatusID *int64 `json:"status_id,omitempty"`
|
|
||||||
PriorityID *int64 `json:"priority_id,omitempty"`
|
|
||||||
TypeID *int64 `json:"type_id,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EditIssueOption options for editing an issue
|
// EditIssueOption options for editing an issue
|
||||||
@@ -137,10 +125,6 @@ type EditIssueOption struct {
|
|||||||
RemoveDeadline *bool `json:"unset_due_date"`
|
RemoveDeadline *bool `json:"unset_due_date"`
|
||||||
// The current version of the issue content to detect conflicts during editing
|
// The current version of the issue content to detect conflicts during editing
|
||||||
ContentVersion *int `json:"content_version"`
|
ContentVersion *int `json:"content_version"`
|
||||||
// org-level issue metadata IDs
|
|
||||||
StatusID *int64 `json:"status_id,omitempty"`
|
|
||||||
PriorityID *int64 `json:"priority_id,omitempty"`
|
|
||||||
TypeID *int64 `json:"type_id,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EditDeadlineOption options for creating a deadline
|
// EditDeadlineOption options for creating a deadline
|
||||||
@@ -157,39 +141,6 @@ type IssueDeadline struct {
|
|||||||
Deadline *time.Time `json:"due_date"`
|
Deadline *time.Time `json:"due_date"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IssueStatusDef represents an org-level issue status definition
|
|
||||||
// swagger:model
|
|
||||||
type IssueStatusDef struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Color string `json:"color"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
ClosesIssue bool `json:"closes_issue"`
|
|
||||||
SortOrder int `json:"sort_order"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// IssuePriorityDef represents an org-level issue priority definition
|
|
||||||
// swagger:model
|
|
||||||
type IssuePriorityDef struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Color string `json:"color"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
SortOrder int `json:"sort_order"`
|
|
||||||
IsDefault bool `json:"is_default"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// IssueTypeDef represents an org-level issue type definition
|
|
||||||
// swagger:model
|
|
||||||
type IssueTypeDef struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Color string `json:"color"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
SortOrder int `json:"sort_order"`
|
|
||||||
IsDefault bool `json:"is_default"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// IssueFormFieldType defines issue form field type, can be "markdown", "textarea", "input", "dropdown" or "checkboxes"
|
// IssueFormFieldType defines issue form field type, can be "markdown", "textarea", "input", "dropdown" or "checkboxes"
|
||||||
//
|
//
|
||||||
// swagger:enum IssueFormFieldType
|
// swagger:enum IssueFormFieldType
|
||||||
|
|||||||
@@ -1773,9 +1773,6 @@ func Routes() *web.Router {
|
|||||||
m.Post("", reqToken(), reqOrgOwnership(), org.CreateOrgCustomField)
|
m.Post("", reqToken(), reqOrgOwnership(), org.CreateOrgCustomField)
|
||||||
m.Delete("/{id}", reqToken(), reqOrgOwnership(), org.DeleteOrgCustomField)
|
m.Delete("/{id}", reqToken(), reqOrgOwnership(), org.DeleteOrgCustomField)
|
||||||
})
|
})
|
||||||
m.Get("/issue-statuses", org.ListIssueStatuses)
|
|
||||||
m.Get("/issue-priorities", org.ListIssuePriorities)
|
|
||||||
m.Get("/issue-types", org.ListIssueTypes)
|
|
||||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true), checkTokenPublicOnly())
|
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true), checkTokenPublicOnly())
|
||||||
m.Group("/teams/{teamid}", func() {
|
m.Group("/teams/{teamid}", func() {
|
||||||
m.Combo("").Get(reqToken(), org.GetTeam).
|
m.Combo("").Get(reqToken(), org.GetTeam).
|
||||||
|
|||||||
@@ -1,138 +0,0 @@
|
|||||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package org
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
|
|
||||||
api "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
|
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ListIssueStatuses returns active issue status definitions for an org.
|
|
||||||
func ListIssueStatuses(ctx *context.APIContext) {
|
|
||||||
// swagger:operation GET /orgs/{org}/issue-statuses organization orgListIssueStatuses
|
|
||||||
// ---
|
|
||||||
// summary: List an organization's issue status definitions
|
|
||||||
// produces:
|
|
||||||
// - application/json
|
|
||||||
// parameters:
|
|
||||||
// - name: org
|
|
||||||
// in: path
|
|
||||||
// description: name of the organization
|
|
||||||
// type: string
|
|
||||||
// required: true
|
|
||||||
// responses:
|
|
||||||
// "200":
|
|
||||||
// description: "IssueStatusDefList"
|
|
||||||
// schema:
|
|
||||||
// type: array
|
|
||||||
// items:
|
|
||||||
// "$ref": "#/definitions/IssueStatusDef"
|
|
||||||
// "404":
|
|
||||||
// "$ref": "#/responses/notFound"
|
|
||||||
|
|
||||||
defs, err := issues_model.GetIssueStatusDefsByOrg(ctx, ctx.Org.Organization.ID)
|
|
||||||
if err != nil {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
result := make([]*api.IssueStatusDef, 0, len(defs))
|
|
||||||
for _, d := range defs {
|
|
||||||
result = append(result, &api.IssueStatusDef{
|
|
||||||
ID: d.ID,
|
|
||||||
Name: d.Name,
|
|
||||||
Color: d.Color,
|
|
||||||
Description: d.Description,
|
|
||||||
ClosesIssue: d.ClosesIssue,
|
|
||||||
SortOrder: d.SortOrder,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
ctx.JSON(http.StatusOK, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListIssuePriorities returns active issue priority definitions for an org.
|
|
||||||
func ListIssuePriorities(ctx *context.APIContext) {
|
|
||||||
// swagger:operation GET /orgs/{org}/issue-priorities organization orgListIssuePriorities
|
|
||||||
// ---
|
|
||||||
// summary: List an organization's issue priority definitions
|
|
||||||
// produces:
|
|
||||||
// - application/json
|
|
||||||
// parameters:
|
|
||||||
// - name: org
|
|
||||||
// in: path
|
|
||||||
// description: name of the organization
|
|
||||||
// type: string
|
|
||||||
// required: true
|
|
||||||
// responses:
|
|
||||||
// "200":
|
|
||||||
// description: "IssuePriorityDefList"
|
|
||||||
// schema:
|
|
||||||
// type: array
|
|
||||||
// items:
|
|
||||||
// "$ref": "#/definitions/IssuePriorityDef"
|
|
||||||
// "404":
|
|
||||||
// "$ref": "#/responses/notFound"
|
|
||||||
|
|
||||||
defs, err := issues_model.GetIssuePriorityDefsByOrg(ctx, ctx.Org.Organization.ID)
|
|
||||||
if err != nil {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
result := make([]*api.IssuePriorityDef, 0, len(defs))
|
|
||||||
for _, d := range defs {
|
|
||||||
result = append(result, &api.IssuePriorityDef{
|
|
||||||
ID: d.ID,
|
|
||||||
Name: d.Name,
|
|
||||||
Color: d.Color,
|
|
||||||
Description: d.Description,
|
|
||||||
SortOrder: d.SortOrder,
|
|
||||||
IsDefault: d.IsDefault,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
ctx.JSON(http.StatusOK, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListIssueTypes returns active issue type definitions for an org.
|
|
||||||
func ListIssueTypes(ctx *context.APIContext) {
|
|
||||||
// swagger:operation GET /orgs/{org}/issue-types organization orgListIssueTypes
|
|
||||||
// ---
|
|
||||||
// summary: List an organization's issue type definitions
|
|
||||||
// produces:
|
|
||||||
// - application/json
|
|
||||||
// parameters:
|
|
||||||
// - name: org
|
|
||||||
// in: path
|
|
||||||
// description: name of the organization
|
|
||||||
// type: string
|
|
||||||
// required: true
|
|
||||||
// responses:
|
|
||||||
// "200":
|
|
||||||
// description: "IssueTypeDefList"
|
|
||||||
// schema:
|
|
||||||
// type: array
|
|
||||||
// items:
|
|
||||||
// "$ref": "#/definitions/IssueTypeDef"
|
|
||||||
// "404":
|
|
||||||
// "$ref": "#/responses/notFound"
|
|
||||||
|
|
||||||
defs, err := issues_model.GetIssueTypeDefsByOrg(ctx, ctx.Org.Organization.ID)
|
|
||||||
if err != nil {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
result := make([]*api.IssueTypeDef, 0, len(defs))
|
|
||||||
for _, d := range defs {
|
|
||||||
result = append(result, &api.IssueTypeDef{
|
|
||||||
ID: d.ID,
|
|
||||||
Name: d.Name,
|
|
||||||
Color: d.Color,
|
|
||||||
Description: d.Description,
|
|
||||||
SortOrder: d.SortOrder,
|
|
||||||
IsDefault: d.IsDefault,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
ctx.JSON(http.StatusOK, result)
|
|
||||||
}
|
|
||||||
@@ -756,26 +756,6 @@ func CreateIssue(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set org-level issue metadata (status/priority/type) if provided
|
|
||||||
if form.StatusID != nil && *form.StatusID > 0 {
|
|
||||||
if err := issues_model.SetIssueStatusID(ctx, issue.ID, *form.StatusID); err != nil {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if form.PriorityID != nil && *form.PriorityID > 0 {
|
|
||||||
if err := issues_model.SetIssuePriorityID(ctx, issue.ID, *form.PriorityID); err != nil {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if form.TypeID != nil && *form.TypeID > 0 {
|
|
||||||
if err := issues_model.SetIssueTypeID(ctx, issue.ID, *form.TypeID); err != nil {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if form.Closed {
|
if form.Closed {
|
||||||
if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil {
|
if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil {
|
||||||
if issues_model.IsErrDependenciesLeft(err) {
|
if issues_model.IsErrDependenciesLeft(err) {
|
||||||
@@ -1000,26 +980,6 @@ func EditIssue(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update org-level issue metadata (status/priority/type)
|
|
||||||
if canWrite && form.StatusID != nil {
|
|
||||||
if err := issues_model.SetIssueStatusID(ctx, issue.ID, *form.StatusID); err != nil {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if canWrite && form.PriorityID != nil {
|
|
||||||
if err := issues_model.SetIssuePriorityID(ctx, issue.ID, *form.PriorityID); err != nil {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if canWrite && form.TypeID != nil {
|
|
||||||
if err := issues_model.SetIssueTypeID(ctx, issue.ID, *form.TypeID); err != nil {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refetch from database to assign some automatic values
|
// Refetch from database to assign some automatic values
|
||||||
issue, err = issues_model.GetIssueByID(ctx, issue.ID)
|
issue, err = issues_model.GetIssueByID(ctx, issue.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -126,17 +126,6 @@ func SettingsPost(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save wiki mode settings.
|
|
||||||
wikiMode := ctx.FormString("wiki_mode")
|
|
||||||
wikiURL := ctx.FormString("wiki_url")
|
|
||||||
orgUser := org.AsUser()
|
|
||||||
orgUser.WikiMode = wikiMode
|
|
||||||
orgUser.WikiURL = wikiURL
|
|
||||||
if err := user_model.UpdateUserCols(ctx, orgUser, "wiki_mode", "wiki_url"); err != nil {
|
|
||||||
ctx.ServerError("UpdateUserCols(wiki)", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Trace("Organization setting updated: %s", org.Name)
|
log.Trace("Organization setting updated: %s", org.Name)
|
||||||
ctx.Flash.Success(ctx.Tr("org.settings.update_setting_success"))
|
ctx.Flash.Success(ctx.Tr("org.settings.update_setting_success"))
|
||||||
ctx.Redirect(ctx.Org.OrgLink + "/settings")
|
ctx.Redirect(ctx.Org.OrgLink + "/settings")
|
||||||
|
|||||||
@@ -1,211 +0,0 @@
|
|||||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package org
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/renderhelper"
|
|
||||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
|
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/gitrepo"
|
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/markup/markdown"
|
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
|
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/util"
|
|
||||||
shared_user "code.mokoconsulting.tech/MokoConsulting/MokoGitea/routers/web/shared/user"
|
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
|
||||||
)
|
|
||||||
|
|
||||||
const tplOrgWiki templates.TplName = "org/wiki/view"
|
|
||||||
|
|
||||||
// OrgWikiPage represents a single page in the org wiki sidebar.
|
|
||||||
type OrgWikiPage struct {
|
|
||||||
Name string
|
|
||||||
SubURL string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wiki renders the org wiki tab.
|
|
||||||
func Wiki(ctx *context.Context) {
|
|
||||||
org := ctx.Org.Organization
|
|
||||||
|
|
||||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
|
||||||
ctx.ServerError("RenderUserOrgHeader", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Data["PageIsViewWiki"] = true
|
|
||||||
ctx.Data["Title"] = org.DisplayName() + " - Wiki"
|
|
||||||
|
|
||||||
// Determine which wiki repo to use (public vs member).
|
|
||||||
viewAs := ctx.FormString("view_as", util.Iif(ctx.Org.IsMember, "member", "public"))
|
|
||||||
viewAsMember := viewAs == "member"
|
|
||||||
|
|
||||||
wikiRepo, commit := findOrgWikiCommit(ctx, org.ID, util.Iif(viewAsMember, shared_user.RepoNameWikiPrivate, shared_user.RepoNameWikiPublic))
|
|
||||||
if wikiRepo == nil && viewAsMember {
|
|
||||||
// Fall back to public wiki if member wiki doesn't exist.
|
|
||||||
wikiRepo, commit = findOrgWikiCommit(ctx, org.ID, shared_user.RepoNameWikiPublic)
|
|
||||||
viewAsMember = false
|
|
||||||
}
|
|
||||||
if wikiRepo == nil && !viewAsMember {
|
|
||||||
// Fall back to member wiki if public wiki doesn't exist.
|
|
||||||
wikiRepo, commit = findOrgWikiCommit(ctx, org.ID, shared_user.RepoNameWikiPrivate)
|
|
||||||
viewAsMember = true
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Data["IsViewingWikiAsMember"] = viewAsMember
|
|
||||||
|
|
||||||
// Check whether both repos exist (for the dropdown toggle).
|
|
||||||
publicExists := shared_user.OrgWikiRepoExists(ctx, org.ID, shared_user.RepoNameWikiPublic)
|
|
||||||
privateExists := shared_user.OrgWikiRepoExists(ctx, org.ID, shared_user.RepoNameWikiPrivate)
|
|
||||||
ctx.Data["ShowWikiViewSelector"] = publicExists && privateExists && ctx.Org.IsMember
|
|
||||||
|
|
||||||
if wikiRepo == nil || commit == nil {
|
|
||||||
ctx.Data["WikiEmpty"] = true
|
|
||||||
ctx.HTML(http.StatusOK, tplOrgWiki)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.Data["WikiRepoLink"] = wikiRepo.Link()
|
|
||||||
|
|
||||||
// Build page list from repo root.
|
|
||||||
entries, err := commit.ListEntries()
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("ListEntries", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
pages := make([]OrgWikiPage, 0, len(entries))
|
|
||||||
for _, entry := range entries {
|
|
||||||
if !entry.IsRegular() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
name := entry.Name()
|
|
||||||
if !isMarkdownFile(name) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
displayName := strings.TrimSuffix(name, path.Ext(name))
|
|
||||||
if strings.EqualFold(displayName, "_sidebar") || strings.EqualFold(displayName, "_footer") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
pages = append(pages, OrgWikiPage{
|
|
||||||
Name: displayName,
|
|
||||||
SubURL: displayName,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
ctx.Data["Pages"] = pages
|
|
||||||
|
|
||||||
// Determine which page to render.
|
|
||||||
pageName := ctx.PathParamRaw("*")
|
|
||||||
if pageName == "" {
|
|
||||||
pageName = "Home"
|
|
||||||
}
|
|
||||||
ctx.Data["CurrentPage"] = pageName
|
|
||||||
|
|
||||||
// Try to find the file: exact match, then with .md extension.
|
|
||||||
blob := findWikiBlob(commit, pageName)
|
|
||||||
if blob == nil {
|
|
||||||
// Page not found — show empty state with page list.
|
|
||||||
ctx.Data["WikiPageNotFound"] = true
|
|
||||||
ctx.HTML(http.StatusOK, tplOrgWiki)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
content, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("GetBlobContent", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rctx := renderhelper.NewRenderContextRepoFile(ctx, wikiRepo, renderhelper.RepoFileOptions{
|
|
||||||
CurrentRefPath: path.Join("branch", util.PathEscapeSegments(wikiRepo.DefaultBranch)),
|
|
||||||
})
|
|
||||||
renderedContent, err := markdown.RenderString(rctx, content)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to render org wiki page %q: %v", pageName, err)
|
|
||||||
ctx.ServerError("RenderString", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.Data["WikiContent"] = renderedContent
|
|
||||||
|
|
||||||
// Render _Sidebar if it exists.
|
|
||||||
sidebarBlob := findWikiBlob(commit, "_Sidebar")
|
|
||||||
if sidebarBlob != nil {
|
|
||||||
sidebarContent, err := sidebarBlob.GetBlobContent(setting.UI.MaxDisplayFileSize)
|
|
||||||
if err == nil {
|
|
||||||
rendered, err := markdown.RenderString(rctx, sidebarContent)
|
|
||||||
if err == nil {
|
|
||||||
ctx.Data["WikiSidebarHTML"] = rendered
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render _Footer if it exists.
|
|
||||||
footerBlob := findWikiBlob(commit, "_Footer")
|
|
||||||
if footerBlob != nil {
|
|
||||||
footerContent, err := footerBlob.GetBlobContent(setting.UI.MaxDisplayFileSize)
|
|
||||||
if err == nil {
|
|
||||||
rendered, err := markdown.RenderString(rctx, footerContent)
|
|
||||||
if err == nil {
|
|
||||||
ctx.Data["WikiFooterHTML"] = rendered
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.HTML(http.StatusOK, tplOrgWiki)
|
|
||||||
}
|
|
||||||
|
|
||||||
// findOrgWikiCommit locates the convention wiki repo and returns its HEAD commit.
|
|
||||||
func findOrgWikiCommit(ctx *context.Context, orgID int64, repoName string) (*repo_model.Repository, *git.Commit) {
|
|
||||||
dbRepo, err := repo_model.GetRepositoryByName(ctx, orgID, repoName)
|
|
||||||
if err != nil {
|
|
||||||
if !repo_model.IsErrRepoNotExist(err) {
|
|
||||||
log.Error("findOrgWikiCommit: GetRepositoryByName(%d, %s): %v", orgID, repoName, err)
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if dbRepo.IsEmpty {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
gitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, dbRepo)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("findOrgWikiCommit: OpenRepository(%s): %v", dbRepo.FullName(), err)
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
commit, err := gitRepo.GetBranchCommit(dbRepo.DefaultBranch)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("findOrgWikiCommit: GetBranchCommit(%s, %s): %v", dbRepo.FullName(), dbRepo.DefaultBranch, err)
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return dbRepo, commit
|
|
||||||
}
|
|
||||||
|
|
||||||
// findWikiBlob looks up a markdown file in the commit by name.
|
|
||||||
// Tries exact match first, then appends .md.
|
|
||||||
func findWikiBlob(commit *git.Commit, name string) *git.Blob {
|
|
||||||
// Try exact match (e.g., "Home.md").
|
|
||||||
if blob, _ := commit.GetBlobByPath(name); blob != nil {
|
|
||||||
return blob
|
|
||||||
}
|
|
||||||
// Try with .md extension (e.g., "Home" → "Home.md").
|
|
||||||
if blob, _ := commit.GetBlobByPath(name + ".md"); blob != nil {
|
|
||||||
return blob
|
|
||||||
}
|
|
||||||
// Try with .markdown extension.
|
|
||||||
if blob, _ := commit.GetBlobByPath(name + ".markdown"); blob != nil {
|
|
||||||
return blob
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// isMarkdownFile returns true if the filename looks like a markdown file.
|
|
||||||
func isMarkdownFile(name string) bool {
|
|
||||||
ext := strings.ToLower(path.Ext(name))
|
|
||||||
return ext == ".md" || ext == ".markdown"
|
|
||||||
}
|
|
||||||
@@ -137,8 +137,6 @@ type PrepareOwnerHeaderResult struct {
|
|||||||
const (
|
const (
|
||||||
RepoNameProfilePrivate = ".profile-private"
|
RepoNameProfilePrivate = ".profile-private"
|
||||||
RepoNameProfile = ".profile"
|
RepoNameProfile = ".profile"
|
||||||
RepoNameWikiPublic = "wiki"
|
|
||||||
RepoNameWikiPrivate = "wiki-private"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func RenderUserOrgHeader(ctx *context.Context) (result *PrepareOwnerHeaderResult, err error) {
|
func RenderUserOrgHeader(ctx *context.Context) (result *PrepareOwnerHeaderResult, err error) {
|
||||||
@@ -157,18 +155,6 @@ func RenderUserOrgHeader(ctx *context.Context) (result *PrepareOwnerHeaderResult
|
|||||||
result.ProfilePrivateRepo, result.ProfilePrivateReadmeBlob = FindOwnerProfileReadme(ctx, ctx.Doer, RepoNameProfilePrivate)
|
result.ProfilePrivateRepo, result.ProfilePrivateReadmeBlob = FindOwnerProfileReadme(ctx, ctx.Doer, RepoNameProfilePrivate)
|
||||||
result.HasOrgProfileReadme = result.ProfilePublicReadmeBlob != nil || result.ProfilePrivateReadmeBlob != nil
|
result.HasOrgProfileReadme = result.ProfilePublicReadmeBlob != nil || result.ProfilePrivateReadmeBlob != nil
|
||||||
ctx.Data["HasOrgProfileReadme"] = result.HasOrgProfileReadme // many pages need it to show the "overview" tab
|
ctx.Data["HasOrgProfileReadme"] = result.HasOrgProfileReadme // many pages need it to show the "overview" tab
|
||||||
|
|
||||||
// Check if org has a wiki (internal convention repos or external URL).
|
|
||||||
orgUser := ctx.ContextUser
|
|
||||||
if orgUser.WikiMode == "external" && orgUser.WikiURL != "" {
|
|
||||||
ctx.Data["HasOrgWiki"] = true
|
|
||||||
ctx.Data["OrgWikiIsExternal"] = true
|
|
||||||
ctx.Data["OrgWikiExternalURL"] = orgUser.WikiURL
|
|
||||||
} else {
|
|
||||||
hasWiki := OrgWikiRepoExists(ctx, ctx.ContextUser.ID, RepoNameWikiPublic) ||
|
|
||||||
OrgWikiRepoExists(ctx, ctx.ContextUser.ID, RepoNameWikiPrivate)
|
|
||||||
ctx.Data["HasOrgWiki"] = hasWiki
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
_, profileReadmeBlob := FindOwnerProfileReadme(ctx, ctx.Doer)
|
_, profileReadmeBlob := FindOwnerProfileReadme(ctx, ctx.Doer)
|
||||||
ctx.Data["HasUserProfileReadme"] = profileReadmeBlob != nil
|
ctx.Data["HasUserProfileReadme"] = profileReadmeBlob != nil
|
||||||
@@ -208,12 +194,3 @@ func loadHeaderCount(ctx *context.Context) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// OrgWikiRepoExists checks whether a convention wiki repo exists and is non-empty.
|
|
||||||
func OrgWikiRepoExists(ctx *context.Context, ownerID int64, repoName string) bool {
|
|
||||||
dbRepo, err := repo_model.GetRepositoryByName(ctx, ownerID, repoName)
|
|
||||||
if err != nil || dbRepo.IsEmpty {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1162,11 +1162,6 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
|||||||
m.Get("/repositories", org.Repositories)
|
m.Get("/repositories", org.Repositories)
|
||||||
m.Get("/heatmap", user.DashboardHeatmap)
|
m.Get("/heatmap", user.DashboardHeatmap)
|
||||||
|
|
||||||
m.Group("/wiki", func() {
|
|
||||||
m.Get("", org.Wiki)
|
|
||||||
m.Get("/*", org.Wiki)
|
|
||||||
})
|
|
||||||
|
|
||||||
m.Group("/projects", func() {
|
m.Group("/projects", func() {
|
||||||
m.Group("", func() {
|
m.Group("", func() {
|
||||||
m.Get("", org.Projects)
|
m.Get("", org.Projects)
|
||||||
|
|||||||
@@ -131,26 +131,6 @@ func toIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Iss
|
|||||||
apiIssue.Deadline = issue.DeadlineUnix.AsTimePtr()
|
apiIssue.Deadline = issue.DeadlineUnix.AsTimePtr()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate org-level issue metadata (status/priority/type)
|
|
||||||
apiIssue.StatusID = issue.StatusID
|
|
||||||
apiIssue.PriorityID = issue.PriorityID
|
|
||||||
apiIssue.TypeID = issue.TypeID
|
|
||||||
if issue.StatusID > 0 {
|
|
||||||
if def, err := issues_model.GetIssueStatusDefByID(ctx, issue.StatusID); err == nil {
|
|
||||||
apiIssue.StatusName = def.Name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if issue.PriorityID > 0 {
|
|
||||||
if def, err := issues_model.GetIssuePriorityDefByID(ctx, issue.PriorityID); err == nil {
|
|
||||||
apiIssue.PriorityName = def.Name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if issue.TypeID > 0 {
|
|
||||||
if def, err := issues_model.GetIssueTypeDefByID(ctx, issue.TypeID); err == nil {
|
|
||||||
apiIssue.TypeName = def.Name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return apiIssue
|
return apiIssue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,17 +38,6 @@
|
|||||||
{{svg "octicon-code"}} {{ctx.Locale.Tr "org.code"}}
|
{{svg "octicon-code"}} {{ctx.Locale.Tr "org.code"}}
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if .HasOrgWiki}}
|
|
||||||
{{if .OrgWikiIsExternal}}
|
|
||||||
<a class="item" href="{{.OrgWikiExternalURL}}" target="_blank" rel="noopener noreferrer">
|
|
||||||
{{svg "octicon-book"}} Wiki {{svg "octicon-link-external" 12}}
|
|
||||||
</a>
|
|
||||||
{{else}}
|
|
||||||
<a class="{{if .PageIsViewWiki}}active {{end}}item" href="{{$.Org.HomeLink}}/-/wiki/">
|
|
||||||
{{svg "octicon-book"}} Wiki
|
|
||||||
</a>
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
{{if .NumMembers}}
|
{{if .NumMembers}}
|
||||||
<a class="{{if $.PageIsOrgMembers}}active {{end}}item" href="{{$.OrgLink}}/members">
|
<a class="{{if $.PageIsOrgMembers}}active {{end}}item" href="{{$.OrgLink}}/members">
|
||||||
{{svg "octicon-person"}} {{ctx.Locale.Tr "org.members"}}
|
{{svg "octicon-person"}} {{ctx.Locale.Tr "org.members"}}
|
||||||
|
|||||||
@@ -63,31 +63,6 @@
|
|||||||
|
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label>{{svg "octicon-book" 16}} Wiki</label>
|
|
||||||
<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>wiki</code> / <code>wiki-private</code> repos)</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="internal_wiki_box" class="field tw-pl-4 {{if ne .Org.WikiMode ""}}disabled{{end}}">
|
|
||||||
<p class="help">Create repos named <code>wiki</code> (public) and/or <code>wiki-private</code> (members-only) under this organization.</p>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<div class="ui radio checkbox">
|
|
||||||
<input class="enable-system-radio" name="wiki_mode" type="radio" value="external" data-context="#internal_wiki_box" data-target="#external_wiki_box" {{if eq .Org.WikiMode "external"}}checked{{end}}>
|
|
||||||
<label>External wiki</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="external_wiki_box" class="field tw-pl-4 {{if ne .Org.WikiMode "external"}}disabled{{end}}">
|
|
||||||
<label for="wiki_url">External wiki URL</label>
|
|
||||||
<input id="wiki_url" name="wiki_url" type="url" value="{{.Org.WikiURL}}" placeholder="https://wiki.example.com">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="divider"></div>
|
|
||||||
|
|
||||||
<div class="inline field">
|
<div class="inline field">
|
||||||
<div class="ui checkbox">
|
<div class="ui checkbox">
|
||||||
<input type="checkbox" name="require_2fa" {{if .Org.Require2FA}}checked{{end}}>
|
<input type="checkbox" name="require_2fa" {{if .Org.Require2FA}}checked{{end}}>
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
{{template "base/head" .}}
|
|
||||||
<div role="main" aria-label="{{.Title}}" class="page-content organization wiki">
|
|
||||||
{{template "org/header" .}}
|
|
||||||
{{template "org/menu" .}}
|
|
||||||
<div class="ui container">
|
|
||||||
{{if .WikiEmpty}}
|
|
||||||
<div class="ui placeholder segment">
|
|
||||||
<div class="ui icon header">
|
|
||||||
{{svg "octicon-book" 48}}
|
|
||||||
<br>
|
|
||||||
This organization doesn't have a wiki yet.
|
|
||||||
</div>
|
|
||||||
<p class="tw-text-center">
|
|
||||||
Create a repository named <code>wiki</code> (public) or <code>wiki-private</code> (members-only)
|
|
||||||
with markdown files to get started.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
<div class="repo-button-row tw-flex tw-items-center tw-gap-2 tw-mb-4">
|
|
||||||
<div class="tw-flex-1 tw-flex tw-items-center tw-gap-2">
|
|
||||||
{{svg "octicon-book" 16}}
|
|
||||||
<strong>{{.CurrentPage}}</strong>
|
|
||||||
</div>
|
|
||||||
{{if .ShowWikiViewSelector}}
|
|
||||||
<div class="ui dropdown jump">
|
|
||||||
{{- $viewAsRole := Iif (.IsViewingWikiAsMember) (ctx.Locale.Tr "org.members.member") (ctx.Locale.Tr "settings.visibility.public") -}}
|
|
||||||
<span class="text">{{svg "octicon-eye"}} {{ctx.Locale.Tr "org.view_as_role" $viewAsRole}}</span>
|
|
||||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
|
||||||
<div class="menu">
|
|
||||||
<a href="{{$.Org.HomeLink}}/-/wiki/{{.CurrentPage}}?view_as=public" class="item {{if not .IsViewingWikiAsMember}}selected{{end}}">
|
|
||||||
{{svg "octicon-check" 14 (Iif (not .IsViewingWikiAsMember) "" "tw-invisible")}} {{ctx.Locale.Tr "settings.visibility.public"}}
|
|
||||||
</a>
|
|
||||||
<a href="{{$.Org.HomeLink}}/-/wiki/{{.CurrentPage}}?view_as=member" class="item {{if .IsViewingWikiAsMember}}selected{{end}}">
|
|
||||||
{{svg "octicon-check" 14 (Iif .IsViewingWikiAsMember "" "tw-invisible")}} {{ctx.Locale.Tr "org.members.member"}}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{if .WikiPageNotFound}}
|
|
||||||
<div class="ui segment">
|
|
||||||
<div class="ui icon message">
|
|
||||||
{{svg "octicon-alert" 24}}
|
|
||||||
<div class="content">
|
|
||||||
<div class="header">Page not found</div>
|
|
||||||
<p>The page "{{.CurrentPage}}" does not exist in this wiki.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{if .Pages}}
|
|
||||||
<h4>Available pages:</h4>
|
|
||||||
<ul>
|
|
||||||
{{range .Pages}}
|
|
||||||
<li><a href="{{$.Org.HomeLink}}/-/wiki/{{.SubURL}}">{{.Name}}</a></li>
|
|
||||||
{{end}}
|
|
||||||
</ul>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
<div class="wiki-content-parts">
|
|
||||||
<div class="render-content markup wiki-content-main {{if or .WikiSidebarHTML .Pages}}with-sidebar{{end}}">
|
|
||||||
{{.WikiContent}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{if or .WikiSidebarHTML .Pages}}
|
|
||||||
<div class="render-content markup wiki-content-sidebar">
|
|
||||||
{{if .WikiSidebarHTML}}
|
|
||||||
{{.WikiSidebarHTML}}
|
|
||||||
<div class="ui divider"></div>
|
|
||||||
{{end}}
|
|
||||||
{{if .Pages}}
|
|
||||||
<strong>{{svg "octicon-list-unordered" 14}} Pages</strong>
|
|
||||||
<ul class="wiki-tree-list">
|
|
||||||
{{range .Pages}}
|
|
||||||
<li>
|
|
||||||
{{svg "octicon-file" 14}}
|
|
||||||
<a href="{{$.Org.HomeLink}}/-/wiki/{{.SubURL}}" {{if eq $.CurrentPage .Name}}class="active"{{end}}>{{.Name}}</a>
|
|
||||||
</li>
|
|
||||||
{{end}}
|
|
||||||
</ul>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
<div class="tw-clear-both"></div>
|
|
||||||
|
|
||||||
{{if .WikiFooterHTML}}
|
|
||||||
<div class="render-content markup wiki-content-footer">
|
|
||||||
{{.WikiFooterHTML}}
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{template "base/footer" .}}
|
|
||||||
@@ -126,6 +126,17 @@
|
|||||||
</select>
|
</select>
|
||||||
<p class="help">{{ctx.Locale.Tr "repo.settings.manifest_package_type_help"}}</p>
|
<p class="help">{{ctx.Locale.Tr "repo.settings.manifest_package_type_help"}}</p>
|
||||||
</div>
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<div class="field">
|
||||||
|
<label>{{ctx.Locale.Tr "repo.settings.manifest_element_full"}}</label>
|
||||||
|
<input name="element_name" value="{{.Manifest.ElementName}}" placeholder="{{.Manifest.AutoElementName}}">
|
||||||
|
{{if .Manifest.ElementNameMismatch}}
|
||||||
|
<p class="help tw-text-yellow-600">{{ctx.Locale.Tr "repo.settings.manifest_element_mismatch" .Manifest.AutoElementName}}</p>
|
||||||
|
{{else}}
|
||||||
|
<p class="help">{{ctx.Locale.Tr "repo.settings.manifest_element_full_help"}}</p>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>{{ctx.Locale.Tr "repo.settings.manifest_element_full"}}</label>
|
<label>{{ctx.Locale.Tr "repo.settings.manifest_element_full"}}</label>
|
||||||
<input name="element_name" value="{{.Manifest.ElementName}}" placeholder="{{.Manifest.AutoElementName}}">
|
<input name="element_name" value="{{.Manifest.ElementName}}" placeholder="{{.Manifest.AutoElementName}}">
|
||||||
|
|||||||
Reference in New Issue
Block a user