Skip to content

Commit 8707dcd

Browse files
vdiezclaude
andauthored
SCANNPM-133 feat: Automatically create Update Center PR on release (#400)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b22062a commit 8707dcd

File tree

6 files changed

+259
-21
lines changed

6 files changed

+259
-21
lines changed

.github/RELEASE_TEMPLATE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Description:
2+
3+
## What's Changed

.github/workflows/build.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ jobs:
8484
id: secrets
8585
with:
8686
secrets: |
87-
development/artifactory/token/${{ github.repository_owner }}-${{ github.event.repository.name }}-qa-deployer access_token | ARTIFACTORY_DEPLOY_ACCESS_TOKEN;
8887
development/artifactory/token/${{ github.repository_owner }}-${{ github.event.repository.name }}-private-reader access_token | ARTIFACTORY_ACCESS_TOKEN;
8988
9089
- name: Configure npm registry
@@ -94,7 +93,7 @@ jobs:
9493
9594
- name: Run integration tests
9695
env:
97-
ARTIFACTORY_ACCESS_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).ARTIFACTORY_DEPLOY_ACCESS_TOKEN }}
96+
ARTIFACTORY_ACCESS_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).ARTIFACTORY_ACCESS_TOKEN }}
9897
run: npm run test-integration
9998
shell: bash
10099

.github/workflows/release.yml

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ on:
1111
release_tag:
1212
description: 'Release tag to test (e.g., 1.2.3)'
1313
required: true
14+
release_description:
15+
description: 'Release description for Update Center (e.g., "Support new feature")'
16+
required: true
1417
prerelease:
1518
description: 'Mark as prerelease (publishes to npm "next" tag instead of "latest")'
1619
type: boolean
@@ -19,6 +22,10 @@ on:
1922
description: 'Skip updating "latest" tag (use for older version patches)'
2023
type: boolean
2124
default: false
25+
slack_channel:
26+
description: 'Slack channel for release notification'
27+
type: string
28+
default: 'ask-squad-web'
2229

