Skip to content

Commit 42219b0

Browse files
authored
Add SemVer checks (#3375)
* Fixes #1605 * Check "stable" Rust on Linux only (others add significant time to build) * Add `eng/cgmanfiest.json` to track installation of a dev tool in the build system * `Test-Semver.ps1` uses the version specified in `eng/cgmanifest.json` Tested: | Scenario | Result | | -------- | ------ | | No packages changed | | | [Checks SemVer of changed package in PR](https://dev.azure.com/azure-sdk/public/_build/results?buildId=5614015&view=logs&j=906b8cf8-77d6-5b97-38eb-78e393929662&t=a7849eae-de43-5ea0-75a4-5b970d68e244) | Success (expected) | | [Fails on SemVer violation in PR](https://dev.azure.com/azure-sdk/public/_build/results?buildId=5614218&view=logs&j=b766ebde-1fdb-5f11-1350-46ddc53b23cf&t=7ef29fa0-f532-5653-3473-16dc04608431) | Fail (expected) | | [Checks SemVer of packages in service](https://dev.azure.com/azure-sdk/internal/_build/results?buildId=5614246&view=logs&j=b766ebde-1fdb-5f11-1350-46ddc53b23cf&t=9494b470-f365-50d2-5008-6394593b39c5) | Fail (expected) | | [Checks SemVer of packages in release (and blocks release)](https://dev.azure.com/azure-sdk/internal/_build/results?buildId=5614282&view=logs&j=b766ebde-1fdb-5f11-1350-46ddc53b23cf&t=9494b470-f365-50d2-5008-6394593b39c5) | Fail (expected) | # Test performance SemVer checks add a non-trivial amount of time in some OS configurations (Windows, MacOS) because `rust-semver-checks` is built. | Test Job | OS | Time | | -------- | -- | ---- | | Unit | Linux | 1m42s | | Unit | Windows | 5m40s | | Unit | Mac | 7m2s | | Semver | Linux | 6m13s | | Semver | Windows | 15m3s | | Semver | Mac | 21m19s | `rust-semver-check` build times (look at logs for builds of stable in [this PR run](https://dev.azure.com/azure-sdk/public/_build/results?buildId=5613783&view=logs&j=7e382a76-12de-5903-1057-64e59e53ed06&t=ba3464b8-2ae1-508a-306c-badcf96fe2c9)): | OS | Time | | -- | ---- | | Linux | 4m50s | | Windows | 10m15s | | Mac | 17m12s | ## Caching considerations Also discussed here: #3158 Azure DevOps caches are scoped according to [several factors](https://learn.microsoft.com/en-us/azure/devops/pipelines/release/caching?view=azure-devops&tabs=bundler#cache-isolation-and-security). In the case of a PR where speed is most important, we would need a job that runs against `main` to populate a cache that PRs could read. We don't currently schedule PR pipelines to run. In fact, vcpkg, uses its own cache mechanism with relevant rules. If we needed to semver check across OS quickly, we could use a storage account to hold pre-built binaries (similar to vcpkg). The binaries would be built in a Rust pipeline and uploaded to the storage account with a path prefix like `carge-semver-checks/ubuntu-24/0.45.0/`. Something would need to update the cache and cgmanifes.json file and proper arrangements would need to be made locally so that calling `cargo semver-checks` would use the correct binary. Similar work could also be done to cache source analysis binaries # Other considerations Checking "nightly" and "msrv" versions of Rust is more complicated. Generally, [`cargo-semver-checks` supports "stable"](https://github.com/obi1kenobi/cargo-semver-checks?tab=readme-ov-file#what-rust-versions-does-cargo-semver-checks-support) so SemVer checking focuses there. PRs should also block on semver-breaking changes. It may be possible that future version schemes (e.g. adding `-beta.x` to a version number) may not block PRs because semver allows breaking changes in `-beta.x` versions. This will need to be investigated as part of the version incrementing script. We'd either need to adjust the version scheme or update `-beta.x` at CI execution time.
1 parent 3b81ef1 commit 42219b0

File tree

5 files changed

+181
-53
lines changed

5 files changed

+181
-53
lines changed

eng/cgmanifest.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"$schema": "https://json.schemastore.org/component-detection-manifest.json",
3+
"version": 1,
4+
"registrations": [
5+
{
6+
"component": {
7+
"type": "cargo",
8+
"cargo": { "name": "cargo-semver-checks", "version": "0.45.0" }
9+
},
10+
"developmentDependency": true
11+
}
12+
]
13+
}

eng/pipelines/templates/jobs/pack.yml

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,19 @@ jobs:
6868
PackageInfoDirectory: $(Build.ArtifactStagingDirectory)/PackageInfo
6969

