Add new Scheduled rule: EnforceMaxLifeOfIssues (#7693)

* Add new Scheduled rule: EnforceMax2YearLifeOfIssues

* Fix a minor error in the metadata file. AzureSdkOwners must be part of a block that contains a ServiceLabel entry which was missing from the example.

* Revert metadata.md file changes in this PR to update in a differnt PR

* Change rule name to EnforceMaxLifeOfIssues

* Update the scheduled-event-processor.yml sample yml's event name to remove the 2

* Update sample yml, RULES.md and Trigger EnforceMaxLifeOfIssues comment.

* scheduled if statement needs to match the actual cron
This commit is contained in:
James Suplizio 2024-02-26 09:36:54 -08:00 коммит произвёл GitHub
Родитель 53199a0e90
Коммит 7d9603e4f8
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
8 изменённых файлов: 208 добавлений и 25 удалений

Просмотреть файл

@ -275,5 +275,53 @@ namespace Azure.Sdk.Tools.GitHubEventProcessor.Tests.Static
Assert.AreEqual(0, totalUpdates, $"{rule} is {ruleState} and should not have produced any updates.");
}
}
/// <summary>
/// Test the EnforceMaxLifeOfIssues scheduled event.
/// Each item returned from the query will have three updates:
/// Issue will be closed
/// Issue will have a comment added
/// Issue will be locked
/// </summary>
/// <param name="rule">String, RulesConstants for the rule being tested</param>
/// <param name="payloadFile">JSon payload file for the event being tested</param>
/// <param name="ruleState">Whether or not the rule is on/off</param>
[Category("static")]
[TestCase(RulesConstants.EnforceMaxLifeOfIssues, "Tests.JsonEventPayloads/ScheduledEvent_payload.json", RuleState.On)]
[TestCase(RulesConstants.EnforceMaxLifeOfIssues, "Tests.JsonEventPayloads/ScheduledEvent_payload.json", RuleState.Off)]
public async Task TestEnforceMaxLifeOfIssues(string rule, string payloadFile, RuleState ruleState)
{
// Need something divisible by 3 Because EnforceMaxLifeOfIssues does 3 updates per issue
// creating 100 results should only result in 34 issues being closed and 34 comments created
// and 34 issues being locked = 102 expected updates.
int expectedUpdates = 102;
var mockGitHubEventClient = new MockGitHubEventClient(OrgConstants.ProductHeaderName);
mockGitHubEventClient.RulesConfiguration.Rules[rule] = ruleState;
var rawJson = TestHelpers.GetTestEventPayload(payloadFile);
ScheduledEventGitHubPayload scheduledEventPayload = SimpleJsonSerializer.Deserialize<ScheduledEventGitHubPayload>(rawJson);
// Create the fake issues to update.
mockGitHubEventClient.CreateSearchIssuesResult(expectedUpdates, scheduledEventPayload.Repository, ItemState.Open);
await ScheduledEventProcessing.EnforceMaxLifeOfIssues(mockGitHubEventClient, scheduledEventPayload);
var totalUpdates = await mockGitHubEventClient.ProcessPendingUpdates(scheduledEventPayload.Repository.Id);
// Verify the RuleCheck
Assert.AreEqual(ruleState == RuleState.On, mockGitHubEventClient.RulesConfiguration.RuleEnabled(rule), $"Rule '{rule}' enabled should have been {ruleState == RuleState.On} but RuleEnabled returned {ruleState != RuleState.On}.'");
if (RuleState.On == ruleState)
{
// Create the fake issues to update.
Assert.AreEqual(expectedUpdates, totalUpdates, $"The number of updates should have been {expectedUpdates} but was instead, {totalUpdates}");
// There should be expectedUpdates/3 issueUpdates, expectedUpdates/3 comments and expectedUpdates/3 issues to lock
int numIssueUpdates = mockGitHubEventClient.GetGitHubIssuesToUpdate().Count;
Assert.AreEqual(expectedUpdates / 3, numIssueUpdates, $"The number of issue updates should have been {expectedUpdates / 3} but was instead, {numIssueUpdates}");
int numComments = mockGitHubEventClient.GetComments().Count;
Assert.AreEqual(expectedUpdates / 3, numComments, $"The number of comments should have been {expectedUpdates / 3} but was instead, {numComments}");
int numIssuesToLock = mockGitHubEventClient.GetGitHubIssuesToLock().Count;
Assert.AreEqual(expectedUpdates / 3, numIssuesToLock, $"The number of issues to lock should have been {expectedUpdates / 3} but was instead, {numIssuesToLock}");
}
else
{
Assert.AreEqual(0, totalUpdates, $"{rule} is {ruleState} and should not have produced any updates.");
}
}
}
}

