Repository navigation
v0.20.0 — distribution: api --paginate, Docker/GHCR, brew+Scoop, CI recipe #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
dbe8235
743f91d
af77fcb
6ecb1b4
2709260
08b7dfe
9cb11c2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,20 @@ | ||||||||||||||||||||||
| # Run pdcli without a local Node install (env-var auth — a container has no OS | ||||||||||||||||||||||
| # keychain, so pass the token/domain as environment variables): | ||||||||||||||||||||||
| # docker run --rm -e PDCLI_API_TOKEN -e PDCLI_COMPANY_DOMAIN \ | ||||||||||||||||||||||
| # ghcr.io/wavyx/pdcli deal list --output json | ||||||||||||||||||||||
| # | ||||||||||||||||||||||
| # The image installs the published npm package, so build it after publishing | ||||||||||||||||||||||
| # (the release workflow passes the released version via PDCLI_VERSION). | ||||||||||||||||||||||
| FROM node:20-slim | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| ARG PDCLI_VERSION=latest | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| LABEL org.opencontainers.image.source="https://github.com/wavyx/pdcli" | ||||||||||||||||||||||
| LABEL org.opencontainers.image.description="Command-line interface for Pipedrive" | ||||||||||||||||||||||
| LABEL org.opencontainers.image.licenses="MIT" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| RUN npm install -g "@wavyx/pdcli@${PDCLI_VERSION}" \ | ||||||||||||||||||||||
| && npm cache clean --force | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| ENTRYPOINT ["pdcli"] | ||||||||||||||||||||||
|
Comment on lines
+16
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
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! |
||||||||||||||||||||||
| CMD ["--help"] | ||||||||||||||||||||||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| # Packaging | ||
|
|
||
| pdcli ships through several channels. npm is the source of truth; the others | ||
| are generated from the published tarball. | ||
|
|
||
| | Channel | Install | Source | | ||
| | -------- | ------------------------------------------------------------------------------------ | ------------------------------ | | ||
| | npm | `npm install -g @wavyx/pdcli` | this repo (`npm publish`) | | ||
| | Docker | `docker run --rm ghcr.io/wavyx/pdcli --help` | `Dockerfile` (release.yml) | | ||
| | Homebrew | `brew tap wavyx/tap && brew install pdcli` | `wavyx/homebrew-tap` (formula) | | ||
| | Scoop | `scoop bucket add pdcli https://github.com/wavyx/scoop-pdcli && scoop install pdcli` | `wavyx/scoop-pdcli` (manifest) | | ||
|
|
||
| ## Updating Homebrew + Scoop on release | ||
|
|
||
| After a version is published to npm: | ||
|
|
||
| ```bash | ||
| node scripts/gen-dist.mjs <version> # e.g. 0.19.0 | ||
| ``` | ||
|
|
||
| This downloads the npm tarball, computes its sha256, and writes: | ||
|
|
||
| - `packaging/homebrew/pdcli.rb` → commit to `wavyx/homebrew-tap` as `Formula/pdcli.rb` | ||
| - `packaging/scoop/pdcli.json` → commit to `wavyx/scoop-pdcli` as `bucket/pdcli.json` | ||
|
|
||
| Both generated files are git-ignored here — they live in their own repos. | ||
|
|
||
| > Auto-bumping these on every release would need a cross-repo token (PAT); for | ||
| > now it's a one-line manual step. The Docker image and npm publish are fully | ||
| > automated in `.github/workflows/release.yml`. | ||
|
|
||
| ## Container security caveat (env-var tokens) | ||
|
|
||
| The container has no OS keychain, so `pdcli` in Docker authenticates from | ||
| environment variables — `PDCLI_API_TOKEN` and `PDCLI_COMPANY_DOMAIN`: | ||
|
|
||
| ```bash | ||
| docker run --rm -e PDCLI_API_TOKEN -e PDCLI_COMPANY_DOMAIN \ | ||
| ghcr.io/wavyx/pdcli deal list --output json | ||
| ``` | ||
|
|
||
| Env-var tokens are **plaintext** — that is fine for an ephemeral CI runner that | ||
| holds the secret only for the job, but on a desktop the OS keychain is the whole | ||
| point of pdcli's security posture, so prefer the npm/Homebrew/Scoop installs and | ||
| `pdcli auth login` there. Two rules regardless of host: | ||
|
|
||
| - **Never bake a token into an image layer** (no `ENV PDCLI_API_TOKEN=…`, no | ||
| `--build-arg` secrets). Layers are cached and shippable; the token would leak | ||
| to anyone who pulls the image. Pass it at runtime with `-e` / `--env-file`. | ||
| - Scope the token to what the job needs and rotate it like any other CI secret. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,109 @@ | ||||||||||||||||||||||||||||
| // Regenerate the Homebrew formula + Scoop manifest for a published version. | ||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||
| // node scripts/gen-dist.mjs <version> | ||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||
| // Writes packaging/homebrew/pdcli.rb and packaging/scoop/pdcli.json (both | ||||||||||||||||||||||||||||
| // git-ignored — they live in the wavyx/homebrew-tap and wavyx/scoop-pdcli | ||||||||||||||||||||||||||||
| // repos). See packaging/README.md. Run AFTER the version is on npm. | ||||||||||||||||||||||||||||
| import { writeFileSync, mkdirSync } from 'node:fs' | ||||||||||||||||||||||||||||
| import { createHash } from 'node:crypto' | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const PKG = '@wavyx/pdcli' | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| /** Render the Homebrew formula (standard node-CLI pattern). */ | ||||||||||||||||||||||||||||
| export function renderHomebrewFormula({ url, sha256 }) { | ||||||||||||||||||||||||||||
| return `class Pdcli < Formula | ||||||||||||||||||||||||||||
| desc "Command-line interface for Pipedrive" | ||||||||||||||||||||||||||||
| homepage "https://github.com/wavyx/pdcli" | ||||||||||||||||||||||||||||
| url "${url}" | ||||||||||||||||||||||||||||
| sha256 "${sha256}" | ||||||||||||||||||||||||||||
| license "MIT" | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| depends_on "jq" | ||||||||||||||||||||||||||||
| depends_on "node" | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def install | ||||||||||||||||||||||||||||
| system "npm", "install", *std_npm_args | ||||||||||||||||||||||||||||
| # pdcli's --jq flag uses node-jq, which normally downloads its own jq binary | ||||||||||||||||||||||||||||
| # via a postinstall script. std_npm_args passes --ignore-scripts and the | ||||||||||||||||||||||||||||
| # Homebrew build sandbox blocks network, so point node-jq at the Homebrew jq | ||||||||||||||||||||||||||||
| # instead (node-jq honors $JQ_PATH at runtime). | ||||||||||||||||||||||||||||
| (bin/"pdcli").write_env_script libexec/"bin/pdcli", | ||||||||||||||||||||||||||||
| JQ_PATH: Formula["jq"].opt_bin/"jq" | ||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| test do | ||||||||||||||||||||||||||||
| assert_match "pdcli", shell_output("#{bin}/pdcli version") | ||||||||||||||||||||||||||||
| # Exercise --jq so the node-jq / Homebrew-jq wiring can't silently regress. | ||||||||||||||||||||||||||||
| system bin/"pdcli", "config", "list", "--output", "json", "--jq", "." | ||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||
| ` | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| /** Render the Scoop manifest (installs via npm; needs Node). */ | ||||||||||||||||||||||||||||
| export function renderScoopManifest({ version }) { | ||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||
| JSON.stringify( | ||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||
| version, | ||||||||||||||||||||||||||||
| description: 'Command-line interface for Pipedrive', | ||||||||||||||||||||||||||||
| homepage: 'https://github.com/wavyx/pdcli', | ||||||||||||||||||||||||||||
| license: 'MIT', | ||||||||||||||||||||||||||||
| depends: 'nodejs', | ||||||||||||||||||||||||||||
| installer: { script: [`npm install -g ${PKG}@${version}`] }, | ||||||||||||||||||||||||||||
| uninstaller: { script: [`npm uninstall -g ${PKG}`] }, | ||||||||||||||||||||||||||||
| checkver: { | ||||||||||||||||||||||||||||
| url: `https://registry.npmjs.org/${PKG}`, | ||||||||||||||||||||||||||||
| jsonpath: "$.['dist-tags'].latest", | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| // Templated with Scoop's $version placeholder so autoupdate rewrites the | ||||||||||||||||||||||||||||
| // pinned version inside installer.script too — not just top-level version. | ||||||||||||||||||||||||||||
| // Without this, autoupdate bumps `version` while the install command stays | ||||||||||||||||||||||||||||
| // pinned to the previous release. | ||||||||||||||||||||||||||||
| autoupdate: { | ||||||||||||||||||||||||||||
| installer: { script: [`npm install -g ${PKG}@$version`] }, | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| null, | ||||||||||||||||||||||||||||
| 2, | ||||||||||||||||||||||||||||
| ) + '\n' | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| /** Fetch the npm tarball URL + its sha256 for a version. */ | ||||||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||||||
|
Comment on lines
+75
to
+79
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||
| const buf = Buffer.from(await fetchFn(url).then((r) => r.arrayBuffer())) | ||||||||||||||||||||||||||||
| const sha256 = createHash('sha256').update(buf).digest('hex') | ||||||||||||||||||||||||||||
| return { version, url, sha256 } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) { | ||||||||||||||||||||||||||||
| const version = process.argv[2] | ||||||||||||||||||||||||||||
| if (!version) { | ||||||||||||||||||||||||||||
| console.error('usage: node scripts/gen-dist.mjs <version>') | ||||||||||||||||||||||||||||
| process.exit(1) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| const dist = await fetchDist(version) | ||||||||||||||||||||||||||||
| mkdirSync(new URL('../packaging/homebrew/', import.meta.url), { | ||||||||||||||||||||||||||||
| recursive: true, | ||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||
| mkdirSync(new URL('../packaging/scoop/', import.meta.url), { | ||||||||||||||||||||||||||||
| recursive: true, | ||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||
| writeFileSync( | ||||||||||||||||||||||||||||
| new URL('../packaging/homebrew/pdcli.rb', import.meta.url), | ||||||||||||||||||||||||||||
| renderHomebrewFormula(dist), | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| writeFileSync( | ||||||||||||||||||||||||||||
| new URL('../packaging/scoop/pdcli.json', import.meta.url), | ||||||||||||||||||||||||||||
| renderScoopManifest(dist), | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| console.log( | ||||||||||||||||||||||||||||
| `Wrote packaging/homebrew/pdcli.rb + packaging/scoop/pdcli.json for ${version} (sha256 ${dist.sha256})`, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All five actions in the
dockerjob (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 holdspackages: writeandid-token: writepermissions.