diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b0e1cfa3aef..9c33f5ff976 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -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: + - "*" diff --git a/.github/instructions/pr-check-workflows.instructions.md b/.github/instructions/pr-check-workflows.instructions.md index 41fe37d9efa..48460fc3988 100644 --- a/.github/instructions/pr-check-workflows.instructions.md +++ b/.github/instructions/pr-check-workflows.instructions.md @@ -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/ diff --git a/.github/workflows/ado/templates/steps/get-changes-info.yml b/.github/workflows/ado/templates/steps/get-changes-info.yml index d3e455480be..4df16a5c479 100644 --- a/.github/workflows/ado/templates/steps/get-changes-info.yml +++ b/.github/workflows/ado/templates/steps/get-changes-info.yml @@ -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//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') }} @@ -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: diff --git a/.github/workflows/ado/templates/steps/install-deps.yml b/.github/workflows/ado/templates/steps/install-deps.yml index 17380e131f7..627d0bb16bd 100644 --- a/.github/workflows/ado/templates/steps/install-deps.yml +++ b/.github/workflows/ado/templates/steps/install-deps.yml @@ -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). @@ -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" diff --git a/.github/workflows/azldev-smoke.yml b/.github/workflows/azldev-smoke.yml index e67ac2fe143..fc3c72d1ee6 100644 --- a/.github/workflows/azldev-smoke.yml +++ b/.github/workflows/azldev-smoke.yml @@ -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. @@ -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/ diff --git a/.github/workflows/check-rendered-specs.yml b/.github/workflows/check-rendered-specs.yml index 705019c9641..9397f48fc81 100644 --- a/.github/workflows/check-rendered-specs.yml +++ b/.github/workflows/check-rendered-specs.yml @@ -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: @@ -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) @@ -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/ @@ -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/ @@ -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" @@ -402,12 +416,13 @@ 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" @@ -415,19 +430,12 @@ jobs: 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 diff --git a/.github/workflows/containers/azldev-runner.Dockerfile b/.github/workflows/containers/azldev-runner.Dockerfile index fe2fe36be8b..42df945b870 100644 --- a/.github/workflows/containers/azldev-runner.Dockerfile +++ b/.github/workflows/containers/azldev-runner.Dockerfile @@ -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 @@ -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 diff --git a/.github/workflows/dependency-smoke.yml b/.github/workflows/dependency-smoke.yml index c97891ee321..1b0f9106bd0 100644 --- a/.github/workflows/dependency-smoke.yml +++ b/.github/workflows/dependency-smoke.yml @@ -27,6 +27,7 @@ jobs: matrix: dir: - scripts/ci/control-tower + - scripts/ci/render-specs-check - scripts/ci/spec-review - scripts/mcps steps: @@ -47,7 +48,9 @@ jobs: # shellcheck disable=SC1091 . .smoke-venv/bin/activate python -m pip install --quiet --upgrade pip - pip install --quiet -r requirements.txt + if [[ -f requirements.txt ]]; then + pip install --quiet -r requirements.txt + fi shopt -s nullglob scripts=( *.py ) @@ -69,12 +72,12 @@ jobs: continue ;; esac + echo "import $mod" python -c "import $mod" done - - name: Run Control Tower helper tests - if: matrix.dir == 'scripts/ci/control-tower' + - name: Run helper tests env: DIR: ${{ matrix.dir }} run: | @@ -82,5 +85,13 @@ jobs: cd "$DIR" # shellcheck disable=SC1091 . .smoke-venv/bin/activate - pip install --quiet -r tests/requirements.txt + + if [[ ! -d tests ]]; then + echo "No helper tests in $DIR; skipping." + exit 0 + fi + + if [[ -f tests/requirements.txt ]]; then + pip install --quiet -r tests/requirements.txt + fi python -m pytest tests diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 9a877dd842c..28e7ef086f7 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -29,9 +29,5 @@ jobs: go-version: stable cache: false - - name: Install azldev - run: | - go install "github.com/microsoft/azure-linux-dev-tools/cmd/azldev@$(cat .azldev-version)" - - name: "Validate config (strict)" - run: azldev config dump > /dev/null + run: go tool azldev config dump > /dev/null diff --git a/DEVELOPING.md b/DEVELOPING.md index 6a2edbbfde6..9a304fdc8b7 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -4,13 +4,20 @@ ### Install azldev -The [`azldev`](https://github.com/microsoft/azure-linux-dev-tools) CLI tool drives all component, image, and build workflows. Install it from source (requires Go): +The [`azldev`](https://github.com/microsoft/azure-linux-dev-tools) CLI tool drives all component, image, and build workflows. Its version is declared as a Go tool in the repo-root `go.mod`. + +From the repository root, install the selected version with: ```bash -go install "github.com/microsoft/azure-linux-dev-tools/cmd/azldev@$(cat .azldev-version)" +go install github.com/microsoft/azure-linux-dev-tools/cmd/azldev +azldev --version ``` -> **Note:** azldev is still in active development, using the latest commit from the `main` branch is recommended for the most up-to-date features and fixes. +Go installs the command in `$(go env GOPATH)/bin`; add that directory to `PATH` if needed. Installation is recommended for normal development and shell completion. For one-off use without installing: + +```bash +go tool azldev --version +``` ### Render specs @@ -33,9 +40,11 @@ If the workspace is opened correctly, the agent will automatically gain access t The `azl-diagnose` agent and Koji-related tools require: 1. **MCP Python packages** — the MCP servers won't start without them: + ```bash pip3 install --user -r scripts/mcps/requirements.txt ``` + 2. **Network access to the internal Koji instance** — The internal Koji is only accessible via VPN or the corporate network. If the agent reports connection errors, verify you are connected before retrying. 3. **(Optional) `.env` configuration** — Create a `.env` file (in the workspace root or `scripts/mcps/`) to pre-configure MCP server settings like the Koji base URL and pre-approved insecure URLs. This avoids the agent having to set the URL or approve self-signed certificates every session. See [scripts/mcps/.env.example](scripts/mcps/.env.example) for available variables. diff --git a/go.mod b/go.mod new file mode 100644 index 00000000000..455f836a4cc --- /dev/null +++ b/go.mod @@ -0,0 +1,110 @@ +module github.com/microsoft/azurelinux + +go 1.25.6 + +tool github.com/microsoft/azure-linux-dev-tools/cmd/azldev + +require ( + cyphar.com/go-pathrs v0.2.4 // indirect + dario.cat/mergo v1.0.2 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.1.6 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/acobaugh/osrelease v0.1.0 // indirect + github.com/auribuo/stylishcobra v1.0.1 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/binalyze/ctxio v0.0.0-20210917063359-64c39a15e009 // indirect + github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect + github.com/briandowns/spinner v1.23.2 // indirect + github.com/brunoga/deep v1.3.1 // indirect + github.com/buger/jsonparser v1.1.2 // indirect + github.com/cavaliergopher/cpio v1.0.1 // indirect + github.com/cavaliergopher/rpm v1.3.0 // indirect + github.com/charmbracelet/bubbles v1.0.0 // indirect + github.com/charmbracelet/bubbletea v1.3.10 // indirect + github.com/charmbracelet/colorprofile v0.4.3 // indirect + github.com/charmbracelet/gum v0.17.0 // indirect + github.com/charmbracelet/harmonica v0.2.0 // indirect + github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect + github.com/charmbracelet/x/ansi v0.11.6 // indirect + github.com/charmbracelet/x/cellbuf v0.0.15 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/cloudflare/circl v1.6.3 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect + github.com/creachadair/tomledit v0.0.29 // indirect + github.com/cyphar/filepath-securejoin v0.6.1 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/fatih/color v1.19.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.13 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.9.0 // indirect + github.com/go-git/go-git/v5 v5.19.1 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.30.2 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/renameio v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/invopop/jsonschema v0.14.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jedib0t/go-pretty/v6 v6.7.10 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/klauspost/compress v1.18.6 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/lmittmann/tint v1.1.3 // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect + github.com/mark3labs/mcp-go v0.54.1 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.22 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.21 // indirect + github.com/microsoft/azure-linux-dev-tools v0.1.1-0.20260604190238-0256227f5434 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/nxadm/tail v1.4.11 // indirect + github.com/opencontainers/selinux v1.15.0 // indirect + github.com/pb33f/ordered-map/v2 v2.3.1 // indirect + github.com/pelletier/go-toml/v2 v2.3.1 // indirect + github.com/pjbgf/sha1cd v0.6.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/samber/lo v1.53.0 // indirect + github.com/samber/slog-common v0.21.0 // indirect + github.com/samber/slog-multi v1.8.0 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect + github.com/sergi/go-diff v1.4.0 // indirect + github.com/skeema/knownhosts v1.3.1 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/ulikunitz/xz v0.5.15 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect + go.szostok.io/version v1.2.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + go.yaml.in/yaml/v4 v4.0.0-rc.2 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/net v0.53.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/term v0.42.0 // indirect + golang.org/x/text v0.36.0 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000000..a74b2ae19e9 --- /dev/null +++ b/go.sum @@ -0,0 +1,303 @@ +cyphar.com/go-pathrs v0.2.4 h1:iD/mge36swa1UFKdINkr1Frkpp6wZsy3YYEildj9cLY= +cyphar.com/go-pathrs v0.2.4/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= +github.com/acobaugh/osrelease v0.1.0 h1:Yb59HQDGGNhCj4suHaFQQfBps5wyoKLSSX/J/+UifRE= +github.com/acobaugh/osrelease v0.1.0/go.mod h1:4bFEs0MtgHNHBrmHCt67gNisnabCRAlzdVasCEGHTWY= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/auribuo/stylishcobra v1.0.1 h1:bKGeCcLCW1Pzk5ZiT2Ubijdccn+unGrB5SBf3TQBGeA= +github.com/auribuo/stylishcobra v1.0.1/go.mod h1:NRNkmflkRM1078o68o3pFpPFZeb7wtD6EscbqLZmDsU= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= +github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/binalyze/ctxio v0.0.0-20210917063359-64c39a15e009 h1:rfYIdUKTmGdK3Dmxz2mp8z4LV8X7N+Yawbg0GASu/UM= +github.com/binalyze/ctxio v0.0.0-20210917063359-64c39a15e009/go.mod h1:YuOZ+lC/JejWMd9pp6GYQiGlW5jlsyVsDH0IPq+Lo/E= +github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= +github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/briandowns/spinner v1.23.2 h1:Zc6ecUnI+YzLmJniCfDNaMbW0Wid1d5+qcTq4L2FW8w= +github.com/briandowns/spinner v1.23.2/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM= +github.com/brunoga/deep v1.3.1 h1:bSrL6FhAZa6JlVv4vsi7Hg8SLwroDb1kgDERRVipBCo= +github.com/brunoga/deep v1.3.1/go.mod h1:GDV6dnXqn80ezsLSZ5Wlv1PdKAWAO4L5PnKYtv2dgaI= +github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= +github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM= +github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc= +github.com/cavaliergopher/rpm v1.3.0 h1:UHX46sasX8MesUXXQ+UbkFLUX4eUWTlEcX8jcnRBIgI= +github.com/cavaliergopher/rpm v1.3.0/go.mod h1:vEumo1vvtrHM1Ov86f6+k8j7zNKOxQfHDCAIcR/36ZI= +github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc= +github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= +github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= +github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= +github.com/charmbracelet/gum v0.17.0 h1:zlBYATCSG128gdZcCx0BfCsaADk3g9d0QyD19jjQrx0= +github.com/charmbracelet/gum v0.17.0/go.mod h1:All7PGZUc3z0ALtAhp1oLw8r+25sSKCkNdg5FjtOi5I= +github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= +github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= +github.com/charmbracelet/lipgloss v0.13.1/go.mod h1:zaYVJ2xKSKEnTEEbX6uAHabh2d975RJ+0yfkFpRBz5U= +github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= +github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= +github.com/charmbracelet/x/ansi v0.3.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/ansi v0.4.0/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= +github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= +github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= +github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creachadair/tomledit v0.0.29 h1:dB5CbdwJMpn/fmfAPTAAleXF/KJwY0Ggc1eL/zvZRgk= +github.com/creachadair/tomledit v0.0.29/go.mod h1:4SoTXxzHgvzHRMIJPw+o6zK/yXii4VjLrb6/3gCQnyA= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA= +github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00= +github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ= +github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= +github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/renameio v1.0.1 h1:Lh/jXZmvZxb0BBeSY5VKEfidcbcbenKjZFzM/q0fSeU= +github.com/google/renameio v1.0.1/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE= +github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/invopop/jsonschema v0.14.0 h1:MHQqLhvpNUZfw+hM3AZDYK7jxO8FZoQeQM77g8iyZjg= +github.com/invopop/jsonschema v0.14.0/go.mod h1:ygm6C2EaVNMBDPpaPlnOA2pFAxBnxGjFlMZABxm9n2I= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jedib0t/go-pretty/v6 v6.7.10 h1:B/2qW2Bkv2L6n14PP8o1kx75kWzHOQ3YTluWzg9icac= +github.com/jedib0t/go-pretty/v6 v6.7.10/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f h1:xt29M2T6STgldg+WEP51gGePQCsQvklmP2eIhPIBK3g= +github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f/go.mod h1:i4sF0l1fFnY1aiw08QQSwVAFxHEm311Me3WsU/X7nL0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lmittmann/tint v1.1.3 h1:Hv4EaHWXQr+GTFnOU4VKf8UvAtZgn0VuKT+G0wFlO3I= +github.com/lmittmann/tint v1.1.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mark3labs/mcp-go v0.54.1 h1:Ap/ptEB9FtWzFKM8NDsTA7QDxerQOC06eZigrTldVj0= +github.com/mark3labs/mcp-go v0.54.1/go.mod h1:+8WclSK1ZUweCP3hvktSji8n8ABG/95QaEkeVE/Uwas= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= +github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w= +github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/microsoft/azure-linux-dev-tools v0.1.1-0.20260604190238-0256227f5434 h1:P8RB5VXD8EX9jeGGXp9N8QP7jk2TbhrclLukB6akBNQ= +github.com/microsoft/azure-linux-dev-tools v0.1.1-0.20260604190238-0256227f5434/go.mod h1:/eGwJNU8G8/t9UgLRdfHRljRQ8QU8ILd5A/bL5ECFUg= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/opencontainers/selinux v1.15.0 h1:4Gs40e/R2FvM8PC1HPaPncLLaDor8Y2WDfk5gjU9o5M= +github.com/opencontainers/selinux v1.15.0/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ= +github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY= +github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ= +github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc= +github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU= +github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM= +github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/samber/slog-common v0.21.0 h1:Wo2hTly1Br5RjYqX/BTWJJeDnTE85oWk/7vqlpZuAUc= +github.com/samber/slog-common v0.21.0/go.mod h1:d/6OaSlzdkl9PFpfRLgn8FwY1OW6EFmPtBpsHX4MrU0= +github.com/samber/slog-multi v1.8.0 h1:E05c1wnQ+8M58oQDBABlJ4TEIJWssNgtckso3zlaLlI= +github.com/samber/slog-multi v1.8.0/go.mod h1:6+3j/ILxDvAcLD75YdQAm6iKWu6AmwlohLgQxL/2aiI= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/thejerf/slogassert v0.3.4 h1:VoTsXixRbXMrRSSxDjYTiEDCM4VWbsYPW5rB/hX24kM= +github.com/thejerf/slogassert v0.3.4/go.mod h1:0zn9ISLVKo1aPMTqcGfG1o6dWwt+Rk574GlUxHD4rs8= +github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= +github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +go.szostok.io/version v1.2.0 h1:8eMMdfsonjbibwZRLJ8TnrErY8bThFTQsZYV16mcXms= +go.szostok.io/version v1.2.0/go.mod h1:EiU0gPxaXb6MZ+apSN0WgDO6F4JXyC99k9PIXf2k2E8= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s= +go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/scripts/ci/render-specs-check/resolve_azldev_version.py b/scripts/ci/render-specs-check/resolve_azldev_version.py new file mode 100644 index 00000000000..6fbb6af9a00 --- /dev/null +++ b/scripts/ci/render-specs-check/resolve_azldev_version.py @@ -0,0 +1,318 @@ +"""Resolve the selected azldev Go module to an immutable Git hash. + +Resolved values are printed as ``key=value`` records; diagnostics go to stderr. +The ``hash`` command prints only the hash for local and non-PR callers. +""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import subprocess +import sys +import tempfile +import time +from dataclasses import dataclass +from pathlib import Path + +VERSION_RE = re.compile(r"[0-9A-Za-z._+-]+") +REPO_RE = re.compile(r"[A-Za-z0-9._-]+/[A-Za-z0-9._-]+") +SHA_RE = re.compile(r"[0-9a-f]{40}") +AZLDEV_MODULE = "github.com/microsoft/azure-linux-dev-tools" +AZLDEV_REPOSITORY = f"https://{AZLDEV_MODULE}" +MERGE_ATTEMPTS = 30 +CONTENT_ATTEMPTS = 5 +RETRY_SECONDS = 4 +GH_API_TIMEOUT_SECONDS = 15 +GO_COMMAND_TIMEOUT_SECONDS = 60 +GITHUB_API_VERSION = "2026-03-10" +PR_FIELD_COUNT = 3 + + +@dataclass(frozen=True, slots=True) +class PullRequest: + """Expected pull request snapshot.""" + + repo: str + number: int + head_sha: str + base_sha: str + + +class ResolutionError(RuntimeError): + """Raised when the post-merge version cannot be resolved safely.""" + + +class GitHubApiError(ResolutionError): + """Raised when a GitHub API request fails.""" + + +def _log(message: str) -> None: + """Print a diagnostic without contaminating machine-readable stdout.""" + print(message, file=sys.stderr) + + +def validate_version(value: str, source: str) -> str: + """Return a validated, single-token azldev version.""" + version = value.rstrip("\r\n") + if not VERSION_RE.fullmatch(version): + message = f"{source} is empty or contains unexpected characters" + raise ResolutionError(message) + return version + + +def _run_go(*args: str) -> str: + """Run Go with toolchain downloads disabled and return stdout.""" + environment = os.environ.copy() + environment["GOTOOLCHAIN"] = "local" + try: + result = subprocess.run( + ["go", *args], + capture_output=True, + text=True, + check=False, + timeout=GO_COMMAND_TIMEOUT_SECONDS, + env=environment, + ) + except (OSError, subprocess.TimeoutExpired) as error: + message = f"could not run go {' '.join(args)}: {error}" + raise ResolutionError(message) from error + if result.returncode != 0: + detail = result.stderr.strip() or f"go exited with {result.returncode}" + message = f"go {' '.join(args)} failed: {detail}" + raise ResolutionError(message) + return result.stdout + + +def read_version(path: Path) -> str: + """Read Go's selected azldev version without following an untrusted symlink.""" + if path.is_symlink(): + message = f"{path} must be a regular file, not a symlink" + raise ResolutionError(message) + if not path.is_file(): + message = f"{path} must be a regular file" + raise ResolutionError(message) + version = _run_go( + "list", + "-mod=readonly", + f"-modfile={path.resolve()}", + "-m", + "-f", + "{{.Version}}", + AZLDEV_MODULE, + ) + return validate_version(version, f"{path} azldev version") + + +def _version_from_content(content: str, source: str) -> str: + """Parse an API-provided go.mod through Go and return its azldev version.""" + try: + with tempfile.TemporaryDirectory() as directory: + modfile = Path(directory, "go.mod") + modfile.write_text(content, encoding="utf-8") + return validate_version(read_version(modfile), f"{source} azldev version") + except (OSError, UnicodeError) as error: + message = f"could not parse {source}: {error}" + raise ResolutionError(message) from error + + +def resolve_hash(version: str) -> str: + """Resolve an azldev module version to its full Microsoft Git revision.""" + version = validate_version(version, "azldev version") + try: + module = json.loads(_run_go("mod", "download", "-json", f"{AZLDEV_MODULE}@{version}")) + except json.JSONDecodeError as error: + message = f"go returned invalid JSON while resolving {AZLDEV_MODULE}@{version}" + raise ResolutionError(message) from error + origin = module.get("Origin", {}) if isinstance(module, dict) else {} + if not isinstance(origin, dict) or origin.get("VCS") != "git" or origin.get("URL") != AZLDEV_REPOSITORY: + message = f"Go resolved {AZLDEV_MODULE}@{version} from an unexpected source" + raise ResolutionError(message) + revision = origin.get("Hash", "") + if not isinstance(revision, str) or not SHA_RE.fullmatch(revision): + message = f"Go did not resolve {AZLDEV_MODULE}@{version} to a full Git hash" + raise ResolutionError(message) + return revision + + +def validate_inputs(pull_request: PullRequest) -> None: + """Validate values used in API paths and workflow output.""" + if not REPO_RE.fullmatch(pull_request.repo): + message = f"repo is not a valid owner/repo: {pull_request.repo!r}" + raise ResolutionError(message) + if pull_request.number < 1: + message = f"pr-number is not a positive integer: {pull_request.number!r}" + raise ResolutionError(message) + if not SHA_RE.fullmatch(pull_request.head_sha): + message = f"head-sha is not a 40-character lowercase hex SHA: {pull_request.head_sha!r}" + raise ResolutionError(message) + if not SHA_RE.fullmatch(pull_request.base_sha): + message = f"base-sha is not a 40-character lowercase hex SHA: {pull_request.base_sha!r}" + raise ResolutionError(message) + + +def github_api(endpoint: str, *options: str) -> str: + """Call GitHub through the authenticated gh CLI and return stdout.""" + command = ["gh", "api", "-H", f"X-GitHub-Api-Version: {GITHUB_API_VERSION}", endpoint, *options] + try: + result = subprocess.run( + command, + capture_output=True, + text=True, + check=False, + timeout=GH_API_TIMEOUT_SECONDS, + ) + except subprocess.TimeoutExpired as error: + message = f"gh api timed out after {GH_API_TIMEOUT_SECONDS} seconds" + raise GitHubApiError(message) from error + if result.returncode != 0: + detail = result.stderr.strip() or f"gh api exited with {result.returncode}" + raise GitHubApiError(detail) + return result.stdout + + +def _validate_pr_snapshot(fields: list[str], pull_request: PullRequest) -> str: + """Validate live PR fields against the trusted workflow snapshot.""" + if len(fields) != PR_FIELD_COUNT: + message = "GitHub returned an invalid PR response" + raise ResolutionError(message) + polled_head, polled_base, mergeable = fields + if polled_head != pull_request.head_sha: + message = f"PR head advanced from {pull_request.head_sha!r} to {polled_head!r}; a newer run supersedes this one" + raise ResolutionError(message) + if polled_base != pull_request.base_sha: + message = f"PR base advanced from {pull_request.base_sha!r} to {polled_base!r}; a newer run must validate it" + raise ResolutionError(message) + return mergeable + + +def _wait_for_merge_ref(pull_request: PullRequest, *, wait_for_merge: bool) -> None: + """Validate the live PR snapshot and optionally wait for its merge ref.""" + endpoint = f"repos/{pull_request.repo}/pulls/{pull_request.number}" + query = "[.head.sha, .base.sha, (.mergeable | tostring)] | @tsv" + last_error: GitHubApiError | None = None + for attempt in range(1, MERGE_ATTEMPTS + 1): + try: + fields = github_api(endpoint, "--jq", query).rstrip("\r\n").split("\t") + except GitHubApiError as error: + last_error = error + _log(f"PR API call failed ({attempt}/{MERGE_ATTEMPTS}): {error}; retrying...") + else: + last_error = None + mergeable = _validate_pr_snapshot(fields, pull_request) + if not wait_for_merge: + return + if mergeable == "false": + message = "PR conflicts with the base branch" + raise ResolutionError(message) + if mergeable == "true": + return + _log(f"Waiting for GitHub to publish the merge ref ({attempt}/{MERGE_ATTEMPTS})...") + if attempt < MERGE_ATTEMPTS: + time.sleep(RETRY_SECONDS) + + message = ( + f"GitHub API failed after {MERGE_ATTEMPTS} attempts: {last_error}" + if last_error + else f"GitHub did not publish the merge ref after {MERGE_ATTEMPTS} attempts" + ) + raise ResolutionError(message) + + +def _read_merged_version(pull_request: PullRequest) -> str: + """Read the azldev pin from the pull request merge ref.""" + # A moving merge ref matters only if another pin change lands; that supersedes this run or conflicts. + merge_ref = f"refs/pull/{pull_request.number}/merge" + endpoint = f"repos/{pull_request.repo}/contents/go.mod?ref={merge_ref}" + last_error: GitHubApiError | None = None + for attempt in range(1, CONTENT_ATTEMPTS + 1): + try: + content = github_api(endpoint, "-H", "Accept: application/vnd.github.raw") + except GitHubApiError as error: + last_error = error + _log(f"Could not read the post-merge go.mod ({attempt}/{CONTENT_ATTEMPTS}): {error}; retrying...") + if attempt < CONTENT_ATTEMPTS: + time.sleep(RETRY_SECONDS) + else: + return _version_from_content(content, "post-merge go.mod") + + message = f"could not read go.mod from the pull request merge ref: {last_error}" + raise ResolutionError(message) + + +def resolve_version( + base_version: str, + head_version: str, + *, + pull_request: PullRequest, +) -> tuple[str, bool]: + """Return the post-merge version and whether all specs must be rendered.""" + validate_inputs(pull_request) + base_version = validate_version(base_version, "base version") + head_version = validate_version(head_version, "PR-head version") + + versions_differ = head_version != base_version + _wait_for_merge_ref(pull_request, wait_for_merge=versions_differ) + if not versions_differ: + return base_version, False + + merged_version = _read_merged_version(pull_request) + return merged_version, merged_version != base_version + + +def parse_args() -> argparse.Namespace: + """Parse command-line arguments.""" + parser = argparse.ArgumentParser(description=__doc__) + commands = parser.add_subparsers(dest="command", required=True) + + hash_parser = commands.add_parser("hash", help="resolve one go.mod to an azldev hash") + hash_parser.add_argument("modfile", type=Path) + + post_merge_parser = commands.add_parser("post-merge", help="resolve the hash after a pull request merges") + post_merge_parser.add_argument("--repo", required=True, help="GitHub owner/repository") + post_merge_parser.add_argument("--pr-number", required=True, type=int, help="pull request number") + post_merge_parser.add_argument("--head-sha", required=True, help="expected PR head commit") + post_merge_parser.add_argument("--base-sha", required=True, help="trusted base commit") + post_merge_parser.add_argument("--base-modfile", required=True, type=Path, help="base Go module file") + post_merge_parser.add_argument("--head-modfile", required=True, type=Path, help="PR-head Go module file") + return parser.parse_args() + + +def main() -> int: + """Resolve the version and emit machine-readable output.""" + args = parse_args() + try: + if args.command == "hash": + print(resolve_hash(read_version(args.modfile))) + return 0 + + pull_request = PullRequest( + repo=args.repo, + number=args.pr_number, + head_sha=args.head_sha, + base_sha=args.base_sha, + ) + base_version = read_version(args.base_modfile) + head_version = read_version(args.head_modfile) + version, render_all = resolve_version( + base_version=base_version, + head_version=head_version, + pull_request=pull_request, + ) + revision = resolve_hash(version) + _log( + f"Resolved azldev version: {version} ({revision}); render all: {str(render_all).lower()} " + f"(base: {base_version}, PR head: {head_version})" + ) + print(f"azldev-hash={revision}") + print(f"render-all={str(render_all).lower()}") + except ResolutionError as error: + _log(f"Error: {error}") + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/ci/render-specs-check/tests/conftest.py b/scripts/ci/render-specs-check/tests/conftest.py new file mode 100644 index 00000000000..1bb90a9106f --- /dev/null +++ b/scripts/ci/render-specs-check/tests/conftest.py @@ -0,0 +1,8 @@ +"""Configure imports for render-specs-check tests.""" + +from __future__ import annotations + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) diff --git a/scripts/ci/render-specs-check/tests/requirements.txt b/scripts/ci/render-specs-check/tests/requirements.txt new file mode 100644 index 00000000000..b3b2b3ca4cc --- /dev/null +++ b/scripts/ci/render-specs-check/tests/requirements.txt @@ -0,0 +1 @@ +pytest==9.0.3 diff --git a/scripts/ci/render-specs-check/tests/test_resolve_azldev_version.py b/scripts/ci/render-specs-check/tests/test_resolve_azldev_version.py new file mode 100644 index 00000000000..ab4a6fc8295 --- /dev/null +++ b/scripts/ci/render-specs-check/tests/test_resolve_azldev_version.py @@ -0,0 +1,307 @@ +"""Tests for post-merge azldev version resolution.""" + +from __future__ import annotations + +import subprocess +import sys +from typing import TYPE_CHECKING +from unittest.mock import Mock + +import pytest +import resolve_azldev_version as resolver + +if TYPE_CHECKING: + from pathlib import Path + + +def pull_request(*, number: int = 1, head_sha: str = "a" * 40, base_sha: str = "b" * 40) -> resolver.PullRequest: + """Build a test pull request snapshot.""" + return resolver.PullRequest( + repo="microsoft/azurelinux", + number=number, + head_sha=head_sha, + base_sha=base_sha, + ) + + +def test_equal_versions_skip_merge_poll(monkeypatch: pytest.MonkeyPatch) -> None: + """Validate one PR snapshot without polling for a test-merge commit.""" + github_api = Mock(return_value=f"{'a' * 40}\t{'b' * 40}\ttrue\n") + monkeypatch.setattr(resolver, "github_api", github_api) + + version, render_all = resolver.resolve_version( + "v1.2.3", + "v1.2.3", + pull_request=pull_request(), + ) + + if version != "v1.2.3" or render_all: + pytest.fail(f"unexpected resolution: version={version!r}, render_all={render_all!r}") + if github_api.call_count != 1: + pytest.fail(f"expected one PR snapshot request, got {github_api.call_count}") + + +def test_read_version_uses_selected_azldev_module( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + """Read the selected azldev module version through Go.""" + modfile = tmp_path / "go.mod" + modfile.write_text("module example.com/test\n", encoding="ascii") + run = Mock( + return_value=subprocess.CompletedProcess( + [], + 0, + stdout="v1.2.3\n", + stderr="", + ) + ) + monkeypatch.setattr(resolver.subprocess, "run", run) + + if resolver.read_version(modfile) != "v1.2.3": + pytest.fail("unexpected azldev tool version") + command = run.call_args.args[0] + if command[:2] != ["go", "list"] or "-mod=readonly" not in command: + pytest.fail(f"unexpected Go command: {command}") + + +def test_resolve_hash_validates_module_origin(monkeypatch: pytest.MonkeyPatch) -> None: + """Resolve a selected module version to its full Microsoft repository hash.""" + revision = "c" * 40 + run = Mock( + return_value=subprocess.CompletedProcess( + [], + 0, + stdout=( + '{"Origin":{"VCS":"git",' + '"URL":"https://github.com/microsoft/azure-linux-dev-tools",' + f'"Hash":"{revision}"}}}}' + ), + stderr="", + ) + ) + monkeypatch.setattr(resolver.subprocess, "run", run) + + if resolver.resolve_hash("v1.2.3") != revision: + pytest.fail("unexpected resolved hash") + + +def test_hash_cli_emits_resolved_hash( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + """Print the hash resolved from the module tool requirement.""" + revision = "d" * 40 + monkeypatch.setattr(resolver, "read_version", Mock(return_value="v1.2.3")) + monkeypatch.setattr(resolver, "resolve_hash", Mock(return_value=revision)) + monkeypatch.setattr( + sys, + "argv", + ["resolve_azldev_version.py", "hash", str(tmp_path / "go.mod")], + ) + + if resolver.main() != 0: + pytest.fail("hash resolver unexpectedly failed") + if capsys.readouterr().out != f"{revision}\n": + pytest.fail("hash resolver emitted unexpected output") + + +def test_hash_cli_rejects_multiline_go_output( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + """Reject an injected output record in Go's selected module version.""" + modfile = tmp_path / "go.mod" + modfile.write_text("module example.com/test\n", encoding="ascii") + injected_hash = "e" * 40 + monkeypatch.setattr( + resolver.subprocess, + "run", + Mock( + return_value=subprocess.CompletedProcess( + [], + 0, + stdout=f"v1.2.3\nazldev-hash={injected_hash}\n", + stderr="", + ) + ), + ) + monkeypatch.setattr( + sys, + "argv", + ["resolve_azldev_version.py", "hash", str(modfile)], + ) + + if resolver.main() == 0: + pytest.fail("multiline Go output unexpectedly succeeded") + if capsys.readouterr().out: + pytest.fail("resolver emitted machine-readable output after rejecting Go output") + + +def test_differing_versions_use_test_merge_version(monkeypatch: pytest.MonkeyPatch) -> None: + """Use the test-merge pin when the base and PR-head pins differ.""" + github_api = Mock( + side_effect=[ + f"{'a' * 40}\t{'c' * 40}\tnull\n", + f"{'a' * 40}\t{'c' * 40}\ttrue\n", + "v2.0.0\n", + ] + ) + monkeypatch.setattr(resolver, "github_api", github_api) + monkeypatch.setattr(resolver, "_version_from_content", Mock(return_value="v2.0.0")) + monkeypatch.setattr(resolver.time, "sleep", Mock()) + + version, render_all = resolver.resolve_version( + "v1.2.3", + "v2.0.0", + pull_request=pull_request(base_sha="c" * 40), + ) + + if version != "v2.0.0" or not render_all: + pytest.fail(f"unexpected resolution: version={version!r}, render_all={render_all!r}") + content_endpoint = github_api.call_args_list[-1].args[0] + if content_endpoint != "repos/microsoft/azurelinux/contents/go.mod?ref=refs/pull/1/merge": + pytest.fail(f"unexpected merge-ref endpoint: {content_endpoint}") + + +def test_behind_base_uses_base_version_without_full_render(monkeypatch: pytest.MonkeyPatch) -> None: + """Avoid a full render when the PR merely has an older pin than base.""" + github_api = Mock(side_effect=[f"{'a' * 40}\t{'c' * 40}\ttrue\n", "v2.0.0\n"]) + monkeypatch.setattr(resolver, "github_api", github_api) + monkeypatch.setattr(resolver, "_version_from_content", Mock(return_value="v2.0.0")) + + version, render_all = resolver.resolve_version( + "v2.0.0", + "v1.2.3", + pull_request=pull_request(base_sha="c" * 40), + ) + + if version != "v2.0.0" or render_all: + pytest.fail(f"unexpected resolution: version={version!r}, render_all={render_all!r}") + + +@pytest.mark.parametrize( + ("pr_number", "expected_head", "expected_base", "message"), + [ + (0, "a" * 40, "b" * 40, "positive integer"), + (1, "not-a-sha\n", "b" * 40, "head-sha.*40-character lowercase hex SHA"), + (1, "a" * 40, "not-a-sha\n", "base-sha.*40-character lowercase hex SHA"), + ], +) +def test_invalid_inputs_fail_before_github( + monkeypatch: pytest.MonkeyPatch, + pr_number: int, + expected_head: str, + expected_base: str, + message: str, +) -> None: + """Reject malformed reusable-workflow inputs before calling GitHub.""" + github_api = Mock() + monkeypatch.setattr(resolver, "github_api", github_api) + + with pytest.raises(resolver.ResolutionError, match=message): + resolver.resolve_version( + "v1.2.3", + "v1.2.3", + pull_request=pull_request( + number=pr_number, + head_sha=expected_head, + base_sha=expected_base, + ), + ) + + github_api.assert_not_called() + + +def test_stale_base_fails_before_equal_pin_result(monkeypatch: pytest.MonkeyPatch) -> None: + """Reject a live PR base that differs from the trusted checkout.""" + github_api = Mock(return_value=f"{'a' * 40}\t{'c' * 40}\ttrue\n") + monkeypatch.setattr(resolver, "github_api", github_api) + + with pytest.raises(resolver.ResolutionError, match="PR base advanced"): + resolver.resolve_version( + "v1.2.3", + "v1.2.3", + pull_request=pull_request(), + ) + + +def test_github_api_timeout_is_retryable(monkeypatch: pytest.MonkeyPatch) -> None: + """Translate a hung gh process into the resolver's retryable error.""" + run = Mock(side_effect=subprocess.TimeoutExpired(["gh", "api"], 1)) + monkeypatch.setattr(resolver.subprocess, "run", run) + + with pytest.raises(resolver.GitHubApiError, match="timed out"): + resolver.github_api("repos/microsoft/azurelinux/pulls/1") + + +def test_github_api_pins_current_rest_version(monkeypatch: pytest.MonkeyPatch) -> None: + """Request the REST contract used by the merge-ref implementation.""" + result = subprocess.CompletedProcess([], 0, stdout="ok", stderr="") + run = Mock(return_value=result) + monkeypatch.setattr(resolver.subprocess, "run", run) + + if resolver.github_api("repos/microsoft/azurelinux/pulls/1") != "ok": + pytest.fail("unexpected API output") + command = run.call_args.args[0] + expected_header = f"X-GitHub-Api-Version: {resolver.GITHUB_API_VERSION}" + if expected_header not in command: + pytest.fail(f"API version header missing from command: {command}") + + +def test_main_rejects_module_without_azldev_requirement( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + """Reject a module without an azldev requirement before emitting output.""" + base_file = tmp_path / "base.mod" + head_file = tmp_path / "head.mod" + base_file.write_text("module example.com/base\n", encoding="ascii") + head_file.write_text("module example.com/head\n", encoding="ascii") + github_api = Mock() + monkeypatch.setattr(resolver, "github_api", github_api) + monkeypatch.setattr( + resolver, + "_run_go", + Mock(side_effect=resolver.ResolutionError("azldev is not a known dependency")), + ) + monkeypatch.setattr( + sys, + "argv", + [ + "resolve_azldev_version.py", + "post-merge", + "--repo", + "microsoft/azurelinux", + "--pr-number", + "1", + "--head-sha", + "a" * 40, + "--base-sha", + "b" * 40, + "--base-modfile", + str(base_file), + "--head-modfile", + str(head_file), + ], + ) + + if resolver.main() == 0: + pytest.fail("module without azldev requirement unexpectedly succeeded") + if capsys.readouterr().out: + pytest.fail("resolver emitted machine-readable output after rejecting the version") + github_api.assert_not_called() + + +def test_read_version_rejects_symlink(tmp_path: Path) -> None: + """Reject a go.mod symlink before parsing its target.""" + target = tmp_path / "target" + link = tmp_path / "version" + target.write_text("v1.2.3\n", encoding="ascii") + link.symlink_to(target) + + with pytest.raises(resolver.ResolutionError, match="symlink"): + resolver.read_version(link)