Skip to content
Draft
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
14 changes: 14 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,17 @@ updates:
actions:
patterns:
- "*"

# The azldev tool declaration and its transitive module dependencies.
# We control azldev publishing, so we can bump it immediately when a new release is cut.
- package-ecosystem: gomod
directory: /
schedule:
interval: daily
open-pull-requests-limit: 5
labels:
- dependencies
groups:
go:
patterns:
- "*"
5 changes: 3 additions & 2 deletions .github/instructions/pr-check-workflows.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,17 @@ If the check builds, renders, or runs PR code, do the whole thing inside the bui

The shared runner image is [`.github/workflows/containers/azldev-runner.Dockerfile`](../workflows/containers/azldev-runner.Dockerfile). It's a minimal Azure Linux base with `mock`, `git`, `python3`, `sudo`, and `azldev` itself (installed to `/usr/local/bin` during image build) — enough to run any `azldev` subcommand. Reuse it rather than building a per-check image; add extras via a derived `FROM localhost/azldev-runner` stage if a check genuinely needs more.

`azldev` is baked in via `go install` during image build. The version is pinned in `.azldev-version` at the repo root and passed to the Dockerfile as `--build-arg AZLDEV_VERSION=…`. All CI workflows (GH Actions, ADO, Dockerfile) read from the same file. Image build context is `.github/workflows/containers/` only — keep it that way so the build can never see PR-controlled files.
`azldev` is baked in via `go install` during image build. The tool is declared in the repo-root `go.mod`; callers resolve it to a validated full Git hash before passing it to the Dockerfile as `--build-arg AZLDEV_HASH=…`. Image build context is `.github/workflows/containers/` only — keep it that way so the build can never see PR-controlled files.

Build it with the caller's UID so bind-mounted writes don't end up root-owned:

