Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,17 @@ jobs:
- name: Report binary size
run: ls -lh target/release/pup.exe

npm-packaging:
name: npm Packaging
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
- name: Test launcher and package builder
run: node --test 'npm/**/*.test.js'

wasm:
name: WASM (WASI + Browser)
runs-on: ubuntu-latest
Expand Down
130 changes: 130 additions & 0 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
name: Publish to npm

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

you'll probably want to hook release.yml to trigger this automatically

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

馃憢 I don't intend to merge this PR in, creating this PR draft for a POC i'm working on for migrating my team's AI-CLI into pup


on:
workflow_dispatch:
inputs:
tag:
description: Existing GitHub release tag to publish (e.g. v1.23.7)
required: true
type: string
dry_run:
description: Run npm publish with --dry-run
required: true
default: true
type: boolean

permissions:
contents: read
id-token: write # npm provenance

concurrency:
group: npm-publish
cancel-in-progress: false

jobs:
publish:
name: Publish ${{ inputs.tag }}
runs-on: ubuntu-latest
environment: npm-publish
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
registry-url: https://registry.npmjs.org

- name: Require npm token for real publishes
if: ${{ !inputs.dry_run }}
run: |
if [ -z "$NODE_AUTH_TOKEN" ]; then
echo "NPM_TOKEN secret is not configured" >&2
exit 1
fi
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Download release assets
run: |
VERSION="${TAG#v}"
mkdir -p assets
gh release download "$TAG" --dir assets \
--pattern "pup_${VERSION}_Darwin_*.tar.gz" \
--pattern "pup_${VERSION}_Linux_*.tar.gz" \
--pattern "pup_${VERSION}_Windows_*.zip" \
--pattern "pup_${VERSION}_checksums.txt" \
--pattern "pup_${VERSION}_checksums.txt.sigstore.json"
env:
TAG: ${{ inputs.tag }}
GH_TOKEN: ${{ github.token }}

- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2

- name: Verify release signature
working-directory: assets
run: |
CHECKSUMS="pup_${TAG#v}_checksums.txt"
cosign verify-blob \
--bundle "${CHECKSUMS}.sigstore.json" \
--certificate-identity "https://github.com/DataDog/pup/.github/workflows/release.yml@refs/tags/${TAG}" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
"$CHECKSUMS"
env:
TAG: ${{ inputs.tag }}

- name: Build packages
run: |
node npm/scripts/build-packages.js \
--version "${TAG#v}" \
--assets assets \
--checksums "assets/pup_${TAG#v}_checksums.txt" \
--out npm-out
env:
TAG: ${{ inputs.tag }}

