Public Access
11eb1e2649
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Site Health (pull_request) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Generic: Repo Health / Access control (pull_request) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Generic: Repo Health / Release configuration (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
339 lines
12 KiB
PHP
339 lines
12 KiB
PHP
#!/usr/bin/env php
|
|
<?php
|
|
/**
|
|
* 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: MokoPlatform.Release
|
|
* INGROUP: MokoPlatform.Scripts
|
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
* PATH: /release/generate_dolibarr_version_txt.php
|
|
* BRIEF: Create or update version.txt on Dolibarr module release
|
|
*
|
|
* Dolibarr Update Server helper. On each release it:
|
|
* 1. Creates or overwrites version.txt at the repo root with just the
|
|
* bare version string (e.g. "1.4.2") — no whitespace, under 30 chars.
|
|
* 2. Optionally injects / refreshes the $this->url_last_version property
|
|
* in the Dolibarr module descriptor class (--inject-urlversion).
|
|
*
|
|
* Dolibarr's DolibarrModules::checkForUpdate() fetches the URL stored in
|
|
* $this->url_last_version and compares the plain-text response to the
|
|
* installed version. The response MUST be:
|
|
* - Under 30 characters total
|
|
* - Only alphanumeric chars, dots, dashes, underscores (others stripped)
|
|
* - No newline or trailing whitespace
|
|
*
|
|
* Runs in two modes:
|
|
* Local — reads/writes version.txt on disk (GitHub Actions release workflow).
|
|
* Remote — reads/commits version.txt via GitHub API.
|
|
*
|
|
* Usage (local):
|
|
* php generate_dolibarr_version_txt.php \
|
|
* --tag=v1.2.0 \
|
|
* [--output=./version.txt] \
|
|
* [--inject-urlversion]
|
|
*
|
|
* Usage (remote):
|
|
* php generate_dolibarr_version_txt.php \
|
|
* --repo=mokoconsulting-tech/MyModule \
|
|
* --tag=v1.2.0 \
|
|
* [--inject-urlversion]
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/../vendor/autoload.php';
|
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
|
|
|
use MokoEnterprise\CliFramework;
|
|
use MokoEnterprise\{ApiClient, AuditLogger, Config};
|
|
|
|
class GenerateDolibarrVersionTxtCli extends CliFramework
|
|
{
|
|
public const VERSION = '09.23.00';
|
|
|
|
private ?ApiClient $api = null;
|
|
private AuditLogger $logger;
|
|
|
|
protected function configure(): void
|
|
{
|
|
$this->setDescription('Create or update version.txt for Dolibarr module update checks');
|
|
$this->addArgument('--repo', 'GitHub repo (org/repo) for remote mode', '');
|
|
$this->addArgument('--tag', 'Git tag for this release, e.g. v1.2.0 (required)', '');
|
|
$this->addArgument('--output', 'Local output path for version.txt (default: ./version.txt)', './version.txt');
|
|
$this->addArgument('--makefile', 'Local path to Makefile (default: ./Makefile)', './Makefile');
|
|
$this->addArgument('--inject-urlversion', 'Inject / refresh $this->url_last_version in the Dolibarr module descriptor class', false);
|
|
}
|
|
|
|
protected function run(): int
|
|
{
|
|
$this->log('INFO', 'Dolibarr version.txt — release updater v' . self::VERSION);
|
|
$this->logger = new AuditLogger('dolibarr_version_txt');
|
|
|
|
$tag = $this->getArgument('--tag');
|
|
if (empty($tag)) {
|
|
$this->log('ERROR', '--tag is required (e.g. --tag=v1.2.0)');
|
|
return 1;
|
|
}
|
|
|
|
$version = $this->sanitizeVersion(ltrim($tag, 'vV'));
|
|
if (strlen($version) === 0 || strlen($version) > 29) {
|
|
$this->log('ERROR', "Sanitized version '{$version}' is empty or > 29 characters");
|
|
return 1;
|
|
}
|
|
|
|
$repoArg = $this->getArgument('--repo');
|
|
return !empty($repoArg)
|
|
? $this->runRemote($repoArg, $tag, $version)
|
|
: $this->runLocal($tag, $version);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Remote mode
|
|
// -------------------------------------------------------------------------
|
|
|
|
private function runRemote(string $repoArg, string $tag, string $version): int
|
|
{
|
|
if (!$this->initApi()) {
|
|
return 1;
|
|
}
|
|
|
|
if (!str_contains($repoArg, '/')) {
|
|
$this->log('ERROR', '--repo must be in org/repo format');
|
|
return 1;
|
|
}
|
|
[$org, $repo] = explode('/', $repoArg, 2);
|
|
|
|
$repoData = $this->api->get("/repos/{$org}/{$repo}");
|
|
$defaultBranch = $repoData['default_branch'] ?? 'main';
|
|
|
|
if ($this->dryRun) {
|
|
$this->log('INFO', "(dry-run) version.txt content: {$version}");
|
|
return 0;
|
|
}
|
|
|
|
$sha = $this->fetchRemoteFileSha($org, $repo, 'version.txt');
|
|
$payload = [
|
|
'message' => "chore(release): update version.txt to {$version} for {$tag}",
|
|
'content' => base64_encode($version),
|
|
'branch' => $defaultBranch,
|
|
];
|
|
if ($sha !== null) {
|
|
$payload['sha'] = $sha;
|
|
$this->log('INFO', 'Updating existing version.txt');
|
|
} else {
|
|
$this->log('INFO', 'Creating new version.txt');
|
|
}
|
|
|
|
try {
|
|
$this->api->put("/repos/{$org}/{$repo}/contents/version.txt", $payload);
|
|
$this->log('INFO', "version.txt committed to {$defaultBranch} -> {$version}");
|
|
} catch (\Exception $e) {
|
|
$this->log('ERROR', 'Failed to commit version.txt: ' . $e->getMessage());
|
|
return 1;
|
|
}
|
|
|
|
if ($this->getArgument('--inject-urlversion')) {
|
|
$rawUrl = "https://raw.githubusercontent.com/{$org}/{$repo}/{$defaultBranch}/version.txt";
|
|
$this->injectUrlVersionRemote($org, $repo, $defaultBranch, $rawUrl);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Local mode
|
|
// -------------------------------------------------------------------------
|
|
|
|
private function runLocal(string $tag, string $version): int
|
|
{
|
|
$outputPath = $this->getArgument('--output');
|
|
|
|
if ($this->dryRun) {
|
|
$this->log('INFO', "(dry-run) version.txt content: {$version}");
|
|
return 0;
|
|
}
|
|
|
|
$action = file_exists($outputPath) ? 'Updated' : 'Created';
|
|
if (file_put_contents($outputPath, $version) === false) {
|
|
$this->log('ERROR', "Failed to write: {$outputPath}");
|
|
return 1;
|
|
}
|
|
$this->log('INFO', "{$action} {$outputPath} -> {$version}");
|
|
|
|
if ($this->getArgument('--inject-urlversion')) {
|
|
$ghRepo = getenv('GITHUB_REPOSITORY') ?: '';
|
|
if (!str_contains($ghRepo, '/')) {
|
|
$this->warning('GITHUB_REPOSITORY not set — cannot inject url_last_version');
|
|
} else {
|
|
[$org, $repo] = explode('/', $ghRepo, 2);
|
|
$rawUrl = "https://raw.githubusercontent.com/{$org}/{$repo}/main/version.txt";
|
|
foreach ($this->findModuleDescriptors('.') as $path) {
|
|
$this->injectUrlVersionIntoFile($path, $rawUrl);
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// url_last_version injection into module descriptor class
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Find all Dolibarr module descriptor files (modXxx.class.php).
|
|
*
|
|
* @return string[]
|
|
*/
|
|
private function findModuleDescriptors(string $dir): array
|
|
{
|
|
$found = [];
|
|
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir));
|
|
foreach ($iterator as $file) {
|
|
if (!$file->isFile()) {
|
|
continue;
|
|
}
|
|
$basename = $file->getBasename();
|
|
if (preg_match('/^mod[A-Z][^.]+\.class\.php$/', $basename)) {
|
|
$content = file_get_contents((string) $file) ?: '';
|
|
// Must extend DolibarrModules to be a real module descriptor
|
|
if (str_contains($content, 'DolibarrModules')) {
|
|
$found[] = (string) $file;
|
|
}
|
|
}
|
|
}
|
|
return $found;
|
|
}
|
|
|
|
private function injectUrlVersionIntoFile(string $path, string $rawUrl): void
|
|
{
|
|
$content = file_get_contents($path) ?: '';
|
|
$updated = $this->injectUrlVersionIntoPhp($content, $rawUrl);
|
|
if ($updated !== null) {
|
|
file_put_contents($path, $updated);
|
|
$this->log('INFO', " url_last_version -> {$path}");
|
|
} else {
|
|
$this->log('INFO', " url_last_version already correct in {$path}");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Inject or update $this->url_last_version in a Dolibarr module descriptor.
|
|
*
|
|
* Returns null if no change is needed.
|
|
*/
|
|
private function injectUrlVersionIntoPhp(string $content, string $rawUrl): ?string
|
|
{
|
|
$newLine = "\t\t\$this->url_last_version = '{$rawUrl}';";
|
|
|
|
// Replace any existing url_last_version line (commented or not)
|
|
if (preg_match('/^[ \t]*\/?\/?[ \t]*\$this->url_last_version\s*=.+$/m', $content)) {
|
|
$updated = preg_replace(
|
|
'/^[ \t]*\/?\/?[ \t]*\$this->url_last_version\s*=.+$/m',
|
|
$newLine,
|
|
$content
|
|
);
|
|
return ($updated !== $content) ? $updated : null;
|
|
}
|
|
|
|
// Insert after $this->version assignment
|
|
$updated = preg_replace(
|
|
'/^([ \t]*\$this->version\s*=.+)$/m',
|
|
"$1\n{$newLine}",
|
|
$content,
|
|
1
|
|
);
|
|
return ($updated !== null && $updated !== $content) ? $updated : null;
|
|
}
|
|
|
|
private function injectUrlVersionRemote(string $org, string $repo, string $branch, string $rawUrl): void
|
|
{
|
|
try {
|
|
$tree = $this->api->get("/repos/{$org}/{$repo}/git/trees/{$branch}", ['recursive' => '1']);
|
|
foreach ($tree['tree'] ?? [] as $node) {
|
|
if ($node['type'] !== 'blob') {
|
|
continue;
|
|
}
|
|
$path = $node['path'] ?? '';
|
|
if (!preg_match('/mod[A-Z][^\/]+\.class\.php$/', $path)) {
|
|
continue;
|
|
}
|
|
$content = $this->fetchRemoteFile($org, $repo, $path);
|
|
if ($content === null || !str_contains($content, 'DolibarrModules')) {
|
|
continue;
|
|
}
|
|
$updated = $this->injectUrlVersionIntoPhp($content, $rawUrl);
|
|
if ($updated === null) {
|
|
$this->log('INFO', " url_last_version already correct in {$path}");
|
|
continue;
|
|
}
|
|
$sha = $this->fetchRemoteFileSha($org, $repo, $path);
|
|
$payload = [
|
|
'message' => 'chore(release): inject url_last_version in module descriptor',
|
|
'content' => base64_encode($updated),
|
|
'branch' => $branch,
|
|
];
|
|
if ($sha !== null) {
|
|
$payload['sha'] = $sha;
|
|
}
|
|
$this->api->put("/repos/{$org}/{$repo}/contents/{$path}", $payload);
|
|
$this->log('INFO', " url_last_version -> {$path}");
|
|
}
|
|
} catch (\Exception $e) {
|
|
$this->warning('url_last_version injection failed: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Helpers
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Strip any characters that Dolibarr would strip from the version string.
|
|
*/
|
|
private function sanitizeVersion(string $version): string
|
|
{
|
|
return preg_replace('/[^a-zA-Z0-9_.\-]+/', '', $version) ?? '';
|
|
}
|
|
|
|
private function initApi(): bool
|
|
{
|
|
$config = Config::load();
|
|
try {
|
|
$adapter = \MokoEnterprise\PlatformAdapterFactory::create($config);
|
|
$this->api = $adapter->getApiClient();
|
|
return true;
|
|
} catch (\Exception $e) {
|
|
$this->log('ERROR', 'API init failed: ' . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private function fetchRemoteFile(string $org, string $repo, string $path): ?string
|
|
{
|
|
try {
|
|
$r = $this->api->get("/repos/{$org}/{$repo}/contents/{$path}");
|
|
return base64_decode(str_replace(["\n", "\r"], '', $r['content'] ?? '')) ?: null;
|
|
} catch (\Exception $e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private function fetchRemoteFileSha(string $org, string $repo, string $path): ?string
|
|
{
|
|
try {
|
|
return $this->api->get("/repos/{$org}/{$repo}/contents/{$path}")['sha'] ?? null;
|
|
} catch (\Exception $e) {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
$app = new GenerateDolibarrVersionTxtCli();
|
|
exit($app->execute());
|