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..c324061 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,97 @@ +name: Release + +# Publishes to npm when a GitHub Release is published, or on manual dispatch. +# +# 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] + workflow_dispatch: + inputs: + # 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 + +permissions: + contents: read + +jobs: + publish: + name: Publish to npm + runs-on: ubuntu-latest + + # `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 + + 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 + + # 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 + + - 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!