feat(block-tools): manage block CI workflows in the structurer#1705
Conversation
Add `.github/workflows/build.yaml` and `mark-stable.yaml` as engine-owned (`fixed`) scaffold files, generated from the block's short name. The shared reusable-workflow pin (`@v4`) and job wiring are now centrally refreshable instead of hand-copied per block (which silently absorbed shared-CI changes and drifted). Per-block bits are derived (`app-name`, `app-name-slug`); `team-id`, `test`, and the `@v4` pin are constants baked into the templates. Standalone blocks only — sdk-internal blocks (`etc/blocks/*`) get neither file. Engine cleanups enabling the above: - Thread the active RunContext into `tpl()`/`generate()` content lambdas, so they receive `ctx` as an argument (consistent with `when()` triggers) instead of reaching for the module-global accessor. All 16 call sites migrated; `getActiveRunContext()`/`blockVars()` stay for managed bodies. - Fix discovery deriving `blockVars.shortName` with a stray `.block` suffix on refresh (the block package is named `<facade>.block`): strip it so `ctx.blockVars` is correct on init and refresh alike. Workflow templates are excluded from oxfmt (verbatim payloads).
🦋 Changeset detectedLatest commit: 06ffd7b The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
Code Review
This pull request introduces centralized management of block CI workflows (build.yaml and mark-stable.yaml) as engine-owned, fixed scaffold files generated from the block's short name. It also refactors the template and generation engines to pass the active RunContext directly to lambdas instead of relying on a module-global getActiveRunContext(). Feedback highlights a potential bug in discovery-fs.ts where unconditionally stripping the .block suffix from a legacy block named *.block could result in an invalid facade name and cause parseBlockVars to throw an error.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const facadeName = blockMod.name.replace(/\.block$/, ""); | ||
| const blockVars = parseBlockVars(facadeName); |
There was a problem hiding this comment.
If a legacy block has the short name block (e.g., @platforma-open/test-org.block), its package name is @platforma-open/test-org.block. Unconditionally stripping .block will turn this into @platforma-open/test-org, which lacks a dot in the base name and will cause parseBlockVars to throw an error. We should only strip the .block suffix if the resulting base name still contains a dot (indicating it was indeed a <facade>.block suffix and not the actual short name block).
let facadeName = blockMod.name;
if (facadeName.endsWith(".block")) {
const stripped = facadeName.slice(0, -6);
const baseName = stripped.slice(stripped.indexOf("/") + 1);
if (baseName.includes(".")) {
facadeName = stripped;
}
}
const blockVars = parseBlockVars(facadeName);| // everywhere. No-op for legacy blocks whose block package IS the bare facade | ||
| // and for the root-as-block fallback. | ||
| const facadeName = blockMod.name.replace(/\.block$/, ""); | ||
| const blockVars = parseBlockVars(facadeName); |
There was a problem hiding this comment.
Suffix strip clobbers legacy block whose shortName is "block"
/\.block$/ strips the suffix from the bare-facade package name of a legacy block named @org/scope.block (shortName = "block"), reducing it to @org/scope — a package string with no dot in the base name, so parseBlockVars immediately throws "base name 'scope' has no '.' separating org-scope from short-name." on refresh. A structurer-shaped block is unaffected (its block package is @org/scope.block.block, strip → @org/scope.block is correct), but the legacy path is broken for this specific name. A test covering shortName = "block" with the legacy shape is missing from discovery-shortname.test.ts.
Prompt To Fix With AI
This is a comment left during a code review.
Path: tools/block-tools/src/structure/engine/discovery-fs.ts
Line: 204-207
Comment:
**Suffix strip clobbers legacy block whose shortName is "block"**
`/\.block$/` strips the suffix from the bare-facade package name of a legacy block named `@org/scope.block` (shortName = "block"), reducing it to `@org/scope` — a package string with no dot in the base name, so `parseBlockVars` immediately throws `"base name 'scope' has no '.' separating org-scope from short-name."` on refresh. A structurer-shaped block is unaffected (its block package is `@org/scope.block.block`, strip → `@org/scope.block` is correct), but the legacy path is broken for this specific name. A test covering `shortName = "block"` with the legacy shape is missing from `discovery-shortname.test.ts`.
How can I resolve this? If you propose a fix, please make it concise.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1705 +/- ##
===========================================
+ Coverage 44.56% 55.28% +10.71%
===========================================
Files 43 312 +269
Lines 2540 17508 +14968
Branches 663 3819 +3156
===========================================
+ Hits 1132 9679 +8547
- Misses 1225 6595 +5370
- Partials 183 1234 +1051 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…e two layers Complete the "execution-level lambdas receive ctx by argument" theme: `ManagedBody` is now `(ctx: RunContext) => void`, so `managed(...)` bodies get `ctx` the same way `generate`/`tpl` producers and `when` triggers do. The two rule bodies that read run state (`root-catalog-bump` software check, `block-package-json` block.components) now use the body's `ctx` arg instead of the module-global `getActiveRunContext()` — which is now absent from `rules/` entirely and stays as engine-internal plumbing. Document the composition-vs-execution mental model in the structurer CLAUDE.md: composition declares the tree at build time (no ctx); execution lambdas compute per-block values and each receives ctx by argument.
…dy-ctx refactor(block-tools): pass RunContext to managed bodies + document the two layers
…er CLAUDE.md Most managed bodies are zero-arg and rely on the builders they call to resolve ctx; reword so the invariant doesn't imply every body takes ctx as an argument.
What
Brings the block CI workflows under structurer management as engine-owned (
fixed) scaffold files, generated from the block's short name:.github/workflows/build.yamlandmark-stable.yamlare now generated and kept in sync onrefresh.@v4) and job wiring are centrally managed instead of hand-copied per block — which is how blocks silently absorbed a breaking shared-CI change and drifted.app-name(humanized short name),app-name-slug(block-<shortName>).team-id(ciplopen),test: true, and the@v4pin are constants baked into the templates.etc/blocks/*) get neither file (when(!isSdkInternal)).Engine cleanups (enabling the above)
tpl()/generate()content lambdas now receive the activeRunContextas an argument (consistent withwhen()triggers) instead of reaching for a module-global. All 16 call sites migrated.blockVars.shortNamewith a stray.blocksuffix on refresh (the block package is named<facade>.block); now stripped soctx.blockVarsis correct on init and refresh alike.Notes
.tpl.yamltemplates are excluded from oxfmt (verbatim payloads).@v4is intentionally left as-is; pinning to an immutable ref is a separate shared-CI decision.refreshper block),app-name-slugnormalizes toblock-<shortName>— this changes the release-artifact filename for ~14 blocks whose current slug differs (some are typo fixes, some deliberate). Worth a look before mass-refresh.Test
243 structure tests pass (incl. init→check parity, new
root-ci-workflowsanddiscovery-shortnametests);pnpm checkclean.Greptile Summary
This PR brings the per-block GitHub Actions CI workflows (
build.yaml,mark-stable.yaml) under structurer engine ownership asfixedscaffold files, and cleans up thegenerate()/tpl()content-lambda API so lambdas receiveRunContextas an explicit argument instead of pulling from a module-global.root-ci.ts,build.tpl.yaml,mark-stable.tpl.yaml): Two newfixedfiles are generated from the block'sshortNameon everyrefresh;app-nameandapp-name-slugare derived, all other fields (pin@v4,team-id, secrets wiring) are constants baked into the templates. Standalone blocks only —--sdk-internalblocks are excluded via the existingwhen(!isSdkInternal)guard.generate()/tpl()lambda migration (16 call sites): The lambdas now receive(ctx: RunContext)directly;getActiveRunContext()is retained only where it is the correct tool — inside managed-body lambdas, which do not receiveRunContextas a direct argument.discovery-fs.ts): Onrefresh/check, the block-scope package name<facade>.blockwould previously leak the.blocksuffix intoblockVars.shortName; stripping it makesshortNameconsistent betweeninitandrefresh.Confidence Score: 4/5
The PR is safe to merge; the engine refactor is mechanical and well-covered by 243 tests including the new init→check parity assertions.
The generate/tpl lambda migration is a clean, thoroughly tested mechanical refactor. The discovery fix and CI workflow generation are new, well-tested features. The one gap is that the
.blocksuffix strip in discovery-fs.ts would throw for a legacy block whose shortName happens to be "block" — but such a block is extraordinarily unlikely to exist in the ecosystem, making this a very low-probability regression.tools/block-tools/src/structure/engine/discovery-fs.ts and its companion test tools/block-tools/src/structure/tests/discovery-shortname.test.ts — the edge case of a legacy block with shortName "block" is untested.
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[engine.run / DISCOVERY] --> B[discoverRunContext] B --> C{blockMod.name ends with .block?} C -- yes --> D[Strip .block suffix] C -- no --> E[Name unchanged - legacy bare-facade] D --> F[parseBlockVars - blockVars.shortName] E --> F F --> G[RunContext with correct shortName] G --> H[engine.run with tpl/generate ctx lambdas] H --> I{isSdkInternal?} I -- no --> J[rootCiRules fired] I -- yes --> K[Skip CI workflow files] J --> L[tpl lambda receives ctx - Derives appName and appNameSlug] L --> M[substituteVars replaces placeholders - Leaves GitHub expressions untouched] M --> O[fixed files written on every refresh]%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% flowchart TD A[engine.run / DISCOVERY] --> B[discoverRunContext] B --> C{blockMod.name ends with .block?} C -- yes --> D[Strip .block suffix] C -- no --> E[Name unchanged - legacy bare-facade] D --> F[parseBlockVars - blockVars.shortName] E --> F F --> G[RunContext with correct shortName] G --> H[engine.run with tpl/generate ctx lambdas] H --> I{isSdkInternal?} I -- no --> J[rootCiRules fired] I -- yes --> K[Skip CI workflow files] J --> L[tpl lambda receives ctx - Derives appName and appNameSlug] L --> M[substituteVars replaces placeholders - Leaves GitHub expressions untouched] M --> O[fixed files written on every refresh]Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "feat(block-tools): manage block CI workf..." | Re-trigger Greptile