chore: Sync MokoStandards v04.05 #121
372
.github/CLAUDE.md
vendored
372
.github/CLAUDE.md
vendored
@@ -1,22 +1,3 @@
|
||||
<!--
|
||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
|
||||
This file is part of a Moko Consulting project.
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: MokoStandards.Templates.GitHub
|
||||
INGROUP: MokoStandards.Templates
|
||||
REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
||||
PATH: /templates/github/copilot-instructions.joomla.md.template
|
||||
VERSION: XX.YY.ZZ
|
||||
BRIEF: GitHub Copilot custom instructions template for Joomla/MokoWaaS governed repositories
|
||||
NOTE: Synced to .github/copilot-instructions.md in all Joomla/WaaS repos via bulk sync.
|
||||
Tokens replaced at sync time: MokoCassiopeia, https://github.com/mokoconsulting-tech/MokoCassiopeia, {{EXTENSION_NAME}},
|
||||
{{EXTENSION_TYPE}}, {{EXTENSION_ELEMENT}}
|
||||
-->
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **🔧 AI Self-Update Required on First Use**
|
||||
>
|
||||
@@ -38,34 +19,135 @@ NOTE: Synced to .github/copilot-instructions.md in all Joomla/WaaS repos via bul
|
||||
> |---|---|
|
||||
> | `MokoCassiopeia` | The GitHub repository name (visible in the URL, `README.md` heading, or `git remote -v`) |
|
||||
> | `https://github.com/mokoconsulting-tech/MokoCassiopeia` | Full GitHub URL, e.g. `https://github.com/mokoconsulting-tech/<repo-name>` |
|
||||
> | `A modern enhancement layer for Joomla’s Cassiopeia template. MokoCassiopeia adds Font Awesome 7, Bootstrap 5 helpers, an automatic Table of Contents (TOC) utility, and optional expansions including Google Tag Manager and Google Analytics (GA4) hooks.` | First paragraph of `README.md` body, or the GitHub repo description |
|
||||
> | `{{EXTENSION_NAME}}` | The `<name>` element in `manifest.xml` at the repository root |
|
||||
> | `{{EXTENSION_TYPE}}` | The `type` attribute of the `<extension>` tag in `manifest.xml` (`component`, `module`, `plugin`, or `template`) |
|
||||
> | `{{EXTENSION_ELEMENT}}` | The `<element>` tag in `manifest.xml`, or the filename prefix (e.g. `com_myextension`, `mod_mymodule`) |
|
||||
>
|
||||
> ---
|
||||
|
||||
# MokoCassiopeia — GitHub Copilot Custom Instructions
|
||||
# What This Repo Is
|
||||
|
||||
## What This Repo Is
|
||||
**MokoCassiopeia** is a Moko Consulting **MokoWaaS** (Joomla) extension repository.
|
||||
|
||||
This is a **Moko Consulting MokoWaaS** (Joomla) repository governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). All coding standards, workflows, and policies are defined there and enforced here via bulk sync.
|
||||
A modern enhancement layer for Joomla’s Cassiopeia template. MokoCassiopeia adds Font Awesome 7, Bootstrap 5 helpers, an automatic Table of Contents (TOC) utility, and optional expansions including Google Tag Manager and Google Analytics (GA4) hooks.
|
||||
|
||||
Repository URL: https://github.com/mokoconsulting-tech/MokoCassiopeia
|
||||
Extension name: **{{EXTENSION_NAME}}**
|
||||
Extension type: **{{EXTENSION_TYPE}}** (`{{EXTENSION_ELEMENT}}`)
|
||||
Platform: **Joomla 4.x / MokoWaaS**
|
||||
Repository URL: https://github.com/mokoconsulting-tech/MokoCassiopeia
|
||||
|
||||
This repository is governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards) — the single source of truth for coding standards, file-header policies, GitHub Actions workflows, and Terraform configuration templates across all Moko Consulting repositories.
|
||||
|
||||
---
|
||||
|
||||
## Primary Language
|
||||
# Repo Structure
|
||||
|
||||
**PHP** (≥ 7.4) is the primary language for this Joomla extension. JavaScript may be used for frontend enhancements. YAML uses 2-space indentation. All other text files use tabs per `.editorconfig`.
|
||||
```
|
||||
MokoCassiopeia/
|
||||
├── manifest.xml # Joomla installer manifest (root — required)
|
||||
├── update.xml # Update server manifest (root — required)
|
||||
├── site/ # Frontend (site) code
|
||||
│ ├── controller.php
|
||||
│ ├── controllers/
|
||||
│ ├── models/
|
||||
│ └── views/
|
||||
├── admin/ # Backend (admin) code
|
||||
│ ├── controller.php
|
||||
│ ├── controllers/
|
||||
│ ├── models/
|
||||
│ ├── views/
|
||||
│ └── sql/
|
||||
├── language/ # Language INI files
|
||||
├── media/ # CSS, JS, images
|
||||
├── docs/ # Technical documentation
|
||||
├── tests/ # Test suite
|
||||
├── .github/
|
||||
│ ├── workflows/ # CI/CD workflows (synced from MokoStandards)
|
||||
│ ├── copilot-instructions.md
|
||||
│ └── CLAUDE.md # This file
|
||||
├── README.md # Version source of truth
|
||||
├── CHANGELOG.md
|
||||
├── CONTRIBUTING.md
|
||||
└── LICENSE # GPL-3.0-or-later
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Header — Always Required on New Files
|
||||
# Primary Language
|
||||
|
||||
Every new file needs a copyright header as its first content.
|
||||
**PHP** (≥ 7.4) is the primary language for this Joomla extension. YAML uses 2-space indentation. All other text files use tabs per `.editorconfig`.
|
||||
|
||||
---
|
||||
|
||||
# Version Management
|
||||
|
||||
**`README.md` is the single source of truth for the repository version.**
|
||||
|
||||
- **Bump the patch version on every PR** — increment `XX.YY.ZZ` (e.g. `01.02.03` → `01.02.04`) in `README.md` before opening the PR; the `sync-version-on-merge` workflow propagates it to all `FILE INFORMATION` headers automatically on merge.
|
||||
- Version format is zero-padded semver: `XX.YY.ZZ` (e.g. `01.02.03`).
|
||||
- Never hardcode a version number in body text — use the badge or FILE INFORMATION header only.
|
||||
|
||||
### Joomla Version Alignment
|
||||
|
||||
Three files must **always have the same version**:
|
||||
|
||||
| File | Where the version lives |
|
||||
|------|------------------------|
|
||||
| `README.md` | `FILE INFORMATION` block + badge |
|
||||
| `manifest.xml` | `<version>` tag |
|
||||
| `update.xml` | `<version>` in the most recent `<update>` block |
|
||||
|
||||
The `make release` command / release workflow syncs all three automatically.
|
||||
|
||||
---
|
||||
|
||||
# update.xml — Required in Repo Root
|
||||
|
||||
`update.xml` is the Joomla update server manifest. It allows Joomla installations to check for new versions of this extension via:
|
||||
|
||||
```xml
|
||||
<!-- In manifest.xml -->
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
|
||||
https://github.com/mokoconsulting-tech/MokoCassiopeia/raw/main/update.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Every release prepends a new `<update>` block at the top — older entries are preserved.
|
||||
- `<version>` in `update.xml` must exactly match `<version>` in `manifest.xml` and `README.md`.
|
||||
- `<downloadurl>` must be a publicly accessible GitHub Releases asset URL.
|
||||
- `<targetplatform version="4\.[0-9]+">` — backslash is literal (Joomla regex syntax).
|
||||
|
||||
Example `update.xml` entry for a new release:
|
||||
```xml
|
||||
<updates>
|
||||
<update>
|
||||
<name>{{EXTENSION_NAME}}</name>
|
||||
<description>MokoCassiopeia</description>
|
||||
<element>{{EXTENSION_ELEMENT}}</element>
|
||||
<type>{{EXTENSION_TYPE}}</type>
|
||||
<version>01.02.04</version>
|
||||
<infourl title="Release Information">https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/01.02.04</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="zip">
|
||||
https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/01.02.04/{{EXTENSION_ELEMENT}}-01.02.04.zip
|
||||
</downloadurl>
|
||||
</downloads>
|
||||
<targetplatform name="joomla" version="4\.[0-9]+" />
|
||||
<php_minimum>7.4</php_minimum>
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||
</update>
|
||||
</updates>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# File Header Requirements
|
||||
|
||||
Every new file **must** have a copyright header as its first content. JSON files, binary files, generated files, and third-party files are exempt.
|
||||
|
||||
**PHP:**
|
||||
```php
|
||||
@@ -80,141 +162,47 @@ Every new file needs a copyright header as its first content.
|
||||
* DEFGROUP: MokoCassiopeia.{{EXTENSION_TYPE}}
|
||||
* INGROUP: MokoCassiopeia
|
||||
* REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia
|
||||
* PATH: /path/to/file.php
|
||||
* PATH: /site/controllers/item.php
|
||||
* VERSION: XX.YY.ZZ
|
||||
* BRIEF: One-line description of purpose
|
||||
* BRIEF: One-line description of file purpose
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
```
|
||||
|
||||
**Markdown:**
|
||||
```markdown
|
||||
<!--
|
||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
|
||||
This file is part of a Moko Consulting project.
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: MokoCassiopeia.Documentation
|
||||
INGROUP: MokoCassiopeia
|
||||
REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia
|
||||
PATH: /docs/file.md
|
||||
VERSION: XX.YY.ZZ
|
||||
BRIEF: One-line description
|
||||
-->
|
||||
```
|
||||
|
||||
**YAML / Shell / XML:** Use the appropriate comment syntax with the same fields. JSON files are exempt.
|
||||
**Markdown / YAML / Shell / XML:** Use the appropriate comment syntax with the same fields.
|
||||
|
||||
---
|
||||
|
||||
## Version Management
|
||||
# Coding Standards
|
||||
|
||||
**`README.md` is the single source of truth for the repository version.**
|
||||
## Naming Conventions
|
||||
|
||||
- **Bump the patch version on every PR** — increment `XX.YY.ZZ` (e.g. `01.02.03` → `01.02.04`) in `README.md` before opening the PR; the `sync-version-on-merge` workflow propagates it automatically to all badges and `FILE INFORMATION` headers on merge to `main`.
|
||||
- The `VERSION: XX.YY.ZZ` field in `README.md` governs all other version references.
|
||||
- Version format is zero-padded semver: `XX.YY.ZZ` (e.g. `01.02.03`).
|
||||
- Never hardcode a specific version in document body text — use the badge or FILE INFORMATION header only.
|
||||
| Context | Convention | Example |
|
||||
|---------|-----------|---------|
|
||||
| PHP class | `PascalCase` | `ItemModel` |
|
||||
| PHP method / function | `camelCase` | `getItems()` |
|
||||
| PHP variable | `$snake_case` | `$item_id` |
|
||||
| PHP constant | `UPPER_SNAKE_CASE` | `MAX_ITEMS` |
|
||||
| PHP class file | `PascalCase.php` | `ItemModel.php` |
|
||||
| YAML workflow | `kebab-case.yml` | `ci-joomla.yml` |
|
||||
| Markdown doc | `kebab-case.md` | `installation-guide.md` |
|
||||
|
||||
### Joomla Version Alignment
|
||||
## Commit Messages
|
||||
|
||||
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `updates.xml`. The `make release` command / release workflow updates all three automatically.
|
||||
Format: `<type>(<scope>): <subject>` — imperative, lower-case subject, no trailing period.
|
||||
|
||||
```xml
|
||||
<!-- In manifest.xml — must match README.md version -->
|
||||
<version>01.02.04</version>
|
||||
Valid types: `feat` · `fix` · `docs` · `chore` · `ci` · `refactor` · `style` · `test` · `perf` · `revert` · `build`
|
||||
|
||||
<!-- In updates.xml — prepend a new <update> block for every release.
|
||||
Note: the backslash in version="4\.[0-9]+" is a literal backslash character
|
||||
in the XML attribute value. Joomla's update server treats the value as a
|
||||
regular expression, so \. matches a literal dot. -->
|
||||
<updates>
|
||||
<update>
|
||||
<name>{{EXTENSION_NAME}}</name>
|
||||
<version>01.02.04</version>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="zip">
|
||||
https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/01.02.04/{{EXTENSION_ELEMENT}}-01.02.04.zip
|
||||
</downloadurl>
|
||||
</downloads>
|
||||
<targetplatform name="joomla" version="4\.[0-9]+" />
|
||||
</update>
|
||||
<!-- … older entries preserved below … -->
|
||||
</updates>
|
||||
```
|
||||
## Branch Naming
|
||||
|
||||
Format: `<prefix>/<MAJOR.MINOR.PATCH>[/description]`
|
||||
|
||||
Approved prefixes: `dev/` · `rc/` · `version/` · `patch/` · `copilot/` · `dependabot/`
|
||||
|
||||
---
|
||||
|
||||
## Joomla Extension Structure
|
||||
|
||||
```
|
||||
MokoCassiopeia/
|
||||
├── manifest.xml # Joomla installer manifest (root — required)
|
||||
├── updates.xml # Update server manifest (root — required, see below)
|
||||
├── site/ # Frontend (site) code
|
||||
│ ├── controller.php
|
||||
│ ├── controllers/
|
||||
│ ├── models/
|
||||
│ └── views/
|
||||
├── admin/ # Backend (admin) code
|
||||
│ ├── controller.php
|
||||
│ ├── controllers/
|
||||
│ ├── models/
|
||||
│ ├── views/
|
||||
│ └── sql/
|
||||
├── language/ # Language INI files
|
||||
├── media/ # CSS, JS, images (deployed to /media/{{EXTENSION_ELEMENT}}/)
|
||||
├── docs/ # Technical documentation
|
||||
├── tests/ # Test suite
|
||||
├── .github/
|
||||
│ ├── workflows/
|
||||
│ ├── copilot-instructions.md # This file
|
||||
│ └── CLAUDE.md
|
||||
├── README.md # Version source of truth
|
||||
├── CHANGELOG.md
|
||||
├── CONTRIBUTING.md
|
||||
├── LICENSE # GPL-3.0-or-later
|
||||
└── Makefile # Build automation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## updates.xml — Required in Repo Root
|
||||
|
||||
`updates.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
|
||||
|
||||
The `manifest.xml` must reference it via:
|
||||
```xml
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
|
||||
https://github.com/mokoconsulting-tech/MokoCassiopeia/raw/main/updates.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Every release must prepend a new `<update>` block at the top of `updates.xml` — old entries must be preserved below.
|
||||
- The `<version>` in `updates.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
|
||||
- The `<downloadurl>` must be a publicly accessible direct download link (GitHub Releases asset URL).
|
||||
- `<targetplatform name="joomla" version="4\.[0-9]+">` — the backslash is a **literal backslash character** in the XML attribute value; Joomla's update-server parser treats the value as a regular expression, so `\.` matches a literal dot and `[0-9]+` matches one or more digits. Do not double-escape it.
|
||||
|
||||
---
|
||||
|
||||
## manifest.xml Rules
|
||||
|
||||
- Lives at the repo root as `manifest.xml` (not inside `site/` or `admin/`).
|
||||
- `<version>` tag must be kept in sync with `README.md` version and `updates.xml`.
|
||||
- Must include `<updateservers>` block pointing to this repo's `updates.xml`.
|
||||
- Must include `<files folder="site">` and `<administration>` sections.
|
||||
- Joomla 4.x requires `<namespace path="src">Moko\{{EXTENSION_NAME}}</namespace>` for namespaced extensions.
|
||||
|
||||
---
|
||||
|
||||
## GitHub Actions — Token Usage
|
||||
# GitHub Actions — Token Usage
|
||||
|
||||
Every workflow must use **`secrets.GH_TOKEN`** (the org-level Personal Access Token).
|
||||
|
||||
@@ -229,76 +217,58 @@ env:
|
||||
```
|
||||
|
||||
```yaml
|
||||
# ❌ Wrong — never use these in workflows
|
||||
# ❌ Wrong — never use these
|
||||
token: ${{ github.token }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## MokoStandards Reference
|
||||
|
||||
This repository is governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). Authoritative policies:
|
||||
|
||||
| Document | Purpose |
|
||||
|----------|---------|
|
||||
| [file-header-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/file-header-standards.md) | Copyright-header rules for every file type |
|
||||
| [coding-style-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md) | Naming and formatting conventions |
|
||||
| [branching-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/branching-strategy.md) | Branch naming, hierarchy, and release workflow |
|
||||
| [merge-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR title/body conventions |
|
||||
| [changelog-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md |
|
||||
| [joomla-development-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/waas/joomla-development-guide.md) | MokoWaaS Joomla extension development guide |
|
||||
|
||||
---
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
| Context | Convention | Example |
|
||||
|---------|-----------|---------|
|
||||
| PHP class | `PascalCase` | `MyController` |
|
||||
| PHP method / function | `camelCase` | `getItems()` |
|
||||
| PHP variable | `$snake_case` | `$item_id` |
|
||||
| PHP constant | `UPPER_SNAKE_CASE` | `MAX_ITEMS` |
|
||||
| PHP class file | `PascalCase.php` | `ItemModel.php` |
|
||||
| YAML workflow | `kebab-case.yml` | `ci-joomla.yml` |
|
||||
| Markdown doc | `kebab-case.md` | `installation-guide.md` |
|
||||
|
||||
---
|
||||
|
||||
## Commit Messages
|
||||
|
||||
Format: `<type>(<scope>): <subject>` — imperative, lower-case subject, no trailing period.
|
||||
|
||||
Valid types: `feat` · `fix` · `docs` · `chore` · `ci` · `refactor` · `style` · `test` · `perf` · `revert` · `build`
|
||||
|
||||
---
|
||||
|
||||
## Branch Naming
|
||||
|
||||
Format: `<prefix>/<MAJOR.MINOR.PATCH>[/description]`
|
||||
|
||||
Approved prefixes: `dev/` · `rc/` · `version/` · `patch/` · `copilot/` · `dependabot/`
|
||||
|
||||
---
|
||||
|
||||
## Keeping Documentation Current
|
||||
# Keeping Documentation Current
|
||||
|
||||
| Change type | Documentation to update |
|
||||
|-------------|------------------------|
|
||||
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
|
||||
| New or changed manifest.xml | Update `updates.xml` version; bump README.md version |
|
||||
| New release | Prepend `<update>` block to `updates.xml`; update CHANGELOG.md; bump README.md version |
|
||||
| New or changed `manifest.xml` | Sync version to `update.xml` and `README.md` |
|
||||
| New release | Prepend `<update>` to `update.xml`; update `CHANGELOG.md`; bump `README.md` |
|
||||
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
|
||||
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
|
||||
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
|
||||
|
||||
---
|
||||
|
||||
## Key Constraints
|
||||
# What NOT to Do
|
||||
|
||||
- Never commit directly to `main` — all changes go via PR, squash-merged
|
||||
- Never skip the FILE INFORMATION block on a new file
|
||||
- Never add `defined('_JEXEC') or die;` to CLI scripts or model tests — only to web-accessible PHP files
|
||||
- Never hardcode version numbers in body text — update `README.md` and let automation propagate
|
||||
- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows — always use `secrets.GH_TOKEN`
|
||||
- Never let `manifest.xml` version, `updates.xml` version, and `README.md` version go out of sync
|
||||
- **Never commit directly to `main`** — all changes go through a PR.
|
||||
- **Never hardcode version numbers** in body text — update `README.md` and let automation propagate.
|
||||
- **Never let `manifest.xml`, `update.xml`, and `README.md` versions diverge.**
|
||||
- **Never skip the FILE INFORMATION block** on a new source file.
|
||||
- **Never use bare `catch (\Throwable $e) {}`** — always log or re-throw.
|
||||
- **Never mix tabs and spaces** within a file — follow `.editorconfig`.
|
||||
- **Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows** — always use `secrets.GH_TOKEN`.
|
||||
- **Never remove `defined('_JEXEC') or die;`** from web-accessible PHP files.
|
||||
|
||||
---
|
||||
|
||||
# PR Checklist
|
||||
|
||||
Before opening a PR, verify:
|
||||
|
||||
- [ ] Patch version bumped in `README.md` (e.g. `01.02.03` → `01.02.04`)
|
||||
- [ ] If this is a release: `manifest.xml` version updated; `update.xml` updated with new entry
|
||||
- [ ] FILE INFORMATION headers updated in modified files
|
||||
- [ ] CHANGELOG.md updated
|
||||
- [ ] Tests pass
|
||||
|
||||
---
|
||||
|
||||
# Key Policy Documents (MokoStandards)
|
||||
|
||||
| Document | Purpose |
|
||||
|----------|---------|
|
||||
| [file-header-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/file-header-standards.md) | Copyright-header rules for every file type |
|
||||
| [coding-style-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md) | Naming and formatting conventions |
|
||||
| [branching-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/branching-strategy.md) | Branch naming, hierarchy, and release workflow |
|
||||
| [merge-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR conventions |
|
||||
| [changelog-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md |
|
||||
| [joomla-development-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/waas/joomla-development-guide.md) | MokoWaaS Joomla extension development guide |
|
||||
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
@@ -11,7 +11,7 @@
|
||||
# ── Synced workflows (managed by MokoStandards — do not edit manually) ────
|
||||
/.github/workflows/deploy-dev.yml @jmiller-moko
|
||||
/.github/workflows/deploy-demo.yml @jmiller-moko
|
||||
/.github/workflows/deploy-manual.yml @jmiller-moko
|
||||
/.github/workflows/deploy-rs.yml @jmiller-moko
|
||||
/.github/workflows/auto-release.yml @jmiller-moko
|
||||
/.github/workflows/auto-dev-issue.yml @jmiller-moko
|
||||
/.github/workflows/auto-assign.yml @jmiller-moko
|
||||
@@ -23,11 +23,9 @@
|
||||
/.github/workflows/repo_health.yml @jmiller-moko
|
||||
/.github/workflows/ci-joomla.yml @jmiller-moko
|
||||
/.github/workflows/update-server.yml @jmiller-moko
|
||||
/.github/workflows/deploy-manual.yml @jmiller-moko
|
||||
/.github/workflows/ci-dolibarr.yml @jmiller-moko
|
||||
/.github/workflows/publish-to-mokodolimods.yml @jmiller-moko
|
||||
/.github/workflows/changelog-validation.yml @jmiller-moko
|
||||
/.github/workflows/branch-freeze.yml @jmiller-moko
|
||||
# Custom workflows in .github/workflows/ not listed above are repo-owned.
|
||||
|
||||
# ── GitHub configuration ─────────────────────────────────────────────────
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/firewall-request.md
vendored
2
.github/ISSUE_TEMPLATE/firewall-request.md
vendored
@@ -3,7 +3,7 @@ name: Firewall Request
|
||||
about: Request firewall rule changes or access to external resources
|
||||
title: '[FIREWALL] [Resource Name] - [Brief Description]'
|
||||
labels: ['firewall-request', 'infrastructure', 'security']
|
||||
assignees: []
|
||||
assignees: ['jmiller-moko']
|
||||
---
|
||||
|
||||
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/question.md
vendored
2
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -3,7 +3,7 @@ name: Question
|
||||
about: Ask a question about usage, features, or best practices
|
||||
title: '[QUESTION] '
|
||||
labels: ['question']
|
||||
assignees: []
|
||||
assignees: ['jmiller-moko']
|
||||
---
|
||||
|
||||
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/request-license.md
vendored
2
.github/ISSUE_TEMPLATE/request-license.md
vendored
@@ -3,7 +3,7 @@ name: License Request
|
||||
about: Request an organization license for Sublime Text
|
||||
title: '[LICENSE REQUEST] Sublime Text - [Your Name]'
|
||||
labels: ['license-request', 'admin']
|
||||
assignees: []
|
||||
assignees: ['jmiller-moko']
|
||||
---
|
||||
|
||||
|
||||
|
||||
45
.github/copilot-instructions.md
vendored
45
.github/copilot-instructions.md
vendored
@@ -1,22 +1,3 @@
|
||||
<!--
|
||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
|
||||
This file is part of a Moko Consulting project.
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: MokoStandards.Templates.GitHub
|
||||
INGROUP: MokoStandards.Templates
|
||||
REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
||||
PATH: /templates/github/copilot-instructions.joomla.md.template
|
||||
VERSION: XX.YY.ZZ
|
||||
BRIEF: GitHub Copilot custom instructions template for Joomla/MokoWaaS governed repositories
|
||||
NOTE: Synced to .github/copilot-instructions.md in all Joomla/WaaS repos via bulk sync.
|
||||
Tokens replaced at sync time: MokoCassiopeia, https://github.com/mokoconsulting-tech/MokoCassiopeia, {{EXTENSION_NAME}},
|
||||
{{EXTENSION_TYPE}}, {{EXTENSION_ELEMENT}}
|
||||
-->
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **🔧 AI Self-Update Required on First Use**
|
||||
>
|
||||
@@ -122,13 +103,13 @@ BRIEF: One-line description
|
||||
|
||||
### Joomla Version Alignment
|
||||
|
||||
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `updates.xml`. The `make release` command / release workflow updates all three automatically.
|
||||
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `update.xml`. The `make release` command / release workflow updates all three automatically.
|
||||
|
||||
```xml
|
||||
<!-- In manifest.xml — must match README.md version -->
|
||||
<version>01.02.04</version>
|
||||
|
||||
<!-- In updates.xml — prepend a new <update> block for every release.
|
||||
<!-- In update.xml — prepend a new <update> block for every release.
|
||||
Note: the backslash in version="4\.[0-9]+" is a literal backslash character
|
||||
in the XML attribute value. Joomla's update server treats the value as a
|
||||
regular expression, so \. matches a literal dot. -->
|
||||
@@ -154,7 +135,7 @@ The version in `README.md` **must always match** the `<version>` tag in `manifes
|
||||
```
|
||||
MokoCassiopeia/
|
||||
├── manifest.xml # Joomla installer manifest (root — required)
|
||||
├── updates.xml # Update server manifest (root — required, see below)
|
||||
├── update.xml # Update server manifest (root — required, see below)
|
||||
├── site/ # Frontend (site) code
|
||||
│ ├── controller.php
|
||||
│ ├── controllers/
|
||||
@@ -183,22 +164,22 @@ MokoCassiopeia/
|
||||
|
||||
---
|
||||
|
||||
## updates.xml — Required in Repo Root
|
||||
## update.xml — Required in Repo Root
|
||||
|
||||
`updates.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
|
||||
`update.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
|
||||
|
||||
The `manifest.xml` must reference it via:
|
||||
```xml
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
|
||||
https://github.com/mokoconsulting-tech/MokoCassiopeia/raw/main/updates.xml
|
||||
https://github.com/mokoconsulting-tech/MokoCassiopeia/raw/main/update.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Every release must prepend a new `<update>` block at the top of `updates.xml` — old entries must be preserved below.
|
||||
- The `<version>` in `updates.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
|
||||
- Every release must prepend a new `<update>` block at the top of `update.xml` — old entries must be preserved below.
|
||||
- The `<version>` in `update.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
|
||||
- The `<downloadurl>` must be a publicly accessible direct download link (GitHub Releases asset URL).
|
||||
- `<targetplatform name="joomla" version="4\.[0-9]+">` — the backslash is a **literal backslash character** in the XML attribute value; Joomla's update-server parser treats the value as a regular expression, so `\.` matches a literal dot and `[0-9]+` matches one or more digits. Do not double-escape it.
|
||||
|
||||
@@ -207,8 +188,8 @@ The `manifest.xml` must reference it via:
|
||||
## manifest.xml Rules
|
||||
|
||||
- Lives at the repo root as `manifest.xml` (not inside `site/` or `admin/`).
|
||||
- `<version>` tag must be kept in sync with `README.md` version and `updates.xml`.
|
||||
- Must include `<updateservers>` block pointing to this repo's `updates.xml`.
|
||||
- `<version>` tag must be kept in sync with `README.md` version and `update.xml`.
|
||||
- Must include `<updateservers>` block pointing to this repo's `update.xml`.
|
||||
- Must include `<files folder="site">` and `<administration>` sections.
|
||||
- Joomla 4.x requires `<namespace path="src">Moko\{{EXTENSION_NAME}}</namespace>` for namespaced extensions.
|
||||
|
||||
@@ -286,8 +267,8 @@ Approved prefixes: `dev/` · `rc/` · `version/` · `patch/` · `copilot/` · `d
|
||||
| Change type | Documentation to update |
|
||||
|-------------|------------------------|
|
||||
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
|
||||
| New or changed manifest.xml | Update `updates.xml` version; bump README.md version |
|
||||
| New release | Prepend `<update>` block to `updates.xml`; update CHANGELOG.md; bump README.md version |
|
||||
| New or changed manifest.xml | Update `update.xml` version; bump README.md version |
|
||||
| New release | Prepend `<update>` block to `update.xml`; update CHANGELOG.md; bump README.md version |
|
||||
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
|
||||
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
|
||||
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
|
||||
@@ -301,4 +282,4 @@ Approved prefixes: `dev/` · `rc/` · `version/` · `patch/` · `copilot/` · `d
|
||||
- Never add `defined('_JEXEC') or die;` to CLI scripts or model tests — only to web-accessible PHP files
|
||||
- Never hardcode version numbers in body text — update `README.md` and let automation propagate
|
||||
- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows — always use `secrets.GH_TOKEN`
|
||||
- Never let `manifest.xml` version, `updates.xml` version, and `README.md` version go out of sync
|
||||
- Never let `manifest.xml` version, `update.xml` version, and `README.md` version go out of sync
|
||||
2
.github/workflows/auto-assign.yml
vendored
2
.github/workflows/auto-assign.yml
vendored
@@ -6,7 +6,7 @@
|
||||
# INGROUP: MokoStandards.Workflows.Shared
|
||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
||||
# PATH: /.github/workflows/auto-assign.yml
|
||||
# VERSION: 04.06.00
|
||||
# VERSION: 04.05.11
|
||||
# BRIEF: Auto-assign jmiller-moko to unassigned issues and PRs every 15 minutes
|
||||
|
||||
name: Auto-Assign Issues & PRs
|
||||
|
||||
67
.github/workflows/auto-dev-issue.yml
vendored
67
.github/workflows/auto-dev-issue.yml
vendored
@@ -8,8 +8,8 @@
|
||||
# DEFGROUP: GitHub.Workflow
|
||||
# INGROUP: MokoStandards.Automation
|
||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
||||
# PATH: /templates/workflows/shared/auto-dev-issue.yml.template
|
||||
# VERSION: 04.06.00
|
||||
# PATH: /templates/workflows/shared/auto-dev-issue.yml
|
||||
# VERSION: 04.05.13
|
||||
# BRIEF: Auto-create tracking issue with sub-issues for dev/rc branch workflow
|
||||
# NOTE: Synced via bulk-repo-sync to .github/workflows/auto-dev-issue.yml in all governed repos.
|
||||
|
||||
@@ -39,10 +39,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
(github.event_name == 'workflow_dispatch') ||
|
||||
(github.event.ref_type == 'branch' &&
|
||||
(startsWith(github.event.ref, 'rc/') ||
|
||||
startsWith(github.event.ref, 'alpha/') ||
|
||||
startsWith(github.event.ref, 'beta/')))
|
||||
(github.event.ref_type == 'branch' && startsWith(github.event.ref, 'rc/'))
|
||||
|
||||
steps:
|
||||
- name: Create tracking issue and sub-issues
|
||||
@@ -65,16 +62,6 @@ jobs:
|
||||
BRANCH_TYPE="Release Candidate"
|
||||
LABEL_TYPE="type: release"
|
||||
TITLE_PREFIX="rc"
|
||||
elif [[ "$BRANCH" == beta/* ]]; then
|
||||
VERSION="${BRANCH#beta/}"
|
||||
BRANCH_TYPE="Beta"
|
||||
LABEL_TYPE="type: release"
|
||||
TITLE_PREFIX="beta"
|
||||
elif [[ "$BRANCH" == alpha/* ]]; then
|
||||
VERSION="${BRANCH#alpha/}"
|
||||
BRANCH_TYPE="Alpha"
|
||||
LABEL_TYPE="type: release"
|
||||
TITLE_PREFIX="alpha"
|
||||
else
|
||||
VERSION="${BRANCH#dev/}"
|
||||
BRANCH_TYPE="Development"
|
||||
@@ -93,20 +80,14 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Define sub-issues for the workflow ─────────────────────────
|
||||
# ── Define sub-issues for the dev workflow ────────────────────────
|
||||
if [[ "$BRANCH" == rc/* ]]; then
|
||||
SUB_ISSUES=(
|
||||
"RC Testing|Verify all features work on rc branch|type: test,release-candidate"
|
||||
"Regression Testing|Run full regression suite before merge|type: test,release-candidate"
|
||||
"Regression Testing|Run full regression suite before merge to main|type: test,release-candidate"
|
||||
"Version Bump|Bump version in README.md and all headers|type: version,release-candidate"
|
||||
"Changelog Update|Update CHANGELOG.md with release notes|documentation,release-candidate"
|
||||
"Merge to Version Branch|Create PR to version/XX|type: release,needs-review"
|
||||
)
|
||||
elif [[ "$BRANCH" == alpha/* ]] || [[ "$BRANCH" == beta/* ]]; then
|
||||
SUB_ISSUES=(
|
||||
"Testing|Verify features on ${BRANCH_TYPE} branch|type: test,status: in-progress"
|
||||
"Bug Fixes|Fix issues found during ${BRANCH_TYPE} testing|type: bug,status: pending"
|
||||
"Promote to Next Stage|Create PR to promote to next release stage|type: release,needs-review"
|
||||
"Merge to Main|Create PR from rc branch to main|type: release,needs-review"
|
||||
)
|
||||
else
|
||||
SUB_ISSUES=(
|
||||
@@ -175,26 +156,30 @@ jobs:
|
||||
done
|
||||
fi
|
||||
|
||||
# ── Create or update prerelease for alpha/beta/rc ────────────────
|
||||
if [[ "$BRANCH" == rc/* ]] || [[ "$BRANCH" == alpha/* ]] || [[ "$BRANCH" == beta/* ]]; then
|
||||
case "$BRANCH_TYPE" in
|
||||
Alpha) RELEASE_TAG="alpha" ;;
|
||||
Beta) RELEASE_TAG="beta" ;;
|
||||
"Release Candidate") RELEASE_TAG="release-candidate" ;;
|
||||
esac
|
||||
# ── RC: Create or update draft release ────────────────────────────
|
||||
if [[ "$BRANCH" == rc/* ]]; then
|
||||
MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
|
||||
RELEASE_TAG="v${MAJOR}"
|
||||
DRAFT_EXISTS=$(gh release view "$RELEASE_TAG" --json isDraft -q .isDraft 2>/dev/null || true)
|
||||
|
||||
EXISTING=$(gh release view "$RELEASE_TAG" --json tagName -q .tagName 2>/dev/null || true)
|
||||
if [ -z "$EXISTING" ]; then
|
||||
if [ -z "$DRAFT_EXISTS" ]; then
|
||||
# No release exists — create draft
|
||||
gh release create "$RELEASE_TAG" \
|
||||
--title "${RELEASE_TAG} (${VERSION})" \
|
||||
--notes "## ${BRANCH_TYPE} ${VERSION}\n\nBranch: \`${BRANCH}\`\nTracking issue: ${PARENT_URL}" \
|
||||
--prerelease \
|
||||
--title "v${MAJOR} (RC: ${VERSION})" \
|
||||
--notes "## Release Candidate ${VERSION}\n\nRC branch: \`${BRANCH}\`\nTracking issue: ${PARENT_URL}" \
|
||||
--draft \
|
||||
--target main 2>/dev/null || true
|
||||
echo "${BRANCH_TYPE} release created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "Draft release created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
|
||||
elif [ "$DRAFT_EXISTS" = "true" ]; then
|
||||
# Draft exists — update title
|
||||
gh release edit "$RELEASE_TAG" \
|
||||
--title "${RELEASE_TAG} (${VERSION})" --prerelease 2>/dev/null || true
|
||||
echo "${BRANCH_TYPE} release updated: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
|
||||
--title "v${MAJOR} (RC: ${VERSION})" --draft 2>/dev/null || true
|
||||
echo "Draft release updated: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
# Release exists and is published — set back to draft for RC
|
||||
gh release edit "$RELEASE_TAG" \
|
||||
--title "v${MAJOR} (RC: ${VERSION})" --draft 2>/dev/null || true
|
||||
echo "Release ${RELEASE_TAG} set to draft for RC" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
255
.github/workflows/auto-release.yml
vendored
255
.github/workflows/auto-release.yml
vendored
@@ -6,24 +6,22 @@
|
||||
# DEFGROUP: GitHub.Workflow
|
||||
# INGROUP: MokoStandards.Release
|
||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
||||
# PATH: /templates/workflows/joomla/auto-release.yml.template
|
||||
# VERSION: 04.06.00
|
||||
# BRIEF: Joomla build & release — ZIP package, updates.xml, SHA-256 checksum
|
||||
# PATH: /templates/workflows/shared/auto-release.yml
|
||||
# VERSION: 04.05.13
|
||||
# BRIEF: Generic build & release pipeline — version branch, platform version, badges, tag, release
|
||||
#
|
||||
# +========================================================================+
|
||||
# | BUILD & RELEASE PIPELINE (JOOMLA) |
|
||||
# | BUILD & RELEASE PIPELINE |
|
||||
# +========================================================================+
|
||||
# | |
|
||||
# | Triggers on push to main (skips bot commits + [skip ci]): |
|
||||
# | |
|
||||
# | Every push: |
|
||||
# | 1. Read version from README.md |
|
||||
# | 3. Set platform version (Joomla <version>) |
|
||||
# | 3. Set platform version |
|
||||
# | 4. Update [VERSION: XX.YY.ZZ] badges in markdown files |
|
||||
# | 5. Write updates.xml (Joomla update server XML) |
|
||||
# | 6. Create git tag vXX.YY.ZZ |
|
||||
# | 7a. Patch: update existing GitHub Release for this minor |
|
||||
# | 8. Build ZIP, upload asset, write SHA-256 to updates.xml |
|
||||
# | |
|
||||
# | Every version change: archives main -> version/XX.YY branch |
|
||||
# | Patch 00 = development (no release). First release = patch 01. |
|
||||
@@ -35,14 +33,13 @@
|
||||
name: Build & Release
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'htdocs/**'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
@@ -55,7 +52,8 @@ jobs:
|
||||
name: Build & Release Pipeline
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
|
||||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||
github.actor != 'github-actions[bot]'
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -143,11 +141,11 @@ jobs:
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
ERRORS=0
|
||||
|
||||
echo "## Pre-Release Sanity Checks (Joomla)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "## Pre-Release Sanity Checks" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# -- Version drift check (must pass before release) --------
|
||||
README_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1)
|
||||
README_VER=$(grep -oP 'VERSION:\s*\K[\d.]+' README.md 2>/dev/null | head -1)
|
||||
if [ "$README_VER" != "$VERSION" ]; then
|
||||
echo "- Version drift: README says \`${README_VER}\` but releasing \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS+1))
|
||||
@@ -156,7 +154,7 @@ jobs:
|
||||
fi
|
||||
|
||||
# Check CHANGELOG version matches
|
||||
CL_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' CHANGELOG.md 2>/dev/null | head -1)
|
||||
CL_VER=$(grep -oP 'VERSION:\s*\K[\d.]+' CHANGELOG.md 2>/dev/null | head -1)
|
||||
if [ -n "$CL_VER" ] && [ "$CL_VER" != "$VERSION" ]; then
|
||||
echo "- CHANGELOG drift: \`${CL_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS+1))
|
||||
@@ -164,7 +162,7 @@ jobs:
|
||||
|
||||
# Check composer.json version if present
|
||||
if [ -f "composer.json" ]; then
|
||||
COMP_VER=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' composer.json 2>/dev/null | head -1)
|
||||
COMP_VER=$(grep -oP '"version"\s*:\s*"\K[^"]+' composer.json 2>/dev/null | head -1)
|
||||
if [ -n "$COMP_VER" ] && [ "$COMP_VER" != "$VERSION" ]; then
|
||||
echo "- composer.json drift: \`${COMP_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS+1))
|
||||
@@ -185,30 +183,6 @@ jobs:
|
||||
echo "- Source directory present" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
# -- Joomla: manifest version drift --------
|
||||
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||
if [ -n "$MANIFEST" ]; then
|
||||
XML_VER=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
|
||||
if [ -n "$XML_VER" ] && [ "$XML_VER" != "$VERSION" ]; then
|
||||
echo "- Manifest drift: \`${XML_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS+1))
|
||||
else
|
||||
echo "- Manifest version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
fi
|
||||
|
||||
# -- Joomla: XML manifest existence --------
|
||||
if [ -z "$MANIFEST" ]; then
|
||||
echo "- No Joomla XML manifest found" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS+1))
|
||||
else
|
||||
echo "- Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# -- Joomla: extension type check --------
|
||||
TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null)
|
||||
echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
if [ "$ERRORS" -gt 0 ]; then
|
||||
echo "**${ERRORS} error(s) — release may be incomplete**" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -259,120 +233,6 @@ jobs:
|
||||
fi
|
||||
done
|
||||
|
||||
# -- STEP 5: Write updates.xml (Joomla update server) ---------------------
|
||||
- name: "Step 5: Write updates.xml"
|
||||
if: >-
|
||||
steps.version.outputs.skip != 'true' &&
|
||||
steps.check.outputs.already_released != 'true'
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
REPO="${{ github.repository }}"
|
||||
|
||||
# -- Parse extension metadata from XML manifest ----------------
|
||||
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||
if [ -z "$MANIFEST" ]; then
|
||||
echo "Warning: No Joomla XML manifest found — skipping updates.xml" >> $GITHUB_STEP_SUMMARY
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Extract fields using sed (portable — no grep -P)
|
||||
EXT_NAME=$(sed -n 's/.*<name>\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1)
|
||||
EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
||||
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1)
|
||||
EXT_CLIENT=$(sed -n 's/.*<extension[^>]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
||||
EXT_FOLDER=$(sed -n 's/.*<extension[^>]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
||||
TARGET_PLATFORM=$(sed -n 's/.*\(<targetplatform[^/]*\/>\).*/\1/p' "$MANIFEST" | head -1)
|
||||
PHP_MINIMUM=$(sed -n 's/.*<php_minimum>\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1)
|
||||
|
||||
# Fallbacks
|
||||
[ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}"
|
||||
[ -z "$EXT_TYPE" ] && EXT_TYPE="component"
|
||||
|
||||
# Templates/modules don't have <element> — derive from <name> (lowercased)
|
||||
if [ -z "$EXT_ELEMENT" ]; then
|
||||
EXT_ELEMENT=$(echo "$EXT_NAME" | tr '[:upper:]' '[:lower:]' | tr -d ' ')
|
||||
fi
|
||||
|
||||
# Build client tag: plugins and frontend modules need <client>site</client>
|
||||
CLIENT_TAG=""
|
||||
if [ -n "$EXT_CLIENT" ]; then
|
||||
CLIENT_TAG="<client>${EXT_CLIENT}</client>"
|
||||
elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then
|
||||
CLIENT_TAG="<client>site</client>"
|
||||
fi
|
||||
|
||||
# Build folder tag for plugins (required for Joomla to match the update)
|
||||
FOLDER_TAG=""
|
||||
if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then
|
||||
FOLDER_TAG="<folder>${EXT_FOLDER}</folder>"
|
||||
fi
|
||||
|
||||
# Build targetplatform (fallback to Joomla 5 if not in manifest)
|
||||
if [ -z "$TARGET_PLATFORM" ]; then
|
||||
TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="5.*" %s>' "/")
|
||||
fi
|
||||
|
||||
# Build php_minimum tag
|
||||
PHP_TAG=""
|
||||
if [ -n "$PHP_MINIMUM" ]; then
|
||||
PHP_TAG="<php_minimum>${PHP_MINIMUM}</php_minimum>"
|
||||
fi
|
||||
|
||||
DOWNLOAD_URL="https://github.com/${REPO}/releases/download/v${VERSION}/${EXT_ELEMENT}-${VERSION}.zip"
|
||||
INFO_URL="https://github.com/${REPO}/releases/tag/v${VERSION}"
|
||||
|
||||
# -- Build stable entry to temp file ─────────────────────────
|
||||
{
|
||||
printf '%s\n' ' <update>'
|
||||
printf '%s\n' " <name>${EXT_NAME}</name>"
|
||||
printf '%s\n' " <description>${EXT_NAME} update</description>"
|
||||
printf '%s\n' " <element>${EXT_ELEMENT}</element>"
|
||||
printf '%s\n' " <type>${EXT_TYPE}</type>"
|
||||
printf '%s\n' " <version>${VERSION}</version>"
|
||||
[ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}"
|
||||
[ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}"
|
||||
printf '%s\n' ' <tags>'
|
||||
printf '%s\n' ' <tag>stable</tag>'
|
||||
printf '%s\n' ' </tags>'
|
||||
printf '%s\n' " <infourl title=\"${EXT_NAME}\">${INFO_URL}</infourl>"
|
||||
printf '%s\n' ' <downloads>'
|
||||
printf '%s\n' " <downloadurl type=\"full\" format=\"zip\">${DOWNLOAD_URL}</downloadurl>"
|
||||
printf '%s\n' ' </downloads>'
|
||||
printf '%s\n' " ${TARGET_PLATFORM}"
|
||||
[ -n "$PHP_TAG" ] && printf '%s\n' " ${PHP_TAG}"
|
||||
printf '%s\n' ' <maintainer>Moko Consulting</maintainer>'
|
||||
printf '%s\n' ' <maintainerurl>https://mokoconsulting.tech</maintainerurl>'
|
||||
printf '%s\n' ' </update>'
|
||||
} > /tmp/stable_entry.xml
|
||||
|
||||
# -- Write updates.xml preserving dev/rc entries ──────────────
|
||||
# Extract existing entries for other stability levels
|
||||
# Order reflects release workflow: development → alpha → beta → rc → stable
|
||||
if [ -f "updates.xml" ]; then
|
||||
printf 'import re, sys\n' > /tmp/extract.py
|
||||
printf 'with open("updates.xml") as f: c = f.read()\n' >> /tmp/extract.py
|
||||
printf 'tag = sys.argv[1]\n' >> /tmp/extract.py
|
||||
printf 'm = re.search(r"( <update>.*?<tag>" + re.escape(tag) + r"</tag>.*?</update>)", c, re.DOTALL)\n' >> /tmp/extract.py
|
||||
printf 'if m: print(m.group(1))\n' >> /tmp/extract.py
|
||||
fi
|
||||
DEV_ENTRY=$(python3 /tmp/extract.py development 2>/dev/null || true)
|
||||
ALPHA_ENTRY=$(python3 /tmp/extract.py alpha 2>/dev/null || true)
|
||||
BETA_ENTRY=$(python3 /tmp/extract.py beta 2>/dev/null || true)
|
||||
RC_ENTRY=$(python3 /tmp/extract.py rc 2>/dev/null || true)
|
||||
|
||||
{
|
||||
printf '%s\n' '<?xml version="1.0" encoding="utf-8"?>'
|
||||
printf '%s\n' '<updates>'
|
||||
[ -n "$DEV_ENTRY" ] && echo "$DEV_ENTRY"
|
||||
[ -n "$ALPHA_ENTRY" ] && echo "$ALPHA_ENTRY"
|
||||
[ -n "$BETA_ENTRY" ] && echo "$BETA_ENTRY"
|
||||
[ -n "$RC_ENTRY" ] && echo "$RC_ENTRY"
|
||||
cat /tmp/stable_entry.xml
|
||||
printf '%s\n' '</updates>'
|
||||
} > updates.xml
|
||||
|
||||
echo "updates.xml: ${VERSION} (stable + rc/dev preserved)" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# -- Commit all changes ---------------------------------------------------
|
||||
- name: Commit release changes
|
||||
if: >-
|
||||
@@ -430,14 +290,14 @@ jobs:
|
||||
EXISTING=$(gh release view "$RELEASE_TAG" --json tagName -q .tagName 2>/dev/null || true)
|
||||
|
||||
if [ -z "$EXISTING" ]; then
|
||||
# First release for this major
|
||||
# First release for this major: create GitHub Release
|
||||
gh release create "$RELEASE_TAG" \
|
||||
--title "v${MAJOR} (latest: ${VERSION})" \
|
||||
--notes-file /tmp/release_notes.md \
|
||||
--target "$BRANCH"
|
||||
echo "Release created: ${RELEASE_TAG} (${VERSION})" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
# Append version notes to existing major release
|
||||
# Update existing major release with new version info
|
||||
CURRENT_NOTES=$(gh release view "$RELEASE_TAG" --json body -q .body 2>/dev/null || true)
|
||||
{
|
||||
echo "$CURRENT_NOTES"
|
||||
@@ -454,89 +314,6 @@ jobs:
|
||||
echo "Release updated: ${RELEASE_TAG} -> ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
# -- STEP 8: Build Joomla install ZIP + SHA-256 checksum ------------------
|
||||
# Every patch builds an install-ready ZIP and uploads it to the minor release.
|
||||
# Result: one Release per minor version with a ZIP for each patch.
|
||||
- name: "Step 8: Build Joomla package and update checksum"
|
||||
if: >-
|
||||
steps.version.outputs.skip != 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||
REPO="${{ github.repository }}"
|
||||
|
||||
# All ZIPs upload to the major release tag (vXX)
|
||||
gh release view "$RELEASE_TAG" --json tagName > /dev/null 2>&1 || {
|
||||
echo "No release ${RELEASE_TAG} found — skipping ZIP upload"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Find extension element name from manifest
|
||||
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)
|
||||
[ -z "$MANIFEST" ] && exit 0
|
||||
|
||||
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1 || basename "$MANIFEST" .xml)
|
||||
ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
|
||||
TAR_NAME="${EXT_ELEMENT}-${VERSION}.tar.gz"
|
||||
|
||||
# -- Build install packages from src/ ----------------------------
|
||||
SOURCE_DIR="src"
|
||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ — skipping package"; exit 0; }
|
||||
|
||||
EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*"
|
||||
|
||||
# ZIP package
|
||||
cd "$SOURCE_DIR"
|
||||
zip -r "/tmp/${ZIP_NAME}" . -x $EXCLUDES
|
||||
cd ..
|
||||
|
||||
# tar.gz package
|
||||
tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \
|
||||
--exclude='.ftpignore' --exclude='sftp-config*' \
|
||||
--exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' .
|
||||
|
||||
ZIP_SIZE=$(stat -c%s "/tmp/${ZIP_NAME}" 2>/dev/null || stat -f%z "/tmp/${ZIP_NAME}" 2>/dev/null || echo "unknown")
|
||||
TAR_SIZE=$(stat -c%s "/tmp/${TAR_NAME}" 2>/dev/null || stat -f%z "/tmp/${TAR_NAME}" 2>/dev/null || echo "unknown")
|
||||
|
||||
# -- Calculate SHA-256 for both ----------------------------------
|
||||
SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1)
|
||||
SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1)
|
||||
|
||||
# -- Upload both to release tag ----------------------------------
|
||||
gh release upload "$RELEASE_TAG" "/tmp/${ZIP_NAME}" --clobber 2>/dev/null || true
|
||||
gh release upload "$RELEASE_TAG" "/tmp/${TAR_NAME}" --clobber 2>/dev/null || true
|
||||
|
||||
# -- Update updates.xml with both download formats ---------------
|
||||
if [ -f "updates.xml" ]; then
|
||||
ZIP_URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}"
|
||||
TAR_URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${TAR_NAME}"
|
||||
|
||||
# Replace downloads block with both formats + SHA
|
||||
sed -i "s|<downloads>.*</downloads>|<downloads>\n <downloadurl type=\"full\" format=\"zip\">${ZIP_URL}</downloadurl>\n <downloadurl type=\"full\" format=\"tar.gz\">${TAR_URL}</downloadurl>\n </downloads>|" updates.xml 2>/dev/null || true
|
||||
if grep -q '<sha256>' updates.xml; then
|
||||
sed -i "s|<sha256>.*</sha256>|<sha256>sha256:${SHA256_ZIP}</sha256>|" updates.xml
|
||||
else
|
||||
sed -i "s|</downloads>|</downloads>\n <sha256>sha256:${SHA256_ZIP}</sha256>|" updates.xml
|
||||
fi
|
||||
|
||||
git add updates.xml
|
||||
git commit -m "chore(release): ZIP + tar.gz for ${VERSION} [skip ci]" \
|
||||
--author="github-actions[bot] <github-actions[bot]@users.noreply.github.com>" || true
|
||||
git push || true
|
||||
fi
|
||||
|
||||
echo "### Joomla Packages" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Package | Size | SHA-256 |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|---------|------|---------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| \`${ZIP_NAME}\` | ${ZIP_SIZE} | \`${SHA256_ZIP}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| \`${TAR_NAME}\` | ${TAR_SIZE} | \`${SHA256_TAR}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Release | \`${RELEASE_TAG}\` | |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Download | [${PACKAGE_NAME}](https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}) |" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# -- Summary --------------------------------------------------------------
|
||||
- name: Pipeline Summary
|
||||
if: always()
|
||||
@@ -549,7 +326,7 @@ jobs:
|
||||
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "## Build & Release Complete (Joomla)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "## Build & Release Complete" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
8
.github/workflows/changelog-validation.yml
vendored
8
.github/workflows/changelog-validation.yml
vendored
@@ -8,18 +8,20 @@
|
||||
# DEFGROUP: GitHub.Workflow.Template
|
||||
# INGROUP: MokoStandards.CI
|
||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
||||
# PATH: /templates/workflows/shared/changelog-validation.yml.template
|
||||
# VERSION: 04.06.00
|
||||
# PATH: /templates/workflows/shared/changelog-validation.yml
|
||||
# VERSION: 04.05.13
|
||||
# BRIEF: Validates CHANGELOG.md format and version consistency
|
||||
# NOTE: Deployed to .github/workflows/changelog-validation.yml in governed repos.
|
||||
|
||||
name: Changelog Validation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- 'dev/**'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
||||
13
.github/workflows/ci-joomla.yml
vendored
13
.github/workflows/ci-joomla.yml
vendored
@@ -8,18 +8,25 @@
|
||||
# DEFGROUP: GitHub.Workflow.Template
|
||||
# INGROUP: MokoStandards.CI
|
||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
||||
# PATH: /templates/workflows/joomla/ci-joomla.yml.template
|
||||
# VERSION: 04.06.00
|
||||
# PATH: /templates/workflows/joomla/ci-joomla.yml
|
||||
# VERSION: 04.05.13
|
||||
# BRIEF: CI workflow for Joomla extensions — lint, validate, test
|
||||
# NOTE: Deployed to .github/workflows/ci-joomla.yml in governed Joomla extension repos.
|
||||
|
||||
name: Joomla Extension CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev/**
|
||||
- rc/**
|
||||
- version/**
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- 'dev/**'
|
||||
- dev/**
|
||||
- rc/**
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
||||
13
.github/workflows/codeql-analysis.yml
vendored
13
.github/workflows/codeql-analysis.yml
vendored
@@ -8,8 +8,8 @@
|
||||
# DEFGROUP: GitHub.Workflow.Template
|
||||
# INGROUP: MokoStandards.Security
|
||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
||||
# PATH: /templates/workflows/generic/codeql-analysis.yml.template
|
||||
# VERSION: 03.09.03
|
||||
# PATH: /templates/workflows/generic/codeql-analysis.yml
|
||||
# VERSION: 04.05.00
|
||||
# BRIEF: CodeQL security scanning workflow (generic — all repo types)
|
||||
# NOTE: Deployed to .github/workflows/codeql-analysis.yml in governed repos.
|
||||
# CodeQL does not support PHP directly; JavaScript scans JSON/YAML/shell.
|
||||
@@ -21,7 +21,14 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- version/*
|
||||
- dev/**
|
||||
- rc/**
|
||||
- version/**
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev/**
|
||||
- rc/**
|
||||
schedule:
|
||||
# Weekly on Monday at 06:00 UTC
|
||||
- cron: '0 6 * * 1'
|
||||
|
||||
734
.github/workflows/deploy-demo.yml
vendored
Normal file
734
.github/workflows/deploy-demo.yml
vendored
Normal file
@@ -0,0 +1,734 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# This file is part of a Moko Consulting project.
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: GitHub.Workflow
|
||||
# INGROUP: MokoStandards.Deploy
|
||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
||||
# PATH: /templates/workflows/shared/deploy-demo.yml
|
||||
# VERSION: 04.05.13
|
||||
# BRIEF: SFTP deployment workflow for demo server — synced to all governed repos
|
||||
# NOTE: Synced via bulk-repo-sync to .github/workflows/deploy-demo.yml in all governed repos.
|
||||
# Port is resolved in order: DEMO_FTP_PORT variable → :port suffix in DEMO_FTP_HOST → 22.
|
||||
|
||||
name: Deploy to Demo Server (SFTP)
|
||||
|
||||
# Deploys the contents of the src/ directory to the demo server via SFTP.
|
||||
# Triggers on push/merge to main — deploys the production-ready build to the demo server.
|
||||
#
|
||||
# Required org-level variables: DEMO_FTP_HOST, DEMO_FTP_PATH, DEMO_FTP_USERNAME
|
||||
# Optional org-level variable: DEMO_FTP_PORT (auto-detected from host or defaults to 22)
|
||||
# Optional org/repo variable: DEMO_FTP_SUFFIX — when set, appended to DEMO_FTP_PATH to form the
|
||||
# full remote destination: DEMO_FTP_PATH/DEMO_FTP_SUFFIX
|
||||
# Ignore rules: Place a .ftpignore file in the repository root. Each non-empty,
|
||||
# non-comment line is a glob pattern tested against the relative path
|
||||
# of each file (e.g. "subdir/file.txt"). The .gitignore is NOT used.
|
||||
# Required org-level secret: DEMO_FTP_KEY (preferred) or DEMO_FTP_PASSWORD
|
||||
#
|
||||
# Access control: only users with admin or maintain role on the repository may deploy.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'htdocs/**'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, closed]
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'htdocs/**'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
clear_remote:
|
||||
description: 'Delete all files inside the remote destination folder before uploading'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
check-permission:
|
||||
name: Verify Deployment Permission
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check actor permission
|
||||
env:
|
||||
# Prefer the org-scoped GH_TOKEN secret (needed for the org membership
|
||||
# fallback). Falls back to the built-in github.token so the collaborator
|
||||
# endpoint still works even if GH_TOKEN is not configured.
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
||||
run: |
|
||||
ACTOR="${{ github.actor }}"
|
||||
REPO="${{ github.repository }}"
|
||||
ORG="${{ github.repository_owner }}"
|
||||
|
||||
METHOD=""
|
||||
AUTHORIZED="false"
|
||||
|
||||
# Hardcoded authorized users — always allowed to deploy
|
||||
AUTHORIZED_USERS="jmiller-moko github-actions[bot]"
|
||||
for user in $AUTHORIZED_USERS; do
|
||||
if [ "$ACTOR" = "$user" ]; then
|
||||
AUTHORIZED="true"
|
||||
METHOD="hardcoded allowlist"
|
||||
PERMISSION="admin"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# For other actors, check repo/org permissions via API
|
||||
if [ "$AUTHORIZED" != "true" ]; then
|
||||
PERMISSION=$(gh api "repos/${REPO}/collaborators/${ACTOR}/permission" \
|
||||
--jq '.permission' 2>/dev/null)
|
||||
METHOD="repo collaborator API"
|
||||
|
||||
if [ -z "$PERMISSION" ]; then
|
||||
ORG_ROLE=$(gh api "orgs/${ORG}/memberships/${ACTOR}" \
|
||||
--jq '.role' 2>/dev/null)
|
||||
METHOD="org membership API"
|
||||
if [ "$ORG_ROLE" = "owner" ]; then
|
||||
PERMISSION="admin"
|
||||
else
|
||||
PERMISSION="none"
|
||||
fi
|
||||
fi
|
||||
|
||||
case "$PERMISSION" in
|
||||
admin|maintain) AUTHORIZED="true" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Write detailed summary
|
||||
{
|
||||
echo "## 🔐 Deploy Authorization"
|
||||
echo ""
|
||||
echo "| Field | Value |"
|
||||
echo "|-------|-------|"
|
||||
echo "| **Actor** | \`${ACTOR}\` |"
|
||||
echo "| **Repository** | \`${REPO}\` |"
|
||||
echo "| **Permission** | \`${PERMISSION}\` |"
|
||||
echo "| **Method** | ${METHOD} |"
|
||||
echo "| **Authorized** | ${AUTHORIZED} |"
|
||||
echo "| **Trigger** | \`${{ github.event_name }}\` |"
|
||||
echo "| **Branch** | \`${{ github.ref_name }}\` |"
|
||||
echo ""
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
if [ "$AUTHORIZED" = "true" ]; then
|
||||
echo "✅ ${ACTOR} authorized to deploy (${METHOD})" >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "❌ ${ACTOR} is NOT authorized to deploy." >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "Deployment requires one of:" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "- Being in the hardcoded allowlist" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "- Having \`admin\` or \`maintain\` role on the repository" >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
deploy:
|
||||
name: SFTP Deploy → Demo
|
||||
runs-on: ubuntu-latest
|
||||
needs: [check-permission]
|
||||
if: >-
|
||||
!startsWith(github.head_ref || github.ref_name, 'chore/') &&
|
||||
(github.event_name == 'workflow_dispatch' ||
|
||||
github.event_name == 'push' ||
|
||||
(github.event_name == 'pull_request' &&
|
||||
(github.event.action == 'opened' ||
|
||||
github.event.action == 'synchronize' ||
|
||||
github.event.action == 'reopened' ||
|
||||
github.event.pull_request.merged == true)))
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Resolve source directory
|
||||
id: source
|
||||
run: |
|
||||
# Resolve source directory: src/ preferred, htdocs/ as fallback
|
||||
if [ -d "src" ]; then
|
||||
SRC="src"
|
||||
elif [ -d "htdocs" ]; then
|
||||
SRC="htdocs"
|
||||
else
|
||||
echo "⚠️ No src/ or htdocs/ directory found — skipping deployment"
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
COUNT=$(find "$SRC" -type f | wc -l)
|
||||
echo "✅ Source: ${SRC}/ (${COUNT} file(s))"
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
echo "dir=${SRC}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Preview files to deploy
|
||||
if: steps.source.outputs.skip == 'false'
|
||||
env:
|
||||
SOURCE_DIR: ${{ steps.source.outputs.dir }}
|
||||
run: |
|
||||
# ── Convert a ftpignore-style glob line to an ERE pattern ──────────────
|
||||
ftpignore_to_regex() {
|
||||
local line="$1"
|
||||
local anchored=false
|
||||
# Strip inline comments and whitespace
|
||||
line=$(printf '%s' "$line" | sed 's/[[:space:]]*#.*$//' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
[ -z "$line" ] && return
|
||||
# Skip negation patterns (not supported)
|
||||
[[ "$line" == !* ]] && return
|
||||
# Trailing slash = directory marker; strip it
|
||||
line="${line%/}"
|
||||
# Leading slash = anchored to root; strip it
|
||||
if [[ "$line" == /* ]]; then
|
||||
anchored=true
|
||||
line="${line#/}"
|
||||
fi
|
||||
# Escape ERE special chars, then restore glob semantics
|
||||
local regex
|
||||
regex=$(printf '%s' "$line" \
|
||||
| sed 's/[.+^${}()|[\\]/\\&/g' \
|
||||
| sed 's/\\\*\\\*/\x01/g' \
|
||||
| sed 's/\\\*/[^\/]*/g' \
|
||||
| sed 's/\x01/.*/g' \
|
||||
| sed 's/\\\?/[^\/]/g')
|
||||
if $anchored; then
|
||||
printf '^%s(/|$)' "$regex"
|
||||
else
|
||||
printf '(^|/)%s(/|$)' "$regex"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Read .ftpignore (ftpignore-style globs) ─────────────────────────
|
||||
IGNORE_PATTERNS=()
|
||||
IGNORE_SOURCES=()
|
||||
if [ -f ".ftpignore" ]; then
|
||||
while IFS= read -r line; do
|
||||
[[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]] && continue
|
||||
regex=$(ftpignore_to_regex "$line")
|
||||
[ -n "$regex" ] && IGNORE_PATTERNS+=("$regex") && IGNORE_SOURCES+=("$line")
|
||||
done < ".ftpignore"
|
||||
fi
|
||||
|
||||
# ── Walk src/ and classify every file ────────────────────────────────
|
||||
WILL_UPLOAD=()
|
||||
IGNORED_FILES=()
|
||||
while IFS= read -r -d '' file; do
|
||||
rel="${file#${SOURCE_DIR}/}"
|
||||
SKIP=false
|
||||
for i in "${!IGNORE_PATTERNS[@]}"; do
|
||||
if echo "$rel" | grep -qE "${IGNORE_PATTERNS[$i]}" 2>/dev/null; then
|
||||
IGNORED_FILES+=("$rel | .ftpignore \`${IGNORE_SOURCES[$i]}\`")
|
||||
SKIP=true; break
|
||||
fi
|
||||
done
|
||||
$SKIP && continue
|
||||
WILL_UPLOAD+=("$rel")
|
||||
done < <(find "$SOURCE_DIR" -type f -print0 | sort -z)
|
||||
|
||||
UPLOAD_COUNT="${#WILL_UPLOAD[@]}"
|
||||
IGNORE_COUNT="${#IGNORED_FILES[@]}"
|
||||
|
||||
echo "ℹ️ ${UPLOAD_COUNT} file(s) will be uploaded, ${IGNORE_COUNT} ignored"
|
||||
|
||||
# ── Write deployment preview to step summary ──────────────────────────
|
||||
{
|
||||
echo "## 📋 Deployment Preview"
|
||||
echo ""
|
||||
echo "| Field | Value |"
|
||||
echo "|---|---|"
|
||||
echo "| Source | \`${SOURCE_DIR}/\` |"
|
||||
echo "| Files to upload | **${UPLOAD_COUNT}** |"
|
||||
echo "| Files ignored | **${IGNORE_COUNT}** |"
|
||||
echo ""
|
||||
if [ "${UPLOAD_COUNT}" -gt 0 ]; then
|
||||
echo "### 📂 Files that will be uploaded"
|
||||
echo '```'
|
||||
printf '%s\n' "${WILL_UPLOAD[@]}"
|
||||
echo '```'
|
||||
echo ""
|
||||
fi
|
||||
if [ "${IGNORE_COUNT}" -gt 0 ]; then
|
||||
echo "### ⏭️ Files excluded"
|
||||
echo "| File | Reason |"
|
||||
echo "|---|---|"
|
||||
for entry in "${IGNORED_FILES[@]}"; do
|
||||
f="${entry% | *}"; r="${entry##* | }"
|
||||
echo "| \`${f}\` | ${r} |"
|
||||
done
|
||||
echo ""
|
||||
fi
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Resolve SFTP host and port
|
||||
if: steps.source.outputs.skip == 'false'
|
||||
id: conn
|
||||
env:
|
||||
HOST_RAW: ${{ vars.DEMO_FTP_HOST }}
|
||||
PORT_VAR: ${{ vars.DEMO_FTP_PORT }}
|
||||
run: |
|
||||
HOST="$HOST_RAW"
|
||||
PORT="$PORT_VAR"
|
||||
|
||||
if [ -z "$HOST" ]; then
|
||||
echo "⏭️ DEMO_FTP_HOST not configured — skipping demo deployment."
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Priority 1 — explicit DEMO_FTP_PORT variable
|
||||
if [ -n "$PORT" ]; then
|
||||
echo "ℹ️ Using explicit DEMO_FTP_PORT=${PORT}"
|
||||
|
||||
# Priority 2 — port embedded in DEMO_FTP_HOST (host:port)
|
||||
elif [[ "$HOST" == *:* ]]; then
|
||||
PORT="${HOST##*:}"
|
||||
HOST="${HOST%:*}"
|
||||
echo "ℹ️ Extracted port ${PORT} from DEMO_FTP_HOST"
|
||||
|
||||
# Priority 3 — SFTP default
|
||||
else
|
||||
PORT="22"
|
||||
echo "ℹ️ No port specified — defaulting to SFTP port 22"
|
||||
fi
|
||||
|
||||
echo "host=${HOST}" >> "$GITHUB_OUTPUT"
|
||||
echo "port=${PORT}" >> "$GITHUB_OUTPUT"
|
||||
echo "SFTP target: ${HOST}:${PORT}"
|
||||
|
||||
- name: Build remote path
|
||||
if: steps.source.outputs.skip == 'false' && steps.conn.outputs.skip != 'true'
|
||||
id: remote
|
||||
env:
|
||||
DEMO_FTP_PATH: ${{ vars.DEMO_FTP_PATH }}
|
||||
DEMO_FTP_SUFFIX: ${{ vars.DEMO_FTP_SUFFIX }}
|
||||
run: |
|
||||
BASE="$DEMO_FTP_PATH"
|
||||
|
||||
if [ -z "$BASE" ]; then
|
||||
echo "⏭️ DEMO_FTP_PATH not configured — skipping demo deployment."
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# DEMO_FTP_SUFFIX is required — it identifies the remote subdirectory for this repo.
|
||||
# Without it we cannot safely determine the deployment target.
|
||||
if [ -z "$DEMO_FTP_SUFFIX" ]; then
|
||||
echo "⏭️ DEMO_FTP_SUFFIX variable is not set — skipping deployment."
|
||||
echo " Set DEMO_FTP_SUFFIX as a repo or org variable to enable deploy-demo."
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
echo "path=" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
REMOTE="${BASE%/}/${DEMO_FTP_SUFFIX#/}"
|
||||
|
||||
# ── Platform-specific path safety guards ──────────────────────────────
|
||||
PLATFORM=""
|
||||
MOKO_FILE=".github/.mokostandards"; [ ! -f "$MOKO_FILE" ] && MOKO_FILE=".mokostandards"; if [ -f "$MOKO_FILE" ]; then
|
||||
PLATFORM=$(grep -E '^platform:' "$MOKO_FILE" | sed 's/.*:[[:space:]]*//' | tr -d '"')
|
||||
fi
|
||||
|
||||
if [ "$PLATFORM" = "crm-module" ]; then
|
||||
# Dolibarr modules must deploy under htdocs/custom/ — guard against
|
||||
# accidentally overwriting server root or unrelated directories.
|
||||
if [[ "$REMOTE" != *custom* ]]; then
|
||||
echo "❌ Safety check failed: Dolibarr (crm-module) remote path must contain 'custom'."
|
||||
echo " Current path: ${REMOTE}"
|
||||
echo " Set DEMO_FTP_SUFFIX to the module's htdocs/custom/ subdirectory."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$PLATFORM" = "waas-component" ]; then
|
||||
# Joomla extensions may only deploy to the server's tmp/ directory.
|
||||
if [[ "$REMOTE" != *tmp* ]]; then
|
||||
echo "❌ Safety check failed: Joomla (waas-component) remote path must contain 'tmp'."
|
||||
echo " Current path: ${REMOTE}"
|
||||
echo " Set DEMO_FTP_SUFFIX to a path under the server tmp/ directory."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "ℹ️ Remote path: ${REMOTE}"
|
||||
echo "path=${REMOTE}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Detect SFTP authentication method
|
||||
if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
|
||||
id: auth
|
||||
env:
|
||||
HAS_KEY: ${{ secrets.DEMO_FTP_KEY }}
|
||||
HAS_PASSWORD: ${{ secrets.DEMO_FTP_PASSWORD }}
|
||||
run: |
|
||||
if [ -n "$HAS_KEY" ] && [ -n "$HAS_PASSWORD" ]; then
|
||||
# Both set: key auth with password as passphrase; falls back to password-only if key fails
|
||||
echo "method=key" >> "$GITHUB_OUTPUT"
|
||||
echo "use_passphrase=true" >> "$GITHUB_OUTPUT"
|
||||
echo "has_password=true" >> "$GITHUB_OUTPUT"
|
||||
echo "ℹ️ Primary: SSH key + passphrase (DEMO_FTP_KEY / DEMO_FTP_PASSWORD)"
|
||||
echo "ℹ️ Fallback: password-only auth if key authentication fails"
|
||||
elif [ -n "$HAS_KEY" ]; then
|
||||
# Key only: no passphrase, no password fallback
|
||||
echo "method=key" >> "$GITHUB_OUTPUT"
|
||||
echo "use_passphrase=false" >> "$GITHUB_OUTPUT"
|
||||
echo "has_password=false" >> "$GITHUB_OUTPUT"
|
||||
echo "ℹ️ Using SSH key authentication (DEMO_FTP_KEY, no passphrase, no fallback)"
|
||||
elif [ -n "$HAS_PASSWORD" ]; then
|
||||
# Password only: direct SFTP password auth
|
||||
echo "method=password" >> "$GITHUB_OUTPUT"
|
||||
echo "use_passphrase=false" >> "$GITHUB_OUTPUT"
|
||||
echo "has_password=true" >> "$GITHUB_OUTPUT"
|
||||
echo "ℹ️ Using password authentication (DEMO_FTP_PASSWORD)"
|
||||
else
|
||||
echo "❌ No SFTP credentials configured."
|
||||
echo " Set DEMO_FTP_KEY (preferred) or DEMO_FTP_PASSWORD as an org-level secret."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Setup PHP
|
||||
if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
|
||||
uses: shivammathur/setup-php@fcafdd6392932010c2bd5094439b8e33be2a8a09 # v2.37.0
|
||||
with:
|
||||
php-version: '8.1'
|
||||
tools: composer
|
||||
|
||||
- name: Setup MokoStandards deploy tools
|
||||
if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}'
|
||||
run: |
|
||||
git clone --depth 1 --branch version/04 --quiet \
|
||||
"https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \
|
||||
/tmp/mokostandards
|
||||
cd /tmp/mokostandards
|
||||
composer install --no-dev --no-interaction --quiet
|
||||
|
||||
- name: Clear remote destination folder (manual only)
|
||||
if: >-
|
||||
steps.source.outputs.skip == 'false' &&
|
||||
steps.remote.outputs.skip != 'true' &&
|
||||
inputs.clear_remote == true
|
||||
env:
|
||||
SFTP_HOST: ${{ steps.conn.outputs.host }}
|
||||
SFTP_PORT: ${{ steps.conn.outputs.port }}
|
||||
SFTP_USER: ${{ vars.DEMO_FTP_USERNAME }}
|
||||
SFTP_KEY: ${{ secrets.DEMO_FTP_KEY }}
|
||||
SFTP_PASSWORD: ${{ secrets.DEMO_FTP_PASSWORD }}
|
||||
AUTH_METHOD: ${{ steps.auth.outputs.method }}
|
||||
USE_PASSPHRASE: ${{ steps.auth.outputs.use_passphrase }}
|
||||
HAS_PASSWORD: ${{ steps.auth.outputs.has_password }}
|
||||
REMOTE_PATH: ${{ steps.remote.outputs.path }}
|
||||
run: |
|
||||
cat > /tmp/moko_clear.php << 'PHPEOF'
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require '/tmp/mokostandards/vendor/autoload.php';
|
||||
|
||||
use phpseclib3\Net\SFTP;
|
||||
use phpseclib3\Crypt\PublicKeyLoader;
|
||||
|
||||
$host = (string) getenv('SFTP_HOST');
|
||||
$port = (int) getenv('SFTP_PORT');
|
||||
$username = (string) getenv('SFTP_USER');
|
||||
$authMethod = (string) getenv('AUTH_METHOD');
|
||||
$usePassphrase = getenv('USE_PASSPHRASE') === 'true';
|
||||
$hasPassword = getenv('HAS_PASSWORD') === 'true';
|
||||
$remotePath = rtrim((string) getenv('REMOTE_PATH'), '/');
|
||||
|
||||
echo "⚠️ Clearing remote folder: {$remotePath}\n";
|
||||
|
||||
$sftp = new SFTP($host, $port);
|
||||
|
||||
// ── Authentication ──────────────────────────────────────────────
|
||||
if ($authMethod === 'key') {
|
||||
$keyData = (string) getenv('SFTP_KEY');
|
||||
$passphrase = $usePassphrase ? (string) getenv('SFTP_PASSWORD') : false;
|
||||
$password = $hasPassword ? (string) getenv('SFTP_PASSWORD') : '';
|
||||
$key = PublicKeyLoader::load($keyData, $passphrase);
|
||||
if (!$sftp->login($username, $key)) {
|
||||
if ($password !== '') {
|
||||
echo "⚠️ Key auth failed — falling back to password\n";
|
||||
if (!$sftp->login($username, $password)) {
|
||||
fwrite(STDERR, "❌ Both key and password authentication failed\n");
|
||||
exit(1);
|
||||
}
|
||||
echo "✅ Connected via password authentication (key fallback)\n";
|
||||
} else {
|
||||
fwrite(STDERR, "❌ Key authentication failed and no password fallback is available\n");
|
||||
exit(1);
|
||||
}
|
||||
} else {
|
||||
echo "✅ Connected via SSH key authentication\n";
|
||||
}
|
||||
} else {
|
||||
if (!$sftp->login($username, (string) getenv('SFTP_PASSWORD'))) {
|
||||
fwrite(STDERR, "❌ Password authentication failed\n");
|
||||
exit(1);
|
||||
}
|
||||
echo "✅ Connected via password authentication\n";
|
||||
}
|
||||
|
||||
// ── Recursive delete ────────────────────────────────────────────
|
||||
function rmrf(SFTP $sftp, string $path): void
|
||||
{
|
||||
$entries = $sftp->nlist($path);
|
||||
if ($entries === false) {
|
||||
return; // path does not exist — nothing to clear
|
||||
}
|
||||
foreach ($entries as $name) {
|
||||
if ($name === '.' || $name === '..') {
|
||||
continue;
|
||||
}
|
||||
$entry = "{$path}/{$name}";
|
||||
if ($sftp->is_dir($entry)) {
|
||||
rmrf($sftp, $entry);
|
||||
$sftp->rmdir($entry);
|
||||
echo " 🗑️ Removed dir: {$entry}\n";
|
||||
} else {
|
||||
$sftp->delete($entry);
|
||||
echo " 🗑️ Removed file: {$entry}\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Create remote directory tree ────────────────────────────────
|
||||
function sftpMakedirs(SFTP $sftp, string $path): void
|
||||
{
|
||||
$parts = array_values(array_filter(explode('/', $path), fn(string $p) => $p !== ''));
|
||||
$current = str_starts_with($path, '/') ? '' : '';
|
||||
foreach ($parts as $part) {
|
||||
$current .= '/' . $part;
|
||||
$sftp->mkdir($current); // silently returns false if already exists
|
||||
}
|
||||
}
|
||||
|
||||
rmrf($sftp, $remotePath);
|
||||
sftpMakedirs($sftp, $remotePath);
|
||||
echo "✅ Remote folder ready: {$remotePath}\n";
|
||||
PHPEOF
|
||||
php /tmp/moko_clear.php
|
||||
|
||||
- name: Deploy via SFTP
|
||||
if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
|
||||
env:
|
||||
SFTP_HOST: ${{ steps.conn.outputs.host }}
|
||||
SFTP_PORT: ${{ steps.conn.outputs.port }}
|
||||
SFTP_USER: ${{ vars.DEMO_FTP_USERNAME }}
|
||||
SFTP_KEY: ${{ secrets.DEMO_FTP_KEY }}
|
||||
SFTP_PASSWORD: ${{ secrets.DEMO_FTP_PASSWORD }}
|
||||
AUTH_METHOD: ${{ steps.auth.outputs.method }}
|
||||
USE_PASSPHRASE: ${{ steps.auth.outputs.use_passphrase }}
|
||||
REMOTE_PATH: ${{ steps.remote.outputs.path }}
|
||||
SOURCE_DIR: ${{ steps.source.outputs.dir }}
|
||||
run: |
|
||||
# ── Write SSH key to temp file (key auth only) ────────────────────────
|
||||
if [ "$AUTH_METHOD" = "key" ]; then
|
||||
printf '%s' "$SFTP_KEY" > /tmp/deploy_key
|
||||
chmod 600 /tmp/deploy_key
|
||||
fi
|
||||
|
||||
# ── Generate sftp-config.json safely via jq ───────────────────────────
|
||||
if [ "$AUTH_METHOD" = "key" ]; then
|
||||
jq -n \
|
||||
--arg host "$SFTP_HOST" \
|
||||
--argjson port "${SFTP_PORT:-22}" \
|
||||
--arg user "$SFTP_USER" \
|
||||
--arg path "$REMOTE_PATH" \
|
||||
--arg key "/tmp/deploy_key" \
|
||||
'{host:$host, port:$port, user:$user, remote_path:$path, ssh_key_file:$key}' \
|
||||
> /tmp/sftp-config.json
|
||||
else
|
||||
jq -n \
|
||||
--arg host "$SFTP_HOST" \
|
||||
--argjson port "${SFTP_PORT:-22}" \
|
||||
--arg user "$SFTP_USER" \
|
||||
--arg path "$REMOTE_PATH" \
|
||||
--arg pass "$SFTP_PASSWORD" \
|
||||
'{host:$host, port:$port, user:$user, remote_path:$path, password:$pass}' \
|
||||
> /tmp/sftp-config.json
|
||||
fi
|
||||
|
||||
# ── Write update files (demo = stable) ─────────────────────────────
|
||||
PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true)
|
||||
VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null || echo "unknown")
|
||||
REPO="${{ github.repository }}"
|
||||
|
||||
if [ "$PLATFORM" = "crm-module" ]; then
|
||||
printf '%s' "$VERSION" > update.txt
|
||||
fi
|
||||
|
||||
if [ "$PLATFORM" = "waas-component" ]; then
|
||||
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)
|
||||
if [ -n "$MANIFEST" ]; then
|
||||
EXT_NAME=$(grep -oP '<name>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "${{ github.event.repository.name }}")
|
||||
EXT_TYPE=$(grep -oP '<extension[^>]+type="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "component")
|
||||
EXT_ELEMENT=$(grep -oP '<element>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || basename "$MANIFEST" .xml)
|
||||
EXT_CLIENT=$(grep -oP '<extension[^>]+client="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "")
|
||||
EXT_FOLDER=$(grep -oP '<extension[^>]+group="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "")
|
||||
TARGET_PLATFORM=$(grep -oP '<targetplatform[^/]*/' "$MANIFEST" 2>/dev/null | head -1 || true)
|
||||
[ -n "$TARGET_PLATFORM" ] && TARGET_PLATFORM="${TARGET_PLATFORM}>"
|
||||
[ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="5.*" %s>' "/")
|
||||
|
||||
CLIENT_TAG=""
|
||||
if [ -n "$EXT_CLIENT" ]; then CLIENT_TAG="<client>${EXT_CLIENT}</client>"; elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then CLIENT_TAG="<client>site</client>"; fi
|
||||
FOLDER_TAG=""
|
||||
if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then FOLDER_TAG="<folder>${EXT_FOLDER}</folder>"; fi
|
||||
|
||||
DOWNLOAD_URL="https://github.com/${REPO}/releases/download/v${VERSION}/${EXT_ELEMENT}-${VERSION}.zip"
|
||||
{
|
||||
printf '%s\n' '<?xml version="1.0" encoding="utf-8"?>'
|
||||
printf '%s\n' '<updates>'
|
||||
printf '%s\n' ' <update>'
|
||||
printf '%s\n' " <name>${EXT_NAME}</name>"
|
||||
printf '%s\n' " <description>${EXT_NAME} update</description>"
|
||||
printf '%s\n' " <element>${EXT_ELEMENT}</element>"
|
||||
printf '%s\n' " <type>${EXT_TYPE}</type>"
|
||||
printf '%s\n' " <version>${VERSION}</version>"
|
||||
[ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}"
|
||||
[ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}"
|
||||
printf '%s\n' ' <tags>'
|
||||
printf '%s\n' ' <tag>stable</tag>'
|
||||
printf '%s\n' ' </tags>'
|
||||
printf '%s\n' " <infourl title=\"${EXT_NAME}\">https://github.com/${REPO}</infourl>"
|
||||
printf '%s\n' ' <downloads>'
|
||||
printf '%s\n' " <downloadurl type=\"full\" format=\"zip\">${DOWNLOAD_URL}</downloadurl>"
|
||||
printf '%s\n' ' </downloads>'
|
||||
printf '%s\n' " ${TARGET_PLATFORM}"
|
||||
printf '%s\n' ' <maintainer>Moko Consulting</maintainer>'
|
||||
printf '%s\n' ' <maintainerurl>https://mokoconsulting.tech</maintainerurl>'
|
||||
printf '%s\n' ' </update>'
|
||||
printf '%s\n' '</updates>'
|
||||
} > updates.xml
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Run deploy-sftp.php from MokoStandards ────────────────────────────
|
||||
DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
|
||||
if [ "$USE_PASSPHRASE" = "true" ]; then
|
||||
DEPLOY_ARGS+=(--key-passphrase "$SFTP_PASSWORD")
|
||||
fi
|
||||
|
||||
PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true)
|
||||
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards/api/deploy/deploy-joomla.php" ]; then
|
||||
php /tmp/mokostandards/api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
|
||||
else
|
||||
php /tmp/mokostandards/api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
|
||||
fi
|
||||
# Remove temp files that should never be left behind
|
||||
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
||||
|
||||
- name: Create or update failure issue
|
||||
if: failure() && steps.remote.outputs.skip != 'true' && steps.conn.outputs.skip != 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
||||
run: |
|
||||
REPO="${{ github.repository }}"
|
||||
RUN_URL="${{ github.server_url }}/${REPO}/actions/runs/${{ github.run_id }}"
|
||||
ACTOR="${{ github.actor }}"
|
||||
BRANCH="${{ github.ref_name }}"
|
||||
EVENT="${{ github.event_name }}"
|
||||
NOW=$(date -u '+%Y-%m-%d %H:%M:%S UTC')
|
||||
LABEL="deploy-failure"
|
||||
|
||||
TITLE="fix: Demo deployment failed — ${REPO}"
|
||||
BODY="## Demo Deployment Failed
|
||||
|
||||
A deployment to the demo server failed and requires attention.
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Repository** | \`${REPO}\` |
|
||||
| **Branch** | \`${BRANCH}\` |
|
||||
| **Trigger** | ${EVENT} |
|
||||
| **Actor** | @${ACTOR} |
|
||||
| **Failed at** | ${NOW} |
|
||||
| **Run** | [View workflow run](${RUN_URL}) |
|
||||
|
||||
### Next steps
|
||||
1. Review the [workflow run log](${RUN_URL}) for the specific error.
|
||||
2. Fix the underlying issue (credentials, SFTP connectivity, permissions).
|
||||
3. Re-trigger the deployment via **Actions → Deploy to Demo Server → Run workflow**.
|
||||
|
||||
---
|
||||
*Auto-created by deploy-demo.yml — close this issue once the deployment is resolved.*"
|
||||
|
||||
# Ensure the label exists (idempotent — no-op if already present)
|
||||
gh label create "$LABEL" \
|
||||
--repo "$REPO" \
|
||||
--color "CC0000" \
|
||||
--description "Automated deploy failure tracking" \
|
||||
--force 2>/dev/null || true
|
||||
|
||||
# Look for an existing open deploy-failure issue
|
||||
EXISTING=$(gh api "repos/${REPO}/issues?labels=${LABEL}&state=all&per_page=1&sort=created&direction=desc" \
|
||||
--jq '.[0].number' 2>/dev/null)
|
||||
|
||||
if [ -n "$EXISTING" ] && [ "$EXISTING" != "null" ]; then
|
||||
gh api "repos/${REPO}/issues/${EXISTING}" \
|
||||
-X PATCH \
|
||||
-f title="$TITLE" \
|
||||
-f body="$BODY" \
|
||||
-f state="open" \
|
||||
--silent
|
||||
echo "📋 Failure issue #${EXISTING} updated/reopened: ${REPO}" >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
gh issue create \
|
||||
--repo "$REPO" \
|
||||
--title "$TITLE" \
|
||||
--body "$BODY" \
|
||||
--label "$LABEL" \
|
||||
--assignee "jmiller-moko" \
|
||||
| tee -a "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
- name: Deployment summary
|
||||
if: always()
|
||||
run: |
|
||||
if [ "${{ steps.source.outputs.skip }}" == "true" ]; then
|
||||
echo "### ⏭️ Deployment Skipped" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "No \`src/\` directory found in this repository." >> "$GITHUB_STEP_SUMMARY"
|
||||
elif [ "${{ job.status }}" == "success" ]; then
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "### ✅ Demo Deployment Successful" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Field | Value |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "|-------|-------|" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Host | \`${{ steps.conn.outputs.host }}:${{ steps.conn.outputs.port }}\` |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Remote path | \`${{ steps.remote.outputs.path }}\` |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Source | \`src/\` |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Trigger | ${{ github.event_name }} |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Auth | ${{ steps.auth.outputs.method }} |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Clear remote | ${{ inputs.clear_remote || 'false' }} |" >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "### ❌ Demo Deployment Failed" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "Check the job log above for error details." >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
700
.github/workflows/deploy-dev.yml
vendored
Normal file
700
.github/workflows/deploy-dev.yml
vendored
Normal file
@@ -0,0 +1,700 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# This file is part of a Moko Consulting project.
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: GitHub.Workflow
|
||||
# INGROUP: MokoStandards.Deploy
|
||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
||||
# PATH: /templates/workflows/shared/deploy-dev.yml
|
||||
# VERSION: 04.05.13
|
||||
# BRIEF: SFTP deployment workflow for development server — synced to all governed repos
|
||||
# NOTE: Synced via bulk-repo-sync to .github/workflows/deploy-dev.yml in all governed repos.
|
||||
# Port is resolved in order: DEV_FTP_PORT variable → :port suffix in DEV_FTP_HOST → 22.
|
||||
|
||||
name: Deploy to Dev Server (SFTP)
|
||||
|
||||
# Deploys the contents of the src/ directory to the development server via SFTP.
|
||||
# Triggers on every pull_request to development branches (so the dev server always
|
||||
# reflects the latest PR state) and on push/merge to main branches.
|
||||
#
|
||||
# Required org-level variables: DEV_FTP_HOST, DEV_FTP_PATH, DEV_FTP_USERNAME
|
||||
# Optional org-level variable: DEV_FTP_PORT (auto-detected from host or defaults to 22)
|
||||
# Optional org/repo variable: DEV_FTP_SUFFIX — when set, appended to DEV_FTP_PATH to form the
|
||||
# full remote destination: DEV_FTP_PATH/DEV_FTP_SUFFIX
|
||||
# Ignore rules: Place a .ftpignore file in the repository root. Each non-empty,
|
||||
# non-comment line is a glob pattern tested against the relative path
|
||||
# of each file (e.g. "subdir/file.txt"). The .gitignore is NOT used.
|
||||
# Required org-level secret: DEV_FTP_KEY (preferred) or DEV_FTP_PASSWORD
|
||||
#
|
||||
# Access control: only users with admin or maintain role on the repository may deploy.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'dev/**'
|
||||
- 'rc/**'
|
||||
- develop
|
||||
- development
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'htdocs/**'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, closed]
|
||||
branches:
|
||||
- 'dev/**'
|
||||
- 'rc/**'
|
||||
- develop
|
||||
- development
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'htdocs/**'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
clear_remote:
|
||||
description: 'Delete all files inside the remote destination folder before uploading'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
check-permission:
|
||||
name: Verify Deployment Permission
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check actor permission
|
||||
env:
|
||||
# Prefer the org-scoped GH_TOKEN secret (needed for the org membership
|
||||
# fallback). Falls back to the built-in github.token so the collaborator
|
||||
# endpoint still works even if GH_TOKEN is not configured.
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
||||
run: |
|
||||
ACTOR="${{ github.actor }}"
|
||||
REPO="${{ github.repository }}"
|
||||
ORG="${{ github.repository_owner }}"
|
||||
|
||||
METHOD=""
|
||||
AUTHORIZED="false"
|
||||
|
||||
# Hardcoded authorized users — always allowed to deploy
|
||||
AUTHORIZED_USERS="jmiller-moko github-actions[bot]"
|
||||
for user in $AUTHORIZED_USERS; do
|
||||
if [ "$ACTOR" = "$user" ]; then
|
||||
AUTHORIZED="true"
|
||||
METHOD="hardcoded allowlist"
|
||||
PERMISSION="admin"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# For other actors, check repo/org permissions via API
|
||||
if [ "$AUTHORIZED" != "true" ]; then
|
||||
PERMISSION=$(gh api "repos/${REPO}/collaborators/${ACTOR}/permission" \
|
||||
--jq '.permission' 2>/dev/null)
|
||||
METHOD="repo collaborator API"
|
||||
|
||||
if [ -z "$PERMISSION" ]; then
|
||||
ORG_ROLE=$(gh api "orgs/${ORG}/memberships/${ACTOR}" \
|
||||
--jq '.role' 2>/dev/null)
|
||||
METHOD="org membership API"
|
||||
if [ "$ORG_ROLE" = "owner" ]; then
|
||||
PERMISSION="admin"
|
||||
else
|
||||
PERMISSION="none"
|
||||
fi
|
||||
fi
|
||||
|
||||
case "$PERMISSION" in
|
||||
admin|maintain) AUTHORIZED="true" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Write detailed summary
|
||||
{
|
||||
echo "## 🔐 Deploy Authorization"
|
||||
echo ""
|
||||
echo "| Field | Value |"
|
||||
echo "|-------|-------|"
|
||||
echo "| **Actor** | \`${ACTOR}\` |"
|
||||
echo "| **Repository** | \`${REPO}\` |"
|
||||
echo "| **Permission** | \`${PERMISSION}\` |"
|
||||
echo "| **Method** | ${METHOD} |"
|
||||
echo "| **Authorized** | ${AUTHORIZED} |"
|
||||
echo "| **Trigger** | \`${{ github.event_name }}\` |"
|
||||
echo "| **Branch** | \`${{ github.ref_name }}\` |"
|
||||
echo ""
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
if [ "$AUTHORIZED" = "true" ]; then
|
||||
echo "✅ ${ACTOR} authorized to deploy (${METHOD})" >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "❌ ${ACTOR} is NOT authorized to deploy." >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "Deployment requires one of:" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "- Being in the hardcoded allowlist" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "- Having \`admin\` or \`maintain\` role on the repository" >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
deploy:
|
||||
name: SFTP Deploy → Dev
|
||||
runs-on: ubuntu-latest
|
||||
needs: [check-permission]
|
||||
if: >-
|
||||
!startsWith(github.head_ref || github.ref_name, 'chore/') &&
|
||||
(github.event_name == 'workflow_dispatch' ||
|
||||
github.event_name == 'push' ||
|
||||
(github.event_name == 'pull_request' &&
|
||||
(github.event.action == 'opened' ||
|
||||
github.event.action == 'synchronize' ||
|
||||
github.event.action == 'reopened' ||
|
||||
github.event.pull_request.merged == true)))
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Resolve source directory
|
||||
id: source
|
||||
run: |
|
||||
# Resolve source directory: src/ preferred, htdocs/ as fallback
|
||||
if [ -d "src" ]; then
|
||||
SRC="src"
|
||||
elif [ -d "htdocs" ]; then
|
||||
SRC="htdocs"
|
||||
else
|
||||
echo "⚠️ No src/ or htdocs/ directory found — skipping deployment"
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
COUNT=$(find "$SRC" -type f | wc -l)
|
||||
echo "✅ Source: ${SRC}/ (${COUNT} file(s))"
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
echo "dir=${SRC}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Preview files to deploy
|
||||
if: steps.source.outputs.skip == 'false'
|
||||
env:
|
||||
SOURCE_DIR: ${{ steps.source.outputs.dir }}
|
||||
run: |
|
||||
# ── Convert a ftpignore-style glob line to an ERE pattern ──────────────
|
||||
ftpignore_to_regex() {
|
||||
local line="$1"
|
||||
local anchored=false
|
||||
# Strip inline comments and whitespace
|
||||
line=$(printf '%s' "$line" | sed 's/[[:space:]]*#.*$//' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
[ -z "$line" ] && return
|
||||
# Skip negation patterns (not supported)
|
||||
[[ "$line" == !* ]] && return
|
||||
# Trailing slash = directory marker; strip it
|
||||
line="${line%/}"
|
||||
# Leading slash = anchored to root; strip it
|
||||
if [[ "$line" == /* ]]; then
|
||||
anchored=true
|
||||
line="${line#/}"
|
||||
fi
|
||||
# Escape ERE special chars, then restore glob semantics
|
||||
local regex
|
||||
regex=$(printf '%s' "$line" \
|
||||
| sed 's/[.+^${}()|[\\]/\\&/g' \
|
||||
| sed 's/\\\*\\\*/\x01/g' \
|
||||
| sed 's/\\\*/[^\/]*/g' \
|
||||
| sed 's/\x01/.*/g' \
|
||||
| sed 's/\\\?/[^\/]/g')
|
||||
if $anchored; then
|
||||
printf '^%s(/|$)' "$regex"
|
||||
else
|
||||
printf '(^|/)%s(/|$)' "$regex"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Read .ftpignore (ftpignore-style globs) ─────────────────────────
|
||||
IGNORE_PATTERNS=()
|
||||
IGNORE_SOURCES=()
|
||||
if [ -f ".ftpignore" ]; then
|
||||
while IFS= read -r line; do
|
||||
[[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]] && continue
|
||||
regex=$(ftpignore_to_regex "$line")
|
||||
[ -n "$regex" ] && IGNORE_PATTERNS+=("$regex") && IGNORE_SOURCES+=("$line")
|
||||
done < ".ftpignore"
|
||||
fi
|
||||
|
||||
# ── Walk src/ and classify every file ────────────────────────────────
|
||||
WILL_UPLOAD=()
|
||||
IGNORED_FILES=()
|
||||
while IFS= read -r -d '' file; do
|
||||
rel="${file#${SOURCE_DIR}/}"
|
||||
SKIP=false
|
||||
for i in "${!IGNORE_PATTERNS[@]}"; do
|
||||
if echo "$rel" | grep -qE "${IGNORE_PATTERNS[$i]}" 2>/dev/null; then
|
||||
IGNORED_FILES+=("$rel | .ftpignore \`${IGNORE_SOURCES[$i]}\`")
|
||||
SKIP=true; break
|
||||
fi
|
||||
done
|
||||
$SKIP && continue
|
||||
WILL_UPLOAD+=("$rel")
|
||||
done < <(find "$SOURCE_DIR" -type f -print0 | sort -z)
|
||||
|
||||
UPLOAD_COUNT="${#WILL_UPLOAD[@]}"
|
||||
IGNORE_COUNT="${#IGNORED_FILES[@]}"
|
||||
|
||||
echo "ℹ️ ${UPLOAD_COUNT} file(s) will be uploaded, ${IGNORE_COUNT} ignored"
|
||||
|
||||
# ── Write deployment preview to step summary ──────────────────────────
|
||||
{
|
||||
echo "## 📋 Deployment Preview"
|
||||
echo ""
|
||||
echo "| Field | Value |"
|
||||
echo "|---|---|"
|
||||
echo "| Source | \`${SOURCE_DIR}/\` |"
|
||||
echo "| Files to upload | **${UPLOAD_COUNT}** |"
|
||||
echo "| Files ignored | **${IGNORE_COUNT}** |"
|
||||
echo ""
|
||||
if [ "${UPLOAD_COUNT}" -gt 0 ]; then
|
||||
echo "### 📂 Files that will be uploaded"
|
||||
echo '```'
|
||||
printf '%s\n' "${WILL_UPLOAD[@]}"
|
||||
echo '```'
|
||||
echo ""
|
||||
fi
|
||||
if [ "${IGNORE_COUNT}" -gt 0 ]; then
|
||||
echo "### ⏭️ Files excluded"
|
||||
echo "| File | Reason |"
|
||||
echo "|---|---|"
|
||||
for entry in "${IGNORED_FILES[@]}"; do
|
||||
f="${entry% | *}"; r="${entry##* | }"
|
||||
echo "| \`${f}\` | ${r} |"
|
||||
done
|
||||
echo ""
|
||||
fi
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Resolve SFTP host and port
|
||||
if: steps.source.outputs.skip == 'false'
|
||||
id: conn
|
||||
env:
|
||||
HOST_RAW: ${{ vars.DEV_FTP_HOST }}
|
||||
PORT_VAR: ${{ vars.DEV_FTP_PORT }}
|
||||
run: |
|
||||
HOST="$HOST_RAW"
|
||||
PORT="$PORT_VAR"
|
||||
|
||||
# Priority 1 — explicit DEV_FTP_PORT variable
|
||||
if [ -n "$PORT" ]; then
|
||||
echo "ℹ️ Using explicit DEV_FTP_PORT=${PORT}"
|
||||
|
||||
# Priority 2 — port embedded in DEV_FTP_HOST (host:port)
|
||||
elif [[ "$HOST" == *:* ]]; then
|
||||
PORT="${HOST##*:}"
|
||||
HOST="${HOST%:*}"
|
||||
echo "ℹ️ Extracted port ${PORT} from DEV_FTP_HOST"
|
||||
|
||||
# Priority 3 — SFTP default
|
||||
else
|
||||
PORT="22"
|
||||
echo "ℹ️ No port specified — defaulting to SFTP port 22"
|
||||
fi
|
||||
|
||||
echo "host=${HOST}" >> "$GITHUB_OUTPUT"
|
||||
echo "port=${PORT}" >> "$GITHUB_OUTPUT"
|
||||
echo "SFTP target: ${HOST}:${PORT}"
|
||||
|
||||
- name: Build remote path
|
||||
if: steps.source.outputs.skip == 'false'
|
||||
id: remote
|
||||
env:
|
||||
DEV_FTP_PATH: ${{ vars.DEV_FTP_PATH }}
|
||||
DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
|
||||
run: |
|
||||
BASE="$DEV_FTP_PATH"
|
||||
|
||||
if [ -z "$BASE" ]; then
|
||||
echo "❌ DEV_FTP_PATH is not set."
|
||||
echo " Configure it as an org-level variable (Settings → Variables) and"
|
||||
echo " ensure this repository has been granted access to it."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# DEV_FTP_SUFFIX is required — it identifies the remote subdirectory for this repo.
|
||||
# Without it we cannot safely determine the deployment target.
|
||||
if [ -z "$DEV_FTP_SUFFIX" ]; then
|
||||
echo "⏭️ DEV_FTP_SUFFIX variable is not set — skipping deployment."
|
||||
echo " Set DEV_FTP_SUFFIX as a repo or org variable to enable deploy-dev."
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
echo "path=" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
REMOTE="${BASE%/}/${DEV_FTP_SUFFIX#/}"
|
||||
|
||||
# ── Platform-specific path safety guards ──────────────────────────────
|
||||
PLATFORM=""
|
||||
MOKO_FILE=".github/.mokostandards"; [ ! -f "$MOKO_FILE" ] && MOKO_FILE=".mokostandards"; if [ -f "$MOKO_FILE" ]; then
|
||||
PLATFORM=$(grep -oP '^platform:.*' "$MOKO_FILE" 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
if [ "$PLATFORM" = "crm-module" ]; then
|
||||
# Dolibarr modules must deploy under htdocs/custom/ — guard against
|
||||
# accidentally overwriting server root or unrelated directories.
|
||||
if [[ "$REMOTE" != *custom* ]]; then
|
||||
echo "❌ Safety check failed: Dolibarr (crm-module) remote path must contain 'custom'."
|
||||
echo " Current path: ${REMOTE}"
|
||||
echo " Set DEV_FTP_SUFFIX to the module's htdocs/custom/ subdirectory."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$PLATFORM" = "waas-component" ]; then
|
||||
# Joomla extensions may only deploy to the server's tmp/ directory.
|
||||
if [[ "$REMOTE" != *tmp* ]]; then
|
||||
echo "❌ Safety check failed: Joomla (waas-component) remote path must contain 'tmp'."
|
||||
echo " Current path: ${REMOTE}"
|
||||
echo " Set DEV_FTP_SUFFIX to a path under the server tmp/ directory."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "ℹ️ Remote path: ${REMOTE}"
|
||||
echo "path=${REMOTE}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Detect SFTP authentication method
|
||||
if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
|
||||
id: auth
|
||||
env:
|
||||
HAS_KEY: ${{ secrets.DEV_FTP_KEY }}
|
||||
HAS_PASSWORD: ${{ secrets.DEV_FTP_PASSWORD }}
|
||||
run: |
|
||||
if [ -n "$HAS_KEY" ] && [ -n "$HAS_PASSWORD" ]; then
|
||||
# Both set: key auth with password as passphrase; falls back to password-only if key fails
|
||||
echo "method=key" >> "$GITHUB_OUTPUT"
|
||||
echo "use_passphrase=true" >> "$GITHUB_OUTPUT"
|
||||
echo "has_password=true" >> "$GITHUB_OUTPUT"
|
||||
echo "ℹ️ Primary: SSH key + passphrase (DEV_FTP_KEY / DEV_FTP_PASSWORD)"
|
||||
echo "ℹ️ Fallback: password-only auth if key authentication fails"
|
||||
elif [ -n "$HAS_KEY" ]; then
|
||||
# Key only: no passphrase, no password fallback
|
||||
echo "method=key" >> "$GITHUB_OUTPUT"
|
||||
echo "use_passphrase=false" >> "$GITHUB_OUTPUT"
|
||||
echo "has_password=false" >> "$GITHUB_OUTPUT"
|
||||
echo "ℹ️ Using SSH key authentication (DEV_FTP_KEY, no passphrase, no fallback)"
|
||||
elif [ -n "$HAS_PASSWORD" ]; then
|
||||
# Password only: direct SFTP password auth
|
||||
echo "method=password" >> "$GITHUB_OUTPUT"
|
||||
echo "use_passphrase=false" >> "$GITHUB_OUTPUT"
|
||||
echo "has_password=true" >> "$GITHUB_OUTPUT"
|
||||
echo "ℹ️ Using password authentication (DEV_FTP_PASSWORD)"
|
||||
else
|
||||
echo "❌ No SFTP credentials configured."
|
||||
echo " Set DEV_FTP_KEY (preferred) or DEV_FTP_PASSWORD as an org-level secret."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Setup PHP
|
||||
if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
|
||||
uses: shivammathur/setup-php@fcafdd6392932010c2bd5094439b8e33be2a8a09 # v2.37.0
|
||||
with:
|
||||
php-version: '8.1'
|
||||
tools: composer
|
||||
|
||||
- name: Setup MokoStandards deploy tools
|
||||
if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}'
|
||||
run: |
|
||||
git clone --depth 1 --branch version/04 --quiet \
|
||||
"https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \
|
||||
/tmp/mokostandards
|
||||
cd /tmp/mokostandards
|
||||
composer install --no-dev --no-interaction --quiet
|
||||
|
||||
- name: Clear remote destination folder (manual only)
|
||||
if: >-
|
||||
steps.source.outputs.skip == 'false' &&
|
||||
steps.remote.outputs.skip != 'true' &&
|
||||
inputs.clear_remote == true
|
||||
env:
|
||||
SFTP_HOST: ${{ steps.conn.outputs.host }}
|
||||
SFTP_PORT: ${{ steps.conn.outputs.port }}
|
||||
SFTP_USER: ${{ vars.DEV_FTP_USERNAME }}
|
||||
SFTP_KEY: ${{ secrets.DEV_FTP_KEY }}
|
||||
SFTP_PASSWORD: ${{ secrets.DEV_FTP_PASSWORD }}
|
||||
AUTH_METHOD: ${{ steps.auth.outputs.method }}
|
||||
USE_PASSPHRASE: ${{ steps.auth.outputs.use_passphrase }}
|
||||
HAS_PASSWORD: ${{ steps.auth.outputs.has_password }}
|
||||
REMOTE_PATH: ${{ steps.remote.outputs.path }}
|
||||
run: |
|
||||
cat > /tmp/moko_clear.php << 'PHPEOF'
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require '/tmp/mokostandards/vendor/autoload.php';
|
||||
|
||||
use phpseclib3\Net\SFTP;
|
||||
use phpseclib3\Crypt\PublicKeyLoader;
|
||||
|
||||
$host = (string) getenv('SFTP_HOST');
|
||||
$port = (int) getenv('SFTP_PORT');
|
||||
$username = (string) getenv('SFTP_USER');
|
||||
$authMethod = (string) getenv('AUTH_METHOD');
|
||||
$usePassphrase = getenv('USE_PASSPHRASE') === 'true';
|
||||
$hasPassword = getenv('HAS_PASSWORD') === 'true';
|
||||
$remotePath = rtrim((string) getenv('REMOTE_PATH'), '/');
|
||||
|
||||
echo "⚠️ Clearing remote folder: {$remotePath}\n";
|
||||
|
||||
$sftp = new SFTP($host, $port);
|
||||
|
||||
// ── Authentication ──────────────────────────────────────────────
|
||||
if ($authMethod === 'key') {
|
||||
$keyData = (string) getenv('SFTP_KEY');
|
||||
$passphrase = $usePassphrase ? (string) getenv('SFTP_PASSWORD') : false;
|
||||
$password = $hasPassword ? (string) getenv('SFTP_PASSWORD') : '';
|
||||
$key = PublicKeyLoader::load($keyData, $passphrase);
|
||||
if (!$sftp->login($username, $key)) {
|
||||
if ($password !== '') {
|
||||
echo "⚠️ Key auth failed — falling back to password\n";
|
||||
if (!$sftp->login($username, $password)) {
|
||||
fwrite(STDERR, "❌ Both key and password authentication failed\n");
|
||||
exit(1);
|
||||
}
|
||||
echo "✅ Connected via password authentication (key fallback)\n";
|
||||
} else {
|
||||
fwrite(STDERR, "❌ Key authentication failed and no password fallback is available\n");
|
||||
exit(1);
|
||||
}
|
||||
} else {
|
||||
echo "✅ Connected via SSH key authentication\n";
|
||||
}
|
||||
} else {
|
||||
if (!$sftp->login($username, (string) getenv('SFTP_PASSWORD'))) {
|
||||
fwrite(STDERR, "❌ Password authentication failed\n");
|
||||
exit(1);
|
||||
}
|
||||
echo "✅ Connected via password authentication\n";
|
||||
}
|
||||
|
||||
// ── Recursive delete ────────────────────────────────────────────
|
||||
function rmrf(SFTP $sftp, string $path): void
|
||||
{
|
||||
$entries = $sftp->nlist($path);
|
||||
if ($entries === false) {
|
||||
return; // path does not exist — nothing to clear
|
||||
}
|
||||
foreach ($entries as $name) {
|
||||
if ($name === '.' || $name === '..') {
|
||||
continue;
|
||||
}
|
||||
$entry = "{$path}/{$name}";
|
||||
if ($sftp->is_dir($entry)) {
|
||||
rmrf($sftp, $entry);
|
||||
$sftp->rmdir($entry);
|
||||
echo " 🗑️ Removed dir: {$entry}\n";
|
||||
} else {
|
||||
$sftp->delete($entry);
|
||||
echo " 🗑️ Removed file: {$entry}\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Create remote directory tree ────────────────────────────────
|
||||
function sftpMakedirs(SFTP $sftp, string $path): void
|
||||
{
|
||||
$parts = array_values(array_filter(explode('/', $path), fn(string $p) => $p !== ''));
|
||||
$current = str_starts_with($path, '/') ? '' : '';
|
||||
foreach ($parts as $part) {
|
||||
$current .= '/' . $part;
|
||||
$sftp->mkdir($current); // silently returns false if already exists
|
||||
}
|
||||
}
|
||||
|
||||
rmrf($sftp, $remotePath);
|
||||
sftpMakedirs($sftp, $remotePath);
|
||||
echo "✅ Remote folder ready: {$remotePath}\n";
|
||||
PHPEOF
|
||||
php /tmp/moko_clear.php
|
||||
|
||||
- name: Deploy via SFTP
|
||||
if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
|
||||
env:
|
||||
SFTP_HOST: ${{ steps.conn.outputs.host }}
|
||||
SFTP_PORT: ${{ steps.conn.outputs.port }}
|
||||
SFTP_USER: ${{ vars.DEV_FTP_USERNAME }}
|
||||
SFTP_KEY: ${{ secrets.DEV_FTP_KEY }}
|
||||
SFTP_PASSWORD: ${{ secrets.DEV_FTP_PASSWORD }}
|
||||
AUTH_METHOD: ${{ steps.auth.outputs.method }}
|
||||
USE_PASSPHRASE: ${{ steps.auth.outputs.use_passphrase }}
|
||||
REMOTE_PATH: ${{ steps.remote.outputs.path }}
|
||||
SOURCE_DIR: ${{ steps.source.outputs.dir }}
|
||||
run: |
|
||||
# ── Write SSH key to temp file (key auth only) ────────────────────────
|
||||
if [ "$AUTH_METHOD" = "key" ]; then
|
||||
printf '%s' "$SFTP_KEY" > /tmp/deploy_key
|
||||
chmod 600 /tmp/deploy_key
|
||||
fi
|
||||
|
||||
# ── Generate sftp-config.json safely via jq ───────────────────────────
|
||||
if [ "$AUTH_METHOD" = "key" ]; then
|
||||
jq -n \
|
||||
--arg host "$SFTP_HOST" \
|
||||
--argjson port "${SFTP_PORT:-22}" \
|
||||
--arg user "$SFTP_USER" \
|
||||
--arg path "$REMOTE_PATH" \
|
||||
--arg key "/tmp/deploy_key" \
|
||||
'{host:$host, port:$port, user:$user, remote_path:$path, ssh_key_file:$key}' \
|
||||
> /tmp/sftp-config.json
|
||||
else
|
||||
jq -n \
|
||||
--arg host "$SFTP_HOST" \
|
||||
--argjson port "${SFTP_PORT:-22}" \
|
||||
--arg user "$SFTP_USER" \
|
||||
--arg path "$REMOTE_PATH" \
|
||||
--arg pass "$SFTP_PASSWORD" \
|
||||
'{host:$host, port:$port, user:$user, remote_path:$path, password:$pass}' \
|
||||
> /tmp/sftp-config.json
|
||||
fi
|
||||
|
||||
# Dev deploys skip minified files — use unminified sources for debugging
|
||||
echo "*.min.js" >> .ftpignore
|
||||
echo "*.min.css" >> .ftpignore
|
||||
|
||||
# ── Run deploy-sftp.php from MokoStandards ────────────────────────────
|
||||
DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
|
||||
if [ "$USE_PASSPHRASE" = "true" ]; then
|
||||
DEPLOY_ARGS+=(--key-passphrase "$SFTP_PASSWORD")
|
||||
fi
|
||||
|
||||
# Set platform version to "development" before deploy (Dolibarr + Joomla)
|
||||
php /tmp/mokostandards/api/cli/version_set_platform.php --path . --version development
|
||||
|
||||
# Write update files — dev/** = development, rc/** = rc
|
||||
PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true)
|
||||
REPO="${{ github.repository }}"
|
||||
BRANCH="${{ github.ref_name }}"
|
||||
|
||||
# Determine stability tag from branch prefix
|
||||
STABILITY="development"
|
||||
VERSION_LABEL="development"
|
||||
if [[ "$BRANCH" == rc/* ]]; then
|
||||
STABILITY="rc"
|
||||
VERSION_LABEL=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null || echo "${BRANCH#rc/}")-rc
|
||||
fi
|
||||
|
||||
if [ "$PLATFORM" = "crm-module" ]; then
|
||||
printf '%s' "$VERSION_LABEL" > update.txt
|
||||
fi
|
||||
|
||||
if [ "$PLATFORM" = "waas-component" ]; then
|
||||
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)
|
||||
if [ -n "$MANIFEST" ]; then
|
||||
EXT_NAME=$(grep -oP '<name>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "${{ github.event.repository.name }}")
|
||||
EXT_TYPE=$(grep -oP '<extension[^>]+type="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "component")
|
||||
EXT_ELEMENT=$(grep -oP '<element>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || basename "$MANIFEST" .xml)
|
||||
EXT_CLIENT=$(grep -oP '<extension[^>]+client="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "")
|
||||
EXT_FOLDER=$(grep -oP '<extension[^>]+group="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "")
|
||||
TARGET_PLATFORM=$(grep -oP '<targetplatform[^/]*/' "$MANIFEST" 2>/dev/null | head -1 || true)
|
||||
[ -n "$TARGET_PLATFORM" ] && TARGET_PLATFORM="${TARGET_PLATFORM}>"
|
||||
[ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="5.*" %s>' "/")
|
||||
|
||||
CLIENT_TAG=""
|
||||
if [ -n "$EXT_CLIENT" ]; then
|
||||
CLIENT_TAG="<client>${EXT_CLIENT}</client>"
|
||||
elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then
|
||||
CLIENT_TAG="<client>site</client>"
|
||||
fi
|
||||
|
||||
FOLDER_TAG=""
|
||||
if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then
|
||||
FOLDER_TAG="<folder>${EXT_FOLDER}</folder>"
|
||||
fi
|
||||
|
||||
DOWNLOAD_URL="https://github.com/${REPO}/archive/refs/heads/${BRANCH}.zip"
|
||||
|
||||
{
|
||||
printf '%s\n' '<?xml version="1.0" encoding="utf-8"?>'
|
||||
printf '%s\n' '<updates>'
|
||||
printf '%s\n' ' <update>'
|
||||
printf '%s\n' " <name>${EXT_NAME}</name>"
|
||||
printf '%s\n' " <description>${EXT_NAME} ${STABILITY} build</description>"
|
||||
printf '%s\n' " <element>${EXT_ELEMENT}</element>"
|
||||
printf '%s\n' " <type>${EXT_TYPE}</type>"
|
||||
printf '%s\n' " <version>${VERSION_LABEL}</version>"
|
||||
[ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}"
|
||||
[ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}"
|
||||
printf '%s\n' ' <tags>'
|
||||
printf '%s\n' " <tag>${STABILITY}</tag>"
|
||||
printf '%s\n' ' </tags>'
|
||||
printf '%s\n' " <infourl title=\"${EXT_NAME}\">https://github.com/${REPO}/tree/${BRANCH}</infourl>"
|
||||
printf '%s\n' ' <downloads>'
|
||||
printf '%s\n' " <downloadurl type=\"full\" format=\"zip\">${DOWNLOAD_URL}</downloadurl>"
|
||||
printf '%s\n' ' </downloads>'
|
||||
printf '%s\n' " ${TARGET_PLATFORM}"
|
||||
printf '%s\n' ' <maintainer>Moko Consulting</maintainer>'
|
||||
printf '%s\n' ' <maintainerurl>https://mokoconsulting.tech</maintainerurl>'
|
||||
printf '%s\n' ' </update>'
|
||||
printf '%s\n' '</updates>'
|
||||
} > updates.xml
|
||||
sed -i '/^[[:space:]]*$/d' updates.xml
|
||||
fi
|
||||
fi
|
||||
|
||||
# Use Joomla-aware deploy for waas-component (routes files to correct Joomla dirs)
|
||||
# Use standard SFTP deploy for everything else
|
||||
PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true)
|
||||
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards/api/deploy/deploy-joomla.php" ]; then
|
||||
php /tmp/mokostandards/api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
|
||||
else
|
||||
php /tmp/mokostandards/api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
|
||||
fi
|
||||
# (both scripts handle dotfile skipping and .ftpignore natively)
|
||||
# Remove temp files that should never be left behind
|
||||
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
||||
|
||||
# Dev deploys fail silently — no issue creation.
|
||||
# Demo and RS deploys create failure issues (production-facing).
|
||||
|
||||
- name: Deployment summary
|
||||
if: always()
|
||||
run: |
|
||||
if [ "${{ steps.source.outputs.skip }}" == "true" ]; then
|
||||
echo "### ⏭️ Deployment Skipped" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "No \`src/\` directory found in this repository." >> "$GITHUB_STEP_SUMMARY"
|
||||
elif [ "${{ job.status }}" == "success" ]; then
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "### ✅ Dev Deployment Successful" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Field | Value |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "|-------|-------|" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Host | \`${{ steps.conn.outputs.host }}:${{ steps.conn.outputs.port }}\` |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Remote path | \`${{ steps.remote.outputs.path }}\` |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Source | \`src/\` |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Trigger | ${{ github.event_name }} |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Auth | ${{ steps.auth.outputs.method }} |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Clear remote | ${{ inputs.clear_remote || 'false' }} |" >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "### ❌ Dev Deployment Failed" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "Check the job log above for error details." >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
661
.github/workflows/deploy-rs.yml
vendored
Normal file
661
.github/workflows/deploy-rs.yml
vendored
Normal file
@@ -0,0 +1,661 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# This file is part of a Moko Consulting project.
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: GitHub.Workflow
|
||||
# INGROUP: MokoStandards.Deploy
|
||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
||||
# PATH: /templates/workflows/shared/deploy-rs.yml
|
||||
# VERSION: 04.05.13
|
||||
# BRIEF: SFTP deployment workflow for release staging server — synced to all governed repos
|
||||
# NOTE: Synced via bulk-repo-sync to .github/workflows/deploy-rs.yml in all governed repos.
|
||||
# Port is resolved in order: RS_FTP_PORT variable → :port suffix in RS_FTP_HOST → 22.
|
||||
|
||||
name: Deploy to RS Server (SFTP)
|
||||
|
||||
# Deploys the contents of the src/ directory to the release staging server via SFTP.
|
||||
# Triggers on push/merge to main — deploys the production-ready build to the release staging server.
|
||||
#
|
||||
# Required org-level variables: RS_FTP_HOST, RS_FTP_PATH, RS_FTP_USERNAME
|
||||
# Optional org-level variable: RS_FTP_PORT (auto-detected from host or defaults to 22)
|
||||
# Optional org/repo variable: RS_FTP_SUFFIX — when set, appended to RS_FTP_PATH to form the
|
||||
# full remote destination: RS_FTP_PATH/RS_FTP_SUFFIX
|
||||
# Ignore rules: Place a .ftpignore file in the repository root. Each non-empty,
|
||||
# non-comment line is a glob pattern tested against the relative path
|
||||
# of each file (e.g. "subdir/file.txt"). The .gitignore is NOT used.
|
||||
# Required org-level secret: RS_FTP_KEY (preferred) or RS_FTP_PASSWORD
|
||||
#
|
||||
# Access control: only users with admin or maintain role on the repository may deploy.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'htdocs/**'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, closed]
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'htdocs/**'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
clear_remote:
|
||||
description: 'Delete all files inside the remote destination folder before uploading'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
check-permission:
|
||||
name: Verify Deployment Permission
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check actor permission
|
||||
env:
|
||||
# Prefer the org-scoped GH_TOKEN secret (needed for the org membership
|
||||
# fallback). Falls back to the built-in github.token so the collaborator
|
||||
# endpoint still works even if GH_TOKEN is not configured.
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
||||
run: |
|
||||
ACTOR="${{ github.actor }}"
|
||||
REPO="${{ github.repository }}"
|
||||
ORG="${{ github.repository_owner }}"
|
||||
|
||||
METHOD=""
|
||||
AUTHORIZED="false"
|
||||
|
||||
# Hardcoded authorized users — always allowed to deploy
|
||||
AUTHORIZED_USERS="jmiller-moko github-actions[bot]"
|
||||
for user in $AUTHORIZED_USERS; do
|
||||
if [ "$ACTOR" = "$user" ]; then
|
||||
AUTHORIZED="true"
|
||||
METHOD="hardcoded allowlist"
|
||||
PERMISSION="admin"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# For other actors, check repo/org permissions via API
|
||||
if [ "$AUTHORIZED" != "true" ]; then
|
||||
PERMISSION=$(gh api "repos/${REPO}/collaborators/${ACTOR}/permission" \
|
||||
--jq '.permission' 2>/dev/null)
|
||||
METHOD="repo collaborator API"
|
||||
|
||||
if [ -z "$PERMISSION" ]; then
|
||||
ORG_ROLE=$(gh api "orgs/${ORG}/memberships/${ACTOR}" \
|
||||
--jq '.role' 2>/dev/null)
|
||||
METHOD="org membership API"
|
||||
if [ "$ORG_ROLE" = "owner" ]; then
|
||||
PERMISSION="admin"
|
||||
else
|
||||
PERMISSION="none"
|
||||
fi
|
||||
fi
|
||||
|
||||
case "$PERMISSION" in
|
||||
admin|maintain) AUTHORIZED="true" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Write detailed summary
|
||||
{
|
||||
echo "## 🔐 Deploy Authorization"
|
||||
echo ""
|
||||
echo "| Field | Value |"
|
||||
echo "|-------|-------|"
|
||||
echo "| **Actor** | \`${ACTOR}\` |"
|
||||
echo "| **Repository** | \`${REPO}\` |"
|
||||
echo "| **Permission** | \`${PERMISSION}\` |"
|
||||
echo "| **Method** | ${METHOD} |"
|
||||
echo "| **Authorized** | ${AUTHORIZED} |"
|
||||
echo "| **Trigger** | \`${{ github.event_name }}\` |"
|
||||
echo "| **Branch** | \`${{ github.ref_name }}\` |"
|
||||
echo ""
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
if [ "$AUTHORIZED" = "true" ]; then
|
||||
echo "✅ ${ACTOR} authorized to deploy (${METHOD})" >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "❌ ${ACTOR} is NOT authorized to deploy." >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "Deployment requires one of:" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "- Being in the hardcoded allowlist" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "- Having \`admin\` or \`maintain\` role on the repository" >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
deploy:
|
||||
name: SFTP Deploy → RS
|
||||
runs-on: ubuntu-latest
|
||||
needs: [check-permission]
|
||||
if: >-
|
||||
!startsWith(github.head_ref || github.ref_name, 'chore/') &&
|
||||
(github.event_name == 'workflow_dispatch' ||
|
||||
github.event_name == 'push' ||
|
||||
(github.event_name == 'pull_request' &&
|
||||
(github.event.action == 'opened' ||
|
||||
github.event.action == 'synchronize' ||
|
||||
github.event.action == 'reopened' ||
|
||||
github.event.pull_request.merged == true)))
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Resolve source directory
|
||||
id: source
|
||||
run: |
|
||||
# Resolve source directory: src/ preferred, htdocs/ as fallback
|
||||
if [ -d "src" ]; then
|
||||
SRC="src"
|
||||
elif [ -d "htdocs" ]; then
|
||||
SRC="htdocs"
|
||||
else
|
||||
echo "⚠️ No src/ or htdocs/ directory found — skipping deployment"
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
COUNT=$(find "$SRC" -type f | wc -l)
|
||||
echo "✅ Source: ${SRC}/ (${COUNT} file(s))"
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
echo "dir=${SRC}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Preview files to deploy
|
||||
if: steps.source.outputs.skip == 'false'
|
||||
env:
|
||||
SOURCE_DIR: ${{ steps.source.outputs.dir }}
|
||||
run: |
|
||||
# ── Convert a ftpignore-style glob line to an ERE pattern ──────────────
|
||||
ftpignore_to_regex() {
|
||||
local line="$1"
|
||||
local anchored=false
|
||||
# Strip inline comments and whitespace
|
||||
line=$(printf '%s' "$line" | sed 's/[[:space:]]*#.*$//' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
[ -z "$line" ] && return
|
||||
# Skip negation patterns (not supported)
|
||||
[[ "$line" == !* ]] && return
|
||||
# Trailing slash = directory marker; strip it
|
||||
line="${line%/}"
|
||||
# Leading slash = anchored to root; strip it
|
||||
if [[ "$line" == /* ]]; then
|
||||
anchored=true
|
||||
line="${line#/}"
|
||||
fi
|
||||
# Escape ERE special chars, then restore glob semantics
|
||||
local regex
|
||||
regex=$(printf '%s' "$line" \
|
||||
| sed 's/[.+^${}()|[\\]/\\&/g' \
|
||||
| sed 's/\\\*\\\*/\x01/g' \
|
||||
| sed 's/\\\*/[^\/]*/g' \
|
||||
| sed 's/\x01/.*/g' \
|
||||
| sed 's/\\\?/[^\/]/g')
|
||||
if $anchored; then
|
||||
printf '^%s(/|$)' "$regex"
|
||||
else
|
||||
printf '(^|/)%s(/|$)' "$regex"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Read .ftpignore (ftpignore-style globs) ─────────────────────────
|
||||
IGNORE_PATTERNS=()
|
||||
IGNORE_SOURCES=()
|
||||
if [ -f ".ftpignore" ]; then
|
||||
while IFS= read -r line; do
|
||||
[[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]] && continue
|
||||
regex=$(ftpignore_to_regex "$line")
|
||||
[ -n "$regex" ] && IGNORE_PATTERNS+=("$regex") && IGNORE_SOURCES+=("$line")
|
||||
done < ".ftpignore"
|
||||
fi
|
||||
|
||||
# ── Walk src/ and classify every file ────────────────────────────────
|
||||
WILL_UPLOAD=()
|
||||
IGNORED_FILES=()
|
||||
while IFS= read -r -d '' file; do
|
||||
rel="${file#${SOURCE_DIR}/}"
|
||||
SKIP=false
|
||||
for i in "${!IGNORE_PATTERNS[@]}"; do
|
||||
if echo "$rel" | grep -qE "${IGNORE_PATTERNS[$i]}" 2>/dev/null; then
|
||||
IGNORED_FILES+=("$rel | .ftpignore \`${IGNORE_SOURCES[$i]}\`")
|
||||
SKIP=true; break
|
||||
fi
|
||||
done
|
||||
$SKIP && continue
|
||||
WILL_UPLOAD+=("$rel")
|
||||
done < <(find "$SOURCE_DIR" -type f -print0 | sort -z)
|
||||
|
||||
UPLOAD_COUNT="${#WILL_UPLOAD[@]}"
|
||||
IGNORE_COUNT="${#IGNORED_FILES[@]}"
|
||||
|
||||
echo "ℹ️ ${UPLOAD_COUNT} file(s) will be uploaded, ${IGNORE_COUNT} ignored"
|
||||
|
||||
# ── Write deployment preview to step summary ──────────────────────────
|
||||
{
|
||||
echo "## 📋 Deployment Preview"
|
||||
echo ""
|
||||
echo "| Field | Value |"
|
||||
echo "|---|---|"
|
||||
echo "| Source | \`${SOURCE_DIR}/\` |"
|
||||
echo "| Files to upload | **${UPLOAD_COUNT}** |"
|
||||
echo "| Files ignored | **${IGNORE_COUNT}** |"
|
||||
echo ""
|
||||
if [ "${UPLOAD_COUNT}" -gt 0 ]; then
|
||||
echo "### 📂 Files that will be uploaded"
|
||||
echo '```'
|
||||
printf '%s\n' "${WILL_UPLOAD[@]}"
|
||||
echo '```'
|
||||
echo ""
|
||||
fi
|
||||
if [ "${IGNORE_COUNT}" -gt 0 ]; then
|
||||
echo "### ⏭️ Files excluded"
|
||||
echo "| File | Reason |"
|
||||
echo "|---|---|"
|
||||
for entry in "${IGNORED_FILES[@]}"; do
|
||||
f="${entry% | *}"; r="${entry##* | }"
|
||||
echo "| \`${f}\` | ${r} |"
|
||||
done
|
||||
echo ""
|
||||
fi
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Resolve SFTP host and port
|
||||
if: steps.source.outputs.skip == 'false'
|
||||
id: conn
|
||||
env:
|
||||
HOST_RAW: ${{ vars.RS_FTP_HOST }}
|
||||
PORT_VAR: ${{ vars.RS_FTP_PORT }}
|
||||
run: |
|
||||
HOST="$HOST_RAW"
|
||||
PORT="$PORT_VAR"
|
||||
|
||||
if [ -z "$HOST" ]; then
|
||||
echo "⏭️ RS_FTP_HOST not configured — skipping RS deployment."
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Priority 1 — explicit RS_FTP_PORT variable
|
||||
if [ -n "$PORT" ]; then
|
||||
echo "ℹ️ Using explicit RS_FTP_PORT=${PORT}"
|
||||
|
||||
# Priority 2 — port embedded in RS_FTP_HOST (host:port)
|
||||
elif [[ "$HOST" == *:* ]]; then
|
||||
PORT="${HOST##*:}"
|
||||
HOST="${HOST%:*}"
|
||||
echo "ℹ️ Extracted port ${PORT} from RS_FTP_HOST"
|
||||
|
||||
# Priority 3 — SFTP default
|
||||
else
|
||||
PORT="22"
|
||||
echo "ℹ️ No port specified — defaulting to SFTP port 22"
|
||||
fi
|
||||
|
||||
echo "host=${HOST}" >> "$GITHUB_OUTPUT"
|
||||
echo "port=${PORT}" >> "$GITHUB_OUTPUT"
|
||||
echo "SFTP target: ${HOST}:${PORT}"
|
||||
|
||||
- name: Build remote path
|
||||
if: steps.source.outputs.skip == 'false' && steps.conn.outputs.skip != 'true'
|
||||
id: remote
|
||||
env:
|
||||
RS_FTP_PATH: ${{ vars.RS_FTP_PATH }}
|
||||
RS_FTP_SUFFIX: ${{ vars.RS_FTP_SUFFIX }}
|
||||
run: |
|
||||
BASE="$RS_FTP_PATH"
|
||||
|
||||
if [ -z "$BASE" ]; then
|
||||
echo "⏭️ RS_FTP_PATH not configured — skipping RS deployment."
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# RS_FTP_SUFFIX is required — it identifies the remote subdirectory for this repo.
|
||||
# Without it we cannot safely determine the deployment target.
|
||||
if [ -z "$RS_FTP_SUFFIX" ]; then
|
||||
echo "⏭️ RS_FTP_SUFFIX variable is not set — skipping deployment."
|
||||
echo " Set RS_FTP_SUFFIX as a repo or org variable to enable deploy-rs."
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
echo "path=" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
REMOTE="${BASE%/}/${RS_FTP_SUFFIX#/}"
|
||||
|
||||
# ── Platform-specific path safety guards ──────────────────────────────
|
||||
PLATFORM=""
|
||||
MOKO_FILE=".github/.mokostandards"; [ ! -f "$MOKO_FILE" ] && MOKO_FILE=".mokostandards"; if [ -f "$MOKO_FILE" ]; then
|
||||
PLATFORM=$(grep -E '^platform:' "$MOKO_FILE" | sed 's/.*:[[:space:]]*//' | tr -d '"')
|
||||
fi
|
||||
|
||||
# RS deployment: no path restrictions for any platform
|
||||
|
||||
echo "ℹ️ Remote path: ${REMOTE}"
|
||||
echo "path=${REMOTE}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Detect SFTP authentication method
|
||||
if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
|
||||
id: auth
|
||||
env:
|
||||
HAS_KEY: ${{ secrets.RS_FTP_KEY }}
|
||||
HAS_PASSWORD: ${{ secrets.RS_FTP_PASSWORD }}
|
||||
run: |
|
||||
if [ -n "$HAS_KEY" ] && [ -n "$HAS_PASSWORD" ]; then
|
||||
# Both set: key auth with password as passphrase; falls back to password-only if key fails
|
||||
echo "method=key" >> "$GITHUB_OUTPUT"
|
||||
echo "use_passphrase=true" >> "$GITHUB_OUTPUT"
|
||||
echo "has_password=true" >> "$GITHUB_OUTPUT"
|
||||
echo "ℹ️ Primary: SSH key + passphrase (RS_FTP_KEY / RS_FTP_PASSWORD)"
|
||||
echo "ℹ️ Fallback: password-only auth if key authentication fails"
|
||||
elif [ -n "$HAS_KEY" ]; then
|
||||
# Key only: no passphrase, no password fallback
|
||||
echo "method=key" >> "$GITHUB_OUTPUT"
|
||||
echo "use_passphrase=false" >> "$GITHUB_OUTPUT"
|
||||
echo "has_password=false" >> "$GITHUB_OUTPUT"
|
||||
echo "ℹ️ Using SSH key authentication (RS_FTP_KEY, no passphrase, no fallback)"
|
||||
elif [ -n "$HAS_PASSWORD" ]; then
|
||||
# Password only: direct SFTP password auth
|
||||
echo "method=password" >> "$GITHUB_OUTPUT"
|
||||
echo "use_passphrase=false" >> "$GITHUB_OUTPUT"
|
||||
echo "has_password=true" >> "$GITHUB_OUTPUT"
|
||||
echo "ℹ️ Using password authentication (RS_FTP_PASSWORD)"
|
||||
else
|
||||
echo "❌ No SFTP credentials configured."
|
||||
echo " Set RS_FTP_KEY (preferred) or RS_FTP_PASSWORD as an org-level secret."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Setup PHP
|
||||
if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
|
||||
uses: shivammathur/setup-php@fcafdd6392932010c2bd5094439b8e33be2a8a09 # v2.37.0
|
||||
with:
|
||||
php-version: '8.1'
|
||||
tools: composer
|
||||
|
||||
- name: Setup MokoStandards deploy tools
|
||||
if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}'
|
||||
run: |
|
||||
git clone --depth 1 --branch version/04 --quiet \
|
||||
"https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \
|
||||
/tmp/mokostandards
|
||||
cd /tmp/mokostandards
|
||||
composer install --no-dev --no-interaction --quiet
|
||||
|
||||
- name: Clear remote destination folder (manual only)
|
||||
if: >-
|
||||
steps.source.outputs.skip == 'false' &&
|
||||
steps.remote.outputs.skip != 'true' &&
|
||||
inputs.clear_remote == true
|
||||
env:
|
||||
SFTP_HOST: ${{ steps.conn.outputs.host }}
|
||||
SFTP_PORT: ${{ steps.conn.outputs.port }}
|
||||
SFTP_USER: ${{ vars.RS_FTP_USERNAME }}
|
||||
SFTP_KEY: ${{ secrets.RS_FTP_KEY }}
|
||||
SFTP_PASSWORD: ${{ secrets.RS_FTP_PASSWORD }}
|
||||
AUTH_METHOD: ${{ steps.auth.outputs.method }}
|
||||
USE_PASSPHRASE: ${{ steps.auth.outputs.use_passphrase }}
|
||||
HAS_PASSWORD: ${{ steps.auth.outputs.has_password }}
|
||||
REMOTE_PATH: ${{ steps.remote.outputs.path }}
|
||||
run: |
|
||||
cat > /tmp/moko_clear.php << 'PHPEOF'
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require '/tmp/mokostandards/vendor/autoload.php';
|
||||
|
||||
use phpseclib3\Net\SFTP;
|
||||
use phpseclib3\Crypt\PublicKeyLoader;
|
||||
|
||||
$host = (string) getenv('SFTP_HOST');
|
||||
$port = (int) getenv('SFTP_PORT');
|
||||
$username = (string) getenv('SFTP_USER');
|
||||
$authMethod = (string) getenv('AUTH_METHOD');
|
||||
$usePassphrase = getenv('USE_PASSPHRASE') === 'true';
|
||||
$hasPassword = getenv('HAS_PASSWORD') === 'true';
|
||||
$remotePath = rtrim((string) getenv('REMOTE_PATH'), '/');
|
||||
|
||||
echo "⚠️ Clearing remote folder: {$remotePath}\n";
|
||||
|
||||
$sftp = new SFTP($host, $port);
|
||||
|
||||
// ── Authentication ──────────────────────────────────────────────
|
||||
if ($authMethod === 'key') {
|
||||
$keyData = (string) getenv('SFTP_KEY');
|
||||
$passphrase = $usePassphrase ? (string) getenv('SFTP_PASSWORD') : false;
|
||||
$password = $hasPassword ? (string) getenv('SFTP_PASSWORD') : '';
|
||||
$key = PublicKeyLoader::load($keyData, $passphrase);
|
||||
if (!$sftp->login($username, $key)) {
|
||||
if ($password !== '') {
|
||||
echo "⚠️ Key auth failed — falling back to password\n";
|
||||
if (!$sftp->login($username, $password)) {
|
||||
fwrite(STDERR, "❌ Both key and password authentication failed\n");
|
||||
exit(1);
|
||||
}
|
||||
echo "✅ Connected via password authentication (key fallback)\n";
|
||||
} else {
|
||||
fwrite(STDERR, "❌ Key authentication failed and no password fallback is available\n");
|
||||
exit(1);
|
||||
}
|
||||
} else {
|
||||
echo "✅ Connected via SSH key authentication\n";
|
||||
}
|
||||
} else {
|
||||
if (!$sftp->login($username, (string) getenv('SFTP_PASSWORD'))) {
|
||||
fwrite(STDERR, "❌ Password authentication failed\n");
|
||||
exit(1);
|
||||
}
|
||||
echo "✅ Connected via password authentication\n";
|
||||
}
|
||||
|
||||
// ── Recursive delete ────────────────────────────────────────────
|
||||
function rmrf(SFTP $sftp, string $path): void
|
||||
{
|
||||
$entries = $sftp->nlist($path);
|
||||
if ($entries === false) {
|
||||
return; // path does not exist — nothing to clear
|
||||
}
|
||||
foreach ($entries as $name) {
|
||||
if ($name === '.' || $name === '..') {
|
||||
continue;
|
||||
}
|
||||
$entry = "{$path}/{$name}";
|
||||
if ($sftp->is_dir($entry)) {
|
||||
rmrf($sftp, $entry);
|
||||
$sftp->rmdir($entry);
|
||||
echo " 🗑️ Removed dir: {$entry}\n";
|
||||
} else {
|
||||
$sftp->delete($entry);
|
||||
echo " 🗑️ Removed file: {$entry}\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Create remote directory tree ────────────────────────────────
|
||||
function sftpMakedirs(SFTP $sftp, string $path): void
|
||||
{
|
||||
$parts = array_values(array_filter(explode('/', $path), fn(string $p) => $p !== ''));
|
||||
$current = str_starts_with($path, '/') ? '' : '';
|
||||
foreach ($parts as $part) {
|
||||
$current .= '/' . $part;
|
||||
$sftp->mkdir($current); // silently returns false if already exists
|
||||
}
|
||||
}
|
||||
|
||||
rmrf($sftp, $remotePath);
|
||||
sftpMakedirs($sftp, $remotePath);
|
||||
echo "✅ Remote folder ready: {$remotePath}\n";
|
||||
PHPEOF
|
||||
php /tmp/moko_clear.php
|
||||
|
||||
- name: Deploy via SFTP
|
||||
if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
|
||||
env:
|
||||
SFTP_HOST: ${{ steps.conn.outputs.host }}
|
||||
SFTP_PORT: ${{ steps.conn.outputs.port }}
|
||||
SFTP_USER: ${{ vars.RS_FTP_USERNAME }}
|
||||
SFTP_KEY: ${{ secrets.RS_FTP_KEY }}
|
||||
SFTP_PASSWORD: ${{ secrets.RS_FTP_PASSWORD }}
|
||||
AUTH_METHOD: ${{ steps.auth.outputs.method }}
|
||||
USE_PASSPHRASE: ${{ steps.auth.outputs.use_passphrase }}
|
||||
REMOTE_PATH: ${{ steps.remote.outputs.path }}
|
||||
SOURCE_DIR: ${{ steps.source.outputs.dir }}
|
||||
run: |
|
||||
# ── Write SSH key to temp file (key auth only) ────────────────────────
|
||||
if [ "$AUTH_METHOD" = "key" ]; then
|
||||
printf '%s' "$SFTP_KEY" > /tmp/deploy_key
|
||||
chmod 600 /tmp/deploy_key
|
||||
fi
|
||||
|
||||
# ── Generate sftp-config.json safely via jq ───────────────────────────
|
||||
if [ "$AUTH_METHOD" = "key" ]; then
|
||||
jq -n \
|
||||
--arg host "$SFTP_HOST" \
|
||||
--argjson port "${SFTP_PORT:-22}" \
|
||||
--arg user "$SFTP_USER" \
|
||||
--arg path "$REMOTE_PATH" \
|
||||
--arg key "/tmp/deploy_key" \
|
||||
'{host:$host, port:$port, user:$user, remote_path:$path, ssh_key_file:$key}' \
|
||||
> /tmp/sftp-config.json
|
||||
else
|
||||
jq -n \
|
||||
--arg host "$SFTP_HOST" \
|
||||
--argjson port "${SFTP_PORT:-22}" \
|
||||
--arg user "$SFTP_USER" \
|
||||
--arg path "$REMOTE_PATH" \
|
||||
--arg pass "$SFTP_PASSWORD" \
|
||||
'{host:$host, port:$port, user:$user, remote_path:$path, password:$pass}' \
|
||||
> /tmp/sftp-config.json
|
||||
fi
|
||||
|
||||
# ── Run deploy-sftp.php from MokoStandards ────────────────────────────
|
||||
DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
|
||||
if [ "$USE_PASSPHRASE" = "true" ]; then
|
||||
DEPLOY_ARGS+=(--key-passphrase "$SFTP_PASSWORD")
|
||||
fi
|
||||
|
||||
PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true)
|
||||
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards/api/deploy/deploy-joomla.php" ]; then
|
||||
php /tmp/mokostandards/api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
|
||||
else
|
||||
php /tmp/mokostandards/api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
|
||||
fi
|
||||
# Remove temp files that should never be left behind
|
||||
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
||||
|
||||
- name: Create or update failure issue
|
||||
if: failure() && steps.remote.outputs.skip != 'true' && steps.conn.outputs.skip != 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
||||
run: |
|
||||
REPO="${{ github.repository }}"
|
||||
RUN_URL="${{ github.server_url }}/${REPO}/actions/runs/${{ github.run_id }}"
|
||||
ACTOR="${{ github.actor }}"
|
||||
BRANCH="${{ github.ref_name }}"
|
||||
EVENT="${{ github.event_name }}"
|
||||
NOW=$(date -u '+%Y-%m-%d %H:%M:%S UTC')
|
||||
LABEL="deploy-failure"
|
||||
|
||||
TITLE="fix: RS deployment failed — ${REPO}"
|
||||
BODY="## RS Deployment Failed
|
||||
|
||||
A deployment to the RS server failed and requires attention.
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Repository** | \`${REPO}\` |
|
||||
| **Branch** | \`${BRANCH}\` |
|
||||
| **Trigger** | ${EVENT} |
|
||||
| **Actor** | @${ACTOR} |
|
||||
| **Failed at** | ${NOW} |
|
||||
| **Run** | [View workflow run](${RUN_URL}) |
|
||||
|
||||
### Next steps
|
||||
1. Review the [workflow run log](${RUN_URL}) for the specific error.
|
||||
2. Fix the underlying issue (credentials, SFTP connectivity, permissions).
|
||||
3. Re-trigger the deployment via **Actions → Deploy to RS Server → Run workflow**.
|
||||
|
||||
---
|
||||
*Auto-created by deploy-rs.yml — close this issue once the deployment is resolved.*"
|
||||
|
||||
# Ensure the label exists (idempotent — no-op if already present)
|
||||
gh label create "$LABEL" \
|
||||
--repo "$REPO" \
|
||||
--color "CC0000" \
|
||||
--description "Automated deploy failure tracking" \
|
||||
--force 2>/dev/null || true
|
||||
|
||||
# Look for an existing deploy-failure issue (any state — reopen if closed)
|
||||
EXISTING=$(gh api "repos/${REPO}/issues?labels=${LABEL}&state=all&per_page=1&sort=created&direction=desc" \
|
||||
--jq '.[0].number' 2>/dev/null)
|
||||
|
||||
if [ -n "$EXISTING" ] && [ "$EXISTING" != "null" ]; then
|
||||
gh api "repos/${REPO}/issues/${EXISTING}" \
|
||||
-X PATCH \
|
||||
-f title="$TITLE" \
|
||||
-f body="$BODY" \
|
||||
-f state="open" \
|
||||
--silent
|
||||
echo "📋 Failure issue #${EXISTING} updated/reopened: ${REPO}" >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
gh issue create \
|
||||
--repo "$REPO" \
|
||||
--title "$TITLE" \
|
||||
--body "$BODY" \
|
||||
--label "$LABEL" \
|
||||
--assignee "jmiller-moko" \
|
||||
| tee -a "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
- name: Deployment summary
|
||||
if: always()
|
||||
run: |
|
||||
if [ "${{ steps.source.outputs.skip }}" == "true" ]; then
|
||||
echo "### ⏭️ Deployment Skipped" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "No \`src/\` directory found in this repository." >> "$GITHUB_STEP_SUMMARY"
|
||||
elif [ "${{ job.status }}" == "success" ]; then
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "### ✅ RS Deployment Successful" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Field | Value |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "|-------|-------|" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Host | \`${{ steps.conn.outputs.host }}:${{ steps.conn.outputs.port }}\` |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Remote path | \`${{ steps.remote.outputs.path }}\` |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Source | \`src/\` |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Trigger | ${{ github.event_name }} |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Auth | ${{ steps.auth.outputs.method }} |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Clear remote | ${{ inputs.clear_remote || 'false' }} |" >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "### ❌ RS Deployment Failed" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "Check the job log above for error details." >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
@@ -21,8 +21,8 @@
|
||||
# DEFGROUP: GitHub.Workflow
|
||||
# INGROUP: MokoStandards.Firewall
|
||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
||||
# PATH: /templates/workflows/shared/enterprise-firewall-setup.yml.template
|
||||
# VERSION: 04.06.00
|
||||
# PATH: /templates/workflows/shared/enterprise-firewall-setup.yml
|
||||
# VERSION: 04.05.13
|
||||
# BRIEF: Enterprise firewall configuration — generates outbound allow-rules including SFTP deployment server
|
||||
# NOTE: Reads DEV_FTP_HOST / DEV_FTP_PORT variables to include SFTP egress rules alongside HTTPS rules.
|
||||
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
|
||||
2
.github/workflows/repo_health.yml
vendored
2
.github/workflows/repo_health.yml
vendored
@@ -10,7 +10,7 @@
|
||||
# INGROUP: MokoStandards.Validation
|
||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
||||
# PATH: /.github/workflows/repo_health.yml
|
||||
# VERSION: 04.06.00
|
||||
# VERSION: 04.05.00
|
||||
# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts.
|
||||
# NOTE: Field is user-managed.
|
||||
# ============================================================================
|
||||
|
||||
8
.github/workflows/repository-cleanup.yml
vendored
8
.github/workflows/repository-cleanup.yml
vendored
@@ -8,8 +8,8 @@
|
||||
# DEFGROUP: GitHub.Workflow
|
||||
# INGROUP: MokoStandards.Maintenance
|
||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
||||
# PATH: /templates/workflows/shared/repository-cleanup.yml.template
|
||||
# VERSION: 04.06.00
|
||||
# PATH: /templates/workflows/shared/repository-cleanup.yml
|
||||
# VERSION: 04.05.13
|
||||
# BRIEF: Recurring repository maintenance — labels, branches, workflows, logs, doc indexes
|
||||
# NOTE: Synced via bulk-repo-sync to .github/workflows/repository-cleanup.yml in all governed repos.
|
||||
# Runs on the 1st and 15th of each month at 6:00 AM UTC, and on manual dispatch.
|
||||
@@ -154,10 +154,6 @@ jobs:
|
||||
".github/workflows/auto-version-branch.yml"
|
||||
".github/workflows/publish-to-mokodolibarr.yml"
|
||||
".github/workflows/ci.yml"
|
||||
".github/workflows/deploy-rs.yml"
|
||||
"sftp-config.json"
|
||||
"sftp-config.json.template"
|
||||
"scripts/sftp-config"
|
||||
)
|
||||
|
||||
DELETED=0
|
||||
|
||||
2
.github/workflows/standards-compliance.yml
vendored
2
.github/workflows/standards-compliance.yml
vendored
@@ -5,7 +5,7 @@
|
||||
# INGROUP: MokoStandards.Compliance
|
||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
||||
# PATH: /.github/workflows/standards-compliance.yml
|
||||
# VERSION: 04.06.00
|
||||
# VERSION: 04.05.00
|
||||
# BRIEF: MokoStandards compliance validation workflow
|
||||
# NOTE: Validates repository structure, documentation, and coding standards
|
||||
|
||||
|
||||
12
.github/workflows/sync-version-on-merge.yml
vendored
12
.github/workflows/sync-version-on-merge.yml
vendored
@@ -8,8 +8,8 @@
|
||||
# DEFGROUP: GitHub.Workflow
|
||||
# INGROUP: MokoStandards.Automation
|
||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
||||
# PATH: /templates/workflows/shared/sync-version-on-merge.yml.template
|
||||
# VERSION: 04.06.00
|
||||
# PATH: /templates/workflows/shared/sync-version-on-merge.yml
|
||||
# VERSION: 04.05.13
|
||||
# BRIEF: Auto-bump patch version on every push to main and propagate to all file headers
|
||||
# NOTE: Synced via bulk-repo-sync to .github/workflows/sync-version-on-merge.yml in all governed repos.
|
||||
# README.md is the single source of truth for the repository version.
|
||||
@@ -17,10 +17,10 @@
|
||||
name: Sync Version from README
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
@@ -39,8 +39,6 @@ jobs:
|
||||
sync-version:
|
||||
name: Propagate README version
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -67,7 +65,7 @@ jobs:
|
||||
composer install --no-dev --no-interaction --quiet
|
||||
|
||||
- name: Auto-bump patch version
|
||||
if: ${{ github.event_name != 'workflow_dispatch' && github.actor != 'github-actions[bot]' }}
|
||||
if: ${{ github.event_name == 'push' && github.actor != 'github-actions[bot]' }}
|
||||
run: |
|
||||
if git diff --name-only HEAD~1 HEAD 2>/dev/null | grep -q '^README\.md$'; then
|
||||
echo "README.md changed in this push — skipping auto-bump"
|
||||
|
||||
290
.github/workflows/update-server.yml
vendored
290
.github/workflows/update-server.yml
vendored
@@ -6,8 +6,8 @@
|
||||
# DEFGROUP: GitHub.Workflow
|
||||
# INGROUP: MokoStandards.Joomla
|
||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
||||
# PATH: /templates/workflows/joomla/update-server.yml.template
|
||||
# VERSION: 04.06.00
|
||||
# PATH: /templates/workflows/joomla/update-server.yml
|
||||
# VERSION: 04.05.13
|
||||
# BRIEF: Update Joomla update server XML feed with stable/rc/dev entries
|
||||
#
|
||||
# Writes updates.xml with multiple <update> entries:
|
||||
@@ -20,12 +20,9 @@
|
||||
name: Update Joomla Update Server XML Feed
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
push:
|
||||
branches:
|
||||
- 'dev/**'
|
||||
- 'alpha/**'
|
||||
- 'beta/**'
|
||||
- 'rc/**'
|
||||
paths:
|
||||
- 'src/**'
|
||||
@@ -33,14 +30,12 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
stability:
|
||||
description: 'Stability tag'
|
||||
description: 'Stability tag (development, rc, stable)'
|
||||
required: true
|
||||
default: 'development'
|
||||
type: choice
|
||||
options:
|
||||
- development
|
||||
- alpha
|
||||
- beta
|
||||
- rc
|
||||
- stable
|
||||
|
||||
@@ -54,8 +49,6 @@ jobs:
|
||||
update-xml:
|
||||
name: Update updates.xml
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -82,64 +75,33 @@ jobs:
|
||||
REPO="${{ github.repository }}"
|
||||
VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null || echo "0.0.0")
|
||||
|
||||
# Auto-bump patch on alpha/beta/rc branches (not dev — dev bumps manually)
|
||||
if [[ "$BRANCH" != dev/* ]]; then
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
BUMPED=$(php /tmp/mokostandards/api/cli/version_bump.php --path . 2>/dev/null || true)
|
||||
if [ -n "$BUMPED" ]; then
|
||||
VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null || echo "$VERSION")
|
||||
git add -A
|
||||
git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \
|
||||
--author="github-actions[bot] <github-actions[bot]@users.noreply.github.com>" 2>/dev/null || true
|
||||
git push 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Determine stability from branch or input
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
STABILITY="${{ inputs.stability }}"
|
||||
elif [[ "$BRANCH" == rc/* ]]; then
|
||||
STABILITY="rc"
|
||||
elif [[ "$BRANCH" == beta/* ]]; then
|
||||
STABILITY="beta"
|
||||
elif [[ "$BRANCH" == alpha/* ]]; then
|
||||
STABILITY="alpha"
|
||||
elif [[ "$BRANCH" == dev/* ]]; then
|
||||
STABILITY="development"
|
||||
else
|
||||
STABILITY="stable"
|
||||
fi
|
||||
|
||||
# Parse manifest (portable — no grep -P)
|
||||
# Parse manifest
|
||||
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||
if [ -z "$MANIFEST" ]; then
|
||||
echo "No Joomla manifest found — skipping"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Extract fields using sed (works on all runners)
|
||||
EXT_NAME=$(sed -n 's/.*<name>\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1)
|
||||
EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
||||
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1)
|
||||
EXT_CLIENT=$(sed -n 's/.*<extension[^>]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
||||
EXT_FOLDER=$(sed -n 's/.*<extension[^>]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
||||
EXT_VERSION=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1)
|
||||
TARGET_PLATFORM=$(sed -n 's/.*\(<targetplatform[^/]*\/>\).*/\1/p' "$MANIFEST" | head -1)
|
||||
PHP_MINIMUM=$(sed -n 's/.*<php_minimum>\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1)
|
||||
|
||||
# Fallbacks
|
||||
[ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}"
|
||||
[ -z "$EXT_TYPE" ] && EXT_TYPE="component"
|
||||
|
||||
# Templates and modules don't have <element> — derive from <name>
|
||||
if [ -z "$EXT_ELEMENT" ]; then
|
||||
EXT_ELEMENT=$(echo "$EXT_NAME" | tr '[:upper:]' '[:lower:]' | tr -d ' ')
|
||||
fi
|
||||
|
||||
# Use manifest version if README version is empty
|
||||
[ "$VERSION" = "0.0.0" ] && [ -n "$EXT_VERSION" ] && VERSION="$EXT_VERSION"
|
||||
EXT_NAME=$(grep -oP '<name>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "${{ github.event.repository.name }}")
|
||||
EXT_TYPE=$(grep -oP '<extension[^>]+type="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "component")
|
||||
EXT_ELEMENT=$(grep -oP '<element>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || basename "$MANIFEST" .xml)
|
||||
EXT_CLIENT=$(grep -oP '<extension[^>]+client="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "")
|
||||
EXT_FOLDER=$(grep -oP '<extension[^>]+group="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "")
|
||||
TARGET_PLATFORM=$(grep -oP '<targetplatform[^/]*/>' "$MANIFEST" 2>/dev/null | head -1 || echo "")
|
||||
PHP_MINIMUM=$(grep -oP '<php_minimum>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "")
|
||||
|
||||
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(basename "$MANIFEST" .xml)
|
||||
[ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="5.*" %s>' "/")
|
||||
|
||||
CLIENT_TAG=""
|
||||
@@ -154,117 +116,127 @@ jobs:
|
||||
|
||||
# Version suffix for non-stable
|
||||
DISPLAY_VERSION="$VERSION"
|
||||
case "$STABILITY" in
|
||||
development) DISPLAY_VERSION="${VERSION}-dev" ;;
|
||||
alpha) DISPLAY_VERSION="${VERSION}-alpha" ;;
|
||||
beta) DISPLAY_VERSION="${VERSION}-beta" ;;
|
||||
rc) DISPLAY_VERSION="${VERSION}-rc" ;;
|
||||
esac
|
||||
[ "$STABILITY" = "rc" ] && DISPLAY_VERSION="${VERSION}-rc"
|
||||
[ "$STABILITY" = "development" ] && DISPLAY_VERSION="${VERSION}-dev"
|
||||
|
||||
MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
|
||||
|
||||
# Each stability level has its own release tag
|
||||
case "$STABILITY" in
|
||||
development) RELEASE_TAG="development" ;;
|
||||
alpha) RELEASE_TAG="alpha" ;;
|
||||
beta) RELEASE_TAG="beta" ;;
|
||||
rc) RELEASE_TAG="release-candidate" ;;
|
||||
*) RELEASE_TAG="v${MAJOR}" ;;
|
||||
esac
|
||||
|
||||
RELEASE_TAG="v${MAJOR}"
|
||||
PACKAGE_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.zip"
|
||||
DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}"
|
||||
INFO_URL="https://github.com/${REPO}"
|
||||
|
||||
# ── Build install packages (ZIP + tar.gz) ───────────────────
|
||||
# ── Build install-ready ZIP ─────────────────────────────────
|
||||
SOURCE_DIR="src"
|
||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||
if [ -d "$SOURCE_DIR" ]; then
|
||||
EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*"
|
||||
TAR_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.tar.gz"
|
||||
|
||||
cd "$SOURCE_DIR"
|
||||
zip -r "/tmp/${PACKAGE_NAME}" . -x $EXCLUDES
|
||||
zip -r "/tmp/${PACKAGE_NAME}" .
|
||||
cd ..
|
||||
tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \
|
||||
--exclude='.ftpignore' --exclude='sftp-config*' \
|
||||
--exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' .
|
||||
|
||||
SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1)
|
||||
|
||||
# Ensure release exists
|
||||
# Ensure draft release exists for this major
|
||||
gh release view "$RELEASE_TAG" --json tagName > /dev/null 2>&1 || \
|
||||
gh release create "$RELEASE_TAG" --title "${RELEASE_TAG} (${DISPLAY_VERSION})" --notes "${STABILITY} release" --prerelease --target main 2>/dev/null || true
|
||||
gh release create "$RELEASE_TAG" --title "v${MAJOR}" --notes "Development release" --draft --target main 2>/dev/null || true
|
||||
|
||||
# Upload both formats
|
||||
# Upload ZIP to the major release
|
||||
gh release upload "$RELEASE_TAG" "/tmp/${PACKAGE_NAME}" --clobber 2>/dev/null || true
|
||||
gh release upload "$RELEASE_TAG" "/tmp/${TAR_NAME}" --clobber 2>/dev/null || true
|
||||
|
||||
echo "Packages: ${PACKAGE_NAME} + ${TAR_NAME} (SHA: ${SHA256})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Package: ${PACKAGE_NAME} (SHA: ${SHA256})" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
SHA256=""
|
||||
fi
|
||||
|
||||
# ── Build the new entry ───────────────────────────────────────
|
||||
NEW_ENTRY=""
|
||||
NEW_ENTRY="${NEW_ENTRY} <update>\n"
|
||||
NEW_ENTRY="${NEW_ENTRY} <name>${EXT_NAME}</name>\n"
|
||||
NEW_ENTRY="${NEW_ENTRY} <description>${EXT_NAME} (${STABILITY})</description>\n"
|
||||
NEW_ENTRY="${NEW_ENTRY} <element>${EXT_ELEMENT}</element>\n"
|
||||
NEW_ENTRY="${NEW_ENTRY} <type>${EXT_TYPE}</type>\n"
|
||||
NEW_ENTRY="${NEW_ENTRY} <version>${DISPLAY_VERSION}</version>\n"
|
||||
[ -n "$CLIENT_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${CLIENT_TAG}\n"
|
||||
[ -n "$FOLDER_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${FOLDER_TAG}\n"
|
||||
NEW_ENTRY="${NEW_ENTRY} <tags>\n"
|
||||
NEW_ENTRY="${NEW_ENTRY} <tag>${STABILITY}</tag>\n"
|
||||
NEW_ENTRY="${NEW_ENTRY} </tags>\n"
|
||||
NEW_ENTRY="${NEW_ENTRY} <infourl title=\"${EXT_NAME}\">${INFO_URL}</infourl>\n"
|
||||
NEW_ENTRY="${NEW_ENTRY} <downloads>\n"
|
||||
TAR_URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${EXT_ELEMENT}-${DISPLAY_VERSION}.tar.gz"
|
||||
NEW_ENTRY="${NEW_ENTRY} <downloadurl type=\"full\" format=\"zip\">${DOWNLOAD_URL}</downloadurl>\n"
|
||||
NEW_ENTRY="${NEW_ENTRY} <downloadurl type=\"full\" format=\"tar.gz\">${TAR_URL}</downloadurl>\n"
|
||||
NEW_ENTRY="${NEW_ENTRY} </downloads>\n"
|
||||
[ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} <sha256>sha256:${SHA256}</sha256>\n"
|
||||
NEW_ENTRY="${NEW_ENTRY} ${TARGET_PLATFORM}\n"
|
||||
[ -n "$PHP_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${PHP_TAG}\n"
|
||||
NEW_ENTRY="${NEW_ENTRY} <maintainer>Moko Consulting</maintainer>\n"
|
||||
NEW_ENTRY="${NEW_ENTRY} <maintainerurl>https://mokoconsulting.tech</maintainerurl>\n"
|
||||
NEW_ENTRY="${NEW_ENTRY} </update>"
|
||||
|
||||
# ── Write new entry to temp file ───────────────────────────────
|
||||
printf '%b' "$NEW_ENTRY" > /tmp/new_entry.xml
|
||||
NEW_ENTRY=$(cat <<XMLEOF
|
||||
<update>
|
||||
<name>${EXT_NAME}</name>
|
||||
<description>${EXT_NAME} (${STABILITY})</description>
|
||||
<element>${EXT_ELEMENT}</element>
|
||||
<type>${EXT_TYPE}</type>
|
||||
<version>${DISPLAY_VERSION}</version>
|
||||
$([ -n "$CLIENT_TAG" ] && echo " ${CLIENT_TAG}")
|
||||
$([ -n "$FOLDER_TAG" ] && echo " ${FOLDER_TAG}")
|
||||
<tags>
|
||||
<tag>${STABILITY}</tag>
|
||||
</tags>
|
||||
<infourl title="${EXT_NAME}">${INFO_URL}</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="zip">${DOWNLOAD_URL}</downloadurl>
|
||||
</downloads>
|
||||
$([ -n "$SHA256" ] && echo " <sha256>sha256:${SHA256}</sha256>")
|
||||
${TARGET_PLATFORM}
|
||||
$([ -n "$PHP_TAG" ] && echo " ${PHP_TAG}")
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||
</update>
|
||||
XMLEOF
|
||||
)
|
||||
|
||||
# ── Merge into updates.xml ─────────────────────────────────────
|
||||
if [ ! -f "updates.xml" ]; then
|
||||
# Create fresh
|
||||
printf '%s\n' '<?xml version="1.0" encoding="utf-8"?>' > updates.xml
|
||||
printf '%s\n' '<updates>' >> updates.xml
|
||||
cat /tmp/new_entry.xml >> updates.xml
|
||||
printf '\n%s\n' '</updates>' >> updates.xml
|
||||
echo "$NEW_ENTRY" >> updates.xml
|
||||
printf '%s\n' '</updates>' >> updates.xml
|
||||
else
|
||||
# Remove existing entry for this stability, insert new one
|
||||
printf 'import re\nstability = "%s"\n' "${STABILITY}" > /tmp/merge_xml.py
|
||||
printf 'with open("updates.xml") as f: content = f.read()\n' >> /tmp/merge_xml.py
|
||||
printf 'with open("/tmp/new_entry.xml") as f: new_entry = f.read()\n' >> /tmp/merge_xml.py
|
||||
printf 'pattern = r" <update>.*?<tag>" + re.escape(stability) + r"</tag>.*?</update>\\n?"\n' >> /tmp/merge_xml.py
|
||||
printf 'content = re.sub(pattern, "", content, flags=re.DOTALL)\n' >> /tmp/merge_xml.py
|
||||
printf 'content = content.replace("</updates>", new_entry + "\\n</updates>")\n' >> /tmp/merge_xml.py
|
||||
printf 'content = re.sub(r"\\n{3,}", "\\n\\n", content)\n' >> /tmp/merge_xml.py
|
||||
printf 'with open("updates.xml", "w") as f: f.write(content)\n' >> /tmp/merge_xml.py
|
||||
python3 /tmp/merge_xml.py 2>/dev/null || {
|
||||
# Fallback: rebuild keeping other stability entries
|
||||
# Remove existing entry for this stability, add new one
|
||||
# Use python for reliable XML manipulation
|
||||
python3 -c "
|
||||
import re, sys
|
||||
|
||||
with open('updates.xml', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Remove existing entry with this stability tag
|
||||
pattern = r' <update>.*?<tag>${STABILITY}</tag>.*?</update>\n?'
|
||||
content = re.sub(pattern, '', content, flags=re.DOTALL)
|
||||
|
||||
# Insert new entry before </updates>
|
||||
new_entry = '''${NEW_ENTRY}'''
|
||||
content = content.replace('</updates>', new_entry + '\n</updates>')
|
||||
|
||||
# Clean up empty lines
|
||||
content = re.sub(r'\n{3,}', '\n\n', content)
|
||||
|
||||
with open('updates.xml', 'w') as f:
|
||||
f.write(content)
|
||||
" 2>/dev/null || {
|
||||
# Fallback: just rewrite the whole file if python fails
|
||||
# Keep existing stable entry if present
|
||||
STABLE_ENTRY=""
|
||||
if [ "$STABILITY" != "stable" ] && grep -q '<tag>stable</tag>' updates.xml; then
|
||||
STABLE_ENTRY=$(sed -n '/<update>/,/<\/update>/{ /<tag>stable<\/tag>/,/<\/update>/p; /<update>/,/<tag>stable<\/tag>/p }' updates.xml | sort -u)
|
||||
fi
|
||||
RC_ENTRY=""
|
||||
if [ "$STABILITY" != "rc" ] && grep -q '<tag>rc</tag>' updates.xml; then
|
||||
RC_ENTRY=$(python3 -c "
|
||||
import re
|
||||
with open('updates.xml') as f: c = f.read()
|
||||
m = re.search(r'(<update>.*?<tag>rc</tag>.*?</update>)', c, re.DOTALL)
|
||||
if m: print(m.group(1))
|
||||
" 2>/dev/null || true)
|
||||
fi
|
||||
DEV_ENTRY=""
|
||||
if [ "$STABILITY" != "development" ] && grep -q '<tag>development</tag>' updates.xml; then
|
||||
DEV_ENTRY=$(python3 -c "
|
||||
import re
|
||||
with open('updates.xml') as f: c = f.read()
|
||||
m = re.search(r'(<update>.*?<tag>development</tag>.*?</update>)', c, re.DOTALL)
|
||||
if m: print(m.group(1))
|
||||
" 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
{
|
||||
printf '%s\n' '<?xml version="1.0" encoding="utf-8"?>'
|
||||
printf '%s\n' '<updates>'
|
||||
for TAG in stable rc development; do
|
||||
[ "$TAG" = "${STABILITY}" ] && continue
|
||||
if grep -q "<tag>${TAG}</tag>" updates.xml 2>/dev/null; then
|
||||
sed -n "/<update>/,/<\/update>/{ /<tag>${TAG}<\/tag>/p; }" updates.xml
|
||||
fi
|
||||
done
|
||||
cat /tmp/new_entry.xml
|
||||
printf '\n%s\n' '</updates>'
|
||||
} > /tmp/updates_new.xml
|
||||
mv /tmp/updates_new.xml updates.xml
|
||||
[ -n "$STABLE_ENTRY" ] && echo "$STABLE_ENTRY"
|
||||
[ -n "$RC_ENTRY" ] && echo "$RC_ENTRY"
|
||||
[ -n "$DEV_ENTRY" ] && echo "$DEV_ENTRY"
|
||||
echo "$NEW_ENTRY"
|
||||
printf '%s\n' '</updates>'
|
||||
} > updates.xml
|
||||
}
|
||||
fi
|
||||
|
||||
@@ -278,64 +250,6 @@ jobs:
|
||||
git push
|
||||
}
|
||||
|
||||
- name: SFTP deploy to dev server
|
||||
if: contains(github.ref, 'dev/')
|
||||
env:
|
||||
DEV_HOST: ${{ vars.DEV_FTP_HOST }}
|
||||
DEV_PATH: ${{ vars.DEV_FTP_PATH }}
|
||||
DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
|
||||
DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
|
||||
DEV_PORT: ${{ vars.DEV_FTP_PORT }}
|
||||
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
|
||||
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
||||
run: |
|
||||
# ── Permission check: admin or maintain role required ──────
|
||||
ACTOR="${{ github.actor }}"
|
||||
REPO="${{ github.repository }}"
|
||||
PERMISSION=$(gh api "repos/${REPO}/collaborators/${ACTOR}/permission" \
|
||||
--jq '.permission' 2>/dev/null || \
|
||||
gh api "repos/${REPO}/collaborators/${ACTOR}" \
|
||||
--jq '.role' 2>/dev/null || echo "read")
|
||||
case "$PERMISSION" in
|
||||
admin|maintain|write) ;;
|
||||
*)
|
||||
echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
[ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
|
||||
|
||||
SOURCE_DIR="src"
|
||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||
[ ! -d "$SOURCE_DIR" ] && exit 0
|
||||
|
||||
PORT="${DEV_PORT:-22}"
|
||||
REMOTE="${DEV_PATH%/}"
|
||||
[ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
|
||||
|
||||
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
|
||||
"$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
|
||||
if [ -n "$DEV_KEY" ]; then
|
||||
echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
|
||||
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
|
||||
else
|
||||
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
|
||||
fi
|
||||
|
||||
PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true)
|
||||
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards/api/deploy/deploy-joomla.php" ]; then
|
||||
php /tmp/mokostandards/api/deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
||||
elif [ -f "/tmp/mokostandards/api/deploy/deploy-sftp.php" ]; then
|
||||
php /tmp/mokostandards/api/deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
||||
fi
|
||||
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
||||
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "## Joomla Update Server" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
20
.mokostandards
Normal file
20
.mokostandards
Normal file
@@ -0,0 +1,20 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: MokoStandards.Templates.Config
|
||||
# INGROUP: MokoStandards.Templates
|
||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
||||
# PATH: /templates/configs/moko-standards.yml
|
||||
# VERSION: 04.05.13
|
||||
# BRIEF: Governance attachment template — synced to .mokostandards in every governed repository
|
||||
# NOTE: Tokens replaced at sync time: mokoconsulting-tech, MokoCassiopeia, waas-component, 04.05.13
|
||||
#
|
||||
# This file is managed automatically by MokoStandards bulk sync.
|
||||
# Do not edit manually — changes will be overwritten on the next sync.
|
||||
# To update governance settings, open a PR in MokoStandards instead:
|
||||
# https://github.com/mokoconsulting-tech/MokoStandards
|
||||
|
||||
standards_source: "https://github.com/mokoconsulting-tech/MokoStandards"
|
||||
standards_version: "04.05.13"
|
||||
platform: "waas-component"
|
||||
governed_repo: "mokoconsulting-tech/MokoCassiopeia"
|
||||
@@ -1,98 +1,87 @@
|
||||
<!--
|
||||
Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
||||
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
|
||||
This file is part of a Moko Consulting project.
|
||||
This file is part of a Moko Consulting project.
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: Joomla.Template
|
||||
INGROUP: MokoCassiopeia.Governance
|
||||
REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia
|
||||
FILE: CODE_OF_CONDUCT.md
|
||||
VERSION: 03.09.03
|
||||
BRIEF: Contributor code of conduct for the MokoCassiopeia project.
|
||||
PATH: /CODE_OF_CONDUCT.md
|
||||
NOTE: This document defines behavioral expectations and enforcement processes.
|
||||
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License (./LICENSE.md).
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP:
|
||||
INGROUP: Project.Documentation
|
||||
REPO:
|
||||
VERSION: 04.05.13
|
||||
PATH: ./CODE_OF_CONDUCT.md
|
||||
BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default
|
||||
-->
|
||||
# Code of Conduct
|
||||
|
||||
## Code of Conduct
|
||||
## 1. Purpose
|
||||
|
||||
This Code of Conduct establishes expectations for behavior within the MokoCassiopeia project community. The objective is to maintain a professional, inclusive, and respectful environment aligned with open source governance best practices.
|
||||
The purpose of this Code of Conduct is to ensure a safe, inclusive, and respectful environment for all contributors and participants in Moko Consulting projects. This applies to all interactions, whether in repositories, issue trackers, documentation, meetings, or community spaces.
|
||||
|
||||
## Scope
|
||||
## 2. Our Standards
|
||||
|
||||
This Code of Conduct applies to all project spaces, including:
|
||||
Participants are expected to uphold behaviors that strengthen our community, including:
|
||||
|
||||
* GitHub repositories, issues, pull requests, discussions, and security advisories.
|
||||
* Project documentation, workflows, and release processes.
|
||||
* Any communication channels officially associated with the project.
|
||||
Demonstrating empathy and respect toward others.
|
||||
Being inclusive of diverse viewpoints and backgrounds.
|
||||
Gracefully accepting constructive feedback.
|
||||
Prioritizing collaboration over conflict.
|
||||
Showing professionalism in all interactions.
|
||||
|
||||
## Our Standards
|
||||
### Unacceptable behavior includes:
|
||||
|
||||
Participants are expected to:
|
||||
Harassment, discrimination, or derogatory comments.
|
||||
Threatening or violent language or actions.
|
||||
Disruptive, aggressive, or intentionally harmful behavior.
|
||||
Publishing others’ private information without permission.
|
||||
Any behavior that violates applicable laws.
|
||||
|
||||
* Communicate professionally and respectfully.
|
||||
* Provide constructive feedback focused on technical merit and project objectives.
|
||||
* Respect differing viewpoints, experience levels, and backgrounds.
|
||||
* Follow documented contribution, security, and governance policies.
|
||||
## 3. Responsibilities of Maintainers
|
||||
|
||||
Unacceptable behavior includes:
|
||||
Maintainers are responsible for:
|
||||
|
||||
* Harassment, discrimination, or exclusionary conduct.
|
||||
* Personal attacks, insults, or inflammatory comments.
|
||||
* Publishing private information without consent.
|
||||
* Disruptive behavior that materially interferes with project operations.
|
||||
Clarifying acceptable behavior.
|
||||
Taking appropriate corrective action when unacceptable behavior occurs.
|
||||
Removing, editing, or rejecting contributions that violate this Code.
|
||||
Temporarily or permanently banning contributors who engage in repeated or severe violations.
|
||||
|
||||
## Enforcement Responsibilities
|
||||
## 4. Scope
|
||||
|
||||
Project maintainers are responsible for:
|
||||
This Code applies to:
|
||||
|
||||
* Clarifying standards when questions arise.
|
||||
* Taking appropriate and proportionate corrective action when violations occur.
|
||||
* Maintaining confidentiality to the extent practical during investigations.
|
||||
All Moko Consulting repositories.
|
||||
All documentation and collaboration platforms.
|
||||
Public and private communication related to project activities.
|
||||
Any representation of Moko Consulting in online or offline spaces.
|
||||
|
||||
## Reporting
|
||||
## 5. Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported through:
|
||||
Instances of misconduct may be reported to:
|
||||
**[hello@mokoconsulting.tech](mailto:hello@mokoconsulting.tech)**
|
||||
|
||||
* Email: `hello@mokoconsulting.tech` with subject `CODE OF CONDUCT: MokoCassiopeia`.
|
||||
All reports will be reviewed and investigated promptly and fairly. Maintainers are obligated to maintain confidentiality where possible.
|
||||
|
||||
Reports should include relevant context, links, screenshots, or other supporting information.
|
||||
Consequences may include:
|
||||
|
||||
## Enforcement Guidelines
|
||||
A warning.
|
||||
Required training or mediation.
|
||||
Temporary or permanent bans.
|
||||
Escalation to legal authorities when required.
|
||||
|
||||
Corrective actions may include, but are not limited to:
|
||||
## 6. Acknowledgements
|
||||
|
||||
* Private warning or request for corrective action.
|
||||
* Temporary or permanent restriction from project participation.
|
||||
* Removal of content that violates this Code of Conduct.
|
||||
This Code of Conduct is inspired by widely adopted community guidelines, including the Contributor Covenant and major open-source collaboration standards.
|
||||
|
||||
Decisions are made based on impact, severity, and pattern of behavior.
|
||||
## 7. Related Documents
|
||||
|
||||
## No Retaliation
|
||||
[Governance Guide](./docs-governance.md)
|
||||
[Contributor Guide](./docs-contributing.md)
|
||||
[Documentation Index](./docs-index.md)
|
||||
|
||||
Retaliation against individuals who report concerns in good faith is not tolerated. Any retaliatory behavior will be treated as a separate violation.
|
||||
|
||||
## Jurisdiction
|
||||
|
||||
This project is managed from Tennessee, USA. This statement is informational and does not constitute legal advice.
|
||||
|
||||
---
|
||||
|
||||
## Metadata
|
||||
|
||||
* **Document:** CODE_OF_CONDUCT.md
|
||||
* **Repository:** [https://github.com/mokoconsulting-tech/MokoCassiopeia](https://github.com/mokoconsulting-tech/MokoCassiopeia)
|
||||
* **Path:** /CODE_OF_CONDUCT.md
|
||||
* **Owner:** Moko Consulting
|
||||
* **Version:** 03.06.00
|
||||
* **Status:** Active
|
||||
* **Effective Date:** 2025-12-18
|
||||
* **Last Reviewed:** 2025-12-18
|
||||
|
||||
## Revision History
|
||||
|
||||
| Date | Change Summary | Author |
|
||||
| ---------- | ----------------------------------------------------------------------------- | --------------- |
|
||||
| 2025-12-18 | Initial publication of contributor conduct standards and enforcement process. | Moko Consulting |
|
||||
This Code of Conduct is a living document and may be updated following the established Change Management process.
|
||||
|
||||
189
CONTRIBUTING.md
189
CONTRIBUTING.md
@@ -1,145 +1,128 @@
|
||||
<!--
|
||||
Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
|
||||
This file is part of a Moko Consulting project.
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License (./LICENSE).
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: Joomla.Template
|
||||
INGROUP: MokoCassiopeia.Governance
|
||||
DEFGROUP: {{DEFGROUP}}
|
||||
INGROUP: Project.Documentation
|
||||
REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia
|
||||
FILE: CONTRIBUTING.md
|
||||
VERSION: 03.09.03
|
||||
BRIEF: Contribution guidelines for the MokoCassiopeia project.
|
||||
PATH: /CONTRIBUTING.md
|
||||
NOTE: This document defines contribution workflow, standards, and governance alignment.
|
||||
-->
|
||||
VERSION: 04.05.13
|
||||
PATH: ./CONTRIBUTING.md
|
||||
BRIEF: How to contribute; branch strategy, commit conventions, PR workflow, and release pipeline
|
||||
-->
|
||||
|
||||
## Contributing
|
||||
# Contributing
|
||||
|
||||
This document defines how to contribute to the MokoCassiopeia project. The goal is to ensure changes are reviewable, auditable, and aligned with project governance and release processes.
|
||||
Thank you for your interest in contributing to **MokoCassiopeia**!
|
||||
|
||||
## Scope
|
||||
This repository is governed by **[MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards)** — the authoritative source of coding standards, workflows, and policies for all Moko Consulting repositories.
|
||||
|
||||
These guidelines apply to all contributions, including:
|
||||
## Branch Strategy
|
||||
|
||||
* Source code changes
|
||||
* Documentation updates
|
||||
* Bug reports and enhancement proposals
|
||||
| Branch | Purpose | Deploys To |
|
||||
|--------|---------|------------|
|
||||
| `main` | Bleeding edge — all development merges here | CI only |
|
||||
| `dev/XX.YY.ZZ` | Feature development | Dev server (version: "development") |
|
||||
| `version/XX` | Stable frozen snapshot | Demo + RS servers |
|
||||
|
||||
## Prerequisites
|
||||
### Development Workflow
|
||||
|
||||
Contributors are expected to:
|
||||
|
||||
* Have a working understanding of Joomla template structure.
|
||||
* Be familiar with Git and GitHub pull request workflows.
|
||||
* Review repository governance documents prior to submitting changes.
|
||||
* Set up the development environment using the provided tools.
|
||||
|
||||
### Quick Setup
|
||||
|
||||
For first-time contributors:
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/mokoconsulting-tech/MokoCassiopeia.git
|
||||
cd MokoCassiopeia
|
||||
```
|
||||
1. Create branch: git checkout -b dev/XX.YY.ZZ/my-feature
|
||||
2. Develop + test (dev server auto-deploys on push)
|
||||
3. Open PR → main (squash merge only)
|
||||
4. Auto-release (version branch + tag + GitHub Release created automatically)
|
||||
```
|
||||
|
||||
See [docs/QUICK_START.md](./docs/QUICK_START.md) for detailed setup instructions.
|
||||
### Branch Naming
|
||||
|
||||
## Development Tools
|
||||
| Prefix | Use |
|
||||
|--------|-----|
|
||||
| `dev/XX.YY.ZZ` | Feature development (e.g., `dev/02.00.00/add-extrafields`) |
|
||||
| `version/XX` | Stable release (auto-created, never manually pushed) |
|
||||
| `chore/` | Automated sync branches (managed by MokoStandards) |
|
||||
|
||||
The repository provides several tools to streamline development:
|
||||
> **Never use** `feature/`, `hotfix/`, or `release/` prefixes — they are not part of the MokoStandards branch strategy.
|
||||
|
||||
* **Pre-commit Hooks**: Automatic local validation before commits
|
||||
## Commit Conventions
|
||||
|
||||
## Contribution Workflow
|
||||
Use [conventional commits](https://www.conventionalcommits.org/):
|
||||
|
||||
1. Fork the repository.
|
||||
2. Create a branch from the active development branch.
|
||||
3. Make focused, minimal changes that address a single concern.
|
||||
4. Submit a pull request with a clear description of intent and impact.
|
||||
```
|
||||
feat(scope): add new extrafield for invoice tracking
|
||||
fix(sql): correct column type in llx_mytable
|
||||
docs(readme): update installation instructions
|
||||
chore(deps): bump enterprise library to 04.02.30
|
||||
```
|
||||
|
||||
Direct commits to protected branches are not permitted.
|
||||
**Valid types:** `feat` | `fix` | `docs` | `chore` | `ci` | `refactor` | `style` | `test` | `perf` | `revert` | `build`
|
||||
|
||||
## Branching and Versioning
|
||||
## Pull Request Workflow
|
||||
|
||||
* Development work occurs on designated development branches.
|
||||
* Releases are produced from versioned branches following repository standards.
|
||||
* Contributors should not bump version numbers unless explicitly requested.
|
||||
1. **Branch** from `main` using `dev/XX.YY.ZZ/description` format
|
||||
2. **Bump** the patch version in `README.md` before opening the PR
|
||||
3. **Title** must be a valid conventional commit subject line
|
||||
4. **Target** `main` — squash merge only (merge commits are disabled)
|
||||
5. **CI checks** must pass before merge
|
||||
|
||||
## Coding and Formatting Standards
|
||||
### What Happens on Merge
|
||||
|
||||
All contributions must:
|
||||
When your PR is merged to `main`, these workflows run automatically:
|
||||
|
||||
* Follow Joomla coding standards where applicable.
|
||||
* Conform to Moko Consulting repository standards for headers, metadata, and file structure.
|
||||
* Avoid introducing tabs, inconsistent path separators, or non portable assumptions.
|
||||
1. **sync-version-on-merge** — auto-bumps patch version, propagates to all file headers
|
||||
2. **auto-release** — creates `version/XX` branch, git tag, and GitHub Release
|
||||
3. **deploy-demo / deploy-rs** — deploys to demo and RS servers (if `src/**` changed)
|
||||
|
||||
Automated checks may reject changes that do not meet these requirements.
|
||||
## Coding Standards
|
||||
|
||||
## Documentation Standards
|
||||
All contributions must follow [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards):
|
||||
|
||||
Documentation changes must:
|
||||
| Standard | Reference |
|
||||
|----------|-----------|
|
||||
| Coding Style | [coding-style-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md) |
|
||||
| File Headers | [file-header-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/file-header-standards.md) |
|
||||
| Branching | [branch-release-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/branch-release-strategy.md) |
|
||||
| Merge Strategy | [merge-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) |
|
||||
| Scripting | [scripting-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/scripting-standards.md) |
|
||||
| Build & Release | [build-release.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/workflows/build-release.md) |
|
||||
|
||||
* Include required metadata and revision history sections.
|
||||
* Avoid embedding version numbers in revision history tables.
|
||||
* Preserve existing structure unless a structural change is explicitly proposed.
|
||||
## PR Checklist
|
||||
|
||||
## Commit Messages
|
||||
- [ ] Branch named `dev/XX.YY.ZZ/description`
|
||||
- [ ] Patch version bumped in `README.md`
|
||||
- [ ] Conventional commit format for PR title
|
||||
- [ ] All new files have FILE INFORMATION headers
|
||||
- [ ] `declare(strict_types=1)` in all PHP files
|
||||
- [ ] PHPDoc on all public methods
|
||||
- [ ] Tests pass
|
||||
- [ ] CHANGELOG.md updated
|
||||
- [ ] No secrets, tokens, or credentials committed
|
||||
|
||||
Commit messages should:
|
||||
## Custom Workflows
|
||||
|
||||
* Be concise and descriptive.
|
||||
* Focus on what changed and why.
|
||||
* Avoid referencing internal issue trackers unless required.
|
||||
Place repo-specific workflows in `.github/workflows/custom/` — they are **never overwritten or deleted** by MokoStandards sync:
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
Bug reports and enhancement requests should be filed as GitHub issues and include:
|
||||
|
||||
* Clear reproduction steps or use cases.
|
||||
* Expected versus actual behavior.
|
||||
* Relevant environment details.
|
||||
|
||||
Security related issues must follow the process defined in SECURITY.md and must not be reported publicly.
|
||||
|
||||
## Review Process
|
||||
|
||||
All pull requests are subject to review. Review criteria include:
|
||||
|
||||
* Technical correctness
|
||||
* Alignment with project goals
|
||||
* Maintainability and clarity
|
||||
* Risk introduced to release and update processes
|
||||
|
||||
Maintainers may request changes prior to approval.
|
||||
```
|
||||
.github/workflows/
|
||||
├── deploy-dev.yml ← Synced from MokoStandards
|
||||
├── auto-release.yml ← Synced from MokoStandards
|
||||
└── custom/ ← Your custom workflows (safe)
|
||||
└── my-custom-ci.yml
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under GPL-3.0-or-later, consistent with the rest of the project.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Participation in this project is governed by the Code of Conduct. Unacceptable behavior may result in contribution restrictions.
|
||||
By contributing, you agree that your contributions will be licensed under the [GPL-3.0-or-later](LICENSE) license.
|
||||
|
||||
---
|
||||
|
||||
## Metadata
|
||||
|
||||
* **Document:** CONTRIBUTING.md
|
||||
* **Repository:** [https://github.com/mokoconsulting-tech/MokoCassiopeia](https://github.com/mokoconsulting-tech/MokoCassiopeia)
|
||||
* **Path:** /CONTRIBUTING.md
|
||||
* **Owner:** Moko Consulting
|
||||
* **Version:** 03.06.00
|
||||
* **Status:** Active
|
||||
* **Effective Date:** 2025-12-18
|
||||
* **Last Reviewed:** 2025-12-18
|
||||
|
||||
## Revision History
|
||||
|
||||
| Date | Change Summary | Author |
|
||||
| ---------- | ------------------------------------------------------------------------- | --------------- |
|
||||
| 2025-12-18 | Initial publication of contribution guidelines and workflow expectations. | Moko Consulting |
|
||||
*This file is synced from [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). Do not edit directly — changes will be overwritten on the next sync.*
|
||||
|
||||
287
SECURITY.md
287
SECURITY.md
@@ -1,185 +1,240 @@
|
||||
<!--
|
||||
Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
|
||||
This file is part of a Moko Consulting project.
|
||||
This file is part of a Moko Consulting project.
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: Joomla.Template
|
||||
INGROUP: MokoCassiopeia.Governance
|
||||
REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia
|
||||
FILE: SECURITY.md
|
||||
VERSION: 03.09.03
|
||||
BRIEF: Security policy and vulnerability reporting process for MokoCassiopeia.
|
||||
PATH: /SECURITY.md
|
||||
NOTE: This policy is process oriented and does not replace secure engineering practices.
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: [PROJECT_NAME]
|
||||
INGROUP: [PROJECT_NAME].Documentation
|
||||
REPO: [REPOSITORY_URL]
|
||||
PATH: /SECURITY.md
|
||||
VERSION: 04.05.13
|
||||
BRIEF: Security vulnerability reporting and handling policy
|
||||
-->
|
||||
|
||||
## Security Policy
|
||||
# Security Policy
|
||||
|
||||
This document defines how MokoCassiopeia handles vulnerability intake, triage, remediation, and disclosure. The objective is to reduce risk, protect downstream users, and preserve operational continuity with a verifiable audit trail.
|
||||
## Purpose and Scope
|
||||
|
||||
## Scope
|
||||
|
||||
This policy applies to:
|
||||
|
||||
* Repository source code, workflows, scripts, and build artifacts.
|
||||
* Release packaging (ZIP outputs) generated from the repository.
|
||||
* Configuration and metadata used for distribution (for example manifests and update metadata).
|
||||
|
||||
Out of scope:
|
||||
|
||||
* Vulnerabilities in upstream Joomla core, third party extensions, or external infrastructure not controlled by this repository.
|
||||
* Issues that require physical access to a host, compromised administrator credentials, or a compromised hosting provider, unless the repository materially increases impact.
|
||||
This document defines the security vulnerability reporting, response, and disclosure policy for [PROJECT_NAME] and all repositories governed by these standards. It establishes the authoritative process for responsible disclosure, assessment, remediation, and communication of security issues.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Security fixes are prioritized for:
|
||||
Security updates are provided for the following versions:
|
||||
|
||||
* The latest released version.
|
||||
* The current development line when it is actively used for release engineering.
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| [X.x.x] | :white_check_mark: |
|
||||
| < [X.0] | :x: |
|
||||
|
||||
Backports may be provided based on impact, deployment footprint, and engineering capacity.
|
||||
Only the current major version receives security updates. Users should upgrade to the latest supported version to receive security patches.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Use one of the following channels:
|
||||
### Where to Report
|
||||
|
||||
* GitHub Security Advisories (preferred): use the repository security tab to submit a private report.
|
||||
* Email: send details to `hello@mokoconsulting.tech` with subject `SECURITY: MokoCassiopeia vulnerability report`.
|
||||
**DO NOT** create public GitHub issues for security vulnerabilities.
|
||||
|
||||
Do not file a public GitHub issue for suspected security vulnerabilities.
|
||||
Report security vulnerabilities privately to:
|
||||
|
||||
### What to include
|
||||
**Email**: `security@[DOMAIN]`
|
||||
|
||||
Provide enough detail to reproduce and triage:
|
||||
**Subject Line**: `[SECURITY] Brief Description`
|
||||
|
||||
* A clear description of the vulnerability and expected impact.
|
||||
* A minimal proof of concept or reproduction steps.
|
||||
* Affected versions, configuration assumptions, and environment details.
|
||||
* Any proposed mitigation or patch.
|
||||
* Your preferred contact details for follow up.
|
||||
### What to Include
|
||||
|
||||
## Triage and Response Targets
|
||||
A complete vulnerability report should include:
|
||||
|
||||
The project operates with response targets aligned to practical delivery realities:
|
||||
1. **Description**: Clear explanation of the vulnerability
|
||||
2. **Impact**: Potential security impact and severity assessment
|
||||
3. **Affected Versions**: Which versions are vulnerable
|
||||
4. **Reproduction Steps**: Detailed steps to reproduce the issue
|
||||
5. **Proof of Concept**: Code, configuration, or demonstration (if applicable)
|
||||
6. **Suggested Fix**: Proposed remediation (if known)
|
||||
7. **Disclosure Timeline**: Your expectations for public disclosure
|
||||
|
||||
* **Acknowledgement:** within 3 business days.
|
||||
* **Initial triage:** within 10 business days.
|
||||
* **Fix plan:** communicated once severity is confirmed.
|
||||
### Response Timeline
|
||||
|
||||
These targets are not guarantees. Complex issues, supply chain considerations, and coordination with upstream vendors may extend timelines.
|
||||
* **Initial Response**: Within 3 business days
|
||||
* **Assessment Complete**: Within 7 business days
|
||||
* **Fix Timeline**: Depends on severity (see below)
|
||||
* **Disclosure**: Coordinated with reporter
|
||||
|
||||
## Severity Assessment
|
||||
## Severity Classification
|
||||
|
||||
Issues are triaged based on business impact and technical exploitability, including:
|
||||
Vulnerabilities are classified using the following severity levels:
|
||||
|
||||
* Remote exploitability and required privileges.
|
||||
* Data confidentiality, integrity, and availability impact.
|
||||
* Likelihood of exploitation in typical Joomla deployments.
|
||||
* Exposure surface (public endpoints, administrator area, installation flows, and update mechanisms).
|
||||
### Critical
|
||||
* Remote code execution
|
||||
* Authentication bypass
|
||||
* Data breach or exposure of sensitive information
|
||||
* **Fix Timeline**: 7 days
|
||||
|
||||
When appropriate, industry standard scoring such as CVSS may be used for internal prioritization.
|
||||
### High
|
||||
* Privilege escalation
|
||||
* SQL injection or command injection
|
||||
* Cross-site scripting (XSS) with significant impact
|
||||
* **Fix Timeline**: 14 days
|
||||
|
||||
## Coordinated Disclosure
|
||||
### Medium
|
||||
* Information disclosure (limited scope)
|
||||
* Denial of service
|
||||
* Security misconfigurations with moderate impact
|
||||
* **Fix Timeline**: 30 days
|
||||
|
||||
The project follows coordinated vulnerability disclosure:
|
||||
### Low
|
||||
* Security best practice violations
|
||||
* Minor information leaks
|
||||
* Issues requiring user interaction or complex preconditions
|
||||
* **Fix Timeline**: 60 days or next release
|
||||
|
||||
* Reports are treated as confidential until remediation is available.
|
||||
* A public advisory may be published once a fix is released.
|
||||
* A reasonable embargo period is expected to enable patch distribution.
|
||||
## Remediation Process
|
||||
|
||||
If you believe disclosure is time sensitive due to active exploitation, include that assessment and any supporting indicators.
|
||||
1. **Acknowledgment**: Security team confirms receipt and begins investigation
|
||||
2. **Assessment**: Vulnerability is validated, severity assigned, and impact analyzed
|
||||
3. **Development**: Security patch is developed and tested
|
||||
4. **Review**: Patch undergoes security review and validation
|
||||
5. **Release**: Fixed version is released with security advisory
|
||||
6. **Disclosure**: Public disclosure follows coordinated timeline
|
||||
|
||||
## Security Updates and Advisories
|
||||
## Security Advisories
|
||||
|
||||
Security updates are distributed through:
|
||||
Security advisories are published via:
|
||||
|
||||
* GitHub releases for the repository.
|
||||
* GitHub Security Advisories when applicable.
|
||||
* GitHub Security Advisories
|
||||
* Release notes and CHANGELOG.md
|
||||
* Security mailing list (when established)
|
||||
|
||||
Advisories may include:
|
||||
Advisories include:
|
||||
|
||||
* Affected versions and fixed versions.
|
||||
* Mitigations and workarounds when a fix is not immediately available.
|
||||
* Upgrade guidance.
|
||||
* CVE identifier (if applicable)
|
||||
* Severity rating
|
||||
* Affected versions
|
||||
* Fixed versions
|
||||
* Mitigation steps
|
||||
* Attribution (with reporter consent)
|
||||
|
||||
## Dependencies and Supply Chain Controls
|
||||
## Security Best Practices
|
||||
|
||||
The project aims to manage supply chain risk through:
|
||||
For repositories adopting MokoStandards:
|
||||
|
||||
* Pinning and review of workflow dependencies where feasible.
|
||||
* Minimizing privileged GitHub token permissions.
|
||||
* Validating build inputs prior to packaging releases.
|
||||
### Required Controls
|
||||
|
||||
If you identify a supply chain issue (for example compromised action, dependency confusion, or malicious upstream artifact), report it as a vulnerability.
|
||||
* Enable GitHub security features (Dependabot, code scanning)
|
||||
* Implement branch protection on `main`
|
||||
* Require code review for all changes
|
||||
* Enforce signed commits (recommended)
|
||||
* Use secrets management (never commit credentials)
|
||||
* Maintain security documentation
|
||||
* Follow secure coding standards defined in `/docs/policy/`
|
||||
|
||||
## Secure Development and CI Expectations
|
||||
### CI/CD Security
|
||||
|
||||
Security posture is reinforced through operational controls:
|
||||
* Validate all inputs
|
||||
* Sanitize outputs
|
||||
* Use least privilege access
|
||||
* Pin dependencies with hash verification
|
||||
* Scan for vulnerabilities in dependencies
|
||||
* Audit third-party actions and tools
|
||||
|
||||
* CI validation for packaging inputs and manifest integrity.
|
||||
* Consistent path normalization and whitespace hygiene checks where required for release correctness.
|
||||
* Least privilege for GitHub Actions permissions.
|
||||
#### Automated Security Scanning
|
||||
|
||||
### Template Security Features
|
||||
All repositories MUST implement:
|
||||
|
||||
**Custom Head Content Injection**
|
||||
**CodeQL Analysis**:
|
||||
* Enabled for all supported languages (Python, JavaScript, TypeScript, Java, C/C++, C#, Go, Ruby)
|
||||
* Runs on: push to main, pull requests, weekly schedule
|
||||
* Query sets: `security-extended` and `security-and-quality`
|
||||
* Configuration: `.github/workflows/codeql-analysis.yml`
|
||||
|
||||
The template provides Custom Head Code fields (`custom_head_start` and `custom_head_end`) that allow administrators to inject custom HTML, CSS, and JavaScript code. This is an intentional feature for:
|
||||
**Dependabot Security Updates**:
|
||||
* Weekly scans for vulnerable dependencies
|
||||
* Automated pull requests for security patches
|
||||
* Configuration: `.github/dependabot.yml`
|
||||
|
||||
* Adding analytics scripts (Google Analytics, Google Tag Manager)
|
||||
* Custom meta tags
|
||||
* Third-party integrations
|
||||
* Custom styling
|
||||
**Secret Scanning**:
|
||||
* Enabled by default with push protection
|
||||
* Prevents accidental credential commits
|
||||
* Partner patterns enabled
|
||||
|
||||
**Security Considerations:**
|
||||
**Dependency Review**:
|
||||
* Required for all pull requests
|
||||
* Blocks introduction of known vulnerable dependencies
|
||||
* Automatic license compliance checking
|
||||
|
||||
* These fields use `filter="raw"` to allow HTML/JS injection
|
||||
* **Access is restricted to Joomla administrators only** via template configuration
|
||||
* This is not an XSS vulnerability as it requires administrator privileges
|
||||
* Administrators should only add trusted code from verified sources
|
||||
* Regular security audits should review custom head content
|
||||
See [Security Scanning Policy](docs/policy/security-scanning.md) for detailed requirements.
|
||||
|
||||
This policy does not guarantee that all vulnerabilities will be prevented. It defines how risk is managed when issues are discovered.
|
||||
### Dependency Management
|
||||
|
||||
## Safe Harbor
|
||||
* Keep dependencies up to date
|
||||
* Monitor security advisories for dependencies
|
||||
* Remove unused dependencies
|
||||
* Audit new dependencies before adoption
|
||||
* Document security-critical dependencies
|
||||
|
||||
The project supports good faith security research. When you:
|
||||
## Compliance and Governance
|
||||
|
||||
* Avoid privacy violations, data destruction, and service disruption.
|
||||
* Limit testing to systems you own or have explicit permission to test.
|
||||
* Provide a reasonable window for coordinated disclosure.
|
||||
This security policy is binding for all repositories governed by MokoStandards. Deviations require documented justification and approval from the Security Owner.
|
||||
|
||||
Then the project will treat your report as a constructive security contribution.
|
||||
Security policies are reviewed and updated at least annually or following significant security incidents.
|
||||
|
||||
Jurisdiction note: this repository is managed from Tennessee, USA. This note is informational only and does not constitute legal advice.
|
||||
## Attribution and Recognition
|
||||
|
||||
## Public Communications
|
||||
We acknowledge and appreciate responsible disclosure. With your permission, we will:
|
||||
|
||||
Only maintainers will publish security advisories or public statements for confirmed vulnerabilities. Public communication will focus on actionable remediation and operational risk reduction.
|
||||
* Credit you in security advisories
|
||||
* List you in CHANGELOG.md for the fix release
|
||||
* Recognize your contribution publicly (if desired)
|
||||
|
||||
## Acknowledgements
|
||||
## Contact and Escalation
|
||||
|
||||
If you want credit, include the name or handle to list in an advisory. If you prefer anonymity, state that explicitly.
|
||||
* **Security Team**: security@[DOMAIN]
|
||||
* **Primary Contact**: [CONTACT_EMAIL]
|
||||
* **Escalation**: For urgent matters requiring immediate attention, contact the maintainer directly via GitHub
|
||||
|
||||
## Out of Scope
|
||||
|
||||
The following are explicitly out of scope:
|
||||
|
||||
* Issues in third-party dependencies (report directly to maintainers)
|
||||
* Social engineering attacks
|
||||
* Physical security issues
|
||||
* Denial of service via resource exhaustion without amplification
|
||||
* Issues requiring physical access to systems
|
||||
* Theoretical vulnerabilities without proof of exploitability
|
||||
|
||||
---
|
||||
|
||||
## Metadata
|
||||
|
||||
* **Document:** SECURITY.md
|
||||
* **Repository:** [https://github.com/mokoconsulting-tech/MokoCassiopeia](https://github.com/mokoconsulting-tech/MokoCassiopeia)
|
||||
* **Path:** /SECURITY.md
|
||||
* **Owner:** Moko Consulting
|
||||
* **Version:** 03.06.00
|
||||
* **Status:** Active
|
||||
* **Effective Date:** 2025-12-18
|
||||
* **Last Reviewed:** 2025-12-18
|
||||
| Field | Value |
|
||||
| ------------ | ----------------------------------------------- |
|
||||
| Document | Security Policy |
|
||||
| Path | /SECURITY.md |
|
||||
| Repository | [REPOSITORY_URL] |
|
||||
| Owner | [OWNER_NAME] |
|
||||
| Scope | Security vulnerability handling |
|
||||
| Applies To | All repositories governed by MokoStandards |
|
||||
| Status | Active |
|
||||
| Effective | [YYYY-MM-DD] |
|
||||
|
||||
## Revision History
|
||||
|
||||
| Date | Change Summary | Author |
|
||||
| ---------- | ------------------------------------------------------------------------------------------------ | --------------- |
|
||||
| 2026-01-30 | Added Template Security Features section documenting custom head content injection controls. | Copilot Agent |
|
||||
| 2025-12-18 | Initial publication of security policy, intake channels, triage targets, and disclosure process. | Moko Consulting |
|
||||
| Date | Change Description | Author |
|
||||
| ---------- | ------------------------------------------------- | --------------- |
|
||||
| [YYYY-MM-DD] | Initial creation | [AUTHOR_NAME] |
|
||||
|
||||
119
docs/update-server.md
Normal file
119
docs/update-server.md
Normal file
@@ -0,0 +1,119 @@
|
||||
<!--
|
||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
|
||||
This file is part of a Moko Consulting project.
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: MokoCassiopeia.Documentation
|
||||
INGROUP: MokoStandards.Templates
|
||||
REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia
|
||||
PATH: /docs/update-server.md
|
||||
VERSION: 04.05.13
|
||||
BRIEF: How this extension's Joomla update server file (updates.xml) is managed
|
||||
-->
|
||||
|
||||
# Joomla Update Server
|
||||
|
||||
[](https://github.com/mokoconsulting-tech/MokoStandards)
|
||||
|
||||
This document explains how `updates.xml` is automatically managed for this Joomla extension following the [Joomla Update Server specification](https://docs.joomla.org/Deploying_an_Update_Server).
|
||||
|
||||
## How It Works
|
||||
|
||||
Joomla checks for extension updates by fetching an XML file from the URL defined in the `<updateservers>` tag in the extension's XML manifest. MokoStandards generates this file automatically.
|
||||
|
||||
### Automatic Generation
|
||||
|
||||
| Event | Workflow | `<tag>` | `<version>` |
|
||||
|-------|----------|---------|-------------|
|
||||
| Merge to `main` | `auto-release.yml` | `stable` | `XX.YY.ZZ` |
|
||||
| Push to `dev/**` | `deploy-dev.yml` | `development` | `development` |
|
||||
| Push to `rc/**` | `deploy-dev.yml` | `rc` | `XX.YY.ZZ-rc` |
|
||||
|
||||
### Generated XML Structure
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<updates>
|
||||
<update>
|
||||
<name>Extension Name</name>
|
||||
<description>Extension Name update</description>
|
||||
<element>com_extensionname</element>
|
||||
<type>component</type>
|
||||
<version>01.02.03</version>
|
||||
<client>site</client>
|
||||
<folder>system</folder> <!-- plugins only -->
|
||||
<tags>
|
||||
<tag>stable</tag>
|
||||
</tags>
|
||||
<infourl title="Extension Name">https://github.com/.../releases/tag/v01.02.03</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="zip">https://github.com/.../releases/download/v01.02.03/com_ext-01.02.03.zip</downloadurl>
|
||||
</downloads>
|
||||
<targetplatform name="joomla" version="((5\.[0-9])|(6\.[0-9]))" />
|
||||
<php_minimum>8.2</php_minimum> <!-- if present in manifest -->
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||
</update>
|
||||
</updates>
|
||||
```
|
||||
|
||||
### Metadata Source
|
||||
|
||||
All metadata is extracted from the extension's XML manifest (`src/*.xml`) at build time:
|
||||
|
||||
| XML Element | Source | Notes |
|
||||
|-------------|--------|-------|
|
||||
| `<name>` | `<name>` in manifest | Extension display name |
|
||||
| `<element>` | `<element>` in manifest | Must match installed extension identifier |
|
||||
| `<type>` | `type` attribute on `<extension>` | `component`, `module`, `plugin`, `library`, `package`, `template` |
|
||||
| `<client>` | `client` attribute on `<extension>` | `site` or `administrator` — **required for plugins and modules** |
|
||||
| `<folder>` | `group` attribute on `<extension>` | Plugin group (e.g., `system`, `content`) — **required for plugins** |
|
||||
| `<targetplatform>` | `<targetplatform>` in manifest | Falls back to Joomla 5.x / 6.x if not specified |
|
||||
| `<php_minimum>` | `<php_minimum>` in manifest | Included only if present |
|
||||
|
||||
### Extension Manifest Setup
|
||||
|
||||
Your XML manifest must include an `<updateservers>` tag pointing to the `updates.xml` on the `main` branch:
|
||||
|
||||
```xml
|
||||
<extension type="component" client="site" method="upgrade">
|
||||
<name>My Extension</name>
|
||||
<element>com_myextension</element>
|
||||
<!-- ... -->
|
||||
<updateservers>
|
||||
<server type="extension" name="My Extension Updates">
|
||||
https://raw.githubusercontent.com/mokoconsulting-tech/MokoCassiopeia/main/updates.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
</extension>
|
||||
```
|
||||
|
||||
### Branch Lifecycle
|
||||
|
||||
```
|
||||
dev/XX.YY.ZZ → rc/XX.YY.ZZ → main → version/XX
|
||||
(development) (rc) (stable) (frozen snapshot)
|
||||
```
|
||||
|
||||
1. **Development** (`dev/**`): `updates.xml` with `<tag>development</tag>`, download points to branch archive
|
||||
2. **Release Candidate** (`rc/**`): `updates.xml` with `<tag>rc</tag>`, version set to `XX.YY.ZZ-rc`
|
||||
3. **Stable Release** (merge to `main`): `updates.xml` with `<tag>stable</tag>`, download points to GitHub Release asset
|
||||
4. **Frozen Snapshot** (`version/XX`): immutable, never force-pushed
|
||||
|
||||
### Health Checks
|
||||
|
||||
The `repo_health.yml` workflow verifies on every commit:
|
||||
|
||||
- `updates.xml` exists in the repository root
|
||||
- XML manifest exists with `<extension>` tag
|
||||
- `<version>`, `<name>`, `<author>`, `<namespace>` tags present
|
||||
- Extension `type` attribute is valid
|
||||
- Language `.ini` files exist
|
||||
- `index.html` directory listing protection in `src/`, `src/admin/`, `src/site/`
|
||||
|
||||
---
|
||||
|
||||
*Managed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). See [docs/workflows/update-server.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/workflows/update-server.md) for the full specification.*
|
||||
34
update.xml
Normal file
34
update.xml
Normal file
@@ -0,0 +1,34 @@
|
||||
<!--
|
||||
Joomla Extension Update Server XML
|
||||
See: https://docs.joomla.org/Deploying_an_Update_Server
|
||||
|
||||
This file is the update server manifest for {{EXTENSION_NAME}}.
|
||||
The Joomla installer polls this URL to check for new versions.
|
||||
|
||||
The manifest.xml in this repository must reference this file:
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
|
||||
https://github.com/mokoconsulting-tech/MokoCassiopeia/raw/main/update.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
|
||||
When a new release is made, run `make release` or the release workflow to
|
||||
prepend a new <update> entry to this file automatically.
|
||||
-->
|
||||
<updates>
|
||||
<update>
|
||||
<name>{{EXTENSION_NAME}}</name>
|
||||
<description>MokoCassiopeia — Moko Consulting Joomla extension</description>
|
||||
<element>{{EXTENSION_ELEMENT}}</element>
|
||||
<type>{{EXTENSION_TYPE}}</type>
|
||||
<version>{{VERSION}}</version>
|
||||
<infourl title="Release Information">https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/{{VERSION}}</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="zip">{{DOWNLOAD_URL}}</downloadurl>
|
||||
</downloads>
|
||||
<targetplatform name="joomla" version="4\.[0-9]+" />
|
||||
<php_minimum>7.4</php_minimum>
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
<maintainerurl>{{MAINTAINER_URL}}</maintainerurl>
|
||||
</update>
|
||||
</updates>
|
||||
Reference in New Issue
Block a user