Skip to content

Commit cee44f8

Browse files
authored
Introduce a pipeline to automatically bump decoupled local dependencies after publish. (#5661)
* Add AzDO pipeline to bump decoupled local dependencies after publish * Fix GitHub PR step to show API error responses on failure * Use service connection credentials from git config for GitHub API calls
1 parent 73b6913 commit cee44f8

2 files changed

Lines changed: 340 additions & 0 deletions

File tree

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
parameters:
2+
- name: delayMinutes
3+
displayName: 'Minutes to wait for packages to propagate before running'
4+
type: number
5+
default: 5
6+
7+
name: 'Post-publish $(Date:yyyyMMdd).$(Rev:r) (triggered by $(resources.triggeringAlias))'
8+
9+
variables:
10+
- name: FORCE_COLOR
11+
value: 1
12+
13+
# This pipeline is triggered only by pipeline resources (npm publish pipelines),
14+
# not by CI pushes or PR builds.
15+
trigger: none
16+
pr: none
17+
18+
resources:
19+
pipelines:
20+
- pipeline: npmPublish
21+
source: 'rushstack NPM Publish'
22+
trigger:
23+
enabled: true
24+
branches:
25+
include:
26+
- refs/heads/main
27+
- pipeline: npmPublishRush
28+
source: 'rushstack NPM Publish (rush)'
29+
trigger:
30+
enabled: true
31+
branches:
32+
include:
33+
- refs/heads/main
34+
repositories:
35+
- repository: 1esPipelines
36+
type: git
37+
name: 1ESPipelineTemplates/1ESPipelineTemplates
38+
ref: refs/tags/release
39+
- repository: rushstackWebsites
40+
type: github
41+
name: microsoft/rushstack-websites
42+
endpoint: GitHubProjects
43+
ref: refs/heads/main
44+
45+
extends:
46+
template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines
47+
parameters:
48+
sdl:
49+
sourceRepositoriesToScan:
50+
exclude:
51+
- repository: rushstackWebsites
52+
pool:
53+
name: Azure-Pipelines-1ESPT-ExDShared
54+
os: windows
55+
stages:
56+
# ──────────────────────────────────────────────────────────────────────────
57+
# Stage 0: Wait for packages to propagate to the npm registry
58+
# ──────────────────────────────────────────────────────────────────────────
59+
- stage: WaitForPropagation
60+
displayName: 'Wait for npm propagation'
61+
jobs:
62+
- job:
63+
displayName: 'Delay'
64+
pool: server
65+
timeoutInMinutes: 120
66+
steps:
67+
- task: Delay@1
68+
displayName: 'Wait ${{ parameters.delayMinutes }} minute(s)'
69+
inputs:
70+
delayForMinutes: '${{ parameters.delayMinutes }}'
71+
72+
# ──────────────────────────────────────────────────────────────────────────
73+
# Stage 1: Bump decoupled local dependencies
74+
# ──────────────────────────────────────────────────────────────────────────
75+
- stage: BumpDecoupledDeps
76+
displayName: 'Bump decoupled local dependencies'
77+
dependsOn: WaitForPropagation
78+
variables:
79+
BranchName: 'automated/bump-decoupled-deps'
80+
CommitMessage: 'chore: bump decoupled local dependencies'
81+
jobs:
82+
- job:
83+
displayName: 'Bump decoupled dependencies and create PR'
84+
pool:
85+
name: publish-rushstack
86+
os: linux
87+
steps:
88+
- checkout: self
89+
persistCredentials: true
90+
91+
- template: /common/config/azure-pipelines/templates/install-node.yaml@self
92+
93+
- script: 'git config --local user.email rushbot@users.noreply.github.com'
94+
displayName: 'git config email'
95+
96+
- script: 'git config --local user.name Rushbot'
97+
displayName: 'git config name'
98+
99+
- script: 'node common/scripts/install-run-rush.js install --to repo-toolbox'
100+
displayName: 'Rush Install'
101+
102+
- script: 'node common/scripts/install-run-rush.js build --to repo-toolbox --verbose'
103+
displayName: 'Rush Build (repo-toolbox)'
104+
105+
- script: 'node repo-scripts/repo-toolbox/lib-commonjs/start.js bump-decoupled-local-dependencies'
106+
displayName: 'Bump decoupled local dependencies'
107+
108+
- script: 'node common/scripts/install-run-rush.js update'
109+
displayName: 'Rush Update'
110+
111+
- bash: |
112+
set -e
113+
114+
if git diff --quiet; then
115+
echo "No changes detected. Skipping commit and PR."
116+
echo "##vso[task.setvariable variable=HasChanges]false"
117+
exit 0
118+
fi
119+
120+
echo "##vso[task.setvariable variable=HasChanges]true"
121+
122+
git checkout -B $(BranchName)
123+
git add --all
124+
git commit -m "$(CommitMessage)"
125+
displayName: 'Commit dependency changes'
126+
127+
- bash: |
128+
set -e
129+
130+
node common/scripts/install-run-rush.js change \
131+
--bulk \
132+
--bump-type none \
133+
--commit-message "chore: generate change files for decoupled dependency bump"
134+
displayName: 'Generate change files'
135+
condition: and(succeeded(), eq(variables.HasChanges, 'true'))
136+
137+
- template: /common/config/azure-pipelines/templates/push-and-create-github-pr.yaml@self
138+
parameters:
139+
BranchName: $(BranchName)
140+
PrTitle: $(CommitMessage)
141+
PrDescription: 'Automated PR to bump decoupled local dependencies to the latest published versions.'
142+
143+
# ──────────────────────────────────────────────────────────────────────────
144+
# Stage 2: Update API documentation on rushstack-websites
145+
# ──────────────────────────────────────────────────────────────────────────
146+
- stage: UpdateApiDocs
147+
displayName: 'Update API documentation'
148+
dependsOn: WaitForPropagation
149+
variables:
150+
BranchName: 'automated/update-api-docs'
151+
CommitMessage: 'docs: update API documentation'
152+
jobs:
153+
- job:
154+
displayName: 'Update API docs and create PR'
155+
pool:
156+
name: publish-rushstack
157+
os: linux
158+
steps:
159+
- checkout: rushstackWebsites
160+
persistCredentials: true
161+
162+
- template: /common/config/azure-pipelines/templates/install-node.yaml@self
163+
parameters:
164+
NodeMajorVersion: 24
165+
166+
# Build the custom Docusaurus plugin for api-documenter in the
167+
# rushstack-websites repo.
168+
- script: 'node common/scripts/install-run-rush.js install'
169+
displayName: 'Rush Install (rushstack-websites)'
170+
171+
- script: 'node common/scripts/install-run-rush.js build --to-except api.rushstack.io --verbose'
172+
displayName: 'Rush Build to-except api.rushstack.io (rushstack-websites)'
173+
174+
# Download the api artifact from the triggering publish pipeline.
175+
# AzDO automatically resolves which pipeline resource triggered this run.
176+
- task: DownloadPipelineArtifact@2
177+
displayName: 'Download API review files'
178+
inputs:
179+
source: specific
180+
project: GitHubProjects
181+
pipeline: 'rushstack NPM Publish'
182+
runVersion: latest
183+
artifact: api
184+
path: $(Pipeline.Workspace)/api
185+
186+
# Run api-documenter with the Docusaurus plugin from the
187+
# api.rushstack.io project directory so it picks up
188+
# config/api-documenter.json.
189+
- script: 'npx @microsoft/api-documenter@latest generate --input-folder $(Pipeline.Workspace)/api --output-folder ./docs/pages'
190+
displayName: 'Generate API documentation'
191+
workingDirectory: websites/api.rushstack.io
192+
193+
# Update the API docs folder in rushstack-websites and commit.
194+
- bash: |
195+
set -e
196+
197+
git config --local user.email rushbot@users.noreply.github.com
198+
git config --local user.name Rushbot
199+
200+
# Move the generated nav data file to the expected location.
201+
mv websites/api.rushstack.io/docs/api_nav.json websites/api.rushstack.io/data/api_nav.json
202+
203+
# Check for changes (tracked and untracked)
204+
if git diff --quiet && [ -z "$(git ls-files --others --exclude-standard)" ]; then
205+
echo "No API documentation changes detected."
206+
echo "##vso[task.setvariable variable=HasChanges]false"
207+
exit 0
208+
fi
209+
210+
echo "##vso[task.setvariable variable=HasChanges]true"
211+
212+
git checkout -B $(BranchName)
213+
git add --all
214+
git commit -m "$(CommitMessage)"
215+
displayName: 'Update API docs and commit'
216+
217+
- template: /common/config/azure-pipelines/templates/push-and-create-github-pr.yaml@self
218+
parameters:
219+
BranchName: $(BranchName)
220+
PrTitle: $(CommitMessage)
221+
PrDescription: 'Automated PR to update API reference documentation from the latest published packages.'
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
parameters:
2+
- name: BranchName
3+
type: string
4+
- name: PrTitle
5+
type: string
6+
- name: PrDescription
7+
type: string
8+
default: ''
9+
- name: TargetBranch
10+
type: string
11+
default: 'main'
12+
- name: HasChangesVariableName
13+
type: string
14+
default: 'HasChanges'
15+
- name: WorkingDirectory
16+
type: string
17+
default: '$(Build.SourcesDirectory)'
18+
19+
steps:
20+
# Force-push the branch. This is safe because the branch (e.g. "automated/bump-decoupled-deps")
21+
# is exclusively owned by this pipeline and is never manually committed to.
22+
- bash: |
23+
set -e
24+
git push origin ${{ parameters.BranchName }} --force
25+
displayName: 'Push branch'
26+
condition: and(succeeded(), eq(variables['${{ parameters.HasChangesVariableName }}'], 'true'))
27+
workingDirectory: ${{ parameters.WorkingDirectory }}
28+
29+
- bash: |
30+
set -e
31+
32+
# ── Resolve the GitHub owner/repo from the git remote URL ──
33+
# Handles both HTTPS (https://github.com/owner/repo.git) and SSH (git@github.com:owner/repo.git) URLs.
34+
REPO_SLUG=$(git remote get-url origin | sed -E 's#.*github\.com[:/](.+/[^.]+)(\.git)?$#\1#')
35+
echo "Repository: ${REPO_SLUG}"
36+
OWNER=$(echo "${REPO_SLUG}" | cut -d/ -f1)
37+
38+
# ── Extract credentials from the AzDO-managed git config ──
39+
# When "persistCredentials: true" is set on the checkout step, AzDO injects an
40+
# "http.<url>.extraheader" git config entry containing an "AUTHORIZATION: basic <token>"
41+
# header for the GitHub service connection. We reuse this for GitHub API calls so that
42+
# no additional secrets or PATs need to be configured.
43+
AUTH_HEADER=$(git config --get-regexp 'http\..*\.extraheader' | head -1 | sed 's/^[^ ]* //')
44+
if [ -z "$AUTH_HEADER" ]; then
45+
echo "##[error]Could not extract authorization header from git config. Ensure persistCredentials is enabled on the checkout step."
46+
exit 1
47+
fi
48+
49+
# ── Write credentials to a temporary curl config file ──
50+
# This avoids passing the auth token as a command-line argument, which would be
51+
# visible in process listings (e.g. "ps aux") and could leak into logs.
52+
CURL_CONFIG=$(mktemp)
53+
trap 'rm -f "$CURL_CONFIG"' EXIT
54+
echo "-H \"${AUTH_HEADER}\"" > "$CURL_CONFIG"
55+
echo '-H "Accept: application/vnd.github+json"' >> "$CURL_CONFIG"
56+
57+
API_BASE="https://api.github.com/repos/${REPO_SLUG}"
58+
59+
# ── GitHub API helper ──
60+
# Calls the GitHub API using the temporary curl config file for auth headers.
61+
# On success (2xx), prints the response body to stdout.
62+
# On failure, prints the HTTP status and error body to stderr and returns non-zero.
63+
github_api() {
64+
local RESPONSE HTTP_CODE BODY
65+
RESPONSE=$(curl -s -w "\n%{http_code}" -K "$CURL_CONFIG" "$@")
66+
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
67+
BODY=$(echo "$RESPONSE" | sed '$d')
68+
69+
if [[ "$HTTP_CODE" -ge 200 && "$HTTP_CODE" -lt 300 ]]; then
70+
echo "$BODY"
71+
else
72+
echo "##[error]GitHub API returned HTTP ${HTTP_CODE}:" >&2
73+
echo "$BODY" >&2
74+
return 1
75+
fi
76+
}
77+
78+
# ── Check for an existing open PR from this branch ──
79+
# The GitHub "List pull requests" API filters by "head=OWNER:BRANCH" to find any
80+
# open PR already targeting this branch. If one exists, we update it instead of
81+
# creating a duplicate.
82+
EXISTING_PR=$(github_api \
83+
"${API_BASE}/pulls?head=${OWNER}:${{ parameters.BranchName }}&state=open" \
84+
| jq '.[0].number // empty')
85+
86+
if [ -n "$EXISTING_PR" ]; then
87+
# ── Update existing PR ──
88+
# Only the description is updated; the title is left as-is since the branch was
89+
# already force-pushed with the new commits above.
90+
echo "Updating existing PR #${EXISTING_PR}"
91+
github_api -X PATCH \
92+
"${API_BASE}/pulls/${EXISTING_PR}" \
93+
-d "$(jq -n --arg body "$PR_BODY" '{body: $body}')" > /dev/null
94+
else
95+
# ── Create new PR ──
96+
# jq --arg safely handles JSON escaping of the title and body, so special
97+
# characters (quotes, newlines, etc.) in the parameter values are safe.
98+
echo "Creating new PR"
99+
github_api -X POST \
100+
"${API_BASE}/pulls" \
101+
-d "$(jq -n \
102+
--arg title "$PR_TITLE" \
103+
--arg body "$PR_BODY" \
104+
--arg head "${{ parameters.BranchName }}" \
105+
--arg base "${{ parameters.TargetBranch }}" \
106+
'{title: $title, body: $body, head: $head, base: $base}')" > /dev/null
107+
fi
108+
displayName: 'Create or update GitHub PR'
109+
condition: and(succeeded(), eq(variables['${{ parameters.HasChangesVariableName }}'], 'true'))
110+
# Pass PR title and description as environment variables rather than using
111+
# ${{ }} template expansion inside the script. Template expansion would
112+
# substitute the raw string into the Bash source code, which breaks if the
113+
# value contains quotes or other shell metacharacters. Environment variables
114+
# are set by the AzDO agent outside of the shell, so they are safe regardless
115+
# of content.
116+
workingDirectory: ${{ parameters.WorkingDirectory }}
117+
env:
118+
PR_TITLE: ${{ parameters.PrTitle }}
119+
PR_BODY: ${{ parameters.PrDescription }}

0 commit comments

Comments
 (0)