Skip to content

Commit

Permalink
Add ability to run this workflow in shadow mode
Browse files Browse the repository at this point in the history
In "shadow mode", this workflow will compute a score even for users who
have not opted into the workflow, but it will not do any labeling or
commenting. This sets up a future feature in which this workflow
serializes the scores it computes in order to enable discriminatory
analysis between those who have opted into the workflow and those wh
have not.
  • Loading branch information
lerebear committed Mar 3, 2024
1 parent ef04130 commit 1c79755
Show file tree
Hide file tree
Showing 11 changed files with 199 additions and 55 deletions.
2 changes: 2 additions & 0 deletions .github/linters/.eslintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ rules:
'i18n-text/no-en': 'off',
'import/no-namespace': 'off',
'no-console': 'off',
'no-shadow': 'off',
'no-unused-vars': 'off',
'prettier/prettier': 'error',
'semi': 'off',
Expand All @@ -63,6 +64,7 @@ rules:
'@typescript-eslint/no-misused-new': 'error',
'@typescript-eslint/no-namespace': 'error',
'@typescript-eslint/no-require-imports': 'error',
'@typescript-eslint/no-shadow': 'error',
'@typescript-eslint/no-unnecessary-qualifier': 'error',
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
'@typescript-eslint/no-unused-vars': 'error',
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,6 @@ sizeup:
The default configuration that is used when no configuration file is provided can be found in [`src/config/default.yaml`](./src/config/default.yaml).

The full specification for the configuration file is provide by the JSON schema at [`src/config/schema.json`](./src/config/schema.json).
The full specification for the configuration file, which includes options that are not specified in the example above or in the default configuration, is provided by the JSON schema at [`src/config/schema.json`](./src/config/schema.json).

For details about what configuration can be provided under the `sizeup` key, please see the [`sizeup-core` library's configuration guide](https://github.com/lerebear/sizeup-core#configuration).
47 changes: 46 additions & 1 deletion __tests__/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,28 @@ describe('action', () => {
)
})

it('skips labelling a pull request when running in shadow mode', async () => {
// Mock the @actions/github context.
Object.defineProperty(github, 'context', {
value: pullRequestEventContext()
})

loadConfigurationMock.mockImplementation(() => ({
// Mock config such that the only opted-in user is @glortho. The pull request will be created
// by @lerebear, who should therefore *not* be considered opted in.
optIns: ['glortho'],
shadowOptOuts: true
}))

await main.run()

expect(runMock).toHaveReturned()
expect(setFailedMock).not.toHaveBeenCalled()
expect(infoMock).toHaveBeenCalledWith(
'Skipping labeling because this workflow is running in shadow mode'
)
})

