Skip to content

feat(block-tools): manage block CI workflows in the structurer#1705

Merged
dbolotin merged 4 commits into
mainfrom
feat/structurer-ci-scaffold
Jun 18, 2026
Merged

feat(block-tools): manage block CI workflows in the structurer#1705
dbolotin merged 4 commits into
mainfrom
feat/structurer-ci-scaffold

Conversation

@dbolotin

@dbolotin dbolotin commented Jun 18, 2026

Copy link
Copy Markdown
Member

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.yaml and mark-stable.yaml are now generated and kept in sync on refresh.
  • The shared reusable-workflow pin (@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.
  • Per-block bits are derived: app-name (humanized short name), app-name-slug (block-<shortName>). team-id (ciplopen), test: true, and the @v4 pin are constants baked into the templates.
  • Standalone blocks only — sdk-internal blocks (etc/blocks/*) get neither file (when(!isSdkInternal)).

Engine cleanups (enabling the above)

  • tpl()/generate() content lambdas now receive the active RunContext as an argument (consistent with when() triggers) instead of reaching for a module-global. All 16 call sites migrated.
  • Discovery derived blockVars.shortName with a stray .block suffix on refresh (the block package is named <facade>.block); now stripped so ctx.blockVars is correct on init and refresh alike.

Notes

  • Workflow .tpl.yaml templates are excluded from oxfmt (verbatim payloads).
  • @v4 is intentionally left as-is; pinning to an immutable ref is a separate shared-CI decision.
  • On rollout (refresh per block), app-name-slug normalizes to block-<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-workflows and discovery-shortname tests); pnpm check clean.

Greptile Summary

This PR brings the per-block GitHub Actions CI workflows (build.yaml, mark-stable.yaml) under structurer engine ownership as fixed scaffold files, and cleans up the generate()/tpl() content-lambda API so lambdas receive RunContext as an explicit argument instead of pulling from a module-global.

  • CI workflow generation (root-ci.ts, build.tpl.yaml, mark-stable.tpl.yaml): Two new fixed files are generated from the block's shortName on every refresh; app-name and app-name-slug are derived, all other fields (pin @v4, team-id, secrets wiring) are constants baked into the templates. Standalone blocks only — --sdk-internal blocks are excluded via the existing when(!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 receive RunContext as a direct argument.
  • Discovery bug fix (discovery-fs.ts): On refresh/check, the block-scope package name <facade>.block would previously leak the .block suffix into blockVars.shortName; stripping it makes shortName consistent between init and refresh.

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 .block suffix 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

Filename Overview
tools/block-tools/src/structure/engine/discovery-fs.ts Adds .block suffix stripping when deriving BlockVars from the block-scope package name on refresh/check. Correctly handles the structurer-shaped .block package name and is a no-op for bare-facade legacy blocks — except for the untested edge case where a legacy block's shortName is literally "block".
tools/block-tools/src/structure/rules/root-ci.ts New rule file registering fixed CI workflow files (.github/workflows/build.yaml and mark-stable.yaml) for standalone (non-sdk-internal) blocks. Correctly gated behind when(!isSdkInternal), uses tpl() with ctx-receiving lambdas, and humanizes the shortName consistently.
tools/block-tools/src/structure/engine/runner.ts Propagates full RunContext to resolveContent/writeLeafContent/applyStructural/applyManaged instead of extracting isSdkInternal, enabling tpl/generate lambdas to receive ctx directly. Clean mechanical refactor with no logic changes.
tools/block-tools/src/structure/engine/builders.ts Updates tpl() and generate() signatures to accept (ctx: RunContext) callbacks. getActiveRunContext() and blockVars() are preserved for managed-body use cases that cannot receive ctx as a direct argument.
tools/block-tools/src/structure/templates/text/workflows/build.tpl.yaml New CI build workflow template. Uses ${appName}/${appNameSlug} placeholders for per-block identity; ${{ ... }} GitHub expressions are unaffected by substituteVars (double-brace pattern is excluded by the regex). Verified by test assertions.
tools/block-tools/src/structure/templates/text/workflows/mark-stable.tpl.yaml New mark-stable workflow template. Only substitutes ${appName}; simpler than build.yaml. Consistent structure with the build template.
tools/block-tools/src/structure/tests/root-ci-workflows.test.ts New test covering CI workflow generation, content correctness, fixed idempotency (init→check zero-diff), and sdk-internal exclusion. Good coverage of the happy path and the guard condition.
tools/block-tools/src/structure/tests/discovery-shortname.test.ts Tests the .block suffix stripping for a structurer-shaped block and the no-op path for a legacy block. Missing coverage for the edge case where a legacy block's shortName is "block".

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]
Loading
%%{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]
Loading

Fix All in Claude Code

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
tools/block-tools/src/structure/engine/discovery-fs.ts:204-207
**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`.

Reviews (1): Last reviewed commit: "feat(block-tools): manage block CI workf..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

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-bot

changeset-bot Bot commented Jun 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 06ffd7b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@platforma-sdk/block-tools Minor
@milaboratories/pl-middle-layer Patch
@platforma-sdk/pl-cli Patch
@platforma-sdk/test Patch

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

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +206 to +207
const facadeName = blockMod.name.replace(/\.block$/, "");
const blockVars = parseBlockVars(facadeName);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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);

Comment on lines +204 to +207
// 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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Fix in Claude Code

@codecov

codecov Bot commented Jun 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.23810% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 55.28%. Comparing base (79dc4bd) to head (06ffd7b).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
tools/block-tools/src/structure/engine/runner.ts 88.88% 0 Missing and 1 partial ⚠️
...ock-tools/src/structure/rules/root-catalog-bump.ts 50.00% 1 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…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.
dbolotin added 2 commits June 18, 2026 11:52
…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.
@dbolotin dbolotin requested review from AStaroverov and mzueva June 18, 2026 10:07
@dbolotin dbolotin added this pull request to the merge queue Jun 18, 2026
Merged via the queue into main with commit ae44f36 Jun 18, 2026
14 checks passed
@dbolotin dbolotin deleted the feat/structurer-ci-scaffold branch June 18, 2026 11:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants