Skip to content

Commit 17cdbd7

Browse files
authored
feat: add 'GetLatest' method to 'IPipelineClient' (#922)
* feat: add 'GetLatest' method to 'IPipelineClient' - Resolves #920 * Add unit tests * Failing pipeline * fix: update GetLatestAsync method to use correct pipelines path * test: add unit test for handling forbidden response on non-existing pipeline ref * fix: update comment to clarify behavior for invalid refs in GetLatestAsync method
1 parent cf1f242 commit 17cdbd7

File tree

10 files changed

+113
-0
lines changed

10 files changed

+113
-0
lines changed

NGitLab.Mock/Clients/PipelineClient.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,22 @@ public IEnumerable<PipelineBasic> All
5151

5252
public IEnumerable<Models.Job> AllJobs => _jobClient.GetJobs(JobScopeMask.All);
5353

54+
public Task<Models.Pipeline> GetLatestAsync(string @ref, CancellationToken cancellationToken = default)
55+
{
56+
using (Context.BeginOperationScope())
57+
{
58+
var project = GetProject(_projectId, ProjectPermission.View);
59+
var pipeline = project.Pipelines.GetLatest(@ref);
60+
if (pipeline != null)
61+
{
62+
return Task.FromResult(pipeline.ToPipelineClient());
63+
}
64+
65+
// GitLab returns 403 Forbidden if the ref is invalid, so we mimic that behavior here
66+
throw GitLabException.Forbidden();
67+
}
68+
}
69+
5470
public Models.Pipeline Create(string @ref)
5571
{
5672
using (Context.BeginOperationScope())

NGitLab.Mock/PipelineCollection.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@ public Pipeline GetById(long id)
1919
return this.FirstOrDefault(pipeline => pipeline.Id == id);
2020
}
2121

22+
public Pipeline GetLatest(string @ref)
23+
{
24+
var branchName = string.IsNullOrWhiteSpace(@ref) ? _project.DefaultBranch : @ref;
25+
26+
return this
27+
.Where(pipeline => string.Equals(pipeline.Ref, branchName, StringComparison.OrdinalIgnoreCase))
28+
.OrderByDescending(pipeline => pipeline.CreatedAt)
29+
.FirstOrDefault();
30+
}
31+
2232
public override void Add(Pipeline pipeline)
2333
{
2434
if (pipeline is null)

NGitLab.Mock/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -837,6 +837,7 @@ NGitLab.Mock.Pipeline.YamlError.set -> void
837837
NGitLab.Mock.PipelineCollection
838838
NGitLab.Mock.PipelineCollection.Add(string ref, NGitLab.JobStatus status, NGitLab.Mock.User user) -> NGitLab.Mock.Pipeline
839839
NGitLab.Mock.PipelineCollection.GetById(long id) -> NGitLab.Mock.Pipeline
840+
NGitLab.Mock.PipelineCollection.GetLatest(string ref) -> NGitLab.Mock.Pipeline
840841
NGitLab.Mock.PipelineCollection.PipelineCollection(NGitLab.Mock.GitLabObject parent) -> void
841842
NGitLab.Mock.PipelineSchedule
842843
NGitLab.Mock.PipelineSchedule.Active.get -> bool

NGitLab.Tests/Docker/GitLabTestContext.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,24 @@ public static async Task<T> RetryUntilAsync<T>(Func<T> action, Func<T, bool> pre
465465
return await RetryUntilAsync(action, predicate, cts.Token).ConfigureAwait(false);
466466
}
467467

468+
public static async Task<T> RetryUntilAsync<T>(Func<Task<T>> action, Func<T, Task<bool>> predicate, TimeSpan timeSpan)
469+
{
470+
using var cts = new CancellationTokenSource(timeSpan);
471+
return await RetryUntilAsync(action, predicate, cts.Token).ConfigureAwait(false);
472+
}
473+
474+
public static async Task<T> RetryUntilAsync<T>(Func<Task<T>> action, Func<T, Task<bool>> predicate, CancellationToken cancellationToken)
475+
{
476+
while (true)
477+
{
478+
cancellationToken.ThrowIfCancellationRequested();
479+
var result = await action().ConfigureAwait(false);
480+
if (await predicate(result).ConfigureAwait(false))
481+
return result;
482+
await Task.Delay(1000, cancellationToken).ConfigureAwait(false);
483+
}
484+
}
485+
468486
public static async Task<T> RetryUntilAsync<T>(Func<T> action, Func<T, bool> predicate, CancellationToken cancellationToken)
469487
{
470488
var retryCount = 1;

NGitLab.Tests/PipelineTests.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,52 @@ public async Task Test_can_list_the_pipelines()
3131
}
3232
}
3333

34+
[Test]
35+
[NGitLabRetry]
36+
public async Task Test_can_get_latest_pipeline()
37+
{
38+
using var context = await GitLabTestContext.CreateAsync();
39+
var project = context.CreateProject();
40+
var pipelineClient = context.Client.GetPipelines(project.Id);
41+
JobTests.AddGitLabCiFile(context.Client, project);
42+
Pipeline latestPipeline;
43+
using (await context.StartRunnerForOneJobAsync(project.Id))
44+
{
45+
latestPipeline = await GitLabTestContext.RetryUntilAsync(
46+
() => pipelineClient.GetLatestAsync(project.DefaultBranch),
47+
p => Task.FromResult(p != null),
48+
TimeSpan.FromSeconds(120));
49+
}
50+
51+
Assert.That(latestPipeline, Is.Not.Null);
52+
Assert.That(latestPipeline.ProjectId, Is.EqualTo(project.Id));
53+
}
54+
55+
[Test]
56+
[NGitLabRetry]
57+
public async Task Test_get_latest_pipeline_for_non_existing_ref_returns_forbidden()
58+
{
59+
using var context = await GitLabTestContext.CreateAsync();
60+
var project = context.CreateProject();
61+
var pipelineClient = context.Client.GetPipelines(project.Id);
62+
JobTests.AddGitLabCiFile(context.Client, project);
63+
64+
const string nonExistingRef = "non-existing-branch";
65+
66+
GitLabException exception = null;
67+
try
68+
{
69+
await pipelineClient.GetLatestAsync(nonExistingRef);
70+
}
71+
catch (GitLabException ex) when (ex.StatusCode == HttpStatusCode.Forbidden)
72+
{
73+
exception = ex;
74+
}
75+
76+
Assert.That(exception, Is.Not.Null, "Expected a Forbidden exception when retrieving the latest pipeline for a non-existing ref.");
77+
Assert.That(exception.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden));
78+
}
79+
3480
[Test]
3581
[NGitLabRetry]
3682
public async Task Test_can_get_coverage()