it('skips commenting on a draft pull request when configured to do so', async () => {
// Mock the @actions/github context.
Object.defineProperty(github, 'context', {
Expand All @@ -164,6 +186,29 @@ describe('action', () => {
)
})

it('skips commenting on a pull request when running in shadow mode', async () => {
// Mock the @actions/github context.
Object.defineProperty(github, 'context', {
value: pullRequestEventContext()
})

loadConfigurationMock.mockImplementation(() => ({
// Mock config such that the only opted-in user is @glortho. The pull request will be created
// by @lerebear, who should therefore *not* be considered opted in.
optIns: ['glortho'],
shadowOptOuts: true,
commenting: { scoreThreshold: 0 }
}))

await main.run()

expect(runMock).toHaveReturned()
expect(setFailedMock).not.toHaveBeenCalled()
expect(infoMock).toHaveBeenCalledWith(
'Skipping commenting because this workflow is running in shadow mode'
)
})

it('runs the workflow sucessfully when optIns configuration is present and the pull request author is in it', async () => {
// Mock the @actions/github context.
Object.defineProperty(github, 'context', {
Expand All @@ -172,7 +217,7 @@ describe('action', () => {

loadConfigurationMock.mockImplementation(() => ({
// Mock config such that the only opted-in user is @glortho. The pull request will be created
// by @lerebear, who should therefore not be considered optedpin.
// by @lerebear, who should therefore *not* be considered opted in.
optIns: ['lerebear']
}))

Expand Down
2 changes: 1 addition & 1 deletion badges/coverage.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions dist/config/schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 35 additions & 15 deletions dist/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion src/commenting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as github from '@actions/github'
import { PullRequest } from '@octokit/webhooks-types' // eslint-disable-line import/no-unresolved
import { Score } from 'sizeup-core'
import { Configuration } from './configuration'
import { configOrDefault } from './initializer'
import { OptInStatus, configOrDefault } from './initializer'

const DEFAULT_COMMENT_TEMPLATE = `
👋 @{{author}} this pull request exceeds the configured reviewability score threshold of {{threshold}}. Your actual score was {{score}}.
Expand All @@ -28,6 +28,7 @@ const COMMENT_METADATA =
export async function addOrUpdateScoreThresholdExceededComment(
pull: PullRequest,
score: Score,
optInStatus: OptInStatus,
config: Configuration
): Promise<void> {
const threshold = configOrDefault(
Expand All @@ -44,6 +45,13 @@ export async function addOrUpdateScoreThresholdExceededComment(
return
}

if (optInStatus === OptInStatus.Shadow) {
core.info(
'Skipping commenting because this workflow is running in shadow mode'
)
return
}

const comment = scoreThresholdExceededComment(pull, score, config)

if (
Expand Down
5 changes: 5 additions & 0 deletions src/config/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@
"type": "string"
}
},
"shadowOptOuts": {
"description": "Whether or not to compute a score even for users who have opted out of the workflow",
"type": "boolean",
"default": false
},
"sizeup": {
"$ref": "https://raw.githubusercontent.com/lerebear/sizeup/main/src/config/schema.json"
}
Expand Down
79 changes: 58 additions & 21 deletions src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ export interface Configuration {
*
* @minItems 1
*/
optIns?: string[]
optIns?: [string, ...string[]]
/**
* Whether or not to compute a score even for users who have opted out of the workflow
*/
shadowOptOuts?: boolean
sizeup?: Configuration1
}
/**
Expand All @@ -63,33 +67,62 @@ export interface Configuration1 {
*
* @minItems 1
*/
categories?: {
/**
* human-friendly name of the category
*/
name: string
/**
* A visual label that should be used to represent this category
*/
label?: {
categories?: [
{
/**
* name of the label that should be used to represent this category
* human-friendly name of the category
*/
name: string
/**
* describes the meaning of the label that will be used to represent this category
* A visual label that should be used to represent this category
*/
description?: string
label?: {
/**
* name of the label that should be used to represent this category
*/
name: string
/**
* describes the meaning of the label that will be used to represent this category
*/
description?: string
/**
* preferred CSS hex color label that should be used to represent this category
*/
color?: string
}
/**
* preferred CSS hex color label that should be used to represent this category
* inclusive upper bound on the score that a pull request must have to be assigned this category
*/
color?: string
}
/**
* inclusive upper bound on the score that a pull request must have to be assigned this category
*/
lte?: number
}[]
lte?: number
},
...{
/**
* human-friendly name of the category
*/
name: string
/**
* A visual label that should be used to represent this category
*/
label?: {
/**
* name of the label that should be used to represent this category
*/
name: string
/**
* describes the meaning of the label that will be used to represent this category
*/
description?: string
/**
* preferred CSS hex color label that should be used to represent this category
*/
color?: string
}
/**
* inclusive upper bound on the score that a pull request must have to be assigned this category
*/
lte?: number
}[]
]
scoring?: {
/**
* an expression, written in prefix-notation, that describes how to combine features to produce a score
Expand All @@ -99,6 +132,10 @@ export interface Configuration1 {
* named expression aliases, each of which can be used as shortand in a formula
*/
aliases?: {
/**
* This interface was referenced by `undefined`'s JSON-Schema definition
* via the `patternProperty` "^[\w][\w-]*$".
*/
[k: string]: string
}
}
Expand Down
Loading

0 comments on commit 1c79755

Please sign in to comment.