Exemple #1
0
        /// <summary>
        /// Uploads an asset file to a GitHub release.  Any existing asset with same name will be replaced.
        /// </summary>
        /// <param name="repo">Identifies the target repository.</param>
        /// <param name="release">The target release.</param>
        /// <param name="assetPath">Path to the source asset file.</param>
        /// <param name="assetName">Optionally specifies the file name to assign to the asset.  This defaults to the file name in <paramref name="assetPath"/>.</param>
        /// <param name="contentType">Optionally specifies the asset's <b>Content-Type</b>.  This defaults to: <b> application/octet-stream</b></param>
        /// <returns>The new <see cref="ReleaseAsset"/>.</returns>
        /// <exception cref="NotSupportedException">Thrown when the releas has already been published.</exception>
        /// <remarks>
        /// <note>
        /// The current implementation only works for unpublished releases where <c>Draft=true</c>.
        /// </note>
        /// </remarks>
        public ReleaseAsset UploadAsset(string repo, Release release, string assetPath, string assetName = null, string contentType = "application/octet-stream")
        {
            Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(repo), nameof(repo));
            Covenant.Requires <ArgumentNullException>(release != null, nameof(release));
            Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(assetPath), nameof(assetPath));

            if (!release.Draft)
            {
                throw new NotSupportedException("Cannot upload asset to already published release.");
            }

            var repoPath = GitHubRepoPath.Parse(repo);
            var client   = GitHub.CreateGitHubClient(repo);

            using (var assetStream = File.OpenRead(assetPath))
            {
                if (string.IsNullOrEmpty(assetName))
                {
                    assetName = Path.GetFileName(assetPath);
                }

                var upload = new ReleaseAssetUpload()
                {
                    FileName    = assetName,
                    ContentType = contentType,
                    RawData     = assetStream
                };

                return(client.Repository.Release.UploadAsset(release, upload).Result);
            }
        }
Exemple #2
0
        /// <summary>
        /// Creates a GitHub release.
        /// </summary>
        /// <param name="repo">Identifies the target repo.</param>
        /// <param name="tagName">Specifies the tag to be referenced by the release.</param>
        /// <param name="releaseName">Optionally specifies the release name (defaults to <paramref name="tagName"/>).</param>
        /// <param name="body">Optionally specifies the markdown formatted release notes.</param>
        /// <param name="draft">Optionally indicates that the release won't be published immediately.</param>
        /// <param name="prerelease">Optionally indicates that the release is not production ready.</param>
        /// <param name="branch">Optionally identifies the branch to be tagged.  This defaults to <b>master</b> or <b>main</b> when either of those branches are already present.</param>
        /// <returns>The newly created <see cref="Release"/>.</returns>
        /// <remarks>
        /// <para>
        /// If the <paramref name="tagName"/> doesn't already exist in the repo, this method will
        /// tag the latest commit on the specified <paramref name="branch"/> or else the defailt branch
        /// in the target repo and before creating the release.
        /// </para>
        /// </remarks>
        public Release Create(string repo, string tagName, string releaseName = null, string body = null, bool draft = false, bool prerelease = false, string branch = null)
        {
            Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(repo), nameof(repo));
            Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(tagName), nameof(tagName));

            releaseName = releaseName ?? tagName;

            var repoPath = GitHubRepoPath.Parse(repo);
            var client   = GitHub.CreateGitHubClient(repo);
            var tags     = client.Repository.GetAllTags(repoPath.Owner, repoPath.Repo).Result;
            var tag      = tags.SingleOrDefault(tag => tag.Name == tagName);

            // Tag the specified or default branch when the tag doesn't already exist.
            // Note that we may need to

            if (tag == null)
            {
                if (string.IsNullOrEmpty(branch))
                {
                    // Identify the default branch.

                    var branches = client.Repository.Branch.GetAll(repoPath.Owner, repoPath.Repo).Result;

                    foreach (var branchDetails in branches)
                    {
                        if (branchDetails.Name == "master")
                        {
                            branch = "master";
                            break;
                        }
                        else if (branchDetails.Name == "main")
                        {
                            branch = "main";
                            break;
                        }
                    }

                    var newTag = new NewTag()
                    {
                        Message = $"release-tag: {tagName}",
                        Tag     = tagName,
                        Object  = "",
                    };

                    client.Git.Tag.Create(repoPath.Owner, repoPath.Repo, newTag);
                }
            }

            // Create the release.

            var release = new NewRelease(tagName)
            {
                Name       = releaseName,
                Draft      = draft,
                Prerelease = prerelease,
                Body       = body
            };

            return(client.Repository.Release.Create(repoPath.Owner, repoPath.Repo, release).Result);
        }
