diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 0956c1d4149..4a044fb215a 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,7 +4,7 @@ "features": { "ghcr.io/devcontainers/features/docker-in-docker:1": {}, "ghcr.io/devcontainers/features/dotnet": { - "version": "8.0.408" + "version": "8.0.410" }, "ghcr.io/devcontainers/features/node:1": { "version": "20" diff --git a/.github/workflows/docker-buildx-upgrade.yml b/.github/workflows/docker-buildx-upgrade.yml new file mode 100644 index 00000000000..afc8bb05ff7 --- /dev/null +++ b/.github/workflows/docker-buildx-upgrade.yml @@ -0,0 +1,144 @@ +name: "Docker/Buildx Version Upgrade" + +on: + schedule: + - cron: '0 0 * * 1' # Run every Monday at midnight + workflow_dispatch: # Allow manual triggering + +jobs: + check-versions: + runs-on: ubuntu-latest + outputs: + DOCKER_SHOULD_UPDATE: ${{ steps.check_docker_version.outputs.SHOULD_UPDATE }} + DOCKER_LATEST_VERSION: ${{ steps.check_docker_version.outputs.LATEST_VERSION }} + DOCKER_CURRENT_VERSION: ${{ steps.check_docker_version.outputs.CURRENT_VERSION }} + BUILDX_SHOULD_UPDATE: ${{ steps.check_buildx_version.outputs.SHOULD_UPDATE }} + BUILDX_LATEST_VERSION: ${{ steps.check_buildx_version.outputs.LATEST_VERSION }} + BUILDX_CURRENT_VERSION: ${{ steps.check_buildx_version.outputs.CURRENT_VERSION }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Check Docker version + id: check_docker_version + shell: bash + run: | + # Extract current Docker version from Dockerfile + current_version=$(grep "ARG DOCKER_VERSION=" ./images/Dockerfile | cut -d'=' -f2) + + # Fetch latest Docker Engine version from Docker's download site + # This gets the latest Linux static binary version which matches what's used in the Dockerfile + latest_version=$(curl -s https://download.docker.com/linux/static/stable/x86_64/ | grep -o 'docker-[0-9]*\.[0-9]*\.[0-9]*\.tgz' | sort -V | tail -n 1 | sed 's/docker-\(.*\)\.tgz/\1/') + + # Extra check to ensure we got a valid version + if [[ ! $latest_version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Failed to retrieve a valid Docker version" + exit 1 + fi + + should_update=0 + [ "$current_version" != "$latest_version" ] && should_update=1 + + echo "CURRENT_VERSION=${current_version}" >> $GITHUB_OUTPUT + echo "LATEST_VERSION=${latest_version}" >> $GITHUB_OUTPUT + echo "SHOULD_UPDATE=${should_update}" >> $GITHUB_OUTPUT + + - name: Check Buildx version + id: check_buildx_version + shell: bash + run: | + # Extract current Buildx version from Dockerfile + current_version=$(grep "ARG BUILDX_VERSION=" ./images/Dockerfile | cut -d'=' -f2) + + # Fetch latest Buildx version + latest_version=$(curl -s https://api.github.com/repos/docker/buildx/releases/latest | jq -r '.tag_name' | sed 's/^v//') + + should_update=0 + [ "$current_version" != "$latest_version" ] && should_update=1 + + echo "CURRENT_VERSION=${current_version}" >> $GITHUB_OUTPUT + echo "LATEST_VERSION=${latest_version}" >> $GITHUB_OUTPUT + echo "SHOULD_UPDATE=${should_update}" >> $GITHUB_OUTPUT + + - name: Create annotations for versions + run: | + docker_should_update="${{ steps.check_docker_version.outputs.SHOULD_UPDATE }}" + buildx_should_update="${{ steps.check_buildx_version.outputs.SHOULD_UPDATE }}" + + # Show annotation if only Docker needs update + if [[ "$docker_should_update" == "1" && "$buildx_should_update" == "0" ]]; then + echo "::warning ::Docker version (${{ steps.check_docker_version.outputs.LATEST_VERSION }}) needs update but Buildx is current. Only updating when both need updates." + fi + + # Show annotation if only Buildx needs update + if [[ "$docker_should_update" == "0" && "$buildx_should_update" == "1" ]]; then + echo "::warning ::Buildx version (${{ steps.check_buildx_version.outputs.LATEST_VERSION }}) needs update but Docker is current. Only updating when both need updates." + fi + + # Show annotation when both are current + if [[ "$docker_should_update" == "0" && "$buildx_should_update" == "0" ]]; then + echo "::warning ::Latest Docker version is ${{ steps.check_docker_version.outputs.LATEST_VERSION }} and Buildx version is ${{ steps.check_buildx_version.outputs.LATEST_VERSION }}. No updates needed." + fi + + update-versions: + permissions: + pull-requests: write + contents: write + needs: [check-versions] + if: ${{ needs.check-versions.outputs.DOCKER_SHOULD_UPDATE == 1 && needs.check-versions.outputs.BUILDX_SHOULD_UPDATE == 1 }} + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Update Docker version + shell: bash + run: | + latest_version="${{ needs.check-versions.outputs.DOCKER_LATEST_VERSION }}" + current_version="${{ needs.check-versions.outputs.DOCKER_CURRENT_VERSION }}" + + # Update version in Dockerfile + sed -i "s/ARG DOCKER_VERSION=$current_version/ARG DOCKER_VERSION=$latest_version/g" ./images/Dockerfile + + - name: Update Buildx version + shell: bash + run: | + latest_version="${{ needs.check-versions.outputs.BUILDX_LATEST_VERSION }}" + current_version="${{ needs.check-versions.outputs.BUILDX_CURRENT_VERSION }}" + + # Update version in Dockerfile + sed -i "s/ARG BUILDX_VERSION=$current_version/ARG BUILDX_VERSION=$latest_version/g" ./images/Dockerfile + + - name: Commit changes and create Pull Request + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Setup branch and commit information + branch_name="feature/docker-buildx-upgrade" + commit_message="Upgrade Docker to v${{ needs.check-versions.outputs.DOCKER_LATEST_VERSION }} and Buildx to v${{ needs.check-versions.outputs.BUILDX_LATEST_VERSION }}" + pr_title="Update Docker to v${{ needs.check-versions.outputs.DOCKER_LATEST_VERSION }} and Buildx to v${{ needs.check-versions.outputs.BUILDX_LATEST_VERSION }}" + + # Configure git + git config --global user.name "github-actions[bot]" + git config --global user.email "<41898282+github-actions[bot]@users.noreply.github.com>" + + # Create branch or switch to it if it exists + if git show-ref --quiet refs/remotes/origin/$branch_name; then + git fetch origin + git checkout -B "$branch_name" origin/$branch_name + else + git checkout -b "$branch_name" + fi + + # Commit and push changes + git commit -a -m "$commit_message" + git push --force origin "$branch_name" + + # Create PR + pr_body="Upgrades Docker version from ${{ needs.check-versions.outputs.DOCKER_CURRENT_VERSION }} to ${{ needs.check-versions.outputs.DOCKER_LATEST_VERSION }} and Docker Buildx version from ${{ needs.check-versions.outputs.BUILDX_CURRENT_VERSION }} to ${{ needs.check-versions.outputs.BUILDX_LATEST_VERSION }}.\n\n" + pr_body+="Release notes: https://docs.docker.com/engine/release-notes/\n\n" + pr_body+="---\n\nAutogenerated by [Docker/Buildx Version Upgrade Workflow](https://github.com/actions/runner/blob/main/.github/workflows/docker-buildx-upgrade.yml)" + + gh pr create -B main -H "$branch_name" \ + --title "$pr_title" \ + --body "$pr_body" diff --git a/images/Dockerfile b/images/Dockerfile index 38c76814ce3..1b8428ce809 100644 --- a/images/Dockerfile +++ b/images/Dockerfile @@ -5,8 +5,8 @@ ARG TARGETOS ARG TARGETARCH ARG RUNNER_VERSION ARG RUNNER_CONTAINER_HOOKS_VERSION=0.7.0 -ARG DOCKER_VERSION=28.1.1 -ARG BUILDX_VERSION=0.23.0 +ARG DOCKER_VERSION=28.2.1 +ARG BUILDX_VERSION=0.24.0 RUN apt update -y && apt install curl unzip -y diff --git a/releaseNote.md b/releaseNote.md index 4ebc703f006..cfaeb974a8d 100644 --- a/releaseNote.md +++ b/releaseNote.md @@ -1,37 +1,13 @@ ## What's Changed -* Increase error body max length before truncation by @ericsciple in https://github.com/actions/runner/pull/3762 -* Fix release.yml break by upgrading actions/github-script by @TingluoHuang in https://github.com/actions/runner/pull/3772 -* Small runner code cleanup. by @TingluoHuang in https://github.com/actions/runner/pull/3773 -* Enable hostcontext to track auth migration. by @TingluoHuang in https://github.com/actions/runner/pull/3776 -* Add option in OAuthCred to load authUrlV2. by @TingluoHuang in https://github.com/actions/runner/pull/3777 -* Remove create session with broker in MessageListener. by @TingluoHuang in https://github.com/actions/runner/pull/3782 -* Enable auth migration based on config refresh. by @TingluoHuang in https://github.com/actions/runner/pull/3786 -* Set JWT.alg to PS256 with PssPadding. by @TingluoHuang in https://github.com/actions/runner/pull/3789 -* Enable FIPS by default. by @TingluoHuang in https://github.com/actions/runner/pull/3793 -* Support auth migration using authUrlV2 in Runner/MessageListener. by @TingluoHuang in https://github.com/actions/runner/pull/3787 -* Cleanup feature flag actions_skip_retry_complete_job_upon_known_errors by @ericsciple in https://github.com/actions/runner/pull/3806 -* Update dotnet sdk to latest version @8.0.408 by @github-actions in https://github.com/actions/runner/pull/3808 -* Bump hook to 0.7.0 by @nikola-jokic in https://github.com/actions/runner/pull/3813 -* Allow enable auth migration by default. by @TingluoHuang in https://github.com/actions/runner/pull/3804 -* Do not retry /renewjob on 404 by @ericsciple in https://github.com/actions/runner/pull/3828 -* Bump Microsoft.NET.Test.Sdk from 17.12.0 to 17.13.0 in /src by @dependabot in https://github.com/actions/runner/pull/3719 -* Add copilot-instructions.md by @pje in https://github.com/actions/runner/pull/3810 -* Bump actions/upload-release-asset from 1.0.1 to 1.0.2 by @dependabot in https://github.com/actions/runner/pull/3553 -* Ignore exception during auth migration. by @TingluoHuang in https://github.com/actions/runner/pull/3835 -* feat: default fromPath for problem matchers by @dsanders11 in https://github.com/actions/runner/pull/3802 -* Bump Azure.Storage.Blobs from 12.23.0 to 12.24.0 in /src by @dependabot in https://github.com/actions/runner/pull/3837 -* Bump nodejs version. by @TingluoHuang in https://github.com/actions/runner/pull/3840 -* Feature-flagged support for `JobContext.CheckRunID` by @pje in https://github.com/actions/runner/pull/3811 -* Bump System.ServiceProcess.ServiceController from 8.0.0 to 8.0.1 in /src by @dependabot in https://github.com/actions/runner/pull/3844 -* Bump xunit.runner.visualstudio from 2.5.8 to 2.8.2 in /src by @dependabot in https://github.com/actions/runner/pull/3845 -* Make sure the token's claims are match as expected. by @TingluoHuang in https://github.com/actions/runner/pull/3846 -* Prefer _migrated config on startup by @lokesh755 in https://github.com/actions/runner/pull/3853 -* Update docker and buildx by @TingluoHuang in https://github.com/actions/runner/pull/3854 - -## New Contributors -* @dsanders11 made their first contribution in https://github.com/actions/runner/pull/3802 - -**Full Changelog**: https://github.com/actions/runner/compare/v2.323.0...v2.324.0 +* Create schedule workflow to upgrade docker and buildx version. by @TingluoHuang in https://github.com/actions/runner/pull/3859 +* Update dotnet sdk to latest version @8.0.409 by @github-actions in https://github.com/actions/runner/pull/3860 +* Allow runner to use authv2 during config. by @TingluoHuang in https://github.com/actions/runner/pull/3866 +* show helpful error message when resolving actions directly with launch by @aiqiaoy in https://github.com/actions/runner/pull/3874 +* Update dotnet sdk to latest version @8.0.410 by @github-actions in https://github.com/actions/runner/pull/3871 +* Update Docker to v28.2.1 and Buildx to v0.24.0 by @github-actions in https://github.com/actions/runner/pull/3881 +* Allow NO_SSL_VERIFY in RawHttpMessageHandler. by @TingluoHuang in https://github.com/actions/runner/pull/3883 + +**Full Changelog**: https://github.com/actions/runner/compare/v2.324.0...v2.325.0 _Note: Actions Runner follows a progressive release policy, so the latest release might not be available to your enterprise, organization, or repository yet. To confirm which version of the Actions Runner you should expect, please view the download instructions for your enterprise, organization, or repository. diff --git a/src/Runner.Common/Constants.cs b/src/Runner.Common/Constants.cs index 51503142400..03d01b6288d 100644 --- a/src/Runner.Common/Constants.cs +++ b/src/Runner.Common/Constants.cs @@ -168,6 +168,7 @@ public static class Features public static readonly string UseContainerPathForTemplate = "DistributedTask.UseContainerPathForTemplate"; public static readonly string AllowRunnerContainerHooks = "DistributedTask.AllowRunnerContainerHooks"; public static readonly string AddCheckRunIdToJobContext = "actions_add_check_run_id_to_job_context"; + public static readonly string DisplayHelpfulActionsDownloadErrors = "actions_display_helpful_actions_download_errors"; } public static readonly string InternalTelemetryIssueDataKey = "_internal_telemetry"; diff --git a/src/Runner.Common/LaunchServer.cs b/src/Runner.Common/LaunchServer.cs index f8584ac5341..6fb69833e0d 100644 --- a/src/Runner.Common/LaunchServer.cs +++ b/src/Runner.Common/LaunchServer.cs @@ -15,7 +15,7 @@ public interface ILaunchServer : IRunnerService { void InitializeLaunchClient(Uri uri, string token); - Task ResolveActionsDownloadInfoAsync(Guid planId, Guid jobId, ActionReferenceList actionReferenceList, CancellationToken cancellationToken); + Task ResolveActionsDownloadInfoAsync(Guid planId, Guid jobId, ActionReferenceList actionReferenceList, CancellationToken cancellationToken, bool displayHelpfulActionsDownloadErrors); } public sealed class LaunchServer : RunnerService, ILaunchServer @@ -42,12 +42,16 @@ public void InitializeLaunchClient(Uri uri, string token) } public Task ResolveActionsDownloadInfoAsync(Guid planId, Guid jobId, ActionReferenceList actionReferenceList, - CancellationToken cancellationToken) + CancellationToken cancellationToken, bool displayHelpfulActionsDownloadErrors) { if (_launchClient != null) { - return _launchClient.GetResolveActionsDownloadInfoAsync(planId, jobId, actionReferenceList, - cancellationToken: cancellationToken); + if (!displayHelpfulActionsDownloadErrors) + { + return _launchClient.GetResolveActionsDownloadInfoAsync(planId, jobId, actionReferenceList, + cancellationToken: cancellationToken); + } + return _launchClient.GetResolveActionsDownloadInfoAsyncV2(planId, jobId, actionReferenceList, cancellationToken); } throw new InvalidOperationException("Launch client is not initialized."); diff --git a/src/Runner.Listener/Configuration/ConfigurationManager.cs b/src/Runner.Listener/Configuration/ConfigurationManager.cs index 15a57631704..474e6a360f2 100644 --- a/src/Runner.Listener/Configuration/ConfigurationManager.cs +++ b/src/Runner.Listener/Configuration/ConfigurationManager.cs @@ -70,7 +70,7 @@ public RunnerSettings LoadSettings() public RunnerSettings LoadMigratedSettings() { Trace.Info(nameof(LoadMigratedSettings)); - + // Check if migrated settings file exists if (!_store.IsMigratedConfigured()) { @@ -387,6 +387,14 @@ public async Task ConfigureAsync(CommandSettings command) }, }; + if (agent.Properties.GetValue("EnableAuthMigrationByDefault", false) && + agent.Properties.TryGetValue("AuthorizationUrlV2", out var authUrlV2) && + !string.IsNullOrEmpty(authUrlV2)) + { + credentialData.Data["enableAuthMigrationByDefault"] = "true"; + credentialData.Data["authorizationUrlV2"] = authUrlV2; + } + // Save the negotiated OAuth credential data _store.SaveCredential(credentialData); } diff --git a/src/Runner.Sdk/Util/VssUtil.cs b/src/Runner.Sdk/Util/VssUtil.cs index f35b0c21dd1..012d27f7345 100644 --- a/src/Runner.Sdk/Util/VssUtil.cs +++ b/src/Runner.Sdk/Util/VssUtil.cs @@ -38,6 +38,7 @@ public static void InitializeVssClientSettings(List addi if (StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("GITHUB_ACTIONS_RUNNER_TLS_NO_VERIFY"))) { VssClientHttpRequestSettings.Default.ServerCertificateValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; + RawClientHttpRequestSettings.Default.ServerCertificateValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; } var rawHeaderValues = new List(); diff --git a/src/Runner.Worker/ActionManager.cs b/src/Runner.Worker/ActionManager.cs index 47a66dd12fa..9a21aeb4cb0 100644 --- a/src/Runner.Worker/ActionManager.cs +++ b/src/Runner.Worker/ActionManager.cs @@ -688,7 +688,8 @@ private async Task BuildActionContainerAsync(IExecutionContext executionContext, { if (MessageUtil.IsRunServiceJob(executionContext.Global.Variables.Get(Constants.Variables.System.JobRequestType))) { - actionDownloadInfos = await launchServer.ResolveActionsDownloadInfoAsync(executionContext.Global.Plan.PlanId, executionContext.Root.Id, new WebApi.ActionReferenceList { Actions = actionReferences }, executionContext.CancellationToken); + var displayHelpfulActionsDownloadErrors = executionContext.Global.Variables.GetBoolean(Constants.Runner.Features.DisplayHelpfulActionsDownloadErrors) ?? false; + actionDownloadInfos = await launchServer.ResolveActionsDownloadInfoAsync(executionContext.Global.Plan.PlanId, executionContext.Root.Id, new WebApi.ActionReferenceList { Actions = actionReferences }, executionContext.CancellationToken, displayHelpfulActionsDownloadErrors); } else { diff --git a/src/Sdk/Common/Common/RawHttpMessageHandler.cs b/src/Sdk/Common/Common/RawHttpMessageHandler.cs index 316bcd576ff..e80e6a74727 100644 --- a/src/Sdk/Common/Common/RawHttpMessageHandler.cs +++ b/src/Sdk/Common/Common/RawHttpMessageHandler.cs @@ -106,6 +106,18 @@ protected override async Task SendAsync( { VssTraceActivity traceActivity = VssTraceActivity.Current; + if (!m_appliedServerCertificateValidationCallbackToTransportHandler && + request.RequestUri.Scheme == "https") + { + HttpClientHandler httpClientHandler = m_transportHandler as HttpClientHandler; + if (httpClientHandler != null && + this.Settings.ServerCertificateValidationCallback != null) + { + httpClientHandler.ServerCertificateCustomValidationCallback = this.Settings.ServerCertificateValidationCallback; + } + m_appliedServerCertificateValidationCallbackToTransportHandler = true; + } + lock (m_thisLock) { // Ensure that we attempt to use the most appropriate authentication mechanism by default. @@ -291,6 +303,7 @@ private static void ApplySettings( } } + private bool m_appliedServerCertificateValidationCallbackToTransportHandler; private readonly HttpMessageHandler m_transportHandler; private HttpMessageInvoker m_messageInvoker; private CredentialWrapper m_credentialWrapper; diff --git a/src/Sdk/WebApi/WebApi/LaunchContracts.cs b/src/Sdk/WebApi/WebApi/LaunchContracts.cs index 7b896fd758e..28b6ce3cf0e 100644 --- a/src/Sdk/WebApi/WebApi/LaunchContracts.cs +++ b/src/Sdk/WebApi/WebApi/LaunchContracts.cs @@ -29,7 +29,7 @@ public class ActionDownloadInfoResponse { [DataMember(EmitDefaultValue = false, Name = "authentication")] public ActionDownloadAuthenticationResponse Authentication { get; set; } - + [DataMember(EmitDefaultValue = false, Name = "package_details")] public ActionDownloadPackageDetailsResponse PackageDetails { get; set; } @@ -64,7 +64,7 @@ public class ActionDownloadAuthenticationResponse [DataContract] - public class ActionDownloadPackageDetailsResponse + public class ActionDownloadPackageDetailsResponse { [DataMember(EmitDefaultValue = false, Name = "version")] public string Version { get; set; } @@ -81,4 +81,25 @@ public class ActionDownloadInfoResponseCollection [DataMember(EmitDefaultValue = false, Name = "actions")] public IDictionary Actions { get; set; } } + + [DataContract] + public class ActionDownloadResolutionError + { + /// + /// The error message associated with the action download error. + /// + [DataMember(EmitDefaultValue = false, Name = "message")] + public string Message { get; set; } + } + + [DataContract] + public class ActionDownloadResolutionErrorCollection + { + /// + /// A mapping of action specifications to their download errors. + /// The key is the full name of the action plus version, e.g. "actions/checkout@v2". + /// + [DataMember(EmitDefaultValue = false, Name = "errors")] + public IDictionary Errors { get; set; } + } } diff --git a/src/Sdk/WebApi/WebApi/LaunchHttpClient.cs b/src/Sdk/WebApi/WebApi/LaunchHttpClient.cs index 6ba06a6a052..24e398636b1 100644 --- a/src/Sdk/WebApi/WebApi/LaunchHttpClient.cs +++ b/src/Sdk/WebApi/WebApi/LaunchHttpClient.cs @@ -2,6 +2,7 @@ using System; using System.Linq; +using System.Net; using System.Net.Http; using System.Net.Http.Formatting; using System.Net.Http.Headers; @@ -32,11 +33,52 @@ public LaunchHttpClient( public async Task GetResolveActionsDownloadInfoAsync(Guid planId, Guid jobId, ActionReferenceList actionReferenceList, CancellationToken cancellationToken) { var GetResolveActionsDownloadInfoURLEndpoint = new Uri(m_launchServiceUrl, $"/actions/build/{planId.ToString()}/jobs/{jobId.ToString()}/runnerresolve/actions"); - return ToServerData(await GetLaunchSignedURLResponse(GetResolveActionsDownloadInfoURLEndpoint, ToGitHubData(actionReferenceList), cancellationToken)); + var response = await GetLaunchSignedURLResponse(GetResolveActionsDownloadInfoURLEndpoint, ToGitHubData(actionReferenceList), cancellationToken); + return ToServerData(await ReadJsonContentAsync(response, cancellationToken)); } - // Resolve Actions - private async Task GetLaunchSignedURLResponse(Uri uri, R request, CancellationToken cancellationToken) + public async Task GetResolveActionsDownloadInfoAsyncV2(Guid planId, Guid jobId, ActionReferenceList actionReferenceList, CancellationToken cancellationToken) + { + var GetResolveActionsDownloadInfoURLEndpoint = new Uri(m_launchServiceUrl, $"/actions/build/{planId.ToString()}/jobs/{jobId.ToString()}/runnerresolve/actions"); + var response = await GetLaunchSignedURLResponse(GetResolveActionsDownloadInfoURLEndpoint, ToGitHubData(actionReferenceList), cancellationToken); + + if (response.IsSuccessStatusCode) + { + // Success response - deserialize the action download info + return ToServerData(await ReadJsonContentAsync(response, cancellationToken)); + } + + var responseError = response.ReasonPhrase ?? ""; + if (response.StatusCode == HttpStatusCode.UnprocessableEntity) + { + // 422 response - unresolvable actions, error details are in the body + var errors = await ReadJsonContentAsync(response, cancellationToken); + string combinedErrorMessage; + if (errors?.Errors != null && errors.Errors.Any()) + { + combinedErrorMessage = String.Join(". ", errors.Errors.Select(kvp => kvp.Value.Message)); + } + else + { + combinedErrorMessage = responseError; + } + + throw new UnresolvableActionDownloadInfoException(combinedErrorMessage); + } + else if (response.StatusCode == HttpStatusCode.TooManyRequests) + { + // Here we want to add a message so customers don't think it's a rate limit scoped to them + // Ideally this would be 500 but the runner retries 500s, which we don't want to do when we're being rate limited + // See: https://github.com/github/ecosystem-api/issues/4084 + throw new NonRetryableActionDownloadInfoException(responseError + " (GitHub has reached an internal rate limit, please try again later)"); + } + else + { + throw new Exception(responseError); + } + } + + private async Task GetLaunchSignedURLResponse(Uri uri, R request, CancellationToken cancellationToken) { using (HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, uri)) { @@ -46,10 +88,7 @@ private async Task GetLaunchSignedURLResponse(Uri uri, R request, Cance using (HttpContent content = new ObjectContent(request, m_formatter)) { requestMessage.Content = content; - using (var response = await SendAsync(requestMessage, HttpCompletionOption.ResponseContentRead, cancellationToken: cancellationToken)) - { - return await ReadJsonContentAsync(response, cancellationToken); - } + return await SendAsync(requestMessage, HttpCompletionOption.ResponseContentRead, cancellationToken: cancellationToken); } } } diff --git a/src/Sdk/WebApi/WebApi/ResultsHttpClient.cs b/src/Sdk/WebApi/WebApi/ResultsHttpClient.cs index fb250d4be03..31819a4b2bf 100644 --- a/src/Sdk/WebApi/WebApi/ResultsHttpClient.cs +++ b/src/Sdk/WebApi/WebApi/ResultsHttpClient.cs @@ -520,8 +520,8 @@ private Step ConvertTimelineRecordToStep(TimelineRecord r) Number = r.Order.GetValueOrDefault(), Name = r.Name, Status = ConvertStateToStatus(r.State.GetValueOrDefault()), - StartedAt = r.StartTime?.ToString(Constants.TimestampFormat), - CompletedAt = r.FinishTime?.ToString(Constants.TimestampFormat), + StartedAt = r.StartTime?.ToString(Constants.TimestampFormat, CultureInfo.InvariantCulture), + CompletedAt = r.FinishTime?.ToString(Constants.TimestampFormat, CultureInfo.InvariantCulture), Conclusion = ConvertResultToConclusion(r.Result) }; } diff --git a/src/Test/L0/Sdk/LaunchWebApi/LaunchHttpClientL0.cs b/src/Test/L0/Sdk/LaunchWebApi/LaunchHttpClientL0.cs new file mode 100644 index 00000000000..bda56141c6e --- /dev/null +++ b/src/Test/L0/Sdk/LaunchWebApi/LaunchHttpClientL0.cs @@ -0,0 +1,126 @@ +using GitHub.Actions.RunService.WebApi; +using GitHub.DistributedTask.WebApi; +using GitHub.Services.Launch.Client; +using GitHub.Services.Launch.Contracts; +using Moq; +using Moq.Protected; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace GitHub.Actions.RunService.WebApi.Tests +{ + public sealed class LaunchHttpClientL0 + { + [Fact] + public async Task GetResolveActionsDownloadInfoAsync_SuccessResponse() + { + var baseUrl = new Uri("https://api.github.com/"); + var planId = Guid.NewGuid(); + var jobId = Guid.NewGuid(); + var token = "fake-token"; + + var actionReferenceList = new ActionReferenceList + { + Actions = new List + { + new ActionReference + { + NameWithOwner = "owner1/action1", + Ref = "0123456789" + } + } + }; + + var responseContent = @"{ + ""actions"": { + ""owner1/action1@0123456789"": { + ""name"": ""owner1/action1"", + ""resolved_name"": ""owner1/action1"", + ""resolved_sha"": ""0123456789"", + ""version"": ""0123456789"", + ""zip_url"": ""https://github.com/owner1/action1/zip"", + ""tar_url"": ""https://github.com/owner1/action1/tar"" + } + } + }"; + + var httpResponse = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(responseContent, Encoding.UTF8, "application/json"), + RequestMessage = new HttpRequestMessage() + { + RequestUri = new Uri($"{baseUrl}actions/build/{planId}/jobs/{jobId}/runnerresolve/actions") + } + }; + + var mockHandler = new Mock(); + mockHandler.Protected().Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(httpResponse); + + var client = new LaunchHttpClient(baseUrl, mockHandler.Object, token, false); + var result = await client.GetResolveActionsDownloadInfoAsyncV2(planId, jobId, actionReferenceList, CancellationToken.None); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result.Actions); + Assert.Equal(actionReferenceList.Actions.Count, result.Actions.Count); + Assert.True(result.Actions.ContainsKey("owner1/action1@0123456789")); + } + + [Fact] + public async Task GetResolveActionsDownloadInfoAsync_UnprocessableEntityResponse() + { + var baseUrl = new Uri("https://api.github.com/"); + var planId = Guid.NewGuid(); + var jobId = Guid.NewGuid(); + var token = "fake-token"; + + var actionReferenceList = new ActionReferenceList + { + Actions = new List + { + new ActionReference + { + NameWithOwner = "owner1/action1", + Ref = "0123456789" + } + } + }; + + var responseContent = @"{ + ""errors"": { + ""owner1/invalid-action@0123456789"": { + ""message"": ""Unable to resolve action 'owner1/invalid-action@0123456789', repository not found"" + } + } + }"; + + var httpResponse = new HttpResponseMessage(HttpStatusCode.UnprocessableEntity) + { + Content = new StringContent(responseContent, Encoding.UTF8, "application/json"), + RequestMessage = new HttpRequestMessage() + { + RequestUri = new Uri($"{baseUrl}actions/build/{planId}/jobs/{jobId}/runnerresolve/actions") + } + }; + + var mockHandler = new Mock(); + mockHandler.Protected().Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(httpResponse); + + var client = new LaunchHttpClient(baseUrl, mockHandler.Object, token, false); + + var exception = await Assert.ThrowsAsync( + () => client.GetResolveActionsDownloadInfoAsyncV2(planId, jobId, actionReferenceList, CancellationToken.None)); + + Assert.Contains("repository not found", exception.Message); + } + } +} \ No newline at end of file diff --git a/src/Test/L0/Worker/ActionManagerL0.cs b/src/Test/L0/Worker/ActionManagerL0.cs index 91f183ae220..50d5b99d106 100644 --- a/src/Test/L0/Worker/ActionManagerL0.cs +++ b/src/Test/L0/Worker/ActionManagerL0.cs @@ -2411,8 +2411,8 @@ private void Setup([CallerMemberName] string name = "", bool enableComposite = t }); _launchServer = new Mock(); - _launchServer.Setup(x => x.ResolveActionsDownloadInfoAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((Guid planId, Guid jobId, ActionReferenceList actions, CancellationToken cancellationToken) => + _launchServer.Setup(x => x.ResolveActionsDownloadInfoAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((Guid planId, Guid jobId, ActionReferenceList actions, CancellationToken cancellationToken, bool displayHelpfulActionsDownloadErrors) => { var result = new ActionDownloadInfoCollection { Actions = new Dictionary() }; foreach (var action in actions.Actions) diff --git a/src/dev.sh b/src/dev.sh index 2a008cbd128..84dceb7e461 100755 --- a/src/dev.sh +++ b/src/dev.sh @@ -17,7 +17,7 @@ LAYOUT_DIR="$SCRIPT_DIR/../_layout" DOWNLOAD_DIR="$SCRIPT_DIR/../_downloads/netcore2x" PACKAGE_DIR="$SCRIPT_DIR/../_package" DOTNETSDK_ROOT="$SCRIPT_DIR/../_dotnetsdk" -DOTNETSDK_VERSION="8.0.408" +DOTNETSDK_VERSION="8.0.410" DOTNETSDK_INSTALLDIR="$DOTNETSDK_ROOT/$DOTNETSDK_VERSION" RUNNER_VERSION=$(cat runnerversion) diff --git a/src/global.json b/src/global.json index fc88f757ace..f2f4b022766 100644 --- a/src/global.json +++ b/src/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "8.0.408" + "version": "8.0.410" } } diff --git a/src/runnerversion b/src/runnerversion index c0fbc63596d..3ba5855e2dc 100644 --- a/src/runnerversion +++ b/src/runnerversion @@ -1 +1 @@ -2.324.0 +2.325.0