feat: universal branching workflow, RC promotion, branch rename #207
@@ -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
|
||||
|
||||
@@ -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] <gitea-actions[bot]@mokoconsulting.tech>"
|
||||
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:
|
||||
|
||||
@@ -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 }}" \
|
||||
|
||||
+126
-15
@@ -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 (`<version>` 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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
*
|
||||
* 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);
|
||||
Reference in New Issue
Block a user