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:
Родитель
53199a0e90
Коммит
7d9603e4f8
|
@ -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 }}
|
||||
|
|
Загрузка…
Ссылка в новой задаче