Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions bin/tiger
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,58 @@ switch ($cmd) {
}
break;

case 'update:auto':
// Manage unattended auto-updates (the WordPress "enable auto-updates" opt-in).
// update:auto on --all [--modules-only] enroll core + every module & skill (skip core)
// update:auto on <slug> enroll one slug (`tiger-core` for the platform)
// update:auto off --all | off <slug> un-enroll everything / one slug
// update:auto list show what's enrolled
tiger_db();
$sub = isset($argv[2]) ? $argv[2] : '';
$all = in_array('--all', $argv, true);
$modulesOnly = in_array('--modules-only', $argv, true);
$slug = (isset($argv[3]) && strlen($argv[3]) && $argv[3][0] !== '-') ? $argv[3] : '';
if ($sub === 'list') {
$on = Tiger_Update_AutoUpdate::onSlugs();
echo $on ? ' ' . implode("\n ", $on) . "\n" : " (nothing enrolled)\n";
break;
}
if ($sub === 'on') {
if ($all) {
$set = Tiger_Update_AutoUpdate::enableAll(!$modulesOnly);
echo " \xE2\x9C\x93 auto-update ON for " . count($set) . ' item(s): ' . implode(', ', $set) . "\n";
} elseif ($slug !== '') {
Tiger_Update_AutoUpdate::set($slug, true);
echo " \xE2\x9C\x93 auto-update ON for {$slug}\n";
} else {
fwrite(STDERR, " usage: update:auto on <slug> | update:auto on --all [--modules-only]\n"); exit(1);
}
break;
}
if ($sub === 'off') {
if ($all) { Tiger_Update_AutoUpdate::disableAll(); echo " \xE2\x9C\x93 auto-update OFF for all\n"; }
elseif ($slug !== '') { Tiger_Update_AutoUpdate::set($slug, false); echo " \xE2\x9C\x93 auto-update OFF for {$slug}\n"; }
else { fwrite(STDERR, " usage: update:auto off <slug> | update:auto off --all\n"); exit(1); }
break;
}
fwrite(STDERR, " usage: update:auto <on|off|list> [<slug>|--all] [--modules-only]\n"); exit(1);

case 'update:apply':
// Apply the enrolled auto-update set NOW (the immediate pass firstboot/the installer runs so a
// just-provisioned box is current from minute one). Same engine as the daily scheduled job.
tiger_db();
if (!in_array('--auto', $argv, true)) {
fwrite(STDERR, " usage: update:apply --auto (apply the enrolled auto-update set now)\n"); exit(1);
}
$results = System_Service_Updates::runScheduledAutoUpdates();
if (!$results) { $log(' nothing to apply (nothing enrolled, or everything current)'); break; }
foreach ($results as $res) {
$log(sprintf(' [%s] %s%s', !empty($res['ok']) ? 'ok' : 'ERR',
(string) ($res['name'] ?? ($res['slug'] ?? '?')),
!empty($res['version']) ? ' -> ' . $res['version'] : ''));
}
break;

case 'version':
echo 'Tiger Core ' . Tiger_Version::VERSION . "\n";
break;
Expand All @@ -431,6 +483,9 @@ switch ($cmd) {
echo " user:password Reset a user's password (console recovery when email can't\n";
echo " be delivered) — [--user=<email|username>] [--password=] [--force]\n";
echo " make:module <name> Scaffold a new application module (controller + /api service + acl)\n";
echo " update:auto <on|off|list> Manage unattended auto-updates — `on --all` enrolls core +\n";
echo " every installed module & skill ([--modules-only] skips core)\n";
echo " update:apply --auto Apply the enrolled auto-update set now (the immediate pass)\n";
echo " version Show the Tiger Core version\n";
break;
}
43 changes: 43 additions & 0 deletions library/Tiger/Update/AutoUpdate.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,47 @@ public static function set(string $slug, bool $on): bool
(new Tiger_Model_Option())->setJson(Tiger_Model_Option::SCOPE_GLOBAL, '', self::OPTION_KEY, array_values($slugs));
return $on;
}

