Skip to content

feat: add useLocalCli input to run the project-local CLI - #11

Open
AntoineMontane wants to merge 1 commit into
cdktn-io:mainfrom
AntoineMontane:feat/use-local-cli
Open

AntoineMontane wants to merge 1 commit into
cdktn-io:mainfrom
AntoineMontane:feat/use-local-cli

Conversation

@AntoineMontane

Copy link
Copy Markdown

Adds a useLocalCli input. When it's true, the action runs the cdktn CLI the project already has in node_modules (npx --no cdktn …) instead of fetching cdktn-cli@<cdktnVersion> with npx --yes.

Why: projects that already depend on cdktn-cli usually want CI to run the same version as local runs and their lockfile, kept up to date by Dependabot or Renovate, with no extra package download per run. hashicorp/terraform-cdk-action had no way to do that. Some forks added a similar flag, for example useLocalCdktf.

Behaviour:

  • npx --no makes the step fail if cdktn isn't installed, rather than silently downloading some other version. The dependency install has to run before this step.
  • No version input is required in this mode. If cdktnVersion or cdktfVersion is also set, it's ignored, with a warning.
  • customNpxArgs and cdktnArgs are still passed as before.
  • Default false, so existing workflows behave the same.

Changes:

  • the input, in action.yml, models.ts and inputs.ts;
  • the argv building in buildNpxArgs;
  • a README row;
  • tests (3 new);
  • rebuilt dist/.

npm run all passes: check, typecheck, 35 tests and the bundle.

🤖 Generated with Claude Code

@AntoineMontane
AntoineMontane requested a review from a team as a code owner October 24, 2026 07:37
Projects that already depend on cdktn-cli can set `useLocalCli: true` to
run that CLI from node_modules via `npx --no cdktn` instead of fetching a
version with npx. The CLI version then follows the project's lockfile, in
sync with local runs and dependency updates, and no extra package is
downloaded per run.

`--no` makes npx fail rather than silently download a CLI when it isn't
installed. No version input is required in this mode; if one is set, it
is ignored with a warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@so0k
so0k force-pushed the feat/use-local-cli branch from e3b265f to 5d54b90 Compare October 26, 2026 15:30

@so0k so0k left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for this, running the project's own CLI is a good fit for lockfile and Renovate workflows. #12 merged first and touched the same buildNpxArgs and test file, so I rebased this branch onto main for you (5d54b90), keeping both features. With useLocalCli, an apply now runs npx --no cdktn apply <stack> --auto-approve --outputs-file <file>. dist/ was rebuilt after npm ci, and npm run all passes (41 tests). Please pull before pushing again (git fetch && git reset --hard origin/feat/use-local-cli).

A few things need changing before we merge; details are in the inline comments:

  1. Legacy cdktf-cli projects can't use useLocalCli. npx --no cdktn looks for a cdktn command, but cdktf-cli@0.21.0 installs only a cdktf command and the cdktn library package installs none. Such projects fail with npm error could not determine executable to run.
  2. The warnings are confusing in local mode. The ignored input isn't named, and the cdktf deprecation and "using cdktnVersion" warnings still fire for inputs local mode ignores.
  3. The docs overstate "project-local". If no local CLI is installed, npx falls back to a globally installed one, because --no only blocks downloads. Also, a --yes in customNpxArgs re-enables downloads.
  4. Tests. We reviewed them against our test-audit guidelines (.claude/skills/test-audit/SKILL.md):
    • Keep "runs the project-local cdktn without fetching it".
    • Fold the other two into it as a table-driven local-mode test (one mostly duplicates an existing test, and the other locks in point 1).
    • Add the cases that aren't covered: getInputs() accepting useLocalCli with no version input, the new warnings, and local mode passing --outputs-file on apply.