NGitLab/IPipelineClient.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ public interface IPipelineClient
2626
/// </summary>
2727
IEnumerable<Job> AllJobs { get; }
2828

29+
/// <summary>
30+
/// Retrieves the latest pipeline associated with the specified reference.
31+
/// </summary>
32+
/// <remarks>Use this method to retrieve the most recent pipeline for a given branch or tag
33+
/// tag.</remarks>
34+
/// <param name="ref">Optional reference (e.g., branch name or tag) for which to retrieve the latest pipeline.
35+
/// Defaults to the default branch when not specified.</param>
36+
/// <returns>The task result contains the latest <see cref="Pipeline"/> associated with the specified reference.</returns>
37+
Task<Pipeline> GetLatestAsync(string @ref, CancellationToken cancellationToken = default);
38+
2939
GitLabCollectionResponse<Job> GetAllJobsAsync();
3040

3141
/// <summary>

NGitLab/Impl/PipelineClient.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ public PipelineClient(API api, ProjectId projectId)
2626

2727
public IEnumerable<Job> AllJobs => _api.Get().GetAll<Job>($"{_projectPath}/jobs");
2828

29+
public Task<Pipeline> GetLatestAsync(string @ref, CancellationToken cancellationToken = default)
30+
{
31+
var urlParameter = string.IsNullOrWhiteSpace(@ref) ? string.Empty : $"?ref={Uri.EscapeDataString(@ref)}";
32+
return _api.Get().ToAsync<Pipeline>($"{_pipelinesPath}/latest{urlParameter}", cancellationToken);
33+
}
34+
2935
public GitLabCollectionResponse<Job> GetAllJobsAsync()
3036
{
3137
return _api.Get().GetAllAsync<Job>($"{_projectPath}/jobs");

NGitLab/PublicAPI/net472/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,7 @@ NGitLab.Impl.PipelineClient.GetByIdAsync(long id, System.Threading.CancellationT
816816
NGitLab.Impl.PipelineClient.GetJobs(long pipelineId) -> NGitLab.Models.Job[]
817817
NGitLab.Impl.PipelineClient.GetJobs(NGitLab.Models.PipelineJobQuery query) -> System.Collections.Generic.IEnumerable<NGitLab.Models.Job>
818818
NGitLab.Impl.PipelineClient.GetJobsAsync(NGitLab.Models.PipelineJobQuery query) -> NGitLab.GitLabCollectionResponse<NGitLab.Models.Job>
819+
NGitLab.Impl.PipelineClient.GetLatestAsync(string ref, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<NGitLab.Models.Pipeline>
819820
NGitLab.Impl.PipelineClient.GetTestReports(long pipelineId) -> NGitLab.Models.TestReport
820821
NGitLab.Impl.PipelineClient.GetTestReportsSummary(long pipelineId) -> NGitLab.Models.TestReportSummary
821822
NGitLab.Impl.PipelineClient.GetVariables(long pipelineId) -> System.Collections.Generic.IEnumerable<NGitLab.Models.PipelineVariable>
@@ -1013,6 +1014,7 @@ NGitLab.IPipelineClient.GetByIdAsync(long id, System.Threading.CancellationToken
10131014
NGitLab.IPipelineClient.GetJobs(long pipelineId) -> NGitLab.Models.Job[]
10141015
NGitLab.IPipelineClient.GetJobs(NGitLab.Models.PipelineJobQuery query) -> System.Collections.Generic.IEnumerable<NGitLab.Models.Job>
10151016
NGitLab.IPipelineClient.GetJobsAsync(NGitLab.Models.PipelineJobQuery query) -> NGitLab.GitLabCollectionResponse<NGitLab.Models.Job>
1017+
NGitLab.IPipelineClient.GetLatestAsync(string ref, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<NGitLab.Models.Pipeline>
10161018
NGitLab.IPipelineClient.GetTestReports(long pipelineId) -> NGitLab.Models.TestReport
10171019
NGitLab.IPipelineClient.GetTestReportsSummary(long pipelineId) -> NGitLab.Models.TestReportSummary
10181020
NGitLab.IPipelineClient.GetVariables(long pipelineId) -> System.Collections.Generic.IEnumerable<NGitLab.Models.PipelineVariable>

NGitLab/PublicAPI/net8.0/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,6 +815,7 @@ NGitLab.Impl.PipelineClient.GetByIdAsync(long id, System.Threading.CancellationT
815815
NGitLab.Impl.PipelineClient.GetJobs(long pipelineId) -> NGitLab.Models.Job[]
816816
NGitLab.Impl.PipelineClient.GetJobs(NGitLab.Models.PipelineJobQuery query) -> System.Collections.Generic.IEnumerable<NGitLab.Models.Job>
817817
NGitLab.Impl.PipelineClient.GetJobsAsync(NGitLab.Models.PipelineJobQuery query) -> NGitLab.GitLabCollectionResponse<NGitLab.Models.Job>
818+
NGitLab.Impl.PipelineClient.GetLatestAsync(string ref, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<NGitLab.Models.Pipeline>
818819
NGitLab.Impl.PipelineClient.GetTestReports(long pipelineId) -> NGitLab.Models.TestReport
819820
NGitLab.Impl.PipelineClient.GetTestReportsSummary(long pipelineId) -> NGitLab.Models.TestReportSummary
820821
NGitLab.Impl.PipelineClient.GetVariables(long pipelineId) -> System.Collections.Generic.IEnumerable<NGitLab.Models.PipelineVariable>
@@ -1012,6 +1013,7 @@ NGitLab.IPipelineClient.GetByIdAsync(long id, System.Threading.CancellationToken
10121013
NGitLab.IPipelineClient.GetJobs(long pipelineId) -> NGitLab.Models.Job[]
10131014
NGitLab.IPipelineClient.GetJobs(NGitLab.Models.PipelineJobQuery query) -> System.Collections.Generic.IEnumerable<NGitLab.Models.Job>
10141015
NGitLab.IPipelineClient.GetJobsAsync(NGitLab.Models.PipelineJobQuery query) -> NGitLab.GitLabCollectionResponse<NGitLab.Models.Job>
1016+
NGitLab.IPipelineClient.GetLatestAsync(string ref, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<NGitLab.Models.Pipeline>
10151017
NGitLab.IPipelineClient.GetTestReports(long pipelineId) -> NGitLab.Models.TestReport
10161018
NGitLab.IPipelineClient.GetTestReportsSummary(long pipelineId) -> NGitLab.Models.TestReportSummary
10171019
NGitLab.IPipelineClient.GetVariables(long pipelineId) -> System.Collections.Generic.IEnumerable<NGitLab.Models.PipelineVariable>

NGitLab/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,7 @@ NGitLab.Impl.PipelineClient.GetByIdAsync(long id, System.Threading.CancellationT
816816
NGitLab.Impl.PipelineClient.GetJobs(long pipelineId) -> NGitLab.Models.Job[]
817817
NGitLab.Impl.PipelineClient.GetJobs(NGitLab.Models.PipelineJobQuery query) -> System.Collections.Generic.IEnumerable<NGitLab.Models.Job>
818818
NGitLab.Impl.PipelineClient.GetJobsAsync(NGitLab.Models.PipelineJobQuery query) -> NGitLab.GitLabCollectionResponse<NGitLab.Models.Job>
819+
NGitLab.Impl.PipelineClient.GetLatestAsync(string ref, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<NGitLab.Models.Pipeline>
819820
NGitLab.Impl.PipelineClient.GetTestReports(long pipelineId) -> NGitLab.Models.TestReport
820821
NGitLab.Impl.PipelineClient.GetTestReportsSummary(long pipelineId) -> NGitLab.Models.TestReportSummary
821822
NGitLab.Impl.PipelineClient.GetVariables(long pipelineId) -> System.Collections.Generic.IEnumerable<NGitLab.Models.PipelineVariable>
@@ -1013,6 +1014,7 @@ NGitLab.IPipelineClient.GetByIdAsync(long id, System.Threading.CancellationToken
10131014
NGitLab.IPipelineClient.GetJobs(long pipelineId) -> NGitLab.Models.Job[]
10141015
NGitLab.IPipelineClient.GetJobs(NGitLab.Models.PipelineJobQuery query) -> System.Collections.Generic.IEnumerable<NGitLab.Models.Job>
10151016
NGitLab.IPipelineClient.GetJobsAsync(NGitLab.Models.PipelineJobQuery query) -> NGitLab.GitLabCollectionResponse<NGitLab.Models.Job>
1017+
NGitLab.IPipelineClient.GetLatestAsync(string ref, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<NGitLab.Models.Pipeline>
10161018
NGitLab.IPipelineClient.GetTestReports(long pipelineId) -> NGitLab.Models.TestReport
10171019
NGitLab.IPipelineClient.GetTestReportsSummary(long pipelineId) -> NGitLab.Models.TestReportSummary
10181020
NGitLab.IPipelineClient.GetVariables(long pipelineId) -> System.Collections.Generic.IEnumerable<NGitLab.Models.PipelineVariable>

0 commit comments

Comments
 (0)