Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 12 additions & 44 deletions .ado/build-template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,58 +112,23 @@ extends:
pool: ${{ parameters.AgentPool.Medium }}
timeoutInMinutes: 60 # CodeQL requires 3x usual build timeout
steps:
# 1. Detect whether this is a release build or a developer build
- pwsh: |
$param = '${{ parameters.isReleaseBuild }}'
$buildReason = $env:BUILD_REASON
$sourceMessage = $env:SOURCE_MESSAGE
$prSourceBranch = $env:PR_SOURCE_BRANCH

if ($param -eq 'true') {
$isRelease = $true
Write-Host "Release build forced by parameter"
}
elseif ($param -eq 'false') {
$isRelease = $false
Write-Host "Developer build forced by parameter"
}
else {
# Auto-detect
if ($buildReason -eq 'PullRequest') {
# PR: check source branch
$isRelease = $prSourceBranch.StartsWith('prepare-release/')
Write-Host "PR source branch: $prSourceBranch -> isRelease=$isRelease"
}
else {
# CI/Manual: check commit message
$firstLine = ($sourceMessage -split "`n")[0].Trim()
$isRelease = $firstLine.StartsWith("RELEASE:")
Write-Host "Commit message first line: $firstLine -> isRelease=$isRelease"
}
}

Write-Host "RESULT: isReleaseBuild=$isRelease"
Write-Host "##vso[task.setvariable variable=isReleaseBuild;isOutput=true]$isRelease"
Write-Host "##vso[task.setvariable variable=isReleaseBuild]$isRelease"
name: detectScenario
displayName: Detect build scenario
env:
BUILD_REASON: $(Build.Reason)
SOURCE_MESSAGE: $(Build.SourceVersionMessage)
PR_SOURCE_BRANCH: $(System.PullRequest.SourceBranch)

# 2. Full checkout (needed for beachball bump on developer builds;
# 1. Full checkout (needed for beachball bump on developer builds;
# harmless ~1 min overhead on release builds where bump is skipped)
- template: .ado/templates/checkout-full.yml@self
parameters:
persistCredentials: true

# 3. Node.js (always needed — setVersionEnvVars.js requires it)
# 2. Node.js (always needed — setVersionEnvVars.js requires it)
- task: UseNode@1
inputs:
version: '24.x'
displayName: Use Node.js 24.x

# 3. Detect before any developer-build version bump
- pwsh: node .ado/scripts/detectReleaseBuild.js '${{ parameters.isReleaseBuild }}'
name: detectScenario
displayName: Detect build scenario

# CI is network-isolated; acquire the feed token used by the Verdaccio uplink.
- ${{ if eq(parameters.buildEnvironment, 'Continuous') }}:
- template: .ado/templates/auth-npm-feed.yml@self
Expand Down Expand Up @@ -421,7 +386,10 @@ extends:
# Create NuGet packages
- job: RNWNuget
displayName: Pack NuGet
variables: [template: .ado/variables/shared.yml@self]
variables:
- template: .ado/variables/shared.yml@self
- name: isReleaseCommit
value: $[stageDependencies.Setup.Setup.outputs['detectScenario.isReleaseCommit']]
dependsOn:
- RnwNpmPack
- Linting
Expand Down Expand Up @@ -499,7 +467,7 @@ extends:
git -c http.extraheader="$authHeader" tag $tag "$(Build.SourceVersion)"
git -c http.extraheader="$authHeader" push origin $tag
displayName: Tag release sources
condition: and(succeeded(), startsWith(variables['Build.SourceVersionMessage'], 'RELEASE:'))
condition: and(succeeded(), eq(variables['IsReleaseBuild'], 'True'), eq(variables['isReleaseCommit'], 'True'))
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)

Expand Down
116 changes: 116 additions & 0 deletions .ado/scripts/detectReleaseBuild.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
* @format
*/

const {execFileSync} = require('node:child_process');

// Commit text and diagnostics are untrusted ADO log content,
// not logging commands.
const safeLog = text => String(text).replace(/##vso\[/gi, '##vso [');
const flag = value => (value ? 'True' : 'False');

function validateInputs(env, override) {
if (!['auto', 'true', 'false'].includes(override)) {
throw new Error('Expected one override argument: auto, true, or false');
}
const required = ['BUILD_REASON'];
required.push(
...(env.BUILD_REASON === 'PullRequest'
? ['SYSTEM_PULLREQUEST_SOURCEBRANCH']
: ['BUILD_SOURCEVERSION', 'BUILD_SOURCEBRANCH']),
);
for (const name of required) {
if (!env[name]?.trim()) {
throw new Error(`Missing required input: ${name}`);
}
}
}

// Pure classification: no Git, environment reads, or logging on this path.
function detectReleaseBuild(env, override, message = '') {
validateInputs(env, override);
let isReleaseCommit = false;
let detected;
let warning;
let rule;
if (env.BUILD_REASON === 'PullRequest') {
detected =
env.SYSTEM_PULLREQUEST_SOURCEBRANCH.startsWith('prepare-release/');
rule = `PR source branch: ${env.SYSTEM_PULLREQUEST_SOURCEBRANCH}`;
} else {
const lines = message.split(/\r?\n/);
// The marker is a body line; the subject never counts as one.
const markers = lines
.slice(1)
.filter(line => line.startsWith('RNW-Release-Branch'));
if (markers.length) {
const branch = env.BUILD_SOURCEBRANCH.replace(/^refs\/heads\//, '');
const match = /^RNW-Release-Branch: (\S+)$/.exec(markers[0]);
rule = `Marker found: ${markers.join(' | ')}; target branch: ${branch}`;
if (markers.length !== 1 || !match || match[1] !== branch) {
const problem =
'Invalid release marker (malformed, duplicate, or branch mismatch).' +
` ${rule}`;
if (override === 'auto') {
throw new Error(problem);
}
warning = problem;
} else {
isReleaseCommit = true;
}
} else {
isReleaseCommit = lines[0].trim().startsWith('RELEASE:');
rule = `Legacy subject rule used: ${lines[0]}`;
}
detected = isReleaseCommit;
}
return {
isReleaseBuild: override === 'auto' ? detected : override === 'true',
isReleaseCommit,
warning,
rule,
};
}

function main() {
try {
const override = process.argv.length === 3 ? process.argv[2] : undefined;
validateInputs(process.env, override);
let message = '';
if (process.env.BUILD_REASON !== 'PullRequest') {
message = execFileSync(
'git',
['show', '-s', '--format=%B', process.env.BUILD_SOURCEVERSION],
{encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']},
);
console.log(`Commit message:\n${safeLog(message)}`);
}
const result = detectReleaseBuild(process.env, override, message);
if (result.warning) {
console.log(
`##vso[task.logissue type=warning]${safeLog(result.warning)}`,
);
}
console.log(safeLog(`${result.rule}; override=${override}`));
const build = flag(result.isReleaseBuild);
const commit = flag(result.isReleaseCommit);
console.log(`RESULT: isReleaseBuild=${build}; isReleaseCommit=${commit}`);
console.log(
`##vso[task.setvariable variable=isReleaseBuild;isOutput=true]${build}`,
);
console.log(
`##vso[task.setvariable variable=isReleaseCommit;isOutput=true]${commit}`,
);
console.log(`##vso[task.setvariable variable=isReleaseBuild]${build}`);
} catch (error) {
console.error(`##vso[task.logissue type=error]${safeLog(error.message)}`);
process.exitCode = 1;
}
}

module.exports = {detectReleaseBuild, main};
if (require.main === module) {
main();
}
Loading
Loading