Comment thread src/action.ts
args.push(...splitArgs(inputs.customNpxArgs))
args.push(`${cli.pkg}@${cli.version}`)
if (inputs.useLocalCli) {
args.push('cdktn')

@so0k so0k Oct 26, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This always runs the cdktn command, which only cdktn-cli installs. cdktf-cli@0.21.0 installs only cdktf, and the cdktn library package installs none. So a project still on the legacy CLI (useLocalCli: true with cdktfVersion) fails with npm error could not determine executable to run.

Passing the package name instead works: npx --no <pkg> runs that package's command from the local node_modules, including a hoisted root node_modules when run from a subdirectory. I checked this with npx 11.16 for both cdktn-cli and cdktf-cli. You can't reuse resolveCli(inputs).pkg as is, though: with no version input it returns cdktf-cli. Something like this:

// The deprecated cdktfVersion still selects the legacy package; otherwise the project's cdktn-cli runs.
const pkg = inputs.cdktfVersion && !inputs.cdktnVersion ? 'cdktf-cli' : 'cdktn-cli'
args.push(pkg)

The version inputs would then choose which local package runs rather than being ignored, so the docs and warning need to match.

If you'd rather guarantee the CLI really is project-local, an alternative is to skip npx and resolve the package yourself: createRequire(join(workingDirectory, 'package.json')).resolve(`${pkg}/package.json`), then run its bin entry with node. That rules out the global fallback and lets the action fail early with a clear message naming the missing package. It's more code than npx --no <pkg>, so either is fine with us.

Comment thread src/inputs.ts
@@ -55,8 +55,16 @@ export function getInputs(): ActionInputs {

const cdktnVersion = core.getInput('cdktnVersion')
const cdktfVersion = core.getInput('cdktfVersion')

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

None of the getInputs() changes here are tested yet: accepting useLocalCli with no version input, and the new warning. The run() tests from #12 mock core.getInput and core.getBooleanInput, so a case there can assert both that run() doesn't throw without a version and what core.warning was called with.

Comment thread src/inputs.ts
throw new Error(
'Provide a "cdktnVersion" (or the deprecated "cdktfVersion") input, or set "useLocalCli".',
)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Two things in local mode:

  • This warning doesn't say which input is ignored. Name it, e.g. "cdktfVersion" is ignored because "useLocalCli" is set., or change the wording to match whatever selection rule you adopt in buildNpxArgs.
  • The checks below still run, so a local-mode user also gets "cdktfVersion is deprecated" or "using cdktnVersion" for inputs the action doesn't use. Skip them when useLocalCli is set.

Comment thread action.yml
required: false
useLocalCli:
description: >-
Run the CLI installed in the project's node_modules (`npx --no cdktn`)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Two small accuracy points for this description and the README row:

  • npx --no only blocks downloads. If no local CLI is installed, npx falls back to a globally installed one rather than failing, so "comes from the project's lockfile" isn't guaranteed. One sentence noting this is enough, unless you resolve the package explicitly as suggested on src/action.ts.
  • A --yes in customNpxArgs comes after --no and re-enables downloads. A short note is enough.

The npx --no cdktn text will also need updating if the command changes for cdktf projects.

Comment thread __tests__/action.test.ts
expect(args).toEqual(['--no', 'cdktn', 'plan', 'dev'])
})

it('ignores version inputs when running the local CLI', () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The name promises "ignored, with a warning", but the warning lives in getInputs() and isn't asserted here. The test also makes cdktfVersion + local mode → cdktn the intended behaviour, which is the cdktf gap on src/action.ts. I'd fold it into a table-driven local-mode test, with a row for the legacy package once the selection rule is settled.

Comment thread __tests__/action.test.ts
expect(args).toEqual(['--no', 'cdktn', 'synth'])
})

it('threads custom npx args before the local CLI', () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This mostly repeats "threads custom npx and cdktn args": splitArgs(customNpxArgs) runs before the local/remote branch, so this only catches a refactor that moves the push into one branch. It's cheaper as a row in the same table-driven local-mode test, and that table should also have a row for local mode + apply passing --outputs-file, which nothing covers yet.

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