예제 #1
0
        /// <summary>
        /// <para>
        /// Deletes workflow runs from a GitHub repo.
        /// </para>
        /// <note>
        /// Only completed runs will be deleted.
        /// </note>
        /// </summary>
        /// <param name="repo">Identifies the target repository.</param>
        /// <param name="workflowName">
        /// Optionally specifies the workflow whose runs are to be deleted otherwise
        /// runs from all workflows in the repo will be deleted.
        /// </param>
        /// <param name="maxAge">
        /// Optionally specifies the maximum age for retained workflow runs.  This
        /// defaults to <see cref="TimeSpan.Zero"/> which deletes all runs.
        /// </param>
        /// <returns>The number of runs deleted.</returns>
        public async Task <int> DeleteRunsAsync(string repo, string workflowName = null, TimeSpan maxAge = default)
        {
            await SyncContext.Clear;

            GitHub.GetCredentials();

            var repoPath    = GitHubRepoPath.Parse(repo);
            var deleteCount = 0;

            using (var client = new HttpClient())
            {
                var retry = new ExponentialRetryPolicy(TransientDetector.NetworkOrHttp, 5);

                client.BaseAddress = new Uri("https://api.github.com");
                client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", GitHub.AccessToken);

                client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("neonforge.com", "0"));
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github.v3+json"));

                // List all of the workflow runs for the repo, paging to get all of them.
                //
                //      https://docs.github.com/en/rest/reference/actions#list-workflow-runs-for-a-repository

                var runs = new List <RunInfo>();
                var page = 1;

                while (true)
                {
                    var response = await retry.InvokeAsync(
                        async() =>
                    {
                        var request = new HttpRequestMessage(HttpMethod.Get, $"/repos/{repoPath.Owner}/{repoPath.Repo}/actions/runs?page={page}");

                        // We're seeing some 502 Bad Gateway responses from GHCR.io.  We're going to
                        // treat these as transients.

                        var response = await client.SendAsync(request);

                        if (response.StatusCode == HttpStatusCode.BadGateway)
                        {
                            throw new TransientException("503 (Bad Gateway)");
                        }

                        return(response);
                    });

                    response.EnsureSuccessStatusCode();

                    var json   = response.Content.ReadAsStringAsync().Result;
                    var result = JsonConvert.DeserializeObject <dynamic>(json);

                    var workflowRuns = result.workflow_runs;

                    if (workflowRuns.Count == 0)
                    {
                        // We've seen all of the runs.

                        break;
                    }

                    foreach (var run in workflowRuns)
                    {
                        runs.Add(
                            new RunInfo()
                        {
                            Id           = run.id,
                            Name         = run.name,
                            Status       = run.status,
                            UpdatedAtUtc = run.updated_at
                        });
                    }

                    page++;
                }

                // Here's the reference for deleting runs:
                //
                //      https://docs.github.com/en/rest/reference/actions#delete-a-workflow-run

                var minDate      = DateTime.UtcNow - maxAge;
                var selectedRuns = runs.Where(run => run.UpdatedAtUtc < minDate && run.Status == "completed");

                if (!string.IsNullOrEmpty(workflowName))
                {
                    selectedRuns = selectedRuns.Where(run => run.Name.Equals(workflowName, StringComparison.InvariantCultureIgnoreCase));
                }

                foreach (var run in selectedRuns)
                {
                    var response = await retry.InvokeAsync(
                        async() =>
                    {
                        var request = new HttpRequestMessage(HttpMethod.Delete, $"/repos/{repoPath.Owner}/{repoPath.Repo}/actions/runs/{run.Id}");

                        return(await client.SendAsync(request));
                    });

                    // We're also seeing some 500s but I'm not sure why.  We'll ignore these
                    // for now.

                    if (response.StatusCode == HttpStatusCode.InternalServerError)
                    {
                        Task.Delay(TimeSpan.FromSeconds(2)).WaitWithoutAggregate();     // Pause in case this is a rate-limit thing
                        continue;
                    }

                    // We're seeing 403s for some runs, so we'll ignore those too.

                    if (response.StatusCode != HttpStatusCode.Forbidden)
                    {
                        response.EnsureSuccessStatusCode();
                    }

                    deleteCount++;
                }
            }

            return(deleteCount);
        }