├── .release-please-manifest.json ├── map.js ├── .npmrc ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── config.yml │ └── bug.yml ├── dependabot.yml ├── matchers │ └── tap.json ├── settings.yml ├── workflows │ ├── codeql-analysis.yml │ ├── audit.yml │ ├── pull-request.yml │ ├── release-integration.yml │ ├── ci.yml │ ├── ci-release.yml │ ├── post-dependabot.yml │ └── release.yml └── actions │ ├── create-check │ └── action.yml │ └── install-latest-npm │ └── action.yml ├── CODE_OF_CONDUCT.md ├── .commitlintrc.js ├── .eslintrc.js ├── lib ├── index.js ├── tracker.js └── client.js ├── .gitignore ├── test ├── index.js ├── tracker.js └── client.js ├── LICENSE ├── release-please-config.json ├── SECURITY.md ├── package.json ├── docs └── examples │ ├── npmlog.js │ └── cli-progress.js ├── CONTRIBUTING.md ├── tap-snapshots └── test │ ├── tracker.js.test.cjs │ └── client.js.test.cjs ├── CHANGELOG.md └── README.md /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "4.0.0" 3 | } 4 | -------------------------------------------------------------------------------- /map.js: -------------------------------------------------------------------------------- 1 | module.exports = test => test.replace(/^test\//, 'lib/') 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ; This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | package-lock=false 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | * @npm/cli-team 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | blank_issues_enabled: true 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | All interactions in this repo are covered by the [npm Code of 4 | Conduct](https://docs.npmjs.com/policies/conduct) 5 | 6 | The npm cli team may, at its own discretion, moderate, remove, or edit 7 | any interactions such as pull requests, issues, and comments. 8 | -------------------------------------------------------------------------------- /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | /* This file is automatically added by @npmcli/template-oss. Do not edit. */ 2 | 3 | module.exports = { 4 | extends: ['@commitlint/config-conventional'], 5 | rules: { 6 | 'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'deps', 'chore']], 7 | 'header-max-length': [2, 'always', 80], 8 | 'subject-case': [0], 9 | 'body-max-line-length': [0], 10 | 'footer-max-line-length': [0], 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* This file is automatically added by @npmcli/template-oss. Do not edit. */ 2 | 3 | 'use strict' 4 | 5 | const { readdirSync: readdir } = require('fs') 6 | 7 | const localConfigs = readdir(__dirname) 8 | .filter((file) => file.startsWith('.eslintrc.local.')) 9 | .map((file) => `./${file}`) 10 | 11 | module.exports = { 12 | root: true, 13 | ignorePatterns: [ 14 | 'tap-testdir*/', 15 | ], 16 | extends: [ 17 | '@npmcli', 18 | ...localConfigs, 19 | ], 20 | } 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | version: 2 4 | 5 | updates: 6 | - package-ecosystem: npm 7 | directory: / 8 | schedule: 9 | interval: daily 10 | target-branch: "main" 11 | allow: 12 | - dependency-type: direct 13 | versioning-strategy: increase-if-necessary 14 | commit-message: 15 | prefix: deps 16 | prefix-development: chore 17 | labels: 18 | - "Dependencies" 19 | open-pull-requests-limit: 10 20 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | exports.Client = require('./client.js') 2 | exports.Tracker = require('./tracker.js') 3 | 4 | const trackers = new Map() 5 | exports.createTracker = (name, key, total) => { 6 | const tracker = new exports.Tracker(name, key, total) 7 | if (trackers.has(tracker.key)) { 8 | const msg = `proggy: duplicate progress id ${JSON.stringify(tracker.key)}` 9 | throw new Error(msg) 10 | } 11 | trackers.set(tracker.key, tracker) 12 | tracker.on('done', () => trackers.delete(tracker.key)) 13 | return tracker 14 | } 15 | exports.createClient = (options = {}) => new exports.Client(options) 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | # ignore everything in the root 4 | /* 5 | 6 | !**/.gitignore 7 | !/.commitlintrc.js 8 | !/.eslint.config.js 9 | !/.eslintrc.js 10 | !/.eslintrc.local.* 11 | !/.git-blame-ignore-revs 12 | !/.github/ 13 | !/.gitignore 14 | !/.npmrc 15 | !/.prettierignore 16 | !/.prettierrc.js 17 | !/.release-please-manifest.json 18 | !/bin/ 19 | !/CHANGELOG* 20 | !/CODE_OF_CONDUCT.md 21 | !/CONTRIBUTING.md 22 | !/docs/ 23 | !/lib/ 24 | !/LICENSE* 25 | !/map.js 26 | !/package.json 27 | !/README* 28 | !/release-please-config.json 29 | !/scripts/ 30 | !/SECURITY.md 31 | !/tap-snapshots/ 32 | !/test/ 33 | !/tsconfig.json 34 | tap-testdir*/ 35 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const proggy = require('../lib/index.js') 3 | const Client = require('../lib/client.js') 4 | const Tracker = require('../lib/tracker.js') 5 | t.equal(proggy.Client, Client, 'Client class is exported') 6 | t.equal(proggy.Tracker, Tracker, 'Tracker class is exported') 7 | 8 | const client = proggy.createClient() 9 | t.type(client, Client, 'createClient returns a client') 10 | 11 | t.test('createTracker', t => { 12 | const tracker = proggy.createTracker('hello') 13 | t.throws(() => proggy.createTracker('hello'), { 14 | message: 'proggy: duplicate progress id "hello"', 15 | }) 16 | tracker.emit('done') 17 | t.doesNotThrow(() => proggy.createTracker('hello')) 18 | t.end() 19 | }) 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The ISC License 2 | 3 | Copyright (c) GitHub, Inc. 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 15 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /.github/matchers/tap.json: -------------------------------------------------------------------------------- 1 | { 2 | "//@npmcli/template-oss": "This file is automatically added by @npmcli/template-oss. Do not edit.", 3 | "problemMatcher": [ 4 | { 5 | "owner": "tap", 6 | "pattern": [ 7 | { 8 | "regexp": "^\\s*not ok \\d+ - (.*)", 9 | "message": 1 10 | }, 11 | { 12 | "regexp": "^\\s*---" 13 | }, 14 | { 15 | "regexp": "^\\s*at:" 16 | }, 17 | { 18 | "regexp": "^\\s*line:\\s*(\\d+)", 19 | "line": 1 20 | }, 21 | { 22 | "regexp": "^\\s*column:\\s*(\\d+)", 23 | "column": 1 24 | }, 25 | { 26 | "regexp": "^\\s*file:\\s*(.*)", 27 | "file": 1 28 | } 29 | ] 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "group-pull-request-title-pattern": "chore: release ${version}", 3 | "pull-request-title-pattern": "chore: release${component} ${version}", 4 | "changelog-sections": [ 5 | { 6 | "type": "feat", 7 | "section": "Features", 8 | "hidden": false 9 | }, 10 | { 11 | "type": "fix", 12 | "section": "Bug Fixes", 13 | "hidden": false 14 | }, 15 | { 16 | "type": "docs", 17 | "section": "Documentation", 18 | "hidden": false 19 | }, 20 | { 21 | "type": "deps", 22 | "section": "Dependencies", 23 | "hidden": false 24 | }, 25 | { 26 | "type": "chore", 27 | "section": "Chores", 28 | "hidden": true 29 | } 30 | ], 31 | "packages": { 32 | ".": { 33 | "package-name": "" 34 | } 35 | }, 36 | "prerelease-type": "pre.0" 37 | } 38 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | repository: 4 | allow_merge_commit: false 5 | allow_rebase_merge: true 6 | allow_squash_merge: true 7 | squash_merge_commit_title: PR_TITLE 8 | squash_merge_commit_message: PR_BODY 9 | delete_branch_on_merge: true 10 | enable_automated_security_fixes: true 11 | enable_vulnerability_alerts: true 12 | 13 | branches: 14 | - name: main 15 | protection: 16 | required_status_checks: null 17 | enforce_admins: true 18 | block_creations: true 19 | required_pull_request_reviews: 20 | required_approving_review_count: 1 21 | require_code_owner_reviews: true 22 | require_last_push_approval: true 23 | dismiss_stale_reviews: true 24 | restrictions: 25 | apps: [] 26 | users: [] 27 | teams: [ "cli-team" ] 28 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: CodeQL 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | pull_request: 10 | branches: 11 | - main 12 | schedule: 13 | # "At 10:00 UTC (03:00 PT) on Monday" https://crontab.guru/#0_10_*_*_1 14 | - cron: "0 10 * * 1" 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | analyze: 21 | name: Analyze 22 | runs-on: ubuntu-latest 23 | permissions: 24 | actions: read 25 | contents: read 26 | security-events: write 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | - name: Setup Git User 31 | run: | 32 | git config --global user.email "npm-cli+bot@github.com" 33 | git config --global user.name "npm CLI robot" 34 | - name: Initialize CodeQL 35 | uses: github/codeql-action/init@v3 36 | with: 37 | languages: javascript 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v3 40 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | GitHub takes the security of our software products and services seriously, including the open source code repositories managed through our GitHub organizations, such as [GitHub](https://github.com/GitHub). 4 | 5 | If you believe you have found a security vulnerability in this GitHub-owned open source repository, you can report it to us in one of two ways. 6 | 7 | If the vulnerability you have found is *not* [in scope for the GitHub Bug Bounty Program](https://bounty.github.com/#scope) or if you do not wish to be considered for a bounty reward, please report the issue to us directly through [opensource-security@github.com](mailto:opensource-security@github.com). 8 | 9 | If the vulnerability you have found is [in scope for the GitHub Bug Bounty Program](https://bounty.github.com/#scope) and you would like for your finding to be considered for a bounty reward, please submit the vulnerability to us through [HackerOne](https://hackerone.com/github) in order to be eligible to receive a bounty award. 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** 12 | 13 | Thanks for helping make GitHub safe for everyone. 14 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Audit 4 | 5 | on: 6 | workflow_dispatch: 7 | schedule: 8 | # "At 08:00 UTC (01:00 PT) on Monday" https://crontab.guru/#0_8_*_*_1 9 | - cron: "0 8 * * 1" 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | audit: 16 | name: Audit Dependencies 17 | if: github.repository_owner == 'npm' 18 | runs-on: ubuntu-latest 19 | defaults: 20 | run: 21 | shell: bash 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | - name: Setup Git User 26 | run: | 27 | git config --global user.email "npm-cli+bot@github.com" 28 | git config --global user.name "npm CLI robot" 29 | - name: Setup Node 30 | uses: actions/setup-node@v4 31 | id: node 32 | with: 33 | node-version: 22.x 34 | check-latest: contains('22.x', '.x') 35 | - name: Install Latest npm 36 | uses: ./.github/actions/install-latest-npm 37 | with: 38 | node: ${{ steps.node.outputs.node-version }} 39 | - name: Install Dependencies 40 | run: npm i --ignore-scripts --no-audit --no-fund --package-lock 41 | - name: Run Production Audit 42 | run: npm audit --omit=dev 43 | - name: Run Full Audit 44 | run: npm audit --audit-level=none 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "proggy", 3 | "version": "4.0.0", 4 | "files": [ 5 | "bin/", 6 | "lib/" 7 | ], 8 | "main": "lib/index.js", 9 | "description": "Progress bar updates at a distance", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/npm/proggy.git" 13 | }, 14 | "author": "GitHub Inc.", 15 | "license": "ISC", 16 | "scripts": { 17 | "test": "tap", 18 | "posttest": "npm run lint", 19 | "snap": "tap", 20 | "postsnap": "eslint lib test --fix", 21 | "lint": "npm run eslint", 22 | "postlint": "template-oss-check", 23 | "lintfix": "npm run eslint -- --fix", 24 | "template-oss-apply": "template-oss-apply --force", 25 | "eslint": "eslint \"**/*.{js,cjs,ts,mjs,jsx,tsx}\"" 26 | }, 27 | "devDependencies": { 28 | "@npmcli/eslint-config": "^6.0.0", 29 | "@npmcli/template-oss": "4.28.1", 30 | "chalk": "^4.1.2", 31 | "cli-progress": "^3.10.0", 32 | "npmlog": "^7.0.0", 33 | "tap": "^16.0.1" 34 | }, 35 | "tap": { 36 | "coverage-map": "map.js", 37 | "nyc-arg": [ 38 | "--exclude", 39 | "tap-snapshots/**" 40 | ] 41 | }, 42 | "engines": { 43 | "node": "^20.17.0 || >=22.9.0" 44 | }, 45 | "templateOSS": { 46 | "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", 47 | "version": "4.28.1", 48 | "publish": true 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Bug 4 | description: File a bug/issue 5 | title: "[BUG] " 6 | labels: [ Bug, Needs Triage ] 7 | 8 | body: 9 | - type: checkboxes 10 | attributes: 11 | label: Is there an existing issue for this? 12 | description: Please [search here](./issues) to see if an issue already exists for your problem. 13 | options: 14 | - label: I have searched the existing issues 15 | required: true 16 | - type: textarea 17 | attributes: 18 | label: Current Behavior 19 | description: A clear & concise description of what you're experiencing. 20 | validations: 21 | required: false 22 | - type: textarea 23 | attributes: 24 | label: Expected Behavior 25 | description: A clear & concise description of what you expected to happen. 26 | validations: 27 | required: false 28 | - type: textarea 29 | attributes: 30 | label: Steps To Reproduce 31 | description: Steps to reproduce the behavior. 32 | value: | 33 | 1. In this environment... 34 | 2. With this config... 35 | 3. Run '...' 36 | 4. See error... 37 | validations: 38 | required: false 39 | - type: textarea 40 | attributes: 41 | label: Environment 42 | description: | 43 | examples: 44 | - **npm**: 7.6.3 45 | - **Node**: 13.14.0 46 | - **OS**: Ubuntu 20.04 47 | - **platform**: Macbook Pro 48 | value: | 49 | - npm: 50 | - Node: 51 | - OS: 52 | - platform: 53 | validations: 54 | required: false 55 | -------------------------------------------------------------------------------- /lib/tracker.js: -------------------------------------------------------------------------------- 1 | // The tracker class is intentionally as naive as possible. it is just 2 | // an ergonomic wrapper around process.emit('progress', ...) 3 | const EE = require('events') 4 | class Tracker extends EE { 5 | constructor (name, key, total) { 6 | super() 7 | if (!name) { 8 | throw new Error('proggy: Tracker needs a name') 9 | } 10 | 11 | if (typeof key === 'number' && !total) { 12 | total = key 13 | key = null 14 | } 15 | 16 | if (!total) { 17 | total = 100 18 | } 19 | 20 | if (!key) { 21 | key = name 22 | } 23 | 24 | this.done = false 25 | this.name = name 26 | this.key = key 27 | this.value = 0 28 | this.total = total 29 | } 30 | 31 | finish (metadata = {}) { 32 | this.update(this.total, this.total, metadata) 33 | } 34 | 35 | update (value, total, metadata) { 36 | if (!metadata) { 37 | if (total && typeof total === 'object') { 38 | metadata = total 39 | } else { 40 | metadata = {} 41 | } 42 | } 43 | if (typeof total !== 'number') { 44 | total = this.total 45 | } 46 | 47 | if (this.done) { 48 | const msg = `proggy: updating completed tracker: ${JSON.stringify(this.key)}` 49 | throw new Error(msg) 50 | } 51 | this.value = value 52 | this.total = total 53 | const done = this.value >= this.total 54 | process.emit('progress', this.key, { 55 | ...metadata, 56 | name: this.name, 57 | key: this.key, 58 | value, 59 | total, 60 | done, 61 | }) 62 | if (done) { 63 | this.done = true 64 | this.emit('done') 65 | } 66 | } 67 | } 68 | module.exports = Tracker 69 | -------------------------------------------------------------------------------- /.github/actions/create-check/action.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: 'Create Check' 4 | inputs: 5 | name: 6 | required: true 7 | token: 8 | required: true 9 | sha: 10 | required: true 11 | check-name: 12 | default: '' 13 | outputs: 14 | check-id: 15 | value: ${{ steps.create-check.outputs.check_id }} 16 | runs: 17 | using: "composite" 18 | steps: 19 | - name: Get Workflow Job 20 | uses: actions/github-script@v7 21 | id: workflow 22 | env: 23 | JOB_NAME: "${{ inputs.name }}" 24 | SHA: "${{ inputs.sha }}" 25 | with: 26 | result-encoding: string 27 | script: | 28 | const { repo: { owner, repo}, runId, serverUrl } = context 29 | const { JOB_NAME, SHA } = process.env 30 | 31 | const job = await github.rest.actions.listJobsForWorkflowRun({ 32 | owner, 33 | repo, 34 | run_id: runId, 35 | per_page: 100 36 | }).then(r => r.data.jobs.find(j => j.name.endsWith(JOB_NAME))) 37 | 38 | return [ 39 | `This check is assosciated with ${serverUrl}/${owner}/${repo}/commit/${SHA}.`, 40 | 'Run logs:', 41 | job?.html_url || `could not be found for a job ending with: "${JOB_NAME}"`, 42 | ].join(' ') 43 | - name: Create Check 44 | uses: LouisBrunner/checks-action@v1.6.0 45 | id: create-check 46 | with: 47 | token: ${{ inputs.token }} 48 | sha: ${{ inputs.sha }} 49 | status: in_progress 50 | name: ${{ inputs.check-name || inputs.name }} 51 | output: | 52 | {"summary":"${{ steps.workflow.outputs.result }}"} 53 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Pull Request 4 | 5 | on: 6 | pull_request: 7 | types: 8 | - opened 9 | - reopened 10 | - edited 11 | - synchronize 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | commitlint: 18 | name: Lint Commits 19 | if: github.repository_owner == 'npm' 20 | runs-on: ubuntu-latest 21 | defaults: 22 | run: 23 | shell: bash 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 0 29 | - name: Setup Git User 30 | run: | 31 | git config --global user.email "npm-cli+bot@github.com" 32 | git config --global user.name "npm CLI robot" 33 | - name: Setup Node 34 | uses: actions/setup-node@v4 35 | id: node 36 | with: 37 | node-version: 22.x 38 | check-latest: contains('22.x', '.x') 39 | - name: Install Latest npm 40 | uses: ./.github/actions/install-latest-npm 41 | with: 42 | node: ${{ steps.node.outputs.node-version }} 43 | - name: Install Dependencies 44 | run: npm i --ignore-scripts --no-audit --no-fund 45 | - name: Run Commitlint on Commits 46 | id: commit 47 | continue-on-error: true 48 | run: npx --offline commitlint -V --from 'origin/${{ github.base_ref }}' --to ${{ github.event.pull_request.head.sha }} 49 | - name: Run Commitlint on PR Title 50 | if: steps.commit.outcome == 'failure' 51 | env: 52 | PR_TITLE: ${{ github.event.pull_request.title }} 53 | run: echo "$PR_TITLE" | npx --offline commitlint -V 54 | -------------------------------------------------------------------------------- /test/tracker.js: -------------------------------------------------------------------------------- 1 | const Tracker = require('../lib/tracker.js') 2 | const t = require('tap') 3 | 4 | t.test('ctor', t => { 5 | t.throws(() => new Tracker(), { 6 | message: 'proggy: Tracker needs a name', 7 | }) 8 | t.matchSnapshot(new Tracker('hello'), 'name, no key or total') 9 | t.matchSnapshot(new Tracker('hello', 5), 'name and total') 10 | t.matchSnapshot(new Tracker('hello', 'hellokey'), 'name and key') 11 | t.matchSnapshot(new Tracker('hello', 'hellokey', 5), 'name, key, and total') 12 | t.end() 13 | }) 14 | 15 | t.test('emit some events', t => { 16 | const listener = (key, data) => 17 | t.matchSnapshot([key, data], 'progress event') 18 | 19 | t.teardown(() => process.removeListener('progress', listener)) 20 | process.on('progress', listener) 21 | 22 | const tracker = new Tracker('hello', 'key', 10) 23 | // 6 updates, 1 throw for update after done 24 | t.plan(7) 25 | tracker.on('done', () => { 26 | t.throws(() => tracker.update(1, 2), { 27 | message: 'proggy: updating completed tracker: "key"', 28 | }) 29 | t.end() 30 | }) 31 | tracker.update(2) 32 | tracker.update(2, 5, { message: 'reduced total' }) 33 | tracker.update(3, { message: 'no change to total' }) 34 | tracker.update(7, 100, { message: 'increased total' }) 35 | tracker.update(4, 200, { message: 'reduced value' }) 36 | tracker.update(100, 100, { message: 'implicitly done' }) 37 | }) 38 | 39 | t.test('finish() alias for update(total, total)', t => { 40 | const runTest = data => t => { 41 | const tracker = new Tracker('hello', 'key', 10) 42 | tracker.on('done', doneData => { 43 | t.matchSnapshot(doneData, 'data received by done event') 44 | t.end() 45 | }) 46 | tracker.finish(data) 47 | } 48 | t.test('run with data', runTest({ hello: 'world' })) 49 | t.test('run without', runTest()) 50 | t.end() 51 | }) 52 | -------------------------------------------------------------------------------- /.github/actions/install-latest-npm/action.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: 'Install Latest npm' 4 | description: 'Install the latest version of npm compatible with the Node version' 5 | inputs: 6 | node: 7 | description: 'Current Node version' 8 | required: true 9 | runs: 10 | using: "composite" 11 | steps: 12 | # node 10/12/14 ship with npm@6, which is known to fail when updating itself in windows 13 | - name: Update Windows npm 14 | if: | 15 | runner.os == 'Windows' && ( 16 | startsWith(inputs.node, 'v10.') || 17 | startsWith(inputs.node, 'v12.') || 18 | startsWith(inputs.node, 'v14.') 19 | ) 20 | shell: cmd 21 | run: | 22 | curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz 23 | tar xf npm-7.5.4.tgz 24 | cd package 25 | node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz 26 | cd .. 27 | rmdir /s /q package 28 | - name: Install Latest npm 29 | shell: bash 30 | env: 31 | NODE_VERSION: ${{ inputs.node }} 32 | working-directory: ${{ runner.temp }} 33 | run: | 34 | MATCH="" 35 | SPECS=("latest" "next-10" "next-9" "next-8" "next-7" "next-6") 36 | 37 | echo "node@$NODE_VERSION" 38 | 39 | for SPEC in ${SPECS[@]}; do 40 | ENGINES=$(npm view npm@$SPEC --json | jq -r '.engines.node') 41 | echo "Checking if node@$NODE_VERSION satisfies npm@$SPEC ($ENGINES)" 42 | 43 | if npx semver -r "$ENGINES" "$NODE_VERSION" > /dev/null; then 44 | MATCH=$SPEC 45 | echo "Found compatible version: npm@$MATCH" 46 | break 47 | fi 48 | done 49 | 50 | if [ -z $MATCH ]; then 51 | echo "Could not find a compatible version of npm for node@$NODE_VERSION" 52 | exit 1 53 | fi 54 | 55 | npm i --prefer-online --no-fund --no-audit -g npm@$MATCH 56 | - name: npm Version 57 | shell: bash 58 | run: npm -v 59 | -------------------------------------------------------------------------------- /docs/examples/npmlog.js: -------------------------------------------------------------------------------- 1 | const proggy = require('../../lib') 2 | 3 | // set up our display thing 4 | const npmlog = require('npmlog') 5 | npmlog.enableProgress() 6 | const group = npmlog.newGroup('npmlog example') 7 | 8 | // update it with events from the proggy client 9 | const client = proggy.createClient({ 10 | // don't show reverse progress 11 | // this is false by default 12 | normalize: true, 13 | }) 14 | 15 | const bars = {} 16 | 17 | // new bar is created, tell npmlog about it 18 | client.on('bar', (key, data) => { 19 | bars[key] = group.newItem(key, data.total) 20 | group.notice('starting progress bar', key, data) 21 | }) 22 | 23 | // got some progress for a progress bar, tell npmlog to show it 24 | client.on('progress', (key, data) => { 25 | const bar = bars[key] 26 | group.verbose(key, data.actualValue, data.actualTotal, `${Math.floor(data.value)}%`) 27 | bar.addWork(bar.workTodo - data.total) 28 | bars[key].completeWork(data.value - bar.workDone) 29 | }) 30 | 31 | // a bar is done, tell npmlog to stop updating it 32 | client.on('barDone', (key, data) => { 33 | group.notice('task completed', key, data) 34 | bars[key].finish() 35 | }) 36 | 37 | // all bars done, tell npmlog to close entirely 38 | client.on('done', () => { 39 | group.notice('all progress completed') 40 | npmlog.disableProgress() 41 | }) 42 | 43 | // the thing that emits the events 44 | let b1value = 0 45 | let b1total = 100 46 | let b2value = 0 47 | const b2total = 200 48 | let b3value = 0 49 | const b3total = 50 50 | 51 | const b1 = proggy.createTracker('bar 1') 52 | const b2 = proggy.createTracker('bar 2') 53 | const b3 = proggy.createTracker('bar 3') 54 | 55 | let i = 0 56 | const interval = setInterval(() => { 57 | const inc = Math.ceil(Math.random() * 10) 58 | 59 | if (b1value < b1total) { 60 | if ((i++ % 2 === 0) && b1total < 2000) { 61 | b1total += inc * 3 62 | } else { 63 | b1value += inc 64 | } 65 | b1.update(Math.min(b1total, b1value), b1total) 66 | } 67 | 68 | if (b2value < b2total) { 69 | b2.update(Math.min(b2total, b2value += inc), b2total) 70 | } 71 | 72 | if (b3value < b3total) { 73 | b3value = Math.ceil(b3value + 0.003 * (b3total + b3value)) 74 | b3.update(Math.min(b3value, b3total), b3total) 75 | } 76 | 77 | if (b1value >= b1total && b2value >= b2total && b3value >= b3total) { 78 | clearInterval(interval) 79 | } 80 | }, 50) 81 | -------------------------------------------------------------------------------- /.github/workflows/release-integration.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Release Integration 4 | 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | releases: 9 | required: true 10 | type: string 11 | description: 'A json array of releases. Required fields: publish: tagName, publishTag. publish check: pkgName, version' 12 | workflow_call: 13 | inputs: 14 | releases: 15 | required: true 16 | type: string 17 | description: 'A json array of releases. Required fields: publish: tagName, publishTag. publish check: pkgName, version' 18 | secrets: 19 | PUBLISH_TOKEN: 20 | required: true 21 | 22 | permissions: 23 | contents: read 24 | id-token: write 25 | 26 | jobs: 27 | publish: 28 | name: Publish 29 | runs-on: ubuntu-latest 30 | defaults: 31 | run: 32 | shell: bash 33 | permissions: 34 | id-token: write 35 | steps: 36 | - name: Checkout 37 | uses: actions/checkout@v4 38 | with: 39 | ref: ${{ fromJSON(inputs.releases)[0].tagName }} 40 | - name: Setup Git User 41 | run: | 42 | git config --global user.email "npm-cli+bot@github.com" 43 | git config --global user.name "npm CLI robot" 44 | - name: Setup Node 45 | uses: actions/setup-node@v4 46 | id: node 47 | with: 48 | node-version: 22.x 49 | check-latest: contains('22.x', '.x') 50 | - name: Install Latest npm 51 | uses: ./.github/actions/install-latest-npm 52 | with: 53 | node: ${{ steps.node.outputs.node-version }} 54 | - name: Install Dependencies 55 | run: npm i --ignore-scripts --no-audit --no-fund 56 | - name: Set npm authToken 57 | run: npm config set '//registry.npmjs.org/:_authToken'=\${PUBLISH_TOKEN} 58 | - name: Publish 59 | env: 60 | PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} 61 | RELEASES: ${{ inputs.releases }} 62 | run: | 63 | EXIT_CODE=0 64 | 65 | for release in $(echo $RELEASES | jq -r '.[] | @base64'); do 66 | PUBLISH_TAG=$(echo "$release" | base64 --decode | jq -r .publishTag) 67 | npm publish --provenance --tag="$PUBLISH_TAG" 68 | STATUS=$? 69 | if [[ "$STATUS" -eq 1 ]]; then 70 | EXIT_CODE=$STATUS 71 | fi 72 | done 73 | 74 | exit $EXIT_CODE 75 | -------------------------------------------------------------------------------- /docs/examples/cli-progress.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk') 2 | const proggy = require('../../lib') 3 | 4 | // note that data.actualValue and data.actualTotal will reflect the real 5 | // done/remaining values. data.value will always be less than 100, and 6 | // data.total will always be 100, so we never show reverse motion. 7 | // eslint-disable-next-line max-len 8 | const format = `[${chalk.cyan('{bar}')}] {percentage}% | {name} {actualValue}/{actualTotal} | {duration_formatted} ETA: {eta_formatted}` 9 | 10 | // set up our display thing 11 | const cliProgress = require('cli-progress') 12 | const multibar = new cliProgress.MultiBar({ 13 | clearOnComplete: false, 14 | hideCursor: true, 15 | format, 16 | barCompleteChar: '\u2588', 17 | barIncompleteChar: '\u2591', 18 | }, cliProgress.Presets.shades_grey) 19 | 20 | // update it with events from the proggy client 21 | const client = proggy.createClient({ 22 | // don't show reverse progress 23 | // this is false by default 24 | normalize: true, 25 | }) 26 | const bars = {} 27 | // new bar is created, tell multibar about it 28 | client.on('bar', (key, data) => { 29 | bars[key] = multibar.create(data.total) 30 | }) 31 | // got some progress for a progress bar, tell multibar to show it 32 | client.on('progress', (key, data) => { 33 | bars[key].update(data.value, data) 34 | bars[key].setTotal(data.total) 35 | }) 36 | // a bar is done, tell multibar to stop updating it 37 | client.on('barDone', (key) => { 38 | bars[key].stop() 39 | }) 40 | // all bars done, tell multibar to close entirely 41 | client.on('done', () => { 42 | multibar.stop() 43 | }) 44 | 45 | // the thing that emits the events 46 | let b1value = 0 47 | let b1total = 100 48 | let b2value = 0 49 | const b2total = 2000 50 | let b3value = 0 51 | const b3total = 500 52 | 53 | const b1 = proggy.createTracker('bar 1') 54 | const b2 = proggy.createTracker('bar 2') 55 | const b3 = proggy.createTracker('bar 3') 56 | 57 | const interval = setInterval(() => { 58 | const inc = Math.ceil(Math.random() * 10) 59 | if (b3value < b3total) { 60 | b3value = Math.ceil(b3value + 0.003 * (b3total + b3value)) 61 | b3.update(Math.min(b3value, b3total), b3total) 62 | } 63 | if (2.9 * Math.random() < 1 && b1value < b1total && b1total < 2000) { 64 | b1total += inc * 3 65 | } 66 | if (b1value < b1total) { 67 | b1.update(Math.min(b1total, b1value += inc), b1total) 68 | } 69 | if (b2value < b2total) { 70 | b2.update(Math.min(b2total, b2value += inc), b2total) 71 | } 72 | 73 | if (b1value >= b1total && b2value >= b2total && b3value >= b3total) { 74 | clearInterval(interval) 75 | } 76 | }, 50) 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | <!-- This file is automatically added by @npmcli/template-oss. Do not edit. --> 2 | 3 | # Contributing 4 | 5 | ## Code of Conduct 6 | 7 | All interactions in the **npm** organization on GitHub are considered to be covered by our standard [Code of Conduct](https://docs.npmjs.com/policies/conduct). 8 | 9 | ## Reporting Bugs 10 | 11 | Before submitting a new bug report please search for an existing or similar report. 12 | 13 | Use one of our existing issue templates if you believe you've come across a unique problem. 14 | 15 | Duplicate issues, or issues that don't use one of our templates may get closed without a response. 16 | 17 | ## Pull Request Conventions 18 | 19 | ### Commits 20 | 21 | We use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). 22 | 23 | When opening a pull request please be sure that either the pull request title, or each commit in the pull request, has one of the following prefixes: 24 | 25 | - `feat`: For when introducing a new feature. The result will be a new semver minor version of the package when it is next published. 26 | - `fix`: For bug fixes. The result will be a new semver patch version of the package when it is next published. 27 | - `docs`: For documentation updates. The result will be a new semver patch version of the package when it is next published. 28 | - `chore`: For changes that do not affect the published module. Often these are changes to tests. The result will be *no* change to the version of the package when it is next published (as the commit does not affect the published version). 29 | 30 | ### Test Coverage 31 | 32 | Pull requests made against this repo will run `npm test` automatically. Please make sure tests pass locally before submitting a PR. 33 | 34 | Every new feature or bug fix should come with a corresponding test or tests that validate the solutions. Testing also reports on code coverage and will fail if code coverage drops. 35 | 36 | ### Linting 37 | 38 | Linting is also done automatically once tests pass. `npm run lintfix` will fix most linting errors automatically. 39 | 40 | Please make sure linting passes before submitting a PR. 41 | 42 | ## What _not_ to contribute? 43 | 44 | ### Dependencies 45 | 46 | It should be noted that our team does not accept third-party dependency updates/PRs. If you submit a PR trying to update our dependencies we will close it with or without a reference to these contribution guidelines. 47 | 48 | ### Tools/Automation 49 | 50 | Our core team is responsible for the maintenance of the tooling/automation in this project and we ask contributors to not make changes to these when contributing (e.g. `.github/*`, `.eslintrc.json`, `.licensee.json`). Most of those files also have a header at the top to remind folks they are automatically generated. Pull requests that alter these will not be accepted. 51 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | const EE = require('events') 2 | const onProgress = Symbol('onProgress') 3 | const bars = Symbol('bars') 4 | const listener = Symbol('listener') 5 | const normData = Symbol('normData') 6 | class Client extends EE { 7 | constructor ({ normalize = false, stopOnDone = false } = {}) { 8 | super() 9 | this.normalize = !!normalize 10 | this.stopOnDone = !!stopOnDone 11 | this[bars] = new Map() 12 | this[listener] = null 13 | } 14 | 15 | get size () { 16 | return this[bars].size 17 | } 18 | 19 | get listening () { 20 | return !!this[listener] 21 | } 22 | 23 | addListener (...args) { 24 | return this.on(...args) 25 | } 26 | 27 | on (ev, ...args) { 28 | if (ev === 'progress' && !this[listener]) { 29 | this.start() 30 | } 31 | return super.on(ev, ...args) 32 | } 33 | 34 | off (ev, ...args) { 35 | return this.removeListener(ev, ...args) 36 | } 37 | 38 | removeListener (ev, ...args) { 39 | const ret = super.removeListener(ev, ...args) 40 | if (ev === 'progress' && this.listeners(ev).length === 0) { 41 | this.stop() 42 | } 43 | return ret 44 | } 45 | 46 | stop () { 47 | if (this[listener]) { 48 | process.removeListener('progress', this[listener]) 49 | this[listener] = null 50 | } 51 | } 52 | 53 | start () { 54 | if (!this[listener]) { 55 | this[listener] = (...args) => this[onProgress](...args) 56 | process.on('progress', this[listener]) 57 | } 58 | } 59 | 60 | [onProgress] (key, data) { 61 | data = this[normData](key, data) 62 | if (!this[bars].has(key)) { 63 | this.emit('bar', key, data) 64 | } 65 | this[bars].set(key, data) 66 | this.emit('progress', key, data) 67 | if (data.done) { 68 | this[bars].delete(key) 69 | this.emit('barDone', key, data) 70 | if (this.size === 0) { 71 | if (this.stopOnDone) { 72 | this.stop() 73 | } 74 | this.emit('done') 75 | } 76 | } 77 | } 78 | 79 | [normData] (key, data) { 80 | const actualValue = data.value 81 | const actualTotal = data.total 82 | let value = actualValue 83 | let total = actualTotal 84 | const done = data.done || value >= total 85 | if (this.normalize) { 86 | const bar = this[bars].get(key) 87 | total = 100 88 | if (done) { 89 | value = 100 90 | } else { 91 | // show value as a portion of 100 92 | const pct = 100 * actualValue / actualTotal 93 | if (bar) { 94 | // don't ever go backwards, and don't stand still 95 | // move at least 1% of the remaining value if it wouldn't move. 96 | value = (pct > bar.value) ? pct 97 | : (100 - bar.value) / 100 + bar.value 98 | } 99 | } 100 | } 101 | // include the key 102 | return { 103 | ...data, 104 | key, 105 | name: data.name || key, 106 | value, 107 | total, 108 | actualValue, 109 | actualTotal, 110 | done, 111 | } 112 | } 113 | } 114 | module.exports = Client 115 | -------------------------------------------------------------------------------- /tap-snapshots/test/tracker.js.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/tracker.js TAP ctor > name and key 1`] = ` 9 | Tracker { 10 | "_events": Null Object {}, 11 | "_eventsCount": 0, 12 | "_maxListeners": undefined, 13 | "done": false, 14 | "key": "hellokey", 15 | "name": "hello", 16 | "total": 100, 17 | "value": 0, 18 | } 19 | ` 20 | 21 | exports[`test/tracker.js TAP ctor > name and total 1`] = ` 22 | Tracker { 23 | "_events": Null Object {}, 24 | "_eventsCount": 0, 25 | "_maxListeners": undefined, 26 | "done": false, 27 | "key": "hello", 28 | "name": "hello", 29 | "total": 5, 30 | "value": 0, 31 | } 32 | ` 33 | 34 | exports[`test/tracker.js TAP ctor > name, key, and total 1`] = ` 35 | Tracker { 36 | "_events": Null Object {}, 37 | "_eventsCount": 0, 38 | "_maxListeners": undefined, 39 | "done": false, 40 | "key": "hellokey", 41 | "name": "hello", 42 | "total": 5, 43 | "value": 0, 44 | } 45 | ` 46 | 47 | exports[`test/tracker.js TAP ctor > name, no key or total 1`] = ` 48 | Tracker { 49 | "_events": Null Object {}, 50 | "_eventsCount": 0, 51 | "_maxListeners": undefined, 52 | "done": false, 53 | "key": "hello", 54 | "name": "hello", 55 | "total": 100, 56 | "value": 0, 57 | } 58 | ` 59 | 60 | exports[`test/tracker.js TAP emit some events > progress event 1`] = ` 61 | Array [ 62 | "key", 63 | Object { 64 | "done": false, 65 | "key": "key", 66 | "name": "hello", 67 | "total": 10, 68 | "value": 2, 69 | }, 70 | ] 71 | ` 72 | 73 | exports[`test/tracker.js TAP emit some events > progress event 2`] = ` 74 | Array [ 75 | "key", 76 | Object { 77 | "done": false, 78 | "key": "key", 79 | "message": "reduced total", 80 | "name": "hello", 81 | "total": 5, 82 | "value": 2, 83 | }, 84 | ] 85 | ` 86 | 87 | exports[`test/tracker.js TAP emit some events > progress event 3`] = ` 88 | Array [ 89 | "key", 90 | Object { 91 | "done": false, 92 | "key": "key", 93 | "message": "no change to total", 94 | "name": "hello", 95 | "total": 5, 96 | "value": 3, 97 | }, 98 | ] 99 | ` 100 | 101 | exports[`test/tracker.js TAP emit some events > progress event 4`] = ` 102 | Array [ 103 | "key", 104 | Object { 105 | "done": false, 106 | "key": "key", 107 | "message": "increased total", 108 | "name": "hello", 109 | "total": 100, 110 | "value": 7, 111 | }, 112 | ] 113 | ` 114 | 115 | exports[`test/tracker.js TAP emit some events > progress event 5`] = ` 116 | Array [ 117 | "key", 118 | Object { 119 | "done": false, 120 | "key": "key", 121 | "message": "reduced value", 122 | "name": "hello", 123 | "total": 200, 124 | "value": 4, 125 | }, 126 | ] 127 | ` 128 | 129 | exports[`test/tracker.js TAP emit some events > progress event 6`] = ` 130 | Array [ 131 | "key", 132 | Object { 133 | "done": true, 134 | "key": "key", 135 | "message": "implicitly done", 136 | "name": "hello", 137 | "total": 100, 138 | "value": 100, 139 | }, 140 | ] 141 | ` 142 | 143 | exports[`test/tracker.js TAP finish() alias for update(total, total) run with data > data received by done event 1`] = ` 144 | undefined 145 | ` 146 | 147 | exports[`test/tracker.js TAP finish() alias for update(total, total) run without > data received by done event 1`] = ` 148 | undefined 149 | ` 150 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: CI 4 | 5 | on: 6 | workflow_dispatch: 7 | pull_request: 8 | push: 9 | branches: 10 | - main 11 | schedule: 12 | # "At 09:00 UTC (02:00 PT) on Monday" https://crontab.guru/#0_9_*_*_1 13 | - cron: "0 9 * * 1" 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | lint: 20 | name: Lint 21 | if: github.repository_owner == 'npm' 22 | runs-on: ubuntu-latest 23 | defaults: 24 | run: 25 | shell: bash 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | - name: Setup Git User 30 | run: | 31 | git config --global user.email "npm-cli+bot@github.com" 32 | git config --global user.name "npm CLI robot" 33 | - name: Setup Node 34 | uses: actions/setup-node@v4 35 | id: node 36 | with: 37 | node-version: 22.x 38 | check-latest: contains('22.x', '.x') 39 | - name: Install Latest npm 40 | uses: ./.github/actions/install-latest-npm 41 | with: 42 | node: ${{ steps.node.outputs.node-version }} 43 | - name: Install Dependencies 44 | run: npm i --ignore-scripts --no-audit --no-fund 45 | - name: Lint 46 | run: npm run lint --ignore-scripts 47 | - name: Post Lint 48 | run: npm run postlint --ignore-scripts 49 | 50 | test: 51 | name: Test - ${{ matrix.platform.name }} - ${{ matrix.node-version }} 52 | if: github.repository_owner == 'npm' 53 | strategy: 54 | fail-fast: false 55 | matrix: 56 | platform: 57 | - name: Linux 58 | os: ubuntu-latest 59 | shell: bash 60 | - name: macOS 61 | os: macos-latest 62 | shell: bash 63 | - name: macOS 64 | os: macos-13 65 | shell: bash 66 | - name: Windows 67 | os: windows-latest 68 | shell: cmd 69 | node-version: 70 | - 20.17.0 71 | - 20.x 72 | - 22.9.0 73 | - 22.x 74 | exclude: 75 | - platform: { name: macOS, os: macos-13, shell: bash } 76 | node-version: 20.17.0 77 | - platform: { name: macOS, os: macos-13, shell: bash } 78 | node-version: 20.x 79 | - platform: { name: macOS, os: macos-13, shell: bash } 80 | node-version: 22.9.0 81 | - platform: { name: macOS, os: macos-13, shell: bash } 82 | node-version: 22.x 83 | runs-on: ${{ matrix.platform.os }} 84 | defaults: 85 | run: 86 | shell: ${{ matrix.platform.shell }} 87 | steps: 88 | - name: Checkout 89 | uses: actions/checkout@v4 90 | - name: Setup Git User 91 | run: | 92 | git config --global user.email "npm-cli+bot@github.com" 93 | git config --global user.name "npm CLI robot" 94 | - name: Setup Node 95 | uses: actions/setup-node@v4 96 | id: node 97 | with: 98 | node-version: ${{ matrix.node-version }} 99 | check-latest: contains(matrix.node-version, '.x') 100 | - name: Install Latest npm 101 | uses: ./.github/actions/install-latest-npm 102 | with: 103 | node: ${{ steps.node.outputs.node-version }} 104 | - name: Install Dependencies 105 | run: npm i --ignore-scripts --no-audit --no-fund 106 | - name: Add Problem Matcher 107 | run: echo "::add-matcher::.github/matchers/tap.json" 108 | - name: Test 109 | run: npm test --ignore-scripts 110 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [4.0.0](https://github.com/npm/proggy/compare/v3.0.0...v4.0.0) (2025-10-22) 4 | ### ⚠️ BREAKING CHANGES 5 | * align to npm 11 node engine range (#100) 6 | ### Bug Fixes 7 | * [`9ddf28f`](https://github.com/npm/proggy/commit/9ddf28f6b9644a87cdc4de00795441abbbbd8ec2) [#100](https://github.com/npm/proggy/pull/100) align to npm 11 node engine range (#100) (@owlstronaut) 8 | ### Chores 9 | * [`9f59807`](https://github.com/npm/proggy/commit/9f59807e53f34bfe754bc1934ddeb540ddbd6348) [#94](https://github.com/npm/proggy/pull/94) postinstall workflow updates (#94) (@owlstronaut) 10 | * [`76c68e4`](https://github.com/npm/proggy/commit/76c68e47ffccfcf2d72f413f1faa8ca636ebb108) [#99](https://github.com/npm/proggy/pull/99) bump @npmcli/template-oss from 4.26.0 to 4.27.1 (#99) (@dependabot[bot], @npm-cli-bot) 11 | 12 | ## [3.0.0](https://github.com/npm/proggy/compare/v2.0.0...v3.0.0) (2024-09-24) 13 | ### ⚠️ BREAKING CHANGES 14 | * `proggy` now supports node `^18.17.0 || >=20.5.0` 15 | ### Bug Fixes 16 | * [`9bff3e8`](https://github.com/npm/proggy/commit/9bff3e8eca0a79f9a5fb734ce63b67b7cb2f33d5) [#89](https://github.com/npm/proggy/pull/89) align to npm 10 node engine range (@hashtagchris) 17 | ### Chores 18 | * [`91e9939`](https://github.com/npm/proggy/commit/91e993936040e14a691c6a60c2a1addaf3b68a2e) [#91](https://github.com/npm/proggy/pull/91) enable auto publish (#91) (@reggi) 19 | * [`c601919`](https://github.com/npm/proggy/commit/c60191930cefa87e531b6193996e353c081856ca) [#89](https://github.com/npm/proggy/pull/89) run template-oss-apply (@hashtagchris) 20 | * [`8d9ae26`](https://github.com/npm/proggy/commit/8d9ae2664c90f80a4a98098b4bb362daf3b3e0d1) [#87](https://github.com/npm/proggy/pull/87) bump @npmcli/eslint-config from 4.0.5 to 5.0.0 (@dependabot[bot]) 21 | * [`9795e99`](https://github.com/npm/proggy/commit/9795e993dc4b909f82134e67bdb17c9dc6f51ce5) [#76](https://github.com/npm/proggy/pull/76) linting: no-unused-vars (@lukekarrys) 22 | * [`36b6935`](https://github.com/npm/proggy/commit/36b69351b1a363e6427415820dadeb755b0361fa) [#76](https://github.com/npm/proggy/pull/76) bump @npmcli/template-oss to 4.22.0 (@lukekarrys) 23 | * [`ed3e179`](https://github.com/npm/proggy/commit/ed3e1798abb890a6cd1b5b4a79ceaf8bb1bc0e0e) [#36](https://github.com/npm/proggy/pull/36) bump npmlog from 6.0.2 to 7.0.0 (@dependabot[bot]) 24 | * [`5b40616`](https://github.com/npm/proggy/commit/5b406161b8f18078c848171a1de34f468aac27d7) [#35](https://github.com/npm/proggy/pull/35) bump @npmcli/eslint-config from 3.1.0 to 4.0.0 (@dependabot[bot]) 25 | * [`253f9d4`](https://github.com/npm/proggy/commit/253f9d492b41cbb16155ad1e02f11d98c9e442f8) [#88](https://github.com/npm/proggy/pull/88) postinstall for dependabot template-oss PR (@hashtagchris) 26 | * [`beaba03`](https://github.com/npm/proggy/commit/beaba03289b2c0bf6678004b8526b6d03be3e23a) [#88](https://github.com/npm/proggy/pull/88) bump @npmcli/template-oss from 4.23.1 to 4.23.3 (@dependabot[bot]) 27 | 28 | ## [2.0.0](https://github.com/npm/proggy/compare/v1.0.0...v2.0.0) (2022-10-10) 29 | 30 | ### ⚠️ BREAKING CHANGES 31 | 32 | * `proggy` is now compatible with the following semver range for node: `^14.17.0 || ^16.13.0 || >=18.0.0` 33 | 34 | ### Features 35 | 36 | * [`870d78f`](https://github.com/npm/proggy/commit/870d78fc9a8a88ccdd6f55bbe62b8c487d054489) [#28](https://github.com/npm/proggy/pull/28) postinstall for dependabot template-oss PR (@lukekarrys) 37 | 38 | ## [1.0.0](https://www.github.com/npm/proggy/compare/v0.0.1...v1.0.0) (2022-02-16) 39 | 40 | 41 | ### ⚠ BREAKING CHANGES 42 | 43 | * this drops support for node10 and non-LTS versions of node12 and node14 44 | 45 | ### Features 46 | 47 | * @npmcli/template-oss@2.7.1 ([#1](https://www.github.com/npm/proggy/issues/1)) ([5bfa329](https://www.github.com/npm/proggy/commit/5bfa3293ef9a8771625be646b5a52b4560a766a7)) 48 | -------------------------------------------------------------------------------- /test/client.js: -------------------------------------------------------------------------------- 1 | const Client = require('../lib/client.js') 2 | const t = require('tap') 3 | 4 | t.test('basic', t => { 5 | const c = new Client() 6 | t.equal(c.stopOnDone, false, 'stopOnDone false by default') 7 | t.equal(c.normalize, false, 'normalize false by default') 8 | t.equal(c.size, 0, 'size is 0') 9 | t.equal(c.listening, false, 'not listening') 10 | c.stop() 11 | t.equal(c.listening, false, 'still not listening') 12 | c.start() 13 | t.equal(c.listening, true, 'now listening') 14 | c.start() 15 | t.equal(c.listening, true, 'still listening') 16 | 17 | c.stop() 18 | c.once('progress', () => {}) 19 | t.equal(c.listening, true, 'listening because once handler added') 20 | c.emit('progress') 21 | t.equal(c.listening, false, 'not listening because once handler removed') 22 | 23 | c.addListener('progress', (key, data) => t.matchSnapshot([key, data], 'progress data')) 24 | t.equal(c.listening, true, 'listening, because progress event handler added') 25 | 26 | let sawProgress = false 27 | c.once('progress', () => { 28 | sawProgress = true 29 | }) 30 | 31 | let sawBarDone = false 32 | c.once('barDone', (key, data) => { 33 | sawBarDone = true 34 | t.matchSnapshot([key, data], 'barDone data') 35 | }) 36 | 37 | let sawBar = false 38 | c.once('bar', (key, data) => { 39 | sawBar = true 40 | t.matchSnapshot([key, data], 'bar data') 41 | }) 42 | 43 | process.emit('progress', 'key', { hello: 'world', value: 1, total: 99 }) 44 | t.equal(c.size, 1, '1 progress bar now') 45 | t.equal(sawBar, true, 'saw bar event') 46 | t.equal(sawProgress, true, 'saw progress event') 47 | 48 | process.emit('progress', 'yek', { hello: 'world', value: 5, total: 50 }) 49 | t.equal(c.size, 2, 'got a second bar') 50 | 51 | process.emit('progress', 'key', { hello: 'world', value: 100, total: 50 }) 52 | t.equal(c.size, 1, 'first progress bar ended') 53 | t.equal(sawBarDone, true, 'saw barDone event') 54 | 55 | process.emit('progress', 'yek', { hello: 'world', value: 100, total: 50 }) 56 | t.equal(c.size, 0, 'second progress bar ended, all done') 57 | 58 | process.emit('progress', 'foo', { hello: 'world', value: 100, total: 50 }) 59 | t.equal(c.size, 0, 'progress bar ended right away') 60 | 61 | c.off('progress', c.listeners('progress')[0]) 62 | t.equal(c.listening, false, 'stopped because last listener removed') 63 | 64 | t.end() 65 | }) 66 | 67 | t.test('stop on done', t => { 68 | const c = new Client({ stopOnDone: true }) 69 | c.on('progress', (key, data) => t.matchSnapshot([key, data], 'progress data')) 70 | process.emit('progress', 'key', { hello: 'world', value: 1, total: 99 }) 71 | process.emit('progress', 'key', { hello: 'world', value: 100, total: 100 }) 72 | t.equal(c.listening, false, 'not listening, because last bar done') 73 | t.end() 74 | }) 75 | 76 | t.test('normalize data', t => { 77 | const c = new Client({ normalize: true }) 78 | c.on('progress', (key, data) => t.matchSnapshot([key, data], 'progress data')) 79 | c.on('barDone', () => t.end()) 80 | process.emit('progress', 'key', { value: 1, total: 5 }) 81 | process.emit('progress', 'key', { value: 2, total: 12 }) 82 | process.emit('progress', 'key', { value: 3, total: 12 }) 83 | process.emit('progress', 'key', { value: 4, total: 13 }) 84 | process.emit('progress', 'key', { value: 6, total: 13 }) 85 | process.emit('progress', 'key', { value: 3, total: 6 }) 86 | process.emit('progress', 'key', { value: 5, total: 6 }) 87 | process.emit('progress', 'key', { value: 45, total: 50 }) 88 | process.emit('progress', 'key', { value: 45, total: 50 }) 89 | process.emit('progress', 'key', { value: 45, total: 50 }) 90 | process.emit('progress', 'key', { value: 45, total: 50 }) 91 | process.emit('progress', 'key', { value: 45, total: 50 }) 92 | process.emit('progress', 'key', { value: 45, total: 50 }) 93 | process.emit('progress', 'key', { value: 45, total: 50 }) 94 | process.emit('progress', 'key', { value: 45, total: 50 }) 95 | process.emit('progress', 'key', { value: 9001, total: 420 }) 96 | c.stop() 97 | }) 98 | -------------------------------------------------------------------------------- /.github/workflows/ci-release.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: CI - Release 4 | 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | ref: 9 | required: true 10 | type: string 11 | default: main 12 | workflow_call: 13 | inputs: 14 | ref: 15 | required: true 16 | type: string 17 | check-sha: 18 | required: true 19 | type: string 20 | 21 | permissions: 22 | contents: read 23 | checks: write 24 | 25 | jobs: 26 | lint-all: 27 | name: Lint All 28 | if: github.repository_owner == 'npm' 29 | runs-on: ubuntu-latest 30 | defaults: 31 | run: 32 | shell: bash 33 | steps: 34 | - name: Checkout 35 | uses: actions/checkout@v4 36 | with: 37 | ref: ${{ inputs.ref }} 38 | - name: Setup Git User 39 | run: | 40 | git config --global user.email "npm-cli+bot@github.com" 41 | git config --global user.name "npm CLI robot" 42 | - name: Create Check 43 | id: create-check 44 | if: ${{ inputs.check-sha }} 45 | uses: ./.github/actions/create-check 46 | with: 47 | name: "Lint All" 48 | token: ${{ secrets.GITHUB_TOKEN }} 49 | sha: ${{ inputs.check-sha }} 50 | - name: Setup Node 51 | uses: actions/setup-node@v4 52 | id: node 53 | with: 54 | node-version: 22.x 55 | check-latest: contains('22.x', '.x') 56 | - name: Install Latest npm 57 | uses: ./.github/actions/install-latest-npm 58 | with: 59 | node: ${{ steps.node.outputs.node-version }} 60 | - name: Install Dependencies 61 | run: npm i --ignore-scripts --no-audit --no-fund 62 | - name: Lint 63 | run: npm run lint --ignore-scripts 64 | - name: Post Lint 65 | run: npm run postlint --ignore-scripts 66 | - name: Conclude Check 67 | uses: LouisBrunner/checks-action@v1.6.0 68 | if: steps.create-check.outputs.check-id && always() 69 | with: 70 | token: ${{ secrets.GITHUB_TOKEN }} 71 | conclusion: ${{ job.status }} 72 | check_id: ${{ steps.create-check.outputs.check-id }} 73 | 74 | test-all: 75 | name: Test All - ${{ matrix.platform.name }} - ${{ matrix.node-version }} 76 | if: github.repository_owner == 'npm' 77 | strategy: 78 | fail-fast: false 79 | matrix: 80 | platform: 81 | - name: Linux 82 | os: ubuntu-latest 83 | shell: bash 84 | - name: macOS 85 | os: macos-latest 86 | shell: bash 87 | - name: macOS 88 | os: macos-13 89 | shell: bash 90 | - name: Windows 91 | os: windows-latest 92 | shell: cmd 93 | node-version: 94 | - 20.17.0 95 | - 20.x 96 | - 22.9.0 97 | - 22.x 98 | exclude: 99 | - platform: { name: macOS, os: macos-13, shell: bash } 100 | node-version: 20.17.0 101 | - platform: { name: macOS, os: macos-13, shell: bash } 102 | node-version: 20.x 103 | - platform: { name: macOS, os: macos-13, shell: bash } 104 | node-version: 22.9.0 105 | - platform: { name: macOS, os: macos-13, shell: bash } 106 | node-version: 22.x 107 | runs-on: ${{ matrix.platform.os }} 108 | defaults: 109 | run: 110 | shell: ${{ matrix.platform.shell }} 111 | steps: 112 | - name: Checkout 113 | uses: actions/checkout@v4 114 | with: 115 | ref: ${{ inputs.ref }} 116 | - name: Setup Git User 117 | run: | 118 | git config --global user.email "npm-cli+bot@github.com" 119 | git config --global user.name "npm CLI robot" 120 | - name: Create Check 121 | id: create-check 122 | if: ${{ inputs.check-sha }} 123 | uses: ./.github/actions/create-check 124 | with: 125 | name: "Test All - ${{ matrix.platform.name }} - ${{ matrix.node-version }}" 126 | token: ${{ secrets.GITHUB_TOKEN }} 127 | sha: ${{ inputs.check-sha }} 128 | - name: Setup Node 129 | uses: actions/setup-node@v4 130 | id: node 131 | with: 132 | node-version: ${{ matrix.node-version }} 133 | check-latest: contains(matrix.node-version, '.x') 134 | - name: Install Latest npm 135 | uses: ./.github/actions/install-latest-npm 136 | with: 137 | node: ${{ steps.node.outputs.node-version }} 138 | - name: Install Dependencies 139 | run: npm i --ignore-scripts --no-audit --no-fund 140 | - name: Add Problem Matcher 141 | run: echo "::add-matcher::.github/matchers/tap.json" 142 | - name: Test 143 | run: npm test --ignore-scripts 144 | - name: Conclude Check 145 | uses: LouisBrunner/checks-action@v1.6.0 146 | if: steps.create-check.outputs.check-id && always() 147 | with: 148 | token: ${{ secrets.GITHUB_TOKEN }} 149 | conclusion: ${{ job.status }} 150 | check_id: ${{ steps.create-check.outputs.check-id }} 151 | -------------------------------------------------------------------------------- /.github/workflows/post-dependabot.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Post Dependabot 4 | 5 | on: pull_request 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | template-oss: 12 | name: template-oss 13 | if: github.repository_owner == 'npm' && github.actor == 'dependabot[bot]' 14 | runs-on: ubuntu-latest 15 | defaults: 16 | run: 17 | shell: bash 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | with: 22 | ref: ${{ github.event.pull_request.head.ref }} 23 | - name: Setup Git User 24 | run: | 25 | git config --global user.email "npm-cli+bot@github.com" 26 | git config --global user.name "npm CLI robot" 27 | - name: Setup Node 28 | uses: actions/setup-node@v4 29 | id: node 30 | with: 31 | node-version: 22.x 32 | check-latest: contains('22.x', '.x') 33 | - name: Install Latest npm 34 | uses: ./.github/actions/install-latest-npm 35 | with: 36 | node: ${{ steps.node.outputs.node-version }} 37 | - name: Install Dependencies 38 | run: npm i --ignore-scripts --no-audit --no-fund 39 | - name: Fetch Dependabot Metadata 40 | id: metadata 41 | uses: dependabot/fetch-metadata@v1 42 | with: 43 | github-token: ${{ secrets.GITHUB_TOKEN }} 44 | 45 | # Dependabot can update multiple directories so we output which directory 46 | # it is acting on so we can run the command for the correct root or workspace 47 | - name: Get Dependabot Directory 48 | if: contains(steps.metadata.outputs.dependency-names, '@npmcli/template-oss') 49 | id: flags 50 | run: | 51 | dependabot_dir="${{ steps.metadata.outputs.directory }}" 52 | if [[ "$dependabot_dir" == "/" || "$dependabot_dir" == "/main" ]]; then 53 | echo "workspace=-iwr" >> $GITHUB_OUTPUT 54 | else 55 | # strip leading slash from directory so it works as a 56 | # a path to the workspace flag 57 | echo "workspace=--workspace ${dependabot_dir#/}" >> $GITHUB_OUTPUT 58 | fi 59 | 60 | - name: Apply Changes 61 | if: steps.flags.outputs.workspace 62 | id: apply 63 | run: | 64 | npm run template-oss-apply ${{ steps.flags.outputs.workspace }} 65 | if [[ `git status --porcelain` ]]; then 66 | echo "changes=true" >> $GITHUB_OUTPUT 67 | fi 68 | # This only sets the conventional commit prefix. This workflow can't reliably determine 69 | # what the breaking change is though. If a BREAKING CHANGE message is required then 70 | # this PR check will fail and the commit will be amended with stafftools 71 | if [[ "${{ steps.metadata.outputs.update-type }}" == "version-update:semver-major" ]]; then 72 | prefix='feat!' 73 | else 74 | prefix='chore' 75 | fi 76 | echo "message=$prefix: postinstall for dependabot template-oss PR" >> $GITHUB_OUTPUT 77 | 78 | # This step will fail if template-oss has made any workflow updates. It is impossible 79 | # for a workflow to update other workflows. In the case it does fail, we continue 80 | # and then try to apply only a portion of the changes in the next step 81 | - name: Push All Changes 82 | if: steps.apply.outputs.changes 83 | id: push 84 | continue-on-error: true 85 | env: 86 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 87 | run: | 88 | git commit -am "${{ steps.apply.outputs.message }}" 89 | git push 90 | 91 | # If the previous step failed, then reset the commit and remove any workflow changes 92 | # and attempt to commit and push again. This is helpful because we will have a commit 93 | # with the correct prefix that we can then --amend with @npmcli/stafftools later. 94 | - name: Push All Changes Except Workflows 95 | if: steps.apply.outputs.changes && steps.push.outcome == 'failure' 96 | env: 97 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 98 | run: | 99 | git reset HEAD~ 100 | git checkout HEAD -- .github/workflows/ 101 | git clean -fd .github/workflows/ 102 | git commit -am "${{ steps.apply.outputs.message }}" 103 | git push 104 | 105 | # Check if all the necessary template-oss changes were applied. Since we continued 106 | # on errors in one of the previous steps, this check will fail if our follow up 107 | # only applied a portion of the changes and we need to followup manually. 108 | # 109 | # Note that this used to run `lint` and `postlint` but that will fail this action 110 | # if we've also shipped any linting changes separate from template-oss. We do 111 | # linting in another action, so we want to fail this one only if there are 112 | # template-oss changes that could not be applied. 113 | - name: Check Changes 114 | if: steps.apply.outputs.changes 115 | run: | 116 | npm exec --offline ${{ steps.flags.outputs.workspace }} -- template-oss-check 117 | 118 | - name: Fail on Breaking Change 119 | if: steps.apply.outputs.changes && startsWith(steps.apply.outputs.message, 'feat!') 120 | run: | 121 | echo "This PR has a breaking change. Run 'npx -p @npmcli/stafftools gh template-oss-fix'" 122 | echo "for more information on how to fix this with a BREAKING CHANGE footer." 123 | exit 1 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # proggy 2 | 3 | Progress bar updates at a distance 4 | 5 | A decoupled progress bar connector component that lets you emit events on 6 | the process object to provide progress updates from various parts of a 7 | program. 8 | 9 | ## USAGE 10 | 11 | Somewhere in your program, you have a thing that is performing actions that 12 | take a while. 13 | 14 | In there, you will emit `progress` events on the global `process` object 15 | using Proggy. 16 | 17 | ```js 18 | const proggy = require('proggy') 19 | const doSomething = async (listOfItems) => { 20 | // This name should be unique within your program. 21 | // Proggy will do its best to make sure that you don't create the same 22 | // tracker more than once by throwing if you give it a name it's already 23 | // seen, but if there are multiple instances of Proggy, it won't be able 24 | // to guarantee it. 25 | const tracker = proggy.createTracker('doing something') 26 | let i = 0 27 | for (const item of listOfItems) { 28 | tracker.update(i++, listOfItems.length) 29 | const result = await doSomething(item) 30 | 31 | // changing the length is allowed! The progress bar will never go 32 | // backwards, but it will slow down if the total increases. 33 | if (result.more) 34 | listOfItems.push(result.more) 35 | } 36 | // can either explicitly end, or let it implicitly end when the value 37 | // is >= the total. 38 | tracker.end() 39 | } 40 | ``` 41 | 42 | Proggy is not a UI component. It is a way to decouple the UI of a progress 43 | bar from the thing that is making the actual progress. 44 | 45 | In another part of your program, where you are handling showing stuff to 46 | the user, you can display this information using any of the wonderful 47 | progress bar UI modules available on npm. 48 | 49 | ```js 50 | const proggy = require('proggy') 51 | 52 | // Create a client that can consume the events emitted elsewhere 53 | const client = proggy.createClient() 54 | ``` 55 | 56 | If you set the `normalize: true` flag, then the client will normalize how 57 | far it thinks the progress should have gone, in order to prevent backwards 58 | motion if the length increases along the way. If using this, then the 59 | `total` value will always be set to 100, and the `increment` value will 60 | always be some number smaller than 100. Use the `actualValue` and 61 | `actualTotal` fields to identify how many things have actually been done. 62 | 63 | For example, using 64 | [`cli-progress`](https://www.npmjs.com/package/cli-progress): 65 | 66 | ```js 67 | // set up our display thingmajig 68 | const cliProgress = require('cli-progress') 69 | const multibar = new cliProgress.MultiBar({ 70 | clearOnComplete: true, 71 | hideCursor: true, 72 | // note that data.actualValue and data.actualTotal will reflect the real 73 | // done/remaining values. data.value will always be less than 100, and 74 | // data.total will always be 100, so we never show reverse motion. 75 | format: '[{bar}] {percentage}% | {name} {actualValue}/{actualTotal} {duration_formatted}', 76 | barCompleteChar: '\u2588', 77 | barIncompleteChar: '\u2591', 78 | }, cliProgress.Presets.shades_grey) 79 | 80 | // update it with events from the proggy client 81 | const client = proggy.createClient({ 82 | // don't show reverse progress 83 | // this is false by default 84 | normalize: true, 85 | }) 86 | const bars = {} 87 | // new bar is created, tell multibar about it 88 | client.on('bar', (key, data) => { 89 | bars[key] = multibar.create(data.total) 90 | }) 91 | // got some progress for a progress bar, tell multibar to show it 92 | client.on('progress', (key, data) => { 93 | bars[key].update(data.value, data) 94 | bars[key].setTotal(data.total) 95 | }) 96 | // a bar is done, tell multibar to stop updating it 97 | client.on('barDone', (key, data) => { 98 | bars[key].stop() 99 | }) 100 | // all bars done, tell multibar to close entirely 101 | client.on('done', () => { 102 | multibar.stop() 103 | }) 104 | ``` 105 | 106 | ## API 107 | 108 | ### `proggy.createTracker(name, [key], [total])` 109 | 110 | Create a new `Tracker`. 111 | 112 | `key` will default to `name` if not set. It must be unique. 113 | 114 | ### `new proggy.Tracker(name, [key], [total])` 115 | 116 | The Tracker class, for emitting progress information. 117 | 118 | `total` will default to `100` if not set, but will be updated whenever 119 | progress is tracked. 120 | 121 | #### Fields 122 | 123 | * `name`, `key` - The name and identifer values set in the constructor 124 | * `done` - `true` if the tracker is completed. 125 | * `total` `value` - The most recent values updated. 126 | 127 | #### `tracker.update(value, total, [metadata])` 128 | 129 | Update the tracker and emit a `'progress'` event on the `process` object. 130 | 131 | #### `tracker.finish([metadata])` 132 | 133 | Alias for `tracker.update(tracker.total, tracker.total, metadata)` 134 | 135 | ### `proggy.createClient(options)` 136 | 137 | Create a new `Client`. 138 | 139 | ### `new proggy.Client({ normalize = false, stopOnDone = false })` 140 | 141 | The Client class, for consuming progress information. 142 | 143 | Set `normalize: true` in the options object to prevent backward motion and 144 | fix the `total` value at `100`. 145 | 146 | Set `stopOnDone: true` in the options object to tell the client to 147 | automatically stop when it emits its `'done'` event. 148 | 149 | #### Fields 150 | 151 | * `normalize` - whether this Client is in normalizing mode 152 | * `size` - the number of active trackers this Client is aware of 153 | 154 | #### Events 155 | 156 | * `client.on('bar', (key, data) => {})` - Emitted when a new progress bar 157 | is encountered. 158 | * `client.on('progress', (key, data) => {})` - Emitted when an update is 159 | available for a given progress bar. 160 | * `client.on('barDone', (key, data) => {})` - Emitted when a progress bar 161 | is completed. 162 | * `client.on('done', () => {})` - Emitted when all progress bars are 163 | completed. 164 | 165 | #### `client.start()` 166 | 167 | Begin listening for `'progress'` events on the `process` object. 168 | 169 | Called implicitly if `client.on('progress', fn)` is called. 170 | 171 | #### `client.stop()` 172 | 173 | Stop listening for `'progress'` events on the `process` object. 174 | 175 | Called implicitly when the `'done'` event is emitted, if 176 | `client.stopOnDone` is true. 177 | 178 | #### Progress Data 179 | 180 | All client events receive data objects containing the following fields, in 181 | addition to whatever else was sent by the `tracker`. 182 | 183 | * `key` - The unique key for this progress bar. 184 | * `name` - The name for this progress bar. 185 | * `value` - The current value of the progress. If `client.normalize` is 186 | true, then this will always be a number less than `100`, and never reduce 187 | from one update to the next. 188 | * `total` - The expected final value of the progress. If 189 | `client.normalize` is true, then this number will always be `100`. 190 | * `actualValue` - The value originally sent by the tracker. If 191 | `client.normalize` is not true, then this is always equal to `value`. 192 | * `actualTotal` - The total originally sent by the tracker. If 193 | `client.normalize` is not true, then this is always equal to `total`. 194 | * `done` - True if the tracker explicitly sent `done: true` in the data, or 195 | if `value` is greater than or equal to `total`. 196 | * Whatever other fields were set on the `metadata` sent by the tracker. 197 | -------------------------------------------------------------------------------- /tap-snapshots/test/client.js.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/client.js TAP basic > bar data 1`] = ` 9 | Array [ 10 | "key", 11 | Object { 12 | "actualTotal": 99, 13 | "actualValue": 1, 14 | "done": false, 15 | "hello": "world", 16 | "key": "key", 17 | "name": "key", 18 | "total": 99, 19 | "value": 1, 20 | }, 21 | ] 22 | ` 23 | 24 | exports[`test/client.js TAP basic > barDone data 1`] = ` 25 | Array [ 26 | "key", 27 | Object { 28 | "actualTotal": 50, 29 | "actualValue": 100, 30 | "done": true, 31 | "hello": "world", 32 | "key": "key", 33 | "name": "key", 34 | "total": 50, 35 | "value": 100, 36 | }, 37 | ] 38 | ` 39 | 40 | exports[`test/client.js TAP basic > progress data 1`] = ` 41 | Array [ 42 | "key", 43 | Object { 44 | "actualTotal": 99, 45 | "actualValue": 1, 46 | "done": false, 47 | "hello": "world", 48 | "key": "key", 49 | "name": "key", 50 | "total": 99, 51 | "value": 1, 52 | }, 53 | ] 54 | ` 55 | 56 | exports[`test/client.js TAP basic > progress data 2`] = ` 57 | Array [ 58 | "yek", 59 | Object { 60 | "actualTotal": 50, 61 | "actualValue": 5, 62 | "done": false, 63 | "hello": "world", 64 | "key": "yek", 65 | "name": "yek", 66 | "total": 50, 67 | "value": 5, 68 | }, 69 | ] 70 | ` 71 | 72 | exports[`test/client.js TAP basic > progress data 3`] = ` 73 | Array [ 74 | "key", 75 | Object { 76 | "actualTotal": 50, 77 | "actualValue": 100, 78 | "done": true, 79 | "hello": "world", 80 | "key": "key", 81 | "name": "key", 82 | "total": 50, 83 | "value": 100, 84 | }, 85 | ] 86 | ` 87 | 88 | exports[`test/client.js TAP basic > progress data 4`] = ` 89 | Array [ 90 | "yek", 91 | Object { 92 | "actualTotal": 50, 93 | "actualValue": 100, 94 | "done": true, 95 | "hello": "world", 96 | "key": "yek", 97 | "name": "yek", 98 | "total": 50, 99 | "value": 100, 100 | }, 101 | ] 102 | ` 103 | 104 | exports[`test/client.js TAP basic > progress data 5`] = ` 105 | Array [ 106 | "foo", 107 | Object { 108 | "actualTotal": 50, 109 | "actualValue": 100, 110 | "done": true, 111 | "hello": "world", 112 | "key": "foo", 113 | "name": "foo", 114 | "total": 50, 115 | "value": 100, 116 | }, 117 | ] 118 | ` 119 | 120 | exports[`test/client.js TAP normalize data > progress data 1`] = ` 121 | Array [ 122 | "key", 123 | Object { 124 | "actualTotal": 5, 125 | "actualValue": 1, 126 | "done": false, 127 | "key": "key", 128 | "name": "key", 129 | "total": 100, 130 | "value": 1, 131 | }, 132 | ] 133 | ` 134 | 135 | exports[`test/client.js TAP normalize data > progress data 10`] = ` 136 | Array [ 137 | "key", 138 | Object { 139 | "actualTotal": 50, 140 | "actualValue": 45, 141 | "done": false, 142 | "key": "key", 143 | "name": "key", 144 | "total": 100, 145 | "value": 90.199, 146 | }, 147 | ] 148 | ` 149 | 150 | exports[`test/client.js TAP normalize data > progress data 11`] = ` 151 | Array [ 152 | "key", 153 | Object { 154 | "actualTotal": 50, 155 | "actualValue": 45, 156 | "done": false, 157 | "key": "key", 158 | "name": "key", 159 | "total": 100, 160 | "value": 90.29701, 161 | }, 162 | ] 163 | ` 164 | 165 | exports[`test/client.js TAP normalize data > progress data 12`] = ` 166 | Array [ 167 | "key", 168 | Object { 169 | "actualTotal": 50, 170 | "actualValue": 45, 171 | "done": false, 172 | "key": "key", 173 | "name": "key", 174 | "total": 100, 175 | "value": 90.3940399, 176 | }, 177 | ] 178 | ` 179 | 180 | exports[`test/client.js TAP normalize data > progress data 13`] = ` 181 | Array [ 182 | "key", 183 | Object { 184 | "actualTotal": 50, 185 | "actualValue": 45, 186 | "done": false, 187 | "key": "key", 188 | "name": "key", 189 | "total": 100, 190 | "value": 90.49009950099999, 191 | }, 192 | ] 193 | ` 194 | 195 | exports[`test/client.js TAP normalize data > progress data 14`] = ` 196 | Array [ 197 | "key", 198 | Object { 199 | "actualTotal": 50, 200 | "actualValue": 45, 201 | "done": false, 202 | "key": "key", 203 | "name": "key", 204 | "total": 100, 205 | "value": 90.58519850599, 206 | }, 207 | ] 208 | ` 209 | 210 | exports[`test/client.js TAP normalize data > progress data 15`] = ` 211 | Array [ 212 | "key", 213 | Object { 214 | "actualTotal": 50, 215 | "actualValue": 45, 216 | "done": false, 217 | "key": "key", 218 | "name": "key", 219 | "total": 100, 220 | "value": 90.6793465209301, 221 | }, 222 | ] 223 | ` 224 | 225 | exports[`test/client.js TAP normalize data > progress data 16`] = ` 226 | Array [ 227 | "key", 228 | Object { 229 | "actualTotal": 420, 230 | "actualValue": 9001, 231 | "done": true, 232 | "key": "key", 233 | "name": "key", 234 | "total": 100, 235 | "value": 100, 236 | }, 237 | ] 238 | ` 239 | 240 | exports[`test/client.js TAP normalize data > progress data 2`] = ` 241 | Array [ 242 | "key", 243 | Object { 244 | "actualTotal": 12, 245 | "actualValue": 2, 246 | "done": false, 247 | "key": "key", 248 | "name": "key", 249 | "total": 100, 250 | "value": 16.666666666666668, 251 | }, 252 | ] 253 | ` 254 | 255 | exports[`test/client.js TAP normalize data > progress data 3`] = ` 256 | Array [ 257 | "key", 258 | Object { 259 | "actualTotal": 12, 260 | "actualValue": 3, 261 | "done": false, 262 | "key": "key", 263 | "name": "key", 264 | "total": 100, 265 | "value": 25, 266 | }, 267 | ] 268 | ` 269 | 270 | exports[`test/client.js TAP normalize data > progress data 4`] = ` 271 | Array [ 272 | "key", 273 | Object { 274 | "actualTotal": 13, 275 | "actualValue": 4, 276 | "done": false, 277 | "key": "key", 278 | "name": "key", 279 | "total": 100, 280 | "value": 30.76923076923077, 281 | }, 282 | ] 283 | ` 284 | 285 | exports[`test/client.js TAP normalize data > progress data 5`] = ` 286 | Array [ 287 | "key", 288 | Object { 289 | "actualTotal": 13, 290 | "actualValue": 6, 291 | "done": false, 292 | "key": "key", 293 | "name": "key", 294 | "total": 100, 295 | "value": 46.15384615384615, 296 | }, 297 | ] 298 | ` 299 | 300 | exports[`test/client.js TAP normalize data > progress data 6`] = ` 301 | Array [ 302 | "key", 303 | Object { 304 | "actualTotal": 6, 305 | "actualValue": 3, 306 | "done": false, 307 | "key": "key", 308 | "name": "key", 309 | "total": 100, 310 | "value": 50, 311 | }, 312 | ] 313 | ` 314 | 315 | exports[`test/client.js TAP normalize data > progress data 7`] = ` 316 | Array [ 317 | "key", 318 | Object { 319 | "actualTotal": 6, 320 | "actualValue": 5, 321 | "done": false, 322 | "key": "key", 323 | "name": "key", 324 | "total": 100, 325 | "value": 83.33333333333333, 326 | }, 327 | ] 328 | ` 329 | 330 | exports[`test/client.js TAP normalize data > progress data 8`] = ` 331 | Array [ 332 | "key", 333 | Object { 334 | "actualTotal": 50, 335 | "actualValue": 45, 336 | "done": false, 337 | "key": "key", 338 | "name": "key", 339 | "total": 100, 340 | "value": 90, 341 | }, 342 | ] 343 | ` 344 | 345 | exports[`test/client.js TAP normalize data > progress data 9`] = ` 346 | Array [ 347 | "key", 348 | Object { 349 | "actualTotal": 50, 350 | "actualValue": 45, 351 | "done": false, 352 | "key": "key", 353 | "name": "key", 354 | "total": 100, 355 | "value": 90.1, 356 | }, 357 | ] 358 | ` 359 | 360 | exports[`test/client.js TAP stop on done > progress data 1`] = ` 361 | Array [ 362 | "key", 363 | Object { 364 | "actualTotal": 99, 365 | "actualValue": 1, 366 | "done": false, 367 | "hello": "world", 368 | "key": "key", 369 | "name": "key", 370 | "total": 99, 371 | "value": 1, 372 | }, 373 | ] 374 | ` 375 | 376 | exports[`test/client.js TAP stop on done > progress data 2`] = ` 377 | Array [ 378 | "key", 379 | Object { 380 | "actualTotal": 100, 381 | "actualValue": 100, 382 | "done": true, 383 | "hello": "world", 384 | "key": "key", 385 | "name": "key", 386 | "total": 100, 387 | "value": 100, 388 | }, 389 | ] 390 | ` 391 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Release 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | 10 | permissions: 11 | contents: write 12 | pull-requests: write 13 | checks: write 14 | 15 | jobs: 16 | release: 17 | outputs: 18 | pr: ${{ steps.release.outputs.pr }} 19 | pr-branch: ${{ steps.release.outputs.pr-branch }} 20 | pr-number: ${{ steps.release.outputs.pr-number }} 21 | pr-sha: ${{ steps.release.outputs.pr-sha }} 22 | releases: ${{ steps.release.outputs.releases }} 23 | comment-id: ${{ steps.create-comment.outputs.comment-id || steps.update-comment.outputs.comment-id }} 24 | check-id: ${{ steps.create-check.outputs.check-id }} 25 | name: Release 26 | if: github.repository_owner == 'npm' 27 | runs-on: ubuntu-latest 28 | defaults: 29 | run: 30 | shell: bash 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - name: Setup Git User 35 | run: | 36 | git config --global user.email "npm-cli+bot@github.com" 37 | git config --global user.name "npm CLI robot" 38 | - name: Setup Node 39 | uses: actions/setup-node@v4 40 | id: node 41 | with: 42 | node-version: 22.x 43 | check-latest: contains('22.x', '.x') 44 | - name: Install Latest npm 45 | uses: ./.github/actions/install-latest-npm 46 | with: 47 | node: ${{ steps.node.outputs.node-version }} 48 | - name: Install Dependencies 49 | run: npm i --ignore-scripts --no-audit --no-fund 50 | - name: Release Please 51 | id: release 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | run: npx --offline template-oss-release-please --branch="${{ github.ref_name }}" --backport="" --defaultTag="latest" 55 | - name: Create Release Manager Comment Text 56 | if: steps.release.outputs.pr-number 57 | uses: actions/github-script@v7 58 | id: comment-text 59 | with: 60 | result-encoding: string 61 | script: | 62 | const { runId, repo: { owner, repo } } = context 63 | const { data: workflow } = await github.rest.actions.getWorkflowRun({ owner, repo, run_id: runId }) 64 | return['## Release Manager', `Release workflow run: ${workflow.html_url}`].join('\n\n') 65 | - name: Find Release Manager Comment 66 | uses: peter-evans/find-comment@v2 67 | if: steps.release.outputs.pr-number 68 | id: found-comment 69 | with: 70 | issue-number: ${{ steps.release.outputs.pr-number }} 71 | comment-author: 'github-actions[bot]' 72 | body-includes: '## Release Manager' 73 | - name: Create Release Manager Comment 74 | id: create-comment 75 | if: steps.release.outputs.pr-number && !steps.found-comment.outputs.comment-id 76 | uses: peter-evans/create-or-update-comment@v3 77 | with: 78 | issue-number: ${{ steps.release.outputs.pr-number }} 79 | body: ${{ steps.comment-text.outputs.result }} 80 | - name: Update Release Manager Comment 81 | id: update-comment 82 | if: steps.release.outputs.pr-number && steps.found-comment.outputs.comment-id 83 | uses: peter-evans/create-or-update-comment@v3 84 | with: 85 | comment-id: ${{ steps.found-comment.outputs.comment-id }} 86 | body: ${{ steps.comment-text.outputs.result }} 87 | edit-mode: 'replace' 88 | - name: Create Check 89 | id: create-check 90 | uses: ./.github/actions/create-check 91 | if: steps.release.outputs.pr-sha 92 | with: 93 | name: "Release" 94 | token: ${{ secrets.GITHUB_TOKEN }} 95 | sha: ${{ steps.release.outputs.pr-sha }} 96 | 97 | update: 98 | needs: release 99 | outputs: 100 | sha: ${{ steps.commit.outputs.sha }} 101 | check-id: ${{ steps.create-check.outputs.check-id }} 102 | name: Update - Release 103 | if: github.repository_owner == 'npm' && needs.release.outputs.pr 104 | runs-on: ubuntu-latest 105 | defaults: 106 | run: 107 | shell: bash 108 | steps: 109 | - name: Checkout 110 | uses: actions/checkout@v4 111 | with: 112 | fetch-depth: 0 113 | ref: ${{ needs.release.outputs.pr-branch }} 114 | - name: Setup Git User 115 | run: | 116 | git config --global user.email "npm-cli+bot@github.com" 117 | git config --global user.name "npm CLI robot" 118 | - name: Setup Node 119 | uses: actions/setup-node@v4 120 | id: node 121 | with: 122 | node-version: 22.x 123 | check-latest: contains('22.x', '.x') 124 | - name: Install Latest npm 125 | uses: ./.github/actions/install-latest-npm 126 | with: 127 | node: ${{ steps.node.outputs.node-version }} 128 | - name: Install Dependencies 129 | run: npm i --ignore-scripts --no-audit --no-fund 130 | - name: Create Release Manager Checklist Text 131 | id: comment-text 132 | env: 133 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 134 | run: npm exec --offline -- template-oss-release-manager --pr="${{ needs.release.outputs.pr-number }}" --backport="" --defaultTag="latest" --publish 135 | - name: Append Release Manager Comment 136 | uses: peter-evans/create-or-update-comment@v3 137 | with: 138 | comment-id: ${{ needs.release.outputs.comment-id }} 139 | body: ${{ steps.comment-text.outputs.result }} 140 | edit-mode: 'append' 141 | - name: Run Post Pull Request Actions 142 | env: 143 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 144 | run: npm run rp-pull-request --ignore-scripts --if-present -- --pr="${{ needs.release.outputs.pr-number }}" --commentId="${{ needs.release.outputs.comment-id }}" 145 | - name: Commit 146 | id: commit 147 | env: 148 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 149 | run: | 150 | git commit --all --amend --no-edit || true 151 | git push --force-with-lease 152 | echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT 153 | - name: Create Check 154 | id: create-check 155 | uses: ./.github/actions/create-check 156 | with: 157 | name: "Update - Release" 158 | check-name: "Release" 159 | token: ${{ secrets.GITHUB_TOKEN }} 160 | sha: ${{ steps.commit.outputs.sha }} 161 | - name: Conclude Check 162 | uses: LouisBrunner/checks-action@v1.6.0 163 | with: 164 | token: ${{ secrets.GITHUB_TOKEN }} 165 | conclusion: ${{ job.status }} 166 | check_id: ${{ needs.release.outputs.check-id }} 167 | 168 | ci: 169 | name: CI - Release 170 | needs: [ release, update ] 171 | if: needs.release.outputs.pr 172 | uses: ./.github/workflows/ci-release.yml 173 | with: 174 | ref: ${{ needs.release.outputs.pr-branch }} 175 | check-sha: ${{ needs.update.outputs.sha }} 176 | 177 | post-ci: 178 | needs: [ release, update, ci ] 179 | name: Post CI - Release 180 | if: github.repository_owner == 'npm' && needs.release.outputs.pr && always() 181 | runs-on: ubuntu-latest 182 | defaults: 183 | run: 184 | shell: bash 185 | steps: 186 | - name: Get CI Conclusion 187 | id: conclusion 188 | run: | 189 | result="" 190 | if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then 191 | result="failure" 192 | elif [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then 193 | result="cancelled" 194 | else 195 | result="success" 196 | fi 197 | echo "result=$result" >> $GITHUB_OUTPUT 198 | - name: Conclude Check 199 | uses: LouisBrunner/checks-action@v1.6.0 200 | with: 201 | token: ${{ secrets.GITHUB_TOKEN }} 202 | conclusion: ${{ steps.conclusion.outputs.result }} 203 | check_id: ${{ needs.update.outputs.check-id }} 204 | 205 | post-release: 206 | needs: release 207 | outputs: 208 | comment-id: ${{ steps.create-comment.outputs.comment-id }} 209 | name: Post Release - Release 210 | if: github.repository_owner == 'npm' && needs.release.outputs.releases 211 | runs-on: ubuntu-latest 212 | defaults: 213 | run: 214 | shell: bash 215 | steps: 216 | - name: Create Release PR Comment Text 217 | id: comment-text 218 | uses: actions/github-script@v7 219 | env: 220 | RELEASES: ${{ needs.release.outputs.releases }} 221 | with: 222 | result-encoding: string 223 | script: | 224 | const releases = JSON.parse(process.env.RELEASES) 225 | const { runId, repo: { owner, repo } } = context 226 | const issue_number = releases[0].prNumber 227 | const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${runId}` 228 | 229 | return [ 230 | '## Release Workflow\n', 231 | ...releases.map(r => `- \`${r.pkgName}@${r.version}\` ${r.url}`), 232 | `- Workflow run: :arrows_counterclockwise: ${runUrl}`, 233 | ].join('\n') 234 | - name: Create Release PR Comment 235 | id: create-comment 236 | uses: peter-evans/create-or-update-comment@v3 237 | with: 238 | issue-number: ${{ fromJSON(needs.release.outputs.releases)[0].prNumber }} 239 | body: ${{ steps.comment-text.outputs.result }} 240 | 241 | release-integration: 242 | needs: release 243 | name: Release Integration 244 | if: needs.release.outputs.releases 245 | uses: ./.github/workflows/release-integration.yml 246 | permissions: 247 | contents: read 248 | id-token: write 249 | secrets: 250 | PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} 251 | with: 252 | releases: ${{ needs.release.outputs.releases }} 253 | 254 | post-release-integration: 255 | needs: [ release, release-integration, post-release ] 256 | name: Post Release Integration - Release 257 | if: github.repository_owner == 'npm' && needs.release.outputs.releases && always() 258 | runs-on: ubuntu-latest 259 | defaults: 260 | run: 261 | shell: bash 262 | steps: 263 | - name: Get Post Release Conclusion 264 | id: conclusion 265 | run: | 266 | if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then 267 | result="x" 268 | elif [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then 269 | result="heavy_multiplication_x" 270 | else 271 | result="white_check_mark" 272 | fi 273 | echo "result=$result" >> $GITHUB_OUTPUT 274 | - name: Find Release PR Comment 275 | uses: peter-evans/find-comment@v2 276 | id: found-comment 277 | with: 278 | issue-number: ${{ fromJSON(needs.release.outputs.releases)[0].prNumber }} 279 | comment-author: 'github-actions[bot]' 280 | body-includes: '## Release Workflow' 281 | - name: Create Release PR Comment Text 282 | id: comment-text 283 | if: steps.found-comment.outputs.comment-id 284 | uses: actions/github-script@v7 285 | env: 286 | RESULT: ${{ steps.conclusion.outputs.result }} 287 | BODY: ${{ steps.found-comment.outputs.comment-body }} 288 | with: 289 | result-encoding: string 290 | script: | 291 | const { RESULT, BODY } = process.env 292 | const body = [BODY.replace(/(Workflow run: :)[a-z_]+(:)/, `$1${RESULT}$2`)] 293 | if (RESULT !== 'white_check_mark') { 294 | body.push(':rotating_light::rotating_light::rotating_light:') 295 | body.push([ 296 | '@npm/cli-team: The post-release workflow failed for this release.', 297 | 'Manual steps may need to be taken after examining the workflow output.' 298 | ].join(' ')) 299 | body.push(':rotating_light::rotating_light::rotating_light:') 300 | } 301 | return body.join('\n\n').trim() 302 | - name: Update Release PR Comment 303 | if: steps.comment-text.outputs.result 304 | uses: peter-evans/create-or-update-comment@v3 305 | with: 306 | comment-id: ${{ steps.found-comment.outputs.comment-id }} 307 | body: ${{ steps.comment-text.outputs.result }} 308 | edit-mode: 'replace' 309 | --------------------------------------------------------------------------------