Exemple #3
0
        /// <summary>
        /// List the releases for a GitHub repo.
        /// </summary>
        /// <returns>The list of releases.</returns>
        public IReadOnlyList <Release> List(string repo)
        {
            Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(repo), nameof(repo));

            var repoPath = GitHubRepoPath.Parse(repo);
            var client   = GitHub.CreateGitHubClient(repo);

            return(client.Repository.Release.GetAll(repoPath.Owner, repoPath.Repo).Result);
        }
Exemple #4
0
        /// <summary>
        /// Deletes a GitHub release.
        /// </summary>
        /// <param name="repo">Identifies the target repository.</param>
        /// <param name="release">The target release.</param>
        /// <remarks>
        /// <note>
        /// This fails silently if the release doesn't exist.
        /// </note>
        /// </remarks>
        public void Remove(string repo, Release release)
        {
            Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(repo), nameof(repo));
            Covenant.Requires <ArgumentNullException>(release != null, nameof(release));

            var repoPath = GitHubRepoPath.Parse(repo);
            var client   = GitHub.CreateGitHubClient(repo);

            client.Repository.Release.Delete(repoPath.Owner, repoPath.Repo, release.Id).WaitWithoutAggregate();
        }
Exemple #5
0
        /// <summary>
        /// Returns the releases that satisfies a predicate.
        /// </summary>
        /// <param name="repo">Identifies the target repository.</param>
        /// <param name="predicate">The predicate.</param>
        /// <returns>The list of matching releases.</returns>
        public List <Release> Find(string repo, Func <Release, bool> predicate)
        {
            Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(repo), nameof(repo));
            Covenant.Requires <ArgumentNullException>(predicate != null, nameof(predicate));

            var repoPath = GitHubRepoPath.Parse(repo);
            var client   = GitHub.CreateGitHubClient(repo);

            return(List(repo).Where(predicate).ToList());
        }
Exemple #6
0
        /// <summary>
        /// Creates a REST client that can be used to manage GitHub.
        /// </summary>
        /// <param name="repo">Identifies the target repo.</param>
        /// <returns>The <see cref="GitHubClient"/> instance.</returns>
        internal static GitHubClient CreateGitHubClient(string repo)
        {
            GitHub.GetCredentials();

            var repoPath = GitHubRepoPath.Parse(repo);
            var client   = new GitHubClient(new Octokit.ProductHeaderValue("neonkube.com"));  // $todo(jefflill): https://github.com/nforgeio/neonKUBE/issues/1214

            client.Credentials = new Octokit.Credentials(AccessToken);

            return(client);
        }
Exemple #7
0
        /// <summary>
        /// Updates a GitHub release.
        /// </summary>
        /// <param name="repo">Identifies the target repository.</param>
        /// <param name="release">Specifies the release being updated.</param>
        /// <param name="releaseUpdate">Specifies the revisions.</param>
        /// <returns>The updated release.</returns>
        /// <remarks>
        /// <para>
        /// To update a release, you'll first need to:
        /// </para>
        /// <list type="number">
        /// <item>
        /// Obtain a <see cref="Release"/> referencing the target release returned from
        /// <see cref="Create(string, string, string, string, bool, bool, string)"/>
        /// or by listing or getting releases.
        /// </item>
        /// <item>
        /// Obtain a <see cref="ReleaseUpdate"/> by calling <see cref="Release.ToUpdate"/>.
        /// </item>
        /// <item>
        /// Make your changes to the release update.
        /// </item>
        /// <item>
        /// Call <see cref="Update(string, Release, ReleaseUpdate)"/>, passing the
        /// original release along with the update.
        /// </item>
        /// </list>
        /// </remarks>
        public Release Update(string repo, Release release, ReleaseUpdate releaseUpdate)
        {
            Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(repo), nameof(repo));
            Covenant.Requires <ArgumentNullException>(release != null, nameof(release));
            Covenant.Requires <ArgumentNullException>(releaseUpdate != null, nameof(releaseUpdate));

            var repoPath = GitHubRepoPath.Parse(repo);
            var client   = GitHub.CreateGitHubClient(repo);

            return(client.Repository.Release.Edit(repoPath.Owner, repoPath.Repo, release.Id, releaseUpdate).Result);
        }
