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.

  1. 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.
  2. Edit the real sourceFor a generated artifact that is cli/commands/init.ts; for a file installed verbatim it is the template itself.
  3. 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

platforms/azure/gate-template.yml · 159 lines · 7.6 KB
# 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)