diff --git a/.azuredevops/create-pull-requests.sh b/.azuredevops/create-pull-requests.sh new file mode 100644 index 0000000..45b2237 --- /dev/null +++ b/.azuredevops/create-pull-requests.sh @@ -0,0 +1,130 @@ +#!/bin/bash + +# Adapted from https://github.com/dependabot/example-cli-usage/blob/main/create.sh + +# Expected Environment Variables: +#### AZURE_DEVOPS_EXT_PAT: The PAT token for the Azure DevOps organization. +#### PROJECT_PATH: The path to the repository, relative to Azure DevOps. + +# This script takes a jsonl file as input which is the stdout of a Dependabot CLI run. +# It takes the `type: create_pull_request` events and creates a pull request for each of them +# by using git commands. + +# Note at this time there is minimal error handling. +set -euo pipefail + +if [ $# -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +# This script takes the .jsonl file output from the Dependabout CLI as its only param. +INPUT="$1" + +# DEBUG OUTPUT +echo "AZURE_DEVOPS_EXT_PAT: $AZURE_DEVOPS_EXT_PAT" +echo "PROJECT_PATH: $PROJECT_PATH" + +# Function to check if a string is valid Base64 +is_base64() { + local input="$1" + # Remove any whitespace and newlines + input=$(echo "$input" | tr -d ' \t\n\r') + # Check if the string contains only Base64 characters and has proper length + if [[ "$input" =~ ^[A-Za-z0-9+/]*={0,2}$ ]] && [[ $(( ${#input} % 4 )) -eq 0 ]]; then + return 0 + else + return 1 + fi +} + +echo "INPUT: $INPUT" + +# Configure Git Credentials +git config --global user.email "azure@azuredevops.com" +git config --global user.name "Dependabot" +git config --global advice.detachedHead false + +# Set the credential helper to store the PAT token in the git-credentials file. +git config --global credential.helper store + +echo "https://azure:${AZURE_DEVOPS_EXT_PAT}@dev.azure.com" > ~/.git-credentials +git config --global url."https://azure:${AZURE_DEVOPS_EXT_PAT}@dev.azure.com/".insteadOf "https://dev.azure.com/" + +# Parse each create_pull_request event +jq -c 'select(.type == "create_pull_request")' "$INPUT" | while read -r event; do + # Extract fields + BASE_SHA=$(echo "$event" | jq -r '.data."base-commit-sha"') + PR_TITLE=$(echo "$event" | jq -r '.data."pr-title"') + PR_BODY=$(echo "$event" | jq -r '.data."pr-body"') + COMMIT_MSG=$(echo "$event" | jq -r '.data."commit-message"') + HASH=$(echo -n "$COMMIT_MSG" | sha1sum | awk '{print $1}') + + # OPTIONAL: Truncate the SHA to the short-form to avoid long branch names. + # This ensures that branches are deployable without exceeding the 63 char limit in k8s. + # e.g. `dependabot/123456789012` vs `dependabot/12345678901234567890123456789012` + BRANCH_NAME="dependabot/${HASH:0:12}" + + echo "Processing PR: $PR_TITLE" + echo " Base SHA: $BASE_SHA" + echo " Branch: $BRANCH_NAME" + + git remote set-url origin https://${AZURE_DEVOPS_EXT_PAT}@dev.azure.com/${PROJECT_PATH} + + # Create and checkout new branch from base commit + git fetch origin + git checkout "$BASE_SHA" + git switch -c "$BRANCH_NAME" + + # Apply file changes + echo "$event" | jq -c '.data."updated-dependency-files"[]' | while read -r file; do + # Construct file path more safely to ensure it's relative + DIRECTORY=$(echo "$file" | jq -r '.directory // ""') + FILENAME=$(echo "$file" | jq -r '.name') + + # Build relative path, handling empty directory case + if [ -z "$DIRECTORY" ] || [ "$DIRECTORY" = "." ] || [ "$DIRECTORY" = "/" ]; then + FILE_PATH="$FILENAME" + else + # Remove leading slash if present and ensure relative path + DIRECTORY=$(echo "$DIRECTORY" | sed 's#^/##') + FILE_PATH="$DIRECTORY/$FILENAME" + fi + + DELETED=$(echo "$file" | jq -r '.deleted') + if [ "$DELETED" = "true" ]; then + git rm -f "$FILE_PATH" || true + else + mkdir -p "$(dirname "$FILE_PATH")" + + # Get the content + CONTENT=$(echo "$file" | jq -r '.content') + + # Check if content is Base64 encoded + # Note - this is a necessary check - Directory.Packages.props are written out as a Base64 string. + # Other projects using `packages.config` are written out in their original format. + if is_base64 "$CONTENT"; then + # Decode Base64 content before writing to file + echo "$CONTENT" | base64 -d > "$FILE_PATH" + else + # Content is already in plain text, write directly + echo "$CONTENT" > "$FILE_PATH" + fi + + git add "$FILE_PATH" + fi + done + + git commit -m "$COMMIT_MSG" + git push origin "$BRANCH_NAME" + + # Create PR using Azure CLI (az) + az repos pr create \ + --title "$PR_TITLE" --description "$PR_BODY" \ + --target-branch "main" --source-branch "$BRANCH_NAME" \ + --labels dependencies --auto-complete true \ + --delete-source-branch true --squash true || true + + # Return to main branch for next PR + git checkout main +done \ No newline at end of file diff --git a/.azuredevops/dependabot/npm-and-yarn.yaml b/.azuredevops/dependabot/npm-and-yarn.yaml new file mode 100644 index 0000000..dfb4a1f --- /dev/null +++ b/.azuredevops/dependabot/npm-and-yarn.yaml @@ -0,0 +1,73 @@ +job: + package-manager: "npm_and_yarn" + allowed-updates: + - dependency-type: direct + update-type: all + - dependency-type: indirect + update-type: security + dependency-groups: + - name: rsbuild + applies-to: all + rules: + patterns: + - "@rsbuild/*" + - name: module-federation + applies-to: all + rules: + patterns: + - "@module-federation/*" + - name: testing + applies-to: all + rules: + patterns: + - "@testing-library/*" + - "jest" + - "jest-*" + - "ts-jest" + - name: biome + applies-to: all + rules: + patterns: + - "@biomejs/biome" + - name: types + applies-to: all + rules: + patterns: + - "@types/*" + - name: npm + applies-to: all + rules: + patterns: + - "*" + exclude-patterns: + - "@rsbuild/*" + - "@module-federation/*" + - "@testing-library/*" + - "jest" + - "jest-*" + - "ts-jest" + - "@biomejs/biome" + - "@types/*" + ignore-conditions: + - dependency-name: "*" + update-types: + - "version-update:semver-major" + commit-message-options: + prefix: "dependabot" + # Provides feed authentication context to the *updater* + credentials-metadata: + - type: nuget_feed + url: https://pkgs.dev.azure.com/{AZURE_DEVOPS_ORGANIZATION}/_packaging/{AZURE_ARTIFACT_FEED_NAME}/nuget/v3/index.json + token: $LOCAL_AZURE_ACCESS_TOKEN + source: + provider: azure + repo: AZURE_DEVOPS_ORGANIZATION/{{AZURE_DEVOPS_PROJECT}}/_git/{{{GIT_REPO_NAME}}} + directory: '/' +credentials: # Provides git & artifact feed authentication context to the *proxy*. + - type: git_source + host: dev.azure.com + username: vsts + password: $LOCAL_GITHUB_ACCESS_TOKEN # Note - this has to be `LOCAL_GITHUB_ACCESS_TOKEN`. + - type: nuget-feed + url: https://pkgs.dev.azure.com/{AZURE_DEVOPS_ORGANIZATION}/_packaging/{AZURE_ARTIFACT_FEED_NAME}/nuget/v3/index.json + token: $LOCAL_AZURE_ACCESS_TOKEN \ No newline at end of file diff --git a/.azuredevops/dependabot/nuget.yaml b/.azuredevops/dependabot/nuget.yaml new file mode 100644 index 0000000..4b47f55 --- /dev/null +++ b/.azuredevops/dependabot/nuget.yaml @@ -0,0 +1,64 @@ +# This is an example Dependabot configuration file for a .NET project that demonstrates using a private AzDo Artifacts feed. +job: + package-manager: "nuget" + allowed-updates: + - dependency-type: direct + update-type: all + - dependency-type: indirect + update-type: security + dependency-groups: + - name: MSNet + applies-to: all + rules: + patterns: + - "Microsoft.*" + - "System.*" + - name: Nuget + applies-to: all + rules: + patterns: + - "*" + exclude-patterns: + - "Microsoft.*" + - "System.*" + experiments: + nuget_generate_simple_pr_body: true + # If running against older .NET Framework projects, you may need to set all of the below to 'false' + # When these are enabled, the updater installs a newer version of .NET SDK and runs an SDK-based MSBuild task, which crawls the XML in the .csproj/.vbproj files. + # This can cause issues with older .NET Framework projects. + nuget_native_updater: true + nuget_use_direct_discovery: true + nuget_install_dotnet_sdks: true + nuget_use_new_file_updater: true + ignore-conditions: + # Ignores routine version updates, but allows security updates. + - dependency-name: "System.*" + update-types: + - "version-update:semver-major" + - "version-update:semver-minor" + - "version-update:semver-patch" + # Ignore an entire package. + - dependency-name: "Newtonsoft.Json" + # Constrain all updates to minor or patch version increments + - dependency-name: "*" + update-types: + - "version-update:semver-major" + commit-message-options: + prefix: "dependabot" + # Provides feed authentication context to the *updater* + credentials-metadata: + - type: nuget_feed + url: https://pkgs.dev.azure.com/{AZURE_DEVOPS_ORGANIZATION}/_packaging/{AZURE_ARTIFACT_FEED_NAME}/nuget/v3/index.json + token: $LOCAL_AZURE_ACCESS_TOKEN + source: + provider: azure + repo: AZURE_DEVOPS_ORGANIZATION/{{AZURE_DEVOPS_PROJECT}}/_git/{{{GIT_REPO_NAME}}} + directory: '/' +credentials: # Provides git & artifact feed authentication context to the *proxy*. + - type: git_source + host: dev.azure.com + username: vsts + password: $LOCAL_GITHUB_ACCESS_TOKEN # Note - this has to be `LOCAL_GITHUB_ACCESS_TOKEN`. + - type: nuget-feed + url: https://pkgs.dev.azure.com/{AZURE_DEVOPS_ORGANIZATION}/_packaging/{AZURE_ARTIFACT_FEED_NAME}/nuget/v3/index.json + token: $LOCAL_AZURE_ACCESS_TOKEN \ No newline at end of file diff --git a/.azuredevops/dependabot/pip.yaml b/.azuredevops/dependabot/pip.yaml new file mode 100644 index 0000000..09c5df1 --- /dev/null +++ b/.azuredevops/dependabot/pip.yaml @@ -0,0 +1,30 @@ +job: + package-manager: "pip" + allowed-updates: + - dependency-type: direct + update-type: all + - dependency-type: indirect + update-type: security + ignore-conditions: + - dependency-name: "*" + update-types: + - "version-update:semver-major" + commit-message-options: + prefix: "dependabot" + # Provides feed authentication context to the *updater* + credentials-metadata: + - type: nuget_feed + url: https://pkgs.dev.azure.com/{AZURE_DEVOPS_ORGANIZATION}/_packaging/{AZURE_ARTIFACT_FEED_NAME}/nuget/v3/index.json + token: $LOCAL_AZURE_ACCESS_TOKEN + source: + provider: azure + repo: AZURE_DEVOPS_ORGANIZATION/{{AZURE_DEVOPS_PROJECT}}/_git/{{{GIT_REPO_NAME}}} + directory: '/' +credentials: # Provides git & artifact feed authentication context to the *proxy*. + - type: git_source + host: dev.azure.com + username: vsts + password: $LOCAL_GITHUB_ACCESS_TOKEN # Note - this has to be `LOCAL_GITHUB_ACCESS_TOKEN`. + - type: nuget-feed + url: https://pkgs.dev.azure.com/{AZURE_DEVOPS_ORGANIZATION}/_packaging/{AZURE_ARTIFACT_FEED_NAME}/nuget/v3/index.json + token: $LOCAL_AZURE_ACCESS_TOKEN \ No newline at end of file diff --git a/.azuredevops/pipelines/example.yaml b/.azuredevops/pipelines/example.yaml new file mode 100644 index 0000000..cf23515 --- /dev/null +++ b/.azuredevops/pipelines/example.yaml @@ -0,0 +1,206 @@ +parameters: + # Note: The presence of the 3 parameters below provides a mechanism to centralize this pipeline such that it can be run for multiple repositories. + # Parallelization has not been tested using this approach - but it should be easy enough to do. + # See here for more information: https://learn.microsoft.com/en-us/azure/machine-learning/how-to-use-parallel-job-in-pipeline?view=azureml-api-2&tabs=cliv2 + + # By default, this pipeline is configured to run for a single repository. + + # OPTIONAL: Name of the repository. Automatically set to ADO repository name if not provided. + - name: repositoryName + type: string + default: $(Build.Repository.Name) + + # OPTIONAL: Name of the Azure DevOps project. Automatically set to ADO project name if not provided. + - name: azdoProjectName + type: string + default: $(System.TeamProject) + + # OPTIONAL: Path to the repository, relative to Azure DevOps. + # e.g. {AzureDevOpsOrganization}/${ parameters.azdoProjectName }/_git/{Repository.Name} + - name: projectPath + type: string + default: '' + + # OPTIONAL: Path to the Dependabot configuration file. Relative to the repository root. + - name: dependabotConfigFile + type: string + default: "$(Build.SourcesDirectory)/.azuredevops/dependabot-config.yaml" + + # OPTIONAL: Version of GoLang to install. + # See here for other versions: https://go.dev/dl/ + - name: goVersion + type: string + default: '1.24.5' + values: + - '1.24.5' + - '1.23.11' + - '1.25rc2' + + # OPTIONAL: Version of Dependabot CLI to install - defaults to latest. + # See here for other versions: https://github.com/dependabot/cli/releases + - name: dependabotCliVersion + type: string + default: 'latest' + + +trigger: none + +schedules: + - cron: "0 0 * * 0" # Weekly, Sunday Night, Midnight UTC + always: true # Run even if there have been no code changes. + branches: + include: + - main # The branch to run the schedule on. + batch: true + displayName: "Weekly Dependency Update" + +variables: + - name: System.Secrets + value: true + + # Azure DevOps Repository Path + - name: PROJECT_PATH + ${{ if eq(parameters.projectPath, '') }}: + value: 'MY_AZDO_ORGANIZATION_NAME/${{ parameters.azdoProjectName }}/_git/${{ parameters.repositoryName }}' + ${{ else }}: + value: ${{ parameters.projectPath }} + +stages: + - stage: DependabotDependencyUpdate + jobs: + - job: RunDependabot + pool: + vmImage: 'ubuntu-latest' + steps: + - checkout: self + persistCredentials: true + + # Add .NET + # Parameterize version if desired. + - task: UseDotNet@2 + inputs: + version: '10.0.x' + + # For Legacy .NET Framework Projects - use these steps instead. + + # - task: NuGetToolInstaller@1 + # inputs: + # versionSpec: + # checkLatest: true + + # - task: NuGetAuthenticate@1 + # displayName: "NuGet Authenticate" + + # - task: DotNetCoreCLI@2 + # inputs: + # command: 'restore' + # feedsToUse: config + # nugetConfigPath: ${{ parameters.nugetConfigPath }} + # projects: '**/*.sln' + + # For NPM and Yarn + # - task: UseNode@1 + # inputs: + # version: ${{ parameters.nodeVersion }} + + # For Python + # - task: UsePythonVersion@0 + # inputs: + # versionSpec: ${{ parameters.pythonVersion }} + # architecture: 'x64' + + - task: GoTool@0 + displayName: Install GoLang + inputs: + version: ${{ parameters.goVersion }} + + - task: Go@0 # Install Dependabot CLI + displayName: Install Dependabot CLI + inputs: + command: 'install' + arguments: 'github.com/dependabot/cli/cmd/dependabot@${{ parameters.dependabotCliVersion }}' + + # Run Dependabot + - script: | + # Exit if any step fails. + set -e + + # DEBUG OUTPUT: Print the values. + echo "LOCAL_AZURE_ACCESS_TOKEN: $LOCAL_AZURE_ACCESS_TOKEN" + echo "LOCAL_GITHUB_ACCESS_TOKEN: $LOCAL_GITHUB_ACCESS_TOKEN" + echo "PROJECT_PATH: $PROJECT_PATH" + + # Substitute the PROJECT_PATH value in the config file. + # This one does not appear to update automatically when running in Azure DevOps + sed -i "s|\$PROJECT_PATH|${{ parameters.projectPath }}|g" ${{ parameters.dependabotConfigFile }} + + # DEBUG OUTPUT: Print the config file. + echo "Using Dependabot Configuration File:" + cat ${{ parameters.dependabotConfigFile }} + + # Set the GO /bin path & cd into it. + GO_PATH=$(go env | grep GOPATH | awk -F'=' '{print $2}' | tr -d "'") + cd $GO_PATH/bin + + echo "\n dependabot update \ + -f ${{ parameters.dependabotConfigFile }} \ + --timeout 20m >> $(Pipeline.Workspace)/dependabot_result.jsonl || true" + + # Timeout value optional + ./dependabot update \ + -f ${{ parameters.dependabotConfigFile }} \ + --timeout 20m >> $(Pipeline.Workspace)/dependabot_result.jsonl || true + + echo "Result:" + cat $(Pipeline.Workspace)/dependabot_result.jsonl + displayName: Run Dependabot + env: + LOCAL_AZURE_ACCESS_TOKEN: $(System.AccessToken) + LOCAL_GITHUB_ACCESS_TOKEN: $(System.AccessToken) + + - task: PublishPipelineArtifact@1 + displayName: Publish Dependabot Results + inputs: + targetPath: '$(Pipeline.Workspace)/dependabot_result.jsonl' + publishLocation: 'pipeline' + artifact: 'dependabot_result' + + - stage: CreatePullRequests + jobs: + - job: CreatePullRequests + pool: + vmImage: 'ubuntu-latest' + steps: + - checkout: self + persistCredentials: true + clean: true + + # You could also publish the script as an artifact - this just makes it easier to self-contain. + - checkout: templates + + - task: DownloadPipelineArtifact@2 + displayName: Download Dependabot Results File + inputs: + buildType: 'current' + artifactName: 'dependabot_result' + targetPath: $(Build.ArtifactStagingDirectory) + + # Install jq - for parsing JSON. + - script: | + # Install jq + sudo apt-get update + sudo apt-get install -y jq + displayName: Install 'jq' + + # Create Pull Requests + - task: Bash@3 + displayName: Create Pull Requests + inputs: + targetType: 'filePath' + filePath: "../create-pull-requests.sh" + arguments: > + $(Build.ArtifactStagingDirectory)/dependabot_result.jsonl + workingDirectory: $(Build.SourcesDirectory)/$(Build.Repository.Name) + env: + AZURE_DEVOPS_EXT_PAT: $(System.AccessToken) + PROJECT_PATH: $(PROJECT_PATH) \ No newline at end of file