1d87be7d5e
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
- Update REPO: from MokoStandards-API to moko-platform in 125 files - Fix wrong org path (mokoconsulting-tech → MokoConsulting) in 10 files - Fix SPDX-LICENSE-IDENTIFIER case in 2 template files - Add missing REPO: field to 3 files Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
351 lines
13 KiB
PHP
351 lines
13 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: MokoStandards.Release
|
||
* INGROUP: MokoStandards.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\{ApiClient, AuditLogger, CLIApp, Config};
|
||
|
||
class GenerateDolibarrVersionTxt extends CLIApp
|
||
{
|
||
public const VERSION = '04.06.00';
|
||
|
||
private ?ApiClient $api = null;
|
||
private AuditLogger $logger;
|
||
|
||
protected function setupArguments(): array
|
||
{
|
||
return [
|
||
'repo:' => 'GitHub repo (org/repo) for remote mode.',
|
||
'tag:' => 'Git tag for this release, e.g. v1.2.0 (required)',
|
||
'output:' => 'Local output path for version.txt (default: ./version.txt)',
|
||
'makefile:' => 'Local path to Makefile (default: ./Makefile)',
|
||
'inject-urlversion'=> 'Inject / refresh $this->url_last_version in the Dolibarr module descriptor class.',
|
||
];
|
||
}
|
||
|
||
protected function run(): int
|
||
{
|
||
$this->log('📦 Dolibarr version.txt — release updater v' . self::VERSION, 'INFO');
|
||
$this->logger = new AuditLogger('dolibarr_version_txt');
|
||
|
||
$tag = $this->getOption('tag', '');
|
||
if (empty($tag)) {
|
||
$this->log('❌ --tag is required (e.g. --tag=v1.2.0)', 'ERROR');
|
||
return 1;
|
||
}
|
||
|
||
$version = $this->sanitizeVersion(ltrim($tag, 'vV'));
|
||
if (strlen($version) === 0 || strlen($version) > 29) {
|
||
$this->log("❌ Sanitized version '{$version}' is empty or > 29 characters", 'ERROR');
|
||
return 1;
|
||
}
|
||
|
||
$repoArg = $this->getOption('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('❌ --repo must be in org/repo format', 'ERROR');
|
||
return 1;
|
||
}
|
||
[$org, $repo] = explode('/', $repoArg, 2);
|
||
|
||
$repoData = $this->api->get("/repos/{$org}/{$repo}");
|
||
$defaultBranch = $repoData['default_branch'] ?? 'main';
|
||
|
||
if ($this->dryRun) {
|
||
$this->log("(dry-run) version.txt content: {$version}", 'INFO');
|
||
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("ℹ️ Updating existing version.txt", 'INFO');
|
||
} else {
|
||
$this->log("ℹ️ Creating new version.txt", 'INFO');
|
||
}
|
||
|
||
try {
|
||
$this->api->put("/repos/{$org}/{$repo}/contents/version.txt", $payload);
|
||
$this->log("✅ version.txt committed to {$defaultBranch} → {$version}", 'INFO');
|
||
} catch (\Exception $e) {
|
||
$this->log('❌ Failed to commit version.txt: ' . $e->getMessage(), 'ERROR');
|
||
return 1;
|
||
}
|
||
|
||
if ($this->hasOption('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->getOption('output', './version.txt');
|
||
|
||
if ($this->dryRun) {
|
||
$this->log("(dry-run) version.txt content: {$version}", 'INFO');
|
||
return 0;
|
||
}
|
||
|
||
$action = file_exists($outputPath) ? 'Updated' : 'Created';
|
||
if (file_put_contents($outputPath, $version) === false) {
|
||
$this->log("❌ Failed to write: {$outputPath}", 'ERROR');
|
||
return 1;
|
||
}
|
||
$this->log("✅ {$action} {$outputPath} → {$version}", 'INFO');
|
||
|
||
if ($this->hasOption('inject-urlversion')) {
|
||
$ghRepo = getenv('GITHUB_REPOSITORY') ?: '';
|
||
if (!str_contains($ghRepo, '/')) {
|
||
$this->log('⚠️ GITHUB_REPOSITORY not set — cannot inject url_last_version', 'WARN');
|
||
} 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).
|
||
* These follow the naming pattern: mod<Name>.class.php and live under
|
||
* htdocs/<module>/core/modules/ or core/modules/ at the repo root.
|
||
*
|
||
* @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(" ✅ url_last_version → {$path}", 'INFO');
|
||
} else {
|
||
$this->log(" ℹ️ url_last_version already correct in {$path}", 'INFO');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Inject or update $this->url_last_version in a Dolibarr module descriptor.
|
||
* - If a line with url_last_version exists (commented or uncommented), replace it.
|
||
* - Otherwise, insert after the $this->version assignment.
|
||
*
|
||
* 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(" ℹ️ url_last_version already correct in {$path}", 'INFO');
|
||
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(" ✅ url_last_version → {$path}", 'INFO');
|
||
}
|
||
} catch (\Exception $e) {
|
||
$this->log('⚠️ url_last_version injection failed: ' . $e->getMessage(), 'WARN');
|
||
}
|
||
}
|
||
|
||
// -------------------------------------------------------------------------
|
||
// Helpers
|
||
// -------------------------------------------------------------------------
|
||
|
||
/**
|
||
* Strip any characters that Dolibarr would strip from the version string.
|
||
* DolibarrModules::checkForUpdate() applies: /[^a-zA-Z0-9_\.\-]+/
|
||
*/
|
||
private function sanitizeVersion(string $version): string
|
||
{
|
||
return preg_replace('/[^a-zA-Z0-9_.\-]+/', '', $version) ?? '';
|
||
}
|
||
|
||
private function initApi(): bool
|
||
{
|
||
$config = Config::load();
|
||
try {
|
||
$this->adapter = \MokoEnterprise\PlatformAdapterFactory::create($config);
|
||
$this->api = $this->adapter->getApiClient();
|
||
return true;
|
||
} catch (\Exception $e) {
|
||
$this->log('❌ API init failed: ' . $e->getMessage(), 'ERROR');
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Execute if run directly
|
||
if (php_sapi_name() === 'cli' && isset($argv[0]) && realpath($argv[0]) === __FILE__) {
|
||
$app = new GenerateDolibarrVersionTxt(
|
||
'generate-dolibarr-version-txt',
|
||
'Create or update version.txt for Dolibarr module update checks',
|
||
GenerateDolibarrVersionTxt::VERSION
|
||
);
|
||
exit($app->execute());
|
||
}
|