7070
- ${{ if eq('auto', parameters.ServiceDirectory) }}:
71+
- task: PowerShell@2
72+
displayName: Check SemVer compatibility
73+
condition: >-
74+
and(
75+
succeeded(),
76+
ne(variables['NoPackagesChanged'],'true'),
77+
ne(variables['Skip.Semver'], 'true')
78+
)
79+
inputs:
80+
pwsh: true
81+
filePath: $(Build.SourcesDirectory)/eng/scripts/Test-Semver.ps1
82+
arguments: -PackageInfoDirectory '$(Build.ArtifactStagingDirectory)/PackageInfo'
83+
7184
- task: Powershell@2
7285
displayName: Pack Crates
7386
condition: and(succeeded(), ne(variables['NoPackagesChanged'],'true'))
@@ -101,6 +114,19 @@ jobs:
101114
displayName: Configure crate packing
102115
condition: and(succeeded(), ne(variables['NoPackagesChanged'],'true'))
103116
117+
- task: PowerShell@2
118+
displayName: Check SemVer compatibility
119+
condition: >-
120+
and(
121+
succeeded(),
122+
ne(variables['NoPackagesChanged'],'true'),
123+
ne(variables['Skip.Semver'], 'true')
124+
)
125+
inputs:
126+
pwsh: true
127+
filePath: $(Build.SourcesDirectory)/eng/scripts/Test-Semver.ps1
128+
arguments: -PackageNames $(PackageNames)
129+
104130
- task: Powershell@2
105131
displayName: Pack Crates
106132
condition: and(succeeded(), ne(variables['NoPackagesChanged'],'true'))
@@ -123,5 +149,5 @@ jobs:
123149
- template: /eng/common/pipelines/templates/steps/create-apireview.yml
124150
parameters:
125151
Artifacts: ${{ parameters.Artifacts }}
126-
152+
127153
- template: /eng/common/pipelines/templates/steps/detect-api-changes.yml

eng/scripts/Pack-Crates.ps1

