├── .tool-versions ├── mise.toml ├── .prettierignore ├── .prettierrc.js ├── vitest.config.ts ├── .gitignore ├── .github ├── release.yml ├── dependabot.yml └── workflows │ └── main.yml ├── action.yml ├── CONTRIBUTING.md ├── LICENSE ├── package.json ├── src ├── main.ts ├── util.ts └── diff.ts ├── __tests__ ├── diff.test.ts └── util.test.ts ├── CHANGELOG.md ├── tsconfig.json └── README.md /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 24.11.1 2 | -------------------------------------------------------------------------------- /mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | node = '24.11.1' 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Build outputs 2 | dist/ 3 | lib/ 4 | coverage/ 5 | 6 | # Dependencies 7 | node_modules/ 8 | 9 | # Misc 10 | .github/ 11 | *.log 12 | .DS_Store 13 | 14 | # Package files 15 | package-lock.json 16 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('prettier').Config} 3 | */ 4 | module.exports = { 5 | trailingComma: "all", 6 | tabWidth: 2, 7 | semi: true, 8 | singleQuote: true, 9 | printWidth: 100, 10 | bracketSpacing: true, 11 | }; 12 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | environment: 'node', 6 | coverage: { 7 | reporter: ['text', 'lcov'], 8 | }, 9 | include: ['__tests__/**/*.ts'], 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __tests__/runner/* 2 | # actions requires a node_modules dir https://github.com/actions/toolkit/blob/master/docs/javascript-action.md#publish-a-releasesv1-action 3 | # but its recommended not to check these in https://github.com/actions/toolkit/blob/master/docs/action-versioning.md#recommendations 4 | node_modules 5 | coverage -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore-for-release 5 | - github-actions 6 | authors: 7 | - octocat 8 | - renovate[bot] 9 | categories: 10 | - title: Breaking Changes 🛠 11 | labels: 12 | - breaking-change 13 | - title: Exciting New Features 🎉 14 | labels: 15 | - enhancement 16 | - feature 17 | - title: Bug fixes 🐛 18 | labels: 19 | - bug 20 | - title: Other Changes 🔄 21 | labels: 22 | - "*" 23 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/actions/sharing-automations/creating-actions/metadata-syntax-for-github-actions 2 | name: "diff-set" 3 | description: "Github Action for producing lists of files that changed between branches" 4 | author: "softprops" 5 | env: 6 | "GITHUB_TOKEN": "Repository token provided by Github Actions secrets" 7 | runs: 8 | using: "node24" 9 | main: "dist/index.js" 10 | inputs: 11 | token: 12 | description: 'GitHub access token' 13 | required: false 14 | default: ${{ github.token }} 15 | base: 16 | description: "Base branch to compare with (defaults to master)" 17 | branding: 18 | color: "green" 19 | icon: "activity" 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | groups: 8 | npm: 9 | patterns: 10 | - "*" 11 | ignore: 12 | - dependency-name: node-fetch 13 | versions: 14 | - ">=3.0.0" 15 | - dependency-name: "@types/node" 16 | versions: 17 | - ">=25.0.0" 18 | commit-message: 19 | prefix: "chore(deps)" 20 | - package-ecosystem: github-actions 21 | directory: "/" 22 | schedule: 23 | interval: monthly 24 | groups: 25 | github-actions: 26 | patterns: 27 | - "*" 28 | commit-message: 29 | prefix: "chore(deps)" 30 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## bootstrapping 2 | 3 | This a [JavaScript](https://help.github.com/en/articles/about-actions#types-of-actions) action but uses [TypeScript](https://www.typescriptlang.org/docs/home.html) to generate that JavaScript. 4 | 5 | You can bootstrap your envrinment with a modern version of npm and by running `npm i` at the root of this repo. 6 | 7 | ## testing 8 | 9 | Tests can be found under under `__tests__` directory and are runnable with the `npm t` command 10 | 11 | ## source code 12 | 13 | Source code can be found under the `src` directory. Running `npm run build` will generate the JavaScript that will run within GitHub workflows. 14 | 15 | ## formatting 16 | 17 | A minimal attempt at keeping a consistent code style is can be applied by running `npm run fmt` 18 | 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019-2021 Doug Tangren 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "diffset", 3 | "version": "3.0.2", 4 | "private": true, 5 | "description": "GitHub Action for producing list of files that changed between branches", 6 | "main": "lib/main.js", 7 | "engines": { 8 | "node": ">=24.0.0" 9 | }, 10 | "scripts": { 11 | "build": "ncc build src/main.ts --minify", 12 | "typecheck": "tsc --noEmit", 13 | "test": "vitest --coverage", 14 | "fmt": "prettier --write 'src/**/*.ts' '__tests__/**/*.ts'", 15 | "fmtcheck": "prettier --check 'src/**/*.ts' '__tests__/**/*.ts'" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/softprops/diffset.git" 20 | }, 21 | "keywords": [ 22 | "actions" 23 | ], 24 | "author": "softprops", 25 | "license": "MIT", 26 | "dependencies": { 27 | "@actions/core": "^1.11.1", 28 | "@octokit/plugin-throttling": "^11.0.3", 29 | "@octokit/rest": "^22.0.1", 30 | "minimatch": "^10.1.1" 31 | }, 32 | "devDependencies": { 33 | "@types/minimatch": "^6.0.0", 34 | "@types/node": "^24.10.1", 35 | "@vercel/ncc": "^0.38.4", 36 | "@vitest/coverage-v8": "^4.0.14", 37 | "husky": "^9.1.7", 38 | "lint-staged": "^16.2.7", 39 | "prettier": "3.7.3", 40 | "typescript": "^5.9.3", 41 | "vitest": "^4.0.14" 42 | }, 43 | "lint-staged": { 44 | "{__tests__,src}/**/*.ts": [ 45 | "npm run fmt", 46 | "git add" 47 | ] 48 | }, 49 | "husky": { 50 | "hooks": { 51 | "pre-commit": "lint-staged" 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { debug, setFailed, setOutput, warning } from '@actions/core'; 2 | import { Octokit } from '@octokit/rest'; 3 | import { env } from 'process'; 4 | import { GitHubDiff, sets } from './diff'; 5 | import { intoParams, parseConfig } from './util'; 6 | 7 | async function run() { 8 | try { 9 | const config = parseConfig(env); 10 | Octokit.plugin(require('@octokit/plugin-throttling')); 11 | const differ = new GitHubDiff( 12 | new Octokit({ 13 | auth: config.githubToken, 14 | onRateLimit: (retryAfter, options) => { 15 | warning(`Request quota exhausted for request ${options.method} ${options.url}`); 16 | if (options.request.retryCount === 0) { 17 | // only retries once 18 | warning(`Retrying after ${retryAfter} seconds!`); 19 | return true; 20 | } 21 | }, 22 | onAbuseLimit: (retryAfter, options) => { 23 | // does not retry, only logs a warning 24 | warning(`Abuse detected for request ${options.method} ${options.url}`); 25 | }, 26 | }), 27 | ); 28 | const diffset = await differ.diff(intoParams(config)); 29 | setOutput('files', diffset.join(' ')); 30 | let filterSets = sets(config.fileFilters, diffset); 31 | Array.from(Object.entries(filterSets)).forEach(([key, matches]) => { 32 | debug(`files for ${key} ${matches}`); 33 | setOutput(key, matches.join(' ')); 34 | }); 35 | } catch (error) { 36 | console.log(error); 37 | setFailed((error as { message: string }).message); 38 | } 39 | } 40 | 41 | run(); 42 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import { Params } from './diff'; 2 | export interface Config { 3 | githubToken: string; 4 | githubRef: string; 5 | githubRepository: string; 6 | base?: string | undefined; 7 | fileFilters: Record; 8 | sha: string; 9 | } 10 | 11 | type Env = Record; 12 | 13 | /** GitHub exposes `with` input fields in the form of env vars prefixed with INPUT_ */ 14 | const FileFilter = /INPUT_(\w+)_FILES/; 15 | 16 | const cleanRef = (ref: string): string => { 17 | if (ref.indexOf('refs/heads/') === 0) { 18 | return ref.substring(11); 19 | } 20 | if (ref.indexOf('refs/tags/') === 0) { 21 | return ref.substring(10); 22 | } 23 | return ref; 24 | }; 25 | export const intoParams = (config: Config): Params => { 26 | const [owner, repo] = config.githubRepository.split('/', 2); 27 | const head = cleanRef(config.githubRef); 28 | const base = config.base || 'master'; 29 | const ref = config.sha; 30 | return { 31 | base, 32 | head, 33 | owner, 34 | repo, 35 | ref, 36 | }; 37 | }; 38 | 39 | export const parseConfig = (env: Env): Config => { 40 | return { 41 | githubToken: env['INPUT_TOKEN'] || '', 42 | githubRef: env.GITHUB_HEAD_REF || env.GITHUB_REF || '', 43 | githubRepository: env.GITHUB_REPOSITORY || '', 44 | base: env.INPUT_BASE, 45 | fileFilters: Array.from(Object.entries(env)).reduce((filters, [key, value]) => { 46 | if (FileFilter.test(key)) { 47 | filters[key.toLowerCase().replace('input_', '')] = value; 48 | } 49 | return filters; 50 | }, {}), 51 | sha: env.GITHUB_SHA || '', 52 | }; 53 | }; 54 | -------------------------------------------------------------------------------- /__tests__/diff.test.ts: -------------------------------------------------------------------------------- 1 | import { sets } from '../src/diff'; 2 | 3 | import { assert, describe, it } from 'vitest'; 4 | 5 | describe('diff', () => { 6 | describe('GitHubDiff', () => { 7 | it('generates diff based on compare api', async () => { 8 | // nock("https://api.github.com") 9 | // .persist() 10 | // .get("/repos/owner/repo/compare/master...foo") 11 | // .reply( 12 | // 200, 13 | // JSON.stringify({ 14 | // files: [ 15 | // { 16 | // status: "added", 17 | // filename: "added.txt" 18 | // }, 19 | // { 20 | // status: "removed", 21 | // filename: "removed.txt" 22 | // } 23 | // ] 24 | // }) 25 | // ); 26 | // const response = await new GitHubDiff(new GitHub("fake")).diff({ 27 | // head: "foo", 28 | // base: "master", 29 | // owner: "owner", 30 | // repo: "repo" 31 | // }); 32 | }); 33 | }); 34 | describe('sets', () => { 35 | it('returns a map of filtered files based on simple patterns', () => { 36 | const result = sets({ md_files: '**/*.md' }, ['foo/bar.md', 'baz.md', 'foo.js']); 37 | assert.deepStrictEqual(result, { 38 | md_files: ['foo/bar.md', 'baz.md'], 39 | }); 40 | }); 41 | it("returns yields no map entries for files that don't match", () => { 42 | const result = sets({ rust_files: '**/*.rs' }, ['foo/bar.md', 'baz.md', 'foo.js']); 43 | assert.deepStrictEqual(result, {}); 44 | }); 45 | it('returns a map of filtered files based on multi-line patterns', () => { 46 | const result = sets({ jvm_files: '**/*.java\n**/*.scala' }, [ 47 | 'src/main/java/com/foo/Bar.java', 48 | 'src/main/scala/com/foo/Baz.scala', 49 | ]); 50 | assert.deepStrictEqual(result, { 51 | jvm_files: ['src/main/java/com/foo/Bar.java', 'src/main/scala/com/foo/Baz.scala'], 52 | }); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/diff.ts: -------------------------------------------------------------------------------- 1 | import { Octokit } from '@octokit/rest'; 2 | import { Minimatch } from 'minimatch'; 3 | 4 | export type Params = { 5 | base: string; 6 | head: string; 7 | owner: string; 8 | repo: string; 9 | ref: string; 10 | }; 11 | 12 | /** produce a collection of named diff sets based on patterns defined in sets */ 13 | export const sets = ( 14 | filters: Record, 15 | files: Array, 16 | ): Record> => 17 | Array.from(Object.entries(filters)).reduce( 18 | (filtered, [key, patterns]) => 19 | patterns.split(/\r?\n/).reduce((filtered, pattern) => { 20 | let matcher = new Minimatch(pattern); 21 | let matched = files.filter((file) => matcher.match(file)); 22 | if (matched.length > 0) { 23 | filtered[key] = (filtered[key] || []).concat(matched); 24 | } 25 | return filtered; 26 | }, filtered), 27 | {}, 28 | ); 29 | 30 | export interface Diff { 31 | diff(params: Params): Promise>; 32 | } 33 | 34 | const isDefined = (s: T | undefined): s is T => { 35 | return s != undefined; 36 | }; 37 | export class GitHubDiff implements Diff { 38 | readonly github: Octokit; 39 | constructor(github: Octokit) { 40 | this.github = github; 41 | } 42 | async diff(params: Params): Promise> { 43 | // if this is a merge to master push 44 | // base and head will both be the same 45 | if (params.base === params.head) { 46 | const commit = await this.github.repos.getCommit(params); 47 | return ( 48 | commit.data.files 49 | ?.filter((file) => file.status != 'removed') 50 | .map((file) => file.filename) 51 | .filter(isDefined) || [] 52 | ); 53 | } else { 54 | const response = await this.github.repos.compareCommits({ 55 | ...params, 56 | ref: undefined, 57 | }); 58 | return (response.data.files || []) 59 | .filter((file) => file.status != 'removed') 60 | .map((file) => file.filename); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - "**" 9 | pull_request: 10 | branches: 11 | - master 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-24.04 16 | steps: 17 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5 18 | 19 | - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v4 20 | with: 21 | node-version-file: ".tool-versions" 22 | cache: "npm" 23 | 24 | - name: Install 25 | run: npm ci 26 | 27 | - name: Build 28 | run: npm run build 29 | 30 | - name: Test 31 | run: npm run test 32 | 33 | - name: Format 34 | run: npm run fmtcheck 35 | # - name: "check for uncommitted changes" 36 | # # Ensure no changes, but ignore node_modules dir since dev/fresh ci deps installed. 37 | # run: | 38 | # git diff --exit-code --stat -- . ':!node_modules' \ 39 | # || (echo "##[error] found changed files after build. please 'npm run build && npm run fmt'" \ 40 | # "and check in all changes" \ 41 | # && exit 1) 42 | integration: 43 | needs: build 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5 47 | 48 | - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v4 49 | with: 50 | node-version-file: ".tool-versions" 51 | cache: "npm" 52 | 53 | - name: Diffset 54 | id: diffset 55 | uses: ./ # Uses an action in the root directory 56 | with: 57 | md_files: "*.md" 58 | 59 | - name: Print Diffset 60 | run: ls -al ${{ steps.diffset.outputs.files }} 61 | 62 | - name: Print MD Files 63 | if: steps.diffset.outputs.md_files 64 | run: echo "Files changed were ${{ steps.diffset.outputs.md_files }}" 65 | 66 | - name: Print Rust Files 67 | if: steps.diffset.outputs.rust_files 68 | run: echo "Files changed were ${{ steps.diffset.outputs.rust_files }}" 69 | -------------------------------------------------------------------------------- /__tests__/util.test.ts: -------------------------------------------------------------------------------- 1 | import { intoParams, parseConfig } from '../src/util'; 2 | 3 | import { assert, describe, it } from 'vitest'; 4 | 5 | describe('util', () => { 6 | describe('infoParams', () => { 7 | it('transforms a config into diff params for heads', () => { 8 | assert.deepStrictEqual( 9 | intoParams({ 10 | githubToken: 'aeiou', 11 | githubRef: 'refs/heads/branch', 12 | githubRepository: 'owner/repo', 13 | fileFilters: {}, 14 | sha: 'b04376c43f66b8beed87abe6e28504781a4e461d', 15 | }), 16 | { 17 | base: 'master', 18 | head: 'branch', 19 | owner: 'owner', 20 | repo: 'repo', 21 | ref: 'b04376c43f66b8beed87abe6e28504781a4e461d', 22 | }, 23 | ); 24 | }); 25 | }); 26 | describe('parseConfig', () => { 27 | it('parses configuration from env', () => { 28 | assert.deepStrictEqual( 29 | parseConfig({ 30 | GITHUB_REF: 'head/refs/test', 31 | GITHUB_REPOSITORY: 'softprops/diffset', 32 | GITHUB_SHA: 'b04376c43f66b8beed87abe6e28504781a4e461d', 33 | INPUT_TOKEN: 'aeiou', 34 | INPUT_FOO_FILES: '*.foo', 35 | INPUT_BAR: 'ignored', 36 | }), 37 | { 38 | githubRef: 'head/refs/test', 39 | githubRepository: 'softprops/diffset', 40 | githubToken: 'aeiou', 41 | base: undefined, 42 | fileFilters: { 43 | foo_files: '*.foo', 44 | }, 45 | sha: 'b04376c43f66b8beed87abe6e28504781a4e461d', 46 | }, 47 | ); 48 | }); 49 | it('parses configuration from env including custom base', () => { 50 | assert.deepStrictEqual( 51 | parseConfig({ 52 | GITHUB_REF: 'head/refs/test', 53 | GITHUB_REPOSITORY: 'softprops/diffset', 54 | GITHUB_SHA: 'b04376c43f66b8beed87abe6e28504781a4e461d', 55 | INPUT_TOKEN: 'aeiou', 56 | INPUT_FOO_FILES: '*.foo', 57 | INPUT_BASE: 'develop', 58 | INPUT_BAR: 'ignored', 59 | }), 60 | { 61 | githubRef: 'head/refs/test', 62 | githubRepository: 'softprops/diffset', 63 | githubToken: 'aeiou', 64 | base: 'develop', 65 | fileFilters: { 66 | foo_files: '*.foo', 67 | }, 68 | sha: 'b04376c43f66b8beed87abe6e28504781a4e461d', 69 | }, 70 | ); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 3.0.2 2 | 3 | maintenance release with updated dependencies 4 | 5 | ## 3.0.1 6 | 7 | maintenance release with updated dependencies 8 | 9 | ## 3.0.0 10 | 11 | - Upgrade Node.js version to 24 in action. Make sure your runner is on version v2.327.1 or later to ensure compatibility with this release. [Release Notes](https://github.com/actions/runner/releases/tag/v2.327.1) 12 | 13 | ## 2.2.0 14 | 15 | - Migrate from Jest to Vitest 16 | - Bump to use Node.js 24 17 | - Dependency updates 18 | 19 | ## 2.1.6 20 | 21 | maintenance release with updated dependencies 22 | 23 | ## 2.1.5 24 | 25 | maintenance release with updated dependencies 26 | 27 | ## 2.1.4 28 | 29 | maintenance release with updated dependencies 30 | 31 | ## 2.1.3 32 | 33 | fix lockfile issue 34 | 35 | ## 2.1.2 36 | 37 | maintenance release with updated dependencies 38 | 39 | ## 2.1.1 40 | 41 | maintenance release with updated dependencies 42 | 43 | ## 2.1.0 44 | 45 | ### Exciting New Features 🎉 46 | 47 | * feat: get github.token as default input by @chenrui333 in https://github.com/softprops/diffset/pull/14 48 | 49 | ### Other Changes 🔄 50 | 51 | * fix GITHUB_TOKEN ref by @chenrui333 in https://github.com/softprops/diffset/pull/16 52 | 53 | ## 2.0.1 54 | 55 | - typescript v5 56 | - update deps and npm lockfile 57 | 58 | ## 2.0.0 59 | 60 | - upgrade to Node.js 20 61 | - update deps 62 | 63 | ## 1.0.0 64 | 65 | ## 0.1.7 66 | 67 | - upgrade dependencies 68 | 69 | ## 0.1.6 70 | 71 | - upgrade actions diff to node16 to address deprecation warnings 72 | - upgrade actions/core to address deprecation warnings 73 | - upgrade minimatch 74 | 75 | ## 0.1.5 76 | 77 | - bug fix. upgrade to latest octokit now double-escapes Git-flow–style branches 78 | 79 | ## 0.1.4 80 | 81 | - bug fix. exclude the ref argument when comparing two branches 82 | 83 | ## 0.1.3 84 | 85 | - bug fix. strip ref for refs/tags as well as refs/heads 86 | 87 | ## 0.1.2 88 | 89 | - add support for push to master 90 | 91 | This action uses the ref that triggered an event as a basis of comparison with a base, typically your default branch. For pushes to your default branch, these values will be the same and yield no diff! In this version, the plugin uses the merge SHA to resolve files in a merge commit and produce a diff. 92 | 93 | - add support for pull_request event diffs 94 | 95 | GitHub pull_request events use different values for `GITHUB_REF` 96 | that aren't useful as inputs to api calls to resolve a diff set. 97 | A separate `GITHUB_HEAD_REF` is now resolved when available in place of `GITHUB_REF` which makes pull_request triggers work as expected 98 | 99 | ## 0.1.1 100 | 101 | - fix issue with branches not properly being url encoded before passing off to github api 102 | 103 | ## 0.1.0 104 | 105 | - Initial release 106 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "ES2022", 6 | "module": "NodeNext", 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 12 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 13 | // "outFile": "./", /* Concatenate and emit output to single file. */ 14 | "outDir": "./lib", /* Redirect output structure to the directory. */ 15 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 16 | // "composite": true, /* Enable project compilation */ 17 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 18 | // "removeComments": true, /* Do not emit comments to output. */ 19 | // "noEmit": true, /* Do not emit outputs. */ 20 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 21 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 22 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 23 | 24 | /* Strict Type-Checking Options */ 25 | "strict": true, /* Enable all strict type-checking options. */ 26 | "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ 27 | // "strictNullChecks": true, /* Enable strict null checks. */ 28 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 29 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 30 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 31 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 32 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 33 | 34 | /* Additional Checks */ 35 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 36 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 37 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 38 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 39 | 40 | /* Module Resolution Options */ 41 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 42 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 43 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 44 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 45 | // "typeRoots": [], /* List of folders to include type definitions from. */ 46 | "types": ["vitest/globals"], 47 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 48 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 49 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 50 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 51 | 52 | /* Source Map Options */ 53 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 54 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 55 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 56 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 57 | 58 | /* Experimental Options */ 59 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 60 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 61 | }, 62 | "exclude": ["node_modules", "**/*.test.ts", "vitest.config.ts"] 63 | } 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | diffset 3 |