```yaml
- name: Build azldev runner
run: |
AZLDEV_HASH="$(python3 scripts/ci/render-specs-check/resolve_azldev_version.py hash go.mod)"
docker build \
--build-arg UID=$(id -u) \
--build-arg AZLDEV_VERSION="$(cat .azldev-version)" \
--build-arg AZLDEV_HASH="$AZLDEV_HASH" \
-t localhost/azldev-runner \
-f .github/workflows/containers/azldev-runner.Dockerfile \
.github/workflows/containers/
Expand Down
17 changes: 12 additions & 5 deletions .github/workflows/ado/templates/steps/get-changes-info.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ parameters:
steps:
- template: ensure-full-history.yml

# Validate the PR merge checkout before install-deps resolves azldev from its
# go.mod. This is the same post-merge module state the GitHub render gate
# reads from refs/pull/<number>/merge.
# Sets job variables:
# - baseCommit
# - sourceCommit
- ${{ if eq(parameters.mode, 'pr') }}:
- template: commit-range-pr.yml
parameters:
baseCommitVar: ${{ parameters.baseCommitVar }}
sourceCommitVar: ${{ parameters.sourceCommitVar }}

- template: install-deps.yml
parameters:
installAdoRequirements: ${{ eq(parameters.mode, 'postmerge') }}
Expand All @@ -46,11 +58,6 @@ steps:
# Sets job variables:
# - baseCommit
# - sourceCommit
- ${{ if eq(parameters.mode, 'pr') }}:
- template: commit-range-pr.yml
parameters:
baseCommitVar: ${{ parameters.baseCommitVar }}
sourceCommitVar: ${{ parameters.sourceCommitVar }}
- ${{ if eq(parameters.mode, 'postmerge') }}:
- template: commit-range-postmerge.yml
parameters:
Expand Down
18 changes: 10 additions & 8 deletions .github/workflows/ado/templates/steps/install-deps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#
# Step template: authenticate to the internal pip feed, then install the
# requested host tools. Each tool is opt-in via a boolean parameter. azldev's
# version comes from the committed .azldev-version and is validated before use.
# immutable hash is resolved from the committed go.mod before installation.

parameters:
# azure-devops SDK (scripts/ci/ado/requirements.txt).
Expand Down Expand Up @@ -62,15 +62,17 @@ steps:
- script: |
set -euo pipefail
echo "##[group]Azldev (host)"
# Only the version string comes from the checkout; reject a
# malformed/garbage value before it reaches `go install`.
AZLDEV_VERSION="$(tr -d '\n' < .azldev-version)"
if ! printf '%s' "$AZLDEV_VERSION" | grep -Eq '^[0-9A-Za-z._+-]+$'; then
echo "##[error].azldev-version is empty or has unexpected characters"
# PR callers validate the synthetic merge checkout before reaching this
# step, so go.mod selects the same post-merge version as the GitHub
# render gate. Installing by resolved hash also ignores PR-controlled
# replace directives when building the host binary.
AZLDEV_HASH="$(python3 scripts/ci/render-specs-check/resolve_azldev_version.py hash go.mod)"
if ! printf '%s' "$AZLDEV_HASH" | grep -Eq '^[0-9a-f]{40}$'; then
echo "##[error]Resolver emitted an invalid azldev hash"
exit 1
fi
echo "Installing azldev@${AZLDEV_VERSION}..."
go install "github.com/microsoft/azure-linux-dev-tools/cmd/azldev@${AZLDEV_VERSION}"
echo "Installing azldev@${AZLDEV_HASH}..."
go install "github.com/microsoft/azure-linux-dev-tools/cmd/azldev@${AZLDEV_HASH}"

go_bin_path="$(go env GOPATH)/bin"
echo "##vso[task.prependpath]$go_bin_path"
Expand Down
39 changes: 20 additions & 19 deletions .github/workflows/azldev-smoke.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
# Smoke-test the azldev version pinned in .azldev-version.
# Smoke-test the azldev tool declared in go.mod.
#
# When a PR bumps .azldev-version (or touches the runner image / this workflow),
# build the runner container with that exact pin and confirm the resulting
# binary can (a) run and (b) parse every component definition in the repo via
# `azldev component list`. This catches the two failure modes of a version bump:
# the pin doesn't `go install`, or the new version breaks on the repo's TOMLs.
# When a PR updates the tool declaration or its resolver (or touches the runner
# image / this workflow), build the runner container with its resolved immutable
# hash and confirm the resulting binary can (a) run and (b) parse every component
# definition in the repo via `azldev component list`. This catches resolver/Go
# integration regressions, pins that do not install, and incompatible TOMLs.
name: "azldev Smoke Test"

on:
pull_request:
branches: ["4.0"]
paths:
- ".azldev-version"
- "go.mod"
- "go.sum"
- ".github/workflows/containers/azldev-runner.Dockerfile"
- ".github/workflows/azldev-smoke.yml"
- "scripts/ci/render-specs-check/resolve_azldev_version.py"
workflow_dispatch:

# Cancel in-progress runs of this workflow if a new run is triggered.
Expand All @@ -36,25 +38,24 @@ jobs:
with:
persist-credentials: false

- name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version: stable
cache: false

- name: Build azldev runner container
run: |
set -euo pipefail
version="$(tr -d '\n' < .azldev-version)"
if [ -z "$version" ]; then
echo "::error::.azldev-version is empty"
exit 1
fi
# Restrict to the charset Go module versions use (commit SHAs, tags,
# pseudo-versions). This blocks shell metacharacters so the value is
# safe to pass straight through to `docker build --build-arg` below.
if ! printf '%s' "$version" | grep -Eq '^[0-9A-Za-z._+-]+$'; then
echo "::error::.azldev-version contains unexpected characters: '$version'"
azldev_hash="$(python3 scripts/ci/render-specs-check/resolve_azldev_version.py hash go.mod)"
if ! [[ "$azldev_hash" =~ ^[0-9a-f]{40}$ ]]; then
echo "::error::resolver emitted an invalid azldev hash"
exit 1
fi
echo "azldev version pin: $version"
echo "azldev hash: $azldev_hash"
docker build \
--build-arg UID="$(id -u)" \
--build-arg AZLDEV_VERSION="$version" \
--build-arg AZLDEV_HASH="$azldev_hash" \
-t localhost/azldev-runner \
-f .github/workflows/containers/azldev-runner.Dockerfile \
.github/workflows/containers/
Expand Down
114 changes: 61 additions & 53 deletions .github/workflows/check-rendered-specs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,13 @@
# stale lock would steer it toward the wrong scope. When locks are dirty,
# render is skipped and only the locks comment is posted.
#
# Security: the PR checkout is data-only. We never run PR scripts, and the
# runner Dockerfile + build context come from the trusted base checkout. The
# azldev binary is built from the version pinned in the PR's .azldev-version
# (so a version-bump PR is validated with the binary that will actually run
# post-merge). The Dockerfile hardcodes the module path to the Microsoft-owned
# github.com/microsoft/azure-linux-dev-tools repo, so the PR can only choose a
# *ref* (tag/branch/commit) -- it cannot redirect `go install` to a different
# module. This is NOT the same as "only Microsoft-authored code runs": a ref
# reachable via that repo's fork network (e.g. a commit behind an open
# dev-tools PR) could in principle be selected. That residual is acceptable
# only because the build/render jobs run with `contents: read`, hold no
# secrets, and execute azldev inside the sandboxed container -- see the
# per-job `permissions` below.
# Security: the PR checkout is data-only. Trusted base code resolves the
# post-merge azldev pin and passes only a validated full hash to the trusted
# runner Dockerfile, which hardcodes the Microsoft-owned module path. PR-derived
# azldev runs only inside sandboxed jobs with a read-only token and no secrets.
# A selected ref can still contain code introduced through the module's fork
# network; the sandbox and absence of secrets bound that residual trust.
# Spec/overlay/lock data still comes from PR head, not a materialized merge tree.
name: "Check Rendered Specs"

on:
Expand Down Expand Up @@ -71,8 +65,12 @@ jobs:
timeout-minutes: 60
permissions:
contents: read
pull-requests: read # validate the live PR snapshot and resolve its test-merge version
outputs:
patch-url: ${{ steps.upload-locks-patch.outputs.artifact-url }}
# Resolve once so lock and render checks use the same binary.
azldev-hash: ${{ steps.resolve.outputs.azldev-hash }}
render-all: ${{ steps.resolve.outputs.render-all }}
steps:
# --- Trusted base branch (tools, scripts, container config) ---
- name: Checkout base (trusted)
Expand All @@ -99,25 +97,50 @@ jobs:
# sandboxed container — so the fork head is never executed with trust.
allow-unsafe-pr-checkout: true

- name: Build azldev runner container
# The trusted helper reads PR data but emits only a validated hash and
# boolean. Equal pins need one freshness check but no mergeability polling.
- name: Resolve post-merge azldev hash
id: resolve
env:
REPO: ${{ inputs.repo }}
PR_NUMBER: ${{ inputs.pr-number }}
PR_HEAD_SHA: ${{ inputs.pr-head-sha }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
# Build with the azldev version the PR proposes (pr-head/.azldev-version),
# NOT the base branch's. For a version-bump PR these differ, and the whole
# point of the drift check is to run with the binary that will actually run
# on 4.0 post-merge -- the base binary would pass the check and then drift
# the moment the bump lands. Only the version string comes from the PR; the
# Dockerfile + build context are the trusted base checkout, and the
# Dockerfile hardcodes the module path to the Microsoft-owned repo.
AZLDEV_VERSION="$(tr -d '\n' < pr-head/.azldev-version)"
# Hygiene: reject a malformed/garbage version before it reaches docker build.
if ! printf '%s' "$AZLDEV_VERSION" | grep -Eq '^[0-9A-Za-z._+-]+$'; then
echo "::error::pr-head/.azldev-version is empty or has unexpected characters"
resolve_output="$(python3 scripts/ci/render-specs-check/resolve_azldev_version.py \
post-merge \
--repo "$REPO" \
--pr-number "$PR_NUMBER" \
--head-sha "$PR_HEAD_SHA" \
--base-sha "$(git rev-parse HEAD)" \
--base-modfile go.mod \
--head-modfile pr-head/go.mod)"
echo "$resolve_output"

azldev_hash="$(sed -n 's/^azldev-hash=//p' <<< "$resolve_output")"
render_all="$(sed -n 's/^render-all=//p' <<< "$resolve_output")"
if ! [[ "$azldev_hash" =~ ^[0-9a-f]{40}$ ]]; then
echo "::error::resolver emitted an invalid azldev hash"
exit 1
fi
if [[ "$render_all" != "true" && "$render_all" != "false" ]]; then
echo "::error::resolver emitted an invalid render-all value"
exit 1
fi
{
printf 'azldev-hash=%s\n' "$azldev_hash"
printf 'render-all=%s\n' "$render_all"
} >> "$GITHUB_OUTPUT"

- name: Build azldev runner container
env:
AZLDEV_HASH: ${{ steps.resolve.outputs.azldev-hash }}
run: |
set -euo pipefail
docker build \
--build-arg UID="$(id -u)" \
--build-arg AZLDEV_VERSION="$AZLDEV_VERSION" \
--build-arg AZLDEV_HASH="$AZLDEV_HASH" \
-t localhost/azldev-runner \
-f .github/workflows/containers/azldev-runner.Dockerfile \
.github/workflows/containers/
Expand Down Expand Up @@ -338,24 +361,14 @@ jobs:
allow-unsafe-pr-checkout: true

- name: Build azldev runner container
# Reuse the hash that update_locks already resolved.
env:
AZLDEV_HASH: ${{ needs.update_locks.outputs.azldev-hash }}
run: |
set -euo pipefail
# Build with the azldev version the PR proposes (pr-head/.azldev-version),
# NOT the base branch's. For a version-bump PR these differ, and the whole
# point of the drift check is to run with the binary that will actually run
# on 4.0 post-merge -- the base binary would pass the check and then drift
# the moment the bump lands. Only the version string comes from the PR; the
# Dockerfile + build context are the trusted base checkout, and the
# Dockerfile hardcodes the module path to the Microsoft-owned repo.
AZLDEV_VERSION="$(tr -d '\n' < pr-head/.azldev-version)"
# Hygiene: reject a malformed/garbage version before it reaches docker build.
if ! printf '%s' "$AZLDEV_VERSION" | grep -Eq '^[0-9A-Za-z._+-]+$'; then
echo "::error::pr-head/.azldev-version is empty or has unexpected characters"
exit 1
fi
docker build \
--build-arg UID="$(id -u)" \
--build-arg AZLDEV_VERSION="$AZLDEV_VERSION" \
--build-arg AZLDEV_HASH="$AZLDEV_HASH" \
-t localhost/azldev-runner \
-f .github/workflows/containers/azldev-runner.Dockerfile \
.github/workflows/containers/
Expand Down Expand Up @@ -383,6 +396,7 @@ jobs:
PR_BASE_SHA: ${{ inputs.pr-base-sha }}
PR_HEAD_SHA: ${{ inputs.pr-head-sha }}
BASE_REPO: ${{ inputs.repo }}
RENDER_ALL: ${{ needs.update_locks.outputs.render-all }}
run: |
set -euo pipefail
mkdir -p "$WORKSPACE/render-output"
Expand All @@ -402,32 +416,26 @@ jobs:
-e PR_BASE_SHA \
-e PR_HEAD_SHA \
-e BASE_REPO \
-e RENDER_ALL \
localhost/azldev-runner \
bash -eu -o pipefail -c '
# Stale forks may not have the upstream base SHA in their object
# store. Fetch it explicitly so the version-bump diff below and
# `compute_change_set.sh` can both resolve the base commit. The
# base repo is public so no auth is needed.
# store. Fetch it explicitly so `compute_change_set.sh` can
# resolve the base commit for the PR-scoped render set. The base
# repo is public so no auth is needed.
git -C /workdir remote add base "https://github.com/$BASE_REPO.git" 2>/dev/null || true
git -C /workdir fetch --no-tags base "$PR_BASE_SHA"

# Validate the resolved source origins before any fetch/render.
python3 /scripts-render/source_origin_guard.py
SPECS_DIR=$(azldev config dump -q -f json | jq -r .project.renderedSpecsDir)

# A change to .azldev-version swaps the azldev binary, which can
# alter rendered output for ANY component -- not just the ones
# this PR edits -- so a PR-scoped render would miss that drift.
# Detect it with a plain git diff (both commits are local after
# the fetch above) and render everything when it moved. Using git
# here -- rather than the REST "list PR files" endpoint -- avoids
# the 3000-file cap and 30-results-per-page pagination of that
# endpoint, so detection stays correct for arbitrarily large PRs.
if ! git -C /workdir diff --quiet "$PR_BASE_SHA" "$PR_HEAD_SHA" -- .azldev-version; then
# A new post-merge azldev binary can affect every component.
if [ "$RENDER_ALL" = "true" ]; then
# `--clean-stale` prunes orphaned spec dirs globally, which
# subsumes the targeted deleted-component cleanup the scoped
# branch does below.
echo ".azldev-version changed -- rendering ALL components."
echo "azldev version changes on merge -- rendering ALL components."
azldev component render -q -a --clean-stale -O json \
> /output/render-output.json
else
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/containers/azldev-runner.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ RUN tdnf -y install \
symcrypt-openssl \
&& tdnf clean all

# The version is passed in as a build arg from .azldev-version in the repo
# root. Callers (check-rendered-specs.yml, etc.) read the file and pass it
# via --build-arg so the Dockerfile never needs repo-root build context.
# The azldev Git hash is resolved from the tool declared in the repo-root
# go.mod. Callers (check-rendered-specs.yml, etc.) pass it via --build-arg so
# the Dockerfile never needs repo-root build context.
# No default — omitting --build-arg will fail the build loudly.
# Optional Go module proxy for the `go install` below. Callers that build
# behind an internal-only proxy forward it via --build-arg GOPROXY=...; Docker
Expand All @@ -50,10 +50,10 @@ RUN tdnf -y install \
# direct) and would break the install below. The ADO/OneBranch PR build
# forwards the host's internal proxy.
ARG GOPROXY
ARG AZLDEV_VERSION
RUN test -n "${AZLDEV_VERSION}" || { echo "ERROR: AZLDEV_VERSION build-arg is required (read from .azldev-version)" >&2; exit 1; } \
ARG AZLDEV_HASH
RUN test -n "${AZLDEV_HASH}" || { echo "ERROR: AZLDEV_HASH build-arg is required" >&2; exit 1; } \
&& GOBIN=/usr/local/bin go install \
"github.com/microsoft/azure-linux-dev-tools/cmd/azldev@${AZLDEV_VERSION}" \
"github.com/microsoft/azure-linux-dev-tools/cmd/azldev@${AZLDEV_HASH}" \
&& rm -rf /root/go /root/.cache

ARG UID=1000
Expand Down
Loading
Loading