/**
* Turn auto-update ON for the platform (`tiger-core`) and every installed module and skill — the
* "keep this install current" switch a fresh provision flips so the box both arrives current (an
* immediate pass via {@see System_Service_Updates::runScheduledAutoUpdates}) and STAYS current
* through the daily `system.update_check` job. Network-free: it enrolls what is installed, it does
* not check for updates. Idempotent, merges with anything already enrolled, writes the option once.
*
* @param bool $includeCore enroll `tiger-core` too — pass false on a control plane whose core is
* deploy-driven (a booting member must never swap the shared-RDS schema)
* @return string[] the full opt-in slug set after the call
*/
public static function enableAll(bool $includeCore = true): array
{
$slugs = [];
if ($includeCore) { $slugs[] = 'tiger-core'; }
try {
foreach (array_keys((new Tiger_Model_Module())->bySlugMap()) as $slug) {
$slugs[] = (string) $slug;
}
} catch (Throwable $e) { /* no module table yet — core-only is still a valid enrollment */ }
if (class_exists('Tiger_Agent_Skills')) {
try {
foreach (Tiger_Agent_Skills::installed() as $s) {
$slugs[] = (string) ($s['key'] ?? '');
}
} catch (Throwable $e) { /* no skills store on this core — fine */ }
}
$merged = array_values(array_unique(array_filter(array_merge(self::onSlugs(), $slugs), 'strlen')));
(new Tiger_Model_Option())->setJson(Tiger_Model_Option::SCOPE_GLOBAL, '', self::OPTION_KEY, $merged);
return $merged;
}

/**
* Turn auto-update OFF for every currently enrolled slug (the inverse of {@see enableAll}).
*
* @return string[] the now-empty opt-in set
*/
public static function disableAll(): array
{
(new Tiger_Model_Option())->setJson(Tiger_Model_Option::SCOPE_GLOBAL, '', self::OPTION_KEY, []);
return [];
}
}
2 changes: 1 addition & 1 deletion library/Tiger/Version.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
class Tiger_Version
{
/** Current Tiger Core version. Keep in lockstep with the git tag cut for a release. */
const VERSION = '1.23.1';
const VERSION = '1.24.0';
}
34 changes: 34 additions & 0 deletions tests/Integration/Update/AutoUpdateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,38 @@ public function ignoresABlankSlug(): void
$this->assertFalse(Tiger_Update_AutoUpdate::set('', true));
$this->assertFalse(Tiger_Update_AutoUpdate::isOn(''));
}

#[Test]
public function enableAllEnrollsCoreAndMergesExisting(): void
{
Tiger_Update_AutoUpdate::set('panel', true); // a prior opt-in must survive
$set = Tiger_Update_AutoUpdate::enableAll();
$this->assertContains('tiger-core', $set, 'the platform is enrolled');
$this->assertTrue(Tiger_Update_AutoUpdate::isOn('tiger-core'));
$this->assertContains('panel', $set, 'enableAll merges with what was already on');

// Idempotent — a second call keeps a single tiger-core entry.
$again = Tiger_Update_AutoUpdate::enableAll();
$this->assertSame(1, count(array_keys($again, 'tiger-core', true)), 'no duplicate core on re-run');
}

#[Test]
public function enableAllCanSkipCore(): void
{
Tiger_Update_AutoUpdate::disableAll();
$set = Tiger_Update_AutoUpdate::enableAll(false); // --modules-only (control plane: core is deploy-driven)
$this->assertNotContains('tiger-core', $set);
$this->assertFalse(Tiger_Update_AutoUpdate::isOn('tiger-core'));
}

#[Test]
public function disableAllClearsEverything(): void
{
Tiger_Update_AutoUpdate::set('panel', true);
Tiger_Update_AutoUpdate::enableAll();
$this->assertNotSame([], Tiger_Update_AutoUpdate::onSlugs());
$this->assertSame([], Tiger_Update_AutoUpdate::disableAll());
$this->assertSame([], Tiger_Update_AutoUpdate::onSlugs());
$this->assertFalse(Tiger_Update_AutoUpdate::isOn('tiger-core'));
}
}
Loading