|
9 | 9 | description: 'CLI version to install (e.g. "latest", "0.4.1", "nightly")' |
10 | 10 | required: false |
11 | 11 | 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' |
12 | 16 | node-version: |
13 | 17 | description: 'Node.js version to install' |
14 | 18 | required: false |
@@ -69,20 +73,36 @@ runs: |
69 | 73 | with: |
70 | 74 | node-version: ${{ inputs.node-version }} |
71 | 75 |
|
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 | | -
|
83 | 76 | - name: Install B2C CLI |
84 | 77 | 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 |
86 | 106 |
|
87 | 107 | - name: Set environment variables |
88 | 108 | shell: bash |
@@ -134,14 +154,45 @@ runs: |
134 | 154 | - name: Install plugins |
135 | 155 | if: inputs.plugins != '' |
136 | 156 | shell: bash |
| 157 | + env: |
| 158 | + B2C_PLUGINS: ${{ inputs.plugins }} |
137 | 159 | 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 | +
|
138 | 167 | 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:]]}"}" |
140 | 173 | 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 |
143 | 194 | fi |
144 | | - done <<< "${{ inputs.plugins }}" |
| 195 | + done <<< "$B2C_PLUGINS" |
145 | 196 |
|
146 | 197 | - name: Verify installation |
147 | 198 | id: verify |
|
0 commit comments