Просмотреть файл

@ -48,6 +48,7 @@ namespace Azure.Sdk.Tools.GitHubEventProcessor.Constants
public const string IdentifyStalePullRequests = "IdentifyStalePullRequests";
public const string CloseAddressedIssues = "CloseAddressedIssues";
public const string LockClosedIssues = "LockClosedIssues";
public const string EnforceMaxLifeOfIssues = "EnforceMaxLifeOfIssues";
}
}

Просмотреть файл

@ -63,6 +63,11 @@ namespace Azure.Sdk.Tools.GitHubEventProcessor.EventProcessing
await LockClosedIssues(gitHubEventClient, scheduledEventPayload);
break;
}
case RulesConstants.EnforceMaxLifeOfIssues:
{
await EnforceMaxLifeOfIssues(gitHubEventClient, scheduledEventPayload);
break;
}
default:
{
Console.WriteLine($"{cronTaskToRun} is not valid Scheduled Event rule. Please ensure the scheduled event yml is correctly passing in the correct rules constant.");
@ -539,5 +544,93 @@ namespace Azure.Sdk.Tools.GitHubEventProcessor.EventProcessing
}
}
}
/// <summary>
/// Trigger: Weekly, Monday at 10am
/// Query Criteria
/// Issue is open
/// Issue was last updated more than 30 days ago
/// Issue is unlocked
/// Issue was created more than 2 years ago
/// Resulting Action:
/// Close the issue
/// Add a comment: Hi @{issue.User.Login}, we deeply appreciate your input into this project. Regrettably,
/// this issue has remained inactive for over 2 years, leading us to the decision to close it.
/// We've implemented this policy to maintain the relevance of our issue queue and facilitate
/// easier navigation for new contributors. If you still believe this topic requires attention,
/// please feel free to create a new issue, referencing this one. Thank you for your understanding
/// and ongoing support.
/// Lock the issue
/// </summary>
/// <param name="gitHubEventClient">Authenticated GitHubEventClient</param>
/// <param name="scheduledEventPayload">ScheduledEventGitHubPayload deserialized from the json event payload</param>
public static async Task EnforceMaxLifeOfIssues(GitHubEventClient gitHubEventClient, ScheduledEventGitHubPayload scheduledEventPayload)
{
if (gitHubEventClient.RulesConfiguration.RuleEnabled(RulesConstants.EnforceMaxLifeOfIssues))
{
int ScheduledTaskUpdateLimit = await gitHubEventClient.ComputeScheduledTaskUpdateLimit();
SearchIssuesRequest request = gitHubEventClient.CreateSearchRequest(
scheduledEventPayload.Repository.Owner.Login,
scheduledEventPayload.Repository.Name,
IssueTypeQualifier.Issue,
ItemState.Open,
30, // more than 30 days
new List<IssueIsQualifier> { IssueIsQualifier.Unlocked },
null,
null,
365*2 // Created date > 2 years
);
int numUpdates = 0;
// In theory, maximumPage will be 10 since there's 100 results per-page returned by default but
// this ensures that if we opt to change the page size
int maximumPage = RateLimitConstants.SearchIssuesRateLimit / request.PerPage;
for (request.Page = 1; request.Page <= maximumPage; request.Page++)
{
SearchIssuesResult result = await gitHubEventClient.QueryIssues(request);
int iCounter = 0;
while (
// Process every item in the page returned
iCounter < result.Items.Count &&
// unless the update limit has been hit
numUpdates < ScheduledTaskUpdateLimit
)
{
Issue issue = result.Items[iCounter++];
IssueUpdate issueUpdate = gitHubEventClient.GetIssueUpdate(issue, false);
// Close the issue
issueUpdate.State = ItemState.Closed;
issueUpdate.StateReason = ItemStateReason.NotPlanned;
gitHubEventClient.AddToIssueUpdateList(scheduledEventPayload.Repository.Id,
issue.Number,
issueUpdate);
// Add a comment
string comment = $"Hi @{issue.User.Login}, we deeply appreciate your input into this project. Regrettably, this issue has remained inactive for over 2 years, leading us to the decision to close it. We've implemented this policy to maintain the relevance of our issue queue and facilitate easier navigation for new contributors. If you still believe this topic requires attention, please feel free to create a new issue, referencing this one. Thank you for your understanding and ongoing support.";
gitHubEventClient.CreateComment(scheduledEventPayload.Repository.Id,
issue.Number,
comment);
// Lock the issue
gitHubEventClient.LockIssue(scheduledEventPayload.Repository.Id, issue.Number, LockReason.Resolved);
// Close, Comment and Lock = 3 updates per issue
numUpdates += 3;
}
// The number of items in the query isn't known until the query is run.
// If the number of items in the result equals the total number of items matching the query then
// all the items have been processed.
// OR
// If the number of items in the result is less than the number of items requested per page then
// the last page of results has been processed which was not a full page
// OR
// The number of updates has hit the limit for a scheduled task
if (result.Items.Count == result.TotalCount ||
result.Items.Count < request.PerPage ||
numUpdates >= ScheduledTaskUpdateLimit)
{
break;
}
}
}
}
}
}

