Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enable tracking multiple paths #8

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,15 @@ jobs:

# File path to track. In this example, Ryu-Cho will only track
# commits that modified files under `docs` folder. Optional.
# (deprecated: use `includes` instead)
path-starts-with: docs/

# File paths to track (glob patterns). Optional.
includes: []

# File paths to exclude from tracking (glob patterns). Optional.
excludes: []

# GitHub workflow name that runs Ryu-Cho. This is required since
# Ryu-Cho determines the last run by looking into last workflow
# run timestamp. Optional. Defaults to `ryu-cho`.
Expand Down
19 changes: 7 additions & 12 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@ inputs:
description: 'The git commit to track from. e.g. 4ed8b2f83a2f149734f3c5ecb6438309bd85a9e5'
required: true
path-starts-with:
description: 'File path to track. e.g `docs/`'
description: 'File path to track. e.g `docs/` (deprecated: use `includes` instead)'
required: false
includes:
description: 'File paths to track (glob patterns)'
required: false
excludes:
description: 'File paths to exclude from tracking (glob patterns)'
required: false
workflow-name:
description: 'GitHub workflow name that executes Ryu Cho action. Defaults to `ryu-cho`'
Expand All @@ -43,17 +49,6 @@ runs:
run: ${{ github.action_path }}/scripts/checkout.sh
shell: bash
- name: Run Ryu-Cho
env:
ACCESS_TOKEN: ${{ inputs.access-token }}
USER_NAME: ${{ inputs.username }}
EMAIL: ${{ inputs.email }}
UPSTREAM_REPO: ${{ inputs.upstream-repo }}
UPSTREAM_REPO_BRANCH: ${{ inputs.upstream-repo-branch }}
HEAD_REPO: ${{ inputs.head-repo }}
HEAD_REPO_BRANCH: ${{ inputs.head-repo-branch }}
TRACK_FROM: ${{ inputs.track-from }}
PATH_STARTS_WITH: ${{ inputs.path-starts-with }}
WORKFLOW_NAME: ${{ inputs.workflow-name }}
run: |
cd ryu-cho
yarn install
Expand Down
9 changes: 0 additions & 9 deletions jest.config.js

This file was deleted.

13 changes: 8 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,27 @@
"start": "ts-node src/index.ts",
"lint": "prettier --check --write --parser typescript \"{src,test}/**/*.ts\"",
"lint:fail": "prettier --check --parser typescript \"{src,test}/**/*.ts\"",
"jest": "jest",
"test": "yarn lint && yarn jest"
"vitest": "vitest",
"test": "yarn lint && vitest",
"test:ui": "yarn lint && vitest --ui"
},
"dependencies": {
"@actions/core": "^1.9.0",
"@octokit/rest": "^18.3.0",
"@types/node": "^14.14.31",
"@types/shelljs": "^0.8.8",
"colors": "^1.4.0",
"micromatch": "^4.0.5",
"queue": "^6.0.2",
"rss-parser": "^3.12.0",
"shelljs": "^0.8.4",
"ts-node": "^9.1.1",
"typescript": "^4.2.2"
},
"devDependencies": {
"@types/jest": "^26.0.20",
"jest": "^26.6.3",
"@types/micromatch": "^4.0.2",
"@vitest/ui": "^0.18.1",
"prettier": "^2.2.1",
"ts-jest": "^26.5.2"
"vitest": "^0.18.1"
}
}
17 changes: 17 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,20 @@ export interface UserConfig {
* path will be not tracked.
*
* @example 'docs/'
* @deprecated Use `includes` instead.
*/
pathStartsWith?: string

/**
* File paths to track (glob patterns). If this option is set, commits
* not containing the paths will not be tracked.
*/
includes: string[]

/**
* File paths to exclude (glob patterns).
*/
excludes: string[]
}

