diff --git a/README.md b/README.md
index 9d911de3a..d02ca95ee 100644
--- a/README.md
+++ b/README.md
@@ -4,17 +4,17 @@
## Inputs
-| Parameter | Type | Required | Default | Description |
-| --------------------------- | ------- | ------------------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| `assignees` | String | only if `teams` is not specified | n/a | Comma separated list of user names. Issue will be assigned to those users. |
-| `teams` | String | only if `assignees` is not specified | n/a | Comma separated list of team names without the org prefix. Issue will be assigned to the team members.
**Important Requirement:** if using the `teams` input parameter, you need to use a personal access token with `read:org` scope (the default `GITHUB_TOKEN` is not enough). |
-| `numOfAssignee` | Number | false | n/a | Number of assignees that will be randomly picked from the teams or assignees. If not specified, assigns all users. |
-| `abortIfPreviousAssignees` | Boolean | false | false | Flag that aborts the action if there were assignees previously. |
-| `removePreviousAssignees` | Boolean | false | false | Flag that removes assignees before assigning them (useful the issue is reasigned). |
-| `allowNoAssignees` | Boolean | false | false | Flag that prevents the action from failing when there are no assignees. |
-| `allowSelfAssign` | Boolean | false | true | Flag that allows self-assignment to the issue author.
This flag is ignored when working with PRs as self assigning a PR for review is forbidden by GitHub. |
-| `issueNumber` | Number | false | n/a | Allows to override the issue number. This can be useful when context is missing. |
-| `teamIsPullRequestReviewer` | Boolean | false | false | Sets team as the PR reviewer instead of a member of the team. |
+| Parameter | Type | Required | Default | Description |
+| --------------------------- | ------- | ------------------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `assignees` | String | only if `teams` is not specified | n/a | Comma separated list of user names with optional [weights](#working-with-weighted-assignements). Issue will be assigned to those users. |
+| `teams` | String | only if `assignees` is not specified | n/a | Comma separated list of team names without the org prefix with optional [weights](#working-with-weighted-assignements). Issue will be assigned to the team members.
**Important Requirement:** if using the `teams` input parameter, you need to use a personal access token with `read:org` scope (the default `GITHUB_TOKEN` is not enough). |
+| `numOfAssignee` | Number | false | n/a | Number of assignees that will be randomly picked from the teams or assignees. If not specified, assigns all users. |
+| `abortIfPreviousAssignees` | Boolean | false | false | Flag that aborts the action if there were assignees previously. |
+| `removePreviousAssignees` | Boolean | false | false | Flag that removes assignees before assigning them (useful the issue is reasigned). |
+| `allowNoAssignees` | Boolean | false | false | Flag that prevents the action from failing when there are no assignees. |
+| `allowSelfAssign` | Boolean | false | true | Flag that allows self-assignment to the issue author.
This flag is ignored when working with PRs as self assigning a PR for review is forbidden by GitHub. |
+| `issueNumber` | Number | false | n/a | Allows to override the issue number. This can be useful when context is missing. |
+| `teamIsPullRequestReviewer` | Boolean | false | false | Sets team as the PR reviewer instead of a member of the team. |
## Examples
@@ -69,6 +69,30 @@ jobs:
numOfAssignee: 1
```
+### Working with weighted assignements
+
+When specifying `assignees` or `teams` values, you may provide weights to balance the randomness of the selection.
+The following formats are supported:
+
+```yml
+# No weights specified (same weight for all items)
+assignees: a, b, c
+# Weights specified
+assignees: a:1, b:5, c:2
+# Some weights specified (item weight defaults to 1 when not specified)
+assignees: a, b:2, c
+```
+
+Let's look at a practical example:
+
+```yml
+assignees: octocat:4,cat
+```
+
+- `octocat` has a weight of `4`.
+- `cat` has a weight of `1` (default value).
+- `octocat` has 4 chances out of 5 to be selected.
+
### Working with Project Cards
> [!WARNING]
diff --git a/src/__tests__/utils.test.js b/src/__tests__/utils.test.js
index b64675eb7..03e37b596 100644
--- a/src/__tests__/utils.test.js
+++ b/src/__tests__/utils.test.js
@@ -1,6 +1,6 @@
const {
+ parseAssignments,
parseIntInput,
- parseCsvInput,
pickNRandomFromArray,
getTeamMembers
} = require('../utils.js');
@@ -25,6 +25,45 @@ describe('utils', () => {
jest.clearAllMocks();
});
+ describe('parseAssignments', () => {
+ it('works when value is missing', async () => {
+ const values = parseAssignments('');
+ expect(values).toStrictEqual([]);
+ });
+
+ it('works with string list', async () => {
+ const values = parseAssignments('a,b,c');
+ expect(values).toStrictEqual(['a', 'b', 'c']);
+ });
+
+ it('works with some missing values and whitespace', async () => {
+ const values = parseAssignments(',a ,, , b,c,,');
+ expect(values).toStrictEqual(['a', 'b', 'c']);
+ });
+
+ it('works with weighted list', async () => {
+ const values = parseAssignments('a:1,b:2,c:3');
+ expect(values).toStrictEqual(['a', 'b', 'b', 'c', 'c', 'c']);
+ });
+
+ it('works with semi weighted list', async () => {
+ const values = parseAssignments('a,b:2,c');
+ expect(values).toStrictEqual(['a', 'b', 'b', 'c']);
+ });
+
+ it('fails when too many arguments', async () => {
+ expect(() => parseAssignments('a:1:unknown')).toThrow(
+ /Invalid assignment value/
+ );
+ });
+
+ it('fails when weight is invalid', async () => {
+ expect(() => parseAssignments('a:invalid')).toThrow(
+ /Invalid weight value/
+ );
+ });
+ });
+
describe('parseIntInput', () => {
it('works when value is a number', async () => {
expect(parseIntInput('3', 0)).toBe(3);
@@ -41,24 +80,6 @@ describe('utils', () => {
});
});
- describe('parseCsvInput', () => {
- it('works when value is a CSV', async () => {
- expect(parseCsvInput('1,2,3')).toStrictEqual(['1', '2', '3']);
- });
-
- it('works when value is missing', async () => {
- expect(parseCsvInput('')).toStrictEqual([]);
- });
-
- it('works with some missing values and whitespace', async () => {
- expect(parseCsvInput(',1 ,, , 2,3,,')).toStrictEqual([
- '1',
- '2',
- '3'
- ]);
- });
- });
-
describe('pickNRandomFromArray', () => {
it('works when selection size < array length', () => {
const result = pickNRandomFromArray([1, 2, 3, 4], 2);
diff --git a/src/action.js b/src/action.js
index f1c627c18..9f9ab452d 100644
--- a/src/action.js
+++ b/src/action.js
@@ -104,15 +104,9 @@ const runAction = async (octokit, context, parameters) => {
newAssignees = newAssignees.concat(teamMembers);
}
- // Remove duplicates from assignees
- newAssignees = [...new Set(newAssignees)];
-
// Remove author if allowSelfAssign is false
if (!allowSelfAssign) {
- const foundIndex = newAssignees.indexOf(author);
- if (foundIndex !== -1) {
- newAssignees.splice(foundIndex, 1);
- }
+ newAssignees = newAssignees.filter((name) => name !== author);
}
// Check if there are assignees left
@@ -120,6 +114,9 @@ const runAction = async (octokit, context, parameters) => {
// Select random assignees
if (numOfAssignee) {
newAssignees = pickNRandomFromArray(newAssignees, numOfAssignee);
+ } else {
+ // Remove duplicates from assignees
+ newAssignees = [...new Set(newAssignees)];
}
// Assign issue
diff --git a/src/index.js b/src/index.js
index 3533b92ec..f3481207f 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,15 +1,15 @@
const core = require('@actions/core');
const github = require('@actions/github');
const { runAction } = require('./action');
-const { parseIntInput, parseCsvInput } = require('./utils');
+const { parseIntInput, parseAssignments } = require('./utils');
try {
// Get params
const gitHubToken = core.getInput('repo-token', { required: true });
- const assignees = parseCsvInput(
+ const assignees = parseAssignments(
core.getInput('assignees', { required: false })
);
- const teams = parseCsvInput(core.getInput('teams', { required: false }));
+ const teams = parseAssignments(core.getInput('teams', { required: false }));
let numOfAssignee;
try {
numOfAssignee = parseIntInput(
diff --git a/src/utils.js b/src/utils.js
index b23965a0b..594aff98c 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -5,6 +5,31 @@ const parseCsvInput = (valueString) => {
.filter((item) => item !== '');
};
+const parseAssignments = (valueString) => {
+ const list = parseCsvInput(valueString);
+ const weightedList = [];
+ list.forEach((item) => {
+ const itemValues = item.split(':');
+ const name = itemValues[0];
+ let weight = 1;
+ if (itemValues.length === 2) {
+ try {
+ weight = parseIntInput(itemValues[1]);
+ } catch (e) {
+ throw new Error(
+ `Invalid weight value for ${name} assignment: ${itemValues[1]}`
+ );
+ }
+ } else if (itemValues.length > 2) {
+ throw new Error(`Invalid assignment value: ${valueString}`);
+ }
+ for (let i = 0; i < weight; i++) {
+ weightedList.push(name);
+ }
+ });
+ return weightedList;
+};
+
const parseIntInput = (valueString, defaultValue = 0) => {
let value = defaultValue;
if (valueString) {
@@ -20,11 +45,13 @@ const pickNRandomFromArray = (arr, n) => {
if (arr.length === 0) {
throw new Error('Can not pick random from empty list.');
}
- const available = [...arr];
+ let available = [...arr];
const result = [];
for (let i = 0; i < n && available.length > 0; i++) {
const randomIndex = Math.floor(Math.random() * available.length);
- result.push(available.splice(randomIndex, 1)[0]);
+ const pick = available[randomIndex];
+ result.push(pick);
+ available = available.filter((value) => value !== pick);
}
return result;
};
@@ -133,7 +160,7 @@ const removeAllReviewers = async (octokit, owner, repo, pull_number) => {
};
module.exports = {
- parseCsvInput,
+ parseAssignments,
parseIntInput,
pickNRandomFromArray,
getAssignees,