Просмотреть файл

@ -253,15 +253,6 @@ namespace Azure.Sdk.Tools.GitHubEventProcessor
prReview);
}
// Process any issue locks
foreach (var issueToLock in _gitHubIssuesToLock)
{
numUpdates++;
await _gitHubClient.Issue.LockUnlock.Lock(issueToLock.RepositoryId,
issueToLock.IssueNumber,
issueToLock.LockReason);
}
// Process any Scheduled task IssueUpdates
foreach (var issueToUpdate in _gitHubIssuesToUpdate)
{
@ -270,6 +261,17 @@ namespace Azure.Sdk.Tools.GitHubEventProcessor
issueToUpdate.IssueOrPRNumber,
issueToUpdate.IssueUpdate);
}
// Process any issue locks last in case the issue is being updated or having a comment added
// prior to being locked
foreach (var issueToLock in _gitHubIssuesToLock)
{
numUpdates++;
await _gitHubClient.Issue.LockUnlock.Lock(issueToLock.RepositoryId,
issueToLock.IssueNumber,
issueToLock.LockReason);
}
Console.WriteLine("Finished processing pending updates.");
}
// For the moment, nothing special is being done when rate limit exceptions are
@ -335,16 +337,16 @@ namespace Azure.Sdk.Tools.GitHubEventProcessor
Console.WriteLine($"Number of Review Dismissals {_gitHubReviewDismissals.Count}");
numUpdates += _gitHubReviewDismissals.Count;
}
if (_gitHubIssuesToLock.Count > 0)
{
Console.WriteLine($"Number of Issues to Lock {_gitHubIssuesToLock.Count}");
numUpdates += _gitHubIssuesToLock.Count;
}
if (_gitHubIssuesToUpdate.Count > 0)
{
Console.WriteLine($"Number of IssuesUpdates (only applicable for Scheduled events) {_gitHubIssuesToUpdate.Count}");
numUpdates += _gitHubIssuesToUpdate.Count;
}
if (_gitHubIssuesToLock.Count > 0)
{
Console.WriteLine($"Number of Issues to Lock {_gitHubIssuesToLock.Count}");
numUpdates += _gitHubIssuesToLock.Count;
}
return numUpdates;
}
@ -865,10 +867,11 @@ namespace Azure.Sdk.Tools.GitHubEventProcessor
/// <param name="repoName">Should be repository.Name from the cron payload</param>
/// <param name="issueType">IssueTypeQualifier of Issue or PullRequest</param>
/// <param name="itemState">ItemState of Open or Closed</param>
/// <param name="daysSinceLastUpdate">Optional: Number of days since last updated</param>
/// <param name="issueIsQualifiers">Optional: List of IssueIsQualifier (ex. locked/unlocked) to include, null if none</param>
/// <param name="labelsToInclude">Optional: List of labels to include, null if none</param>
/// <param name="labelsToExclude">Optional: List of labels to exclude, null if none</param>
/// <param name="daysSinceLastUpdate">Optional: Number of days since last updated </param>
/// <param name="daysSinceCreated">Optional: Number of days since the issue was created</param>
/// <returns>SearchIssuesRequest created with the information passed in.</returns>
public SearchIssuesRequest CreateSearchRequest(string repoOwner,
string repoName,
@ -877,7 +880,8 @@ namespace Azure.Sdk.Tools.GitHubEventProcessor
int daysSinceLastUpdate = 0,
List<IssueIsQualifier> issueIsQualifiers = null,
List<string> labelsToInclude = null,
List<string> labelsToExclude = null)
List<string> labelsToExclude = null,
int daysSinceCreated = 0)
{
var request = new SearchIssuesRequest();
@ -904,6 +908,15 @@ namespace Azure.Sdk.Tools.GitHubEventProcessor
request.Updated = new DateRange(daysAgoOffset, SearchQualifierOperator.LessThan);
}
if (daysSinceCreated > 0)
{
// Octokit's DateRange wants a DateTimeOffset as other constructors are depricated
// AddDays of 0-days to effectively subtract them.
DateTime daysAgo = DateTime.UtcNow.AddDays(0 - daysSinceCreated);
DateTimeOffset daysAgoOffset = new DateTimeOffset(daysAgo);
request.Created = new DateRange(daysAgoOffset, SearchQualifierOperator.LessThan);
}
if (null != labelsToInclude)
{
request.Labels = labelsToInclude;

Просмотреть файл

@ -617,3 +617,22 @@ OR
### Actions
- Lock issue conversations
## Enforce max life of issues
### Trigger
- CRON (weekly, Monday at 10am)
### Criteria
- Issue is open
- Issue was opened > 2 years ago
- Issue was last updated more than 30 days ago
### Actions
- Close the issue
- Create the following comment
- "Hi @{issueAuthor}, we deeply appreciate your input into this project. Regrettably, this issue has remained inactive for over 2 years, leading us to the decision to close it. We've implemented this policy to maintain the relevance of our issue queue and facilitate easier navigation for new contributors. If you still believe this topic requires attention, please feel free to create a new issue, referencing this one. Thank you for your understanding and ongoing support."
- Lock issue conversations

Просмотреть файл

@ -21,5 +21,6 @@
"IdentifyStaleIssues": "On",
"IdentifyStalePullRequests": "On",
"CloseAddressedIssues": "On",
"LockClosedIssues": "On"
"LockClosedIssues": "On",
"EnforceMaxLifeOfIssues": "On"
}

