diff --git a/.mokogitea/branch-protection.yml b/.mokogitea/branch-protection.yml index 6fef3e3..ea78ecb 100644 --- a/.mokogitea/branch-protection.yml +++ b/.mokogitea/branch-protection.yml @@ -11,13 +11,13 @@ # | BRANCH PROTECTION SETUP | # +========================================================================+ # | | -# | Applies protection rules for: main, dev, rc/*, beta/*, alpha/* | +# | 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 | +# | 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. | # | | @@ -149,7 +149,7 @@ jobs: }' RULE_RC='{ - "rule_name": "rc/*", + "rule_name": "rc", "enable_push": true, "enable_push_whitelist": false, "enable_force_push": true, @@ -162,7 +162,7 @@ jobs: }' RULE_BETA='{ - "rule_name": "beta/*", + "rule_name": "beta", "enable_push": true, "enable_push_whitelist": false, "enable_force_push": true, @@ -175,7 +175,7 @@ jobs: }' RULE_ALPHA='{ - "rule_name": "alpha/*", + "rule_name": "alpha", "enable_push": true, "enable_push_whitelist": false, "enable_force_push": true, @@ -188,7 +188,7 @@ jobs: }' RULES=("$RULE_MAIN" "$RULE_DEV" "$RULE_RC" "$RULE_BETA" "$RULE_ALPHA") - RULE_NAMES=("main" "dev" "rc/*" "beta/*" "alpha/*") + RULE_NAMES=("main" "dev" "rc" "beta" "alpha") # ── Apply rules to each repo ────────────────────────────── for REPO in $REPOS; do diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 618cb76..73de5d7 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -82,14 +82,56 @@ jobs: cd /tmp/moko-platform-api composer install --no-dev --no-interaction --quiet - - name: Promote to release-candidate + - name: Rename source branch to rc run: | + SOURCE_BRANCH="${{ github.event.pull_request.head.ref || 'dev' }}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/release_promote.php \ - --from auto --to release-candidate \ + PR_NUM="${{ github.event.pull_request.number }}" + php /tmp/moko-platform-api/cli/branch_rename.php \ + --from "$SOURCE_BRANCH" --to rc \ --token "${{ secrets.MOKOGITEA_TOKEN }}" \ --api-base "${API_BASE}" \ - --branch "${{ github.event.pull_request.head.ref || 'dev' }}" + --pr "$PR_NUM" + + - name: Set RC version on renamed branch + run: | + # Checkout the new rc branch + git fetch origin rc + git checkout rc + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + MOKO_CLI="/tmp/moko-platform-api/cli" + + VERSION=$(php ${MOKO_CLI}/version_read.php --path .) || true + [ -z "$VERSION" ] && { echo "No version — skipping"; exit 0; } + + php ${MOKO_CLI}/version_set_platform.php \ + --path . --version "$VERSION" --branch rc --stability rc 2>/dev/null || true + php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true + + if ! git diff --quiet || ! git diff --cached --quiet; then + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git add -A + git commit -m "chore(version): set RC stability suffix [skip ci]" \ + --author="gitea-actions[bot] " + git push origin rc + fi + + - name: Build RC release + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + MOKO_CLI="/tmp/moko-platform-api/cli" + VERSION=$(php ${MOKO_CLI}/version_read.php --path .) || true + + php ${MOKO_CLI}/release_create.php \ + --path . --version "$VERSION" --tag "release-candidate" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --branch rc 2>&1 || true + + php ${MOKO_CLI}/release_package.php \ + --path . --version "$VERSION" --tag "release-candidate" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --output /tmp 2>&1 || true - name: Cascade lesser channels continue-on-error: true @@ -104,7 +146,7 @@ jobs: if: always() run: | echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY - echo "Draft PR opened — promoted highest pre-release to RC" >> $GITHUB_STEP_SUMMARY + echo "Draft PR opened — branch renamed to rc, RC release built" >> $GITHUB_STEP_SUMMARY # ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── release: diff --git a/.mokogitea/workflows/branch-cleanup.yml b/.mokogitea/workflows/branch-cleanup.yml index d45f1fe..daf3b50 100644 --- a/.mokogitea/workflows/branch-cleanup.yml +++ b/.mokogitea/workflows/branch-cleanup.yml @@ -26,14 +26,17 @@ jobs: if: >- github.event.pull_request.merged == true && github.event.pull_request.head.ref != 'dev' && - github.event.pull_request.head.ref != 'main' + github.event.pull_request.head.ref != 'main' && + github.event.pull_request.head.ref != 'rc' && + github.event.pull_request.head.ref != 'alpha' && + github.event.pull_request.head.ref != 'beta' 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=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${BRANCH}', safe=''))") + ENCODED=$(php -r "echo rawurlencode('${BRANCH}');") STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \ -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c976c7e..ea9a5fc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,29 +1,140 @@ -# Contributing to moko-platform +# Contributing to Moko Consulting Projects -Thank you for your interest in contributing to the Moko Consulting platform. +Thank you for your interest in contributing. All Moko Consulting repositories follow this universal workflow and version policy. -## How to Contribute +## Branching Workflow -1. **Fork** the repository -2. Create a **feature branch** from `dev` (e.g., `feature/my-feature`) -3. Make your changes following [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home) -4. Submit a **Pull Request** targeting `dev` +``` +feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main +``` -## Branch Policy +### Step by step -- `feature/*`, `fix/*` branches target `dev` -- `hotfix/*` branches may target `dev` or `main` -- `dev` merges to `main` for releases +1. **Create a feature branch** from `dev`: + ```bash + git checkout dev && git pull + git checkout -b feature/my-change + ``` + +2. **Work and commit** on your feature branch. Push to origin. + +3. **Open a PR**: `feature/my-change` → `dev`. After review and checks, merge it. + +4. **When ready for release**, open a **draft PR**: `dev` → `main`. + - This automatically renames the source branch to `rc` (release candidate) + - An RC pre-release is built and uploaded + +5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage: + - Rename `dev` to `alpha` for early testing → alpha pre-release is built + - Rename `alpha` to `beta` for feature-complete testing → beta pre-release is built + - When the draft PR is created, the branch is renamed to `rc` + +6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`. + +7. **Merging to main** triggers the stable release pipeline: + - Minor version bump (e.g., `02.09.xx` → `02.10.00`) + - Stability suffix stripped (clean version) + - Gitea release created with ZIP/tar.gz packages + - `updates.xml` updated (Joomla extensions) + - `dev` branch recreated from `main` + +### Branch summary + +| Branch | Purpose | Created by | +|--------|---------|-----------| +| `feature/*` | New features and fixes | Developer | +| `dev` | Integration branch | Auto-recreated after release | +| `alpha` | Alpha pre-release testing | Manual rename from `dev` | +| `beta` | Beta pre-release testing | Manual rename from `alpha` | +| `rc` | Release candidate | Auto-renamed on draft PR to main | +| `main` | Stable releases | Protected, merge only | +| `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI | + +### Protected branches + +| Branch | Direct push | Merge via | +|--------|------------|-----------| +| `main` | Blocked (CI bot whitelisted) | PR merge only | +| `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* | +| `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR | +| `alpha` | Blocked (CI bot whitelisted) | Manual rename | +| `beta` | Blocked (CI bot whitelisted) | Manual rename | +| `feature/*` | Open | N/A (source branch) | + +## Version Policy + +### Format + +All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded: + +- **XX** — Major version (breaking changes) +- **YY** — Minor version (new features, bumped on release to main) +- **ZZ** — Patch version (auto-incremented on every push to dev/feature branches) + +Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major. + +### Stability suffixes + +Each branch appends a suffix to indicate stability: + +| Branch | Suffix | Example | +|--------|--------|---------| +| `main` | (none) | `02.09.00` | +| `dev` | `-dev` | `02.09.01-dev` | +| `feature/*` | `-dev` | `02.09.01-dev` | +| `alpha` | `-alpha` | `02.09.01-alpha` | +| `beta` | `-beta` | `02.09.01-beta` | +| `rc` | `-rc` | `02.09.01-rc` | + +### Auto version bump + +On every push to `dev`, `alpha`, `beta`, `rc`, or `feature/*`: + +1. Patch version incremented +2. Stability suffix applied based on branch name +3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.) +4. Commit created with `[skip ci]` to avoid loops + +### Version files + +The version tools update all files containing version stamps: + +- `.mokogitea/manifest.xml` (canonical source) +- Joomla XML manifests (`` tag) +- `README.md`, `CHANGELOG.md` (`VERSION:` pattern) +- `package.json`, `pyproject.toml` +- Any text file with a `VERSION: XX.YY.ZZ` label + +Files synced from other repos (with a `# REPO:` header) are not touched. ## Code Standards -- PHP: follow PSR-12, use tabs for indentation -- All files must include the Moko copyright header and SPDX identifier -- Scripts must be self-contained (no external dependencies unless via composer) +- **PHP**: PSR-12, tabs for indentation +- **Copyright**: all files must include the Moko Consulting copyright header +- **License**: SPDX identifier `GPL-3.0-or-later` (or as specified per repo) +- **Attribution**: use `Authored-by: Moko Consulting` in commits, not individual names + +## Commit Messages + +Use conventional commit format: + +``` +type(scope): short description + +Optional body with context. + +Authored-by: Moko Consulting +``` + +Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `ci` + +Special flags in commit messages: +- `[skip ci]` — skip all CI workflows +- `[skip bump]` — skip auto version bump only ## Reporting Issues -Use the [issue tracker](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/issues) with the appropriate template. +Use the repository's issue tracker with the appropriate template. --- diff --git a/cli/branch_rename.php b/cli/branch_rename.php new file mode 100644 index 0000000..fa68a68 --- /dev/null +++ b/cli/branch_rename.php @@ -0,0 +1,138 @@ +#!/usr/bin/env php + + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: moko-platform.CLI + * INGROUP: moko-platform + * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform + * PATH: /cli/branch_rename.php + * VERSION: 01.00.00 + * BRIEF: Rename a git branch via Gitea API (create new, update PR, delete old) + * + * Usage: + * php branch_rename.php --from dev --to rc --token TOKEN --api-base URL [--pr 42] + * php branch_rename.php --from dev --to rc --token TOKEN --api-base URL --pr 42 --dry-run + */ + +declare(strict_types=1); + +$from = ''; +$to = ''; +$token = ''; +$apiBase = ''; +$prNum = ''; +$dryRun = false; + +foreach ($argv as $i => $arg) { + if ($arg === '--from' && isset($argv[$i + 1])) $from = $argv[$i + 1]; + if ($arg === '--to' && isset($argv[$i + 1])) $to = $argv[$i + 1]; + if ($arg === '--token' && isset($argv[$i + 1])) $token = $argv[$i + 1]; + if ($arg === '--api-base' && isset($argv[$i + 1])) $apiBase = $argv[$i + 1]; + if ($arg === '--pr' && isset($argv[$i + 1])) $prNum = $argv[$i + 1]; + if ($arg === '--dry-run') $dryRun = true; +} + +if (empty($from) || empty($to) || empty($token) || empty($apiBase)) { + fwrite(STDERR, "Usage: branch_rename.php --from BRANCH --to BRANCH --token TOKEN --api-base URL [--pr NUM] [--dry-run]\n"); + exit(1); +} + +if ($from === $to) { + echo "Source and target are the same ({$from}) — nothing to do\n"; + exit(0); +} + +$headers = [ + "Authorization: token {$token}", + 'Content-Type: application/json', + 'Accept: application/json', +]; + +/** + * Make an API request. + */ +function apiRequest(string $method, string $url, array $headers, ?array $body = null): array +{ + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $url, + CURLOPT_CUSTOMREQUEST => $method, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_TIMEOUT => 30, + ]); + + if ($body !== null) { + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body)); + } + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + return [ + 'code' => $httpCode, + 'body' => json_decode($response ?: '{}', true) ?: [], + ]; +} + +// Step 1: Verify source branch exists +echo "Checking source branch: {$from}\n"; +$check = apiRequest('GET', "{$apiBase}/branches/{$from}", $headers); +if ($check['code'] !== 200) { + fwrite(STDERR, "Source branch '{$from}' not found (HTTP {$check['code']})\n"); + exit(1); +} + +// Step 2: Delete target branch if it already exists +$targetCheck = apiRequest('GET', "{$apiBase}/branches/{$to}", $headers); +if ($targetCheck['code'] === 200) { + echo "Target branch '{$to}' already exists — deleting\n"; + if (!$dryRun) { + apiRequest('DELETE', "{$apiBase}/branches/{$to}", $headers); + } +} + +// Step 3: Create new branch from source +echo "Creating branch: {$to} (from {$from})\n"; +if (!$dryRun) { + $create = apiRequest('POST', "{$apiBase}/branches", $headers, [ + 'new_branch_name' => $to, + 'old_branch_name' => $from, + ]); + if ($create['code'] < 200 || $create['code'] >= 300) { + fwrite(STDERR, "Failed to create branch '{$to}': HTTP {$create['code']}\n"); + fwrite(STDERR, json_encode($create['body']) . "\n"); + exit(1); + } +} + +// Step 4: Update PR head branch if PR number provided +if (!empty($prNum)) { + echo "Updating PR #{$prNum} head branch: {$from} -> {$to}\n"; + if (!$dryRun) { + $update = apiRequest('PATCH', "{$apiBase}/pulls/{$prNum}", $headers, [ + 'head' => $to, + ]); + if ($update['code'] < 200 || $update['code'] >= 300) { + fwrite(STDERR, "Warning: Could not update PR head branch (HTTP {$update['code']})\n"); + // Non-fatal — the PR may need manual update + } + } +} + +// Step 5: Delete old source branch +echo "Deleting old branch: {$from}\n"; +if (!$dryRun) { + $delete = apiRequest('DELETE', "{$apiBase}/branches/{$from}", $headers); + if ($delete['code'] !== 204 && $delete['code'] !== 200) { + fwrite(STDERR, "Warning: Could not delete old branch '{$from}' (HTTP {$delete['code']})\n"); + // Non-fatal — branch protection may prevent deletion + } +} + +echo "Renamed: {$from} -> {$to}\n"; +exit(0);