diff --git a/docs/updater.md b/docs/updater.md index 738b807a..03dc43b4 100644 --- a/docs/updater.md +++ b/docs/updater.md @@ -89,13 +89,12 @@ To run the script, some environment variables are required. |DEPENDABOT_TARGET_BRANCH|Update,
vNext|**_Optional_**. The branch to be targeted when creating a pull request. When not specified, Dependabot will resolve the default branch of the repository.| |DEPENDABOT_VERSIONING_STRATEGY|Update,
vNext|**_Optional_**. The versioning strategy to use. See [official docs](https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/configuration-options-for-dependency-updates#versioning-strategy) for the allowed values| |DEPENDABOT_OPEN_PULL_REQUESTS_LIMIT|Update,
vNext|**_Optional_**. The maximum number of open pull requests to have at any one time. Defaults to 5. Setting to 0 implies security only updates.| -|DEPENDABOT_SECURITY_UPDATES_ONLY|vNext|**_Optional_**. If `true`, only security updates will be processed. Can be used in combination `DEPENDABOT_OPEN_PULL_REQUESTS_LIMIT` to exclusively perform security updates whilst also limiting the total number of security PRs opened at once.| |DEPENDABOT_SECURITY_ADVISORIES_FILE|Update,
vNext|**_Optional_**. The absolute file path containing additional user-defined security advisories in JSON format. For example: `/mnt/security_advisories/nuget-2022-12-13.json`| |DEPENDABOT_EXTRA_CREDENTIALS|Update,
vNext|**_Optional_**. The extra credentials in JSON format. Extra credentials can be used to access private NuGet feeds, docker registries, maven repositories, etc. For example a private registry authentication (For example FontAwesome Pro: `[{"type":"npm_registry","token":"","registry":"npm.fontawesome.com"}]`)| |DEPENDABOT_ALLOW_CONDITIONS|Update,
vNext|**_Optional_**. The dependencies whose updates are allowed, in JSON format. This can be used to control which packages can be updated. For example: `[{\"dependency-name\":"django*",\"dependency-type\":\"direct\"}]`. See [official docs](https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/configuration-options-for-dependency-updates#allow) for more.| |DEPENDABOT_IGNORE_CONDITIONS|Update,
vNext|**_Optional_**. The dependencies to be ignored, in JSON format. This can be used to control which packages can be updated. For example: `[{\"dependency-name\":\"express\",\"versions\":[\"4.x\",\"5.x\"]}]`. See [official docs](https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/configuration-options-for-dependency-updates#ignore) for more.| |DEPENDABOT_EXCLUDE_REQUIREMENTS_TO_UNLOCK|Update|**_Optional_**. Exclude certain dependency updates requirements. See list of allowed values [here](https://github.com/dependabot/dependabot-core/issues/600#issuecomment-407808103). Useful if you have lots of dependencies and the update script too slow. The values provided are space-separated. Example: `own all` to only use the `none` version requirement.| -|DEPENDABOT_VENDOR_DEPENDENCIES|vNext|**_Optional_** Determines if dependencies are vendored when updating them. Don't use this option if you're using `gomod` as Dependabot automatically detects vendoring. See [official docs](https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#vendor) for more.| +|DEPENDABOT_VENDOR|vNext|**_Optional_** Determines if dependencies are vendored when updating them. Don't use this option if you're using `gomod` as Dependabot automatically detects vendoring. See [official docs](https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#vendor) for more.| |DEPENDABOT_DEPENDENCY_GROUPS|vNext|**_Optional_**. The dependency group rule mappings, in JSON format. For example: `{"microsoft":{"applies-to":"version-updates","dependency-type":"production","patterns":["microsoft*"],"exclude-patterns":["*azure*"],"update-types":["minor","patch"]}}`. See [official docs](https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#groups) for more. | |DEPENDABOT_UPDATER_OPTIONS|Update,
vNext|**_Optional_**. Comma separated list of updater options (i.e. experiments); available options depend on `PACKAGE_MANAGER`. Example: `goprivate=true,kubernetes_updates=true`.| |DEPENDABOT_REJECT_EXTERNAL_CODE|Update,
vNext|**_Optional_**. Determines if the execution external code is allowed when cloning source repositories. Defaults to `false`.| diff --git a/extension/task/IDependabotConfig.ts b/extension/task/IDependabotConfig.ts index eafa50e1..3951b9a8 100644 --- a/extension/task/IDependabotConfig.ts +++ b/extension/task/IDependabotConfig.ts @@ -26,8 +26,19 @@ export interface IDependabotUpdate { packageEcosystem: string; /** * Location of package manifests. + * https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#directory * */ directory: string; + /** + * Locations of package manifests. + * https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#directories + * */ + directories?: string[]; + /** + * Dependency group rules + * https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#groups + * */ + groups?: string; /** * Customize which updates are allowed. */ diff --git a/extension/task/index.ts b/extension/task/index.ts index eed439ff..b0038cb8 100644 --- a/extension/task/index.ts +++ b/extension/task/index.ts @@ -58,10 +58,13 @@ async function run() { dockerRunner.arg(["-e", `DEPENDABOT_PACKAGE_MANAGER=${update.packageEcosystem}`]); dockerRunner.arg(["-e", `DEPENDABOT_OPEN_PULL_REQUESTS_LIMIT=${update.openPullRequestsLimit}`]); // always has a value - // Set the directory + // Set the directory or directories if (update.directory) { dockerRunner.arg(["-e", `DEPENDABOT_DIRECTORY=${update.directory}`]); } + if (update.directories && update.directories.length > 0) { + dockerRunner.arg(["-e", `DEPENDABOT_DIRECTORIES=${JSON.stringify(update.directories)}`]); + } // Set the target branch if (update.targetBranch) { @@ -111,6 +114,12 @@ async function run() { dockerRunner.arg(["-e", `DEPENDABOT_IGNORE_CONDITIONS=${ignore}`]); } + // Set the dependency groups + let groups = update.groups; + if (groups) { + dockerRunner.arg(["-e", `DEPENDABOT_DEPENDENCY_GROUPS=${groups}`]); + } + // Set the commit message options let commitMessage = update.commitMessage; if (commitMessage) { @@ -164,6 +173,11 @@ async function run() { dockerRunner.arg(["-e", 'DEPENDABOT_SKIP_PULL_REQUESTS=true']); } + // Set skip pull requests if true + if (variables.commentPullRequests === true) { + dockerRunner.arg(["-e", 'DEPENDABOT_COMMENT_PULL_REQUESTS=true']); + } + // Set abandon Unwanted pull requests if true if (variables.abandonUnwantedPullRequests === true) { dockerRunner.arg(["-e", 'DEPENDABOT_CLOSE_PULL_REQUESTS=true']); @@ -230,6 +244,11 @@ async function run() { } } + // Set debug + if (variables.debug === true) { + dockerRunner.arg(["-e", 'DEPENDABOT_DEBUG=true']); + } + // Add in extra environment variables variables.extraEnvironmentVariables.forEach(extraEnvVar => { dockerRunner.arg(["-e", extraEnvVar]); @@ -247,7 +266,7 @@ async function run() { dockerRunner.arg(dockerImage); // set the script to be run - dockerRunner.arg('update_script'); + dockerRunner.arg(variables.command); // Now execute using docker await dockerRunner.exec(); diff --git a/extension/task/task.json b/extension/task/task.json index bb9a553f..d744bbe5 100644 --- a/extension/task/task.json +++ b/extension/task/task.json @@ -12,7 +12,7 @@ "demands": ["docker"], "version": { "Major": 1, - "Minor": 5, + "Minor": 6, "Patch": 0 }, "instanceNameFormat": "Dependabot", @@ -20,12 +20,17 @@ "groups": [ { "name": "security_updates", - "displayName": "Security advisories, vulnerabilities, and updates.", + "displayName": "Security advisories and vulnerabilities", "isExpanded": false }, { - "name": "approval_completion", - "displayName": "Auto Approval and Auto Completion or PRs", + "name": "pull_requests", + "displayName": "Pull request options", + "isExpanded": false + }, + { + "name": "devops", + "displayName": "Azure DevOps authentication", "isExpanded": false }, { @@ -40,6 +45,16 @@ } ], "inputs": [ + { + "name": "useUpdateScriptvNext", + "type": "boolean", + "groupName": "advanced", + "label": "Use latest update script (vNext) (Experimental)", + "defaultValue": "false", + "required": false, + "helpMarkDown": "Determines if the task will use the newest 'vNext' update script instead of the default update script. This Defaults to `false`. See the [vNext update script documentation](https://github.com/tinglesoftware/dependabot-azure-devops/pull/1186) for more information." + }, + { "name": "useConfigFile", "type": "boolean", @@ -52,35 +67,45 @@ { "name": "failOnException", "type": "boolean", - "label": "Determines if the execution should fail when an exception occurs. Defaults to `true`", + "groupName": "advanced", + "label": "Fail task when an update exception occurs.", "defaultValue": true, "required": false, - "helpMarkDown": "When set to true, a failure in updating a single dependency will cause the container execution to fail thereby causing the task to fail. This is important when you want a single failure to prevent trying to update other dependencies." + "helpMarkDown": "When set to `true`, a failure in updating a single dependency will cause the container execution to fail thereby causing the task to fail. This is important when you want a single failure to prevent trying to update other dependencies." }, + { "name": "skipPullRequests", "type": "boolean", - "groupName": "advanced", - "label": "Whether to skip creation and updating of pull requests.", + "groupName": "pull_requests", + "label": "Skip creation and updating of pull requests.", "defaultValue": false, "required": false, "helpMarkDown": "When set to `true` the logic to update the dependencies is executed but the actual Pull Requests are not created/updated. Defaults to `false`." }, + { + "name": "commentPullRequests", + "type": "boolean", + "groupName": "pull_requests", + "label": "Comment on abandoned pull requests with close reason.", + "defaultValue": false, + "required": false, + "helpMarkDown": "When set to `true` a comment will be added to abandoned pull requests explanating why it was closed. Defaults to `false`." + }, { "name": "abandonUnwantedPullRequests", "type": "boolean", - "groupName": "advanced", - "label": "Whether to abandon unwanted pull requests.", + "groupName": "pull_requests", + "label": "Abandon unwanted pull requests.", "defaultValue": false, "required": false, "helpMarkDown": "When set to `true` pull requests that are no longer needed are closed at the tail end of the execution. Defaults to `false`." }, - { "name": "setAutoComplete", "type": "boolean", - "groupName": "approval_completion", - "label": "Determines if the pull requests that dependabot creates should have auto complete set.", + "groupName": "pull_requests", + "label": "Auto-complete pull requests when all policies pass", "defaultValue": false, "required": false, "helpMarkDown": "When set to `true`, pull requests that pass all policies will be merged automatically. Defaults to `false`." @@ -88,7 +113,7 @@ { "name": "mergeStrategy", "type": "pickList", - "groupName": "approval_completion", + "groupName": "pull_requests", "label": "Merge Strategy", "defaultValue": "squash", "required": true, @@ -104,17 +129,18 @@ { "name": "autoCompleteIgnoreConfigIds", "type": "string", - "groupName": "approval_completion", + "groupName": "pull_requests", "label": "Semicolon delimited list of any policy configuration IDs which auto-complete should not wait for.", "defaultValue": "", "required": false, - "helpMarkDown": "A semicolon (`;`) delimited list of any policy configuration IDs which auto-complete should not wait for. Only applies to optional policies (isBlocking == false). Auto-complete always waits for required policies (isBlocking == true)." + "helpMarkDown": "A semicolon (`;`) delimited list of any policy configuration IDs which auto-complete should not wait for. Only applies to optional policies (isBlocking == false). Auto-complete always waits for required policies (isBlocking == true).", + "visibleRule": "setAutoComplete=true" }, { "name": "autoApprove", "type": "boolean", - "groupName": "approval_completion", - "label": "Determines if the pull requests that dependabot creates should be automatically approved.", + "groupName": "pull_requests", + "label": "Auto-approve pull requests", "defaultValue": false, "required": false, "helpMarkDown": "When set to `true`, pull requests will automatically be approved by the specified user. Defaults to `false`." @@ -122,7 +148,7 @@ { "name": "autoApproveUserToken", "type": "string", - "groupName": "approval_completion", + "groupName": "pull_requests", "label": "A personal access token of the user that should approve the PR.", "defaultValue": "", "required": false, @@ -161,7 +187,7 @@ { "name": "azureDevOpsServiceConnection", "type": "connectedService:Externaltfs", - "groupName": "advanced", + "groupName": "devops", "label": "Azure DevOps Service Connection to use.", "required": false, "helpMarkDown": "Specify a service connection to use, if you want to use a different service principal than the default to create your PRs." @@ -169,7 +195,7 @@ { "name": "azureDevOpsAccessToken", "type": "string", - "groupName": "advanced", + "groupName": "devops", "label": "Azure DevOps Personal Access Token.", "required": false, "helpMarkDown": "The Personal Access Token for accessing Azure DevOps repositories. Supply a value here to avoid using permissions for the Build Service either because you cannot change its permissions or because you prefer that the Pull Requests be done by a different user. Use this in place of `azureDevOpsServiceConnection` such as when it is not possible to create a service connection." @@ -195,9 +221,9 @@ "name": "updaterOptions", "type": "string", "groupName": "advanced", - "label": "Comma separated list of updater options.", + "label": "Comma separated list of Dependabot experiments (updater options).", "required": false, - "helpMarkDown": "Set a list of updater options in CSV format. Available options depend on the ecosystem. Example: `goprivate=true,kubernetes_updates=true`." + "helpMarkDown": "Set a list of Dependabot experiments (updater options) in CSV format. Available options depend on the ecosystem. Example: `goprivate=true,kubernetes_updates=true`." }, { "name": "excludeRequirementsToUnlock", diff --git a/extension/task/utils/getSharedVariables.ts b/extension/task/utils/getSharedVariables.ts index 5dab8d86..996c95ed 100644 --- a/extension/task/utils/getSharedVariables.ts +++ b/extension/task/utils/getSharedVariables.ts @@ -51,21 +51,32 @@ export interface ISharedVariables { excludeRequirementsToUnlock: string; updaterOptions: string; + /** Determines if verbose log messages are logged */ + debug: boolean; + /** List of update identifiers to run */ targetUpdateIds: number[]; securityAdvisoriesFile: string | undefined; + /** Determines whether to skip creating/updating pull requests */ skipPullRequests: boolean; + /** Determines whether to comment on pull requests which an explanation of the reason for closing */ + commentPullRequests: boolean; /** Determines whether to abandon unwanted pull requests */ abandonUnwantedPullRequests: boolean; + /** List of extra environment variables */ extraEnvironmentVariables: string[]; + /** Flag used to forward the host ssh socket */ forwardHostSshSocket: boolean; /** Tag of the docker image to be pulled */ dockerImageTag: string; + + /** Dependabot command to run */ + command: string; } /** @@ -74,7 +85,6 @@ export interface ISharedVariables { * @returns shared variables */ export default function getSharedVariables(): ISharedVariables { - // Prepare shared variables let organizationUrl = tl.getVariable("System.TeamFoundationCollectionUri"); //convert url string into a valid JS URL object let formattedOrganizationUrl = new URL(organizationUrl); @@ -119,6 +129,8 @@ export default function getSharedVariables(): ISharedVariables { tl.getInput("excludeRequirementsToUnlock") || ""; let updaterOptions = tl.getInput("updaterOptions"); + let debug: boolean = tl.getVariable("System.Debug")?.localeCompare("true") === 0; + // Get the target identifiers let targetUpdateIds = tl .getDelimitedInput("targetUpdateIds", ";", false) @@ -129,12 +141,15 @@ export default function getSharedVariables(): ISharedVariables { "securityAdvisoriesFile" ); let skipPullRequests: boolean = tl.getBoolInput("skipPullRequests", false); + let commentPullRequests: boolean = tl.getBoolInput("commentPullRequests", false); let abandonUnwantedPullRequests: boolean = tl.getBoolInput("abandonUnwantedPullRequests", true); + let extraEnvironmentVariables = tl.getDelimitedInput( "extraEnvironmentVariables", ";", false ); + let forwardHostSshSocket: boolean = tl.getBoolInput( "forwardHostSshSocket", false @@ -143,9 +158,12 @@ export default function getSharedVariables(): ISharedVariables { // Prepare variables for the docker image to use let dockerImageTag: string = getDockerImageTag(); + let command: string = tl.getBoolInput("useUpdateScriptvNext", false) + ? "update_script_vnext" + : "update_script"; + return { organizationUrl: formattedOrganizationUrl, - protocol, hostname, port, @@ -169,14 +187,22 @@ export default function getSharedVariables(): ISharedVariables { failOnException, excludeRequirementsToUnlock, updaterOptions, + + debug, targetUpdateIds, securityAdvisoriesFile, + skipPullRequests, + commentPullRequests, abandonUnwantedPullRequests, + extraEnvironmentVariables, + forwardHostSshSocket, dockerImageTag, + + command }; } diff --git a/extension/task/utils/parseConfigFile.ts b/extension/task/utils/parseConfigFile.ts index 8fca4117..71c6917a 100644 --- a/extension/task/utils/parseConfigFile.ts +++ b/extension/task/utils/parseConfigFile.ts @@ -165,6 +165,7 @@ function parseUpdates(config: any): IDependabotUpdate[] { var dependabotUpdate: IDependabotUpdate = { packageEcosystem: update["package-ecosystem"], directory: update["directory"], + directories: update["directories"] || [], openPullRequestsLimit: update["open-pull-requests-limit"], registries: update["registries"] || [], @@ -197,6 +198,9 @@ function parseUpdates(config: any): IDependabotUpdate[] { commitMessage: update["commit-message"] ? JSON.stringify(update["commit-message"]) : undefined, + groups: update["groups"] + ? JSON.stringify(update["groups"]) + : undefined, }; if (!dependabotUpdate.packageEcosystem) { @@ -213,9 +217,9 @@ function parseUpdates(config: any): IDependabotUpdate[] { dependabotUpdate.openPullRequestsLimit = 5; } - if (!dependabotUpdate.directory) { + if (!dependabotUpdate.directory && dependabotUpdate.directories.length === 0) { throw new Error( - "The value 'directory' in dependency update config is missing" + "The values 'directory' and 'directories' in dependency update config is missing, you must specify at least one" ); } diff --git a/extension/tests/utils/dependabot.yml b/extension/tests/utils/dependabot.yml index 9376eeb7..35277f63 100644 --- a/extension/tests/utils/dependabot.yml +++ b/extension/tests/utils/dependabot.yml @@ -24,6 +24,17 @@ updates: update-types: ['version-update:semver-major'] - dependency-name: '@types/react-dom' update-types: ['version-update:semver-major'] + - package-ecosystem: 'nuget' + directories: + - '/src/client' + - '/src/server' + groups: + microsoft: + patterns: + - "microsoft*" + update-types: + - "minor" + - "patch" registries: reg1: type: nuget-feed diff --git a/extension/tests/utils/parseConfigFile.test.ts b/extension/tests/utils/parseConfigFile.test.ts index 64e7448e..4e039cc4 100644 --- a/extension/tests/utils/parseConfigFile.test.ts +++ b/extension/tests/utils/parseConfigFile.test.ts @@ -8,11 +8,12 @@ describe("Parse configuration file", () => { it("Parsing works as expected", () => { let config: any = load(fs.readFileSync('tests/utils/dependabot.yml', "utf-8")); let updates = parseUpdates(config); - expect(updates.length).toBe(2); + expect(updates.length).toBe(3); // first const first = updates[0]; expect(first.directory).toBe('/'); + expect(first.directories).toEqual([]); expect(first.packageEcosystem).toBe('docker'); expect(first.insecureExternalCodeExecution).toBe(undefined); expect(first.registries).toEqual([]); @@ -20,9 +21,17 @@ describe("Parse configuration file", () => { // second const second = updates[1]; expect(second.directory).toBe('/client'); + expect(second.directories).toEqual([]); expect(second.packageEcosystem).toBe('npm'); expect(second.insecureExternalCodeExecution).toBe('deny'); expect(second.registries).toEqual(['reg1', 'reg2']); + + // third + const third = updates[2]; + expect(third.directory).toBe(undefined); + expect(third.directories).toEqual(['/src/client', '/src/server']); + expect(third.packageEcosystem).toBe('nuget'); + expect(third.groups).toBe('{\"microsoft\":{\"patterns\":[\"microsoft*\"],\"update-types\":[\"minor\",\"patch\"]}}'); }); }); diff --git a/updater/lib/tinglesoftware/dependabot/job.rb b/updater/lib/tinglesoftware/dependabot/job.rb index 3296c4d2..f6fd6bbc 100644 --- a/updater/lib/tinglesoftware/dependabot/job.rb +++ b/updater/lib/tinglesoftware/dependabot/job.rb @@ -226,7 +226,7 @@ def _lockfile_only end def _vendor_dependencies - ENV.fetch("DEPENDABOT_VENDOR_DEPENDENCIES", nil) == "true" + ENV.fetch("DEPENDABOT_VENDOR", nil) == "true" end def _dependency_groups @@ -402,9 +402,10 @@ def open_pull_request_limit_reached? def security_updates_only # If the pull request limit is set to zero, we assume that the user just wants security updates + # https://docs.github.com/en/code-security/dependabot/dependabot-security-updates/configuring-dependabot-security-updates#overriding-the-default-behavior-with-a-configuration-file return true if open_pull_requests_limit.zero? - ENV.fetch("DEPENDABOT_SECURITY_UPDATES_ONLY", nil) == "true" + false end def security_advisories