Lines changed: 28 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -16,62 +16,13 @@ param(
1616
$ErrorActionPreference = 'Stop'
1717

1818
. ([System.IO.Path]::Combine($PSScriptRoot, '..', 'common', 'scripts', 'common.ps1'))
19+
. ([System.IO.Path]::Combine($PSScriptRoot, 'shared', 'Cargo.ps1'))
1920

2021
Write-Host @"
2122
Packing crates with
2223
RUSTFLAGS: '${env:RUSTFLAGS}'
2324
"@
2425

25-
function Get-OutputPackageNames($workspacePackages) {
26-
$packablePackages = $workspacePackages | Where-Object -Property publish -NE -Value @()
27-
$packablePackageNames = $packablePackages.name
28-
29-
$names = @()
30-
switch ($PsCmdlet.ParameterSetName) {
31-
'Named' {
32-
$names = $PackageNames
33-
}
34-
35-
'PackageInfo' {
36-
$packageInfoFiles = Get-ChildItem -Path $PackageInfoDirectory -Filter '*.json' -File
37-
foreach ($packageInfoFile in $packageInfoFiles) {
38-
$packageInfo = Get-Content -Path $packageInfoFile.FullName | ConvertFrom-Json
39-
$names += $packageInfo.name
40-
}
41-
}
42-
43-
default {
44-
return $packablePackageNames
45-
}
46-
}
47-
48-
foreach ($name in $names) {
49-
if (-not $packablePackageNames.Contains($name)) {
50-
Write-Error "Package '$name' is not in the workspace or does not publish"
51-
exit 1
52-
}
53-
}
54-
55-
return $names
56-
}
57-
58-
function Get-CargoPackages() {
59-
$metadata = Get-CargoMetadata
60-
61-
# Path based dependencies are assumed to be unreleased package versions. In
62-
# non-release builds these should be packed as well.
63-
foreach ($package in $metadata.packages) {
64-
$package.UnreleasedDependencies = @()
65-
foreach ($dependency in $package.dependencies) {
66-
if ($dependency.path -and $dependency.kind -ne 'dev') {
67-
$dependencyPackage = $metadata.packages | Where-Object -Property name -EQ -Value $dependency.name | Select-Object -First 1
68-
$package.UnreleasedDependencies += $dependencyPackage
69-
}
70-
}
71-
}
72-
73-
return $metadata.packages
74-
}
7526

7627
function Get-PackagesToBuild() {
7728
$packages = Get-CargoPackages
@@ -105,8 +56,33 @@ function Get-PackagesToBuild() {
10556
return $packagesToBuild
10657
}
10758

108-
function Get-CargoMetadata() {
109-
cargo metadata --no-deps --format-version 1 --manifest-path "$RepoRoot/Cargo.toml" | ConvertFrom-Json -Depth 100 -AsHashtable
59+
function Get-OutputPackageNames($workspacePackages) {
60+
$packablePackages = $workspacePackages | Where-Object -Property publish -NE -Value @()
61+
$packablePackageNames = $packablePackages.name
62+
63+
$names = @()
64+
switch ($PsCmdlet.ParameterSetName) {
65+
'Named' {
66+
$names = $PackageNames
67+
}
68+
69+
'PackageInfo' {
70+
$names = Get-PackageNamesFromPackageInfo $PackageInfoDirectory
71+
}
72+
73+
default {
74+
return $packablePackageNames
75+
}
76+
}
77+
78+
foreach ($name in $names) {
79+
if (-not $packablePackageNames.Contains($name)) {
80+
Write-Error "Package '$name' is not in the workspace or does not publish"
81+
exit 1
82+
}
83+
}
84+
85+
return $names
11086
}
11187

11288
function Create-ApiViewFile($package) {

eng/scripts/Test-Semver.ps1

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#!/usr/bin/env pwsh
2+
3+
#Requires -Version 7.0
4+
[CmdletBinding(DefaultParameterSetName = "none")]
5+
param(
6+
[Parameter(ParameterSetName = 'Named')]
7+
[string[]]$PackageNames,
8+
[Parameter(ParameterSetName = 'PackageInfo')]
9+
[string]$PackageInfoDirectory,
10+
[switch]$IgnoreCgManifestVersion
11+
)
12+
13+
. ([System.IO.Path]::Combine($PSScriptRoot, '..', 'common', 'scripts', 'common.ps1'))
14+
. ([System.IO.Path]::Combine($PSScriptRoot, 'shared', 'Cargo.ps1'))
15+
16+
function Get-OutputPackageNames($workspacePackages) {
17+
$packablePackages = $workspacePackages | Where-Object -Property publish -NE -Value @()
18+
$packablePackageNames = $packablePackages.name
19+
20+
$names = @()
21+
switch ($PsCmdlet.ParameterSetName) {
22+
'Named' {
23+
$names = $PackageNames
24+
}
25+
26+
'PackageInfo' {
27+
$names = Get-PackageNamesFromPackageInfo $PackageInfoDirectory
28+
}
29+
30+
default {
31+
return $packablePackageNames
32+
}
33+
}
34+
35+
foreach ($name in $names) {
36+
if (-not $packablePackageNames.Contains($name)) {
37+
Write-Error "Package '$name' is not in the workspace or does not publish"
38+
exit 1
39+
}
40+
}
41+
42+
return $names
43+
}
44+
45+
$packages = Get-CargoPackages
46+
$outputPackageNames = Get-OutputPackageNames $packages
47+
48+
# Read version from cgmanifest.json. If ignored the currently installed or
49+
# "latest" version is used.
50+
$versionParams = @()
51+
if (!$IgnoreCgManifestVersion) {
52+
$versionParams += '--version'
53+
$cgManifest = Get-Content ([System.IO.Path]::Combine($PSScriptRoot, '..', 'cgmanifest.json')) `
54+
| ConvertFrom-Json
55+
$versionParams += $cgManifest.
56+
registrations.
57+
Where({ $_.component.type -eq 'cargo' -and $_.component.cargo.name -eq 'cargo-semver-checks' }).
58+
component.cargo.version
59+
}
60+
61+
LogGroupStart "cargo install cargo-semver-checks --locked $($versionParams -join ' ')"
62+
Write-Host "cargo install cargo-semver-checks --locked $($versionParams -join ' ')"
63+
cargo install cargo-semver-checks --locked @versionParams
64+
LogGroupEnd
65+
66+
$packageParams = @()
67+
foreach ($packageName in $outputPackageNames) {
68+
$packageParams += "--package"
69+
$packageParams += $packageName
70+
}
71+
72+
LogGroupStart "cargo semver-checks $($packageParams -join ' ')"
73+
Write-Host "cargo semver-checks $($packageParams -join ' ')"
74+
cargo semver-checks @packageParams
75+
LogGroupEnd
76+
77+
if ($LASTEXITCODE -ne 0) {
78+
LogError "SemVer checks failed"
79+
exit $LASTEXITCODE
80+
}

eng/scripts/shared/Cargo.ps1

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
2+
function Get-CargoMetadata() {
3+
cargo metadata --no-deps --format-version 1 --manifest-path "$RepoRoot/Cargo.toml" | ConvertFrom-Json -Depth 100 -AsHashtable
4+
}
5+
6+
function Get-CargoPackages() {
7+
$metadata = Get-CargoMetadata
8+
9+
# Path based dependencies are assumed to be unreleased package versions. In
10+
# non-release builds these should be packed as well.
11+
foreach ($package in $metadata.packages) {
12+
$package.UnreleasedDependencies = @()
13+
foreach ($dependency in $package.dependencies) {
14+
if ($dependency.path -and $dependency.kind -ne 'dev') {
15+
$dependencyPackage = $metadata.packages | Where-Object -Property name -EQ -Value $dependency.name | Select-Object -First 1
16+
$package.UnreleasedDependencies += $dependencyPackage
17+
}
18+
}
19+
}
20+
21+
return $metadata.packages
22+
}
23+
24+
function Get-PackageNamesFromPackageInfo($packageInfoDirectory) {
25+
$names = @()
26+
$packageInfoFiles = Get-ChildItem -Path $packageInfoDirectory -Filter '*.json' -File
27+
foreach ($packageInfoFile in $packageInfoFiles) {
28+
$packageInfo = Get-Content -Path $packageInfoFile.FullName | ConvertFrom-Json
29+
$names += $packageInfo.name
30+
}
31+
32+
return $names
33+
}

0 commit comments

Comments
 (0)