diff --git a/.mokogit/ISSUE_TEMPLATE/adr.md b/.mokogit/ISSUE_TEMPLATE/adr.md new file mode 100644 index 0000000..eb40760 --- /dev/null +++ b/.mokogit/ISSUE_TEMPLATE/adr.md @@ -0,0 +1,110 @@ +--- +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/.mokogit/ISSUE_TEMPLATE/bug_report.md b/.mokogit/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..38a16a7 --- /dev/null +++ b/.mokogit/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,48 @@ +--- +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/.mokogit/ISSUE_TEMPLATE/config.yml b/.mokogit/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..c52380c --- /dev/null +++ b/.mokogit/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,18 @@ +--- +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: 📚 MokoCLI Documentation + url: https://git.mokoconsulting.tech/MokoConsulting/mokocli + about: View our coding standards and best practices + - name: 🔒 Report a Security Vulnerability + url: https://git.mokoconsulting.tech/MokoConsulting/.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/.mokogit/ISSUE_TEMPLATE/documentation.md b/.mokogit/ISSUE_TEMPLATE/documentation.md new file mode 100644 index 0000000..8156651 --- /dev/null +++ b/.mokogit/ISSUE_TEMPLATE/documentation.md @@ -0,0 +1,52 @@ +--- +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 MokoCLI 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/.mokogit/ISSUE_TEMPLATE/feature_request.md b/.mokogit/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..650523c --- /dev/null +++ b/.mokogit/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,51 @@ +--- +name: Feature Request +about: Suggest a new feature or enhancement +title: '[FEATURE] ' +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 [MokoCLI](https://git.mokoconsulting.tech/MokoConsulting/mokocli)? +- [ ] 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/.mokogit/ISSUE_TEMPLATE/joomla_issue.md b/.mokogit/ISSUE_TEMPLATE/joomla_issue.md new file mode 100644 index 0000000..d808f79 --- /dev/null +++ b/.mokogit/ISSUE_TEMPLATE/joomla_issue.md @@ -0,0 +1,87 @@ +--- +name: Joomla Extension Issue +about: Report an issue with a Joomla extension +title: '[JOOMLA] ' +labels: 'joomla' +assignees: '' + +--- + + +## Issue Type +- [ ] Component Issue +- [ ] Module Issue +- [ ] Plugin Issue +- [ ] Template Issue + +## Extension Details +- **Extension Name**: [e.g., moko-cassiopeia] +- **Extension Version**: [e.g., 1.2.3] +- **Extension Type**: [Component / Module / Plugin / Template] + +## Joomla Environment +- **Joomla Version**: [e.g., 4.4.0, 5.0.0] +- **PHP Version**: [e.g., 8.1.0] +- **Database**: [MySQL / PostgreSQL / MariaDB] +- **Database Version**: [e.g., 8.0] +- **Server**: [Apache / Nginx / IIS] +- **Hosting**: [Shared / VPS / Dedicated / Cloud] + +## Issue Description +Provide a clear and detailed description of the issue. + +## Steps to Reproduce +1. Go to '...' +2. Click on '...' +3. Configure '...' +4. See error + +## Expected Behavior +What you expected to happen. + +## Actual Behavior +What actually happened. + +## Error Messages +``` +# Paste any error messages from Joomla error logs +# Location: administrator/logs/error.php +``` + +## Browser Console Errors +```javascript +// Paste any JavaScript console errors (F12 in browser) +``` + +## Screenshots +Add screenshots to help explain the issue. + +## Configuration +```ini +# Paste extension configuration (sanitize sensitive data) +``` + +## Installed Extensions +List other installed extensions that might conflict: +- Extension 1 (version) +- Extension 2 (version) + +## Template Overrides +- [ ] Using template overrides +- [ ] Custom CSS +- [ ] Custom JavaScript + +## Additional Context +- **Multilingual Site**: [Yes / No] +- **Cache Enabled**: [Yes / No] +- **Debug Mode**: [Yes / No] +- **SEF URLs**: [Yes / No] + +## Checklist +- [ ] I have cleared Joomla cache +- [ ] I have disabled other extensions to test for conflicts +- [ ] I have checked Joomla error logs +- [ ] I have tested with a default Joomla template +- [ ] I have checked browser console for JavaScript errors +- [ ] I have searched for similar issues +- [ ] I am using a supported Joomla version diff --git a/.mokogit/ISSUE_TEMPLATE/question.md b/.mokogit/ISSUE_TEMPLATE/question.md new file mode 100644 index 0000000..3175013 --- /dev/null +++ b/.mokogit/ISSUE_TEMPLATE/question.md @@ -0,0 +1,82 @@ +--- +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/.mokogit/ISSUE_TEMPLATE/rfc.md b/.mokogit/ISSUE_TEMPLATE/rfc.md new file mode 100644 index 0000000..6f09af7 --- /dev/null +++ b/.mokogit/ISSUE_TEMPLATE/rfc.md @@ -0,0 +1,126 @@ +--- +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/.mokogit/ISSUE_TEMPLATE/security.md b/.mokogit/ISSUE_TEMPLATE/security.md new file mode 100644 index 0000000..37a266d --- /dev/null +++ b/.mokogit/ISSUE_TEMPLATE/security.md @@ -0,0 +1,51 @@ +--- +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 [MokoCLI](https://git.mokoconsulting.tech/MokoConsulting/mokocli)? +- [ ] 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/.mokogit/ISSUE_TEMPLATE/version.md b/.mokogit/ISSUE_TEMPLATE/version.md new file mode 100644 index 0000000..6328421 --- /dev/null +++ b/.mokogit/ISSUE_TEMPLATE/version.md @@ -0,0 +1,24 @@ +--- +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/.mokogit/workflows/auto-bump.yml b/.mokogit/workflows/auto-bump.yml index 2a69125..9230c5f 100644 --- a/.mokogit/workflows/auto-bump.yml +++ b/.mokogit/workflows/auto-bump.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: MokoGIT.Workflow # INGROUP: MokoCLI.Release -# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli +# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic # PATH: /.mokogit/workflows/auto-bump.yml # VERSION: 09.02.00 # BRIEF: Auto patch-bump version on every push to dev (skips merge commits) @@ -34,7 +34,8 @@ jobs: 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') + !startsWith(github.event.head_commit.message, 'Merge pull request') && + !startsWith(github.event.repository.name, 'Template-') steps: - name: Checkout diff --git a/.mokogit/workflows/auto-release.yml b/.mokogit/workflows/auto-release.yml index 436396d..6dbd7ff 100644 --- a/.mokogit/workflows/auto-release.yml +++ b/.mokogit/workflows/auto-release.yml @@ -5,16 +5,16 @@ # FILE INFORMATION # DEFGROUP: MokoGIT.Workflow # INGROUP: MokoCLI.Release -# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli -# PATH: /templates/workflows/universal/auto-release.yml.template -# VERSION: 05.01.00 -# BRIEF: Universal build & release � detects platform from MokoGIT repo metadata (API) +# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic +# PATH: /.mokogit/workflows/auto-release.yml +# VERSION: 05.02.00 +# BRIEF: Universal build & release � detects platform from metadata API # # +=======================================================================+ # | UNIVERSAL BUILD & RELEASE PIPELINE | # +=======================================================================+ # | | -# | Reads MokoGIT repo metadata (joomla|dolibarr|generic) to branch logic. | +# | Reads metadata API (joomla|dolibarr|generic) to branch logic. | # | | # | Platform-specific: | # | joomla: XML manifest, type-prefixed packages | @@ -39,6 +39,12 @@ on: - '.gitattributes' - '.gitmessage' - 'LICENSE' + # Daily safety-net: catch merges whose pull_request event never created a run + # (e.g. this workflow file being re-synced from the template concurrently with the + # merge). Off-round minute to avoid a fleet-wide spike. The safety-net job below + # only dispatches a release when main actually has unreleased changes. + schedule: + - cron: '37 8 * * *' workflow_dispatch: inputs: action: @@ -104,11 +110,47 @@ jobs: - name: Rename branch to rc run: | - php ${MOKO_CLI}/branch_rename.php \ - --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ - --token "${{ secrets.MOKOGIT_TOKEN }}" \ - --api-base "${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" \ - --pr "${{ github.event.pull_request.number }}" + API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" + AUTH="Authorization: token ${{ secrets.MOKOGIT_TOKEN }}" + FROM="${{ github.event.pull_request.head.ref || 'dev' }}" + PR="${{ github.event.pull_request.number }}" + + # Resolve the source branch HEAD commit. + SRC_JSON=$(curl -sf -H "$AUTH" "${API_BASE}/branches/${FROM}") \ + || { echo "::error::Source branch ${FROM} not found"; exit 1; } + SRC_SHA=$(printf '%s' "$SRC_JSON" | python3 -c "import sys, json; print(json.load(sys.stdin)['commit']['id'])" 2>/dev/null || true) + [ -n "$SRC_SHA" ] || { echo "::error::Could not resolve HEAD of ${FROM}"; exit 1; } + + # Point rc at the source commit via git push. Git's git/refs PATCH API + # returns HTTP 405 on ANY protected branch (force or not, even for a user in + # the force-push allowlist), so it cannot move a protected rc. git push honors + # the push + force-push allowlists and creates rc if it is absent. + PUSH_URL="https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@${MOKOGIT_URL#https://}/${GIT_ORG}/${GIT_REPO}.git" + git config --global user.name "mokogit-actions[bot]" + git config --global user.email "actions@mokoconsulting.tech" + git fetch --no-tags "$PUSH_URL" "${FROM}" + git push --force "$PUSH_URL" "FETCH_HEAD:refs/heads/rc" \ + || { echo "::error::Failed to point rc at ${FROM} (${SRC_SHA}) via git push"; exit 1; } + echo "rc set to ${FROM} (${SRC_SHA})" + + # Repoint the PR at rc, then delete the old source branch (non-fatal). + if [ -n "$PR" ]; then + curl -s -X PATCH -H "$AUTH" -H "Content-Type: application/json" \ + "${API_BASE}/pulls/${PR}" -d '{"head":"rc"}' >/dev/null || true + fi + # Never delete permanent branches (dev/main/rc/...); only ephemeral feature branches. + case "$FROM" in + dev|main|master|rc|stable|production|release|develop|staging|beta|alpha) + echo "Keeping permanent branch ${FROM} (not deleting)" ;; + *) + curl -s -X DELETE -H "$AUTH" "${API_BASE}/branches/${FROM}" >/dev/null || true ;; + esac + echo "Renamed ${FROM} -> rc" + + - name: Trigger RC deploy + run: | + # Workflow-token pushes do NOT wake downstream workflows; dispatch deploy-rc explicitly. + curl -sf -X POST -H "Authorization: token ${{ secrets.MOKOGIT_TOKEN }}" \n -H "Content-Type: application/json" \n "${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}/actions/workflows/deploy-rc.yml/dispatches" \n -d '{"ref":"rc"}' \n && echo "Dispatched deploy-rc on rc" \n || echo "::warning::deploy-rc dispatch failed (no deploy-rc.yml on rc? non-go platform)" - name: Checkout rc and configure git run: | @@ -119,20 +161,25 @@ jobs: git remote set-url origin "https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - name: Publish RC release + continue-on-error: true run: | php ${MOKO_CLI}/release_publish.php \ --path . --stability rc --bump minor --branch rc \ --token "${{ secrets.MOKOGIT_TOKEN }}" - name: Update RC release notes from CHANGELOG.md + continue-on-error: true run: | API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" TOKEN="${{ secrets.MOKOGIT_TOKEN }}" - # Extract [Unreleased] section from changelog + # Extract [Unreleased] section via the shared mokocli command (#364 centralization). NOTES="" if [ -f "CHANGELOG.md" ]; then - NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) + NOTES=$(php ${MOKO_CLI}/release_notes.php --path . --version Unreleased 2>/dev/null || true) + # release_notes.php echoes "Release Unreleased" when the section is empty; normalize + # to empty so the platform fallback below applies. + [ "$NOTES" = "Release Unreleased" ] && NOTES="" fi [ -z "$NOTES" ] && NOTES="Release candidate" @@ -320,10 +367,13 @@ jobs: print(m.group(1) if m else '') " <<< "$RELEASE_JSON" 2>/dev/null || true) - # Extract [Unreleased] section from changelog + # Extract [Unreleased] section via the shared mokocli command (#364 centralization). NOTES="" if [ -f "CHANGELOG.md" ]; then - NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) + NOTES=$(php ${MOKO_CLI}/release_notes.php --path . --version Unreleased 2>/dev/null || true) + # release_notes.php echoes "Release Unreleased" when the section is empty; normalize + # to empty so the platform fallback below applies. + [ "$NOTES" = "Release Unreleased" ] && NOTES="" fi [ -z "$NOTES" ] && NOTES="Stable release" @@ -345,18 +395,12 @@ jobs: echo "Release notes updated from CHANGELOG.md" fi - # Promote [Unreleased] → [version] in CHANGELOG.md and reset + # Promote [Unreleased] → [version] in CHANGELOG.md via the shared mokocli command (#364). + # changelog_promote.php is idempotent (won't re-promote an existing version), merges + # duplicate headings, drops blank release sections, and collapses whitespace — verified + # byte-identical to the former inline promoter across 8 fixtures. 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" + php ${MOKO_CLI}/changelog_promote.php --path . --version "$VERSION" git add CHANGELOG.md git commit -m "chore: promote changelog [Unreleased] → [${VERSION}]" || true git push origin main || true @@ -467,3 +511,51 @@ jobs: echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY echo "| Release | [View](${MOKOGIT_URL}/${GIT_ORG}/${GIT_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY fi + + # ── Scheduled safety-net ───────────────────────────────────────────────────────── + # A merge to main normally fires the `release` job via the pull_request `closed` + # event. If that event never creates a run (observed when this workflow file is + # being re-synced from the template in the same window as the merge), the release + # is silently skipped. This daily job self-heals that: if main's CHANGELOG + # [Unreleased] section still has content — which the release job empties on a + # successful promote — it dispatches a normal release. Dependency-free (awk only) + # so it runs on any runner; never touches the release job's own conditions. + scheduled-safety-net: + name: Scheduled release safety-net + runs-on: ubuntu-latest + if: github.event_name == 'schedule' && !startsWith(github.event.repository.name, 'Template-') + permissions: + contents: read + steps: + - name: Checkout main + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + ref: main + fetch-depth: 1 + + - name: Dispatch release if main has unreleased changes + env: + MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} + run: | + # [Unreleased] is non-empty precisely when changes were merged but not yet + # released (the release job promotes it to a version and empties it). + NOTES="" + if [ -f CHANGELOG.md ]; then + NOTES=$(awk '/^## \[Unreleased\]/{f=1;next} /^## \[/{if(f)exit} f{print}' CHANGELOG.md) + fi + if [ -z "$(printf '%s' "$NOTES" | tr -d '[:space:]')" ]; then + echo "No unreleased changes on main — safety-net has nothing to do." + exit 0 + fi + echo "Unreleased changes detected on main — a merge release was likely missed." + echo "Dispatching a release via the safety-net." + HTTP=$(curl -s -o /dev/null -w '%{http_code}' -X POST \ + -H "Authorization: token ${MOKOGIT_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{"ref":"main","inputs":{"action":"release"}}' \ + "${MOKOGIT_URL}/api/v1/repos/${{ github.repository }}/actions/workflows/auto-release.yml/dispatches") + if [ "$HTTP" = "204" ] || [ "$HTTP" = "201" ] || [ "$HTTP" = "200" ]; then + echo "Release dispatched (HTTP $HTTP)." + else + echo "::warning::Safety-net release dispatch failed (HTTP $HTTP)" + fi diff --git a/.mokogit/workflows/cascade-dev.yml b/.mokogit/workflows/cascade-dev.yml index d5cd8eb..ddc9ebb 100644 --- a/.mokogit/workflows/cascade-dev.yml +++ b/.mokogit/workflows/cascade-dev.yml @@ -1,65 +1,190 @@ # Copyright (C) 2026 Moko Consulting +# # SPDX-License-Identifier: GPL-3.0-or-later +# # FILE INFORMATION # DEFGROUP: MokoGIT.Workflow -# INGROUP: MokoCLI.Release -# BRIEF: Reset dev to main after each release (cascade). Moved out of auto-release Step 11. -# -# +========================================================================+ -# | CASCADE MAIN -> DEV | -# +========================================================================+ -# | dev mirrors main and is reset on every release. | -# | delete+recreate cannot run against a protected branch, so this | -# | force-pushes main -> dev. The automation identity is force-push | -# | allowlisted on dev via branch-protection.yml. | -# | Runs AFTER the release workflow completes, so dev picks up the | -# | fully version-bumped + changelog-promoted main (not the pre-bump | -# | state). Force-push (not merge) => no version-file conflicts. | -# +========================================================================+ +# INGROUP: MokoCLI.Cascade +# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic +# PATH: /.mokogit/workflows/cascade-dev.yml +# VERSION: 02.01.00 +# BRIEF: Cascade main -> dev; auto-merge clean, auto-resolve VERSION-stamp-only conflicts, else notify name: "Cascade Main -> Dev" on: - workflow_run: - workflows: ["Universal: Build & Release"] - types: [completed] + push: + branches: + - main + # Daily safety net: catches drift even when main only received [skip ci] pushes + # (which never fire the push trigger above). Off-round minute to avoid a fleet-wide spike. + schedule: + - cron: '23 7 * * *' workflow_dispatch: -concurrency: - group: cascade-dev-${{ github.repository }} - cancel-in-progress: true - permissions: contents: write + pull-requests: write + +env: + MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }} + # ntfy destination is configured via repo or org variables (org vars are inherited). + NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} + NTFY_TOPIC: ${{ vars.CASCADE_NTFY_TOPIC || vars.NTFY_TOPIC || 'git-releases' }} jobs: cascade: - name: Reset dev to main - if: >- - !startsWith(github.event.repository.name, 'Template-') && - (github.event_name == 'workflow_dispatch' || - github.event.workflow_run.conclusion == 'success') + name: Cascade main -> dev runs-on: ubuntu-latest steps: - - name: Force dev to main + - name: Checkout (full history for merge/resolve) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.MOKOGIT_TOKEN }} + + - name: Cascade main -> dev (auto-resolve version stamps, else notify) env: TOKEN: ${{ secrets.MOKOGIT_TOKEN }} - SERVER: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }} REPO: ${{ github.repository }} run: | - set -euo pipefail - git init -q sync && cd sync - git config user.email "mokogit-actions[bot]@mokoconsulting.tech" - git config user.name "mokogit-actions[bot]" - git remote add origin "https://x-access-token:${TOKEN}@${SERVER#https://}/${REPO}.git" - git fetch -q --depth=1 origin main + set -uo pipefail + API="${MOKOGIT_URL}/api/v1/repos/${REPO}" + AUTH="Authorization: token ${TOKEN}" + jqget() { python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$1',''))" 2>/dev/null; } - if git ls-remote --exit-code --heads origin dev >/dev/null 2>&1; then - # dev exists -> force it to match main (dev is a mirror of main) - git push --force origin FETCH_HEAD:refs/heads/dev - echo "dev reset to main" >> "$GITHUB_STEP_SUMMARY" - else - # dev missing (e.g. renamed to rc mid-cycle) -> recreate from main - git push origin FETCH_HEAD:refs/heads/dev - echo "dev created from main" >> "$GITHUB_STEP_SUMMARY" + # 0. dev must exist + if ! curl -sf -H "$AUTH" "${API}/branches/dev" >/dev/null 2>&1; then + echo "No dev branch - nothing to cascade."; exit 0 fi + + # 1. is main ahead of dev? + AHEAD=$(curl -sf -H "$AUTH" "${API}/compare/dev...main" \ + | python3 -c "import sys,json; print(json.load(sys.stdin).get('total_commits',0))" 2>/dev/null || echo 0) + if [ "${AHEAD:-0}" -eq 0 ]; then + echo "dev already up to date with main."; exit 0 + fi + echo "main is ${AHEAD} commit(s) ahead of dev." + + # 2. reuse an open main->dev PR, else create one + PR=$(curl -sf -H "$AUTH" "${API}/pulls?state=open&base=dev" \ + | python3 -c "import sys,json; d=json.load(sys.stdin); print(next((str(p['number']) for p in d if p.get('head',{}).get('ref')=='main'), ''))" 2>/dev/null || echo "") + if [ -z "$PR" ]; then + RESP=$(curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST "${API}/pulls" \ + -d '{"head":"main","base":"dev","title":"chore(sync): cascade main -> dev","body":"Automated cascade of main into dev. Auto-merges when conflict-free, auto-resolves VERSION-stamp-only conflicts, otherwise left open for manual resolution."}') + PR=$(printf '%s' "$RESP" | jqget number) + if [ -z "$PR" ]; then + echo "::warning::Could not open cascade PR: $RESP"; exit 0 + fi + echo "Opened cascade PR #${PR}" + else + echo "Reusing open cascade PR #${PR}" + fi + + notify() { + curl -sS \ + -H "Title: ${REPO}: dev cascade needs manual merge" \ + -H "Tags: warning,twisted_rightwards_arrows" \ + -H "Priority: high" \ + -H "Click: ${MOKOGIT_URL}/${REPO}/pulls/${PR}" \ + -d "main -> dev cascade PR #${PR} $1 It was NOT auto-merged; resolve it manually." \ + "${NTFY_URL}/${NTFY_TOPIC}" || true + } + + # 3. wait for MokoGIT to compute mergeability (conflict detection) + MERGEABLE="" + for _ in 1 2 3 4 5 6; do + MERGEABLE=$(curl -sf -H "$AUTH" "${API}/pulls/${PR}" | jqget mergeable) + case "$MERGEABLE" in True|False) break ;; esac + sleep 3 + done + echo "mergeable=${MERGEABLE}" + + # 4a. conflict-free -> merge via API (existing behaviour) + if [ "$MERGEABLE" = "True" ]; then + CODE=$(curl -s -o /tmp/merge.json -w "%{http_code}" -H "$AUTH" -H "Content-Type: application/json" \ + -X POST "${API}/pulls/${PR}/merge" -d '{"Do":"merge","merge_when_checks_succeed":true}') + if [ "$CODE" -ge 200 ] && [ "$CODE" -lt 300 ]; then + echo "Cascade PR #${PR} merged (or scheduled to merge when checks pass)." + exit 0 + fi + echo "::warning::Auto-merge returned HTTP ${CODE}: $(cat /tmp/merge.json)" + notify "could not be auto-merged (HTTP ${CODE})." + exit 0 + fi + + # 4b. conflicts -> try to auto-resolve if they are ONLY VERSION-stamp lines. + echo "PR not cleanly mergeable; checking whether conflicts are VERSION-stamp-only..." + git config user.name "MokoGIT Cascade" + git config user.email "actions@mokoconsulting.tech" + git fetch --quiet origin main dev + git checkout -B dev origin/dev + + if git merge --no-ff --no-commit origin/main >/dev/null 2>&1; then + # Became clean at git level (e.g. mergeability was still computing) -> commit + push. + git commit -m "chore(sync): cascade main -> dev [skip ci]" >/dev/null + git push origin dev + echo "Cascade merged cleanly at git level and pushed to dev." + exit 0 + fi + + CONFLICTS=$(git diff --name-only --diff-filter=U) + echo "Conflicted files:"; echo "${CONFLICTS}" + + # A conflict is "stamp-only" when every line inside every conflict block matches + # a version-stamp pattern (VERSION: header, element, or CHANGELOG title). + is_stamp_only() { + awk ' + /^<<<<<<< / { inc=1; next } + inc && /^=======$/ { next } + /^>>>>>>> / { inc=0; next } + inc { if ($0 !~ /(VERSION:||# Changelog)/) { bad=1 } } + END { exit(bad ? 1 : 0) } + ' "$1" + } + # Resolve a stamp-only file by keeping dev (ours) for the conflicting lines, + # preserving all auto-merged content around them. + keep_ours() { + awk ' + /^<<<<<<< / { inc=1; side="ours"; next } + inc && /^=======$/ { side="theirs"; next } + /^>>>>>>> / { inc=0; next } + { if (!inc) { print; next } if (side=="ours") print } + ' "$1" > "$1.resolved" && mv "$1.resolved" "$1" + } + + ALL_STAMP=1 + for f in ${CONFLICTS}; do + if ! is_stamp_only "$f"; then + echo "::notice::$f has non-stamp conflicts -> manual resolution required." + ALL_STAMP=0; break + fi + done + + if [ "$ALL_STAMP" != "1" ]; then + git merge --abort || true + notify "has non-version-stamp conflicts and cannot be auto-resolved." + exit 0 + fi + + echo "All conflicts are VERSION-stamp-only; resolving in favour of dev." + for f in ${CONFLICTS}; do + keep_ours "$f" + git add "$f" + done + + # Best-effort: normalise stamps to dev's version if mokocli is available. + if [ -f /opt/mokocli/cli/version_check.php ]; then + php /opt/mokocli/cli/version_check.php --fix || true + git add -A + fi + + git commit -m "chore(sync): cascade main -> dev (auto-resolved version stamps) [skip ci]" >/dev/null + git push origin dev + echo "Cascade auto-resolved and pushed to dev." + + # Close the now-redundant PR (its changes are in dev) with an explanatory comment. + curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST "${API}/issues/${PR}/comments" \ + -d '{"body":"Auto-resolved VERSION-stamp-only conflicts and pushed the merge to dev. Closing."}' >/dev/null || true + curl -s -H "$AUTH" -H "Content-Type: application/json" -X PATCH "${API}/pulls/${PR}" \ + -d '{"state":"closed"}' >/dev/null || true diff --git a/.mokogit/workflows/ci-generic.yml b/.mokogit/workflows/ci-generic.yml index 053109a..17feb5b 100644 --- a/.mokogit/workflows/ci-generic.yml +++ b/.mokogit/workflows/ci-generic.yml @@ -131,10 +131,11 @@ jobs: test: name: Tests runs-on: ubuntu-latest - needs: lint - # Run only when lint succeeded; always() forces evaluation so a skipped - # lint (e.g. template repos) skips this job cleanly instead of hanging. - if: ${{ always() && needs.lint.result == 'success' }} + # Independent job (no `needs: lint`): the MokoGIT Actions scheduler does not + # offer the dependent 2nd job of a needs-chain to runners, so it stalls in + # "waiting" and is reaped by ABANDONED_JOB_TIMEOUT. Guard template repos + # directly (same condition lint uses) instead of gating on lint's result. + if: ${{ !startsWith(github.event.repository.name, 'Template-') }} steps: - name: Checkout diff --git a/.mokogit/workflows/gitleaks.yml b/.mokogit/workflows/gitleaks.yml index cea7c9e..b68bcd0 100644 --- a/.mokogit/workflows/gitleaks.yml +++ b/.mokogit/workflows/gitleaks.yml @@ -6,7 +6,7 @@ # DEFGROUP: MokoGIT.Workflow # INGROUP: MokoCLI.Security # REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli -# PATH: /templates/workflows/gitleaks.yml.template +# PATH: /.mokogit/workflows/gitleaks.yml # VERSION: 01.00.00 # BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens # @@ -34,7 +34,8 @@ permissions: env: NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} - NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'git-security' }} + NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'mokogit-security' }} + NTFY_TOKEN: ${{ secrets.NTFY_TOKEN }} jobs: gitleaks: @@ -89,4 +90,5 @@ jobs: -H "Tags: rotating_light,key" \ -H "Priority: urgent" \ -d "Gitleaks found potential secrets. Review and rotate credentials immediately." \ + -H "Authorization: Bearer ${NTFY_TOKEN}" \ "${NTFY_URL}/${NTFY_TOPIC}" || true diff --git a/.mokogit/workflows/notify.yml b/.mokogit/workflows/notify.yml index f51ad68..009ade4 100644 --- a/.mokogit/workflows/notify.yml +++ b/.mokogit/workflows/notify.yml @@ -26,7 +26,8 @@ permissions: env: NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} - NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'git-releases' }} + NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'mokogit-releases' }} + NTFY_TOKEN: ${{ secrets.NTFY_TOKEN }} jobs: notify: @@ -46,13 +47,14 @@ jobs: WORKFLOW="${{ github.event.workflow_run.name }}" URL="${{ github.event.workflow_run.html_url }}" - curl -sS \ + curl -sS --retry 3 --retry-connrefused --retry-delay 2 --max-time 20 \ -H "Title: ${REPO} released" \ -H "Tags: white_check_mark,package" \ -H "Priority: default" \ -H "Click: ${URL}" \ -d "${WORKFLOW} completed successfully." \ - "${NTFY_URL}/${NTFY_TOPIC}" + -H "Authorization: Bearer ${NTFY_TOKEN}" \ + "${NTFY_URL}/${NTFY_TOPIC}" || echo "::warning::ntfy notification could not be delivered (non-fatal)" - name: Notify on failure if: github.event.workflow_run.conclusion == 'failure' @@ -61,10 +63,11 @@ jobs: WORKFLOW="${{ github.event.workflow_run.name }}" URL="${{ github.event.workflow_run.html_url }}" - curl -sS \ + curl -sS --retry 3 --retry-connrefused --retry-delay 2 --max-time 20 \ -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}" + -H "Authorization: Bearer ${NTFY_TOKEN}" \ + "${NTFY_URL}/${NTFY_TOPIC}" || echo "::warning::ntfy notification could not be delivered (non-fatal)" diff --git a/.mokogit/workflows/pr-check.yml b/.mokogit/workflows/pr-check.yml index 3978c8e..e4ae87e 100644 --- a/.mokogit/workflows/pr-check.yml +++ b/.mokogit/workflows/pr-check.yml @@ -5,8 +5,8 @@ # FILE INFORMATION # DEFGROUP: MokoGIT.Workflow # INGROUP: MokoCLI.CI -# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli -# PATH: /templates/workflows/universal/pr-check.yml.template +# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic +# PATH: /.mokogit/workflows/pr-check.yml # VERSION: 09.23.00 # BRIEF: PR gate — branch policy + code validation before merge @@ -47,15 +47,15 @@ jobs: fi ;; fix/*|bugfix/*) - if [ "$BASE" != "dev" ]; then + if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then ALLOWED=false - REASON="Fix branches must target 'dev', not '${BASE}'" + REASON="Fix branches must target 'dev' or 'main', not '${BASE}'" fi ;; patch/*) - if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then + if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ] && [ "$BASE" != "main" ]; then ALLOWED=false - REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'" + REASON="Patch branches must target 'dev', 'rc', or 'main', not '${BASE}'" fi ;; hotfix/*) @@ -86,7 +86,8 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY - echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY + echo "- \`fix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY + echo "- \`patch/*\` → \`dev\`, \`rc\`, or \`main\`" >> $GITHUB_STEP_SUMMARY echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY @@ -96,6 +97,80 @@ jobs: echo "Branch policy: OK (${HEAD} → ${BASE})" echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY + # ── Docs Update Gate (main PRs) ───────────────────────────────────────── + require-docs: + name: Require Docs Update + runs-on: ubuntu-latest + # Enforce only on PRs merging into main: README.md + CHANGELOG.md must both be updated. + if: ${{ github.base_ref == 'main' }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Require README.md and CHANGELOG.md in the PR diff + run: | + BASE="${{ github.event.pull_request.base.sha }}" + HEAD="${{ github.event.pull_request.head.sha }}" + CHANGED="$(git diff --name-only "$BASE" "$HEAD" 2>/dev/null || true)" + if [ -z "$CHANGED" ]; then + git fetch -q origin "${{ github.base_ref }}" 2>/dev/null || true + CHANGED="$(git diff --name-only "origin/${{ github.base_ref }}...HEAD" 2>/dev/null || true)" + fi + echo "Changed files in PR:" + echo "$CHANGED" + MISSING="" + echo "$CHANGED" | grep -qxE 'README\.md' || MISSING="README.md" + echo "$CHANGED" | grep -qxE 'CHANGELOG\.md' || MISSING="${MISSING:+$MISSING, }CHANGELOG.md" + if [ -n "$MISSING" ]; then + echo "::error::PRs into main must update: ${MISSING}" + { + echo "## Docs Update Required" + echo "" + echo "PRs merging into \`main\` must update both **README.md** and **CHANGELOG.md**." + echo "" + echo "Not updated in this PR: **${MISSING}**" + } >> "$GITHUB_STEP_SUMMARY" + exit 1 + fi + echo "Docs update present (README.md + CHANGELOG.md)" + echo "## Docs Update: Passed" >> "$GITHUB_STEP_SUMMARY" + + # ── Wiki Update Reminder (main PRs, non-blocking) ─────────────────────── + wiki-reminder: + name: Wiki Update Reminder + runs-on: ubuntu-latest + if: ${{ github.base_ref == 'main' }} + steps: + - name: Remind to update the wiki + env: + TOKEN: ${{ secrets.MOKOGIT_TOKEN }} + SERVER: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }} + REPO: ${{ github.repository }} + PR: ${{ github.event.pull_request.number }} + run: | + set -uo pipefail + { + echo "## Wiki Update Reminder" + echo "" + echo "Docs are **wiki-first** at MokoConsulting. If this change affects behavior, usage, configuration, or standards, update the repo wiki:" + echo "" + echo "- ${SERVER}/${REPO}/wiki" + echo "" + echo "_Non-blocking reminder._" + } >> "$GITHUB_STEP_SUMMARY" + # Post a single PR comment (idempotent via hidden marker); best-effort, never fails. + API="${SERVER}/api/v1/repos/${REPO}/issues/${PR}/comments" + if [ -n "${TOKEN:-}" ] && [ -n "${PR:-}" ]; then + existing="$(curl -sf -H "Authorization: token ${TOKEN}" "$API" 2>/dev/null | grep -c 'wiki-reminder' || true)" + if [ "${existing:-0}" -eq 0 ]; then + curl -sf -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" -X POST "$API" \ + -d '{"body":"\n\n**Wiki reminder:** docs are wiki-first -- if this PR changes behavior, usage, config, or standards, please update the repo wiki before/after merge. _(non-blocking)_"}' >/dev/null 2>&1 || true + fi + fi + echo "Wiki reminder emitted (non-blocking)." + # ── Secret Scanning ────────────────────────────────────────────────── gitleaks: name: Secret Scan @@ -135,7 +210,7 @@ jobs: - 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) + CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --exclude-dir='.git' --exclude-dir='.mokogit' --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 @@ -149,7 +224,7 @@ jobs: - name: Detect platform id: platform run: | - # Platform comes from the MokoGIT metadata API (public GET); manifest.xml is no longer used. + # Platform comes from the MokoGIT metadata API (public GET). API="${GITHUB_SERVER_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${GITHUB_REPOSITORY}/metadata" PLATFORM="$(curl -sf "$API" 2>/dev/null | python3 -c "import sys, json; print(json.load(sys.stdin).get('platform') or '')" 2>/dev/null || true)" [ -z "$PLATFORM" ] && PLATFORM="generic" @@ -183,8 +258,9 @@ jobs: 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 + # Scan the whole file for the JEXEC/JPATH guard: it is placed after + # the SPDX/file-header docblock, which commonly runs past 20 lines. + if ! grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]" "$file"; then echo "::error file=${file}::Missing JEXEC guard: ${file}" ERRORS=$((ERRORS + 1)) fi @@ -275,7 +351,7 @@ jobs: 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)" + echo "::warning::No Joomla manifest found (MokoSuite site)" exit 0 fi echo "Manifest: ${MANIFEST}" diff --git a/.mokogit/workflows/pre-release.yml b/.mokogit/workflows/pre-release.yml index 4f8919e..469bcc1 100644 --- a/.mokogit/workflows/pre-release.yml +++ b/.mokogit/workflows/pre-release.yml @@ -5,10 +5,10 @@ # FILE INFORMATION # DEFGROUP: MokoGIT.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 +# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic +# PATH: /.mokogit/workflows/pre-release.yml +# VERSION: 05.02.06 +# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches (shared engine: joomla-family + npm/mcp/client) name: "Universal: Pre-Release" @@ -93,19 +93,114 @@ jobs: - 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 + # Authoritative source of truth (#122): the repo's org-customizable + # platform lives in the MokoGIT metadata API. Read it FIRST and, when + # present, emit it as the single `platform` output. This avoids the + # previous clobber where platform_detect.php + manifest_read.php both + # appended `platform=` under this same id (last-write-wins), and where + # manifest_read.php hit the removed /manifest route (404) and fell back + # to generic autodetect — misclassifying an mcp repo as nodejs and + # skipping the whole release. + PLATFORM=$(curl -sf \ + -H "Authorization: token ${{ secrets.MOKOGIT_TOKEN }}" \ + "${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}/metadata" \ + | python3 -c "import json,sys; print((json.load(sys.stdin).get('platform') or '').strip())" 2>/dev/null || true) - - name: Check platform eligibility (Joomla only) + if [ -n "$PLATFORM" ]; then + # Authoritative metadata wins — do NOT run the file-based detectors, + # so nothing can overwrite this value in $GITHUB_OUTPUT. + echo "platform=${PLATFORM}" >> "$GITHUB_OUTPUT" + echo "::notice::Platform '${PLATFORM}' from authoritative metadata API" + else + # Fallback (metadata empty/unreachable): retain the existing + # detection so repos without metadata still resolve a platform. + echo "::notice::No metadata.platform — falling back to file-based detection" + php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true + php ${MOKO_CLI}/manifest_read.php --path . --github-output + fi + + - name: Check platform eligibility id: eligibility run: | + # Shared release engine (#122): the pre-release path runs for every + # artifact platform that produces releases — joomla (+ joomla-family + # like dolibarr), npm, mcp, and client. Deploy-only platforms (go) are + # EXCLUDED: they consume a tag but never bump/release here. + # + # is_joomla_family selects the Joomla/PHP-specific steps (manifest + # element/zip resolution, release_package.php zip, updates.xml). Those + # steps stay TRUE for the Joomla family so Joomla behavior is unchanged, + # and are skipped for npm/mcp/client, which ride only the platform- + # agnostic steps (version, tag, changelog, release-notes, release, + # cascade). Build/publish stay in the platform shim (ADR #124). PLATFORM="${{ steps.platform.outputs.platform }}" - if [[ "$PLATFORM" == joomla* ]] || [[ "$PLATFORM" == "joomla" ]]; then - echo "proceed=true" >> "$GITHUB_OUTPUT" + IS_JOOMLA_FAMILY=false + # STEP 1 — Authoritative baseline (unchanged): the hardcoded case gate + # remains the source of truth for `proceed` and the default value of + # is_joomla_family. Everything below (the registry lookup) is PURELY + # ADDITIVE and can only *refine* is_joomla_family for a cleanly-matched + # platform — it never flips `proceed`, never downgrades, and any + # failure/absence leaves this decision exactly as it is today. + case "$PLATFORM" in + joomla*|dolibarr*) + PROCEED=true + IS_JOOMLA_FAMILY=true + ;; + npm*|mcp*|client*) + PROCEED=true + ;; + *) + PROCEED=false + echo "::notice::Platform '$PLATFORM' — not an artifact release platform, skipping pre-release auto-bump" + ;; + esac + + # STEP 2 — Registry enhancement (#125): classify the Joomla family from + # the live platform registry (GET /api/v1/platforms) so newly-registered + # manifest/packaging platforms get correct family classification without + # editing this workflow. We key on the registry `family` field: families + # `joomla` and `dolibarr` are exactly the Joomla-style manifest/packaging + # path (the manifest_element + release_package steps). We deliberately do + # NOT key on `manifest_based`, because npm and mcp are also + # manifest_based:true yet must ride the platform-agnostic path (family + # `node`) — so `manifest_based` is ambiguous here and `family` is the + # clean discriminator. + # + # Fail-safe contract: the curl is captured into a var (never allowed to + # crash the step under set -e -o pipefail), the JSON is parsed with a + # guarded python3 one-liner, and the result is only applied when it is + # exactly "true"/"false". On ANY of {non-200, curl error, empty/malformed + # JSON, parse failure, platform key absent} REGISTRY_FAMILY stays empty + # and we keep the STEP 1 hardcoded value untouched. + REGISTRY_JSON="$(curl -sf \ + -H "Authorization: token ${{ secrets.MOKOGIT_TOKEN }}" \ + "${GIT_URL}/api/v1/platforms" 2>/dev/null || true)" + REGISTRY_FAMILY="" + if [ -n "$REGISTRY_JSON" ]; then + # Single physical line of python3 so we stay safely inside this YAML + # block scalar (a column-0 continuation would terminate the scalar). + # Emits exactly: "true" -> matched key, family joomla/dolibarr; + # "false" -> matched key, some other family; + # "" -> key absent (fall back to hardcoded). + # On malformed/empty JSON the parse raises, 2>/dev/null hides the + # traceback and the trailing "|| true" keeps set -e -o pipefail from + # crashing the step, so REGISTRY_FAMILY stays "" and we fall back. + REGISTRY_FAMILY="$(printf '%s' "$REGISTRY_JSON" | PLATFORM="$PLATFORM" python3 -c 'import json,os,sys; p=os.environ.get("PLATFORM",""); f=next((x.get("family","") for x in json.load(sys.stdin) if x.get("key")==p), None); sys.stdout.write("true" if f in ("joomla","dolibarr") else ("false" if f is not None else ""))' 2>/dev/null || true)" + fi + if [ "$REGISTRY_FAMILY" = "true" ]; then + IS_JOOMLA_FAMILY=true + echo "::notice::Registry classified '$PLATFORM' as Joomla family (via /api/v1/platforms)" + elif [ "$REGISTRY_FAMILY" = "false" ]; then + IS_JOOMLA_FAMILY=false + echo "::notice::Registry classified '$PLATFORM' as non-Joomla family (via /api/v1/platforms)" else - echo "proceed=false" >> "$GITHUB_OUTPUT" - echo "::notice::Platform '$PLATFORM' — non-Joomla, skipping pre-release auto-bump" + echo "::notice::Registry lookup unavailable/absent for '$PLATFORM' — using hardcoded family classification" + fi + + echo "proceed=${PROCEED}" >> "$GITHUB_OUTPUT" + echo "is_joomla_family=${IS_JOOMLA_FAMILY}" >> "$GITHUB_OUTPUT" + if [ "$PROCEED" = "true" ]; then + echo "::notice::Platform '$PLATFORM' eligible for pre-release (joomla_family=${IS_JOOMLA_FAMILY})" fi - name: Resolve metadata and bump version @@ -162,17 +257,28 @@ jobs: 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 + # Push the bump commit, but do NOT fail the release if the target branch + # is protected and the release identity is not on the push allowlist. + # The build proceeds from the in-tree bumped version regardless; if the + # push is rejected, the next run simply re-bumps from the same base. + if ! git push origin HEAD 2>&1; then + echo "::warning::Version-bump commit could not be pushed (protected branch?). Building from in-tree version ${VERSION} anyway." + fi } - # Auto-detect element via manifest_element.php - php ${MOKO_CLI}/manifest_element.php \ - --path . --version "$VERSION" --stability "$STABILITY" \ - --repo "${GIT_REPO}" --github-output + # Auto-detect element via manifest_element.php (Joomla-family only: + # element/manifest resolution is Joomla-specific). For npm/mcp/client + # the generic fallback below derives ext_element/zip_name from the repo + # name — no Joomla manifest is present or required. + if [ "${{ steps.eligibility.outputs.is_joomla_family }}" = "true" ]; then + php ${MOKO_CLI}/manifest_element.php \ + --path . --version "$VERSION" --stability "$STABILITY" \ + --repo "${GIT_REPO}" --github-output + fi # 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) + EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2 || true) + ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2 || true) [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GIT_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip" @@ -204,9 +310,11 @@ jobs: VERSION="${{ steps.meta.outputs.version }}" API_BASE="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" - # Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading) + # Extract [Unreleased] section via the shared mokocli command (#364 centralization). if [ -f "CHANGELOG.md" ]; then - NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) + NOTES=$(php ${MOKO_CLI}/release_notes.php --path . --version Unreleased 2>/dev/null || true) + # release_notes.php echoes "Release Unreleased" when the section is empty; normalize. + [ "$NOTES" = "Release Unreleased" ] && NOTES="" [ -z "$NOTES" ] && NOTES="Release ${VERSION}" else NOTES="Release ${VERSION}" @@ -233,9 +341,14 @@ jobs: echo "Release notes updated from CHANGELOG.md" fi + # Joomla-family only: mokocli release_package.php builds the extension zip + # (+ sha256, attach). npm/mcp/client package/publish in their platform shim + # (trust boundary, ADR #124), so this PHP zip step is skipped for them. - name: Build package and upload id: package - if: steps.eligibility.outputs.proceed == 'true' + if: >- + steps.eligibility.outputs.proceed == 'true' && + steps.eligibility.outputs.is_joomla_family == 'true' run: | VERSION="${{ steps.meta.outputs.version }}" TAG="${{ steps.meta.outputs.tag }}" @@ -274,4 +387,4 @@ jobs: 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 + echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.mokogit/workflows/push-notify.yml b/.mokogit/workflows/push-notify.yml new file mode 100644 index 0000000..da24468 --- /dev/null +++ b/.mokogit/workflows/push-notify.yml @@ -0,0 +1,43 @@ +# +========================================================================+ +# | Copyright (C) 2026 Moko Consulting | +# | SPDX-License-Identifier: GPL-3.0-or-later | +# | | +# | BRIEF: Clean, formatted ntfy notification on push to a default branch. | +# | Issues use an org-level webhook; releases use notify.yml. | +# +========================================================================+ +name: "Universal: Push Notifications" + +on: + push: + branches: [main, master] + +permissions: + contents: read + +env: + NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} + NTFY_TOKEN: ${{ secrets.NTFY_TOKEN }} + +jobs: + push-notify: + runs-on: ubuntu-latest + steps: + - name: Notify push + # Untrusted values (commit message, actor) passed via env — never + # interpolated directly into the shell — to avoid command injection. + env: + REPO: ${{ github.repository }} + BRANCH: ${{ github.ref_name }} + ACTOR: ${{ github.actor }} + MSG: ${{ github.event.head_commit.message }} + CLICK: ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }} + run: | + SUMMARY=$(printf '%s\n' "$MSG" | head -n1) + curl -sS --retry 3 --retry-connrefused --retry-delay 2 --max-time 20 \ + -H "Title: ${REPO}: push to ${BRANCH}" \ + -H "Tags: package" \ + -H "Priority: default" \ + -H "Click: ${CLICK}" \ + -H "Authorization: Bearer ${NTFY_TOKEN}" \ + -d "${ACTOR}: ${SUMMARY}" \ + "${NTFY_URL}/mokogit-push" || echo "::warning::ntfy notification could not be delivered (non-fatal)" diff --git a/.mokogit/workflows/rc-revert.yml b/.mokogit/workflows/rc-revert.yml index d25b7c9..88a3bbf 100644 --- a/.mokogit/workflows/rc-revert.yml +++ b/.mokogit/workflows/rc-revert.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: MokoGIT.Workflow # INGROUP: MokoCLI.Universal -# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli +# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic # PATH: /.mokogit/workflows/rc-revert.yml # VERSION: 09.23.00 # BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge @@ -25,7 +25,8 @@ jobs: runs-on: ubuntu-latest if: >- github.event.pull_request.merged == false && - startsWith(github.event.pull_request.head.ref, 'rc/') + startsWith(github.event.pull_request.head.ref, 'rc/') && + !startsWith(github.event.repository.name, 'Template-') steps: - name: Rename branch diff --git a/.mokogit/workflows/repo-health.yml b/.mokogit/workflows/repo-health.yml index dbf433b..1b25ff8 100644 --- a/.mokogit/workflows/repo-health.yml +++ b/.mokogit/workflows/repo-health.yml @@ -8,8 +8,8 @@ # FILE INFORMATION # DEFGROUP: MokoGIT.Workflow # INGROUP: MokoCLI.Validation -# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli -# PATH: /templates/workflows/joomla/repo_health.yml.template +# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic +# PATH: /.mokogit/workflows/repo-health.yml # VERSION: 09.23.00 # BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts. # ============================================================================ diff --git a/.mokogit/workflows/version-set.yml b/.mokogit/workflows/version-set.yml index d451b9b..1cd70ee 100644 --- a/.mokogit/workflows/version-set.yml +++ b/.mokogit/workflows/version-set.yml @@ -34,6 +34,7 @@ jobs: set-version: name: Set Version to ${{ inputs.version }} runs-on: ubuntu-latest + if: ${{ !startsWith(github.event.repository.name, 'Template-') }} steps: - name: Validate version format