export interface Config {
Expand All @@ -92,7 +104,10 @@ export interface Config {
accessToken: string
workflowName: string
trackFrom: string
/** @deprecated Use `includes` instead. */
pathStartsWith?: string
includes: string[]
excludes: string[]

remote: {
upstream: Remote
Expand All @@ -115,6 +130,8 @@ export function createConfig(config: UserConfig): Config {
workflowName: config.workflowName ?? 'ryu-cho',
trackFrom: config.trackFrom,
pathStartsWith: config.pathStartsWith,
includes: config.includes,
excludes: config.excludes,

remote: {
upstream: {
Expand Down
30 changes: 14 additions & 16 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
import { assert } from './utils'
import { createConfig } from './config'
import { RyuCho } from './ryu-cho'
import core from '@actions/core'

assert(!!process.env.ACCESS_TOKEN, '`accessToken` is required.')
assert(!!process.env.USER_NAME, '`userName` is required.')
assert(!!process.env.EMAIL, '`email` is required.')
assert(!!process.env.UPSTREAM_REPO, '`upstreamRepo` is required.')
assert(!!process.env.HEAD_REPO, '`headRepo` is required.')
assert(!!process.env.TRACK_FROM, '`trackFrom` is required.')
assert(typeof core !== 'undefined', `core is undefined, which probably means you're not running in a GitHub Action`)

const config = createConfig({
accessToken: process.env.ACCESS_TOKEN!,
userName: process.env.USER_NAME!,
email: process.env.EMAIL!,
upstreamRepo: process.env.UPSTREAM_REPO!,
upstreamRepoBranch: process.env.UPSTREAM_REPO_BRANCH,
headRepo: process.env.HEAD_REPO!,
headRepoBranch: process.env.HEAD_REPO_BRANCH,
workflowName: process.env.WORKFLOW_NAME,
trackFrom: process.env.TRACK_FROM!,
pathStartsWith: process.env.PATH_STARTS_WITH
accessToken: core.getInput('access-token', { required: true }),
userName: core.getInput('username', { required: true }),
email: core.getInput('email', { required: true }),
upstreamRepo: core.getInput('upstream-repo', { required: true }),
upstreamRepoBranch: core.getInput('upstream-repo-branch', { required: true }),
headRepo: core.getInput('head-repo', { required: true }),
headRepoBranch: core.getInput('head-repo-branch'),
workflowName: core.getInput('workflow-name'),
trackFrom: core.getInput('track-from', { required: true }),
pathStartsWith: core.getInput('path-starts-with'),
includes: core.getMultilineInput('includes'),
excludes: core.getMultilineInput('excludes'),
})

const ryuCho = new RyuCho(config)
Expand Down
40 changes: 34 additions & 6 deletions src/ryu-cho.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { Config, Remote } from './config'
import { Rss } from './rss'
import { GitHub } from './github'
import { Repository } from './repository'
import micromatch from 'micromatch'

interface Feed {
export interface Feed {
link: string
title: string
contentSnippet: string
Expand All @@ -23,7 +24,6 @@ export class RyuCho {
this.config = config
this.upstream = config.remote.upstream
this.head = config.remote.head

this.rss = new Rss()

this.github = new GitHub(config.accessToken)
Expand Down Expand Up @@ -98,15 +98,43 @@ export class RyuCho {
}

protected async containsValidFile(feed: Feed, hash: string) {
if (!this.config.pathStartsWith) {
if (!this.config.pathStartsWith && !this.config.includes && !this.config.excludes) {
return true
}

const res = await this.github.getCommit(this.head, hash)

return res.data.files!.some((file) => {
return file.filename!.startsWith(this.config.pathStartsWith!)
})
let hasValidFile = false

if (this.config.pathStartsWith) {
log('W', '`path-starts-with` is deprecated. Use `includes` instead.')

hasValidFile = res.data.files!.some((file) => {
return file.filename!.startsWith(this.config.pathStartsWith!)
})
}

if (this.config.includes?.length) {
const isFileIncluded = (filename: string) => {
return micromatch.isMatch(filename, this.config.includes)
}

hasValidFile = res.data.files!.some((file) => {
return isFileIncluded(file.filename!)
})
}

if (this.config.excludes?.length) {
const isFileExcluded = (filename: string) => {
return micromatch.isMatch(filename, this.config.excludes)
}

hasValidFile = res.data.files!.some((file) => {
return !isFileExcluded(file.filename!)
})
}

return hasValidFile
}

protected async createIssueIfNot(feed: Feed, hash: string) {
Expand Down
136 changes: 136 additions & 0 deletions tests/ryo-cho.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { RyuCho, type Feed } from '../src/ryu-cho'
import type { Config } from '../src/config'
import * as GitHub from '../src/github'
import { describe, it, expect, vi } from 'vitest'

vi.mock('../src/github')

const DEFAULT_CONFIG: Config = {
userName: '',
email: '',
accessToken: '',
workflowName: '',
trackFrom: '',
pathStartsWith: '',
includes: [],
excludes: [],

remote: {
upstream: {
url: '',
owner: '',
name: '',
branch: '',
},
head: {
url: '',
owner: '',
name: '',
branch: '',
}
}
}

const DEFAULT_FEED: Feed = {
isoDate: '',
link: '',
title: '',
contentSnippet: '',
}

type Mutable<T> = {
-readonly [P in keyof T]: T[P];
}

type MutableGitHub = Mutable<typeof GitHub>

function makeRyuCho(config: Partial<Config>, filenames: string[]) {
class TestRyuCho extends RyuCho {
public containsValidFile(feed: Feed, hash: string): Promise<boolean> {
return super.containsValidFile(feed, hash)
}
}
(GitHub as MutableGitHub).GitHub = vi.fn(() => ({
getCommit() {
return Promise.resolve({
data: {
files: filenames.map(n => ({ filename: n }))
}
})
}
})) as unknown as typeof GitHub.GitHub

const ryuCho = new TestRyuCho({
...DEFAULT_CONFIG,
...config,
})

return ryuCho
}

describe('RyuCho', () => {
it('containsValidFile matches single config.includes[]', async () => {
const includes = ['/docs/**/*.md']
const filenames = [
'/docs/guide/index.md',
'/docs/team.md',
'/README.md',
]
const ryuCho = makeRyuCho({ includes }, filenames)

const hasValidFile = ryuCho.containsValidFile(DEFAULT_FEED, 'hash')
await expect(hasValidFile).resolves.toBe(true)
})

it('containsValidFile does not match any config.includes[]', async () => {
const includes = ['/docs/**/*.md']
const filenames = [
'/docs/guide/index.txt',
'/docs/team.txt',
'/README.md',
]
const ryuCho = makeRyuCho({ includes }, filenames)

const hasValidFile = ryuCho.containsValidFile(DEFAULT_FEED, 'hash')
await expect(hasValidFile).resolves.toBe(false)
})

it('containsValidFile matches multiple config.includes[]', async () => {
const includes = ['/docs/**/*.md', '/README.md']
const filenames = [
'/docs/guide/index.md',
'/docs/team.md',
'/README.md',
]
const ryuCho = makeRyuCho({ includes }, filenames)

const hasValidFile = ryuCho.containsValidFile(DEFAULT_FEED, 'hash')
await expect(hasValidFile).resolves.toBe(true)
})

it('containsValidFile excludes specified config.includes[]', async () => {
// match all *.md files except README.md
const includes = ['**/!(README).md']
const filenames = [
'/docs/guide/index.txt',
'/docs/team.txt',
'/README.md',
]
const ryuCho = makeRyuCho({ includes }, filenames)

const hasValidFile = ryuCho.containsValidFile(DEFAULT_FEED, 'hash')
await expect(hasValidFile).resolves.toBe(false)
})

it('containsValidFile excludes specified config.excludes[]', async () => {
const includes = ['**/*.md']
const filenames = [
'/README.md',
]
const excludes = ['/README.md']
const ryuCho = makeRyuCho({ includes, excludes }, filenames)

const hasValidFile = ryuCho.containsValidFile(DEFAULT_FEED, 'hash')
await expect(hasValidFile).resolves.toBe(false)
})
})
1 change: 1 addition & 0 deletions tests/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as Utils from 'src/utils'
import { describe, it, expect } from 'vitest'

describe('utils', () => {
const https = 'https://github.com/vuejs/vuejs.org'
Expand Down
7 changes: 7 additions & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
reporters: 'verbose',
},
})