From a4a1c20301e893c245c69052215e8f72e7390ea1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 6 Nov 2026 10:08:38 +0000 Subject: [PATCH 1/4] docs: sync icon-sets catalog --- icon-sets.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/icon-sets.json b/icon-sets.json index 0ad65f8..d1a3841 100644 --- a/icon-sets.json +++ b/icon-sets.json @@ -45,16 +45,16 @@ "key": "twemoji", "package": "ichava/icon-sets-twemoji", "repository": "ichava/icon-sets-twemoji", - "title": "Twemoji", - "description": "Twemoji, Twitter/X's open-source emoji set, as SVG. Unicode 17 / Emoji 17 spec. Categorised by Unicode CLDR groups. OpenMoji is in ichava/icon-sets-openmoji.", - "license": "MIT (code) / CC-BY 4.0 (Twemoji assets)", - "icon_count": 3435, + "title": "Emoji Sets", + "description": "Multi-source emoji bundle (Twemoji, OpenMoji color, OpenMoji black). Unicode 17 / Emoji 17 spec. Categorised by Unicode CLDR groups.", + "license": "MIT (code) / CC-BY 4.0 (Twemoji assets) / CC-BY-SA 4.0 (OpenMoji assets)", + "icon_count": 10567, "variants": [ "default" ], "latest_version": "0.1.0", "icon_counts": { - "icon": 3435, + "icon": 10567, "illustration": 0 }, "visibility": "public" @@ -78,5 +78,5 @@ "visibility": "public" } ], - "synced_at": "2026-11-03T10:18:20Z" + "synced_at": "2026-11-06T10:08:37Z" } From 5cde6e541a22b99d3191782253126a38cfa09cd9 Mon Sep 17 00:00:00 2001 From: Imani Manyara Date: Fri, 6 Nov 2026 06:42:26 -0500 Subject: [PATCH 2/4] Hold a catalogued set while its pack's latest tag lags its branch bin/sync-icon-sets.php reads every pack at its latest release tag. icon-sets-twemoji's v0.1.0 still carries the OpenMoji sets that its default branch dropped, so the nightly sync proposed 10,567 "Emoji Sets" back over the recorded 3,435 "Twemoji" (#176), and InstallationPackTableTest and the install characterization test caught it. A set may now carry a hold naming a tag and its commit; while that tag is still the latest release at that commit the set keeps its snapshot, and the hold drops itself once a newer tag is cut or the tag moves. --- bin/sync-icon-sets.php | 53 ++++++++++++++++++- tests/Unit/SyncIconSetsHoldTest.php | 79 +++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 tests/Unit/SyncIconSetsHoldTest.php diff --git a/bin/sync-icon-sets.php b/bin/sync-icon-sets.php index ac05418..405d007 100755 --- a/bin/sync-icon-sets.php +++ b/bin/sync-icon-sets.php @@ -17,6 +17,12 @@ * Usage: * php bin/sync-icon-sets.php * + * A set may carry a `hold`: {"tag", "commit", "reason"}. While the pack's latest + * release tag is still that tag at that commit, the set keeps its recorded + * snapshot instead of being re-read, because the recorded values describe work + * on the pack's default branch that the tag does not hold yet. The hold lifts by + * itself once a newer tag is cut or the tag points elsewhere, and is then dropped. + * * Env: * GITHUB_TOKEN optional, raises the API rate limit (workflow provides it) */ @@ -66,7 +72,11 @@ function main(string $manifestPath): int continue; } - $manifest['sets'][$index] = array_merge($set, $snapshot); + $manifest['sets'][$index] = array_filter( + array_merge($set, $snapshot), + static fn (mixed $value, string $field): bool => $field !== 'hold' || $value !== null, + ARRAY_FILTER_USE_BOTH, + ); printf( "[%s] %s | %d icons (%s) | latest %s\n", @@ -127,7 +137,14 @@ function withoutStamp(array $manifest): array */ function snapshot(string $repository, string $configPath, array $previous): ?array { - $tag = latestTag($repository); + $tags = getJson("https://api.github.com/repos/{$repository}/tags?per_page=100"); + $tag = is_array($tags) ? highestVersionTag($tags) : null; + + if (is_array($tags) && isHeld($previous, $tags)) { + fwrite(STDERR, "[{$repository}] held at {$tag}: " . ($previous['hold']['reason'] ?? 'no reason given') . "\n"); + + return $previous; + } $refs = array_filter([$tag, CONFIG_FALLBACK_BRANCH]); $config = null; @@ -159,9 +176,41 @@ function snapshot(string $repository, string $configPath, array $previous): ?arr 'variants' => is_array($variants) ? array_keys($variants) : ($previous['variants'] ?? null), 'latest_version' => $tag !== null ? ltrim($tag, 'vV') : ($previous['latest_version'] ?? null), 'visibility' => $visibility, + 'hold' => null, ]; } +/** + * Whether a set's `hold` still applies: its tag is the highest release tag in the + * tags API response and still points at the recorded commit. A hold without both + * a tag and a commit never applies, so a malformed entry cannot freeze a set. + * + * @param array $set the set as recorded in icon-sets.json + * @param array $tags the tags API response + */ +function isHeld(array $set, array $tags): bool +{ + $hold = $set['hold'] ?? null; + $tag = is_array($hold) ? ($hold['tag'] ?? null) : null; + $commit = is_array($hold) ? ($hold['commit'] ?? null) : null; + + if (! is_string($tag) || $tag === '' || ! is_string($commit) || $commit === '') { + return false; + } + + if (highestVersionTag($tags) !== $tag) { + return false; + } + + foreach ($tags as $entry) { + if (is_array($entry) && ($entry['name'] ?? null) === $tag) { + return (is_array($entry['commit'] ?? null) ? ($entry['commit']['sha'] ?? null) : null) === $commit; + } + } + + return false; +} + /** * The repository's GitHub visibility, `public` or `private`, from the repos * API response. A response that does not say (a failed request, or a private diff --git a/tests/Unit/SyncIconSetsHoldTest.php b/tests/Unit/SyncIconSetsHoldTest.php new file mode 100644 index 0000000..1b19ce8 --- /dev/null +++ b/tests/Unit/SyncIconSetsHoldTest.php @@ -0,0 +1,79 @@ + $tags name => commit sha + * + * @return list + */ +function holdTagsResponse(array $tags): array +{ + $response = []; + + foreach ($tags as $name => $sha) { + $response[] = ['name' => $name, 'commit' => ['sha' => $sha]]; + } + + return $response; +} + +/** + * @return array + */ +function heldSet(?array $hold): array +{ + return ['key' => 'twemoji', 'icon_count' => 3435] + ($hold === null ? [] : ['hold' => $hold]); +} + +it('holds a set while its tag is the latest release at the recorded commit', function (): void { + $set = heldSet(['tag' => 'v0.1.0', 'commit' => 'aaa', 'reason' => 'branch ahead of tag']); + + expect(isHeld($set, holdTagsResponse(['src/v0.1.0' => 'aaa', 'v0.1.0' => 'aaa'])))->toBeTrue(); +}); + +it('lifts the hold once a newer release is tagged', function (): void { + $set = heldSet(['tag' => 'v0.1.0', 'commit' => 'aaa']); + + expect(isHeld($set, holdTagsResponse(['v0.1.0' => 'aaa', 'v0.2.0' => 'bbb'])))->toBeFalse(); +}); + +it('lifts the hold once the tag points at a different commit', function (): void { + $set = heldSet(['tag' => 'v0.1.0', 'commit' => 'aaa']); + + expect(isHeld($set, holdTagsResponse(['v0.1.0' => 'ccc'])))->toBeFalse(); +}); + +it('never holds a set without a complete hold', function (?array $hold): void { + expect(isHeld(heldSet($hold), holdTagsResponse(['v0.1.0' => 'aaa'])))->toBeFalse(); +})->with([ + 'no hold' => [null], + 'no commit' => [['tag' => 'v0.1.0']], + 'no tag' => [['commit' => 'aaa']], + 'an empty value' => [['tag' => '', 'commit' => 'aaa']], +]); + +it('records a complete hold for every held set in icon-sets.json', function (): void { + $catalog = json_decode((string) file_get_contents(dirname(__DIR__, 2) . '/icon-sets.json'), true, flags: JSON_THROW_ON_ERROR); + + expect($catalog['sets'])->not->toBeEmpty(); + + foreach ($catalog['sets'] as $set) { + if (! array_key_exists('hold', $set)) { + continue; + } + + expect($set['hold']['tag'] ?? null)->toMatch('/^v\d+\.\d+\.\d+$/', "{$set['key']} hold has no release tag") + ->and($set['hold']['commit'] ?? null)->toMatch('/^[0-9a-f]{40}$/', "{$set['key']} hold has no commit") + ->and($set['hold']['reason'] ?? '')->not->toBe('', "{$set['key']} hold has no reason"); + } +}); From 14adb213b24facb98e98e530bbdd0fc38f97372c Mon Sep 17 00:00:00 2001 From: Imani Manyara Date: Fri, 6 Nov 2026 06:43:38 -0500 Subject: [PATCH 3/4] Hold the Twemoji set at v0.1.0 and keep its recorded catalogue values Restores the twemoji entry the sync replaced with v0.1.0's pre-split bundle (title, description, licence and count back to the Twemoji-only 3,435 that docs/installation.md lists) and records the hold, so the next sync keeps them until icon-sets-twemoji tags a release that matches its branch. Re-running the sync with the fixed script reports the catalogue unchanged. --- CHANGELOG.md | 1 + icon-sets.json | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a84f8e2..923a3b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ All notable changes to `ichava/core` follow [Keep a Changelog](https://keepachan ### Fixed +- **The nightly catalogue sync no longer puts Twemoji's pre-split values back.** `bin/sync-icon-sets.php` reads each pack at its latest tag, and `icon-sets-twemoji`'s `v0.1.0` still holds OpenMoji, so #176 proposed 10,567 "Emoji Sets" over the recorded 3,435 "Twemoji". A set in `icon-sets.json` may now carry a `hold` (`tag`, `commit`, `reason`): while that tag is the latest release at that commit the sync keeps the set's snapshot, and the hold drops itself once a newer tag is cut or the tag moves. The twemoji set is held at `v0.1.0`. `tests/Unit/SyncIconSetsHoldTest.php` covers it (proven red by ignoring the commit). - **A pack listed with the application boots again.** `PackConfigCache` resolved the cache store when it was built, and a pack's constants class builds it while the providers are still registering, before the store exists, so every pack failed to boot with `Target class [cache] does not exist` (found running the packs against #172). The store is now resolved when it is used. Core's own pack tests registered their fixture after boot, which is why none saw it; `tests/Feature/PackBootsInProviderListTest.php` lists the fixture with the application. - **`InformationService::discoverPackages()` finds the installed packs.** It globbed `vendor/*/ichava-*`, which matches no pack (packs install as `vendor/ichava/icon-sets-*`), and reported `registered` through `IconRegistry::has()`, which looks up an icon, not a package, so `ichava::ichava-core.info discover` listed nothing. It now asks `Composer\InstalledVersions` for every package of type `ichava-icon-set` and reports `registered` through `IconRegistry::isRegistered()`. The signature and return shape are unchanged. A pack is identified by its composer type and nothing else (owner, 2026-11-05): the `vendor/*/ichava-*` directory glob is removed, since no pack in the estate matched it, and the `platform/ichava` scan now keeps only a directory whose composer.json declares `ichava-icon-set`, keyed by that file's `name` instead of `ichava/`. A checkout there without the type is no longer listed. `tests/Feature/InstalledPackDiscoveryTest.php`. - **The installation guide no longer says core is on Packagist.** Nothing in the family is (`repo.packagist.org` answers 404 for core, every pack and the four `laranail/*` packages). `docs/installation.md` step 1 is now the one place that lists the repositories an application adds, `ichava/core` included; the README and every pack link to it. diff --git a/icon-sets.json b/icon-sets.json index d1a3841..961b7a9 100644 --- a/icon-sets.json +++ b/icon-sets.json @@ -45,19 +45,24 @@ "key": "twemoji", "package": "ichava/icon-sets-twemoji", "repository": "ichava/icon-sets-twemoji", - "title": "Emoji Sets", - "description": "Multi-source emoji bundle (Twemoji, OpenMoji color, OpenMoji black). Unicode 17 / Emoji 17 spec. Categorised by Unicode CLDR groups.", - "license": "MIT (code) / CC-BY 4.0 (Twemoji assets) / CC-BY-SA 4.0 (OpenMoji assets)", - "icon_count": 10567, + "title": "Twemoji", + "description": "Twemoji, Twitter/X's open-source emoji set, as SVG. Unicode 17 / Emoji 17 spec. Categorised by Unicode CLDR groups. OpenMoji is in ichava/icon-sets-openmoji.", + "license": "MIT (code) / CC-BY 4.0 (Twemoji assets)", + "icon_count": 3435, "variants": [ "default" ], "latest_version": "0.1.0", "icon_counts": { - "icon": 10567, + "icon": 3435, "illustration": 0 }, - "visibility": "public" + "visibility": "public", + "hold": { + "tag": "v0.1.0", + "commit": "d7af0a20af8c5501ff5857c144573b1cf9329d16", + "reason": "v0.1.0 still carries the OpenMoji sets; the recorded values follow the default branch (Twemoji only) until the next release." + } }, { "key": "openmoji", @@ -78,5 +83,5 @@ "visibility": "public" } ], - "synced_at": "2026-11-06T10:08:37Z" + "synced_at": "2026-11-03T10:18:20Z" } From 7556270044e8dcc9035f2b9053c8037f73535759 Mon Sep 17 00:00:00 2001 From: Imani Manyara Date: Fri, 6 Nov 2026 06:49:22 -0500 Subject: [PATCH 4/4] Align isHeld's docblock with the repository's Pint rules --- bin/sync-icon-sets.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/sync-icon-sets.php b/bin/sync-icon-sets.php index 405d007..eebc103 100755 --- a/bin/sync-icon-sets.php +++ b/bin/sync-icon-sets.php @@ -185,8 +185,8 @@ function snapshot(string $repository, string $configPath, array $previous): ?arr * tags API response and still points at the recorded commit. A hold without both * a tag and a commit never applies, so a malformed entry cannot freeze a set. * - * @param array $set the set as recorded in icon-sets.json - * @param array $tags the tags API response + * @param array $set the set as recorded in icon-sets.json + * @param array $tags the tags API response */ function isHeld(array $set, array $tags): bool {