From 809587db57a69e573d5c3676810822379d4948b7 Mon Sep 17 00:00:00 2001 From: Maxwell Weru Date: Thu, 14 Sep 2023 10:06:48 +0300 Subject: [PATCH] Preliminary new updater setup (#794) * Setup pulling of files from dependabot updater * Update ENV variables passed to the updater to match the official one --- copy-updater-files.ps1 | 20 +++++ .../Models/UpdateJobResponse.cs | 3 + .../Workflow/UpdateRunner.cs | 32 ++++++- .../Workflow/WorkflowConfigureOptions.cs | 20 +++++ .../Workflow/WorkflowOptions.cs | 32 +++++++ server/Tingle.Dependabot/appsettings.json | 6 +- server/main.bicep | 38 +++++++- server/main.json | 89 +++++++++++++++++-- updater/lib/dependabot/.gitkeep | 0 updater/lib/dependabot/environment.rb | 65 ++++++++++++++ updater/spec/dependabot/environment_spec.rb | 39 ++++++++ updater/spec/spec_helper.rb | 3 +- 12 files changed, 335 insertions(+), 12 deletions(-) create mode 100644 copy-updater-files.ps1 delete mode 100644 updater/lib/dependabot/.gitkeep create mode 100644 updater/lib/dependabot/environment.rb create mode 100644 updater/spec/dependabot/environment_spec.rb diff --git a/copy-updater-files.ps1 b/copy-updater-files.ps1 new file mode 100644 index 00000000..a621d85a --- /dev/null +++ b/copy-updater-files.ps1 @@ -0,0 +1,20 @@ +Param( + [string] $tag = "v0.230.0" +) + +$hash = [ordered]@{ + "updater/lib/dependabot/environment.rb" = "lib/dependabot/environment.rb" + "updater/spec/dependabot/environment_spec.rb" = "spec/dependabot/environment_spec.rb" + # "updater/spec/spec_helper.rb" = "spec/spec_helper.rb" +} + +$baseUrl = "https://raw.githubusercontent.com/dependabot/dependabot-core" +$destinationFolder = Join-Path -Path '.' -ChildPath 'updater' + +foreach ($h in $hash.GetEnumerator()) { + $sourceUrl = "$baseUrl/$tag/$($h.Name)" + $destinationPath = Join-Path -Path "$destinationFolder" -ChildPath "$($h.Value)" + Write-Host "`Downloading $($h.Name) ..." + [System.IO.Directory]::CreateDirectory("$(Split-Path -Path "$destinationPath")") | Out-Null + Invoke-WebRequest -Uri $sourceUrl -OutFile $destinationPath +} diff --git a/server/Tingle.Dependabot/Models/UpdateJobResponse.cs b/server/Tingle.Dependabot/Models/UpdateJobResponse.cs index 8f4cb6b5..5e1c75ea 100644 --- a/server/Tingle.Dependabot/Models/UpdateJobResponse.cs +++ b/server/Tingle.Dependabot/Models/UpdateJobResponse.cs @@ -58,6 +58,9 @@ public UpdateJobAttributes(UpdateJob job) : this() [JsonPropertyName("security-updates-only")] public bool? SecurityUpdatesOnly { get; set; } + + [JsonPropertyName("debug")] + public bool? Debug { get; set; } } public sealed record UpdateJobAttributesSource() diff --git a/server/Tingle.Dependabot/Workflow/UpdateRunner.cs b/server/Tingle.Dependabot/Workflow/UpdateRunner.cs index f386c277..2f9b9824 100644 --- a/server/Tingle.Dependabot/Workflow/UpdateRunner.cs +++ b/server/Tingle.Dependabot/Workflow/UpdateRunner.cs @@ -54,15 +54,20 @@ public async Task CreateAsync(Repository repository, RepositoryUpdate update, Up catch (Azure.RequestFailedException rfe) when (rfe.Status is 404) { } // prepare the container + var fileShareName = options.FileShareName; + var volumeName = "working-dir"; var image = options.UpdaterContainerImageTemplate!.Replace("{{ecosystem}}", job.PackageEcosystem); var container = new ContainerInstanceContainer(UpdaterContainerName, image, new(job.Resources!)); var env = CreateVariables(repository, update, job); foreach (var (key, value) in env) container.EnvironmentVariables.Add(new ContainerEnvironmentVariable(key) { Value = value, }); - // prepare the container command/entrypoint (this is what seems to work) + // set the container command/entrypoint (this is what seems to work) container.Command.Add("/bin/bash"); container.Command.Add("bin/run.sh"); container.Command.Add("update-script"); + + // add volume mounts + container.VolumeMounts.Add(new ContainerVolumeMount(volumeName, "/mnt/dependabot")); // prepare the container group var data = new ContainerGroupData(options.Location!, new[] { container, }, ContainerInstanceOperatingSystemType.Linux) @@ -81,6 +86,12 @@ public async Task CreateAsync(Repository repository, RepositoryUpdate update, Up data.ImageRegistryCredentials.Add(new ContainerGroupImageRegistryCredential(registry) { Identity = options.ManagedIdentityId, }); } + // add volumes + data.Volumes.Add(new ContainerVolume(volumeName) + { + AzureFile = new(fileShareName, options.StorageAccountName) { StorageAccountKey = options.StorageAccountKey, }, + }); + // add tags to the data for tracing purposes data.Tags["purpose"] = "dependabot"; data.Tags.AddIfNotDefault("ecosystem", job.PackageEcosystem) @@ -130,6 +141,13 @@ public async Task DeleteAsync(UpdateJob job, CancellationToken cancellationToken // there is no state for jobs that are running if (status is UpdateJobStatus.Running) return null; + + // delete the job directory f it exists + var jobDirectory = Path.Join(options.WorkingDirectory, job.Id); + if (Directory.Exists(jobDirectory)) + { + Directory.Delete(jobDirectory); + } // get the period var currentState = resource.Data.Containers.Single(c => c.Name == UpdaterContainerName).InstanceView?.CurrentState; @@ -193,15 +211,27 @@ internal IDictionary CreateVariables(Repository repository, Repo { static string? ToJson(T? entries) => entries is null ? null : JsonSerializer.Serialize(entries, serializerOptions); // null ensures we do not add to the values + var jobDirectory = Path.Join(options.WorkingDirectory, job.Id); + // Add compulsory values var values = new Dictionary { + ["DEPENDABOT_JOB_ID"] = job.Id!, + ["DEPENDABOT_JOB_TOKEN"] = job.AuthKey!, + ["DEPENDABOT_JOB_PATH"] = Path.Join(jobDirectory, "job.json"), + ["DEPENDABOT_OUTPUT_PATH"] = Path.Join(jobDirectory, "output"), + ["DEPENDABOT_PACKAGE_MANAGER"] = job.PackageEcosystem!, ["DEPENDABOT_DIRECTORY"] = update.Directory!, ["DEPENDABOT_OPEN_PULL_REQUESTS_LIMIT"] = update.OpenPullRequestsLimit!.Value.ToString(), }; // Add optional values + values.AddIfNotDefault("DEPENDABOT_DEBUG", options.DebugJobs?.ToString().ToLower()) + .AddIfNotDefault("DEPENDABOT_API_URL", options.JobsApiUrl) + .AddIfNotDefault("DEPENDABOT_REPO_CONTENTS_PATH", Path.Join(jobDirectory, "repo")) + .AddIfNotDefault("UPDATER_DETERMINISTIC", options.DeterministicUpdates?.ToString().ToLower()); + values.AddIfNotDefault("GITHUB_ACCESS_TOKEN", options.GithubToken) .AddIfNotDefault("DEPENDABOT_REBASE_STRATEGY", update.RebaseStrategy) .AddIfNotDefault("DEPENDABOT_TARGET_BRANCH", update.TargetBranch) diff --git a/server/Tingle.Dependabot/Workflow/WorkflowConfigureOptions.cs b/server/Tingle.Dependabot/Workflow/WorkflowConfigureOptions.cs index fd35d2d4..be321f09 100644 --- a/server/Tingle.Dependabot/Workflow/WorkflowConfigureOptions.cs +++ b/server/Tingle.Dependabot/Workflow/WorkflowConfigureOptions.cs @@ -63,11 +63,31 @@ public ValidateOptionsResult Validate(string? name, WorkflowOptions options) return ValidateOptionsResult.Fail($"'{nameof(options.ManagedIdentityId)}' cannot be null or whitespace"); } + if (string.IsNullOrWhiteSpace(options.WorkingDirectory)) + { + return ValidateOptionsResult.Fail($"'{nameof(options.WorkingDirectory)}' cannot be null or whitespace"); + } + if (string.IsNullOrWhiteSpace(options.Location)) { return ValidateOptionsResult.Fail($"'{nameof(options.Location)}' cannot be null or whitespace"); } + if (string.IsNullOrWhiteSpace(options.StorageAccountName)) + { + return ValidateOptionsResult.Fail($"'{nameof(options.StorageAccountName)}' cannot be null or whitespace"); + } + + if (string.IsNullOrWhiteSpace(options.StorageAccountKey)) + { + return ValidateOptionsResult.Fail($"'{nameof(options.StorageAccountKey)}' cannot be null or whitespace"); + } + + if (string.IsNullOrWhiteSpace(options.FileShareName)) + { + return ValidateOptionsResult.Fail($"'{nameof(options.FileShareName)}' cannot be null or whitespace"); + } + return ValidateOptionsResult.Success; } } diff --git a/server/Tingle.Dependabot/Workflow/WorkflowOptions.cs b/server/Tingle.Dependabot/Workflow/WorkflowOptions.cs index d7dbee2b..48307560 100644 --- a/server/Tingle.Dependabot/Workflow/WorkflowOptions.cs +++ b/server/Tingle.Dependabot/Workflow/WorkflowOptions.cs @@ -51,6 +51,27 @@ public class WorkflowOptions /// Authentication token for accessing the project. public string? ProjectToken { get; set; } + /// Whether to debug all jobs. + public bool? DebugJobs { get; set; } + + /// URL on which to access the API from the jobs. + /// https://dependabot.dummy-123.westeurope.azurecontainerapps.io + public string? JobsApiUrl { get; set; } + + /// + /// Root working directory where file are written during job scheduling and execution. + /// This directory is the root for all jobs. + /// Subdirectories are created for each job and further for each usage type. + /// For example, if this value is set to /mnt/dependabot, + /// A job identified as 123456789 will have files written at /mnt/dependabot/123456789 + /// and some nested directories in it such as /mnt/dependabot/123456789/repo. + /// + /// /mnt/dependabot + public string? WorkingDirectory { get; set; } + + /// Whether updates should be created in the same order. + public bool? DeterministicUpdates { get; set; } + /// Whether update jobs should fail when an exception occurs. public bool FailOnException { get; set; } @@ -82,6 +103,17 @@ public class WorkflowOptions /// Location/region where to create new update jobs. public string? Location { get; set; } // using Azure.Core.Location does not work when binding from IConfiguration + /// Name of the storage account. + /// dependabot-1234567890 + public string? StorageAccountName { get; set; } // only used with ContainerInstances + + /// Access key for the storage account. + public string? StorageAccountKey { get; set; } // only used with ContainerInstances + + /// Name of the file share for the working directory + /// working-dir + public string? FileShareName { get; set; } // only used with ContainerInstances + /// /// Possible/allowed paths for the configuration files in a repository. /// diff --git a/server/Tingle.Dependabot/appsettings.json b/server/Tingle.Dependabot/appsettings.json index f5a0b7f9..68dd3e0e 100644 --- a/server/Tingle.Dependabot/appsettings.json +++ b/server/Tingle.Dependabot/appsettings.json @@ -67,7 +67,11 @@ "UpdaterContainerImageTemplate": "ghcr.io/tinglesoftware/dependabot-updater-{{ecosystem}}:1.20.0-ci.37", "ProjectUrl": "https://dev.azure.com/fabrikam/DefaultCollection", "ProjectToken": "", + "WorkingDirectory": "work", "GithubToken": "", - "Location": "westeurope" + "Location": "westeurope", + "StorageAccountName": "dependabot-1234567890", + "StorageAccountKey": "", + "FileShareName": "working-dir" } } diff --git a/server/main.bicep b/server/main.bicep index d31d8216..e8928e6a 100644 --- a/server/main.bicep +++ b/server/main.bicep @@ -16,6 +16,9 @@ param synchronizeOnStartup bool = false @description('Whether to create or update subscriptions on startup.') param createOrUpdateWebhooksOnStartup bool = false +@description('Whether to debug all jobs.') +param debugAllJobs bool = false + @description('Access token for authenticating requests to GitHub.') param githubToken string = '' @@ -64,6 +67,7 @@ var sqlServerAdministratorLogin = uniqueString(resourceGroup().id) // e.g. zecnx var sqlServerAdministratorLoginPassword = '${skip(uniqueString(resourceGroup().id), 5)}%${uniqueString('sql-password', resourceGroup().id)}' // e.g. abcde%zecnx476et7xm (19 characters) // avoid conflicts across multiple deployments for resources that generate FQDN based on the name var collisionSuffix = uniqueString(resourceGroup().id) // e.g. zecnx476et7xm (13 characters) +var fileShareName = 'working-dir' /* Managed Identities */ resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { @@ -101,6 +105,12 @@ resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = { defaultAction: 'Allow' } } + + resource fileServices 'fileServices' existing = { + name: 'default' + + resource workingDir 'shares' = { name: fileShareName } + } } /* SQL Server */ @@ -168,7 +178,7 @@ resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10 } /* Container App Environment */ -resource appEnvironment 'Microsoft.App/managedEnvironments@2022-10-01' = { +resource appEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' = { name: name location: location properties: { @@ -180,6 +190,18 @@ resource appEnvironment 'Microsoft.App/managedEnvironments@2022-10-01' = { } } } + + resource workingDir 'storages' = { + name: fileShareName + properties: { + azureFile: { + accessMode: 'ReadWrite' + shareName: fileShareName + accountName: storageAccount.name + accountKey: storageAccount.listKeys().keys[0].value + } + } + } } /* Application Insights */ @@ -194,7 +216,7 @@ resource appInsights 'Microsoft.Insights/components@2020-02-02' = { } /* Container App */ -resource app 'Microsoft.App/containerApps@2022-10-01' = { +resource app 'Microsoft.App/containerApps@2023-05-01' = { name: name location: location properties: { @@ -232,6 +254,10 @@ resource app 'Microsoft.App/containerApps@2022-10-01' = { name: 'log-analytics-workspace-key' value: logAnalyticsWorkspace.listKeys().primarySharedKey } + { + name: 'storage-account-key' + value: storageAccount.listKeys().keys[0].value + } ] } template: { @@ -239,6 +265,7 @@ resource app 'Microsoft.App/containerApps@2022-10-01' = { { image: 'ghcr.io/tinglesoftware/dependabot-server:${imageTag}' name: 'dependabot' + volumeMounts: [ { mountPath: '/mnt/dependabot', volumeName: fileShareName } ] env: [ { name: 'AZURE_CLIENT_ID', value: managedIdentity.properties.clientId } // Specifies the User-Assigned Managed Identity to use. Without this, the app attempt to use the system assigned one. { name: 'ASPNETCORE_FORWARDEDHEADERS_ENABLED', value: 'true' } // Application is behind proxy @@ -252,6 +279,9 @@ resource app 'Microsoft.App/containerApps@2022-10-01' = { { name: 'Workflow__CreateOrUpdateWebhooksOnStartup', value: createOrUpdateWebhooksOnStartup ? 'true' : 'false' } { name: 'Workflow__ProjectUrl', value: projectUrl } { name: 'Workflow__ProjectToken', secretRef: 'project-token' } + { name: 'Workflow__DebugJobs', value: '${debugAllJobs}' } + { name: 'Workflow__JobsApiUrl', value: 'https://${name}.${appEnvironment.properties.defaultDomain}' } + { name: 'Workflow__WorkingDirectory', value: '/mnt/dependabot' } { name: 'Workflow__WebhookEndpoint' value: 'https://${name}.${appEnvironment.properties.defaultDomain}/webhooks/azure' @@ -276,6 +306,9 @@ resource app 'Microsoft.App/containerApps@2022-10-01' = { { name: 'Workflow__AutoApprove', value: autoApprove ? 'true' : 'false' } { name: 'Workflow__GithubToken', value: githubToken } { name: 'Workflow__Location', value: location } + { name: 'Workflow__StorageAccountName', value: storageAccount.name } + { name: 'Workflow__StorageAccountKey', secretRef: 'storage-account-key' } + { name: 'Workflow__FileShareName', value: fileShareName } { name: 'Authentication__Schemes__Management__Authority' @@ -311,6 +344,7 @@ resource app 'Microsoft.App/containerApps@2022-10-01' = { ] } ] + volumes: [ { name: fileShareName, storageName: fileShareName, storageType: 'AzureFile' } ] scale: { minReplicas: minReplicas maxReplicas: maxReplicas diff --git a/server/main.json b/server/main.json index 723404a0..3e9158a5 100644 --- a/server/main.json +++ b/server/main.json @@ -42,6 +42,13 @@ "description": "Whether to create or update subscriptions on startup." } }, + "debugAllJobs": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Whether to debug all jobs." + } + }, "githubToken": { "type": "string", "defaultValue": "", @@ -133,9 +140,35 @@ "variables": { "sqlServerAdministratorLogin": "[uniqueString(resourceGroup().id)]", "sqlServerAdministratorLoginPassword": "[format('{0}%{1}', skip(uniqueString(resourceGroup().id), 5), uniqueString('sql-password', resourceGroup().id))]", - "collisionSuffix": "[uniqueString(resourceGroup().id)]" + "collisionSuffix": "[uniqueString(resourceGroup().id)]", + "fileShareName": "working-dir" }, "resources": [ + { + "type": "Microsoft.Storage/storageAccounts/fileServices/shares", + "apiVersion": "2022-09-01", + "name": "[format('{0}/{1}/{2}', format('{0}{1}', parameters('name'), variables('collisionSuffix')), 'default', variables('fileShareName'))]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', format('{0}{1}', parameters('name'), variables('collisionSuffix')))]" + ] + }, + { + "type": "Microsoft.App/managedEnvironments/storages", + "apiVersion": "2023-05-01", + "name": "[format('{0}/{1}', parameters('name'), variables('fileShareName'))]", + "properties": { + "azureFile": { + "accessMode": "ReadWrite", + "shareName": "[variables('fileShareName')]", + "accountName": "[format('{0}{1}', parameters('name'), variables('collisionSuffix'))]", + "accountKey": "[listKeys(resourceId('Microsoft.Storage/storageAccounts', format('{0}{1}', parameters('name'), variables('collisionSuffix'))), '2022-09-01').keys[0].value]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.App/managedEnvironments', parameters('name'))]", + "[resourceId('Microsoft.Storage/storageAccounts', format('{0}{1}', parameters('name'), variables('collisionSuffix')))]" + ] + }, { "type": "Microsoft.ManagedIdentity/userAssignedIdentities", "apiVersion": "2023-01-31", @@ -258,7 +291,7 @@ }, { "type": "Microsoft.App/managedEnvironments", - "apiVersion": "2022-10-01", + "apiVersion": "2023-05-01", "name": "[parameters('name')]", "location": "[parameters('location')]", "properties": { @@ -290,7 +323,7 @@ }, { "type": "Microsoft.App/containerApps", - "apiVersion": "2022-10-01", + "apiVersion": "2023-05-01", "name": "[parameters('name')]", "location": "[parameters('location')]", "properties": { @@ -326,6 +359,10 @@ { "name": "log-analytics-workspace-key", "value": "[listKeys(resourceId('Microsoft.OperationalInsights/workspaces', parameters('name')), '2022-10-01').primarySharedKey]" + }, + { + "name": "storage-account-key", + "value": "[listKeys(resourceId('Microsoft.Storage/storageAccounts', format('{0}{1}', parameters('name'), variables('collisionSuffix'))), '2022-09-01').keys[0].value]" } ] }, @@ -334,6 +371,12 @@ { "image": "[format('ghcr.io/tinglesoftware/dependabot-server:{0}', parameters('imageTag'))]", "name": "dependabot", + "volumeMounts": [ + { + "mountPath": "/mnt/dependabot", + "volumeName": "[variables('fileShareName')]" + } + ], "env": [ { "name": "AZURE_CLIENT_ID", @@ -375,9 +418,21 @@ "name": "Workflow__ProjectToken", "secretRef": "project-token" }, + { + "name": "Workflow__DebugJobs", + "value": "[format('{0}', parameters('debugAllJobs'))]" + }, + { + "name": "Workflow__JobsApiUrl", + "value": "[format('https://{0}.{1}', parameters('name'), reference(resourceId('Microsoft.App/managedEnvironments', parameters('name')), '2023-05-01').defaultDomain)]" + }, + { + "name": "Workflow__WorkingDirectory", + "value": "/mnt/dependabot" + }, { "name": "Workflow__WebhookEndpoint", - "value": "[format('https://{0}.{1}/webhooks/azure', parameters('name'), reference(resourceId('Microsoft.App/managedEnvironments', parameters('name')), '2022-10-01').defaultDomain)]" + "value": "[format('https://{0}.{1}/webhooks/azure', parameters('name'), reference(resourceId('Microsoft.App/managedEnvironments', parameters('name')), '2023-05-01').defaultDomain)]" }, { "name": "Workflow__SubscriptionPassword", @@ -431,13 +486,25 @@ "name": "Workflow__Location", "value": "[parameters('location')]" }, + { + "name": "Workflow__StorageAccountName", + "value": "[format('{0}{1}', parameters('name'), variables('collisionSuffix'))]" + }, + { + "name": "Workflow__StorageAccountKey", + "secretRef": "storage-account-key" + }, + { + "name": "Workflow__FileShareName", + "value": "[variables('fileShareName')]" + }, { "name": "Authentication__Schemes__Management__Authority", "value": "[format('{0}{1}/v2.0', environment().authentication.loginEndpoint, subscription().tenantId)]" }, { "name": "Authentication__Schemes__Management__ValidAudiences__0", - "value": "[format('https://{0}.{1}', parameters('name'), reference(resourceId('Microsoft.App/managedEnvironments', parameters('name')), '2022-10-01').defaultDomain)]" + "value": "[format('https://{0}.{1}', parameters('name'), reference(resourceId('Microsoft.App/managedEnvironments', parameters('name')), '2023-05-01').defaultDomain)]" }, { "name": "Authentication__Schemes__ServiceHooks__Credentials__vsts", @@ -477,6 +544,13 @@ ] } ], + "volumes": [ + { + "name": "[variables('fileShareName')]", + "storageName": "[variables('fileShareName')]", + "storageType": "AzureFile" + } + ], "scale": { "minReplicas": "[parameters('minReplicas')]", "maxReplicas": "[parameters('maxReplicas')]" @@ -497,7 +571,8 @@ "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}-jobs', parameters('name')))]", "[resourceId('Microsoft.ServiceBus/namespaces', format('{0}-{1}', parameters('name'), variables('collisionSuffix')))]", "[resourceId('Microsoft.Sql/servers', format('{0}-{1}', parameters('name'), variables('collisionSuffix')))]", - "[resourceId('Microsoft.Sql/servers/databases', format('{0}-{1}', parameters('name'), variables('collisionSuffix')), parameters('name'))]" + "[resourceId('Microsoft.Sql/servers/databases', format('{0}-{1}', parameters('name'), variables('collisionSuffix')), parameters('name'))]", + "[resourceId('Microsoft.Storage/storageAccounts', format('{0}{1}', parameters('name'), variables('collisionSuffix')))]" ] }, { @@ -560,7 +635,7 @@ }, "webhookEndpoint": { "type": "string", - "value": "[format('https://{0}/webhooks/azure', reference(resourceId('Microsoft.App/containerApps', parameters('name')), '2022-10-01').configuration.ingress.fqdn)]" + "value": "[format('https://{0}/webhooks/azure', reference(resourceId('Microsoft.App/containerApps', parameters('name')), '2023-05-01').configuration.ingress.fqdn)]" }, "notificationsPassword": { "type": "string", diff --git a/updater/lib/dependabot/.gitkeep b/updater/lib/dependabot/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/updater/lib/dependabot/environment.rb b/updater/lib/dependabot/environment.rb new file mode 100644 index 00000000..135a4ae9 --- /dev/null +++ b/updater/lib/dependabot/environment.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module Dependabot + module Environment + def self.job_id + @job_id ||= environment_variable("DEPENDABOT_JOB_ID") + end + + def self.job_token + @job_token ||= environment_variable("DEPENDABOT_JOB_TOKEN") + end + + def self.debug_enabled? + @debug_enabled ||= job_debug_enabled? || environment_debug_enabled? + end + + def self.log_level + debug_enabled? ? :debug : :info + end + + def self.api_url + @api_url ||= environment_variable("DEPENDABOT_API_URL", "http://localhost:3001") + end + + def self.job_path + @job_path ||= environment_variable("DEPENDABOT_JOB_PATH") + end + + def self.output_path + @output_path ||= environment_variable("DEPENDABOT_OUTPUT_PATH") + end + + def self.repo_contents_path + @repo_contents_path ||= environment_variable("DEPENDABOT_REPO_CONTENTS_PATH", nil) + end + + def self.github_actions? + @github_actions ||= environment_variable("GITHUB_ACTIONS", false) + end + + def self.deterministic_updates? + @deterministic_updates ||= environment_variable("UPDATER_DETERMINISTIC", false) + end + + def self.job_definition + @job_definition ||= JSON.parse(File.read(job_path)) + end + + private_class_method def self.environment_variable(variable_name, default = :_undefined) + return ENV.fetch(variable_name, default) unless default == :_undefined + + ENV.fetch(variable_name) do + raise ArgumentError, "Missing environment variable #{variable_name}" + end + end + + private_class_method def self.job_debug_enabled? + !!job_definition.dig("job", "debug") + end + + private_class_method def self.environment_debug_enabled? + !!environment_variable("DEPENDABOT_DEBUG", false) + end + end +end diff --git a/updater/spec/dependabot/environment_spec.rb b/updater/spec/dependabot/environment_spec.rb new file mode 100644 index 00000000..4d9f9673 --- /dev/null +++ b/updater/spec/dependabot/environment_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "spec_helper" + +require "dependabot/environment" + +RSpec.describe Dependabot::Environment do + subject(:environment) { described_class } + + describe "::debug_enabled?" do + after do + # Reset the memoisation after each test + environment.remove_instance_variable(:@debug_enabled) + end + + it "is false by default" do + allow(environment).to receive(:job_definition).and_return({}) + allow(ENV).to receive(:fetch).with("DEPENDABOT_DEBUG", false).and_return(false) + + expect(environment).not_to be_debug_enabled + end + + it "is true if enabled in ENV" do + allow(environment).to receive(:job_definition).and_return({}) + allow(ENV).to receive(:fetch).with("DEPENDABOT_DEBUG", false).and_return("true") + + expect(environment).to be_debug_enabled + end + + it "is true if enabled in the job definition" do + allow(environment).to receive(:job_definition).and_return({ + "job" => { "debug" => true } + }) + allow(ENV).to receive(:fetch).with("DEPENDABOT_DEBUG", false).and_return(false) + + expect(environment).to be_debug_enabled + end + end +end diff --git a/updater/spec/spec_helper.rb b/updater/spec/spec_helper.rb index caf67191..1a085708 100644 --- a/updater/spec/spec_helper.rb +++ b/updater/spec/spec_helper.rb @@ -20,8 +20,9 @@ require "logger" require "vcr" require "webmock/rspec" +require "yaml" -# TODO: Stop rescuing StandardError in Dependabot::BaseJob#run +# TODO: Stop rescuing StandardError in Dependabot::BaseCommand#run # # For now we log errors as these can surface exceptions that currently get rescued # in integration tests.