v0.20.0 — distribution: api --paginate, Docker/GHCR, brew+Scoop, CI recipe#25
Conversation
- api --paginate (alias --all): the host-locked passthrough now follows Pipedrive pagination itself. GET-only (else exit 64); pager inferred from /api/v1/ vs /api/v2/; querystring on the path seeds page 1; --limit caps total items and short-circuits fetching further pages. - Dockerfile (node:20-slim, env-var auth) + a docker job in release.yml that pushes multi-arch amd64+arm64 images to ghcr.io/wavyx/pdcli after the npm publish. - scripts/gen-dist.mjs renders a Homebrew formula and Scoop manifest from the published tarball (run per release; outputs gitignored). - Distribution + GitHub Actions docs pages; README install section. Claude-Session: https://claude.ai/code/session_018dRMUUj9vpKp3tqKQFsvE9
api --paginate: - single-record GET no longer crashes as internal exit 70; a non-list endpoint yields a clean usage error (64). - repeated querystring keys (?ids=1&ids=2) are preserved, not collapsed. - --jq shape difference (bare array vs envelope) documented on the flag. Global hardening: - --jq can no longer hang: node-jq's run() is raced against a timeout and any failure (incl. a missing jq binary on npm>=11) becomes a clear exit 69, not an indefinite hang. - --limit is validated to >= 1 (exit 64) instead of silently treating 0 as 'all pages'. Release infra: - Docker image version derives from package.json (single source), and :latest is skipped for prerelease tags so RCs cannot poison it. - Scoop autoupdate rewrites the version inside installer.script. - distribution docs point at the Releases page / gh release download, since oclif embeds the commit sha in tarball names (no clean URL). Claude-Session: https://claude.ai/code/session_018dRMUUj9vpKp3tqKQFsvE9
- --jq: only the HANG (missing binary) maps to 'unavailable' (exit 69); a genuine node-jq rejection is a real filter error and now surfaces jq's own diagnostic with the prior exit code, instead of a misleading 'reinstall jq'. Distinguished via a timeout sentinel. - PDCLI_JQ_TIMEOUT_MS: a non-positive/non-finite override falls back to the default (a negative value no longer immediately times out). - release.yml: prerelease tags publish to the npm 'next' dist-tag (not 'latest', which npm i and the Scoop checkver read) and are marked --prerelease on GitHub, matching the docker :latest gate. Claude-Session: https://claude.ai/code/session_018dRMUUj9vpKp3tqKQFsvE9
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
| RUN npm install -g "@wavyx/pdcli@${PDCLI_VERSION}" \ | ||
| && npm cache clean --force | ||
|
|
||
| ENTRYPOINT ["pdcli"] |
There was a problem hiding this comment.
The container runs as root because no
USER instruction is present. node:20-slim ships a node user (uid 1000) — switching to it after the global install is the standard Dockerfile hardening step for CLI images.
| RUN npm install -g "@wavyx/pdcli@${PDCLI_VERSION}" \ | |
| && npm cache clean --force | |
| ENTRYPOINT ["pdcli"] | |
| RUN npm install -g "@wavyx/pdcli@${PDCLI_VERSION}" \ | |
| && npm cache clean --force | |
| USER node | |
| ENTRYPOINT ["pdcli"] |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| export async function fetchDist(version, fetchFn = fetch) { | ||
| const meta = await fetchFn( | ||
| `https://registry.npmjs.org/${PKG}/${version}`, | ||
| ).then((r) => r.json()) | ||
| const url = meta.dist.tarball |
There was a problem hiding this comment.
fetchDist doesn't check the HTTP response status before accessing meta.dist.tarball. If the version doesn't yet exist on npm (or a typo is passed), the registry returns {"error":"version not found"}, meta.dist is undefined, and the crash is an opaque TypeError: Cannot read properties of undefined (reading 'tarball') — with no hint that the version is wrong.
| export async function fetchDist(version, fetchFn = fetch) { | |
| const meta = await fetchFn( | |
| `https://registry.npmjs.org/${PKG}/${version}`, | |
| ).then((r) => r.json()) | |
| const url = meta.dist.tarball | |
| export async function fetchDist(version, fetchFn = fetch) { | |
| const res = await fetchFn(`https://registry.npmjs.org/${PKG}/${version}`) | |
| if (!res.ok) { | |
| throw new Error(`npm registry returned ${res.status} for ${PKG}@${version}`) | |
| } | |
| const meta = await res.json() | |
| if (meta.error) throw new Error(`npm registry error: ${meta.error}`) | |
| const url = meta.dist.tarball |
| --notes "${NOTES:-Release $GITHUB_REF_NAME}" \ | ||
| dist/*.tar.gz | ||
|
|
||
| docker: | ||
| name: Publish Docker image | ||
| needs: release # build from the just-published npm version | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v5 | ||
|
|
||
| # Single source of truth: read the version from the SAME package.json the | ||
| # npm publish step used, so the Docker image installs a version that exists | ||
| # on npm (deriving from the git tag can drift from package.json and 404). | ||
| # Only stable releases get :latest — a prerelease (0.20.0-rc1) must not | ||
| # poison the :latest tag that `docker pull` resolves by default. | ||
| - name: Resolve version | ||
| id: ver | ||
| run: | | ||
| V=$(node -p "require('./package.json').version") | ||
| echo "v=$V" >> "$GITHUB_OUTPUT" | ||
| case "$V" in | ||
| *-*) echo "latest=" >> "$GITHUB_OUTPUT" ;; # prerelease: no :latest | ||
| *) echo "latest=ghcr.io/${{ github.repository }}:latest" >> "$GITHUB_OUTPUT" ;; | ||
| esac | ||
|
|
||
| - uses: docker/setup-qemu-action@v4 # cross-build arm64 | ||
|
|
||
| - uses: docker/setup-buildx-action@v4 | ||
|
|
||
| - uses: docker/login-action@v4 | ||
| with: | ||
| registry: ghcr.io | ||
| username: ${{ github.actor }} | ||
| password: ${{ secrets.GITHUB_TOKEN }} | ||
|
|
||
| - uses: docker/build-push-action@v7 | ||
| with: | ||
| context: . | ||
| push: true | ||
| platforms: linux/amd64,linux/arm64 | ||
| build-args: PDCLI_VERSION=${{ steps.ver.outputs.v }} | ||
| # The :latest line is empty for prereleases (see Resolve version); | ||
| # build-push-action ignores blank tag lines. | ||
| tags: | | ||
| ghcr.io/${{ github.repository }}:${{ steps.ver.outputs.v }} | ||
| ${{ steps.ver.outputs.latest }} |
There was a problem hiding this comment.
GitHub Actions pinned to mutable major-version tags
All five actions in the docker job (actions/checkout@v5, docker/setup-qemu-action@v4, docker/setup-buildx-action@v4, docker/login-action@v4, docker/build-push-action@v7) use mutable major-version tags. A compromised tag can silently redirect to a malicious commit on the next run; SHA pinning (e.g. docker/login-action@65d2b36e4f3f) eliminates that class of supply-chain risk for a workflow that also holds packages: write and id-token: write permissions.
Second pre-v1 release (distribution). Makes pdcli installable everywhere and lets the raw passthrough follow pagination.
Added
Changed
Quality gate
https://claude.ai/code/session_018dRMUUj9vpKp3tqKQFsvE9
Greptile Summary
This release adds
pdcli api --paginate(cursor and offset paging for v1/v2 paths), hardens--jqagainst a missing jq binary (timeout guard → exit 69), enforces--limit ≥ 1, and ships distribution infrastructure: Docker/GHCR multi-arch image, Homebrew formula and Scoop manifest generators, and a CI recipe doc.src/commands/api.js,src/lib/pagination.js):runPaginatedcorrectly strips querystrings from the path, infers the pager from/api/v1/or/api/v2/, catchesTypeErrorfrom single-record endpoints and converts it to exit 64, and respects--limitfor early exit.src/base-command.js):runJqracesnode-jq'srun()against a configurable timeout; timeout → exit 69, real jq rejection → surfaces unchanged;timer.unref()prevents the timeout from keeping the event loop alive.Dockerfile,release.yml,scripts/gen-dist.mjs): The Docker image installs the published npm package and is built only after a successful npm publish, correctly skipping:latestfor prerelease tags.Confidence Score: 4/5
The core logic changes are sound and well-tested; the distribution additions are straightforward with minor hardening gaps.
The pagination implementation, jq guard, and --limit enforcement are all correct and comprehensively tested. The distribution layer works as written but has three non-blocking gaps: the Docker image runs as root, fetchDist gives an opaque error on a 404, and the Docker workflow's actions are pinned to mutable major version tags rather than SHAs. None affect runtime correctness.
Dockerfile (missing USER instruction) and .github/workflows/release.yml (action SHA pinning in the docker job).
Security Review
USERinstruction; thenodeuser from the base image should be set post-install.dockerworkflow job uses major-version tags (@v4,@v7) on five actions in a workflow withpackages: writeandid-token: writepermissions — SHA pinning would close the supply-chain window.Important Files Changed
Reviews (1): Last reviewed commit: "chore: release v0.20.0" | Re-trigger Greptile