diff --git a/.azuredevops/create-pull-requests.sh b/.azuredevops/create-pull-requests.sh new file mode 100644 index 0000000..cd44753 --- /dev/null +++ b/.azuredevops/create-pull-requests.sh @@ -0,0 +1,126 @@ +#!/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 +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 +} + +# Configure Git Credentials +git config --global user.email "azure@azuredevops.com" +git config --global user.name "Dependabot Standalone" +git config --global advice.detachedHead false + +# Configure the credential helper to store the PAT token in the git-credentials file. +git config --global credential.helper store + +# Configure the git-credentials to use the PAT token from ADO. +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"') + BRANCH_NAME="dependabot/$(echo -n "$COMMIT_MSG" | sha1sum | awk '{print $1}')" + + echo "Processing PR: $PR_TITLE" + echo " Base SHA: $BASE_SHA" + echo " Branch: $BRANCH_NAME" + + # Set the remote URL to the repository using the PAT. + 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 appears to be 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) - adjust options as desired. + # https://learn.microsoft.com/en-us/cli/azure/repos/pr?view=azure-cli-latest#az-repos-pr-create + 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/nuget.yaml b/.azuredevops/dependabot/nuget.yaml new file mode 100644 index 0000000..a483059 --- /dev/null +++ b/.azuredevops/dependabot/nuget.yaml @@ -0,0 +1,37 @@ +job: + package-manager: "nuget" + allowed-updates: + - update-type: all + dependency-groups: + - name: MSNet + applies-to: all + rules: + patterns: + - "Microsoft.*" + - "System.*" + - name: Nuget + applies-to: all + rules: + patterns: + - "*" + experiments: + nuget_generate_simple_pr_body: true + nuget_native_updater: true + nuget_use_direct_discovery: true + nuget_use_new_file_updater: true + ignore-conditions: + - dependency-name: Newtonsoft.Json + commit-message-options: + prefix: "dependabot" + source: + provider: azure + repo: $PROJECT_PATH + directory: '/' +credentials: + - type: git_source + host: dev.azure.com + username: vsts + password: $LOCAL_GITHUB_ACCESS_TOKEN + - type: nuget-feed + url: https://pkgs.dev.azure.com/{MY_ORGANIZATION_NAME}/_packaging/{MY_NUGET_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..94b87c3 --- /dev/null +++ b/.azuredevops/pipelines/example.yaml @@ -0,0 +1,175 @@ +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. invoicecloud/Src/_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: BuildDependabot + jobs: + - job: RunDependabot + pool: + vmImage: 'ubuntu-latest' + steps: + - checkout: self + persistCredentials: true + + # Add .NET + # Parameterize version if desired. + - task: UseDotNet@2 + inputs: + version: '8.0.x' + + # Install GoLang + - script: | + # Install GoLang + wget https://go.dev/dl/go${{ parameters.goVersion }}.src.tar.gz + sudo tar -C /usr/local -xzf ${{ parameters.goVersion }}.src.tar.gz + + # Add GoLang to PATH + echo "export PATH=/usr/local/go/bin:${PATH}" | sudo tee -a $HOME/.profile + source $HOME/.profile + + go version + displayName: Install GoLang + + # Install Dependabot CLI + - script: | + # Install Dependabot CLI + go install github.com/dependabot/cli/cmd/dependabot@${{ parameters.dependabotCliVersion }} + displayName: Install Dependabot CLI + + # Run Dependabot + - script: | + set -euo pipefail + + # Substitute PROJECT_PATH var in the config. + # This doesn't appear to be automatically interpolated in Azure DevOps. + sed -i 's/\$PROJECT_PATH/${PROJECT_PATH}/g' ${{ parameters.dependabotConfigFile }} + + # Print the updated 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" + + ./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) + + # Publish Dependabot Results + - task: PublishPipelineArtifact@1 + displayName: Publish Dependabot Results + inputs: + targetPath: '$(Pipeline.Workspace)/dependabot_result.jsonl' + publishLocation: 'pipeline' + artifactName: 'dependabot_result' + + - stage: CreatePullRequests + jobs: + - job: CreatePullRequests + pool: + vmImage: 'ubuntu-latest' + steps: + # Download Dependabot Results + - task: DownloadPipelineArtifact@2 + 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