- name: Publish
run: |
VERSION="${TAG#v}"
if ! LATEST=$(npm view @datadog/pup dist-tags.latest 2>npm-view.err); then
if ! grep -q E404 npm-view.err; then
cat npm-view.err >&2
exit 1
fi
LATEST=""
fi
DIST_TAG=latest
if [[ "$VERSION" == *-* ]]; then
DIST_TAG=next
elif [ -n "$LATEST" ] && [ "$(printf '%s\n%s\n' "$LATEST" "$VERSION" | sort -V | tail -n1)" != "$VERSION" ]; then
DIST_TAG=backfill
fi
echo "Publishing ${VERSION} with dist-tag ${DIST_TAG} (current latest: ${LATEST:-none})"
FLAGS="--access public --provenance --tag $DIST_TAG"
if [ "$DRY_RUN" = "true" ]; then
FLAGS="$FLAGS --dry-run"
fi
for dir in npm-out/pup-*/ npm-out/pup/; do
NAME=$(node -p "require('./${dir}package.json').name")
if ! PUBLISHED=$(npm view "${NAME}@${VERSION}" dist.shasum 2>npm-view.err); then
if ! grep -q E404 npm-view.err; then
cat npm-view.err >&2
exit 1
fi
PUBLISHED=""
fi
if [ -n "$PUBLISHED" ]; then
LOCAL=$(npm pack "$dir" --dry-run --json | node -p "JSON.parse(require('fs').readFileSync(0, 'utf8'))[0].shasum")
if [ "$PUBLISHED" != "$LOCAL" ]; then
echo "${NAME}@${VERSION} is already published with different contents" >&2
exit 1
fi
echo "${NAME}@${VERSION} is already published; skipping"
continue
fi
npm publish "$dir" $FLAGS
done
env:
DRY_RUN: ${{ inputs.dry_run }}
TAG: ${{ inputs.tag }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ target/

# Binary
pup
!/npm/pup/

# IDEs
.idea/
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,19 @@ brew tap datadog-labs/pack
brew install datadog-labs/pack/pup
```

### npm

```bash
# Run without installing
npx @datadog/pup auth login
npx @datadog/pup monitors list

# Or install globally
npm install -g @datadog/pup
```

See [npm/pup/README.md](npm/pup/README.md) for how the platform packages work.

### Build from Source

```bash
Expand Down
35 changes: 35 additions & 0 deletions npm/pup/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# @datadog/pup

The [pup](https://github.com/DataDog/pup) Datadog CLI, distributed through npm.

## Usage

Run without installing:

```bash
npx @datadog/pup auth login
npx @datadog/pup monitors list --tags="team:api-platform"
```

Or install globally:

```bash
npm install -g @datadog/pup
pup --help
```

## How it works

This package contains a small Node.js launcher. The native pup binary ships in a separate package per platform, listed as `optionalDependencies`, so npm downloads only the one matching your machine:

| Package | Platform |
| --------------------------- | --------------------- |
| `@datadog/pup-darwin-arm64` | macOS (Apple Silicon) |
| `@datadog/pup-darwin-x64` | macOS (Intel) |
| `@datadog/pup-linux-arm64` | Linux arm64 |
| `@datadog/pup-linux-x64` | Linux x86_64 |
| `@datadog/pup-win32-x64` | Windows x86_64 |

The launcher passes arguments, exit codes, and signals through to the binary. If the platform package is missing, reinstall without `--omit=optional` or `--no-optional`.

The binaries are the same ones attached to each [GitHub release](https://github.com/DataDog/pup/releases).
41 changes: 41 additions & 0 deletions npm/pup/bin/pup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env node
'use strict';

const {spawn} = require('node:child_process');
const os = require('node:os');
const {resolveBinaryPath} = require('../lib/platforms.js');

const FORWARDED_SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP'];

let binary;
try {
binary = resolveBinaryPath();
} catch (error) {
console.error(error.message);
process.exit(1);
}

const child = spawn(binary, process.argv.slice(2), {stdio: 'inherit'});

for (const signal of FORWARDED_SIGNALS) {
process.on(signal, () => child.kill(signal));
}

child.on('error', error => {
console.error(`Failed to run ${binary}: ${error.message}`);
process.exit(1);
});

child.on('exit', (code, signal) => {
if (signal) {
for (const forwarded of FORWARDED_SIGNALS) {
process.removeAllListeners(forwarded);
}

process.exitCode = 128 + (os.constants.signals[signal] ?? 0);
process.kill(process.pid, signal);
return;
}

process.exit(code ?? 1);
});
38 changes: 38 additions & 0 deletions npm/pup/lib/platforms.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use strict';

const PLATFORMS = {
'darwin-arm64': {os: 'darwin', cpu: 'arm64', asset: 'Darwin_arm64.tar.gz', binary: 'pup'},
'darwin-x64': {os: 'darwin', cpu: 'x64', asset: 'Darwin_x86_64.tar.gz', binary: 'pup'},
'linux-arm64': {os: 'linux', cpu: 'arm64', asset: 'Linux_arm64.tar.gz', binary: 'pup'},
'linux-x64': {os: 'linux', cpu: 'x64', asset: 'Linux_x86_64.tar.gz', binary: 'pup'},
'win32-x64': {os: 'win32', cpu: 'x64', asset: 'Windows_x86_64.zip', binary: 'pup.exe'},
};

function platformPackageName(key) {
return `@datadog/pup-${key}`;
}

function resolveBinaryPath({
platform = process.platform,
arch = process.arch,
resolve = require.resolve,
} = {}) {
const key = `${platform}-${arch}`;
const entry = PLATFORMS[key];
if (!entry) {
throw new Error(
`pup does not publish a binary for ${key}. Supported platforms: ${Object.keys(PLATFORMS).join(', ')}.`,
);
}

const packageName = platformPackageName(key);
try {
return resolve(`${packageName}/bin/${entry.binary}`);
} catch {
throw new Error(
`The ${packageName} package is not installed. It is an optional dependency of @datadog/pup; reinstall without --omit=optional or --no-optional.`,
);
}
}

module.exports = {PLATFORMS, platformPackageName, resolveBinaryPath};
28 changes: 28 additions & 0 deletions npm/pup/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@datadog/pup",
"version": "0.0.0",
"description": "Datadog CLI for AI agents and humans",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/DataDog/pup.git",
"directory": "npm/pup"
},
"bin": {
"pup": "bin/pup.js"
},
"files": [
"bin",
"lib"
],
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@datadog/pup-darwin-arm64": "0.0.0",
"@datadog/pup-darwin-x64": "0.0.0",
"@datadog/pup-linux-arm64": "0.0.0",
"@datadog/pup-linux-x64": "0.0.0",
"@datadog/pup-win32-x64": "0.0.0"
}
}
Loading
Loading