...
1# Common Changelog Operations
2. "${PSScriptRoot}\logging.ps1"
3. "${PSScriptRoot}\SemVer.ps1"
4
5$RELEASE_TITLE_REGEX = "(?<releaseNoteTitle>^\#+\s+(?<version>$([AzureEngSemanticVersion]::SEMVER_REGEX))(\s+(?<releaseStatus>\(.+\))))"
6$SECTION_HEADER_REGEX_SUFFIX = "##\s(?<sectionName>.*)"
7$CHANGELOG_UNRELEASED_STATUS = "(Unreleased)"
8$CHANGELOG_DATE_FORMAT = "yyyy-MM-dd"
9$RecommendedSectionHeaders = @("Features Added", "Breaking Changes", "Bugs Fixed", "Other Changes")
10
11# Returns a Collection of changeLogEntry object containing changelog info for all version present in the gived CHANGELOG
12function Get-ChangeLogEntries {
13 param (
14 [Parameter(Mandatory = $true)]
15 [String]$ChangeLogLocation
16 )
17
18 if (!(Test-Path $ChangeLogLocation)) {
19 LogError "ChangeLog[${ChangeLogLocation}] does not exist"
20 return $null
21 }
22 LogDebug "Extracting entries from [${ChangeLogLocation}]."
23 return Get-ChangeLogEntriesFromContent (Get-Content -Path $ChangeLogLocation)
24}
25
26function Get-ChangeLogEntriesFromContent {
27 param (
28 [Parameter(Mandatory = $true)]
29 $changeLogContent
30 )
31
32 if ($changeLogContent -is [string])
33 {
34 $changeLogContent = $changeLogContent.Split("`n")
35 }
36 elseif($changeLogContent -isnot [array])
37 {
38 LogError "Invalid ChangelogContent passed"
39 return $null
40 }
41
42 $changelogEntry = $null
43 $sectionName = $null
44 $changeLogEntries = [Ordered]@{}
45 $initialAtxHeader= "#"
46
47 if ($changeLogContent[0] -match "(?<HeaderLevel>^#+)\s.*")
48 {
49 $initialAtxHeader = $matches["HeaderLevel"]
50 }
51
52 $sectionHeaderRegex = "^${initialAtxHeader}${SECTION_HEADER_REGEX_SUFFIX}"
53 $changeLogEntries | Add-Member -NotePropertyName "InitialAtxHeader" -NotePropertyValue $initialAtxHeader
54 $releaseTitleAtxHeader = $initialAtxHeader + "#"
55
56 try {
57 # walk the document, finding where the version specifiers are and creating lists
58 foreach ($line in $changeLogContent) {
59 if ($line -match $RELEASE_TITLE_REGEX) {
60 $changeLogEntry = [pscustomobject]@{
61 ReleaseVersion = $matches["version"]
62 ReleaseStatus = $matches["releaseStatus"]
63 ReleaseTitle = "$releaseTitleAtxHeader {0} {1}" -f $matches["version"], $matches["releaseStatus"]
64 ReleaseContent = @()
65 Sections = @{}
66 }
67 $changeLogEntries[$changeLogEntry.ReleaseVersion] = $changeLogEntry
68 }
69 else {
70 if ($changeLogEntry) {
71 if ($line.Trim() -match $sectionHeaderRegex)
72 {
73 $sectionName = $matches["sectionName"].Trim()
74 $changeLogEntry.Sections[$sectionName] = @()
75 $changeLogEntry.ReleaseContent += $line
76 continue
77 }
78
79 if ($sectionName)
80 {
81 $changeLogEntry.Sections[$sectionName] += $line
82 }
83
84 $changeLogEntry.ReleaseContent += $line
85 }
86 }
87 }
88 }
89 catch {
90 Write-Error "Error parsing Changelog."
91 Write-Error $_
92 }
93 return $changeLogEntries
94}
95
96# Returns single changeLogEntry object containing the ChangeLog for a particular version
97function Get-ChangeLogEntry {
98 param (
99 [Parameter(Mandatory = $true)]
100 [String]$ChangeLogLocation,
101 [Parameter(Mandatory = $true)]
102 [String]$VersionString
103 )
104 $changeLogEntries = Get-ChangeLogEntries -ChangeLogLocation $ChangeLogLocation
105
106 if ($changeLogEntries -and $changeLogEntries.Contains($VersionString)) {
107 return $changeLogEntries[$VersionString]
108 }
109 return $null
110}
111
112#Returns the changelog for a particular version as string
113function Get-ChangeLogEntryAsString {
114 param (
115 [Parameter(Mandatory = $true)]
116 [String]$ChangeLogLocation,
117 [Parameter(Mandatory = $true)]
118 [String]$VersionString
119 )
120
121 $changeLogEntry = Get-ChangeLogEntry -ChangeLogLocation $ChangeLogLocation -VersionString $VersionString
122 return ChangeLogEntryAsString $changeLogEntry
123}
124
125function ChangeLogEntryAsString($changeLogEntry) {
126 if (!$changeLogEntry) {
127 return "[Missing change log entry]"
128 }
129 [string]$releaseTitle = $changeLogEntry.ReleaseTitle
130 [string]$releaseContent = $changeLogEntry.ReleaseContent -Join [Environment]::NewLine
131 return $releaseTitle, $releaseContent -Join [Environment]::NewLine
132}
133
134function Confirm-ChangeLogEntry {
135 param (
136 [Parameter(Mandatory = $true)]
137 [String]$ChangeLogLocation,
138 [Parameter(Mandatory = $true)]
139 [String]$VersionString,
140 [boolean]$ForRelease = $false,
141 [Switch]$SantizeEntry
142 )
143
144 $changeLogEntries = Get-ChangeLogEntries -ChangeLogLocation $ChangeLogLocation
145 $changeLogEntry = $changeLogEntries[$VersionString]
146
147 if (!$changeLogEntry) {
148 LogError "ChangeLog[${ChangeLogLocation}] does not have an entry for version ${VersionString}."
149 return $false
150 }
151
152 if ($SantizeEntry)
153 {
154 Remove-EmptySections -ChangeLogEntry $changeLogEntry -InitialAtxHeader $changeLogEntries.InitialAtxHeader
155 Set-ChangeLogContent -ChangeLogLocation $ChangeLogLocation -ChangeLogEntries $changeLogEntries
156 }
157
158 Write-Host "Found the following change log entry for version '${VersionString}' in [${ChangeLogLocation}]."
159 Write-Host "-----"
160 Write-Host (ChangeLogEntryAsString $changeLogEntry)
161 Write-Host "-----"
162
163 if ([System.String]::IsNullOrEmpty($changeLogEntry.ReleaseStatus)) {
164 LogError "Entry does not have a correct release status. Please ensure the status is set to a date '($CHANGELOG_DATE_FORMAT)' or '$CHANGELOG_UNRELEASED_STATUS' if not yet released. See https://aka.ms/azsdk/guideline/changelogs for more info."
165 return $false
166 }
167
168 if ($ForRelease -eq $True)
169 {
170 LogDebug "Verifying as a release build because ForRelease parameter is set to true"
171 return Confirm-ChangeLogForRelease -changeLogEntry $changeLogEntry -changeLogEntries $changeLogEntries
172 }
173
174 # If the release status is a valid date then verify like its about to be released
175 $status = $changeLogEntry.ReleaseStatus.Trim().Trim("()")
176 if ($status -as [DateTime])
177 {
178 LogDebug "Verifying like it's a release build because the changelog entry has a valid date."
179 return Confirm-ChangeLogForRelease -changeLogEntry $changeLogEntry -changeLogEntries $changeLogEntries
180 }
181
182 return $true
183}
184
185function New-ChangeLogEntry {
186 param (
187 [Parameter(Mandatory = $true)]
188 [ValidateNotNullOrEmpty()]
189 [String]$Version,
190 [String]$Status=$CHANGELOG_UNRELEASED_STATUS,
191 [String]$InitialAtxHeader="#",
192 [String[]]$Content
193 )
194
195 # Validate RelaseStatus
196 $Status = $Status.Trim().Trim("()")
197 if ($Status -ne "Unreleased") {
198 try {
199 $Status = ([DateTime]$Status).ToString($CHANGELOG_DATE_FORMAT)
200 }
201 catch {
202 LogWarning "Invalid date [ $Status ] passed as status for Version [$Version]. Please use a valid date in the format '$CHANGELOG_DATE_FORMAT' or use '$CHANGELOG_UNRELEASED_STATUS'"
203 return $null
204 }
205 }
206 $Status = "($Status)"
207
208 # Validate Version
209 try {
210 $Version = ([AzureEngSemanticVersion]::ParseVersionString($Version)).ToString()
211 }
212 catch {
213 LogWarning "Invalid version [ $Version ]."
214 return $null
215 }
216
217 if (!$Content) {
218 $Content = @()
219 $Content += ""
220
221 $sectionsAtxHeader = $InitialAtxHeader + "##"
222 foreach ($recommendedHeader in $RecommendedSectionHeaders)
223 {
224 $Content += "$sectionsAtxHeader $recommendedHeader"
225 $Content += ""
226 }
227 }
228
229 $releaseTitleAtxHeader = $initialAtxHeader + "#"
230
231 $newChangeLogEntry = [pscustomobject]@{
232 ReleaseVersion = $Version
233 ReleaseStatus = $Status
234 ReleaseTitle = "$releaseTitleAtxHeader $Version $Status"
235 ReleaseContent = $Content
236 }
237
238 return $newChangeLogEntry
239}
240
241function Set-ChangeLogContent {
242 param (
243 [Parameter(Mandatory = $true)]
244 [String]$ChangeLogLocation,
245 [Parameter(Mandatory = $true)]
246 $ChangeLogEntries
247 )
248
249 $changeLogContent = @()
250 $changeLogContent += "$($ChangeLogEntries.InitialAtxHeader) Release History"
251 $changeLogContent += ""
252
253 $ChangeLogEntries = Sort-ChangeLogEntries -changeLogEntries $ChangeLogEntries
254
255 foreach ($changeLogEntry in $ChangeLogEntries) {
256 $changeLogContent += $changeLogEntry.ReleaseTitle
257 if ($changeLogEntry.ReleaseContent.Count -eq 0) {
258 $changeLogContent += @("","")
259 }
260 else {
261 $changeLogContent += $changeLogEntry.ReleaseContent
262 }
263 }
264
265 Set-Content -Path $ChangeLogLocation -Value $changeLogContent
266}
267
268function Remove-EmptySections {
269 param (
270 [Parameter(Mandatory = $true)]
271 $ChangeLogEntry,
272 $InitialAtxHeader = "#"
273 )
274
275 $sectionHeaderRegex = "^${InitialAtxHeader}${SECTION_HEADER_REGEX_SUFFIX}"
276 $releaseContent = $ChangeLogEntry.ReleaseContent
277
278 if ($releaseContent.Count -gt 0)
279 {
280 $parsedSections = $ChangeLogEntry.Sections
281 $sanitizedReleaseContent = New-Object System.Collections.ArrayList(,$releaseContent)
282
283 foreach ($key in @($parsedSections.Keys))
284 {
285 if ([System.String]::IsNullOrWhiteSpace($parsedSections[$key]))
286 {
287 for ($i = 0; $i -lt $sanitizedReleaseContent.Count; $i++)
288 {
289 $line = $sanitizedReleaseContent[$i]
290 if ($line -match $sectionHeaderRegex -and $matches["sectionName"].Trim() -eq $key)
291 {
292 $sanitizedReleaseContent.RemoveAt($i)
293 while($i -lt $sanitizedReleaseContent.Count -and [System.String]::IsNullOrWhiteSpace($sanitizedReleaseContent[$i]))
294 {
295 $sanitizedReleaseContent.RemoveAt($i)
296 }
297 $ChangeLogEntry.Sections.Remove($key)
298 break
299 }
300 }
301 }
302 }
303 $ChangeLogEntry.ReleaseContent = $sanitizedReleaseContent.ToArray()
304 }
305}
306
307function Get-LatestReleaseDateFromChangeLog
308{
309 param (
310 [Parameter(Mandatory = $true)]
311 $ChangeLogLocation
312 )
313 $changeLogEntries = Get-ChangeLogEntries -ChangeLogLocation $ChangeLogLocation
314 $latestVersion = $changeLogEntries[0].ReleaseStatus.Trim("()")
315 return ($latestVersion -as [DateTime])
316}
317
318function Sort-ChangeLogEntries {
319 param (
320 [Parameter(Mandatory = $true)]
321 $changeLogEntries
322 )
323
324 try
325 {
326 $changeLogEntries = $ChangeLogEntries.Values | Sort-Object -Descending -Property ReleaseStatus, `
327 @{e = {[AzureEngSemanticVersion]::new($_.ReleaseVersion)}}
328 }
329 catch {
330 LogError "Problem sorting version in ChangeLogEntries"
331 exit(1)
332 }
333 return $changeLogEntries
334}
335
336function Confirm-ChangeLogForRelease {
337 param (
338 [Parameter(Mandatory = $true)]
339 $changeLogEntry,
340 [Parameter(Mandatory = $true)]
341 $changeLogEntries
342 )
343
344 $entries = Sort-ChangeLogEntries -changeLogEntries $changeLogEntries
345
346 $isValid = $true
347 if ($changeLogEntry.ReleaseStatus -eq $CHANGELOG_UNRELEASED_STATUS) {
348 LogError "Entry has no release date set. Please ensure to set a release date with format '$CHANGELOG_DATE_FORMAT'. See https://aka.ms/azsdk/guideline/changelogs for more info."
349 $isValid = $false
350 }
351 else {
352 $status = $changeLogEntry.ReleaseStatus.Trim().Trim("()")
353 try {
354 $releaseDate = [DateTime]$status
355 if ($status -ne ($releaseDate.ToString($CHANGELOG_DATE_FORMAT)))
356 {
357 LogError "Date must be in the format $($CHANGELOG_DATE_FORMAT). See https://aka.ms/azsdk/guideline/changelogs for more info."
358 $isValid = $false
359 }
360
361 if (@($entries.ReleaseStatus)[0] -ne $changeLogEntry.ReleaseStatus)
362 {
363 LogError "Invalid date [ $status ]. The date for the changelog being released must be the latest in the file."
364 $isValid = $false
365 }
366 }
367 catch {
368 LogError "Invalid date [ $status ] passed as status for Version [$($changeLogEntry.ReleaseVersion)]. See https://aka.ms/azsdk/guideline/changelogs for more info."
369 $isValid = $false
370 }
371 }
372
373 if ([System.String]::IsNullOrWhiteSpace($changeLogEntry.ReleaseContent)) {
374 LogError "Entry has no content. Please ensure to provide some content of what changed in this version. See https://aka.ms/azsdk/guideline/changelogs for more info."
375 $isValid = $false
376 }
377
378 $foundRecommendedSection = $false
379 $emptySections = @()
380 foreach ($key in $changeLogEntry.Sections.Keys)
381 {
382 $sectionContent = $changeLogEntry.Sections[$key]
383 if ([System.String]::IsNullOrWhiteSpace(($sectionContent | Out-String)))
384 {
385 $emptySections += $key
386 }
387 if ($RecommendedSectionHeaders -contains $key)
388 {
389 $foundRecommendedSection = $true
390 }
391 }
392 if ($emptySections.Count -gt 0)
393 {
394 LogError "The changelog entry has the following sections with no content ($($emptySections -join ', ')). Please ensure to either remove the empty sections or add content to the section."
395 $isValid = $false
396 }
397 if (!$foundRecommendedSection)
398 {
399 LogWarning "The changelog entry did not contain any of the recommended sections ($($RecommendedSectionHeaders -join ', ')), please add at least one. See https://aka.ms/azsdk/guideline/changelogs for more info."
400 }
401 return $isValid
402}
View as plain text