release: 1.1.0-dev.2 — git-backed plugin distribution, i18n cascade, tooling fixes - #107
Merged
Conversation
Remove .claude/, CLAUDE.md, .github/copilot-instructions.md and docs/ from version control (kept locally, gitignored) so they are not published to GitHub.
chore: remove AI-assistant config and internal docs from the repo
Previously the ASCII banner only appeared on 'hkm version'. It now headers the default help output too.
feat(cli): show Sentinel banner on bare hkm and hkm help
hkm upgrade now detects the OS, downloads the matching release artifact, and installs it (Linux apt / macOS install.sh / Windows install.bat), instead of only printing manual instructions.
feat(upgrade): auto-download and install updates per OS
…g.env - run/registry now resolve the kernel relative to the launcher (installed /opt/hkm-kernel or dev repo), fixing 'Kernel registry not found' on packaged installs and stopping use of a dev kernel found via PWD. - hkm-config checks the kernel + writes/repairs HKM_KERNEL_HOME. - launcher loads ~/.config/hkm/config.env at startup (real env wins). No version change.
fix(cli): self-locate installed kernel + real hkm-config
Scaffolding templates moved tools/src/templates -> top-level templates/. tools/ is not bundled, so hkm new / hkm ui init could not find templates on a packaged install. bundle.sh now ships templates/ (exempt from the docs/ tools strip); services resolves <kernel>/templates via self-location. No version change.
fix(templates): ship templates in the kernel (move out of tools/)
Kernel self-location, real hkm-config, config.env loading, and templates shipped inside the kernel.
chore(release): v1.0.3
…help note
- projects/projects.json is committed empty ({}) so developer-local
registrations (and machine paths) never ship in the repo or bundles.
- .githooks/pre-commit forces projects.json to {} in every commit; enable
with: git config core.hooksPath .githooks
- hkm help notes the env vars are auto-detected (override only if needed).
…help chore: empty committed project registry + help note
projects.json + platform.json are user data. HKM_USERDATA_DIR relocates them outside the kernel install (honoured by the hkm CLI registry and the PHP DomainResolver), and the .deb marks them as conffiles so an in-place upgrade preserves the user's registrations. Falls back to <kernel>/projects when unset.
feat(userdata): HKM_USERDATA_DIR so updates don't clobber the registry
…ata) hkm-config now resolves/pins HKM_KERNEL_HOME AND provisions the persistent userdata dir: creates XDG_DATA_HOME/hkm (or ~/.local/share/hkm), migrates any existing registry into it, and pins HKM_USERDATA_DIR. One command configures everything the launcher and runtime need.
feat(config): hkm-config provisions the full environment
- .env (holds generated APP_KEY) written chmod 600; config.env too - debug output force-disabled when APP_ENV=production regardless of APP_DEBUG - new projects ship app/public/.htaccess (deny dotfiles, no listing, drop X-Powered-By, baseline security headers, front-controller rewrite) - env.example documents the production/secret-handling expectations
security: harden scaffolding defaults
Security hardening (scaffolding perms, prod debug gate, Apache+nginx web config), HKM_USERDATA_DIR for persistent registry across updates, and hkm-config full-environment setup.
chore(release): v1.0.4
New projects scaffold app/apache.conf.example (DocumentRoot=app/public, deny dotfiles, only index.php executable, security headers).
feat(scaffold): Apache vhost sample
Adds the Apache virtual-host sample to project scaffolding (alongside nginx).
- README now documents native install (.deb/.tar.gz/.zip), the hkm command set, HKM_* env vars, requirements, dev/build flow, and security defaults. - Remove links to the removed docs/ai-context files from the Auth plugin README.
docs: refresh README + fix broken links
Route policy — the third route verb (add/override/DISABLE):
- Kernel::withRoutePolicy() + proj.json "routePolicy": {"disable": []} let a
project veto plugin routes without forking the plugin. Specs are either
"METHOD /path" (one route) or a module domain (all of a plugin's routes).
- CompileRouteManifestStage applies the policy to plugin routes AFTER they
compile and BEFORE project routes, so a disabled key can be re-declared by
the project. An unmatched spec fails the boot (anti-typo guard).
- EntryHelpers::projectRoutePolicy() reads the proj.json block.
hkm dev environment for contributors:
- `hkm <command> --dev` pins one invocation to the development kernel
(HKM_DEV_HOME from config.env, or walk-up self-location from a repo-built
launcher). Exports HKM_KERNEL_HOME + HKM_CLI_PATH for the child only;
fails loudly when no dev kernel is found.
- hkm-config set-dev-home <path> (validated) + help/README documentation.
Templates: scaffolded proj.json ships the routePolicy stub, bootstrap wires
withRoutePolicy(), project README documents the three route verbs.
Project routePolicy.disable (veto plugin routes without forking) and the hkm --dev contributor environment (stable install + dev checkout side by side).
…, url generation
Eight kernel additions derived from a gap analysis against HKM 0.3
(docs/migration/KERNEL-ADDITIONS.md). All additive and backward compatible —
no existing module.json, proj.json or bootstrap needed editing.
- Config\Repository + CompileConfigManifestStage (boot stage 9) + config()
Dotted, immutable, project deep-merged over plugin per KEY rather than
replacing the whole file.
- Ports\LoggerPort + LogLevel enum
The only binding of Psr\Log\LoggerInterface in the codebase pointed at a
NullLogger, so every line from Database, Tenancy, EventBus and the command
auditor was silently discarded.
- Ports\Lock + AbstractLock + CachePort::lock()/restoreLock()
increment() is atomic but cannot express ownership, blocking acquisition or
TTL-bounded release, so single-flight cron and job idempotency had no correct
implementation. Release must be atomic compare-and-delete.
- Routing\RouteParameter — typed {id:num} params, unknown type fails the boot
Previously every {param} compiled to [^/]+, so /users/{id} matched /users/abc.
- Routing\UrlGenerator — named routes + signed URLs
A project override inherits the plugin route's name, so a plugin view linking
to route('auth.register') survives the project replacing that page.
- QueuePort pop/ack/release/fail + WorkerLoop port mode
The port was write-only; consumption leaked through a caller-supplied puller.
- Exceptions\HttpStatusAware — non-kernel exceptions were all 500 + CRITICAL.
- Ports\ClockPort + SystemClock — CSRF expiry was untestable without sleeping.
Tests: 532 -> 659 (+127). Pre-existing failures unchanged (77 ext-sqlite3
errors, 5 php-io-cli CLI failures).
- Logger (NEW, solves logging.application): FileLogger, StreamLogger, NullLogger, PsrLoggerBridge behind Kernel\Ports\LoggerPort. - Commands: stop binding Psr\Log\LoggerInterface to a NullLogger — that single binding was discarding every log line in the application. Resolve LoggerPort. - Database, Tenancy: migrate off Psr\Log onto LoggerPort. - RedisCache: RedisLock (SET NX PX + Lua compare-and-delete) and the QueuePort read side (pop/ack/release/fail) with a dead-letter list. - Mail, Validation, Storage, Edge: read config through the compiled manifest instead of four copies of a project-file-REPLACES-plugin-file lookup, so a project now overrides only the keys it names. - DevTools: config:show / config:clear. - Auth: drop docblock references to FirewallLayer/RateLimiterLayer, which the kernel does not ship.
- FirewallLayer / RateLimiterLayer were referenced in six files but have never existed; the kernel ships only CsrfTokenLayer (IP filtering and rate limiting are SecurityFilters route filters). Following CLAUDE.md produced a bootstrap that fatals on an undefined class. - CoreContainer's docblock claimed build() freezes the container; Kernel.php freezes in materialize(). - CLAUDE.md documented src/Kernel/Http/, which moved to the alfacode-team/http package (namespace unchanged). - Worker template: drop the hand-written puller that type-checked its adapter and returned null for anything else — swapping to Redis made the worker silently process nothing. - New: docs/ai-context/28_KERNEL_ADDITIONS.md + migration analyses.
InMemoryRefreshTokenStore lacked allActive(), InMemoryScopeStore lacked put()/delete(), and an anonymous RefreshTokenStore in AuthorizedTokenManagementTest lacked allActive(). Each fatals at COLLECTION time, so the whole suite aborted with no output rather than failing one file. Also: FakeCache implements the new CachePort lock methods, and the Zig bootstrap template drops its FirewallLayer/RateLimiterLayer reference.
BREAKING: plugins/ no longer exists in this repo. All 28 plugins are now composer packages (alfacode-team/hkm-plugin-*) with their own repositories, resolved from a sibling ../plugins workspace via a path repository with symlink:true, so edits there stay live without a reinstall. - composer.json: drop the "Plugins\\" PSR-4 root and the six plugin helper files[] entries; add the path repository and 28 requires. - Each plugin package now declares its own autoload.files. Three of them (Storage, ViteManifest, SocialAuth) defined global helpers that were in NO autoload.files at all — not the plugin's, not the kernel's — so callers such as Storage\Provider::register(), which calls storage_config() ten times, would fatal on an undefined function. Pre-existing; surfaced by the move. - Tests: resolve plugin asset paths via ReflectionClass instead of a repo-relative plugins/ path, which breaks once a plugin is a dependency. Suite unchanged at parity: 659 tests, 77 errors (ext-sqlite3 absent), 5 failures (php-io-cli submodule).
…wn repos
Corrects the dependency direction from the previous commit. Requiring the 28
plugin packages from the kernel inverted the placement law (the framework must
not know about plugins) and created a cycle, since every plugin dev-requires
the kernel to run its own tests. The kernel now requires only its modules.
- Removed the 28 plugin requires and the ../plugins path repository.
- Moved tests/Unit/{Plugins,Database,Commands} into the plugin repos, plus the
two kernel tests that reached into plugins (DevTools' config commands, the
Logger adapters). The kernel keeps LogLevelTest, since it owns the enum.
- Bumped modules/php-io-cli for unknown-option rejection, which makes
tests/Unit/Cli/UnknownOptionTest pass — it had been red permanently, testing
a feature that was never implemented upstream either.
Kernel suite is now fully green: 157 tests, 298 assertions, 0 failures.
Previously 659 tests with 77 errors and 5 failures; the 77 were ext-sqlite3
(fixed in the Database plugin) and the 5 were the CLI parser.
The constraint was "*", while the kernel had in fact depended on an UNTAGGED bind-it commit since June — the one adding a PSR-11 compatible Container::get(string $id): mixed. Locally that resolved through the modules/bind-it path repo and worked; any external consumer resolved the published 0.1.3 instead and died at class-load time with Declaration of Kernel\Container\ModuleContainer::get(string $id): mixed must be compatible with PHPShots\Common\Container::get($key) which surfaced as "Premature end of PHP process" — a fatal, not a catchable error, so it read as a crashing test rather than a dependency mismatch. bind-it 0.1.4 now tags that commit, and its composer.json no longer hardcodes a "version" field that overrode (and lagged) the git tags. The constraint accepts the path repo's dev-master for kernel development and the tag for consumers.
…ip messages Adds CompileLangManifestStage (boot stage 7), which compiles every module.json "lang" declaration plus the project's own into lang-manifest.php. WHY The Translator held a single directory, so the only catalogue that could ever load was the one the I18n plugin ships. A plugin had nowhere to register its messages, which is why every user-facing string in every other plugin is hard-coded English. APP_LANG_PATH made it worse rather than better: it REPLACED the directory, so a project pointing it at its own catalogue silently lost the plugin's and got raw keys back for anything it had not copied. MODEL Deliberately identical to views — a platform with two different override models is a platform nobody can predict. Lower priority wins: project sources default to 0, plugin sources to 100, so a project overrides a plugin's wording by default and can do so without forking. A plugin preempts the project only by declaring an explicit lower priority. Ties break by declaration order, never by load or filesystem order. Plugin catalogues are also exposed under a namespace (the module name by default) so two plugins can define the same key. A declared-but-missing directory is skipped rather than fatal — a catalogue is optional content and a typo should not make the app unbootable. An empty path does throw, because it means the author expects messages that will never load. NOTE ON DUPLICATION This mirrors CompileViewManifestStage rather than sharing code with it. The common cascade logic is worth extracting, but that stage has no test coverage and refactoring a shipped boot stage blind is the worse trade. This one ships with 11 tests; extract once both are covered. Full kernel suite green (168 tests).
…r --help THE STRUCTURAL BUG Every command arm ended in `std.process.exit(code)`. std.process.exit does NOT run deferred code, so `threaded.deinit()` and the arena's `deinit()` never executed on any successful command. The process exiting made that harmless in practice, but it also meant nothing could ever run at teardown — no leak check, no report, no cleanup. main() is now a thin wrapper that owns the memory manager and is the only place that exits; dispatch() returns an exit code up to it. MEMORY MANAGER (lib/memory.zig) The file did not compile. `std.heap.DebugAllocator()` was called with no argument (it is a generic taking a Config), and it imported "constants.zig" from src/lib/ where the file lives at src/. Neither was caught because nothing referenced it and Zig only analyses a function body when it is used. Rewritten as a Manager that owns the whole strategy: debug builds get the debug allocator (leak / double-free / use-after-free) wrapped in the inspector; release builds get the page allocator, with the wrapper compiling to nothing. Verified: --release=small is 434 KB and --mem produces no output at all. Manager.init() deliberately does NOT wire itself. An earlier version did, and segfaulted: DebugAllocator.allocator() captures &self, so wiring a local and then returning the struct by value left the copy pointing into a dead stack frame. The crash surfaced inside std.process.Environ.createMap, blaming the first innocent code to allocate. Wiring now happens lazily at the final address. constants.zig derived __DEBUG__ from a hard-coded `true`, so a RELEASE build would have kept never_unmap and retain_metadata — the first stops freed pages returning to the OS, the second keeps every allocation's metadata alive. Both are correct while debugging and both are leaks by design in a shipped binary. Now derived from builtin.mode. INSPECTOR Groups are tagged per command via a CmdScope that sets the group BEFORE opening that command's arena. Order matters: the arena takes memory in large chunks, so whichever group is active at its first chunk is charged for everything served from it — with one process-wide arena opened at startup, every command's memory was attributed to "startup". The group panel now scales by PEAK, not current. The dashboard prints at exit, where a clean run has freed everything, so every bar was drawn at zero. Peak answers what was actually being asked; a non-zero `current` is now called out explicitly, since that is where a leak lives. Event.ret_addr was hard-coded to 0 at both call sites, making the field a lie for anything that read it. It now carries the real return address. --help REPAIRS - discover: --help fell through the catch-all `--` branch and was ignored, so asking for help ran a full scan AND wrote every project it found into the registry. Now matched before the catch-all. - cli/worker: printed help then returned 2, so a successful help request looked like a failed command to any script checking the status. - run: parse() returned null for BOTH --help and invalid arguments, collapsing them into "print help, exit 2". The two are now distinct. - new/update: no --help handling at all; the usage text was inline in an error branch. Extracted to printHelp() and reachable explicitly. All twelve commands now exit 0 for --help. Normal behaviour is unchanged and an unknown command still exits 2. ALSO - build.zig gains a `test` step. There was none. It is rooted at src/main.zig so relative imports resolve — `zig test src/lib/memory.zig` makes src/lib the module root and fails on ../constants.zig, which is misleading rather than useful. - main.zig gains an explicit test-collection block. Zig only runs tests from files it analyses, and a top-level @import is not enough: without it the step reported success having run nothing. Verified by mutation — breaking an assertion in memory.zig now turns the step red. - util.envIsTruthy centralises what HKM_* toggles consider "yes". - --mem prints the dashboard; HKM_MEM_INSPECT does it always; HKM_MEM_STRICT exits 70 on a leak, for CI. All documented in `hkm help`. 6 tests pass. Debug and --release=small both build clean.
Enabling a plugin that was not already on disk used to wire it into the bootstrap "by name anyway" — producing a project that referenced a Provider class which did not exist. The failure landed at boot as a class-not-found, nowhere near the command that caused it. Plugins are now fetched from git, and enable either works or says why it cannot. NEW COMMANDS hkm plugins install <plugin> [proj] fetch from git (aliases: fetch/get) hkm plugins uninstall <plugin> [proj] remove + drop from the lock hkm plugins versions <plugin> list releases on the remote hkm plugins outdated [proj] what has a newer release hkm plugins lock [proj] restore every plugin at its locked version `enable` auto-installs a missing plugin first. A plugin found locally still wins: a contributor's working copy must not be silently replaced by a release. VERSIONING Installs resolve to a concrete TAG, never a branch. A plugin pinned to `main` changes under a project between two deploys, which is exactly what a lock file exists to prevent — so an untagged plugin is refused rather than installed from a moving ref. plugins.lock.json records remote, tag, commit and the kernel version in force. The commit is stored alongside the tag deliberately: a tag can be moved on the remote, and the two disagreeing is itself the signal. Entries are sorted and hand-serialised so the file diffs cleanly in review — a lock is read by people far more often than by machines. KERNEL LOCK module.json gains a "kernel" semver constraint (e.g. "^1.0"), checked before a plugin is committed into plugins/. An ABSENT constraint installs freely — every plugin predating the field declares nothing and failing closed would make all of them uninstallable at once. A MALFORMED one is refused: someone tried to express a requirement and got it wrong, and treating that as "no requirement" defeats the check. A 0.0.0-dev kernel bypasses the gate, since 0.0.0 is below every real floor and contributors would otherwise be unable to install anything. ORDER OF OPERATIONS resolve → resolve tag → fetch to STAGING → gate → commit into plugins/. The gate runs after the fetch because the constraint lives inside the plugin; staging first means a rejected plugin never lands in plugins/, where the bootstrap would then try to wire it. A refused UPDATE rolls back to the previous tag rather than leaving a plugin the project cannot boot with. A working copy with uncommitted changes is never overwritten without --force. SUPPORTING MODULES - semver.zig: parsing and constraint matching (caret, tilde, comparators, wildcards, exact pins), with 0.x handled properly — ^0.2 allows 0.2.x only, because pre-1.0 packages break in the MINOR field. - plugin_registry.zig: name → remote, including the irregular repo spellings (SocialAuth → social-auth, SiteSEO → siteseo, OAuth2 → oauth2) that were otherwise scattered special cases. HKM_PLUGIN_ORG / HKM_PLUGIN_REMOTE support forks and mirrors. - plugin_git.zig: ls-remote/clone/fetch/checkout, shallow by default. Shells out to git so credential helpers, SSH agents and insteadOf rewrites all work. ls-remote retries three times: a single transient DNS hiccup otherwise surfaces as "the repo does not exist", sending people after a permissions problem they do not have. Measured 1-in-3 success before the retry, 5-in-5 after. 29 tests pass. Verified end to end against the real remotes: fetching Logger into a bare project resolved v1.0.0, cloned at that tag, wired the bootstrap and wrote a correct lock; re-running reports up-to-date and touches nothing.
…from
The kernel is really at v1.0.21, and tools/bundle.sh stamps the version from
`git describe --tags` — so any build made between releases carries a trailer
like "1.0.21-138-gbdbbf34", meaning "138 commits after v1.0.21".
Plain semver reads that trailer as a PRE-RELEASE, and a pre-release sorts BELOW
its release. The build was therefore considered OLDER than the tag it was built
from: a plugin requiring ">=1.0.21" was refused on a kernel 138 commits past
v1.0.21. Every nightly and every packaged build cut between two releases would
have hit it.
parseDescribed() recognises the "-<commits>-g<sha>" shape and drops it, treating
the build as its base version. Conservative on purpose: the true version lies
between this tag and the next, so claiming the lower bound can only refuse a
plugin a newer kernel would have accepted — never admit one it should reject.
Detection is deliberately narrow. The count must be all digits and the sha must
be 'g' followed by hex, so a genuine pre-release keeps its lower precedence:
"1.0.0-rc.1" still fails ">=1.0.0", and "0.0.0-dev" still parses as a dev build.
Lookalikes ("1.0.0-1-gz", "1.0.0-beta-g1", "1.0.21-138") are left alone.
32 tests. Verified end to end against a stamped build: the kernel gate now
refuses a plugin requiring ^99.0 with a clear message and leaves nothing behind
in plugins/, accepts one requiring ^1.0, and HKM_PLUGIN_IGNORE_KERNEL=1
overrides. Also confirmed the installer checks out the COMMIT an annotated tag
points to rather than the tag object, and records that commit in the lock.
…ade repo TWO FIXES 1. build.zig stamped the repo as "AlfaCode-Team/php-service-platform", but it was renamed to "hkm-kernel". `hkm upgrade` queries that name to find the latest release, and it only still worked because GitHub redirects the old name — a redirect that lasts only while nothing else claims it. Now points at the real repo. 2. A versioned build writes its version into composer.json, so a release carries it everywhere rather than only in the compiled binary. bundle.sh already copies composer.json into the artifact (SRC_PATHS), so the packaged kernel now reports its own version. WHY THIS IS SCOPED THE WAY IT IS A literal "version" in composer.json is normally a liability: Composer derives a package's version from git tags, and the field OVERRIDES them. Once they can disagree they eventually do — someone tags v1.2.0, forgets the field, and every consumer silently resolves the stale number. This repository has already been bitten by exactly that (phpshots/bind-it pinned "0.1.3" and its real tags were ignored). It earns its place only because the native distribution ships WITHOUT a .git directory: a .deb or zip has no tags to derive from, so the field is the installed kernel's only version marker. So the stamp runs only when -Dversion was passed explicitly — which is what bundle.sh does for a release. A plain `zig build` is 0.0.0-dev and leaves the file untouched, so a dev build never dirties the working tree with a version someone then commits by accident. `zig build stamp -Dversion=X` runs it on demand. The edit is textual, not a JSON re-serialise: the file keeps its hand-maintained key order and indentation, so a release produces a one-line diff instead of reordering every key. Re-stamping the same version is a no-op. 5 tests for the stamper (37 total). Verified: -Dversion=1.0.21 inserts the key after "name" and the result still parses; re-running changes nothing; a bump to 1.0.22 replaces it in place; a plain build leaves composer.json alone.
…e installed kernel
`hkm upgrade` could only install a PUBLISHED release. There was no way to put
local kernel changes into /opt/hkm-kernel — the copy every project on the
machine actually runs — without tagging a release first, so testing a kernel
change against real projects meant cutting a release to try it.
hkm upgrade --local [--dry-run] [--yes]
Copies the same file set a .deb ships (mirroring SRC_PATHS in bundle.sh) via
`git ls-files --recurse-submodules`, so only TRACKED files move: build
artifacts, vendor/ and local scratch cannot leak into the install. bin/psp is
installed as bin/hkm, matching the rename bundle.sh performs so the launcher's
passthrough resolves. The built native launcher is copied to /usr/bin when
present, and install.sh runs afterwards so PHP dependencies resolve against the
TARGET's PHP rather than whatever the checkout happened to resolve.
Guards: refuses when source and target are the same path (copying a checkout
over itself would delete files mid-walk), when the source is not a kernel
checkout, and prompts before overwriting unless --yes. --dry-run reports the
file set without writing.
THREE BUGS FOUND WHILE BUILDING IT
- util.canWrite probed with a 64-byte buffer, so any ordinary path overflowed
bufPrint. The overflow was swallowed as "cannot write", so the caller
escalated to sudo for targets it could write directly — and every one of 645
copies then failed. Now sized to std.fs.max_path_bytes.
- The copy loop counted failures without reporting a cause. "645 file(s) could
not be written" is barely better than failing silently; it now names the first
failure and its error.
- banner.printShort() wrote to STDERR via std.debug.print, while its own
docblock said "for `hkm --version` piped/scripted use". VERSION=$(hkm
--version) returned an empty string and the version went where the caller was
not looking. Now writes to stdout.
The "installed" version is read from the TARGET's composer.json rather than
banner.version(): the latter is the version THIS BINARY was stamped with, which
is normally the local dev build — so both sides would report the source's
version and always look like a no-op. They differ exactly when this command is
worth running. Falls back to the binary's version when the target predates
composer stamping.
Verified end to end against a scratch target: 645 files copied, src/Kernel and
modules present, bin/psp correctly installed as bin/hkm, and vendor/ and var/
absent. plugins/ copies nothing, which is correct — the kernel no longer ships
plugins; they are fetched with `hkm plugins install`.
37 tests pass; debug and --release=small both build clean.
Publishing a single dev tag would have pushed unfinished work to every stable user. `latestTag()` took the highest v* tag with no filtering, and upgrade.zig's local `Ver.parse` STRIPPED any pre-release suffix before comparing. So "v1.1.0-dev.1" was indistinguishable from a stable "v1.1.0" and sorted above "v1.0.21": anyone running `hkm upgrade` would have been told an update was available and given the dev build. That is the opposite of what publishing a pre-release is for. Two changes: - Pre-release tags are skipped when selecting the latest release. `--pre` opts in, for someone who deliberately wants a dev or rc build. - The local `Ver` duplicate is replaced by lib/semver.zig, which sorts a pre-release BELOW its release per semver §11 and understands the `git describe` trailer bundle.sh stamps between releases. Removing the duplicate also means there is now one definition of "which version is newer" rather than two that disagreed on exactly this case. release.yml already marks a tag containing '-' as a GitHub prerelease, so the publish side was correct; only the client side was not. Verified: given tags v1.0.20, v1.0.21 and v1.1.0-dev.1, selection yields v1.0.21 by default and v1.1.0-dev.1 with --pre. 37 tests pass.
The release workflow extracts the '## [VERSION]' block as the release body, so this is what people will actually read on the release page.
PHP Analysis has failed on every commit since db63dae — the refactor that moved plugins out of the kernel into their own repositories. That commit removed plugins/ but left it in phpstan.neon.dist's `paths`, so every run since died on "Path .../plugins does not exist" before analysing a single file. Four months of green-to-red went unexplained because the failure looked like an analysis error rather than a config one. With the path removed, analysis runs and reports 53 findings. The three in src/Kernel — the code that actually ships — are fixed here: - Repository::lookup() and UrlGenerator::substitute() declared nullable by-ref parameters that are never assigned null. PHPStan is right that the nullable half is dead, but simply narrowing the type would have broken BOTH call sites at runtime: they passed undefined variables, and PHP materialises an undefined by-ref argument as null, which a non-nullable parameter rejects with a TypeError. Verified that before changing it. The callers now initialise explicitly, which is clearer anyway, and the types are narrowed. - CompileConfigManifestStage accepted a ManifestReader it never read. It locates a plugin's config by DIRECTORY (config/*.php beside its Provider), never from module.json, so the dependency was there purely for signature symmetry with the sibling stages — and implied a manifest lookup that does not happen. Removed, along with its now-unused import and the argument at both call sites. The remaining 50 are in projects/ and are the same decoupling fallout: project-layer code referencing Plugins\* classes that no longer live in this repository. That is a real architectural coupling the split exposed, not a config error, and fixing it means changing what the project layer depends on — deliberately left for its own change rather than baselined away here. 168 tests pass; src/Kernel is PHPStan-clean.
…uild broke
The v1.1.0-dev.1 release build failed on all three platforms:
dashboard.zig:70:54: error: binary operator '*' has whitespace on one side,
but not the other
p("\n{s}{s}╭{s}╮{s}\n", .{ bold, blue, "─" ** WIDTH, reset });
The message is misleading — the spacing is symmetric. Zig 0.17 REMOVED the `**`
array-repeat operator, so `"─" ** WIDTH` now parses as two pointer-type `*`
tokens (`"─" * (*WIDTH)`), and the whitespace rule fires on the first of them.
Written without spaces it fails differently, with "expected type 'type', found
'comptime_int'", which is the same cause seen from the other side. `**` no
longer appears anywhere in 0.17's own std.
Replaced with a comptime `repeat()` helper, which works on both 0.16 and 0.17.
It has to be `inline`: a plain fn building the string in a comptime block is
rejected with "function called at runtime cannot return value at comptime".
WHY THIS REACHED A RELEASE
It compiled fine on the Zig installed here (0.16.0) and only the PINNED version
in tools/.zig-version (0.17.0-dev.657) rejects it — and that pinned version is
the one every release is built with. I flagged the mismatch twice while working
without acting on it; this is what it cost. Fixed by installing the pinned
toolchain with the repo's own tools/ci/setup-zig.sh and reproducing the failure
locally before changing anything.
Verified on 0.17.0-dev.657: debug, --release=small, and the tests all pass, and
all three release targets cross-compile (aarch64-macos, x86_64-windows,
x86_64-linux). 0.16 still builds, so the fix is good on both.
The v1.1.0-dev.1 release built all three artifacts successfully and then
published with ZERO assets on it.
This repository has immutable releases enabled. The workflow created the release
already published (draft: false) and uploaded assets afterwards, which immutable
releases forbid:
Cannot upload asset ... to an immutable release. GitHub only allows asset
uploads before a release is published.
The .deb, .zip and .tar.gz were all built and had nowhere to go, and because the
release is immutable they can never be added to it — the tag has to be recut.
Assets now attach while the release is still a draft, and a following step
publishes it. That is the flow GitHub's own error message prescribes for
immutable-release repositories.
Worth noting for anything downstream: a draft prerelease fires
release.published when it is published, not release.prereleased.
v1.1.0-dev.1 was withdrawn: it published with no binaries attached because immutable releases forbid post-publish asset uploads, and the repository's tag ruleset (correctly) refuses to let a deleted tag be recreated. The version number is burned; dev.2 is the first installable pre-release.
…r one
The CHANGELOG version detector matched only ^## [x.y.z], so a pre-release
heading like '## [1.1.0-dev.2]' was passed over and the next heading down
('## [1.0.21]') matched instead. That version is already tagged, so the run
reported 'nothing to release' — a dev release merged to main would have
silently done nothing while appearing to have been considered.
The suffix is now part of the pattern.
hakeemRash
requested review from
Alshatri and
craftdevscommunity
as code owners
August 7, 2026 09:08
|
Important Review skippedToo many files! This PR contains 985 files, which is 885 over the limit of 100. To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch. Upgrade to a paid plan to raise the limit. Usage-priced reviews support at most 300 files. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (10)
📒 Files selected for processing (985)
You can disable this status message by setting the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…ffolding PR #107 was red on PHPStan with 50 findings. One was a genuine bug; the other 49 share a single root cause. THE REAL BUG ProcessLocalLock declared its registry as an anonymous `object{locks: ...}` shape. PHPStan treats an object shape's properties as READ-ONLY, so all three writes to $registry->locks were errors — against a type that described the shape but never named the one class that satisfies it. Typed as InMemoryCache, which is the sole owner of that table and the only caller, and is what the docblock already said. THE OTHER 49 projects/ ships opt-in base controllers and SEO helpers that bind to Plugins\Cookie, Plugins\View and Plugins\SiteSEO by design (CLAUDE.md, "Base controllers"). Since db63dae those plugins are deliberately NOT dependencies of this repository — the kernel depends on no plugins — so the classes cannot resolve here and every reference reports class.notFound. Adding them back as require-dev would re-introduce exactly the coupling that commit removed, and create a dev-dependency cycle (the plugins already dev-require the kernel). So these nine files are excluded from analysis here, listed FILE BY FILE rather than excluding projects/ wholesale — any new project-layer code is still analysed, and the rest of projects/ (Support/Casting, Support/Entity, Infrastructure, ApiController) stays covered: 48 of 57 files. Each exclusion was verified to be caused by a plugin reference rather than assumed. InteractsWithCsrf names no plugin itself but calls CookieJar methods the sibling trait provides — checked by removing it from the list and confirming the error returns. This is a scoping decision, not a fix: the coupling is real and those files are analysed where their dependencies exist, in a project that has installed the plugins. Moving them out of the kernel repo is the actual resolution and wants its own change. PHPStan: no errors. 168 tests pass.
craftdevscommunity
approved these changes
Aug 7, 2026
craftdevscommunity
pushed a commit
that referenced
this pull request
Aug 7, 2026
Records the re-cut from main. v1.1.0-dev.2 was tagged from master before #107 merged, so its artefacts predate the PHPStan fixes; dev.3 is the same contents built from the branch releases are supposed to come from.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings
masterup tomainfor the 1.1.0-dev.2 development pre-release.Merging this triggers
auto-release.yml, which reads the top CHANGELOG version,tags it, and runs the release build. That path is the reason for this PR — the
earlier
v1.1.0-dev.1/v1.1.0-dev.2tags were cut by hand offmaster,bypassing it.
What's in it
Plugins install from git.
hkm plugins install|uninstall|versions|outdated|lockfetch a plugin from its own repository, and
hkm plugins enablenow installs amissing plugin instead of wiring it into the bootstrap by name and failing at
boot with a class-not-found. Installs resolve to a tag, never a branch, and
plugins.lock.jsonrecords the remote, tag, commit and kernel version so aninstall is reproducible and reviewable.
Kernel compatibility gate. A plugin declares
"kernel": "^1.0"in itsmodule.json; an incompatible pairing is refused at install time rather than
surfacing at request time as a missing method on a contract. All 28 plugins now
declare it and are released at
v1.0.1/v2.0.1.Translation catalogue cascade.
CompileLangManifestStagecompiles eachplugin's
langdeclaration intolang-manifest.phpusing the sameproject-first priority model as views — so plugins can finally ship messages.
Groups merge across the cascade, so overriding one key does not mean copying
the rest. English + French ship for every plugin with user-facing text.
hkm upgrade --localinstalls a local checkout over the installed kernel,for testing a kernel change against real projects without cutting a release.
Memory inspector wired into the CLI (
--mem,HKM_MEM_STRICT), plus azig build teststep, which did not exist.Fixes worth reviewing
std.process.exit, which skips defers--helpbroken in five commandshkm discover --helpignored the flag and ran a registering scanhkm --versionwrote to stderrVERSION=$(hkm --version)returned emptygit describebuild sorted below its own tag**array-repeat removed in Zig 0.17db63daepluginspath the kernel no longer hasKnown, deliberately not fixed here
projects/— project-layer code referencingPlugins\*classes that no longer live in this repo. Real architecturalcoupling the decoupling exposed, not a config error; fixing it changes what
the project layer depends on and wants its own change.
v1.1.0-dev.1is a burned version number. It published without binariesand the tag ruleset (correctly) refuses to recreate a deleted tag.
not been reviewed by a native speaker.
Verification
--release=small, and all three cross-compile targets build on thepinned Zig 0.17.0-dev.657 (not just the 0.16 installed locally)
src/Kernelis PHPStan-clean