4 | 5 |

6 | A GitHub Action for producing lists of files that changed between branches 7 |

8 | 9 |
10 | 11 | 12 | 13 |
14 | 15 |
16 | 17 | ## ⚡ why bother 18 | 19 | The goal of a workflow is to do its work as quickly as possible. A core feature of GitHub actions enables this called [path filtering](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#onpushpull_requestpaths) 20 | 21 | Many command line tools accept a list of files as inputs to limit the amount of work they need to do. Diffset is a tool that targets the usecase of maximally efficient workflows where such tools are in use so that you can apply them to only the things that changed. Save yourself some time and [money](https://help.github.com/en/github/setting-up-and-managing-billing-and-payments-on-github/about-billing-for-github-actions#about-billing-for-github-actions). 22 | 23 | ✨ Doing less is faster than doing more ✨ 24 | 25 | ## 🤸 Usage 26 | 27 | The typical setup for diffset involves adding job step using `softprops/diffset@v1`. 28 | 29 | This will collect a list of files that have changed and export them to an output named `files`. It retrieves this list of files from the GitHub api and as such it will need your repositories `GITHUB_TOKEN` secret. 30 | 31 | ```diff 32 | name: Main 33 | 34 | on: push 35 | 36 | jobs: 37 | main: 38 | runs-on: ubuntu-latest 39 | steps: 40 | - name: Checkout 41 | uses: actions/checkout@v5 42 | + - name: Diffset 43 | + id: diffset 44 | + uses: softprops/diffset@v3 45 | - name: Print Diffset 46 | run: ls -al ${{ steps.diffset.outputs.files }} 47 | ``` 48 | 49 | ### 💅 Customizing 50 | 51 | The default behavior of diff is to simply introduce an output named `files` which is the set of changed files in your branch. In other cases certain workflows may benefit from skipping jobs when a class of files are not changed. 52 | 53 | #### Custom diff sets 54 | 55 | Diffset also allows you to create filters for named sets of files to avoid doing unessessary work within your pipeline and produces an named output for those sets of files when they changed. These named sets of files can include multiple patterns for any given set to allow for maximum flexibility. 56 | 57 | ```diff 58 | name: Main 59 | 60 | on: push 61 | 62 | jobs: 63 | main: 64 | runs-on: ubuntu-latest 65 | steps: 66 | - name: Checkout 67 | uses: actions/checkout@v5 68 | - name: Diffset 69 | id: diffset 70 | uses: softprops/diffset@v3 71 | + with: 72 | + special_files: | 73 | + src/special/**/*.ts 74 | + src/or-these/**/*.ts 75 | - name: Print Special Files 76 | if: diffset.outputs.special_files 77 | run: ls -al ${{ steps.diffset.outputs.special_files }} 78 | - name: Other work 79 | run: echo "..." 80 | ``` 81 | 82 | #### Custom base branch 83 | 84 | Most GitHub repositories use a default "master" branch. Diffset uses this as a basis of comparison by default. If you use a different base branch you can use the `steps.with` key to provide a custom `base` 85 | 86 | ```diff 87 | name: Main 88 | 89 | on: push 90 | 91 | jobs: 92 | main: 93 | runs-on: ubuntu-latest 94 | steps: 95 | - name: Checkout 96 | uses: actions/checkout@v5 97 | - name: Diffset 98 | id: diffset 99 | uses: softprops/diffset@v3 100 | + with: 101 | + base: develop 102 | - name: Other work 103 | run: ls -al ${{ steps.diffset.outputs.files }} 104 | ``` 105 | 106 | #### inputs 107 | 108 | The following are optional as `step.with` keys 109 | 110 | This action supports dynamically named inputs which will result in dynamically named outputs. 111 | Specifically this action accepts any inputs with a suffix of `_files` 112 | 113 | | Name | Type | Description | 114 | | --------- | ------ | ------------------------------------------------ | 115 | | `*_files` | string | A file pattern to filter changed files | 116 | | `base` | string | Base branch for comparison. Defaults to "master" | 117 | 118 | #### outputs 119 | 120 | The following outputs can be accessed via `${{ steps..outputs }}` from this action 121 | 122 | This action supports dynamically named inputs which will result in dynamically named outputs. 123 | Specifically this action yields outputs based on inputs named with a suffix of `_files` 124 | 125 | | Name | Type | Description | 126 | | --------- | ------ | -------------------------------------------------------------------------- | 127 | | `*_files` | string | A space delimited list of files that changed that matched an input pattern | 128 | 129 | ### 💁‍♀️ pro tips 130 | 131 | In more complicated workflows you may find that simply cloning your repository takes a succfiently long about of time. In these cases you can opt to generate a diffset first, then checkout only if needed. 132 | 133 | ```diff 134 | name: Main 135 | 136 | on: push 137 | 138 | jobs: 139 | main: 140 | runs-on: ubuntu-latest 141 | steps: 142 | - name: Diffset 143 | id: diffset 144 | uses: softprops/diffset@v3 145 | with: 146 | special_files: | 147 | src/special/**/*.ts 148 | src/or-these/**/*.ts 149 | - name: Checkout 150 | + if: diffset.outputs.special_files 151 | uses: actions/checkout@v5 152 | ``` 153 | 154 | Doug Tangren (softprops) 2019-2021 155 | 156 | . 157 | --------------------------------------------------------------------------------