Skip to content

Commit d19802f

Browse files
authored
fix(actions): reuse installed CLI in high-level actions; drop npm cache (#478)
The setup action unconditionally ran `npm install -g` on every invocation and cached `~/.npm` + the oclif data dir — neither of which contains the npm global prefix where the `b2c` binary lives. The cache therefore never let the install be skipped (it only saved download bandwidth) while growing to gigabytes on long-lived self-hosted runners, and high-level actions that call setup internally reinstalled the CLI a second time per pipeline. - Remove the actions/cache step. On self-hosted runners the global install already persists between jobs; on hosted runners a clean install is faster than restoring a multi-GB cache. - High-level actions (code-deploy, data-import, job-run, mrt-deploy, webdav-upload) pass skip-if-present=true so they reuse an already-installed CLI and install only when none is present — no redundant reinstall, no unexpected upgrade. The cold path installs the requested spec directly (npm resolves tags/ranges) with no registry probe that could fail offline. - setup called directly stays explicit: it always installs the requested version. New skip-if-present input (default false) opts into reuse. - Harden plugin install: parse `b2c plugins --json` for exact-name matching (grep -Fxq, no substring/regex collisions), read the plugins list from an env var instead of raw interpolation, and trim whitespace/CR in pure bash instead of xargs. Inputs/outputs are backward compatible; the only behavior change is that a wrapper action on a self-hosted runner with a pre-existing CLI reuses it.
1 parent 637df9e commit d19802f

9 files changed

Lines changed: 87 additions & 21 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@salesforce/b2c-dx-docs': patch
3+
---
4+
5+
Document the GitHub Actions install behavior change. The high-level actions (`code-deploy`, `data-import`, `job-run`, `mrt-deploy`, `webdav-upload`) now reuse an already-installed CLI and install one only when none is present — so a deploy that follows a setup step, and repeated operations on a persistent self-hosted runner, no longer trigger a redundant reinstall or an unexpected upgrade. The `setup` action called directly still installs the version you request (a new `skip-if-present` input opts into reuse). The actions no longer cache the npm download directory, which had grown to gigabytes on long-lived self-hosted runners and slowed restores. Plugin installs are skipped by exact name match.

actions/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ Install CLI plugins via the `plugins` input on the `setup` action (one per line)
113113
sfcc-solutions-share/b2c-plugin-intellij-sfcc-config
114114
```
115115

116-
Each entry is an npm package name or GitHub `owner/repo`. Plugins are cached alongside the CLI.
116+
Each entry is an npm package name or GitHub `owner/repo`. Already-installed plugins are skipped on re-invocation.
117117

118118
## Logging
119119

actions/code-deploy/action.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ runs:
7272
with:
7373
version: ${{ inputs.version }}
7474
node-version: ${{ inputs.node-version }}
75+
# Reuse an already-installed CLI; only install when none is present.
76+
skip-if-present: 'true'
7577
client-id: ${{ inputs.client-id }}
7678
client-secret: ${{ inputs.client-secret }}
7779
server: ${{ inputs.server }}

actions/data-import/action.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ runs:
6161
with:
6262
version: ${{ inputs.version }}
6363
node-version: ${{ inputs.node-version }}
64+
# Reuse an already-installed CLI; only install when none is present.
65+
skip-if-present: 'true'
6466
client-id: ${{ inputs.client-id }}
6567
client-secret: ${{ inputs.client-secret }}
6668
server: ${{ inputs.server }}

actions/job-run/action.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ runs:
5959
with:
6060
version: ${{ inputs.version }}
6161
node-version: ${{ inputs.node-version }}
62+
# Reuse an already-installed CLI; only install when none is present.
63+
skip-if-present: 'true'
6264
client-id: ${{ inputs.client-id }}
6365
client-secret: ${{ inputs.client-secret }}
6466
server: ${{ inputs.server }}

actions/mrt-deploy/action.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ runs:
5151
with:
5252
version: ${{ inputs.version }}
5353
node-version: ${{ inputs.node-version }}
54+
# Reuse an already-installed CLI; only install when none is present.
55+
skip-if-present: 'true'
5456
mrt-api-key: ${{ inputs.mrt-api-key }}
5557
mrt-project: ${{ inputs.project }}
5658
mrt-environment: ${{ inputs.environment }}

actions/setup/action.yml

Lines changed: 67 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ inputs:
99
description: 'CLI version to install (e.g. "latest", "0.4.1", "nightly")'
1010
required: false
1111
default: 'latest'
12+
skip-if-present:
13+
description: 'Reuse an already-installed CLI instead of installing. When true, the CLI is installed only if none is present (no version check, no upgrade). High-level actions set this so they reuse an existing install; a direct setup call leaves it false and always installs the requested version.'
14+
required: false
15+
default: 'false'
1216
node-version:
1317
description: 'Node.js version to install'
1418
required: false
@@ -69,20 +73,36 @@ runs:
6973
with:
7074
node-version: ${{ inputs.node-version }}
7175

72-
- name: Cache npm and CLI data
73-
uses: actions/cache@v4
74-
with:
75-
path: |
76-
~/.npm
77-
~/.local/share/b2c
78-
key: b2c-cli-${{ runner.os }}-${{ inputs.version }}-${{ inputs.plugins || 'no-plugins' }}
79-
restore-keys: |
80-
b2c-cli-${{ runner.os }}-${{ inputs.version }}-
81-
b2c-cli-${{ runner.os }}-
82-
8376
- name: Install B2C CLI
8477
shell: bash
85-
run: npm install -g @salesforce/b2c-cli@${{ inputs.version }}
78+
env:
79+
B2C_VERSION: ${{ inputs.version }}
80+
B2C_SKIP_IF_PRESENT: ${{ inputs.skip-if-present }}
81+
run: |
82+
REQUESTED="${B2C_VERSION:-latest}"
83+
84+
# Is a usable CLI already on PATH? (handles re-invocation within a job and
85+
# persistent self-hosted runners where the install survives between jobs)
86+
INSTALLED=""
87+
if command -v b2c >/dev/null 2>&1; then
88+
# Token anchored to the package name so it can never capture the node version
89+
# from "@salesforce/b2c-cli/<ver> <platform> node-v<ver>".
90+
INSTALLED=$(b2c --version 2>/dev/null | grep -oE '@salesforce/b2c-cli/[^[:space:]]+' | head -n1 | cut -d/ -f3 || true)
91+
fi
92+
93+
if [ "$B2C_SKIP_IF_PRESENT" = "true" ] && [ -n "$INSTALLED" ]; then
94+
# "Ensure a CLI is available" mode (used by high-level actions): reuse whatever
95+
# is installed; never upgrade and never hit the network. This is what makes a
96+
# deploy/import that follows a setup step a no-op instead of a reinstall.
97+
echo "B2C CLI ${INSTALLED} already present; reusing it (skip-if-present)"
98+
else
99+
# Calling setup directly is an explicit request to install the requested
100+
# version. Install the spec directly — npm resolves tags ("latest"/"nightly")
101+
# and ranges itself, so no registry probe is needed, keeping the path fast and
102+
# free of a network call that could fail the step.
103+
echo "Installing @salesforce/b2c-cli@${REQUESTED} (currently: ${INSTALLED:-none})"
104+
npm install -g "@salesforce/b2c-cli@${REQUESTED}"
105+
fi
86106
87107
- name: Set environment variables
88108
shell: bash
@@ -134,14 +154,45 @@ runs:
134154
- name: Install plugins
135155
if: inputs.plugins != ''
136156
shell: bash
157+
env:
158+
B2C_PLUGINS: ${{ inputs.plugins }}
137159
run: |
160+
# Installed plugin names, one per line, exactly as the CLI records them
161+
# (the published package name — not the GitHub repo slug). Parsed from --json
162+
# with node (always present; setup-node ran first) so the match is exact.
163+
INSTALLED_PLUGINS=$(b2c plugins --json 2>/dev/null \
164+
| node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{for(const p of JSON.parse(s))if(p&&p.name)console.log(p.name)}catch(e){}})' \
165+
|| true)
166+
138167
while IFS= read -r plugin; do
139-
plugin=$(echo "$plugin" | xargs)
168+
# Trim surrounding whitespace (incl. a trailing CR from CRLF-encoded YAML)
169+
# with pure bash — avoids `xargs`, which aborts the step on an unbalanced
170+
# quote and mangles backslashes.
171+
plugin="${plugin#"${plugin%%[![:space:]]*}"}"
172+
plugin="${plugin%"${plugin##*[![:space:]]}"}"
140173
if [ -n "$plugin" ]; then
141-
echo "Installing plugin: $plugin"
142-
b2c plugins install "$plugin"
174+
# Determine the name to match against installed plugins. npm specs (scoped
175+
# "@scope/name" or bare "name") are recorded verbatim. For a GitHub
176+
# "owner/repo" spec the published package name is unknown ahead of time, so
177+
# fall back to the repo basename — which equals the package name for the
178+
# common case. Exact, fixed-string line match (grep -Fxq) avoids both
179+
# substring false-positives and regex-metacharacter surprises.
180+
spec="${plugin%/}" # drop a trailing slash before deriving the name
181+
if [ "${spec:0:1}" = "@" ] || [ "${spec%/*}" = "$spec" ]; then
182+
name="$spec" # scoped npm name, or bare npm name (no slash)
183+
else
184+
name="${spec##*/}" # GitHub owner/repo -> repo basename
185+
fi
186+
# Only skip on a real, non-empty exact match; an empty name must never
187+
# match the (possibly empty) installed list and silently skip the install.
188+
if [ -n "$name" ] && printf '%s\n' "$INSTALLED_PLUGINS" | grep -Fxq "$name"; then
189+
echo "Plugin already installed: $plugin"
190+
else
191+
echo "Installing plugin: $plugin"
192+
b2c plugins install "$plugin"
193+
fi
143194
fi
144-
done <<< "${{ inputs.plugins }}"
195+
done <<< "$B2C_PLUGINS"
145196
146197
- name: Verify installation
147198
id: verify

actions/webdav-upload/action.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ runs:
5757
with:
5858
version: ${{ inputs.version }}
5959
node-version: ${{ inputs.node-version }}
60+
# Reuse an already-installed CLI; only install when none is present.
61+
skip-if-present: 'true'
6062
client-id: ${{ inputs.client-id }}
6163
client-secret: ${{ inputs.client-secret }}
6264
server: ${{ inputs.server }}

docs/guide/ci-cd.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ The actions are available from the `SalesforceCommerceCloud/b2c-developer-toolin
1919
- **WebDAV uploads** — upload files for data import, content, etc.
2020
- **Any CLI command** — raw passthrough for operations not covered by high-level actions
2121

22-
All actions are composite YAML — no compiled JavaScript, fully transparent and auditable. They cache the npm install across runs and expose structured JSON outputs for downstream workflow steps.
22+
All actions are composite YAML — no compiled JavaScript, fully transparent and auditable. The high-level actions (`code-deploy`, `data-import`, `job-run`, `mrt-deploy`, `webdav-upload`) reuse an already-installed CLI and only install one when none is present — so running a deploy after a setup step, or repeated operations on a self-hosted runner, do not trigger a redundant reinstall. The `setup` action called directly is explicit: it installs the version you request. Actions expose structured JSON outputs for downstream workflow steps.
2323

2424
## Authentication
2525

@@ -114,7 +114,7 @@ Combines setup and command execution. Pass a `command` to run a CLI command, or
114114
uses: SalesforceCommerceCloud/b2c-developer-tooling/actions/setup@v1
115115
```
116116
117-
Installs the CLI and writes credentials to environment variables. Use this when you need multiple steps after setup.
117+
Installs the CLI and writes credentials to environment variables. Use this when you need multiple steps after setup. Called directly, `setup` always installs the requested `version`. Set `skip-if-present: 'true'` to reuse an already-installed CLI and install only when none is present (this is what the high-level actions do internally so they never reinstall on top of an existing CLI).
118118
119119
```yaml
120120
- uses: SalesforceCommerceCloud/b2c-developer-tooling/actions/setup@v1
@@ -127,7 +127,7 @@ Installs the CLI and writes credentials to environment variables. Use this when
127127
sfcc-solutions-share/b2c-plugin-intellij-sfcc-config
128128
```
129129

130-
Plugins are installed after the CLI and cached across runs. Each line is an npm package name or GitHub `owner/repo`.
130+
Plugins are installed after the CLI; already-installed plugins are skipped by exact name match. Each line is an npm package name or GitHub `owner/repo`. For reliable skip-on-reinstall, prefer the published npm package name — when a GitHub `owner/repo` slug differs from the package it publishes, the plugin is reinstalled each run (harmless, just slower).
131131

132132
### Run
133133

@@ -414,7 +414,7 @@ The CLI supports [plugins](/guide/extending) for custom configuration sources, H
414414
sfcc-solutions-share/b2c-plugin-intellij-sfcc-config
415415
```
416416

417-
Each line is an npm package name or GitHub `owner/repo`. Plugins are installed after the CLI and cached across workflow runs.
417+
Each line is an npm package name or GitHub `owner/repo`. Plugins are installed after the CLI; already-installed plugins are skipped on re-invocation.
418418

419419
## Logging
420420

0 commit comments

Comments
 (0)