diff --git a/.mokogitea/CLAUDE.md b/.mokogitea/CLAUDE.md deleted file mode 100644 index 2152a06532..0000000000 --- a/.mokogitea/CLAUDE.md +++ /dev/null @@ -1,42 +0,0 @@ -# MokoGitea - -Fork of Gitea -- self-hosted Git service at git.mokoconsulting.tech. Go backend + TypeScript frontend. - -## Quick Reference - -| Field | Value | -|---|---| -| **Language** | Go 1.26+ / TypeScript | -| **Module** | `code.mokoconsulting.tech/MokoConsulting/MokoGitea` | -| **Branch** | develop on `dev`, merge to `main` (protected) | -| **Wiki** | [MokoGitea Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/wiki) | - -## Commands - -```bash -make help # List all available targets -make fmt # Format .go files -make lint-go # Lint Go code -make lint-js # Lint TypeScript -make tidy # After go.mod changes -make build # Build binary - -# Testing -go test -run '^TestName$' ./modulepath/ # Single Go test -pnpm exec vitest # Single JS test -GITEA_TEST_E2E_FLAGS='' make test-e2e # Single Playwright test -``` - -## Rules - -- Add current year copyright header on new `.go` files -- No trailing whitespace in edited files -- Conventional Commits for commit messages and PR titles -- Never force-push, amend, or squash unless asked -- use new commits -- Preserve existing code comments -- TypeScript: use `!` (non-null assertion) not `?.`/`??` when value is known to exist -- CSS: prefer `flex-*` helpers over per-child `tw-ml-*`/`tw-mr-*` margins -- Add `Co-Authored-By` lines to all commits -- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`) -- **Attribution**: `Authored-by: Moko Consulting` -- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/mokoplatform/wiki/Home) diff --git a/.mokogitea/ISSUE_TEMPLATE/adr.md b/.mokogitea/ISSUE_TEMPLATE/adr.md deleted file mode 100644 index eb40760acb..0000000000 --- a/.mokogitea/ISSUE_TEMPLATE/adr.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -name: Architecture Decision Record (ADR) -about: Propose or document an architectural decision -title: '[ADR] ' -labels: 'architecture, decision' -assignees: '' - ---- - - -## ADR Number -ADR-XXXX - -## Status -- [ ] Proposed -- [ ] Accepted -- [ ] Deprecated -- [ ] Superseded by ADR-XXXX - -## Context -Describe the issue or problem that motivates this decision. - -## Decision -State the architecture decision and provide rationale. - -## Consequences -### Positive -- List positive consequences - -### Negative -- List negative consequences or trade-offs - -### Neutral -- List neutral aspects - -## Alternatives Considered -### Alternative 1 -- Description -- Pros -- Cons -- Why not chosen - -### Alternative 2 -- Description -- Pros -- Cons -- Why not chosen - -## Implementation Plan -1. Step 1 -2. Step 2 -3. Step 3 - -## Stakeholders -- **Decision Makers**: @user1, @user2 -- **Consulted**: @user3, @user4 -- **Informed**: team-name - -## Technical Details -### Architecture Diagram -``` -[Add diagram or link] -``` - -### Dependencies -- Dependency 1 -- Dependency 2 - -### Impact Analysis -- **Performance**: [Impact description] -- **Security**: [Impact description] -- **Scalability**: [Impact description] -- **Maintainability**: [Impact description] - -## Testing Strategy -- [ ] Unit tests -- [ ] Integration tests -- [ ] Performance tests -- [ ] Security tests - -## Documentation -- [ ] Architecture documentation updated -- [ ] API documentation updated -- [ ] Developer guide updated -- [ ] Runbook created - -## Migration Path -Describe how to migrate from current state to new architecture. - -## Rollback Plan -Describe how to rollback if issues occur. - -## Timeline -- **Proposal Date**: -- **Decision Date**: -- **Implementation Start**: -- **Expected Completion**: - -## References -- Related ADRs: -- External resources: -- RFCs: - -## Review Checklist -- [ ] Aligns with enterprise architecture principles -- [ ] Security implications reviewed -- [ ] Performance implications reviewed -- [ ] Cost implications reviewed -- [ ] Compliance requirements met -- [ ] Team consensus achieved diff --git a/.mokogitea/ISSUE_TEMPLATE/bug_report.md b/.mokogitea/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 38a16a7d89..0000000000 --- a/.mokogitea/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -name: Bug Report -about: Report a bug or issue with the project -title: '[BUG] ' -labels: 'bug' -assignees: '' - ---- - - -## Bug Description -A clear and concise description of what the bug is. - -## Steps to Reproduce -1. Go to '...' -2. Click on '...' -3. Scroll down to '...' -4. See error - -## Expected Behavior -A clear and concise description of what you expected to happen. - -## Actual Behavior -A clear and concise description of what actually happened. - -## Screenshots -If applicable, add screenshots to help explain your problem. - -## Environment -- **Project**: [e.g., MokoDoliTools, moko-cassiopeia] -- **Version**: [e.g., 1.2.3] -- **Platform**: [e.g., Dolibarr 18.0, Joomla 5.0] -- **PHP Version**: [e.g., 8.1] -- **Database**: [e.g., MySQL 8.0, PostgreSQL 14] -- **Browser** (if applicable): [e.g., Chrome 120, Firefox 121] -- **OS**: [e.g., Ubuntu 22.04, Windows 11] - -## Additional Context -Add any other context about the problem here. - -## Possible Solution -If you have suggestions on how to fix the issue, please describe them here. - -## Checklist -- [ ] I have searched for similar issues before creating this one -- [ ] I have provided all the requested information -- [ ] I have tested this on the latest stable version -- [ ] I have checked the documentation and couldn't find a solution diff --git a/.mokogitea/ISSUE_TEMPLATE/config.yml b/.mokogitea/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 4db6885dfa..0000000000 --- a/.mokogitea/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,18 +0,0 @@ ---- -blank_issues_enabled: true -contact_links: - - name: 💼 Enterprise Support - url: https://mokoconsulting.tech/enterprise - about: Enterprise-level support and consultation services - - name: 💬 Ask a Question - url: https://mokoconsulting.tech/ - about: Get help or ask questions through our website - - name: 📚 MokoStandards Documentation - url: https://code.mokoconsulting.tech/MokoConsulting/mokoplatform - about: View our coding standards and best practices - - name: 🔒 Report a Security Vulnerability - url: https://code.mokoconsulting.tech/mokoconsulting-tech/.github-private/security/advisories/new - about: Report security vulnerabilities privately (for critical issues) - - name: 💡 Community Discussions - url: https://github.com/orgs/mokoconsulting-tech/discussions - about: Join community discussions and Q&A diff --git a/.mokogitea/ISSUE_TEMPLATE/documentation.md b/.mokogitea/ISSUE_TEMPLATE/documentation.md deleted file mode 100644 index ed4dabc56d..0000000000 --- a/.mokogitea/ISSUE_TEMPLATE/documentation.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -name: Documentation Issue -about: Report an issue with documentation -title: '[DOCS] ' -labels: 'documentation' -assignees: '' - ---- - - -## Documentation Issue - -**Location**: - - -## Issue Type - -- [ ] Typo or grammar error -- [ ] Outdated information -- [ ] Missing documentation -- [ ] Unclear explanation -- [ ] Broken links -- [ ] Missing examples -- [ ] Other (specify below) - -## Description - - -## Current Content - -``` -Current text here -``` - -## Suggested Improvement - -``` -Suggested text here -``` - -## Additional Context - - -## Standards Alignment -- [ ] Follows MokoStandards documentation guidelines -- [ ] Uses en_US/en_GB localization -- [ ] Includes proper SPDX headers where applicable - -## Checklist -- [ ] I have searched for similar documentation issues -- [ ] I have provided a clear description -- [ ] I have suggested an improvement (if applicable) diff --git a/.mokogitea/ISSUE_TEMPLATE/feature_request.md b/.mokogitea/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 58097e3975..0000000000 --- a/.mokogitea/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -name: Feature Request -about: Suggest a new feature or enhancement -title: '(feat) ' -labels: 'enhancement' -assignees: '' - ---- - - -## Feature Description -A clear and concise description of the feature you'd like to see. - -## Problem or Use Case -Describe the problem this feature would solve or the use case it addresses. -Ex. I'm always frustrated when [...] - -## Proposed Solution -A clear and concise description of what you want to happen. - -## Alternative Solutions -A clear and concise description of any alternative solutions or features you've considered. - -## Benefits -Describe how this feature would benefit users: -- Who would use this feature? -- What problems does it solve? -- What value does it add? - -## Implementation Details (Optional) -If you have ideas about how this could be implemented, share them here: -- Technical approach -- Files/components that might need changes -- Any concerns or challenges you foresee - -## Additional Context -Add any other context, mockups, or screenshots about the feature request here. - -## Relevant Standards -Does this relate to any standards in [MokoStandards](https://code.mokoconsulting.tech/MokoConsulting/MokoStandards)? -- [ ] Accessibility (WCAG 2.1 AA) -- [ ] Localization (en_US/en_GB) -- [ ] Security best practices -- [ ] Code quality standards -- [ ] Other: [specify] - -## Checklist -- [ ] I have searched for similar feature requests before creating this one -- [ ] I have clearly described the use case and benefits -- [ ] I have considered alternative solutions -- [ ] This feature aligns with the project's goals and scope diff --git a/.mokogitea/ISSUE_TEMPLATE/question.md b/.mokogitea/ISSUE_TEMPLATE/question.md deleted file mode 100644 index 3175013bbb..0000000000 --- a/.mokogitea/ISSUE_TEMPLATE/question.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -name: Question -about: Ask a question about usage, features, or best practices -title: '[QUESTION] ' -labels: ['question'] -assignees: ['jmiller'] ---- - - -## Question - -**Your question:** - - -## Context - -**What are you trying to accomplish?** - - -**What have you already tried?** - - -**Category**: -- [ ] Script usage -- [ ] Configuration -- [ ] Workflow setup -- [ ] Documentation interpretation -- [ ] Best practices -- [ ] Integration -- [ ] Other: __________ - -## Environment (if relevant) - -**Your setup**: -- Operating System: -- Version: - -## What You've Researched - -**Documentation reviewed**: -- [ ] README.md -- [ ] Project documentation -- [ ] Other (specify): __________ - -**Similar issues/questions found**: -- # -- # - -## Expected Outcome - -**What result are you hoping for?** - - -## Code/Configuration Samples - -**Relevant code or configuration** (if applicable): - -```bash -# Your code here -``` - -## Additional Context - -**Any other relevant information:** - - -**Screenshots** (if helpful): - - -## Urgency - -- [ ] Urgent (blocking work) -- [ ] Normal (can work on other things meanwhile) -- [ ] Low priority (just curious) - -## Checklist - -- [ ] I have searched existing issues and discussions -- [ ] I have reviewed relevant documentation -- [ ] I have provided sufficient context -- [ ] I have included code/configuration samples if relevant -- [ ] This is a genuine question (not a bug report or feature request) diff --git a/.mokogitea/ISSUE_TEMPLATE/rfc.md b/.mokogitea/ISSUE_TEMPLATE/rfc.md deleted file mode 100644 index 6f09af788c..0000000000 --- a/.mokogitea/ISSUE_TEMPLATE/rfc.md +++ /dev/null @@ -1,126 +0,0 @@ ---- -name: Request for Comments (RFC) -about: Propose a significant change for community discussion -title: '[RFC] ' -labels: 'rfc, discussion' -assignees: '' - ---- - - -## RFC Summary -One-paragraph summary of the proposal. - -## Motivation -Why are we doing this? What use cases does it support? What is the expected outcome? - -## Detailed Design -### Overview -Provide a detailed explanation of the proposed change. - -### API Changes (if applicable) -```php -// Before -function oldApi($param1) { } - -// After -function newApi($param1, $param2) { } -``` - -### User Experience Changes -Describe how users will interact with this change. - -### Implementation Approach -High-level implementation strategy. - -## Drawbacks -Why should we *not* do this? - -## Alternatives -What other designs have been considered? What is the impact of not doing this? - -### Alternative 1 -- Description -- Trade-offs - -### Alternative 2 -- Description -- Trade-offs - -## Adoption Strategy -How will existing users adopt this? Is this a breaking change? - -### Migration Guide -```bash -# Steps to migrate -``` - -### Deprecation Timeline -- **Announcement**: -- **Deprecation**: -- **Removal**: - -## Unresolved Questions -- Question 1 -- Question 2 - -## Future Possibilities -What future work does this enable? - -## Impact Assessment -### Performance -Expected performance impact. - -### Security -Security considerations and implications. - -### Compatibility -- **Backward Compatible**: [Yes / No] -- **Breaking Changes**: [List] - -### Maintenance -Long-term maintenance considerations. - -## Community Input -### Stakeholders -- [ ] Core team -- [ ] Module developers -- [ ] End users -- [ ] Enterprise customers - -### Feedback Period -**Duration**: [e.g., 2 weeks] -**Deadline**: [date] - -## Implementation Timeline -### Phase 1: Design -- [ ] RFC discussion -- [ ] Design finalization -- [ ] Approval - -### Phase 2: Implementation -- [ ] Core implementation -- [ ] Tests -- [ ] Documentation - -### Phase 3: Release -- [ ] Beta release -- [ ] Feedback collection -- [ ] Stable release - -## Success Metrics -How will we measure success? -- Metric 1 -- Metric 2 - -## References -- Related RFCs: -- External documentation: -- Prior art: - -## Open Questions for Community -1. Question 1? -2. Question 2? - ---- -**Note**: This RFC is open for community discussion. Please provide feedback in the comments below. diff --git a/.mokogitea/ISSUE_TEMPLATE/security.md b/.mokogitea/ISSUE_TEMPLATE/security.md deleted file mode 100644 index f2ebd5623c..0000000000 --- a/.mokogitea/ISSUE_TEMPLATE/security.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -name: Security Vulnerability Report -about: Report a security vulnerability (use only for non-critical issues) -title: '[SECURITY] ' -labels: 'security' -assignees: '' - ---- - - -## ⚠️ IMPORTANT: Private Disclosure Required - -**For critical security vulnerabilities, DO NOT use this template.** -Follow the process in [SECURITY.md](../SECURITY.md) for responsible disclosure. - -Use this template only for: -- Security improvements -- Non-critical security suggestions -- Security documentation updates - ---- - -## Security Issue - -**Severity**: - - -## Description - - -## Affected Components - - -## Suggested Mitigation - - -## Standards Reference -Does this relate to security standards in [MokoStandards](https://code.mokoconsulting.tech/MokoConsulting/MokoStandards)? -- [ ] SPDX license identifiers -- [ ] Secret management -- [ ] Dependency security -- [ ] Access control -- [ ] Other: [specify] - -## Additional Context - - -## Checklist -- [ ] This is NOT a critical vulnerability requiring private disclosure -- [ ] I have reviewed the SECURITY.md policy -- [ ] I have provided sufficient detail for evaluation diff --git a/.mokogitea/ISSUE_TEMPLATE/test-mokogitea.md b/.mokogitea/ISSUE_TEMPLATE/test-mokogitea.md deleted file mode 100644 index f5f3e71e8b..0000000000 --- a/.mokogitea/ISSUE_TEMPLATE/test-mokogitea.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: ".mokogitea Test Template" -about: "Verify .mokogitea issue templates work" -labels: ["test"] ---- - -This template was loaded from `.mokogitea/ISSUE_TEMPLATE/`. - -If you can see this, the `.mokogitea` dot-folder feature is working. diff --git a/.mokogitea/ISSUE_TEMPLATE/version.md b/.mokogitea/ISSUE_TEMPLATE/version.md deleted file mode 100644 index 632842171e..0000000000 --- a/.mokogitea/ISSUE_TEMPLATE/version.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -name: Version Bump -about: Request or track a version change -title: '[VERSION] ' -labels: 'version, type: version' -assignees: 'jmiller' ---- - -## Version Change - -**Current version**: -**Requested version**: -**Change type**: - -## Reason - - - -## Checklist - -- [ ] README.md `VERSION:` field updated -- [ ] CHANGELOG.md entry added -- [ ] Module descriptor version updated (Dolibarr: `$this->version`, Joomla: ``) -- [ ] All file headers will be auto-propagated by `sync-version-on-merge` workflow diff --git a/.mokogitea/branch-protection.yml b/.mokogitea/branch-protection.yml deleted file mode 100644 index 31a2729c66..0000000000 --- a/.mokogitea/branch-protection.yml +++ /dev/null @@ -1,251 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# SPDX-License-Identifier: GPL-3.0-or-later -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: mokoplatform.Automation -# REPO: https://code.mokoconsulting.tech/MokoConsulting/mokoplatform -# PATH: /.gitea/workflows/branch-protection.yml -# BRIEF: Apply standardised branch protection rules to all governed repositories -# -# +========================================================================+ -# | BRANCH PROTECTION SETUP | -# +========================================================================+ -# | | -# | Applies protection rules for: main, dev, rc, beta, alpha | -# | | -# | main — Require PR, block rejected reviews, no force push | -# | dev — Allow push, no force push, no delete | -# | rc — Allow push, no force push, no delete | -# | beta — Allow push, no force push, no delete | -# | alpha — Allow push, no force push, no delete | -# | | -# | jmiller has override authority on all branches. | -# | | -# +========================================================================+ - -name: Branch Protection Setup - -on: - schedule: - - cron: '0 2 * * 1' # Weekly Monday 02:00 UTC - workflow_dispatch: - inputs: - dry_run: - description: 'Preview mode (no changes)' - required: false - type: boolean - default: false - repos: - description: 'Comma-separated repo names (empty = all governed repos)' - required: false - type: string - default: '' - -env: - GITEA_URL: https://code.mokoconsulting.tech - GITEA_ORG: MokoConsulting - -permissions: - contents: read - -jobs: - protect: - name: Apply Branch Protection Rules - runs-on: ubuntu-latest - - steps: - - name: Determine target repos - id: repos - env: - MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - run: | - API="${GITEA_URL}/api/v1" - - # Platform/standards/infra repos to exclude - EXCLUDE="gitea-org-config org-profile gitea-private .mokogitea-private MokoStandards mokoplatform MokoTesting" - EXCLUDE="$EXCLUDE MokoStandards-Template-Client MokoStandards-Template-Dolibarr MokoStandards-Template-Generic MokoStandards-Template-Joomla MokoDoliProjTemplate" - - if [ -n "${{ inputs.repos }}" ]; then - # User-specified repos - REPOS=$(echo "${{ inputs.repos }}" | tr ',' ' ') - else - # Fetch all org repos - PAGE=1 - REPOS="" - while true; do - BATCH=$(curl -sS \ - -H "Authorization: token ${MOKOGITEA_TOKEN}" \ - "${API}/orgs/${GITEA_ORG}/repos?page=${PAGE}&limit=50" \ - | jq -r '.[].name // empty') - [ -z "$BATCH" ] && break - REPOS="$REPOS $BATCH" - PAGE=$((PAGE + 1)) - done - - # Filter out excluded repos - FILTERED="" - for REPO in $REPOS; do - SKIP=false - for EX in $EXCLUDE; do - if [ "$REPO" = "$EX" ]; then - SKIP=true - break - fi - done - if [ "$SKIP" = "false" ]; then - FILTERED="$FILTERED $REPO" - fi - done - REPOS="$FILTERED" - fi - - echo "repos=$REPOS" >> "$GITHUB_OUTPUT" - COUNT=$(echo "$REPOS" | wc -w) - echo "📋 Target repos (${COUNT}): $REPOS" - - - name: Apply protection rules - env: - MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - DRY_RUN: ${{ inputs.dry_run || 'false' }} - run: | - API="${GITEA_URL}/api/v1" - REPOS="${{ steps.repos.outputs.repos }}" - - SUCCESS=0 - FAILED=0 - SKIPPED=0 - - # ── Rule definitions ────────────────────────────────────── - # Only the CI bot (jmiller token) can push directly. - # All human contributors must use PRs. - # Force push disabled on all branches. - - RULE_MAIN='{ - "rule_name": "main", - "enable_push": true, - "enable_push_whitelist": true, - "push_whitelist_usernames": ["jmiller"], - "enable_force_push": false, - "enable_force_push_allowlist": false, - "force_push_allowlist_usernames": [], - "enable_merge_whitelist": false, - "required_approvals": 0, - "dismiss_stale_approvals": true, - "block_on_rejected_reviews": true, - "block_on_outdated_branch": false, - "priority": 1 - }' - - RULE_DEV='{ - "rule_name": "dev", - "enable_push": true, - "enable_push_whitelist": true, - "push_whitelist_usernames": ["jmiller"], - "enable_force_push": false, - "enable_force_push_allowlist": false, - "force_push_allowlist_usernames": [], - "enable_merge_whitelist": false, - "required_approvals": 0, - "block_on_rejected_reviews": false, - "priority": 2 - }' - - RULE_RC='{ - "rule_name": "rc", - "enable_push": true, - "enable_push_whitelist": true, - "push_whitelist_usernames": ["jmiller"], - "enable_force_push": false, - "enable_force_push_allowlist": false, - "force_push_allowlist_usernames": [], - "enable_merge_whitelist": false, - "required_approvals": 0, - "block_on_rejected_reviews": false, - "priority": 3 - }' - - RULE_BETA='{ - "rule_name": "beta", - "enable_push": true, - "enable_push_whitelist": true, - "push_whitelist_usernames": ["jmiller"], - "enable_force_push": false, - "enable_force_push_allowlist": false, - "force_push_allowlist_usernames": [], - "enable_merge_whitelist": false, - "required_approvals": 0, - "block_on_rejected_reviews": false, - "priority": 4 - }' - - RULE_ALPHA='{ - "rule_name": "alpha", - "enable_push": true, - "enable_push_whitelist": true, - "push_whitelist_usernames": ["jmiller"], - "enable_force_push": false, - "enable_force_push_allowlist": false, - "force_push_allowlist_usernames": [], - "enable_merge_whitelist": false, - "required_approvals": 0, - "block_on_rejected_reviews": false, - "priority": 5 - }' - - RULES=("$RULE_MAIN" "$RULE_DEV" "$RULE_RC" "$RULE_BETA" "$RULE_ALPHA") - RULE_NAMES=("main" "dev" "rc" "beta" "alpha") - - # ── Apply rules to each repo ────────────────────────────── - for REPO in $REPOS; do - echo "" - echo "═══ ${REPO} ═══" - - for i in "${!RULES[@]}"; do - RULE="${RULES[$i]}" - NAME="${RULE_NAMES[$i]}" - - if [ "$DRY_RUN" = "true" ]; then - echo " [DRY RUN] Would apply rule: ${NAME}" - SKIPPED=$((SKIPPED + 1)) - continue - fi - - # Delete existing rule if present (idempotent recreate) - ENCODED_NAME=$(echo "$NAME" | sed 's|/|%2F|g') - curl -sS -o /dev/null -w "" \ - -X DELETE \ - -H "Authorization: token ${MOKOGITEA_TOKEN}" \ - "${API}/repos/${GITEA_ORG}/${REPO}/branch_protections/${ENCODED_NAME}" 2>/dev/null || true - - # Create rule - RESPONSE=$(curl -sS -w "\n%{http_code}" \ - -X POST \ - -H "Authorization: token ${MOKOGITEA_TOKEN}" \ - -H "Content-Type: application/json" \ - -d "$RULE" \ - "${API}/repos/${GITEA_ORG}/${REPO}/branch_protections") - - HTTP=$(echo "$RESPONSE" | tail -1) - BODY=$(echo "$RESPONSE" | sed '$d') - - if [ "$HTTP" = "201" ]; then - echo " ✅ ${NAME}" - SUCCESS=$((SUCCESS + 1)) - else - echo " ❌ ${NAME} (HTTP ${HTTP}): $(echo "$BODY" | jq -r '.message // .' 2>/dev/null | head -1)" - FAILED=$((FAILED + 1)) - fi - done - done - - # ── Summary ─────────────────────────────────────────────── - echo "" - echo "════════════════════════════════════════" - echo " ✅ Success: ${SUCCESS}" - echo " ❌ Failed: ${FAILED}" - echo " ⏭️ Skipped: ${SKIPPED}" - echo "════════════════════════════════════════" - - if [ "$FAILED" -gt 0 ]; then - echo "::warning::${FAILED} rule(s) failed to apply" - fi diff --git a/.mokogitea/issue_template.md b/.mokogitea/issue_template.md deleted file mode 100644 index cf173a67ca..0000000000 --- a/.mokogitea/issue_template.md +++ /dev/null @@ -1,42 +0,0 @@ - - - - -- Gitea version (or commit ref): -- Git version: -- Operating system: - - - -- Database (use `[x]`): - - [ ] PostgreSQL - - [ ] MySQL - - [ ] MSSQL - - [ ] SQLite -- Can you reproduce the bug at https://demo.gitea.com: - - [ ] Yes (provide example URL) - - [ ] No -- Log gist: - - - - -## Description - - -... - - -## Screenshots - - diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml deleted file mode 100644 index 12bbf0b4bc..0000000000 --- a/.mokogitea/workflows/auto-bump.yml +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: mokocli.Release -# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli -# 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 - MOKOGITEA_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 mokocli 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/mokocli/cli" ]; then - echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV" - else - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \ - /tmp/mokocli - cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet - echo "MOKO_CLI=/tmp/mokocli/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" diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml deleted file mode 100644 index 5865324d5a..0000000000 --- a/.mokogitea/workflows/auto-release.yml +++ /dev/null @@ -1,467 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: mokocli.Release -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli -# 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, synchronize, closed] - branches: - - main - paths-ignore: - - '.mokogitea/workflows/**' - - '*.md' - - 'wiki/**' - - '.editorconfig' - - '.gitignore' - - '.gitattributes' - - '.gitmessage' - - 'LICENSE' - 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 - MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} - -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.action == 'synchronize' && 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 mokocli tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - run: | - if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then - echo Using pre-installed /opt/mokocli - echo MOKO_CLI=/opt/mokocli/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/mokocli - CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git - git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli - cd /tmp/mokocli - composer install --no-dev --no-interaction --quiet - echo MOKO_CLI=/tmp/mokocli/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 "${MOKOGITEA_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: Update RC release notes from CHANGELOG.md - run: | - API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - - # Extract [Unreleased] section from changelog - NOTES="" - if [ -f "CHANGELOG.md" ]; then - NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) - fi - [ -z "$NOTES" ] && NOTES="Release candidate" - - # Find the RC release and update its body - RELEASE_ID=$(curl -sf -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/releases/tags/release-candidate" \ - | 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 ${TOKEN}', - 'Content-Type': 'application/json' - }) - urllib.request.urlopen(req) - " <<< "$NOTES" - echo "RC release notes updated from CHANGELOG.md" - fi - - - 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 mokocli 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/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then - echo Using pre-installed /opt/mokocli - echo MOKO_CLI=/opt/mokocli/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/mokocli - CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git - git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli - cd /tmp/mokocli - composer install --no-dev --no-interaction --quiet - echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV - fi - - - name: "Detect platform" - id: platform - run: | - php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true - php ${MOKO_CLI}/manifest_read.php --path . --github-output 2>/dev/null || true - - - 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: "Read published version" - id: version - run: | - VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "") - VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') - [ -z "$VERSION" ] && VERSION="00.00.00" && echo "skip=true" >> "$GITHUB_OUTPUT" - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - PLATFORM="${{ steps.platform.outputs.platform }}" - if [[ "$PLATFORM" == joomla* ]]; then - echo "tag=stable" >> "$GITHUB_OUTPUT" - echo "release_tag=stable" >> "$GITHUB_OUTPUT" - else - echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT" - echo "release_tag=v${VERSION}" >> "$GITHUB_OUTPUT" - fi - echo "branch=main" >> "$GITHUB_OUTPUT" - echo "Published version: ${VERSION}" - - - name: "Create semver tag for non-Joomla repos" - id: semver - if: | - steps.version.outputs.skip != 'true' && - !startsWith(steps.platform.outputs.platform, 'joomla') - run: | - VERSION="${{ steps.version.outputs.version }}" - API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - SEMVER_TAG="v${VERSION}" - - echo "Creating semver tag: ${SEMVER_TAG}" - - # Create the git tag via API - HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" \ - -X POST -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/json" \ - "${API_BASE}/tags" \ - -d "{\"tag_name\":\"${SEMVER_TAG}\",\"target\":\"main\",\"message\":\"Release ${VERSION}\"}" 2>/dev/null || echo "000") - - if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then - echo "Created semver tag: ${SEMVER_TAG}" - elif [ "$HTTP_CODE" = "409" ]; then - echo "Semver tag ${SEMVER_TAG} already exists (skipped)" - else - echo "::warning::Failed to create semver tag ${SEMVER_TAG} (HTTP ${HTTP_CODE})" - fi - - echo "semver_tag=${SEMVER_TAG}" >> "$GITHUB_OUTPUT" - - - name: Update release notes and promote changelog - run: | - API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - - # Get the stable release info (version and ID) - RELEASE_JSON=$(curl -sf -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/releases/tags/stable" 2>/dev/null || echo '{}') - RELEASE_ID=$(python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" <<< "$RELEASE_JSON" 2>/dev/null || true) - # Extract version from release name (e.g. "06.17.00" or "v06.17.00") - VERSION=$(python3 -c " - import json, sys, re - r = json.load(sys.stdin) - name = r.get('name', '') - m = re.search(r'(\d+\.\d+\.\d+)', name) - print(m.group(1) if m else '') - " <<< "$RELEASE_JSON" 2>/dev/null || true) - - # Extract [Unreleased] section from changelog - NOTES="" - if [ -f "CHANGELOG.md" ]; then - NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) - fi - [ -z "$NOTES" ] && NOTES="Stable release" - - # Update release body via API - 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 ${TOKEN}', - 'Content-Type': 'application/json' - }) - urllib.request.urlopen(req) - " <<< "$NOTES" - echo "Release notes updated from CHANGELOG.md" - fi - - # Promote [Unreleased] → [version] in CHANGELOG.md and reset - if [ -n "$VERSION" ] && [ -f "CHANGELOG.md" ]; then - DATE=$(date +%Y-%m-%d) - python3 -c " - import sys - version, date = sys.argv[1], sys.argv[2] - content = open('CHANGELOG.md').read() - old = '## [Unreleased]' - new = f'## [Unreleased]\n\n## [{version}] --- {date}' - content = content.replace(old, new, 1) - open('CHANGELOG.md', 'w').write(content) - " "$VERSION" "$DATE" - git add CHANGELOG.md - git commit -m "chore: promote changelog [Unreleased] → [${VERSION}]" || true - git push origin main || true - echo "Changelog promoted: [Unreleased] → [${VERSION}]" - 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="${MOKOGITEA_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="${MOKOGITEA_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="${MOKOGITEA_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="${MOKOGITEA_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](${MOKOGITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY - fi diff --git a/.mokogitea/workflows/branch-cleanup.yml b/.mokogitea/workflows/branch-cleanup.yml deleted file mode 100644 index 9d884e70a3..0000000000 --- a/.mokogitea/workflows/branch-cleanup.yml +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: MokoStandards.Universal -# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli -# PATH: /.mokogitea/workflows/branch-cleanup.yml -# VERSION: 01.00.00 -# BRIEF: Delete feature branches after PR merge - -name: "Branch Cleanup" - -on: - pull_request: - types: [closed] - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - cleanup: - name: Delete merged branch - runs-on: ubuntu-latest - if: >- - github.event.pull_request.merged == true && - github.event.pull_request.head.ref != 'dev' && - github.event.pull_request.head.ref != 'main' - - steps: - - name: Delete source branch - run: | - BRANCH="${{ github.event.pull_request.head.ref }}" - API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches" - ENCODED=$(php -r "echo rawurlencode('${BRANCH}');") - - STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \ - -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ - "${API}/${ENCODED}" 2>/dev/null || true) - - if [ "$STATUS" = "204" ]; then - echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY - elif [ "$STATUS" = "404" ]; then - echo "Branch already deleted: ${BRANCH}" >> $GITHUB_STEP_SUMMARY - else - echo "::warning::Failed to delete branch ${BRANCH} (HTTP ${STATUS})" - fi diff --git a/.mokogitea/workflows/cascade-dev.yml b/.mokogitea/workflows/cascade-dev.yml deleted file mode 100644 index 5f7c1d7273..0000000000 --- a/.mokogitea/workflows/cascade-dev.yml +++ /dev/null @@ -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" diff --git a/.mokogitea/workflows/ci-generic.yml b/.mokogitea/workflows/ci-generic.yml deleted file mode 100644 index 92d26853d1..0000000000 --- a/.mokogitea/workflows/ci-generic.yml +++ /dev/null @@ -1,197 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# 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: - 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 diff --git a/.mokogitea/workflows/ci-issue-reporter.yml b/.mokogitea/workflows/ci-issue-reporter.yml deleted file mode 100644 index 7ad19c8a7a..0000000000 --- a/.mokogitea/workflows/ci-issue-reporter.yml +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: mokocli.Universal -# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli -# PATH: /.mokogitea/workflows/ci-issue-reporter.yml -# VERSION: 01.00.00 -# BRIEF: Reusable workflow — creates/updates a Gitea issue when a CI gate fails. -# Clones MokoCLI and runs cli/ci_issue_reporter.sh. - -name: "Universal: CI Issue Reporter" - -on: - workflow_call: - inputs: - gate: - description: "CI gate name (e.g. PR Validation, Repository Health)" - required: true - type: string - details: - description: "Human-readable failure description" - required: true - type: string - severity: - description: "error or warning" - required: false - type: string - default: "error" - workflow: - description: "Workflow name for the issue title" - required: false - type: string - default: "" - secrets: - MOKOGITEA_TOKEN: - required: true - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - report: - name: "Report: ${{ inputs.gate }}" - runs-on: ubuntu-latest - - steps: - - name: Clone MokoCLI - env: - MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - run: | - MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}" - git clone --depth 1 --filter=blob:none --sparse "${MOKOGITEA_URL}/MokoConsulting/MokoCLI.git" /tmp/mokocli - cd /tmp/mokocli && git sparse-checkout set cli/ci_issue_reporter.sh - - - name: Report CI failure - env: - MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - run: | - chmod +x /tmp/mokocli/cli/ci_issue_reporter.sh - /tmp/mokocli/cli/ci_issue_reporter.sh \ - --gate "${{ inputs.gate }}" \ - --details "${{ inputs.details }}" \ - --severity "${{ inputs.severity }}" \ - --workflow "${{ inputs.workflow }}" diff --git a/.mokogitea/workflows/cleanup.yml b/.mokogitea/workflows/cleanup.yml deleted file mode 100644 index 002386274d..0000000000 --- a/.mokogitea/workflows/cleanup.yml +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# 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: - MOKOGITEA_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.MOKOGITEA_TOKEN }} - - - name: Delete merged branches - env: - MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - run: | - echo "=== Merged Branch Cleanup ===" - API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}" - - # List branches via API - BRANCHES=$(curl -sS -H "Authorization: token ${MOKOGITEA_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 ${MOKOGITEA_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: - MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - run: | - echo "=== Workflow Run Cleanup ===" - API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}" - CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ) - - # Get old completed runs - RUNS=$(curl -sS -H "Authorization: token ${MOKOGITEA_TOKEN}" \ - "${API}/actions/runs?status=completed&limit=50" | \ - jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null) - - DELETED=0 - for RUN_ID in $RUNS; do - curl -sS -X DELETE -H "Authorization: token ${MOKOGITEA_TOKEN}" \ - "${API}/actions/runs/${RUN_ID}" 2>/dev/null || true - DELETED=$((DELETED + 1)) - done - - echo "Deleted ${DELETED} old workflow run(s)" diff --git a/.mokogitea/workflows/custom/cascade-dev.yml b/.mokogitea/workflows/custom/cascade-dev.yml deleted file mode 100644 index 5f7c1d7273..0000000000 --- a/.mokogitea/workflows/custom/cascade-dev.yml +++ /dev/null @@ -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" diff --git a/.mokogitea/workflows/custom/deploy-dev.yml b/.mokogitea/workflows/custom/deploy-dev.yml deleted file mode 100644 index 84159835f7..0000000000 --- a/.mokogitea/workflows/custom/deploy-dev.yml +++ /dev/null @@ -1,109 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# SPDX-License-Identifier: GPL-3.0-or-later -# BRIEF: Build and deploy MokoGitea to dev environment on push to dev branch. -# Production deploy (deploy-mokogitea.yml) only succeeds if dev is healthy. - -name: Deploy MokoGitea (Dev) - -on: - push: - branches: - - dev - -concurrency: - group: deploy-mokogitea-dev - cancel-in-progress: true - -env: - REGISTRY: git.mokoconsulting.tech - IMAGE: mokoconsulting/mokogitea - DEPLOY_HOST: git.mokoconsulting.tech - DEPLOY_PORT: 2918 - DEPLOY_USER: mokoconsulting - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - deploy-dev: - name: "Build & Deploy to Dev" - runs-on: ubuntu-latest - steps: - - name: Checkout source - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Determine version - id: config - run: | - VERSION=$(git describe --tags --always 2>/dev/null || echo "dev-$(git rev-parse --short HEAD)") - echo "tag=${VERSION}-dev" >> $GITHUB_OUTPUT - echo "Version: ${VERSION}-dev" - - - name: Write deploy key - env: - DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }} - run: | - mkdir -p ~/.ssh - echo "$DEPLOY_KEY" > ~/.ssh/deploy_key - chmod 600 ~/.ssh/deploy_key - - - name: Build and deploy to dev via SSH - env: - REGISTRY_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - TAG: ${{ steps.config.outputs.tag }} - run: | - HEALTH_FMT='${{ '{{' }}.State.Health.Status${{ '}}' }}' - - ssh -i ~/.ssh/deploy_key -p ${{ env.DEPLOY_PORT }} \ - -o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ - -o ServerAliveInterval=30 -o ServerAliveCountMax=10 \ - ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} bash -s </dev/null || true - docker image prune -af 2>/dev/null || true - - echo 'Pulling source...' - SOURCE_DIR=/opt/gitea-dev/source - if [ ! -d \$SOURCE_DIR/.git ]; then - git clone -b dev https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-Fork.git \$SOURCE_DIR - fi - cd \$SOURCE_DIR - git remote set-url origin https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-Fork.git 2>/dev/null || true - git fetch origin dev - git reset --hard origin/dev - - echo 'Building Docker image...' - docker build --no-cache --build-arg GOFLAGS='-p 1' \ - --tag ${{ env.REGISTRY }}/${{ env.IMAGE }}:\$TAG \ - -f Dockerfile . - - echo 'Pushing to registry...' - echo '\$REGISTRY_TOKEN' | docker login ${{ env.REGISTRY }} -u ${{ env.DEPLOY_USER }} --password-stdin - docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:\$TAG - - echo 'Restarting dev container...' - cd /opt/gitea-dev - sed -i "s|${{ env.IMAGE }}:[^ ]*|${{ env.IMAGE }}:\$TAG|" docker-compose.yml - docker compose up -d mokogitea-dev - - echo 'Health check...' - for i in 1 2 3 4 5 6 7 8; do - sleep 15 - if docker inspect --format='\$HEALTH_FMT' mokogitea-dev 2>/dev/null | grep -q healthy; then - echo 'Dev container healthy!' - exit 0 - fi - echo "Waiting... (attempt \$i/8)" - done - echo 'Health check failed' - docker logs mokogitea-dev --tail 20 - exit 1 - DEPLOY_EOF - - - name: Verify dev instance - run: | - sleep 5 - curl -sf https://git.dev.mokoconsulting.tech/api/healthz && echo " Dev API healthy" diff --git a/.mokogitea/workflows/custom/deploy-mokogitea.yml b/.mokogitea/workflows/custom/deploy-mokogitea.yml deleted file mode 100644 index 43a36f598d..0000000000 --- a/.mokogitea/workflows/custom/deploy-mokogitea.yml +++ /dev/null @@ -1,162 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# SPDX-License-Identifier: GPL-3.0-or-later -# BRIEF: Build MokoGitea Docker image, push to registry, and deploy - -name: Deploy MokoGitea - -on: - push: - branches: - - main - workflow_dispatch: - inputs: - version: - description: 'Version tag' - required: true - default: 'latest' - environment: - description: 'Target environment' - required: true - default: 'dev' - type: choice - options: - - dev - - production - -concurrency: - group: deploy-mokogitea - cancel-in-progress: false - -env: - REGISTRY: git.mokoconsulting.tech - IMAGE: mokoconsulting/mokogitea - DEPLOY_HOST: git.mokoconsulting.tech - DEPLOY_PORT: 2918 - DEPLOY_USER: mokoconsulting - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - name: Verify dev environment is healthy - run: | - echo "Checking git.dev.mokoconsulting.tech health..." - if curl -sf --max-time 10 https://git.dev.mokoconsulting.tech/api/healthz; then - echo " Dev environment is healthy — proceeding with production deploy" - else - echo "::error::Dev environment is NOT healthy — blocking production deploy" - echo "Deploy to dev first (push to dev branch) and verify it passes before merging to main." - exit 1 - fi - - - name: Checkout source (for version detection) - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Determine settings - id: config - run: | - if [ "${{ github.event_name }}" = "push" ]; then - VERSION=$(git describe --tags --always 2>/dev/null || echo "dev-$(git rev-parse --short HEAD)") - ENV="production" - else - VERSION="${{ github.event.inputs.version }}" - ENV="${{ github.event.inputs.environment }}" - fi - if [ "$ENV" = "production" ]; then - echo "compose_dir=/opt/gitea" >> $GITHUB_OUTPUT - echo "container=mokogitea" >> $GITHUB_OUTPUT - echo "source_dir=/opt/gitea/source" >> $GITHUB_OUTPUT - echo "branch=main" >> $GITHUB_OUTPUT - echo "tag=$VERSION" >> $GITHUB_OUTPUT - else - echo "compose_dir=/opt/gitea-dev" >> $GITHUB_OUTPUT - echo "container=mokogitea-dev" >> $GITHUB_OUTPUT - echo "source_dir=/opt/gitea-dev/source" >> $GITHUB_OUTPUT - echo "branch=dev" >> $GITHUB_OUTPUT - echo "tag=$VERSION-dev" >> $GITHUB_OUTPUT - fi - - - name: Write deploy key - env: - DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }} - run: | - mkdir -p ~/.ssh - echo "$DEPLOY_KEY" > ~/.ssh/deploy_key - chmod 600 ~/.ssh/deploy_key - - - name: Build and deploy via SSH - env: - REGISTRY_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - TAG: ${{ steps.config.outputs.tag }} - BRANCH: ${{ steps.config.outputs.branch }} - SOURCE_DIR: ${{ steps.config.outputs.source_dir }} - COMPOSE_DIR: ${{ steps.config.outputs.compose_dir }} - CONTAINER: ${{ steps.config.outputs.container }} - run: | - HEALTH_FMT='${{ '{{' }}.State.Health.Status${{ '}}' }}' - IMAGE_FMT='Image: ${{ '{{' }}.Config.Image${{ '}}' }}' - - ssh -i ~/.ssh/deploy_key -p ${{ env.DEPLOY_PORT }} \ - -o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ - -o ServerAliveInterval=30 -o ServerAliveCountMax=10 \ - ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} bash -s </dev/null || true - docker image prune -af 2>/dev/null || true - free -m | head -3 - - echo 'Pulling source...' - if [ ! -d $SOURCE_DIR/.git ]; then - git clone -b $BRANCH https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-Fork.git $SOURCE_DIR - fi - cd $SOURCE_DIR - # Ensure remote points to MokoGitea-Fork (not the upstream fork) - git remote set-url origin https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-Fork.git 2>/dev/null || true - git fetch origin $BRANCH - git reset --hard origin/$BRANCH - - echo 'Building Docker image...' - docker build --no-cache --build-arg GOFLAGS='-p 1' \ - --tag ${{ env.REGISTRY }}/${{ env.IMAGE }}:$TAG \ - --tag ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest \ - -f Dockerfile . - - echo 'Pushing to registry...' - echo '$REGISTRY_TOKEN' | docker login ${{ env.REGISTRY }} -u ${{ env.DEPLOY_USER }} --password-stdin - docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:$TAG - docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest - - echo 'Restarting container...' - cd $COMPOSE_DIR - sed -i 's|${{ env.IMAGE }}:[^ ]*|${{ env.IMAGE }}:$TAG|' docker-compose.yml - docker compose up -d $CONTAINER - - echo 'Health check...' - for i in 1 2 3 4 5 6 7 8; do - sleep 15 - if docker inspect --format='$HEALTH_FMT' $CONTAINER 2>/dev/null | grep -q healthy; then - echo 'Container healthy!' - docker inspect --format='$IMAGE_FMT' $CONTAINER - exit 0 - fi - echo "Waiting... (attempt \$i/8)" - done - echo 'Health check failed' - docker logs $CONTAINER --tail 20 - exit 1 - DEPLOY_EOF - - - name: Verify - run: | - sleep 5 - curl -sf https://${{ env.DEPLOY_HOST }}/api/healthz && echo " API healthy" - - - name: Notify on failure - if: failure() - run: echo "::error::Deploy failed for ${{ steps.config.outputs.tag }}" diff --git a/.mokogitea/workflows/custom/pr-rc-release.yml b/.mokogitea/workflows/custom/pr-rc-release.yml deleted file mode 100644 index 998a25a7f7..0000000000 --- a/.mokogitea/workflows/custom/pr-rc-release.yml +++ /dev/null @@ -1,170 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# SPDX-License-Identifier: GPL-3.0-or-later -# BRIEF: Auto-build RC release on PR to main, update RC update stream - -name: "PR RC Release" - -on: - pull_request: - types: [opened, synchronize] - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - REGISTRY: code.mokoconsulting.tech - IMAGE: mokoconsulting/mokogitea - -permissions: - contents: write - -jobs: - rc-release: - name: Build RC Release - runs-on: ubuntu-latest - steps: - - name: Check target branch - id: guard - env: - BASE_BRANCH: ${{ github.base_ref }} - run: | - echo "PR target: ${BASE_BRANCH}" - if [ "$BASE_BRANCH" != "main" ]; then - echo "skip=true" >> "$GITHUB_OUTPUT" - echo "Skipping RC — only for PRs targeting main" - else - echo "skip=false" >> "$GITHUB_OUTPUT" - fi - - - name: Checkout PR branch - if: steps.guard.outputs.skip != 'true' - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} - fetch-depth: 0 - - - name: Determine RC version - if: steps.guard.outputs.skip != 'true' - id: version - env: - PR_NUMBER: ${{ github.event.pull_request.number }} - run: | - BASE_VERSION=$(sed -n 's/.*\(.*\)<\/version>.*/\1/p' updates.xml | head -1) - [ -z "$BASE_VERSION" ] && BASE_VERSION="04.00.00" - RC_VERSION="${BASE_VERSION}-rc.${PR_NUMBER}" - RC_TAG="v1.26.1-moko.${RC_VERSION}" - echo "version=$RC_VERSION" >> "$GITHUB_OUTPUT" - echo "tag=$RC_TAG" >> "$GITHUB_OUTPUT" - echo "RC version: $RC_VERSION (tag: $RC_TAG)" - - - name: Update updates.xml RC channel - if: steps.guard.outputs.skip != 'true' - env: - RC_VERSION: ${{ steps.version.outputs.version }} - RC_TAG: ${{ steps.version.outputs.tag }} - PR_URL: ${{ github.event.pull_request.html_url }} - PR_NUM: ${{ github.event.pull_request.number }} - run: | - DOCKER_TAG="${REGISTRY}/${IMAGE}:${RC_TAG}" - - python3 << 'PYEOF' - import os, re - - rc_version = os.environ["RC_VERSION"] - rc_tag = os.environ["RC_TAG"] - pr_url = os.environ["PR_URL"] - pr_num = os.environ["PR_NUM"] - docker_tag = os.environ["REGISTRY"] + "/" + os.environ["IMAGE"] + ":" + rc_tag - - entry = f""" - MokoGitea - MokoGitea RC from PR #{pr_num} - mokogitea - application - {rc_version} - server - rc - {pr_url} - - {docker_tag} - - - - Moko Consulting - https://mokoconsulting.tech - """ - - content = open("updates.xml").read() - # Remove existing RC entry - content = re.sub( - r"\s*[\s\S]*?rc[\s\S]*?", - "", - content, - ) - # Insert before - content = content.replace("", entry + "\n") - open("updates.xml", "w").write(content) - print(f"Updated updates.xml with RC entry: {rc_version}") - PYEOF - - - name: Create RC release - if: steps.guard.outputs.skip != 'true' - env: - GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - RC_TAG: ${{ steps.version.outputs.tag }} - RC_VERSION: ${{ steps.version.outputs.version }} - PR_TITLE: ${{ github.event.pull_request.title }} - PR_URL: ${{ github.event.pull_request.html_url }} - PR_NUMBER: ${{ github.event.pull_request.number }} - HEAD_SHA: ${{ github.event.pull_request.head.sha }} - API_BASE: https://${{ env.REGISTRY }}/api/v1/repos/${{ github.repository }} - run: | - # Delete existing RC release/tag if present - curl -s -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \ - "${API_BASE}/releases/tags/${RC_TAG}" 2>/dev/null || true - curl -s -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \ - "${API_BASE}/tags/${RC_TAG}" 2>/dev/null || true - - # Create prerelease - python3 << PYEOF - import json, os, urllib.request - - api = os.environ["API_BASE"] - token = os.environ["GITEA_TOKEN"] - payload = json.dumps({ - "tag_name": os.environ["RC_TAG"], - "target_commitish": os.environ["HEAD_SHA"], - "name": f"RC: {os.environ['PR_TITLE']}", - "body": f"Release candidate from PR #{os.environ['PR_NUMBER']}\n\nPR: {os.environ['PR_URL']}\nDocker: docker pull {os.environ['REGISTRY']}/{os.environ['IMAGE']}:{os.environ['RC_TAG']}", - "draft": False, - "prerelease": True, - }).encode() - - req = urllib.request.Request( - f"{api}/releases", - data=payload, - headers={ - "Authorization": f"token {token}", - "Content-Type": "application/json", - }, - method="POST", - ) - with urllib.request.urlopen(req) as resp: - result = json.loads(resp.read()) - print(f"Created RC release: {result.get('tag_name')}") - PYEOF - - - name: Commit updates.xml - if: steps.guard.outputs.skip != 'true' - env: - GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - HEAD_REF: ${{ github.event.pull_request.head.ref }} - PR_NUM: ${{ github.event.pull_request.number }} - run: | - git config user.name "MokoGitea Bot" - git config user.email "deploy@mokoconsulting.tech" - git add updates.xml - if git diff --cached --quiet; then - echo "No changes to updates.xml" - else - git commit -m "chore(ci): update RC stream for PR #${PR_NUM}" - git push origin "HEAD:${HEAD_REF}" || echo "Push failed" - fi diff --git a/.mokogitea/workflows/custom/test-mokogitea.yml b/.mokogitea/workflows/custom/test-mokogitea.yml deleted file mode 100644 index a714044279..0000000000 --- a/.mokogitea/workflows/custom/test-mokogitea.yml +++ /dev/null @@ -1,12 +0,0 @@ -# Test workflow to verify .mokogitea/ directory is discovered -name: Test .mokogitea workflows - -on: - workflow_dispatch: - -jobs: - test: - runs-on: ubuntu-latest - steps: - - name: Verify .mokogitea - run: echo "This workflow ran from .mokogitea/workflows/ — feature works!" diff --git a/.mokogitea/workflows/custom/upstream-bug-sync.yml b/.mokogitea/workflows/custom/upstream-bug-sync.yml deleted file mode 100644 index 471674ce1d..0000000000 --- a/.mokogitea/workflows/custom/upstream-bug-sync.yml +++ /dev/null @@ -1,167 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# SPDX-License-Identifier: GPL-3.0-or-later -# BRIEF: Sync upstream Gitea bug fixes into MokoGitea issue tracker - -name: Upstream Bug Sync - -on: - schedule: - - cron: '0 8 * * *' # daily at 08:00 UTC - workflow_dispatch: - inputs: - days_back: - description: 'How many days back to scan (default: 7)' - required: false - default: '7' - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - sync: - runs-on: ubuntu-latest - steps: - - name: Sync upstream bugs - env: - GH_TOKEN: ${{ secrets.GH_MIRROR_TOKEN }} - MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKOGITEA_URL: https://code.mokoconsulting.tech - MOKOGITEA_REPO: MokoConsulting/MokoGitea - UPSTREAM_BRANCH: release/v1.26 - DAYS_BACK: ${{ github.event.inputs.days_back || '7' }} - run: | - python3 << 'PYEOF' - import json, os, re, sys, urllib.parse, urllib.request - from datetime import datetime, timedelta, timezone - - GH_TOKEN = os.environ["GH_TOKEN"] - MOKO_TOKEN = os.environ["MOKOGITEA_TOKEN"] - MOKO_URL = os.environ["MOKOGITEA_URL"] - MOKO_REPO = os.environ["MOKOGITEA_REPO"] - BRANCH = os.environ["UPSTREAM_BRANCH"] - DAYS = int(os.environ.get("DAYS_BACK", "7")) - - # Label IDs in MokoGitea - LABELS = { - "type_bug": 5757, "upstream": 5758, "security": 5032, - "critical": 5018, "high": 5019, "medium": 5020, "low": 5021, - } - - def gh_get(url): - req = urllib.request.Request(url, headers={ - "Authorization": f"token {GH_TOKEN}", - "Accept": "application/vnd.github.v3+json", - }) - with urllib.request.urlopen(req) as r: - return json.loads(r.read()) - - def moko_get(path): - req = urllib.request.Request(f"{MOKO_URL}/api/v1/{path}", headers={ - "Authorization": f"token {MOKO_TOKEN}", - }) - with urllib.request.urlopen(req) as r: - return json.loads(r.read()) - - def moko_post(path, data): - payload = json.dumps(data).encode() - req = urllib.request.Request(f"{MOKO_URL}/api/v1/{path}", - data=payload, method="POST", headers={ - "Authorization": f"token {MOKO_TOKEN}", - "Content-Type": "application/json", - }) - with urllib.request.urlopen(req) as r: - return json.loads(r.read()) - - # ── Step 1: Find recently merged upstream PRs ── - since = (datetime.now(timezone.utc) - timedelta(days=DAYS)).strftime("%Y-%m-%dT%H:%M:%SZ") - query = f"repo:go-gitea/gitea is:pr is:merged base:{BRANCH} merged:>={since}" - encoded = urllib.parse.quote(query) - print(f"Scanning: {query}") - - result = gh_get(f"https://api.github.com/search/issues?q={encoded}&per_page=100&sort=updated&order=desc") - total = result["total_count"] - print(f"Found {total} merged PRs in the last {DAYS} days") - - if total == 0: - print("Nothing to sync.") - sys.exit(0) - - # ── Step 2: Filter for bug/security fixes ── - bugs = [] - for pr in result["items"]: - title = pr["title"] - label_names = [l["name"].lower() for l in pr.get("labels", [])] - is_fix = title.lower().startswith("fix") - is_security = any("security" in l for l in label_names) or "[security]" in title.lower() - is_bug = any("bug" in l for l in label_names) - - if not (is_fix or is_security or is_bug): - continue - - refs = re.findall(r"#(\d+)", title) - severity = "critical" if is_security and "[security]" in title.lower() else \ - "high" if is_security else "medium" - - bugs.append({ - "number": pr["number"], "title": title, "url": pr["html_url"], - "severity": severity, "is_security": is_security, "refs": refs, - "merged": pr.get("pull_request", {}).get("merged_at", "")[:10], - }) - - print(f"Filtered to {len(bugs)} bug/security fixes") - if not bugs: - sys.exit(0) - - # ── Step 3: Collect already-tracked PR numbers ── - tracked = set() - for state in ["open", "closed"]: - try: - issues = moko_get(f"repos/{MOKO_REPO}/issues?state={state}&type=issues&limit=50&labels=upstream") - for iss in issues: - text = (iss.get("body") or "") + " " + (iss.get("title") or "") - tracked.update(re.findall(r"(?:#|/pull/)(\d{4,})", text)) - except Exception: - pass - - print(f"Already tracked: {len(tracked)} upstream PRs") - - # ── Step 4: Create issues for new bugs ── - created = skipped = errors = 0 - for bug in bugs: - if any(r in tracked for r in bug["refs"]): - print(f" SKIP #{bug['number']}: {bug['title'][:55]} (tracked)") - skipped += 1 - continue - - labels = [LABELS["type_bug"], LABELS["upstream"], LABELS[bug["severity"]]] - if bug["is_security"]: - labels.append(LABELS["security"]) - - body = ( - f"## Summary\n\n" - f"Upstream bug fix merged into `{BRANCH}`.\n\n" - f"## Upstream Reference\n\n" - f"- PR: {bug['url']}\n" - f"- Merged: {bug['merged']}\n" - f"- Branch: {BRANCH}\n\n" - f"## Severity: {bug['severity'].title()}" - f"{' (security)' if bug['is_security'] else ''}\n\n" - f"## Action\n\n" - f"Cherry-pick from upstream `{BRANCH}` branch.\n\n" - f"---\n" - f"*Auto-created by upstream-bug-sync workflow*\n" - f"*Authored-by: Claude Opus 4.6 (1M context) *" - ) - - try: - iss = moko_post(f"repos/{MOKO_REPO}/issues", { - "title": bug["title"], "body": body, "labels": labels, - }) - print(f" CREATED #{iss['number']}: {bug['title'][:55]}") - created += 1 - except Exception as e: - print(f" ERROR #{bug['number']}: {e}") - errors += 1 - - print(f"\n=== Done: {created} created, {skipped} skipped, {errors} errors ===") - PYEOF diff --git a/.mokogitea/workflows/deploy-manual.yml b/.mokogitea/workflows/deploy-manual.yml deleted file mode 100644 index 1af323c4ea..0000000000 --- a/.mokogitea/workflows/deploy-manual.yml +++ /dev/null @@ -1,126 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# 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: - MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }} - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }} - MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }} - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_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 diff --git a/.mokogitea/workflows/gitleaks.yml b/.mokogitea/workflows/gitleaks.yml deleted file mode 100644 index 196cf0c726..0000000000 --- a/.mokogitea/workflows/gitleaks.yml +++ /dev/null @@ -1,92 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# 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: - 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 diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml deleted file mode 100644 index 11958bdcd7..0000000000 --- a/.mokogitea/workflows/issue-branch.yml +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: mokocli.Automation -# VERSION: 01.00.00 -# BRIEF: Auto-create feature branch when an issue is opened - -name: "Universal: Issue Branch" - -on: - issues: - types: [opened] - -permissions: - contents: write - issues: write - -env: - MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - -jobs: - create-branch: - name: Create feature branch - runs-on: ubuntu-latest - steps: - - name: Create branch and comment - run: | - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}" - ISSUE_NUM="${{ github.event.issue.number }}" - ISSUE_TITLE="${{ github.event.issue.title }}" - - # Build slug from title: lowercase, replace non-alnum with dash, trim - SLUG=$(echo "${ISSUE_TITLE}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//' | cut -c1-40) - BRANCH="feature/${ISSUE_NUM}-${SLUG}" - - # Check dev branch exists - DEV_EXISTS=$(curl -sf -o /dev/null -w '%{http_code}' \ - -H "Authorization: token ${TOKEN}" \ - "${API}/branches/dev" 2>/dev/null || echo "000") - - if [ "${DEV_EXISTS}" != "200" ]; then - echo "No dev branch -- skipping" - exit 0 - fi - - # Create branch from dev - HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \ - -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/json" \ - "${API}/branches" \ - -d "{\"new_branch_name\":\"${BRANCH}\",\"old_branch_name\":\"dev\"}" 2>/dev/null || echo "000") - - if [ "${HTTP}" = "201" ]; then - echo "Created branch: ${BRANCH}" - - # Comment on issue with branch link - REPO_URL="${MOKOGITEA_URL}/${{ github.repository }}" - BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`" - - curl -sf -X POST \ - -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/json" \ - "${API}/issues/${ISSUE_NUM}/comments" \ - -d "{\"body\":\"${BODY}\"}" > /dev/null 2>&1 - - echo "Commented on issue #${ISSUE_NUM}" - else - echo "Failed to create branch (HTTP ${HTTP}) -- may already exist" - fi diff --git a/.mokogitea/workflows/notify.yml b/.mokogitea/workflows/notify.yml deleted file mode 100644 index 51dfcb5dd3..0000000000 --- a/.mokogitea/workflows/notify.yml +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# 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}" diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml deleted file mode 100644 index 21676fabcf..0000000000 --- a/.mokogitea/workflows/pr-check.yml +++ /dev/null @@ -1,531 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.CI -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform -# PATH: /templates/workflows/universal/pr-check.yml.template -# VERSION: 09.23.00 -# BRIEF: PR gate — branch policy + code validation before merge - -name: "Universal: PR Check" - -on: - pull_request: - types: [opened, synchronize, reopened, edited] - -permissions: - contents: read - pull-requests: write - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - # ── Branch Policy ────────────────────────────────────────────────────── - branch-policy: - name: Branch Policy - runs-on: ubuntu-latest - steps: - - name: Check branch merge target - run: | - HEAD="${{ github.head_ref }}" - BASE="${{ github.base_ref }}" - - echo "PR: ${HEAD} → ${BASE}" - - ALLOWED=true - REASON="" - - case "$HEAD" in - feature/*|feat/*) - if [ "$BASE" != "dev" ]; then - ALLOWED=false - REASON="Feature branches must target 'dev', not '${BASE}'" - fi - ;; - fix/*|bugfix/*) - if [ "$BASE" != "main" ] && [ "$BASE" != "dev" ]; then - ALLOWED=false - REASON="Fix branches must target 'main' or 'dev', not '${BASE}'" - fi - ;; - patch/*) - if [ "$BASE" != "main" ] && [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then - ALLOWED=false - REASON="Patch branches must target 'main', 'dev', or 'rc', not '${BASE}'" - fi - ;; - hotfix/*) - if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then - ALLOWED=false - REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'" - fi - ;; - rc) - if [ "$BASE" != "main" ]; then - ALLOWED=false - REASON="RC branch can only merge into 'main', not '${BASE}'" - fi - ;; - dev) - if [ "$BASE" != "main" ]; then - ALLOWED=false - REASON="Dev branch can only merge into 'main', not '${BASE}'" - fi - ;; - esac - - if [ "$ALLOWED" = false ]; then - echo "::error::${REASON}" - echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "${REASON}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY - echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY - echo "- \`fix/*\` → \`main\` or \`dev\`" >> $GITHUB_STEP_SUMMARY - echo "- \`patch/*\` → \`main\`, \`dev\`, or \`rc\`" >> $GITHUB_STEP_SUMMARY - echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY - echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY - echo "- \`rc\` → \`main\`" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - echo "Branch policy: OK (${HEAD} → ${BASE})" - echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY - - # ── Secret Scanning ────────────────────────────────────────────────── - gitleaks: - name: 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 - - - name: Scan PR commits for secrets - run: | - if gitleaks detect --source . --verbose \ - --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} 2>&1; then - echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY - else - echo "::error::Potential secrets detected in PR commits" - exit 1 - fi - - # ── Code Validation ──────────────────────────────────────────────────── - validate: - name: Validate PR - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - 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 in source files" - echo "## Conflict Markers Found" >> $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: Detect platform - id: platform - env: - MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - REPO: ${{ github.repository }} - run: | - # Query metadata API for platform (manifest.xml is deprecated) - PLATFORM="" - if [ -n "$MOKOGITEA_TOKEN" ]; then - PLATFORM=$(curl -sf -H "Authorization: token ${MOKOGITEA_TOKEN}" \ - "${MOKOGITEA_URL}/api/v1/repos/${REPO}/metadata" 2>/dev/null \ - | sed -n 's/.*"platform"\s*:\s*"\([^"]*\)".*/\1/p' | head -1) || true - fi - [ -z "$PLATFORM" ] && PLATFORM="generic" - echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" - echo "Detected platform: $PLATFORM" - - - name: Setup PHP - if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' - 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: PHP syntax check - if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' - run: | - ERRORS=0 - while IFS= read -r -d '' file; do - if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then - ERRORS=$((ERRORS + 1)) - fi - done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0) - echo "PHP lint: ${ERRORS} error(s)" - [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; } - - - name: Joomla JEXEC guard check - if: steps.platform.outputs.platform == 'joomla' - run: | - ERRORS=0 - while IFS= read -r -d '' file; do - # Skip vendor, node_modules, and index.html stub files - case "$file" in ./vendor/*|./node_modules/*) continue ;; esac - # Check first 10 lines for JEXEC or JPATH guard - if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then - echo "::error file=${file}::Missing JEXEC guard: ${file}" - ERRORS=$((ERRORS + 1)) - fi - done < <(find . -name "*.php" -path "*/src/*" -not -path "./.git/*" -not -path "./vendor/*" -print0) - if [ "$ERRORS" -gt 0 ]; then - echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard" - echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY - echo "${ERRORS} file(s) in src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY - exit 1 - fi - echo "JEXEC guard: OK" - - - name: Joomla directory listing protection - if: steps.platform.outputs.platform == 'joomla' - run: | - MISSING=0 - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && exit 0 - while IFS= read -r dir; do - if [ ! -f "${dir}/index.html" ]; then - echo "::warning::Missing index.html in ${dir} (directory listing protection)" - MISSING=$((MISSING + 1)) - fi - done < <(find "$SOURCE_DIR" -type d -not -path "./.git/*" -not -path "*/vendor/*" -not -path "*/node_modules/*") - if [ "$MISSING" -gt 0 ]; then - echo "## Directory Protection" >> $GITHUB_STEP_SUMMARY - echo "${MISSING} director(ies) missing index.html" >> $GITHUB_STEP_SUMMARY - fi - echo "Directory protection: ${MISSING} missing (advisory)" - - - name: Joomla script file and asset checks - if: steps.platform.outputs.platform == 'joomla' - run: | - ERRORS=0 - MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) - [ -z "$MANIFEST" ] && exit 0 - MANIFEST_DIR=$(dirname "$MANIFEST") - - # Check scriptfile exists if declared - SCRIPTFILE=$(sed -n 's/.*\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null) - if [ -n "$SCRIPTFILE" ]; then - if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then - echo "::error::Manifest declares ${SCRIPTFILE} but file not found at ${MANIFEST_DIR}/${SCRIPTFILE}" - ERRORS=$((ERRORS + 1)) - else - echo "Script file: ${MANIFEST_DIR}/${SCRIPTFILE} (OK)" - fi - fi - - # Require joomla.asset.json and validate it - ASSET_JSON=$(find "$MANIFEST_DIR" -name "joomla.asset.json" -not -path "./.git/*" 2>/dev/null | head -1) - if [ -z "$ASSET_JSON" ]; then - echo "::error::joomla.asset.json not found — Joomla asset system is required" - ERRORS=$((ERRORS + 1)) - else - if command -v php &> /dev/null; then - php -r "json_decode(file_get_contents('$ASSET_JSON')); if(json_last_error()!==JSON_ERROR_NONE){echo json_last_error_msg();exit(1);}" 2>&1 || { - echo "::error::joomla.asset.json is not valid JSON" - ERRORS=$((ERRORS + 1)) - } - fi - echo "joomla.asset.json: valid" - fi - - # Validate all XML files in src/ are well-formed - XML_ERRORS=0 - if command -v php &> /dev/null; then - while IFS= read -r -d '' xmlfile; do - if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$xmlfile'); if(!\$x){foreach(libxml_get_errors() as \$e) echo trim(\$e->message) . ' in $xmlfile'; exit(1);}" 2>&1; then - XML_ERRORS=$((XML_ERRORS + 1)) - fi - done < <(find "$MANIFEST_DIR" -name "*.xml" -not -path "./.git/*" -print0) - fi - if [ "$XML_ERRORS" -gt 0 ]; then - echo "::error::${XML_ERRORS} XML file(s) are malformed" - ERRORS=$((ERRORS + 1)) - else - echo "XML well-formedness: OK" - fi - - [ "$ERRORS" -gt 0 ] && exit 1 - echo "Joomla asset checks: OK" - - - name: Validate platform manifest - run: | - PLATFORM="${{ steps.platform.outputs.platform }}" - case "$PLATFORM" in - joomla) - MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) - if [ -z "$MANIFEST" ]; then - echo "::warning::No Joomla manifest found (WaaS site)" - exit 0 - fi - echo "Manifest: ${MANIFEST}" - if command -v php &> /dev/null; then - php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; } - fi - for ELEMENT in name version description; do - grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; } - done - # Block legacy raw/branch update server URLs on MokoGitea - RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true) - if [ -n "$RAW_URLS" ]; then - echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the Gitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)" - echo "$RAW_URLS" - exit 1 - fi - echo "Joomla manifest valid" - ;; - dolibarr) - MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1) - if [ -z "$MOD_FILE" ]; then - echo "::error::No mod*.class.php found" - exit 1 - fi - echo "Dolibarr module: ${MOD_FILE}" - ;; - *) - echo "Generic platform — no manifest validation" - ;; - esac - - - name: Check update stream format - run: | - PLATFORM="${{ steps.platform.outputs.platform }}" - case "$PLATFORM" in - joomla) - if [ -f "updates.xml" ]; then - if command -v php &> /dev/null; then - php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; } - fi - echo "updates.xml valid" - fi - ;; - dolibarr) - [ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt" - ;; - esac - - - name: Validate Joomla language files - if: steps.platform.outputs.platform == 'joomla' - run: | - ERRORS=0 - WARNINGS=0 - - # Require both en-GB and en-US language directories - LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1) - if [ -z "$LANG_ROOT" ]; then - echo "No language/ directory found — skipping" - exit 0 - fi - - if [ ! -d "$LANG_ROOT/en-GB" ]; then - echo "::error::Missing en-GB language directory (${LANG_ROOT}/en-GB)" - ERRORS=$((ERRORS + 1)) - fi - if [ ! -d "$LANG_ROOT/en-US" ]; then - echo "::error::Missing en-US language directory (${LANG_ROOT}/en-US)" - ERRORS=$((ERRORS + 1)) - fi - - # Check that en-GB and en-US have matching .ini files - if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then - for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do - [ ! -f "$GB_INI" ] && continue - US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")" - if [ ! -f "$US_INI" ]; then - echo "::error::$(basename "$GB_INI") exists in en-GB but missing from en-US" - ERRORS=$((ERRORS + 1)) - fi - done - for US_INI in "$LANG_ROOT/en-US"/*.ini; do - [ ! -f "$US_INI" ] && continue - GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")" - if [ ! -f "$GB_INI" ]; then - echo "::error::$(basename "$US_INI") exists in en-US but missing from en-GB" - ERRORS=$((ERRORS + 1)) - fi - done - fi - - # Find all .ini language files - INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null) - if [ -z "$INI_FILES" ]; then - echo "No .ini language files found" - [ "$ERRORS" -gt 0 ] && exit 1 - exit 0 - fi - - echo "Found $(echo "$INI_FILES" | wc -l) language file(s)" - - for FILE in $INI_FILES; do - FNAME=$(basename "$FILE") - LINENUM=0 - SEEN_KEYS="" - - while IFS= read -r line || [ -n "$line" ]; do - LINENUM=$((LINENUM + 1)) - - # Skip empty lines and comments - [ -z "$line" ] && continue - echo "$line" | grep -qE '^\s*;' && continue - echo "$line" | grep -qE '^\s*$' && continue - - # Must match KEY="VALUE" format - if ! echo "$line" | grep -qE '^[A-Z_][A-Z0-9_]*=".*"$'; then - echo "::error file=${FILE},line=${LINENUM}::Malformed line: ${line}" - ERRORS=$((ERRORS + 1)) - continue - fi - - # Extract key and check for duplicates - KEY=$(echo "$line" | sed 's/=.*//') - if echo "$SEEN_KEYS" | grep -qx "$KEY"; then - echo "::error file=${FILE},line=${LINENUM}::Duplicate key: ${KEY}" - ERRORS=$((ERRORS + 1)) - fi - SEEN_KEYS="${SEEN_KEYS} - ${KEY}" - done < "$FILE" - - echo " ${FILE}: checked ${LINENUM} lines" - done - - # Cross-check en-GB vs en-US key consistency - GB_DIR=$(find . -path "*/language/en-GB" -type d -not -path "./.git/*" 2>/dev/null | head -1) - US_DIR=$(find . -path "*/language/en-US" -type d -not -path "./.git/*" 2>/dev/null | head -1) - - if [ -n "$GB_DIR" ] && [ -n "$US_DIR" ]; then - for GB_FILE in "$GB_DIR"/*.ini; do - [ ! -f "$GB_FILE" ] && continue - FNAME=$(basename "$GB_FILE") - US_FILE="$US_DIR/$FNAME" - [ ! -f "$US_FILE" ] && continue - - GB_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$GB_FILE" 2>/dev/null | sort) - US_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$US_FILE" 2>/dev/null | sort) - - # Keys in en-GB but not en-US - MISSING_US=$(comm -23 <(echo "$GB_KEYS") <(echo "$US_KEYS")) - if [ -n "$MISSING_US" ]; then - echo "::warning::Keys in en-GB/$FNAME but missing from en-US/$FNAME:" - echo "$MISSING_US" | while read -r k; do echo " - $k"; done - WARNINGS=$((WARNINGS + 1)) - fi - - # Keys in en-US but not en-GB - MISSING_GB=$(comm -13 <(echo "$GB_KEYS") <(echo "$US_KEYS")) - if [ -n "$MISSING_GB" ]; then - echo "::warning::Keys in en-US/$FNAME but missing from en-GB/$FNAME:" - echo "$MISSING_GB" | while read -r k; do echo " - $k"; done - WARNINGS=$((WARNINGS + 1)) - fi - done - fi - - { - echo "### Language File Validation" - echo "| Metric | Count |" - echo "|---|---|" - echo "| Files checked | $(echo "$INI_FILES" | wc -l) |" - echo "| Errors | ${ERRORS} |" - echo "| Warnings | ${WARNINGS} |" - } >> $GITHUB_STEP_SUMMARY - - if [ "$ERRORS" -gt 0 ]; then - echo "::error::Language validation failed with ${ERRORS} error(s)" - exit 1 - fi - echo "Language files: OK (${WARNINGS} warning(s))" - - - name: Check changelog has unreleased entry - run: | - if [ ! -f "CHANGELOG.md" ]; then - echo "::warning::No CHANGELOG.md found" - exit 0 - fi - # Check for content under [Unreleased] section - if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then - echo "::error::CHANGELOG.md missing [Unreleased] section" - exit 1 - fi - # Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased - UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true) - if [ "$UNRELEASED_CONTENT" -eq 0 ]; then - echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes." - echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY - echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY - echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]" - - - name: Verify package source - run: | - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" - if [ ! -d "$SOURCE_DIR" ]; then - echo "::warning::No src/ or htdocs/ directory" - exit 0 - fi - FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l) - echo "Source: ${FILE_COUNT} files" - [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; } - - # ── Pre-Release RC Build ───────────────────────────────────────────────── - pre-release: - name: Build RC Package - runs-on: ubuntu-latest - needs: [branch-policy, validate] - - steps: - - name: Trigger RC pre-release - env: - MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - REPO: ${{ github.repository }} - BRANCH: ${{ github.head_ref }} - MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - run: | - curl -s -X POST "${MOKOGITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${MOKOGITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}" - echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY - echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY - - # ── Issue Reporter ────────────────────────────────────────────────────── - report-issues: - name: Report Issues - needs: [branch-policy, validate] - if: >- - always() && - needs.validate.result == 'failure' - uses: ./.mokogitea/workflows/ci-issue-reporter.yml - with: - gate: "PR Validation" - workflow: "PR Check" - severity: error - details: "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed." - secrets: inherit diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml deleted file mode 100644 index efb3d1b4f6..0000000000 --- a/.mokogitea/workflows/pre-release.yml +++ /dev/null @@ -1,273 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: mokocli.Release -# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli -# PATH: /templates/workflows/universal/pre-release.yml.template -# VERSION: 05.02.00 -# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches - -name: "Universal: Pre-Release" - -on: - push: - branches: - - dev - - 'fix/**' - - 'patch/**' - - 'hotfix/**' - - 'bugfix/**' - - 'chore/**' - - 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 }} - submodules: recursive - - - name: Update submodules to main - run: | - git submodule foreach --quiet 'git checkout main && git pull --quiet origin main' 2>/dev/null || true - - - name: Setup mokocli tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - run: | - # Use pre-installed /opt/mokocli if available (updated by cron every 6h) - if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/cli/manifest_element.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then - echo Using pre-installed /opt/mokocli - echo MOKO_CLI=/opt/mokocli/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/mokocli - CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git - git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli - cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet - echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV - fi - - - name: Detect platform - id: platform - run: | - # Auto-detect and update platform if not set in manifest - php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true - php ${MOKO_CLI}/manifest_read.php --path . --github-output - - - name: Check platform eligibility (Joomla only) - id: eligibility - run: | - PLATFORM="${{ steps.platform.outputs.platform }}" - if [[ "$PLATFORM" == joomla* ]] || [[ "$PLATFORM" == "joomla" ]]; then - echo "proceed=true" >> "$GITHUB_OUTPUT" - else - echo "proceed=false" >> "$GITHUB_OUTPUT" - echo "::notice::Platform '$PLATFORM' — non-Joomla, skipping pre-release auto-bump" - fi - - - name: Resolve metadata and bump version - id: meta - if: steps.eligibility.outputs.proceed == 'true' - 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 - if: steps.eligibility.outputs.proceed == 'true' - run: | - TAG="${{ steps.meta.outputs.tag }}" - VERSION="${{ steps.meta.outputs.version }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - 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 - if: steps.eligibility.outputs.proceed == 'true' - run: | - TAG="${{ steps.meta.outputs.tag }}" - VERSION="${{ steps.meta.outputs.version }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - - # 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 - if: steps.eligibility.outputs.proceed == 'true' - 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)" - if: steps.eligibility.outputs.proceed == 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - - 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 diff --git a/.mokogitea/workflows/rc-revert.yml b/.mokogitea/workflows/rc-revert.yml deleted file mode 100644 index 8271593839..0000000000 --- a/.mokogitea/workflows/rc-revert.yml +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: mokocli.Universal -# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli -# PATH: /.mokogitea/workflows/rc-revert.yml -# VERSION: 09.23.00 -# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge - -name: "RC Revert" - -on: - pull_request: - types: [closed] - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - revert: - name: Rename rc/ back to dev/ - runs-on: ubuntu-latest - if: >- - github.event.pull_request.merged == false && - startsWith(github.event.pull_request.head.ref, 'rc/') - - steps: - - name: Rename branch - env: - BRANCH: ${{ github.event.pull_request.head.ref }} - REPO: ${{ github.repository }} - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - run: | - set -euo pipefail - # BRANCH is attacker-controlled (PR head ref). Strict allowlist before ANY use. - if ! printf '%s' "$BRANCH" | grep -Eq '^rc/[A-Za-z0-9._/-]+$'; then - echo "::error::Refusing unsafe branch name: $BRANCH"; exit 1 - fi - SUFFIX="${BRANCH#rc/}" - DEV_BRANCH="dev/${SUFFIX}" - API="${GITEA_URL}/api/v1/repos/${REPO}/branches" - - # Create dev/ branch from rc/ branch - STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \ - -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/json" \ - -d "{\"new_branch_name\": \"${DEV_BRANCH}\", \"old_branch_name\": \"${BRANCH}\"}" \ - "${API}" 2>/dev/null || true) - if [ "$STATUS" = "201" ]; then - echo "Created branch: ${DEV_BRANCH}" >> "$GITHUB_STEP_SUMMARY" - else - echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})"; exit 1 - fi - - # Read BRANCH from the environment inside PHP (getenv, no string interpolation -> no PHP injection) - ENCODED=$(php -r 'echo rawurlencode(getenv("BRANCH"));') - STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \ - -H "Authorization: token ${TOKEN}" \ - "${API}/${ENCODED}" 2>/dev/null || true) - if [ "$STATUS" = "204" ]; then - echo "Deleted branch: ${BRANCH}" >> "$GITHUB_STEP_SUMMARY" - else - echo "::warning::Failed to delete ${BRANCH} (HTTP ${STATUS})" - fi - - echo "### RC Reverted" >> "$GITHUB_STEP_SUMMARY" - echo "${BRANCH} → ${DEV_BRANCH}" >> "$GITHUB_STEP_SUMMARY" diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml deleted file mode 100644 index 092b60e549..0000000000 --- a/.mokogitea/workflows/repo-health.yml +++ /dev/null @@ -1,700 +0,0 @@ -# ============================================================================ -# Copyright (C) 2025 Moko Consulting -# -# This file is part of a Moko Consulting project. -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: mokocli.Validation -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli -# PATH: /templates/workflows/joomla/repo_health.yml.template -# VERSION: 09.23.00 -# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts. -# ============================================================================ - -name: "Generic: Repo Health" - -defaults: - run: - shell: bash - -on: - workflow_dispatch: - inputs: - profile: - description: 'Validation profile: all, scripts, or repo' - required: true - default: all - type: choice - options: - - all - - scripts - - repo - pull_request: - branches: - - main - -permissions: - contents: read - -env: - # Scripts governance policy - SCRIPTS_REQUIRED_DIRS: - SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate - - # Repo health policy - REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.mokogitea/workflows/ - REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/ - REPO_DISALLOWED_DIRS: - REPO_DISALLOWED_FILES: TODO.md,todo.md - - # Extended checks toggles - EXTENDED_CHECKS: "true" - - # File / directory variables - DOCS_INDEX: docs/docs-index.md - SCRIPT_DIR: scripts - WORKFLOWS_DIR: .mokogitea/workflows - SHELLCHECK_PATTERN: '*.sh' - SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml' - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - access_check: - name: Access control - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - contents: read - - outputs: - allowed: ${{ steps.perm.outputs.allowed }} - permission: ${{ steps.perm.outputs.permission }} - - steps: - - name: Check actor permission (admin only) - id: perm - env: - TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }} - REPO: ${{ github.repository }} - ACTOR: ${{ github.actor }} - run: | - set -euo pipefail - ALLOWED=false - PERMISSION=unknown - METHOD="" - - # Hardcoded authorized users — always allowed - case "$ACTOR" in - jmiller|gitea-actions[bot]) - ALLOWED=true - PERMISSION=admin - METHOD="hardcoded allowlist" - ;; - *) - # Detect platform and check permissions via API - API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}" - RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}') - PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown") - if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "maintain" ] || [ "$PERMISSION" = "owner" ]; then - ALLOWED=true - fi - METHOD="collaborator API" - ;; - esac - - echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT" - echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT" - - { - echo "## Access Authorization" - echo "" - echo "| Field | Value |" - echo "|-------|-------|" - echo "| **Actor** | \`${ACTOR}\` |" - echo "| **Repository** | \`${REPO}\` |" - echo "| **Permission** | \`${PERMISSION}\` |" - echo "| **Method** | ${METHOD} |" - echo "| **Authorized** | ${ALLOWED} |" - echo "" - if [ "$ALLOWED" = "true" ]; then - echo "${ACTOR} authorized (${METHOD})" - else - echo "${ACTOR} is NOT authorized. Requires admin or maintain role." - fi - } >> "${GITHUB_STEP_SUMMARY}" - - - name: Deny execution when not permitted - if: ${{ steps.perm.outputs.allowed != 'true' }} - run: | - set -euo pipefail - printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}" - exit 1 - - scripts_governance: - name: Scripts governance - needs: access_check - if: ${{ needs.access_check.outputs.allowed == 'true' }} - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: read - - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - fetch-depth: 0 - - - name: Scripts folder checks - env: - PROFILE_RAW: ${{ github.event.inputs.profile }} - run: | - set -euo pipefail - - profile="${PROFILE_RAW:-all}" - case "${profile}" in - all|scripts|repo) ;; - *) - printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" - exit 1 - ;; - esac - - if [ "${profile}" = 'repo' ]; then - { - printf '%s\n' '### Scripts governance' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' 'Status: SKIPPED' - printf '%s\n' 'Reason: profile excludes scripts governance' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - exit 0 - fi - - if [ ! -d "${SCRIPT_DIR}" ]; then - { - printf '%s\n' '### Scripts governance' - printf '%s\n' 'Status: OK (advisory)' - printf '%s\n' 'scripts/ directory not present. No scripts governance enforced.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - exit 0 - fi - - if [ -n "${SCRIPTS_REQUIRED_DIRS:-}" ]; then IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"; else required_dirs=(); fi - IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}" - - missing_dirs=() - unapproved_dirs=() - - for d in "${required_dirs[@]}"; do - req="${d%/}" - [ ! -d "${req}" ] && missing_dirs+=("${req}/") - done - - while IFS= read -r d; do - allowed=false - for a in "${allowed_dirs[@]}"; do - a_norm="${a%/}" - [ "${d%/}" = "${a_norm}" ] && allowed=true - done - [ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/") - done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##') - - { - printf '%s\n' '### Scripts governance' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' '| Area | Status | Notes |' - printf '%s\n' '|---|---|---|' - - if [ "${#missing_dirs[@]}" -gt 0 ]; then - printf '%s\n' '| Required directories | Warning | Missing required subfolders |' - else - printf '%s\n' '| Required directories | OK | All required subfolders present |' - fi - - if [ "${#unapproved_dirs[@]}" -gt 0 ]; then - printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |' - else - printf '%s\n' '| Directory policy | OK | No unapproved directories |' - fi - - printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |' - printf '\n' - - if [ "${#missing_dirs[@]}" -gt 0 ]; then - printf '%s\n' 'Missing required script directories:' - for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - else - printf '%s\n' 'Missing required script directories: none.' - printf '\n' - fi - - if [ "${#unapproved_dirs[@]}" -gt 0 ]; then - printf '%s\n' 'Unapproved script directories detected:' - for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - else - printf '%s\n' 'Unapproved script directories detected: none.' - printf '\n' - fi - - printf '%s\n' 'Scripts governance completed in advisory mode.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - - repo_health: - name: Repository health - needs: access_check - if: ${{ needs.access_check.outputs.allowed == 'true' }} - runs-on: ubuntu-latest - timeout-minutes: 20 - permissions: - contents: read - - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - fetch-depth: 0 - - - name: Repository health checks - env: - PROFILE_RAW: ${{ github.event.inputs.profile }} - run: | - set -euo pipefail - - profile="${PROFILE_RAW:-all}" - case "${profile}" in - all|scripts|repo) ;; - *) - printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" - exit 1 - ;; - esac - - if [ "${profile}" = 'scripts' ]; then - { - printf '%s\n' '### Repository health' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' 'Status: SKIPPED' - printf '%s\n' 'Reason: profile excludes repository health' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - exit 0 - fi - - IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}" - IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}" - if [ -n "${REPO_DISALLOWED_DIRS:-}" ]; then IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"; else disallowed_dirs=(); fi - IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES:-}" - - missing_required=() - missing_optional=() - - # Source directory: src/ or htdocs/ (either is valid for extension repos) - SOURCE_DIR="" - if [ -d "src" ]; then - SOURCE_DIR="src" - elif [ -d "htdocs" ]; then - SOURCE_DIR="htdocs" - elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then - # Platform/tooling repos don't need src/ - SOURCE_DIR="" - else - missing_required+=("src/ or htdocs/ (source directory required)") - fi - - for item in "${required_artifacts[@]}"; do - if printf '%s' "${item}" | grep -q '/$'; then - d="${item%/}" - [ ! -d "${d}" ] && missing_required+=("${item}") - else - [ ! -f "${item}" ] && missing_required+=("${item}") - fi - done - - for f in "${optional_files[@]}"; do - if printf '%s' "${f}" | grep -q '/$'; then - d="${f%/}" - [ ! -d "${d}" ] && missing_optional+=("${f}") - else - [ ! -f "${f}" ] && missing_optional+=("${f}") - fi - done - - for d in "${disallowed_dirs[@]}"; do - d_norm="${d%/}" - [ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)") - done - - for f in "${disallowed_files[@]}"; do - [ -f "${f}" ] && missing_required+=("${f} (disallowed)") - done - - git fetch origin --prune - - dev_paths=() - dev_branches=() - - while IFS= read -r b; do - name="${b#origin/}" - if [ "${name}" = 'dev' ]; then - dev_branches+=("${name}") - else - dev_paths+=("${name}") - fi - done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//') - - if [ "${#dev_paths[@]}" -eq 0 ] && [ "${#dev_branches[@]}" -eq 0 ]; then - missing_required+=("dev or dev/* branch") - fi - - content_warnings=() - - if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then - content_warnings+=("CHANGELOG.md missing '# Changelog' header") - fi - - if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then - content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)") - fi - - if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then - content_warnings+=("LICENSE does not look like a GPL text") - fi - - if [ -f 'README.md' ] && ! grep -qiE 'moko|Moko' README.md; then - content_warnings+=("README.md missing expected brand keyword") - fi - - export PROFILE_RAW="${profile}" - export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")" - export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")" - export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")" - - report_json=$(printf '{"profile":"%s","missing_required":%d,"missing_optional":%d,"content_warnings":%d}' "$profile" "${#missing_required[@]}" "${#missing_optional[@]}" "${#content_warnings[@]}") - - { - printf '%s\n' '### Repository health' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' '| Metric | Value |' - printf '%s\n' '|---|---|' - printf '%s\n' "| Missing required | ${#missing_required[@]} |" - printf '%s\n' "| Missing optional | ${#missing_optional[@]} |" - printf '%s\n' "| Content warnings | ${#content_warnings[@]} |" - printf '\n' - - printf '%s\n' '### Guardrails report (JSON)' - printf '%s\n' '```json' - printf '%s\n' "${report_json}" - printf '%s\n' '```' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - - if [ "${#missing_required[@]}" -gt 0 ]; then - { - printf '%s\n' '### Missing required repo artifacts' - for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done - printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - exit 1 - fi - - if [ "${#missing_optional[@]}" -gt 0 ]; then - { - printf '%s\n' '### Missing optional repo artifacts' - for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - if [ "${#content_warnings[@]}" -gt 0 ]; then - { - printf '%s\n' '### Repo content warnings' - for m in "${content_warnings[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - # -- Joomla-specific checks -- - joomla_findings=() - - MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '/dev/null | head -1 || true)" - if [ -z "${MANIFEST}" ]; then - joomla_findings+=("Joomla XML manifest not found (no *.xml with tag)") - else - if ! grep -qP '' "${MANIFEST}"; then - joomla_findings+=("XML manifest: tag missing") - fi - if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then - joomla_findings+=("XML manifest: type attribute missing or invalid") - fi - if ! grep -qP '' "${MANIFEST}"; then - joomla_findings+=("XML manifest: tag missing") - fi - if ! grep -qP '' "${MANIFEST}"; then - joomla_findings+=("XML manifest: tag missing") - fi - if ! grep -qP ' missing (required for Joomla 5+)") - fi - fi - - INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)" - if [ "${INI_COUNT}" -eq 0 ]; then - joomla_findings+=("No .ini language files found") - fi - - if [ ! -f 'updates.xml' ]; then - joomla_findings+=("updates.xml missing in root (required for Joomla update server)") - fi - - if [ -n "${SOURCE_DIR}" ]; then - INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site") - for dir in "${INDEX_DIRS[@]}"; do - if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then - joomla_findings+=("${dir}/index.html missing (directory listing protection)") - fi - done - fi - - if [ "${#joomla_findings[@]}" -gt 0 ]; then - { - printf '%s\n' '### Joomla extension checks' - printf '%s\n' '| Check | Status |' - printf '%s\n' '|---|---|' - for f in "${joomla_findings[@]}"; do - printf '%s\n' "| ${f} | Warning |" - done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - else - { - printf '%s\n' '### Joomla extension checks' - printf '%s\n' 'All Joomla-specific checks passed.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - extended_enabled="${EXTENDED_CHECKS:-true}" - extended_findings=() - - if [ "${extended_enabled}" = 'true' ]; then - if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then - : - else - extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)") - fi - - if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then - bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)" - if [ -n "${bad_refs}" ]; then - extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt") - { - printf '%s\n' '### Workflow pinning advisory' - printf '%s\n' 'Found uses: entries pinned to main/master:' - printf '%s\n' '```' - printf '%s\n' "${bad_refs}" - printf '%s\n' '```' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - fi - - if [ -f "${DOCS_INDEX}" ]; then - missing_links="" - while IFS= read -r docline; do - for link in $(echo "$docline" | grep -oE '\]\([^)]+\)' | sed 's/\](//' | sed 's/)$//' || true); do - case "$link" in http://*|https://*|"#"*|mailto:*) continue ;; esac - linkpath="${link%%#*}" - linkpath="${linkpath%%\?*}" - [ -z "$linkpath" ] && continue - if [ "${linkpath:0:1}" = "/" ]; then - testpath="${linkpath#/}" - else - testpath="$(dirname "${DOCS_INDEX}")/${linkpath}" - fi - [ ! -e "$testpath" ] && missing_links="${missing_links}${testpath} " - done - done < "${DOCS_INDEX}" - if [ -n "${missing_links}" ]; then - extended_findings+=("docs/docs-index.md contains broken relative links") - { - printf '%s\n' '### Docs index link integrity' - printf '%s\n' 'Broken relative links:' - for bl in ${missing_links}; do - printf '%s\n' "- ${bl}" - done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - fi - - if [ -d "${SCRIPT_DIR}" ]; then - if ! command -v shellcheck >/dev/null 2>&1; then - sudo apt-get update -qq - sudo apt-get install -y shellcheck >/dev/null - fi - - sc_out='' - while IFS= read -r shf; do - [ -z "${shf}" ] && continue - out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)" - if [ -n "${out_one}" ]; then - sc_out="${sc_out}${out_one}\n" - fi - done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort) - - if [ -n "${sc_out}" ]; then - extended_findings+=("ShellCheck warnings detected (advisory)") - sc_head="$(printf '%s' "${sc_out}" | head -n 200)" - { - printf '%s\n' '### ShellCheck (advisory)' - printf '%s\n' '```' - printf '%s\n' "${sc_head}" - printf '%s\n' '```' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - fi - - spdx_missing=() - IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}" - spdx_args=() - for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done - - while IFS= read -r f; do - [ -z "${f}" ] && continue - if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then - spdx_missing+=("${f}") - fi - done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true) - - if [ "${#spdx_missing[@]}" -gt 0 ]; then - extended_findings+=("SPDX header missing in some tracked files (advisory)") - { - printf '%s\n' '### SPDX header advisory' - printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):' - for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - stale_cutoff_days=180 - stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 > days*86400) print $1}' | head -50)" - if [ -n "${stale_branches}" ]; then - extended_findings+=("Stale remote branches detected (advisory)") - { - printf '%s\n' '### Git hygiene advisory' - printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):" - while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}" - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - fi - - { - printf '%s\n' '### Guardrails coverage matrix' - printf '%s\n' '| Domain | Status | Notes |' - printf '%s\n' '|---|---|---|' - printf '%s\n' '| Access control | OK | Admin-only execution gate |' - printf '%s\n' '| Release policy | N/A | Releases handled by MokoGitea |' - printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |' - printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |' - printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |' - if [ "${extended_enabled}" = 'true' ]; then - if [ "${#extended_findings[@]}" -gt 0 ]; then - printf '%s\n' '| Extended checks | Warning | See extended findings below |' - else - printf '%s\n' '| Extended checks | OK | No findings |' - fi - else - printf '%s\n' '| Extended checks | SKIPPED | EXTENDED_CHECKS disabled |' - fi - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - - if [ "${extended_enabled}" = 'true' ] && [ "${#extended_findings[@]}" -gt 0 ]; then - { - printf '%s\n' '### Extended findings (advisory)' - for f in "${extended_findings[@]}"; do printf '%s\n' "- ${f}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}" - - - site-health: - name: Site Health - runs-on: ubuntu-latest - if: github.event_name == 'workflow_dispatch' - steps: - - uses: actions/checkout@v4 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.3' - - - name: Uptime check - if: env.URLS != '' - run: | - echo "$URLS" > /tmp/urls.txt - php monitoring/uptime-probe.php --urls /tmp/urls.txt --timeout 15 || echo "::warning::Some sites are down" - rm -f /tmp/urls.txt - env: - URLS: ${{ vars.MONITORED_URLS }} - - - name: SSL certificate check - if: env.DOMAINS != '' - run: | - echo "$DOMAINS" > /tmp/domains.txt - php monitoring/ssl-check.php --domains /tmp/domains.txt --warn-days 30 || echo "::warning::SSL certificates expiring soon" - rm -f /tmp/domains.txt - env: - DOMAINS: ${{ vars.MONITORED_DOMAINS }} - - - name: Summary - if: always() - run: | - echo "### Site Health" >> $GITHUB_STEP_SUMMARY - echo "Uptime and SSL checks completed." >> $GITHUB_STEP_SUMMARY - - # ═══════════════════════════════════════════════════════════════════════ - # Issue Reporter — file issues for failed gates - # ═══════════════════════════════════════════════════════════════════════ - report-scripts: - name: "Report: Scripts Governance" - needs: [access_check, scripts_governance] - if: >- - always() && - needs.scripts_governance.result == 'failure' - uses: ./.mokogitea/workflows/ci-issue-reporter.yml - with: - gate: "Scripts Governance" - workflow: "Repo Health" - severity: error - details: "Scripts directory policy violations detected. Review required and allowed directories." - secrets: inherit - - report-health: - name: "Report: Repository Health" - needs: [access_check, repo_health] - if: >- - always() && - needs.repo_health.result == 'failure' - uses: ./.mokogitea/workflows/ci-issue-reporter.yml - with: - gate: "Repository Health" - workflow: "Repo Health" - severity: error - details: "Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary." - secrets: inherit diff --git a/.mokogitea/workflows/version-set.yml b/.mokogitea/workflows/version-set.yml deleted file mode 100644 index 0bedeaae92..0000000000 --- a/.mokogitea/workflows/version-set.yml +++ /dev/null @@ -1,130 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow.Template -# INGROUP: MokoStandards.CI -# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla -# PATH: /.mokogitea/workflows/version-set.yml -# VERSION: 01.00.00 -# BRIEF: Set or reset the extension version across all version-bearing files - -name: "Joomla: Set Version" - -on: - workflow_dispatch: - inputs: - version: - description: "Version number (e.g. 01.00.00)" - required: true - type: string - branch: - description: "Branch to update (default: current)" - required: false - type: string - -permissions: - contents: write - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - set-version: - name: Set Version to ${{ inputs.version }} - runs-on: ubuntu-latest - - steps: - - name: Validate version format - run: | - VERSION="${{ inputs.version }}" - if ! echo "$VERSION" | grep -qP '^\d{2}\.\d{2}\.\d{2}$'; then - echo "::error::Invalid version format '${VERSION}' — expected XX.YY.ZZ (e.g. 01.00.00)" - exit 1 - fi - echo "VERSION=${VERSION}" >> "$GITHUB_ENV" - - - name: Checkout - uses: actions/checkout@v4 - with: - token: ${{ secrets.MOKOGITEA_TOKEN || github.token }} - ref: ${{ inputs.branch || github.ref }} - fetch-depth: 1 - - - name: Update manifest version - run: | - MANIFEST="" - for XML_FILE in $(find . -maxdepth 3 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do - if grep -q "/dev/null; then - MANIFEST="$XML_FILE" - break - fi - done - - if [ -z "$MANIFEST" ]; then - echo "::warning::No Joomla extension manifest found — skipping manifest update" - else - OLD_VER=$(grep -oP '\K[^<]+' "$MANIFEST" | head -1) - sed -i "s|${OLD_VER}|${VERSION}|" "$MANIFEST" - echo "Manifest: ${OLD_VER} → ${VERSION} (${MANIFEST})" - fi - - - name: Update README.md version - run: | - if [ -f "README.md" ]; then - if grep -qP '^\s*VERSION:\s*\d' README.md; then - sed -i -E "s/(VERSION:\s*)[0-9]{2}\.[0-9]{2}\.[0-9]{2}/\1${VERSION}/" README.md - echo "README.md version updated to ${VERSION}" - else - echo "::warning::No VERSION line found in README.md — skipping" - fi - fi - - - name: Update CHANGELOG.md - run: | - if [ -f "CHANGELOG.md" ]; then - DATE=$(date +%Y-%m-%d) - # Check if this version already has an entry - if grep -q "^\#\# \[${VERSION}\]" CHANGELOG.md; then - echo "CHANGELOG.md already has entry for ${VERSION} — skipping" - else - # Insert new version entry after [Unreleased] or at the top after header - if grep -q '^\#\# \[Unreleased\]' CHANGELOG.md; then - sed -i "/^\#\# \[Unreleased\]/a\\\\n## [${VERSION}] --- ${DATE}" CHANGELOG.md - else - sed -i "/^\# Changelog/a\\\\n## [Unreleased]\n\n## [${VERSION}] --- ${DATE}" CHANGELOG.md - fi - echo "CHANGELOG.md: added entry for ${VERSION}" - fi - else - echo "::warning::No CHANGELOG.md found — skipping" - fi - - - name: Update FILE INFORMATION blocks - run: | - # Update VERSION in file header blocks (# VERSION: XX.YY.ZZ) - find . -maxdepth 1 -type f \( -name "*.yml" -o -name "*.yaml" -o -name "*.php" -o -name "*.md" \) \ - -not -path "./.git/*" -not -path "./vendor/*" -print0 2>/dev/null | \ - while IFS= read -r -d '' FILE; do - if head -20 "$FILE" | grep -qP '^\s*#?\s*VERSION:\s*\d{2}\.\d{2}\.\d{2}'; then - sed -i -E "s/(#?\s*VERSION:\s*)[0-9]{2}\.[0-9]{2}\.[0-9]{2}/\1${VERSION}/" "$FILE" - echo "Updated FILE INFORMATION VERSION in ${FILE}" - fi - done - - - name: Commit and push - run: | - git config user.name "Moko Consulting [bot]" - git config user.email "hello@mokoconsulting.tech" - git add -A - if git diff --cached --quiet; then - echo "No version changes detected — nothing to commit" - else - git commit -m "chore: set version to ${VERSION} [skip bump] - -Authored-by: Moko Consulting" - git push - echo "### Version Set" >> $GITHUB_STEP_SUMMARY - echo "Version updated to \`${VERSION}\` on branch \`${GITHUB_REF_NAME}\`" >> $GITHUB_STEP_SUMMARY - fi diff --git a/.mokogitea/workflows/workflow-sync-trigger.yml b/.mokogitea/workflows/workflow-sync-trigger.yml deleted file mode 100644 index 34891e87f7..0000000000 --- a/.mokogitea/workflows/workflow-sync-trigger.yml +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: mokocli.Universal -# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli -# PATH: /.mokogitea/workflows/workflow-sync-trigger.yml -# VERSION: 01.01.00 -# BRIEF: Trigger workflow sync to live repos when a PR is merged to main - -name: "Universal: Workflow Sync Trigger" - -on: - workflow_dispatch: - pull_request: - types: [closed] - branches: - - main - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - sync: - name: Sync workflows to live repos - runs-on: ubuntu-latest - if: >- - github.event_name == 'workflow_dispatch' || - (github.event.pull_request.merged == true && - !contains(github.event.pull_request.title, '[skip sync]')) - - steps: - - name: Determine platform from repo name - id: platform - run: | - REPO="${{ github.event.repository.name }}" - case "$REPO" in - Template-Joomla) PLATFORM="joomla" ;; - Template-Dolibarr) PLATFORM="dolibarr" ;; - Template-Go) PLATFORM="go" ;; - Template-MCP) PLATFORM="mcp" ;; - Template-Generic) PLATFORM="" ;; - *) PLATFORM="" ;; - esac - echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" - echo "Platform: ${PLATFORM:-all}" - - - name: Clone mokocli - env: - MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - run: | - MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}" - git clone --depth 1 "${MOKOGITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli - - - name: Install PHP - run: | - if ! command -v php &> /dev/null; then - apt-get update -qq && apt-get install -y -qq php-cli php-json php-curl > /dev/null 2>&1 - fi - - - name: Install dependencies - run: | - cd /tmp/mokocli - composer install --no-dev --no-interaction --quiet 2>/dev/null || true - - - name: Run workflow sync - env: - MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - run: | - ARGS="--token ${MOKOGITEA_TOKEN}" - ARGS="${ARGS} --org ${{ vars.GITEA_ORG || github.repository_owner }}" - ARGS="${ARGS} --phase repos" - - PLATFORM="${{ steps.platform.outputs.platform }}" - if [ -n "$PLATFORM" ]; then - ARGS="${ARGS} --platform-filter ${PLATFORM}" - fi - - php /tmp/mokocli/cli/workflow_sync.php ${ARGS}