From 7b2084fbe58c06552a55c7249c2c93635f9928b3 Mon Sep 17 00:00:00 2001 From: Eric Martin Date: Wed, 29 Jul 2026 19:11:34 -0700 Subject: [PATCH 1/3] ci: harden workflows, add npm provenance publishing and supply-chain checks Hardens the CI pipeline and adds automated release publishing with provenance attestation. ci.yml: - Add an explicit 'permissions: contents: read' block. The workflow previously inherited the repository default token scope. - Pin third-party actions by commit SHA with a version comment, so a retargeted or compromised tag cannot change what executes. - Set persist-credentials: false, so the checkout token is not left in the git config for later steps to pick up. - Scope the push trigger to main. It was previously unfiltered and ran the full three-node matrix on every branch push, duplicating the pull_request run. - Add concurrency cancel-in-progress and fail-fast: false. - Add an audit job running 'npm audit --audit-level=high'. Nothing watched dependencies between manual bumps before this. Currently reports 0 vulnerabilities. release.yml (new): - Publishes on GitHub Release, with a manual dispatch that defaults to dry-run. - Publishes with --provenance under 'id-token: write', linking each tarball to the commit and workflow run that produced it. Consumers can verify with 'npm audit signatures'. - Re-runs lint, typecheck, test and build before publishing rather than trusting the commit's earlier CI result. - Guards against a tag that disagrees with package.json, and against republishing an existing version. Both guards were verified locally. scorecard.yml (new): - Weekly OpenSSF Scorecard analysis uploaded to code scanning, with published results backing the README badge. dependabot.yml (new): - Weekly npm and github-actions updates. Dev minor/patch bumps are grouped into one PR; majors stay separate for individual review. README: add CI and Scorecard badges, and a Supply Chain section documenting provenance verification. --- .github/dependabot.yml | 42 ++++++++++++++++ .github/workflows/ci.yml | 47 +++++++++++++++++- .github/workflows/release.yml | 88 +++++++++++++++++++++++++++++++++ .github/workflows/scorecard.yml | 55 +++++++++++++++++++++ README.md | 13 +++++ 5 files changed, 243 insertions(+), 2 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/scorecard.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..5e56330 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,42 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: '/' + schedule: + interval: weekly + day: monday + time: '07:00' + open-pull-requests-limit: 5 + commit-message: + prefix: 'chore(deps)' + prefix-development: 'chore(deps-dev)' + include: scope + labels: + - dependencies + groups: + # Everything here is a devDependency, so batch the routine bumps into one + # PR per week instead of five. Major bumps stay separate so tooling + # upgrades like ESLint or Vitest get reviewed on their own. + dev-minor-and-patch: + dependency-type: development + update-types: + - minor + - patch + + - package-ecosystem: github-actions + directory: '/' + schedule: + interval: weekly + day: monday + time: '07:00' + open-pull-requests-limit: 5 + commit-message: + prefix: 'chore(ci)' + include: scope + labels: + - dependencies + - github-actions + groups: + actions: + patterns: + - '*' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27da352..bf384a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,26 +2,69 @@ name: CI on: push: + branches: [main] pull_request: branches: [main] +# The default GITHUB_TOKEN is granted no more than read access to the repo. +permissions: + contents: read + +# A new push to a branch cancels any CI run still in flight for that branch. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: + name: Node ${{ matrix.node-version }} runs-on: ubuntu-latest strategy: + fail-fast: false matrix: node-version: [20.x, 22.x, 24.x] steps: - - uses: actions/checkout@v4 + # Third-party actions are pinned by commit SHA rather than tag, so a + # compromised or retargeted tag cannot silently change what runs here. + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: ${{ matrix.node-version }} cache: 'npm' + - run: npm ci - run: npm run lint - run: npm run typecheck - run: npm run build - run: npm test + + audit: + name: Dependency audit + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Use Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.x + cache: 'npm' + + - run: npm ci + + # Fails the job on a high or critical advisory. Everything this package + # depends on is a devDependency, so a finding here is a build-chain risk + # rather than something shipped to consumers, but it should still block. + - name: npm audit + run: npm audit --audit-level=high diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..0be84a6 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,88 @@ +name: Release + +# Publishes to npm when a GitHub Release is published, or on manual dispatch. +# Requires an NPM_TOKEN secret (an npm automation token) in the repository. +on: + release: + types: [published] + workflow_dispatch: + inputs: + dry-run: + description: 'Run every step but skip the actual npm publish' + type: boolean + default: true + +permissions: + contents: read + +jobs: + publish: + name: Publish to npm + runs-on: ubuntu-latest + + # `id-token: write` is what lets npm mint a provenance attestation binding + # the published tarball to this workflow and commit. Without it, publishing + # with --provenance fails. + permissions: + contents: read + id-token: write + + environment: + name: npm + url: https://www.npmjs.com/package/youtube-transcript-plus + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Use Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.x + cache: 'npm' + registry-url: 'https://registry.npmjs.org' + + - run: npm ci + + # Re-run the full gate here rather than trusting that CI passed on the + # commit — this is the last point before an immutable publish. + - run: npm run lint + - run: npm run typecheck + - run: npm test + - run: npm run build + + # Guards against tagging a release whose package.json was never bumped. + - name: Verify tag matches package.json version + if: github.event_name == 'release' + run: | + PKG_VERSION="$(node -p "require('./package.json').version")" + TAG_VERSION="${GITHUB_REF_NAME#v}" + echo "package.json: $PKG_VERSION" + echo "git tag: $TAG_VERSION" + if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then + echo "::error::Tag $GITHUB_REF_NAME does not match package.json version $PKG_VERSION" + exit 1 + fi + + - name: Verify version is not already published + run: | + PKG_VERSION="$(node -p "require('./package.json').version")" + if npm view "youtube-transcript-plus@$PKG_VERSION" version >/dev/null 2>&1; then + echo "::error::Version $PKG_VERSION is already published to npm" + exit 1 + fi + + - name: Show tarball contents + run: npm pack --dry-run + + - name: Publish + if: github.event_name == 'release' || inputs.dry-run == false + run: npm publish --provenance --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Dry run (no publish) + if: github.event_name == 'workflow_dispatch' && inputs.dry-run + run: echo "Dry run complete. Re-run with dry-run unchecked to publish." diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 0000000..21509b8 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,55 @@ +name: Scorecard + +# OpenSSF Scorecard grades this repo's supply-chain posture (pinned actions, +# token permissions, branch protection, published provenance, and so on) and +# uploads the result to GitHub code scanning. +on: + branch_protection_rule: + schedule: + # Weekly, Monday 07:00 UTC. + - cron: '0 7 * * 1' + push: + branches: [main] + workflow_dispatch: + +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + + permissions: + # Required to upload the SARIF result to code scanning. + security-events: write + # Required by the publish_results option below. + id-token: write + contents: read + actions: read + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Run analysis + uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4 + with: + results_file: results.sarif + results_format: sarif + # Publishes the score to the OpenSSF API, which is what backs the + # public badge in the README. + publish_results: true + + - name: Upload artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: scorecard-results + path: results.sarif + retention-days: 5 + + - name: Upload to code scanning + uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4 + with: + sarif_file: results.sarif diff --git a/README.md b/README.md index a0a2880..44cb321 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # youtube-transcript-plus [![npm version](https://badge.fury.io/js/youtube-transcript-plus.svg)](https://badge.fury.io/js/youtube-transcript-plus) +[![CI](https://github.com/ericmmartin/youtube-transcript-plus/actions/workflows/ci.yml/badge.svg)](https://github.com/ericmmartin/youtube-transcript-plus/actions/workflows/ci.yml) +[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/ericmmartin/youtube-transcript-plus/badge)](https://scorecard.dev/viewer/?uri=github.com/ericmmartin/youtube-transcript-plus) A Node.js library to fetch transcripts from YouTube videos. This package uses YouTube's unofficial API, so it may break if YouTube changes its internal structure. @@ -394,6 +396,17 @@ The library throws the following errors: - **`YoutubeTranscriptInvalidVideoIdError`**: The provided video ID or URL is invalid. - **`YoutubeTranscriptInvalidLangError`**: The provided language code is not a valid BCP 47 code. Properties: `lang`. +## Supply Chain + +Releases are published to npm from a GitHub Actions workflow with +[npm provenance](https://docs.npmjs.com/generating-provenance-statements), so each +published tarball is cryptographically linked to the commit and workflow run that +built it. You can verify the signatures of your installed dependencies with: + +```bash +npm audit signatures +``` + ## Feature Requests Have a feature idea? [Open an issue](https://github.com/ericmmartin/youtube-transcript-plus/issues/new) and let us know! From 10cac7e61d4d73e47a2134fecba1cfb13f8c4dea Mon Sep 17 00:00:00 2001 From: Eric Martin Date: Wed, 29 Jul 2026 20:01:56 -0700 Subject: [PATCH 2/3] fix(ci): rename release dry-run input to dry_run Actions expressions parse `inputs.dry-run` as `inputs.dry - run`, so the hyphenated input name could never be referenced. The publish guard would mis-evaluate (or fail to parse), risking a publish on a run the operator intended as a dry run. Rename the input to `dry_run` and make the publish condition explicit about the workflow_dispatch case. --- .github/workflows/release.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0be84a6..0710143 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,7 +7,10 @@ on: types: [published] workflow_dispatch: inputs: - dry-run: + # Underscore, not a hyphen: Actions expressions parse `inputs.dry-run` + # as subtraction, so a hyphenated input name is unreferenceable via dot + # notation. + dry_run: description: 'Run every step but skip the actual npm publish' type: boolean default: true @@ -78,11 +81,11 @@ jobs: run: npm pack --dry-run - name: Publish - if: github.event_name == 'release' || inputs.dry-run == false + if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && !inputs.dry_run) run: npm publish --provenance --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Dry run (no publish) - if: github.event_name == 'workflow_dispatch' && inputs.dry-run - run: echo "Dry run complete. Re-run with dry-run unchecked to publish." + if: github.event_name == 'workflow_dispatch' && inputs.dry_run + run: echo "Dry run complete. Re-run with dry_run unchecked to publish." From bc356a716be3ff7fc1d004707347d08548702a72 Mon Sep 17 00:00:00 2001 From: Eric Martin Date: Thu, 30 Jul 2026 09:46:35 -0700 Subject: [PATCH 3/3] ci(release): authenticate npm publish via trusted publishing (OIDC) npm revoked classic automation tokens in December 2025, and granular tokens now cap at a 90-day lifetime. Switch to OIDC trusted publishing instead: the npm CLI detects the Actions OIDC environment and exchanges it for short-lived publish credentials, so there is no NPM_TOKEN secret and nothing to rotate. Drops the NODE_AUTH_TOKEN env block and --access public (already covered by publishConfig.access). Keeps --provenance so the attestation stays explicit. --- .github/workflows/release.yml | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0710143..c324061 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,11 @@ name: Release # Publishes to npm when a GitHub Release is published, or on manual dispatch. -# Requires an NPM_TOKEN secret (an npm automation token) in the repository. +# +# Authentication is via npm trusted publishing (OIDC) — there is no NPM_TOKEN +# secret and nothing to rotate. The trusted publisher is registered on npm +# against this repo, this workflow filename, and the `npm` environment below; +# all four must keep matching or the publish will fail to authenticate. on: release: types: [published] @@ -23,9 +27,9 @@ jobs: name: Publish to npm runs-on: ubuntu-latest - # `id-token: write` is what lets npm mint a provenance attestation binding - # the published tarball to this workflow and commit. Without it, publishing - # with --provenance fails. + # `id-token: write` is what mints the OIDC token npm exchanges for publish + # rights, and what binds the provenance attestation to this workflow and + # commit. Without it there is no way to authenticate at all. permissions: contents: read id-token: write @@ -80,11 +84,13 @@ jobs: - name: Show tarball contents run: npm pack --dry-run + # No auth env: the npm CLI detects the Actions OIDC environment and + # exchanges it for short-lived publish credentials on its own. Provenance + # is generated automatically under trusted publishing, but the flag is + # kept so the attestation is requested explicitly rather than implied. - name: Publish if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && !inputs.dry_run) - run: npm publish --provenance --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm publish --provenance - name: Dry run (no publish) if: github.event_name == 'workflow_dispatch' && inputs.dry_run