Exemple #8
0
        /// <summary>
        /// Uploads an asset stream to a GitHub release.  Any existing asset with same name will be replaced.
        /// </summary>
        /// <param name="repo">Identifies the target repository.</param>
        /// <param name="release">The target release.</param>
        /// <param name="assetStream">The asset source stream.</param>
        /// <param name="assetName">Specifies the file name to assign to the asset.</param>
        /// <param name="contentType">Optionally specifies the asset's <b>Content-Type</b>.  This defaults to: <b> application/octet-stream</b></param>
        /// <returns>The new <see cref="ReleaseAsset"/>.</returns>
        public ReleaseAsset UploadAsset(string repo, Release release, Stream assetStream, string assetName, string contentType = "application/octet-stream")
        {
            Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(repo), nameof(repo));
            Covenant.Requires <ArgumentNullException>(release != null, nameof(release));
            Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(assetName), nameof(assetName));
            Covenant.Requires <ArgumentNullException>(assetStream != null, nameof(assetStream));

            var repoPath = GitHubRepoPath.Parse(repo);
            var client   = GitHub.CreateGitHubClient(repo);

            var upload = new ReleaseAssetUpload()
            {
                FileName    = assetName,
                ContentType = contentType,
                RawData     = assetStream
            };

            return(client.Repository.Release.UploadAsset(release, upload).Result);
        }
Exemple #9
0
        //---------------------------------------------------------------------
        // Static members

        /// <summary>
        /// Parses a GitHub repository path.
        /// </summary>
        /// <param name="path">The path, like: <b>[SERVER]/OWNER/REPO</b></param>
        /// <returns>The parsed <see cref="GitHubRepoPath"/>.</returns>
        /// <exception cref="FormatException">Thrown when the input is invalid.</exception>
        /// <remarks>
        /// <note>
        /// <b>github.com</b> will be assumed when no server is specified.
        /// </note>
        /// </remarks>
        public static GitHubRepoPath Parse(string path)
        {
            Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(path), nameof(path));

            var parts = path.Split('/');

            foreach (var part in parts)
            {
                if (part.Length == 0 || part.Contains(' '))
                {
                    throw new FormatException($"Invalid GitHub repo path: {path}");
                }
            }

            var repoPath = new GitHubRepoPath();

            switch (parts.Length)
            {
            case 2:

                repoPath.Server = "github.com";
                repoPath.Owner  = parts[0];
                repoPath.Repo   = parts[1];
                break;

            case 3:

                repoPath.Server = parts[0];
                repoPath.Owner  = parts[1];
                repoPath.Repo   = parts[2];
                break;

            default:

                throw new FormatException($"Invalid GitHub repo path: {path}");
            }

            return(repoPath);
        }
Exemple #10
0
        /// <summary>
        /// Retrieves a specific GitHub release.
        /// </summary>
        /// <param name="repo">Identifies the target repository.</param>
        /// <param name="tagName">Specifies the tag for the target release.</param>
        /// <returns>The release information or <c>null</c> when the requested release doesn't exist.</returns>
        public Release Get(string repo, string tagName)
        {
            Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(repo), nameof(repo));
            Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(tagName), nameof(tagName));

            var repoPath = GitHubRepoPath.Parse(repo);
            var client   = GitHub.CreateGitHubClient(repo);

            try
            {
                return(client.Repository.Release.Get(repoPath.Owner, repoPath.Repo, tagName).Result);
            }
            catch (Exception e)
            {
                if (e.Find <NotFoundException>() != null)
                {
                    return(null);
                }

                throw;
            }
        }
Exemple #11
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);
        }