From f5615455bf0e5b7ed4dca37c38426b25c8e7483f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Jul 2026 19:40:51 +0000 Subject: [PATCH] Add apt distribution: deb packaging, registry publish, docs (#1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements #1 — stoke installable with apt-get install stoke. Packaging: - scripts/build-deb.sh: builds dist/stoke__all.deb from a clean staging copy (src + fresh npm ci --omit=dev), pure-JS Architecture: all, Depends: nodejs (>= 22.12), /usr/lib/stoke payload with /usr/bin/stoke symlink, copyright + changelog, normalized permissions. Lintian-clean. - scripts/publish-deb.sh: uploads a .deb to the Forgejo Debian registry (owner/distribution/component parameterized, defaults heavy-duty/ stable/main), authenticating with STOKE_TOKEN or the stoke login token. - scripts/install-apt.sh: consumer-side one-time setup — adds the registry key and apt source, then apt-get install stoke. Falls back to a [trusted=yes] source when apt's sqv verifier rejects the forge's registry signature (known upstream Forgejo signing bug; the script prefers the signed source so setups heal once the forge is fixed). - .forgejo/workflows/release.yml: on v* tags — test, build, publish to the heavy-duty registry, attach the .deb to the release page. Needs a runner and a RELEASE_TOKEN secret with org package write. New command: - stoke pr merge (-n, --method merge|rebase|rebase-merge|squash, --title, --message, --delete-branch) — gap found while merging !2. Docs and housekeeping: - README: 'Install with apt' as the primary installation method with manual setup and dpkg fallback, signature caveat, pr merge reference, Packaging and releasing section with a release checklist. - dist/ gitignored; version bumped to 1.2.0. Verified end-to-end on this machine: built the deb (lintian-clean), published it to the Forgejo Debian registry, installed it with apt-get install stoke via install-apt.sh, and confirmed the installed CLI works against the live forge. The test upload was removed from the personal namespace afterwards; publishing under heavy-duty needs an org-member token (401 reqPackageAccess with this restricted account). Co-Authored-By: Claude Opus 4.8 (1M context) --- .forgejo/workflows/release.yml | 56 ++++++++++++++++++ .gitignore | 1 + README.md | 78 +++++++++++++++++++++++++ package-lock.json | 4 +- package.json | 2 +- scripts/build-deb.sh | 104 +++++++++++++++++++++++++++++++++ scripts/install-apt.sh | 63 ++++++++++++++++++++ scripts/publish-deb.sh | 52 +++++++++++++++++ src/api.js | 4 ++ src/cli.js | 30 ++++++++++ test/api.test.js | 9 +++ test/cli.test.js | 6 ++ 12 files changed, 406 insertions(+), 3 deletions(-) create mode 100644 .forgejo/workflows/release.yml create mode 100755 scripts/build-deb.sh create mode 100755 scripts/install-apt.sh create mode 100755 scripts/publish-deb.sh diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml new file mode 100644 index 0000000..7f35536 --- /dev/null +++ b/.forgejo/workflows/release.yml @@ -0,0 +1,56 @@ +# Release automation: on every v* tag, build the Debian package, publish it +# to the Forgejo Debian registry (owner: heavy-duty) and attach the .deb to +# the tag's release page as a fallback for direct `dpkg -i` installs. +# +# Requirements: +# - A Forgejo Actions runner on the instance. Adjust `runs-on` to a label +# your runner actually advertises (common: docker, ubuntu-latest). +# - A repository/org secret RELEASE_TOKEN: a token with package:write and +# repository:write scopes for an account allowed to publish packages +# under the heavy-duty org. + +name: release +on: + push: + tags: + - 'v*' + +jobs: + deb: + runs-on: docker + container: + image: node:22-bookworm + steps: + - name: Check out tag + uses: actions/checkout@v4 + + - name: Run tests + run: npm ci && npm test + + - name: Build .deb + run: bash scripts/build-deb.sh + + - name: Publish to Debian registry + env: + STOKE_TOKEN: ${{ secrets.RELEASE_TOKEN }} + run: bash scripts/publish-deb.sh dist/stoke_*_all.deb heavy-duty stable main + + - name: Create release and attach .deb + env: + TOKEN: ${{ secrets.RELEASE_TOKEN }} + TAG: ${{ github.ref_name }} + API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }} + run: | + set -euo pipefail + DEB=$(ls dist/stoke_*_all.deb) + # Create the release if it does not exist yet, then grab its id. + RELEASE_ID=$(curl -sf -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" | node -pe "JSON.parse(require('fs').readFileSync(0,'utf8')).id" 2>/dev/null || true) + if [ -z "$RELEASE_ID" ]; then + RELEASE_ID=$(curl -sf -X POST -H "Authorization: token $TOKEN" -H 'Content-Type: application/json' \ + -d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\",\"draft\":false,\"prerelease\":false}" \ + "$API/releases" | node -pe "JSON.parse(require('fs').readFileSync(0,'utf8')).id") + fi + curl -sf -X POST -H "Authorization: token $TOKEN" \ + -F "attachment=@$DEB" \ + "$API/releases/$RELEASE_ID/assets?name=$(basename "$DEB")" >/dev/null + echo "Attached $(basename "$DEB") to release $TAG" diff --git a/.gitignore b/.gitignore index 3c45938..dd6e803 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ +dist/ *.log .DS_Store diff --git a/README.md b/README.md index db565eb..2b840d6 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,33 @@ A command-line interface for [Forgejo](https://forgejo.org/), built with [Comman ## Installation +### With apt (Debian/Ubuntu — recommended) + +The package is published to the Debian registry of the forge itself. One-time setup: + +```bash +curl -fsSL https://forgejo.heavyduty.builders/heavy-duty/stoke/raw/branch/main/scripts/install-apt.sh | bash +``` + +or manually: + +```bash +sudo install -d /etc/apt/keyrings +curl -fsSL https://forgejo.heavyduty.builders/api/packages/heavy-duty/debian/repository.key \ + | sudo tee /etc/apt/keyrings/forgejo-heavy-duty.asc >/dev/null +echo "deb [signed-by=/etc/apt/keyrings/forgejo-heavy-duty.asc] https://forgejo.heavyduty.builders/api/packages/heavy-duty/debian stable main" \ + | sudo tee /etc/apt/sources.list.d/forgejo-heavy-duty.list +sudo apt-get update && sudo apt-get install stoke +``` + +Upgrades then arrive through regular `apt-get upgrade`. + +Note: apt releases that verify OpenPGP with `sqv` (Debian 13+, apt >= 2.9) currently reject the signature Forgejo generates for its Debian registry (an upstream signing bug). `install-apt.sh` detects this and falls back to a `[trusted=yes]` source — integrity then relies on HTTPS to the forge. The script prefers the signed source, so setups heal automatically once the forge is fixed. + +As a fallback, each release also has the `.deb` attached for direct install: `sudo dpkg -i stoke__all.deb`. + +### From source + ```bash cd stoke npm install @@ -390,6 +417,27 @@ stoke pr create -o heavy-duty -r stoke -t "Fix config handling" \ Calls `POST /api/v1/repos/{owner}/{repo}/pulls`. +### `stoke pr merge` + +Merge a pull request. + +```text +Options: + -o, --owner repository owner (required) + -r, --repo repository name (required) + -n, --number pull request number (required) + --method merge, rebase, rebase-merge, squash (default: merge) + --title custom merge commit title + --message <message> custom merge commit message + --delete-branch delete the source branch after merging +``` + +```bash +stoke pr merge -o heavy-duty -r stoke -n 2 --delete-branch +``` + +Calls `POST /api/v1/repos/{owner}/{repo}/pulls/{number}/merge`. + ### `stoke branch list` List branches in a repository. @@ -601,6 +649,12 @@ test/ ├── cli.test.js # end-to-end CLI behavior (spawned processes) ├── api.test.js # API client with a mocked fetch └── config.test.js # config path resolution and persistence +scripts/ +├── build-deb.sh # build dist/stoke_<version>_all.deb +├── publish-deb.sh # upload a .deb to the Forgejo Debian registry +└── install-apt.sh # consumer-side apt source setup + install +.forgejo/workflows/ +└── release.yml # tag-driven build + publish + release attachment ``` - `cli.js` defines commands and options, handles prompts and prints results. @@ -615,6 +669,30 @@ The test suite uses the Node.js built-in test runner — no extra dependencies: npm test ``` +## Packaging and releasing + +The Debian package is a pure-JS `Architecture: all` package that ships the CLI to `/usr/lib/stoke` with a `/usr/bin/stoke` symlink and declares `Depends: nodejs (>= 22.12)`. + +Build locally (needs `dpkg-deb`; runs `lintian` when installed): + +```bash +scripts/build-deb.sh # -> dist/stoke_<version>_all.deb +``` + +Publish to the Forgejo Debian registry (uses `STOKE_TOKEN` or the `stoke auth login` token; the account needs package write access on the owner): + +```bash +scripts/publish-deb.sh dist/stoke_<version>_all.deb heavy-duty stable main +``` + +Releases are automated in `.forgejo/workflows/release.yml`: pushing a `v*` tag runs the tests, builds the `.deb`, publishes it to the `heavy-duty` registry and attaches it to the tag's release page. The workflow needs a Forgejo Actions runner and a `RELEASE_TOKEN` secret (package + repository write for the `heavy-duty` org); adjust `runs-on` to a label your runner advertises. + +Release checklist: + +1. Bump `version` in `package.json` and `package-lock.json`. +2. Commit, tag `v<version>`, push the tag. +3. CI publishes the package; consumers get it with `apt-get update && apt-get upgrade`. + ## Security notes - Tokens are stored on disk with `0600` permissions. diff --git a/package-lock.json b/package-lock.json index ba2d167..e4a3923 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "stoke", - "version": "1.1.0", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "stoke", - "version": "1.1.0", + "version": "1.2.0", "license": "ISC", "dependencies": { "commander": "^15.0.0" diff --git a/package.json b/package.json index d1b19d4..6e281c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stoke", - "version": "1.1.0", + "version": "1.2.0", "description": "CLI for the heavy-duty forge (Forgejo)", "main": "src/cli.js", "scripts": { diff --git a/scripts/build-deb.sh b/scripts/build-deb.sh new file mode 100755 index 0000000..9d60d6f --- /dev/null +++ b/scripts/build-deb.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +# +# Build a Debian package for stoke. +# +# Usage: scripts/build-deb.sh +# +# Produces dist/stoke_<version>_all.deb from a clean staging copy of the +# working tree (only src/, package.json and package-lock.json are shipped; +# production dependencies are installed fresh with `npm ci --omit=dev`). +# +# Requirements: bash, node/npm, dpkg-deb, gzip. Runs lintian when available. + +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +VERSION="$(node -p "require('$ROOT/package.json').version")" +MAINTAINER="${DEB_MAINTAINER:-Heavy Duty Builders <equipo@lafamilia.so>}" +HOMEPAGE="https://forgejo.heavyduty.builders/heavy-duty/stoke" + +STAGE="$(mktemp -d)" +trap 'rm -rf "$STAGE"' EXIT + +PKG="$STAGE/stoke_${VERSION}_all" +LIB="$PKG/usr/lib/stoke" +DOC="$PKG/usr/share/doc/stoke" + +install -d "$PKG/DEBIAN" "$LIB" "$PKG/usr/bin" "$DOC" + +# --- payload ----------------------------------------------------------------- +cp -r "$ROOT/src" "$LIB/src" +cp "$ROOT/package.json" "$ROOT/package-lock.json" "$LIB/" +(cd "$LIB" && npm ci --omit=dev --silent --no-audit --no-fund) + +# Launcher: the CLI has a `#!/usr/bin/env node` shebang and resolves its +# node_modules relative to its real path, so a symlink is all we need. +ln -s ../lib/stoke/src/cli.js "$PKG/usr/bin/stoke" + +# --- docs (lintian: copyright + Debian changelog) ---------------------------- +cat > "$DOC/copyright" <<EOF +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: stoke +Source: $HOMEPAGE + +Files: * +Copyright: $(date +%Y) Heavy Duty Builders +License: ISC + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + . + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +EOF + +cat > "$STAGE/changelog" <<EOF +stoke ($VERSION) stable; urgency=medium + + * See the repository release page for notes. + + -- $MAINTAINER $(date -R) +EOF +# Native package (no Debian revision in the version), so plain changelog.gz. +gzip -9n -c "$STAGE/changelog" > "$DOC/changelog.gz" + +# Normalize permissions regardless of the builder's umask: no group/other +# write anywhere, executable entry point. +chmod -R go-w "$PKG/usr" +chmod 0755 "$LIB/src/cli.js" + +# --- control ----------------------------------------------------------------- +INSTALLED_SIZE="$(du -sk --exclude=DEBIAN "$PKG" | cut -f1)" +cat > "$PKG/DEBIAN/control" <<EOF +Package: stoke +Version: $VERSION +Section: utils +Priority: optional +Architecture: all +Depends: nodejs (>= 22.12) +Installed-Size: $INSTALLED_SIZE +Maintainer: $MAINTAINER +Homepage: $HOMEPAGE +Description: CLI for the heavy-duty forge (Forgejo) + stoke manages the Forgejo instance at forgejo.heavyduty.builders from the + command line: authentication, repositories, issues, pull requests, + branches, collaborators, organizations, teams and users. +EOF + +# --- build ------------------------------------------------------------------- +mkdir -p "$ROOT/dist" +dpkg-deb --build --root-owner-group "$PKG" "$ROOT/dist/" >/dev/null +DEB="$ROOT/dist/stoke_${VERSION}_all.deb" +echo "Built: $DEB" + +if command -v lintian >/dev/null 2>&1; then + # binary-without-manpage is a known, accepted gap for now. + lintian --suppress-tags binary-without-manpage "$DEB" || true +else + echo "Note: lintian not installed; skipping package lint." +fi diff --git a/scripts/install-apt.sh b/scripts/install-apt.sh new file mode 100755 index 0000000..7823cf6 --- /dev/null +++ b/scripts/install-apt.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# +# One-time setup to install stoke via apt on Debian/Ubuntu. +# +# Adds the heavy-duty Forgejo Debian registry as an APT source (with its +# signing key) and installs the stoke package. Safe to re-run; afterwards +# stoke upgrades through regular `apt-get upgrade`. +# +# Usage: +# ./scripts/install-apt.sh +# FORGE_URL=... OWNER=... ./scripts/install-apt.sh # non-default instance +# +# Run as root or as a user with sudo. + +set -euo pipefail + +FORGE_URL="${FORGE_URL:-https://forgejo.heavyduty.builders}" +OWNER="${OWNER:-heavy-duty}" +DISTRIBUTION="${DISTRIBUTION:-stable}" +COMPONENT="${COMPONENT:-main}" + +KEYRING="/etc/apt/keyrings/forgejo-$OWNER.asc" +LIST="/etc/apt/sources.list.d/forgejo-$OWNER.list" + +SUDO="" +if [ "$(id -u)" -ne 0 ]; then + command -v sudo >/dev/null 2>&1 || { echo "error: run as root or install sudo" >&2; exit 1; } + SUDO="sudo" +fi + +update_only_this_source() { + $SUDO apt-get update \ + -o Dir::Etc::sourcelist="$LIST" \ + -o Dir::Etc::sourceparts=/dev/null \ + -o APT::Get::List-Cleanup=0 +} + +echo "Adding APT source for $FORGE_URL/$OWNER ..." +$SUDO install -d -m 0755 /etc/apt/keyrings +curl -fsSL "$FORGE_URL/api/packages/$OWNER/debian/repository.key" | $SUDO tee "$KEYRING" >/dev/null +echo "deb [signed-by=$KEYRING] $FORGE_URL/api/packages/$OWNER/debian $DISTRIBUTION $COMPONENT" \ + | $SUDO tee "$LIST" >/dev/null + +# Newer apt verifies with sqv (Sequoia), which rejects the signature Forgejo +# currently produces for its Debian registry (malformed Ed25519 MPI encoding +# in the upstream signing library). Try the properly signed source first so +# this heals automatically once the forge is fixed; otherwise fall back to +# [trusted=yes] — package integrity then relies on HTTPS to our own forge. +if ! update_only_this_source; then + echo + echo "WARNING: signature verification failed (known Forgejo registry issue" >&2 + echo "with sqv-based apt). Falling back to [trusted=yes]; transport" >&2 + echo "security is provided by HTTPS to $FORGE_URL." >&2 + echo + echo "deb [trusted=yes] $FORGE_URL/api/packages/$OWNER/debian $DISTRIBUTION $COMPONENT" \ + | $SUDO tee "$LIST" >/dev/null + update_only_this_source +fi + +$SUDO apt-get install -y stoke + +echo +stoke --version >/dev/null && echo "stoke $(stoke --version) installed. Run: stoke auth login" diff --git a/scripts/publish-deb.sh b/scripts/publish-deb.sh new file mode 100755 index 0000000..1e7e3a6 --- /dev/null +++ b/scripts/publish-deb.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# +# Publish a .deb to the Forgejo Debian package registry. +# +# Usage: scripts/publish-deb.sh <path-to-deb> [owner] [distribution] [component] +# +# owner registry owner (user or org), default: heavy-duty +# distribution APT distribution, default: stable +# component APT component, default: main +# +# Authentication (first match wins): +# 1. STOKE_TOKEN environment variable +# 2. The token stored by `stoke auth login` +# +# The Forgejo URL defaults to the instance in the stoke config, falling back +# to https://forgejo.heavyduty.builders. Override with FORGE_URL. + +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +DEB="${1:?usage: publish-deb.sh <path-to-deb> [owner] [distribution] [component]}" +OWNER="${2:-heavy-duty}" +DISTRIBUTION="${3:-stable}" +COMPONENT="${4:-main}" + +[ -f "$DEB" ] || { echo "error: no such file: $DEB" >&2; exit 1; } + +CONFIG_JSON="$(node -e "const c = require('$ROOT/src/config').loadConfig(); if (c) process.stdout.write(JSON.stringify(c));" 2>/dev/null || true)" +TOKEN="${STOKE_TOKEN:-$(node -pe "(JSON.parse(process.argv[1] || '{}').token) || ''" "$CONFIG_JSON")}" +FORGE_URL="${FORGE_URL:-$(node -pe "(JSON.parse(process.argv[1] || '{}').url) || 'https://forgejo.heavyduty.builders'" "$CONFIG_JSON")}" + +[ -n "$TOKEN" ] || { echo "error: no token. Set STOKE_TOKEN or run: stoke auth login" >&2; exit 1; } + +URL="$FORGE_URL/api/packages/$OWNER/debian/pool/$DISTRIBUTION/$COMPONENT/upload" +echo "Uploading $(basename "$DEB") to $URL" + +STATUS="$(curl -sS -o /tmp/stoke-publish-response.$$ -w '%{http_code}' \ + -X PUT -H "Authorization: token $TOKEN" \ + --upload-file "$DEB" "$URL")" + +case "$STATUS" in + 201) echo "Published." ;; + 409) echo "Already published (409): this exact version already exists in the registry." ;; + *) + echo "error: upload failed with HTTP $STATUS" >&2 + cat /tmp/stoke-publish-response.$$ >&2 || true + rm -f /tmp/stoke-publish-response.$$ + exit 1 + ;; +esac +rm -f /tmp/stoke-publish-response.$$ diff --git a/src/api.js b/src/api.js index de0f55b..75137e5 100644 --- a/src/api.js +++ b/src/api.js @@ -167,6 +167,10 @@ class ForgejoClient { return this.post(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls`, payload); } + async mergePullRequest(owner, repo, index, payload) { + return this.post(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${index}/merge`, payload); + } + async listBranches(owner, repo, opts = {}) { return this.getAll(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/branches`, opts); } diff --git a/src/cli.js b/src/cli.js index 8732f76..5522461 100755 --- a/src/cli.js +++ b/src/cli.js @@ -649,6 +649,36 @@ pr } }); +pr + .command('merge') + .description('Merge a pull request') + .requiredOption('-o, --owner <owner>', 'repository owner') + .requiredOption('-r, --repo <repo>', 'repository name') + .requiredOption('-n, --number <number>', 'pull request number', parseId) + .option('--method <method>', 'merge method: merge, rebase, rebase-merge, squash', 'merge') + .option('--title <title>', 'custom merge commit title') + .option('--message <message>', 'custom merge commit message') + .option('--delete-branch', 'delete the source branch after merging', false) + .action(async (options) => { + try { + const config = loadConfig(); + const client = ForgejoClient.fromConfig(config); + const payload = { + Do: options.method, + delete_branch_after_merge: options.deleteBranch, + }; + if (options.title) payload.MergeTitleField = options.title; + if (options.message) payload.MergeMessageField = options.message; + await client.mergePullRequest(options.owner, options.repo, options.number, payload); + console.log(`Merged !${options.number} in ${options.owner}/${options.repo} (${options.method}).`); + if (options.deleteBranch) console.log('Source branch deleted.'); + } catch (err) { + console.error(`Pull request merge failed: ${err.message}`); + if (err.status) console.error(`HTTP status: ${err.status}`); + process.exit(1); + } + }); + const branchCmd = program .command('branch') .description('Manage branches'); diff --git a/test/api.test.js b/test/api.test.js index 927aae1..52eb911 100644 --- a/test/api.test.js +++ b/test/api.test.js @@ -122,6 +122,15 @@ test('searchUsers unwraps the {data: []} envelope and paginates', async () => { assert.equal(users.at(-1).login, 'last'); }); +test('mergePullRequest posts the merge payload to the merge endpoint', async () => { + const calls = mockFetch(() => jsonResponse(null, 200)); + const client = new ForgejoClient('https://forge.test', 'tok'); + await client.mergePullRequest('owner', 'repo', 7, { Do: 'squash', delete_branch_after_merge: true }); + assert.equal(calls[0].url, 'https://forge.test/api/v1/repos/owner/repo/pulls/7/merge'); + assert.equal(calls[0].opts.method, 'POST'); + assert.deepEqual(JSON.parse(calls[0].opts.body), { Do: 'squash', delete_branch_after_merge: true }); +}); + test('createIssue and createPullRequest hit the expected endpoints', async () => { const calls = mockFetch(() => jsonResponse({ number: 1 })); const client = new ForgejoClient('https://forge.test', 'tok'); diff --git a/test/cli.test.js b/test/cli.test.js index 85e4a37..b3b4a48 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -55,6 +55,12 @@ test('invalid --team-id is rejected before any network call', () => { assert.match(res.stderr, /Id must be a positive integer/); }); +test('pr merge validates --number before any network call', () => { + const res = run(['pr', 'merge', '-o', 'o', '-r', 'r', '-n', 'seven']); + assert.equal(res.status, 1); + assert.match(res.stderr, /Id must be a positive integer/); +}); + test('issue create --body-file reports unreadable files cleanly', () => { const cfg = path.join(os.tmpdir(), `stoke-cfg-${process.pid}.json`); fs.writeFileSync(cfg, JSON.stringify({ url: 'https://forge.test', token: 'tok' }));