azure/gate-template.yml
The Azure Pipelines gate — materially weaker than GitHub's: no dependency review, no secret scan.
What this is
The Azure Pipelines gate. One step runs the CLI's own verify command; a second publishes the pull-request status the branch policy requires.
How to onboard it
- Installed as:
.azuredevops/redline-gate.yml - Installed by: redline init, with the ADR threshold, dependency-severity floor and soft-fail labels filled in, on an Azure DevOps repository.
How to use it
Nothing directly — it runs on every PR once installed. If your org needs dependency and secret scanning parity with GitHub, that has to come from elsewhere in the Azure DevOps pipeline; this template does not provide it.
- This file alone runs nothing — Azure Repos ignores its pr: trigger. redline init also registers a redline-gate build definition pointing at it and a "Redline: gate build" Build Validation branch policy that queues that pipeline on every pull request; without Build Administrator rights that registration degrades to a pending gate capability and the redline/gate status policy is written advisory, since no pipeline could publish the status it would require.
- Step "Redline gate" runs npx --yes --package=redlinegate@latest redline verify --gate.
- Step "Publish redline/gate status" always runs (condition: always()) and publishes a PR status with genre redline and name gate — the branch policy requires exactly redline/gate. Renaming either value makes the policy unsatisfiable and every PR in the repo sits blocked.
- Materially weaker than the GitHub gate: redline verify --gate runs none of GitHub's four checks. An Azure repository gets no dependency-review job and no diff secret scan at the gate — the two checks that are hard-fail and never label-exemptible on GitHub. checklist and adr, the two that are soft-fail there, are the lesser loss.
Expected output
.azuredevops/redline-gate.yml, and a pull-request status published with genre redline and name gate — exactly what the branch policy requires. Renaming either value makes the policy unsatisfiable and leaves every pull request in the repository blocked. The status publishes on condition: always(), so a failed verify still reports rather than leaving the policy pending forever.
How to edit it
The real source: This template file. Note what it cannot produce: redline verify --gate runs none of GitHub's dependency review or diff secret scan, so those two hard-fail checks have no Azure equivalent here.
- Check whether the CLI reads this file at allSeveral of these are reference shapes: the CLI generates the equivalent in code and never opens the checked-in copy. Editing one of those changes nothing about what redline init writes. The onboarding section above says which kind this is.
- Edit the real sourceFor a generated artifact that is cli/commands/init.ts; for a file installed verbatim it is the template itself.
- npm test && node scripts/validate.mjsThe install path is unit-tested against a fake host client, and validate.mjs pins the shapes the merge gate's required check name depends on.
The full file
# Managed by Redline; regenerate with `redline init` rather than editing here.
# Redline merge gate for Azure DevOps.
#
# CONTRACT: the final step publishes a pull-request status with genre `redline`
# and name `gate`. The Status branch policy installed by `redline init` requires exactly
# that status. Renaming either value makes the policy unsatisfiable and every pull
# request in this repository will sit blocked. `redline verify` checks the live
# status name against this contract.
#
# There is deliberately no `pr:` block here: Azure Repos ignores YAML `pr:`
# triggers (they only fire for GitHub-hosted repositories). This pipeline is
# queued on pull requests by the "Redline: gate build" Build Validation branch
# policy that `redline init` creates against the registered `redline-gate`
# build definition.
trigger: none
pool:
vmImage: ubuntu-latest
variables:
ADR_DIFF_THRESHOLD: 300
FAIL_ON_DEPENDENCY_SEVERITY: high
SOFT_FAIL_LABELS: redline-exempt,redline-sync
# Declared with a default so the status step can read it even when the scan
# passed and never set it. An undeclared Azure variable interpolates as the
# literal string "$(NAME)" rather than empty, which is the kind of quiet
# wrong answer this gate exists to prevent.
REDLINE_SECURITY_FAILED: 'false'
# Pinned by image digest, not by tag: a tag is mutable and this scans the diff
# of every pull request in the repository. The version in the trailing comment
# is what scripts/check-pins.mjs re-resolves the digest against, exactly as it
# does for the SHA-pinned actions on the GitHub side of the gate. Docker Hub
# spells the tag without the `v` the git tag carries; both sides of the gate
# deliberately run the same version.
TRUFFLEHOG_IMAGE: trufflesecurity/trufflehog@sha256:deb2af10659a488a14d262a323addcde099d99827a1cf1dc4e93c17915c39f08 # 3.97.1
steps:
- checkout: self
fetchDepth: 0
- task: NodeTool@0
displayName: Node 22
inputs:
versionSpec: '22.x'
# The GitHub gate runs this as its own job (workflows/redline-gate.yml,
# `secrets`). It has no substitute in Mend or SonarQube: both scan a state of
# the tree, and a credential committed and reverted inside one pull request
# is still in the history the moment it merges. This scans the RANGE.
#
# `--results=verified` only, matching the GitHub job for the same reason:
# `unknown` produces false positives and this check cannot be waived.
- script: |
set -euo pipefail
target="${SYSTEM_PULLREQUEST_TARGETBRANCH#refs/heads/}"
# The PR build checks out a merge commit, and the target branch is not
# guaranteed to be a local ref on a fresh agent. Fetch it by name before
# asking for a merge base, or `git merge-base` fails and the scan silently
# degrades to scanning nothing.
git fetch --no-tags origin "$target"
base=$(git merge-base FETCH_HEAD HEAD)
echo "scanning $base..HEAD"
# The failure is recorded BEFORE the exit, because the exemption branch in
# the status step below must never downgrade this one. On GitHub the
# security jobs sit outside label exemption; this variable is how that
# exclusion survives the two being one pipeline here.
if ! docker run --rm -v "$(pwd):/repo" "$TRUFFLEHOG_IMAGE" \
git file:///repo --since-commit "$base" --results=verified --fail
then
echo "##vso[task.setvariable variable=REDLINE_SECURITY_FAILED]true"
exit 1
fi
displayName: Secret scan (diff)
name: secrets
env:
TRUFFLEHOG_IMAGE: $(TRUFFLEHOG_IMAGE)
# --package pins npx to the redlinegate package explicitly. `redline` alone
# names a different, unrelated package on the public registry — `redline`
# is only this package's bin name, never resolve npx against it directly.
- script: npx --yes --package=redlinegate@latest redline verify --gate
displayName: Redline gate
name: gate
# A failed secret scan must not hide the configuration findings: the author
# deserves the whole list in one run rather than one blocker at a time.
condition: succeededOrFailed()
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
ADR_DIFF_THRESHOLD: $(ADR_DIFF_THRESHOLD)
FAIL_ON_DEPENDENCY_SEVERITY: $(FAIL_ON_DEPENDENCY_SEVERITY)
SOFT_FAIL_LABELS: $(SOFT_FAIL_LABELS)
- script: |
set -euo pipefail
state="succeeded"
description="Redline merge gate"
if [ "$AGENT_JOBSTATUS" != "Succeeded" ]; then state="failed"; fi
# $SYSTEM_TEAMPROJECT is admin-set, not attacker-controlled, but a
# display name containing a space or a quote must not be allowed to
# break the JSON body or the URL path — encode it, never interpolate
# it raw into either.
project_enc=$(jq -rn --arg v "$SYSTEM_TEAMPROJECT" '$v | @uri')
target_url="${SYSTEM_TEAMFOUNDATIONCOLLECTIONURI}${project_enc}/_build/results?buildId=$BUILD_BUILDID"
pr_url="${SYSTEM_TEAMFOUNDATIONCOLLECTIONURI}${project_enc}/_apis/git/repositories/$BUILD_REPOSITORY_ID/pullRequests/$SYSTEM_PULLREQUEST_PULLREQUESTID"
# The access token must never appear in argv — visible via `ps` or
# /proc/<pid>/cmdline to any process on the agent for the call's
# duration. It is already an env var; hand it to curl through a
# stdin config instead of putting it on the command line with -H.
curl_authed() {
printf 'header = "Authorization: Bearer %s"\n' "$SYSTEM_ACCESSTOKEN" | curl -K - "$@"
}
# Soft-fail escape hatch, the same one workflows/redline-gate.yml gives
# GitHub: a pull request carrying one of SOFT_FAIL_LABELS reports the
# gate succeeded-with-warning instead of blocking, so a reviewer can
# accept a process failure deliberately. It can only ever turn a
# failure into a success, never the reverse.
#
# The secret scan is excluded, exactly as it is on GitHub, where the
# security jobs sit outside the exemption. There the exclusion is
# structural — separate jobs, and the label only reaches some of them.
# Here the whole gate is one pipeline, so the scan records
# REDLINE_SECURITY_FAILED before it exits and this branch refuses to
# downgrade it. A label that could waive a verified credential in the
# diff would make the label the vulnerability.
if [ "${REDLINE_SECURITY_FAILED:-}" = "true" ]; then
echo "##vso[task.logissue type=error]The secret scan failed. This check cannot be waived by a label."
elif [ "$state" = "failed" ] && [ -n "${SOFT_FAIL_LABELS:-}" ]; then
labels=$(curl_authed -sS "$pr_url/labels?api-version=7.1") || labels=''
for label in ${SOFT_FAIL_LABELS//,/ }; do
if printf '%s' "$labels" \
| jq -e --arg l "$label" '[.value[]? | select(.active != false) | .name] | index($l)' >/dev/null 2>&1
then
state="succeeded"
description="Redline gate failed but the \"$label\" label is applied — a reviewer accepted this deliberately"
echo "##vso[task.logissue type=warning]$description"
break
fi
done
fi
body=$(jq -n --arg state "$state" --arg url "$target_url" --arg description "$description" \
'{state: $state, description: $description, context: {name: "gate", genre: "redline"}, targetUrl: $url}')
curl_authed -sS --fail-with-body -X POST \
-H "Content-Type: application/json" \
-d "$body" \
"$pr_url/statuses?api-version=7.1"
displayName: Publish redline/gate status
condition: always()
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
SOFT_FAIL_LABELS: $(SOFT_FAIL_LABELS)
REDLINE_SECURITY_FAILED: $(REDLINE_SECURITY_FAILED)