2330
jobs:
2431
publish:
@@ -33,6 +40,33 @@ jobs:
3340
ARTIFACTORY_REPOSITORY_NAME: 'sonarsource-npm-public-releases'
3441
DRY_RUN: ${{ inputs.dry_run || false }}
3542
steps:
43+
- name: Validate release description
44+
id: description
45+
env:
46+
RELEASE_BODY: ${{ github.event.release.body }}
47+
INPUT_DESCRIPTION: ${{ inputs.release_description }}
48+
run: |
49+
# Use input description for manual trigger, otherwise extract from release body
50+
if [ -n "$INPUT_DESCRIPTION" ]; then
51+
DESCRIPTION="$INPUT_DESCRIPTION"
52+
else
53+
# Extract description from release body (line starting with "Description:")
54+
DESCRIPTION=$(echo "$RELEASE_BODY" | grep -i "^Description:" | sed 's/^[Dd]escription:[[:space:]]*//')
55+
fi
56+
57+
if [ -z "$DESCRIPTION" ]; then
58+
echo "::error::Release body must contain a 'Description:' line for the Update Center entry."
59+
echo "::error::Example format:"
60+
echo "::error:: Description: Support new authentication method"
61+
echo "::error::"
62+
echo "::error:: ## What's Changed"
63+
echo "::error:: * PR details..."
64+
exit 1
65+
fi
66+
67+
echo "description=$DESCRIPTION" >> $GITHUB_OUTPUT
68+
echo "Extracted description: $DESCRIPTION"
69+
3670
- name: Fetch the secrets
3771
if: ${{ !inputs.dry_run }}
3872
id: secrets
@@ -113,3 +147,96 @@ jobs:
113147
# Publish as sonarqube-scanner (legacy alias for backwards compatibility)
114148
echo $(jq '.name = "sonarqube-scanner"' package.json) > package.json
115149
npm publish --tag=${{ steps.npm-tag.outputs.tag }} --access=public
150+
151+
outputs:
152+
description: ${{ steps.description.outputs.description }}
153+
154+
update-center:
155+
needs: publish
156+
permissions:
157+
contents: read
158+
id-token: write
159+
runs-on: ubuntu-latest
160+
env:
161+
RELEASE_TAG: ${{ github.event.release.tag_name || inputs.release_tag }}
162+
steps:
163+
- name: Fetch GitHub token
164+
id: secrets
165+
uses: SonarSource/vault-action-wrapper@v3
166+
with:
167+
secrets: development/github/token/SonarSource-sonar-scanner-npm-release-automation token | github_token;
168+
169+
- name: Checkout sonar-update-center-properties
170+
uses: actions/checkout@v6
171+
with:
172+
repository: SonarSource/sonar-update-center-properties
173+
token: ${{ fromJSON(steps.secrets.outputs.vault).github_token }}
174+
path: update-center
175+
176+
- name: Checkout sonar-scanner-npm (for scripts)
177+
uses: actions/checkout@v6
178+
with:
179+
path: sonar-scanner-npm
180+
181+
- name: Update scannernpm.properties
182+
working-directory: update-center
183+
run: |
184+
bash ../sonar-scanner-npm/scripts/update-update-center.sh \
185+
scannernpm.properties \
186+
"${RELEASE_TAG}" \
187+
"${{ needs.publish.outputs.description }}"
188+
189+
- name: Create Pull Request
190+
id: create-pr
191+
working-directory: update-center
192+
env:
193+
GH_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).github_token }}
194+
run: |
195+
BRANCH="scannernpm-${RELEASE_TAG}"
196+
git config user.name "github-actions[bot]"
197+
git config user.email "github-actions[bot]@users.noreply.github.com"
198+
git checkout -b "$BRANCH"
199+
git add scannernpm.properties
200+
git commit -m "Update SonarScanner for NPM to ${RELEASE_TAG}"
201+
git push origin "$BRANCH"
202+
PR_URL=$(gh pr create \
203+
--title "Update SonarScanner for NPM to ${RELEASE_TAG}" \
204+
--body "Automated PR to update SonarScanner for NPM to version ${RELEASE_TAG}.
205+
206+
Created by [sonar-scanner-npm release workflow](https://github.com/SonarSource/sonar-scanner-npm/actions/runs/${{ github.run_id }})." \
207+
--base master)
208+
echo "pr_url=$PR_URL" >> $GITHUB_OUTPUT
209+
210+
outputs:
211+
pr_url: ${{ steps.create-pr.outputs.pr_url }}
212+
213+
notify:
214+
needs: [publish, update-center]
215+
permissions:
216+
id-token: write
217+
runs-on: ubuntu-latest
218+
env:
219+
RELEASE_TAG: ${{ github.event.release.tag_name || inputs.release_tag }}
220+
steps:
221+
- name: Fetch Slack token
222+
id: secrets
223+
uses: SonarSource/vault-action-wrapper@v3
224+
with:
225+
secrets: development/kv/data/slack token | slack_token;
226+
227+
- name: Send Slack notification
228+
uses: slackapi/slack-github-action@v2.1.0
229+
with:
230+
method: chat.postMessage
231+
token: ${{ fromJSON(steps.secrets.outputs.vault).slack_token }}
232+
errors: true
233+
payload: |
234+
{
235+
"channel": "${{ inputs.slack_channel || 'ask-squad-web' }}",
236+
"attachments": [
237+
{
238+
"color": "#36a64f",
239+
"text": ":package: *SonarScanner for NPM ${{ env.RELEASE_TAG }}* has been released!\n\n<https://github.com/SonarSource/sonar-scanner-npm/releases/tag/${{ env.RELEASE_TAG }}|View Release> | <https://www.npmjs.com/package/@sonar/scan/v/${{ env.RELEASE_TAG }}|npm package>\n\n:clipboard: *Next steps to complete the release:*\n1. Merge the Update Center PR: <${{ needs.update-center.outputs.pr_url }}|sonar-update-center-properties PR>\n2. Run the <https://github.com/SonarSource/sonar-update-center-properties/actions/workflows/deploy.yml|Deploy workflow> to publish the Update Center JSON\n3. Run the <https://github.com/SonarSource/sonarqube-documentation/actions/workflows/generate-release-notes.yml|Generate Release Notes workflow> and merge the resulting PR"
240+
}
241+
]
242+
}

docs/DEV.md

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,23 @@ Releases are handled by the GitHub Actions workflow (`.github/workflows/release.
4141

4242
1. Create a new release on GitHub
4343
2. Set the tag name (e.g., `1.2.3`)
44-
3. The workflow will:
44+
3. Add a `Description:` line in the release body (required for the Update Center)
45+
4. The workflow will:
4546
- Build the package with the release version
4647
- Publish to Artifactory
4748
- Publish to npm under two package names:
4849
- `@sonar/scan` (primary)
4950
- `sonarqube-scanner` (legacy alias for backwards compatibility)
51+
- Create a PR in [sonar-update-center-properties](https://github.com/SonarSource/sonar-update-center-properties)
52+
53+
Example release body:
54+
55+
```
56+
Description: Support new authentication method
57+
58+
## What's Changed
59+
* SCANNPM-XXX Add new auth support by @user in #123
60+
```
5061

5162
### npm Tags
5263

@@ -86,35 +97,49 @@ You can test the release workflow without actually publishing by using the manua
8697
3. Fill in the inputs:
8798
- **Dry run**: ✅ checked (skips all publish steps)
8899
- **Release tag**: The version to simulate (e.g., `1.2.3`)
100+
- **Release description**: The description for the Update Center entry
89101
- **Simulate prerelease**: Check to test prerelease behavior
90102
- **Simulate [skip-latest]**: Check to test the skip-latest behavior
91103

92-
The workflow will run and display the npm tag that would be used without performing any actual build or publish operations.
104+
The workflow will run and display the npm tag that would be used without performing any actual build or publish operations. The Update Center PR is still created during dry runs so you can verify the changes (just close the PR afterwards).
93105

94106
### Sonar Update Center
95107

96-
After publishing a new release, the [Sonar Update Center](https://xtranet-sonarsource.atlassian.net/wiki/spaces/DOC/pages/3385294896/The+Sonar+Update+Center) needs to be updated. This makes release information available at `downloads.sonarsource.com` for documentation and tooling.
108+
The [Sonar Update Center](https://xtranet-sonarsource.atlassian.net/wiki/spaces/DOC/pages/3385294896/The+Sonar+Update+Center) is automatically updated by the release workflow. When a release is published, the workflow creates a PR in [sonar-update-center-properties](https://github.com/SonarSource/sonar-update-center-properties) to update `scannernpm.properties` that:
97109

98-
#### Update Process
110+
- Adds the new version entry using the `Description:` from the release body
111+
- Moves the previous public version to `archivedVersions`
99112

100-
1. **Create a PR** in [sonar-update-center-properties](https://github.com/SonarSource/sonar-update-center-properties) to update `scannernpm.properties`
113+
#### Post-Release Steps
101114

102-
2. **Add the new version entry** with the following format:
115+
After the release workflow completes, the following manual steps are required to fully publish the new version:
103116

104-
```properties
105-
X.Y.Z.description=Short description of the release
106-
X.Y.Z.date=YYYY-MM-DD
107-
X.Y.Z.changelogUrl=https://github.com/SonarSource/sonar-scanner-npm/releases/tag/X.Y.Z
108-
X.Y.Z.downloadUrl=https://www.npmjs.com/package/@sonar/scan/v/X.Y.Z
109-
```
117+
1. **Merge the Update Center PR**
118+
- Review and merge the PR created in [sonar-update-center-properties](https://github.com/SonarSource/sonar-update-center-properties)
119+
- The PR link is included in the Slack notification
110120

111-
3. **Update version lists**:
112-
- Move the previous public version to `archivedVersions`
113-
- Set the new version in `publicVersions`
121+
2. **Deploy the Update Center**
122+
- Run the [deploy workflow](https://github.com/SonarSource/sonar-update-center-properties/actions/workflows/deploy.yml) in sonar-update-center-properties
123+
- This publishes the updated JSON to https://downloads.sonarsource.com/sonarqube/update/scannernpm.json
114124

115-
4. **After PR is merged**, run the scanner release notes GitHub Action on the [SonarQube-Documentation](https://github.com/SonarSource/SonarQube-Documentation) repo to create a PR that pushes the update to product docs
125+
3. **Update the Documentation**
126+
- Run the [generate-release-notes workflow](https://github.com/SonarSource/sonarqube-documentation/actions/workflows/generate-release-notes.yml) in sonarqube-documentation
127+
- This creates a PR to update the scanner versions (example: [sonarqube-documentation#94](https://github.com/SonarSource/sonarqube-documentation/pull/94))
128+
- Merge the generated PR to publish the new version at https://docs.sonarsource.com/sonarqube-server/analyzing-source-code/scanners/npm/installing
129+
130+
#### Version Entry Format
131+
132+
Each version entry in `scannernpm.properties` follows this format:
133+
134+
```properties
135+
X.Y.Z.description=Short description of the release
136+
X.Y.Z.date=YYYY-MM-DD
137+
X.Y.Z.changelogUrl=https://github.com/SonarSource/sonar-scanner-npm/releases/tag/X.Y.Z
138+
X.Y.Z.downloadUrl=https://www.npmjs.com/package/@sonar/scan/v/X.Y.Z
139+
```
116140

117141
#### Reference
118142

119143
- Initial setup PR: [sonar-update-center-properties#742](https://github.com/SonarSource/sonar-update-center-properties/pull/742)
120-
- Published JSON: `https://downloads.sonarsource.com/sonarqube/update/scannernpm.json`
144+
- Published JSON: https://downloads.sonarsource.com/sonarqube/update/scannernpm.json
145+
- Documentation page: https://docs.sonarsource.com/sonarqube-server/analyzing-source-code/scanners/npm/installing

scripts/update-update-center.sh

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/bin/bash
2+
#
3+
# Updates the scannernpm.properties file in sonar-update-center-properties
4+
# for a new release.
5+
#
6+
# Usage: update-update-center.sh <file> <version> <description>
7+
#
8+
# This script:
9+
# 1. Moves the current publicVersions to archivedVersions
10+
# 2. Sets the new version as publicVersions
11+
# 3. Adds version metadata (description, date, URLs)
12+
#
13+
set -euo pipefail
14+
15+
if [ $# -ne 3 ]; then
16+
echo "Usage: $0 <file> <version> <description>"
17+
exit 1
18+
fi
19+
20+
FILE="$1"
21+
VERSION="$2"
22+
DESCRIPTION="$3"
23+
DATE=$(date +%Y-%m-%d)
24+
25+
if [ ! -f "$FILE" ]; then
26+
echo "Error: File not found: $FILE"
27+
exit 1
28+
fi
29+
30+
# Get current publicVersions
31+
CURRENT_PUBLIC=$(grep "^publicVersions=" "$FILE" | cut -d= -f2)
32+
33+
# Get current archivedVersions
34+
CURRENT_ARCHIVED=$(grep "^archivedVersions=" "$FILE" | cut -d= -f2)
35+
36+
# Update archivedVersions: append current public versions
37+
if [ -n "$CURRENT_ARCHIVED" ]; then
38+
NEW_ARCHIVED="${CURRENT_ARCHIVED},${CURRENT_PUBLIC}"
39+
else
40+
NEW_ARCHIVED="${CURRENT_PUBLIC}"
41+
fi
42+
43+
# Update the file
44+
sed -i "s/^archivedVersions=.*/archivedVersions=${NEW_ARCHIVED}/" "$FILE"
45+
sed -i "s/^publicVersions=.*/publicVersions=${VERSION}/" "$FILE"
46+
47+
# Find the line number of publicVersions and insert new version entry after it
48+
LINE_NUM=$(grep -n "^publicVersions=" "$FILE" | cut -d: -f1)
49+
50+
# Create the new version entry
51+
NEW_ENTRY="${VERSION}.description=${DESCRIPTION}
52+
${VERSION}.date=${DATE}
53+
${VERSION}.changelogUrl=https://github.com/SonarSource/sonar-scanner-npm/releases/tag/${VERSION}
54+
${VERSION}.downloadUrl=https://www.npmjs.com/package/@sonar/scan/v/${VERSION}"
55+
56+
# Insert after the publicVersions line
57+
awk -v line="$LINE_NUM" -v entry="$NEW_ENTRY" 'NR==line {print; print ""; print entry; next} 1' "$FILE" > tmp && mv tmp "$FILE"
58+
59+
echo "Updated $FILE for version $VERSION"

test/integration/orchestrator/download.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
1919
*/
2020

21+
import type { IncomingMessage } from 'node:http';
2122
import * as https from 'node:https';
2223
import * as fs from 'node:fs';
2324
import { execSync } from 'node:child_process';
@@ -103,7 +104,29 @@ function download(
103104
return new Promise((resolve, reject) => {
104105
const file = fs.createWriteStream(zipFilePath);
105106
console.log(`Downloading ${url} and saving it into ${zipFilePath}`);
106-
https.get(options, response => {
107+
108+
const handleResponse = (response: IncomingMessage) => {
109+
// Handle redirects (3xx status codes)
110+
if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400) {
111+
const redirectUrl = response.headers.location;
112+
if (redirectUrl) {
113+
console.log(`Following redirect to ${redirectUrl}`);
114+
https.get(redirectUrl, handleResponse).on('error', reject);
115+
return;
116+
}
117+
}
118+
119+
if (response.statusCode !== 200) {
120+
file.close();
121+
fs.unlinkSync(zipFilePath);
122+
reject(
123+
new Error(
124+
`Failed to download SonarQube: HTTP ${response.statusCode}. ` +
125+
`Make sure ARTIFACTORY_ACCESS_TOKEN is set and valid.`,
126+
),
127+
);
128+
return;
129+
}
107130
response.pipe(file);
108131
file.on('finish', () => {
109132
file.close();
@@ -117,7 +140,9 @@ function download(
117140
console.error('Error while downloading file', error);
118141
reject(error);
119142
});
120-
});
143+
};
144+
145+
https.get(options, handleResponse).on('error', reject);
121146
});
122147
}
123148

0 commit comments

Comments
 (0)