Просмотреть файл

@ -11,8 +11,6 @@ on:
# pull request merged is the closed event with github.event.pull_request.merged = true
pull_request_target:
types: [closed, labeled, opened, reopened, review_requested, synchronize, unlabeled]
pull_request_review:
types: [submitted]
# This removes all unnecessary permissions, the ones needed will be set below.
# https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token
@ -29,8 +27,6 @@ jobs:
name: Handle ${{ github.event_name }} ${{ github.event.action }} event
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: 'Az CLI login'
if: ${{ github.event_name == 'issues' && github.event.action == 'opened' }}
uses: azure/login@v1
@ -59,10 +55,9 @@ jobs:
run: >
dotnet tool install
Azure.Sdk.Tools.GitHubEventProcessor
--version 1.0.0-dev.20230929.3
--version 1.0.0-dev.20231114.3
--add-source https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-net/nuget/v3/index.json
--global
working-directory: .github/workflows
shell: bash
# End-Install
@ -89,11 +84,12 @@ jobs:
- name: Process Action Event
run: |
echo $GITHUB_PAYLOAD > payload.json
cat > payload.json << 'EOF'
${{ toJson(github.event) }}
EOF
github-event-processor ${{ github.event_name }} payload.json
shell: bash
env:
GITHUB_PAYLOAD: ${{ toJson(github.event) }}
# This is a temporary secret generated by github
# https://docs.github.com/en/actions/security-guides/automatic-token-authentication#about-the-github_token-secret
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Просмотреть файл

@ -14,6 +14,8 @@ on:
- cron: '30 4,10,16,22 * * *'
# Lock closed issues, every 6 hours at 05:30 AM, 11:30 AM, 05:30 PM and 11:30 PM - LockClosedIssues
- cron: '30 5,11,17,23 * * *'
# Enforce max life of issues, every Monday at 10:00 AM - EnforceMaxLifeOfIssues
- cron: '0 10 * * MON'
# This removes all unnecessary permissions, the ones needed will be set below.
# https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token
permissions: {}
@ -123,3 +125,13 @@ jobs:
env:
GITHUB_PAYLOAD: ${{ toJson(github.event) }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Enforce Max Life of Issues Scheduled Event
if: github.event.schedule == '0 10 * * MON'
run: |
echo $GITHUB_PAYLOAD > payload.json
github-event-processor ${{ github.event_name }} payload.json EnforceMaxLifeOfIssues
shell: bash
env:
GITHUB_PAYLOAD: ${{ toJson(github.event) }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}