diff --git a/.github/workflows/ado/package-build.yml b/.github/workflows/ado/package-build.yml index 8dae92e7b0c..823f3eaee24 100644 --- a/.github/workflows/ado/package-build.yml +++ b/.github/workflows/ado/package-build.yml @@ -36,15 +36,15 @@ # Variable Group (ADO Pipelines > Library): # Name: "ControlTower-PRCheck" # Required variables: -# - ApiAudience : Entra ID audience URI for the Control Tower app -# - ApiBaseAFDUrl : Base URL of the Control Tower service (Azure Front Door endpoint) +# - ApiAudienceDev : DEV Entra ID audience URI +# - ApiBaseAFDUrlDev : DEV Azure Front Door endpoint base URL # Trigger controlled by ADO branch policy — not YAML triggers. trigger: none pr: none -# Variables extracted from this group: ApiAudience, ApiBaseAFDUrl. +# Variables extracted from this group: ApiAudienceDev, ApiBaseAFDUrlDev. variables: - group: ControlTower-PRCheck @@ -78,8 +78,8 @@ extends: stages: - template: /.github/workflows/ado/templates/package-build-stages.yml@self parameters: - apiAudience: $(ApiAudience) - apiBaseAFDUrl: $(ApiBaseAFDUrl) + apiAudience: $(ApiAudienceDev) + apiBaseAFDUrl: $(ApiBaseAFDUrlDev) artifactBaseName: packagebuild containerImage: mcr.microsoft.com/onebranch/azurelinux/build:3.0 outputDirectory: $(Build.ArtifactStagingDirectory)/output diff --git a/.github/workflows/ado/pr-check-ct.yml b/.github/workflows/ado/pr-check-ct.yml index d82aa146d20..6e16885e97e 100644 --- a/.github/workflows/ado/pr-check-ct.yml +++ b/.github/workflows/ado/pr-check-ct.yml @@ -2,12 +2,14 @@ # Licensed under the MIT License. # # Wrapper pipeline — passed to ADO as the entry point for the PR Control Tower -# check. It calls Control Tower 'prcheck' (which uploads the changed -# components' missing lookaside sources) and then submits a *scratch* Control -# Tower build of those components, WAITS for that build to finish, and fails -# the check if the build fails (or is rejected). The package build runs in -# Control Tower's own sandbox; checkout-controlled pipeline helpers still run -# on the CI agent under the configured service connection. +# check. For each configured deployment, it calls Control Tower 'prcheck' +# (which uploads the changed components' missing lookaside sources). DEV then +# submits a *scratch* Control Tower build of those components and WAITS for it +# to finish; STAGING and PROD skip package builds. DEV and PROD API failures +# fail the check, while STAGING prcheck failures are non-blocking. Deployment +# jobs run independently in parallel. Package builds run in Control Tower's own +# sandbox; checkout-controlled pipeline helpers still run on CI agents under +# the deployment-specific service connections. # # This file owns all OneBranch-specific wiring (governed templates repo, # NonOfficial variant, featureFlags) and delegates the actual stages/jobs/steps @@ -21,9 +23,8 @@ # gate: this pipeline does NOT auto-run on every PR push, and a maintainer # triggers it only after inspecting the diff. See the prerequisite below. # -# NonOfficial: this is PR validation. It calls the Control Tower DEV endpoint -# (via the DEV service connection) and produces only scratch builds; it is not -# a production-classified pipeline. +# TEST-ONLY OVERRIDE: use NonOfficial governance while validating this pipeline +# from the temporary draft PR. Do not merge this override into the 4.0 branch. # # Helper scripts live under: # - scripts/ci/control-tower/ - (Control Tower client + submit script). @@ -31,13 +32,13 @@ # with the GitHub Actions PR gates and the Control Tower pipelines). # # Prerequisites (ADO / Azure Portal): -# 1. Entra ID App Registration with audience URI +# 1. Entra ID App Registrations with audience URIs # "api://" (see variable group below). -# 2. Federated identity credential on the app registration for the ADO -# service connection (issuer: https://vstoken.dev.azure.com/, +# 2. Federated identity credentials on the app registrations for the ADO +# service connections (issuer: https://vstoken.dev.azure.com/, # subject: sc:////). -# 3. ARM service connection in ADO project settings using Workload Identity -# Federation (manual). +# 3. DEV, STAGING, and PROD ARM service connections in ADO project settings +# using Workload Identity Federation (manual). # 4. REVIEWER-GATED trigger (configured in ADO, not here): wire this pipeline # as a build-validation check that does NOT automatically queue on every # PR push (e.g. an optional/manual build-validation policy, or a manual @@ -46,15 +47,19 @@ # Variable Group (ADO Pipelines > Library): # Name: "ControlTower-PRCheck" # Required variables: -# - ApiAudience : Entra ID audience URI for the Control Tower app -# - ApiBaseAFDUrl : Base URL of the Control Tower service (Azure Front Door endpoint) +# - ApiAudienceDev : DEV Entra ID audience URI +# - ApiAudienceStaging : STAGING Entra ID audience URI +# - ApiAudienceProd : PROD Entra ID audience URI +# - ApiBaseAFDUrlDev : DEV Azure Front Door endpoint base URL +# - ApiBaseAFDUrlStaging : STAGING Azure Front Door endpoint base URL +# - ApiBaseAFDUrlProd : PROD Azure Front Door endpoint base URL # Trigger controlled by ADO branch policy — not YAML triggers. trigger: none pr: none -# Variables extracted from this group: ApiAudience, ApiBaseAFDUrl. +# Variables extracted from this group: deployment audiences and endpoint URLs. variables: - group: ControlTower-PRCheck @@ -88,10 +93,27 @@ extends: stages: - template: /.github/workflows/ado/templates/pr-check-ct-stages.yml@self parameters: - apiAudience: $(ApiAudience) - apiBaseAFDUrl: $(ApiBaseAFDUrl) artifactBaseName: prcheckct containerImage: mcr.microsoft.com/onebranch/azurelinux/build:3.0 + deployments: + DEV: + apiAudience: $(ApiAudienceDev) + apiBaseAFDUrl: $(ApiBaseAFDUrlDev) + continueOnError: false + serviceConnection: CT-Endpoints-Access-ServiceConnection-DEV + skipPackageBuilds: false + STAGING: + apiAudience: $(ApiAudienceStaging) + apiBaseAFDUrl: $(ApiBaseAFDUrlStaging) + continueOnError: true + serviceConnection: CT-Endpoints-Access-ServiceConnection-STAGING + skipPackageBuilds: true + PROD: + apiAudience: $(ApiAudienceProd) + apiBaseAFDUrl: $(ApiBaseAFDUrlProd) + continueOnError: false + serviceConnection: CT-Endpoints-Access-ServiceConnection-PROD + skipPackageBuilds: true outputDirectory: $(Build.ArtifactStagingDirectory)/output # Distro-config RPM build-environment for the 4.0 branch. packageEnvironment: "4.0" @@ -103,5 +125,4 @@ extends: # Raise both together if a legitimate build is being killed. pollTimeoutSeconds: 21600 poolType: linux - serviceConnection: CT-Endpoints-Access-ServiceConnection-DEV timeoutInMinutes: 420 diff --git a/.github/workflows/ado/templates/package-build-stages.yml b/.github/workflows/ado/templates/package-build-stages.yml index 6bc19794539..a60eeb4991a 100644 --- a/.github/workflows/ado/templates/package-build-stages.yml +++ b/.github/workflows/ado/templates/package-build-stages.yml @@ -89,6 +89,8 @@ stages: API_BASE_URL: ${{ parameters.apiBaseAFDUrl }} CHANGED_COMPONENTS_FILE: $(changedComponentsFile) PACKAGE_ENVIRONMENT: $(packageEnvironment) + # Secret predefined variables are not auto-exposed. + SYSTEM_ACCESSTOKEN: $(System.AccessToken) # Build.Reason is predefined and auto-exposed as $BUILD_REASON; # reference it directly and do not map it here (env: overrides # of reserved variables are silently ignored by the agent). diff --git a/.github/workflows/ado/templates/pr-check-ct-stages.yml b/.github/workflows/ado/templates/pr-check-ct-stages.yml index 7b3fd5b9c34..f8196edad1e 100644 --- a/.github/workflows/ado/templates/pr-check-ct-stages.yml +++ b/.github/workflows/ado/templates/pr-check-ct-stages.yml @@ -6,22 +6,26 @@ # wrapper-coupled knobs as parameters. The wrapper at # .github/workflows/ado/pr-check-ct.yml supplies concrete values. # -# Per PR: gather the change-set info (get-changes-info, mode=pr: full history, -# deps, commit range, lock + render verification, changed-component set), then -# call Control Tower 'prcheck' (uploads the changed components' missing -# lookaside sources) and submit a *scratch* Control Tower build of the PR head -# for those components, waiting for it to finish. NO PR-controlled code is built -# on the CI agent -- only read-only change detection runs here. +# Per PR and deployment: gather the change-set info (get-changes-info, mode=pr: +# full history, deps, commit range, lock + render verification, +# changed-component set), then call Control Tower 'prcheck' (uploads the changed +# components' missing lookaside sources). When skipPackageBuilds is false, also +# submit a *scratch* Control Tower build of the PR head for those components and +# wait for it to finish. Each deployment runs in a separate parallel job. +# Checkout-controlled helpers execute on the CI agent under the +# deployment-specific service connection; package builds run in Control Tower's +# sandbox. parameters: - - name: apiAudience - type: string - - name: apiBaseAFDUrl - type: string - name: artifactBaseName type: string - name: containerImage type: string + # Named map of deployment identifier to apiAudience, apiBaseAFDUrl, + # continueOnError, serviceConnection, and skipPackageBuilds. + # Identifiers must be valid Azure Pipelines job-name suffixes. + - name: deployments + type: object - name: outputDirectory type: string # Distro-config RPM build-environment used by Control Tower. @@ -37,120 +41,131 @@ parameters: - name: poolType type: string default: linux - - name: serviceConnection - type: string - name: timeoutInMinutes type: number stages: - stage: PRCheckCT jobs: - - job: PRCheckCT - # Fail-loud: a failed submission, an immediate Control Tower rejection, - # or a build that fails (or never reaches a terminal state) turns the PR - # check red. The build runs in Control Tower's own sandbox -- NOT on this - # agent -- but this pipeline WAITS for it to finish. Size the timeout to - # cover the FULL build: it must exceed pollTimeoutSeconds (6h default). - timeoutInMinutes: ${{ parameters.timeoutInMinutes }} - pool: - type: ${{ parameters.poolType }} - variables: - - name: LinuxContainerImage - value: ${{ parameters.containerImage }} - - name: ob_artifactBaseName - value: ${{ parameters.artifactBaseName }} - - name: ob_outputDirectory - value: ${{ parameters.outputDirectory }} - - name: packageEnvironment - value: ${{ parameters.packageEnvironment }} - - name: pollTimeoutSeconds - value: ${{ parameters.pollTimeoutSeconds }} - steps: - # Sets job variables: - # - baseCommit - # - changedComponentsFile - # - sourceCommit - - template: steps/get-changes-info.yml - parameters: - mode: pr + - ${{ each deployment in parameters.deployments }}: + - job: PRCheckCT_${{ deployment.key }} + displayName: "PR Control Tower check (${{ deployment.key }})" + # Fail-loud: a failed submission, an immediate Control Tower + # rejection, or a build that fails (or never reaches a terminal + # state) turns the PR check red unless that deployment explicitly + # allows prcheck errors. Deployment jobs run independently in + # parallel, and any failed job fails the stage. The build runs in + # Control Tower's own sandbox -- NOT on this agent -- but this + # pipeline WAITS for it to finish. Size the timeout to cover the + # FULL build: it must exceed pollTimeoutSeconds (6h default). + timeoutInMinutes: ${{ parameters.timeoutInMinutes }} + pool: + type: ${{ parameters.poolType }} + variables: + - name: LinuxContainerImage + value: ${{ parameters.containerImage }} + - name: ob_artifactBaseName + value: ${{ parameters.artifactBaseName }}-${{ deployment.key }} + - name: ob_outputDirectory + value: ${{ parameters.outputDirectory }}/${{ deployment.key }} + - name: packageEnvironment + value: ${{ parameters.packageEnvironment }} + - name: pollTimeoutSeconds + value: ${{ parameters.pollTimeoutSeconds }} + steps: + # Sets job variables: + # - baseCommit + # - changedComponentsFile + # - sourceCommit + - template: steps/get-changes-info.yml + parameters: + mode: pr - - template: steps/install-deps.yml - parameters: - installControlTowerClient: true + - template: steps/install-deps.yml + parameters: + installControlTowerClient: true - # Call Control Tower 'prcheck' BEFORE the build. Among other things, - # prcheck uploads the missing lookaside sources for the changed - # components, so they are available when the scratch build below - # fetches them. - - task: AzureCLI@2 - displayName: "Call Control Tower 'prcheck' API" - inputs: - azureSubscription: ${{ parameters.serviceConnection }} - scriptType: bash - scriptLocation: inlineScript - inlineScript: | - set -euo pipefail + # Call Control Tower 'prcheck' BEFORE the build. Among other + # things, prcheck uploads the missing lookaside sources for the + # changed components, so they are available when the scratch + # build below fetches them. + - task: AzureCLI@2 + displayName: "Call Control Tower 'prcheck' API (${{ deployment.key }})" + continueOnError: ${{ deployment.value.continueOnError }} + inputs: + azureSubscription: ${{ deployment.value.serviceConnection }} + scriptType: bash + scriptLocation: inlineScript + inlineScript: | + set -euo pipefail - python3 scripts/ci/control-tower/run_prcheck.py \ - --api-audience "$API_AUDIENCE" \ - --api-base-url "$API_BASE_URL" \ - --build-reason "$BUILD_REASON" \ - --changed-components-file "$CHANGED_COMPONENTS_FILE" \ - --source-commit "$SOURCE_COMMIT" \ - --repo-uri "$BUILD_REPOSITORY_URI" - env: - # apiAudience / apiBaseAFDUrl are referenced from the parameters - # directly (NOT surfaced as same-named job variables): the wrapper - # passes $(ApiAudience) / $(ApiBaseAFDUrl) from the variable group, - # and a job variable of the same (case-insensitive) name would - # self-reference -> "cyclical reference" and never resolve. - API_AUDIENCE: ${{ parameters.apiAudience }} - API_BASE_URL: ${{ parameters.apiBaseAFDUrl }} - # TODO: Base commit is not used yet. Will be needed once we move - # detection of affected components to CT. ADO task: 18816 - BASE_COMMIT: $(baseCommit) - CHANGED_COMPONENTS_FILE: $(changedComponentsFile) - SOURCE_COMMIT: $(sourceCommit) - # Build.Reason and Build.Repository.Uri are predefined and - # auto-exposed as $BUILD_REASON / $BUILD_REPOSITORY_URI; reference - # them directly and do not map them here (env: overrides of - # reserved variables are silently ignored by the agent). + python3 scripts/ci/control-tower/run_prcheck.py \ + --api-audience "$API_AUDIENCE" \ + --api-base-url "$API_BASE_URL" \ + --build-reason "$BUILD_REASON" \ + --changed-components-file "$CHANGED_COMPONENTS_FILE" \ + --source-commit "$SOURCE_COMMIT" \ + --repo-uri "$BUILD_REPOSITORY_URI" + env: + # Values come directly from parameters rather than same-named + # job variables, which would create cyclical references. + API_AUDIENCE: ${{ deployment.value.apiAudience }} + API_BASE_URL: ${{ deployment.value.apiBaseAFDUrl }} + # TODO: Base commit is not used yet. Will be needed once we + # move detection of affected components to CT. ADO task: + # 18816 + BASE_COMMIT: $(baseCommit) + CHANGED_COMPONENTS_FILE: $(changedComponentsFile) + SOURCE_COMMIT: $(sourceCommit) + # Secret predefined variables are not auto-exposed. + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + # Build.Reason and Build.Repository.Uri are predefined and + # auto-exposed as $BUILD_REASON / $BUILD_REPOSITORY_URI; + # reference them directly and do not map them here (env: + # overrides of reserved variables are silently ignored by + # the agent). - # Submit a SCRATCH Control Tower build of the PR head for the changed - # components. Scratch artifacts are throwaway and are not persisted to - # a production repo. The scratch-only runner has no official endpoint - # or mode switch, preventing accidental official submissions. This is - # not an authorization boundary: PR code can modify checkout-controlled - # helpers. --wait-for-completion blocks until the build reaches a - # terminal state and fails the check on failure or poll timeout. - # - # This step assumes the pipeline is wired as a REVIEWER-GATED check in - # ADO: it should not auto-run on every PR push, so a maintainer - # eyeballs the diff before unmerged code is submitted for a build. - - task: AzureCLI@2 - displayName: "Submit scratch build to Control Tower" - inputs: - azureSubscription: ${{ parameters.serviceConnection }} - scriptType: bash - scriptLocation: inlineScript - inlineScript: | - set -euo pipefail + - ${{ if ne(deployment.value.skipPackageBuilds, true) }}: + # Submit a SCRATCH Control Tower build of the PR head for the + # changed components. Scratch artifacts are throwaway and are + # not persisted to a production repo. The scratch-only runner + # has no official endpoint or mode switch, preventing + # accidental official submissions. This is not an + # authorization boundary: PR code can modify + # checkout-controlled helpers. --wait-for-completion blocks + # until the build reaches a terminal state and fails the + # check on failure or poll timeout. + # + # This step assumes the pipeline is wired as a REVIEWER-GATED + # check in ADO: it should not auto-run on every PR push, so a + # maintainer eyeballs the diff before unmerged code is + # submitted for a build. + - task: AzureCLI@2 + displayName: "Submit scratch build to Control Tower (${{ deployment.key }})" + continueOnError: ${{ deployment.value.continueOnError }} + inputs: + azureSubscription: ${{ deployment.value.serviceConnection }} + scriptType: bash + scriptLocation: inlineScript + inlineScript: | + set -euo pipefail - python3 scripts/ci/control-tower/run_scratch_package_build.py \ - --api-audience "$API_AUDIENCE" \ - --api-base-url "$API_BASE_URL" \ - --changed-components-file "$CHANGED_COMPONENTS_FILE" \ - --environment "$PACKAGE_ENVIRONMENT" \ - --commit-sha "$SOURCE_COMMIT" \ - --wait-for-completion \ - --poll-timeout-seconds "$POLL_TIMEOUT_SECONDS" - env: - # See the prcheck step: apiAudience / apiBaseAFDUrl come from the - # parameters directly to avoid a cyclical self-reference with the - # same-named variable-group variables. - API_AUDIENCE: ${{ parameters.apiAudience }} - API_BASE_URL: ${{ parameters.apiBaseAFDUrl }} - CHANGED_COMPONENTS_FILE: $(changedComponentsFile) - PACKAGE_ENVIRONMENT: $(packageEnvironment) - POLL_TIMEOUT_SECONDS: $(pollTimeoutSeconds) - SOURCE_COMMIT: $(sourceCommit) + python3 scripts/ci/control-tower/run_scratch_package_build.py \ + --api-audience "$API_AUDIENCE" \ + --api-base-url "$API_BASE_URL" \ + --changed-components-file "$CHANGED_COMPONENTS_FILE" \ + --environment "$PACKAGE_ENVIRONMENT" \ + --commit-sha "$SOURCE_COMMIT" \ + --wait-for-completion \ + --poll-timeout-seconds "$POLL_TIMEOUT_SECONDS" + env: + # See the prcheck step: parameter values are used directly + # to avoid cyclical same-named variable references. + API_AUDIENCE: ${{ deployment.value.apiAudience }} + API_BASE_URL: ${{ deployment.value.apiBaseAFDUrl }} + CHANGED_COMPONENTS_FILE: $(changedComponentsFile) + PACKAGE_ENVIRONMENT: $(packageEnvironment) + POLL_TIMEOUT_SECONDS: $(pollTimeoutSeconds) + SOURCE_COMMIT: $(sourceCommit) + # Secret predefined variables are not auto-exposed. + SYSTEM_ACCESSTOKEN: $(System.AccessToken) diff --git a/base/comps/components.toml b/base/comps/components.toml index 5185f32ce02..2fa9a322549 100644 --- a/base/comps/components.toml +++ b/base/comps/components.toml @@ -1987,6 +1987,7 @@ includes = ["**/*.comp.toml", "component-check-disablement.toml", "component-min [components.log4j] [components.logrotate] [components.logwatch] +[components.lolcat] [components.lorax] [components.lpcnetfreedv] [components.lrzsz] diff --git a/locks/lolcat.lock b/locks/lolcat.lock new file mode 100644 index 00000000000..4811675869e --- /dev/null +++ b/locks/lolcat.lock @@ -0,0 +1,6 @@ +# Managed by azldev component update. Do not edit manually. +version = 1 +import-commit = 'b7cdd8866241494b1152a6ba01d0300bb2309df0' +upstream-commit = 'b7cdd8866241494b1152a6ba01d0300bb2309df0' +input-fingerprint = 'sha256:25249ea76688b0ba3392c349e33b9381c6f7915d502843b6a861219fd3d7c95a' +resolution-input-hash = 'sha256:466421704711c4fd3c71f0b2ed715a0e61d49e3e26f3a2637fee755795849c8e' diff --git a/scripts/ci/control-tower/client.py b/scripts/ci/control-tower/client.py index 966480794d0..7b3a95217ad 100644 --- a/scripts/ci/control-tower/client.py +++ b/scripts/ci/control-tower/client.py @@ -11,25 +11,26 @@ Authentication: Requires an active Azure CLI session (e.g. via an ``AzureCLI@2`` pipeline task with a Workload Identity Federation service connection). - ``DefaultAzureCredential`` discovers the session automatically. + Azure Pipelines uses ``AzurePipelinesCredential`` for renewable WIF tokens; + local execution falls back to ``AzureCliCredential``. """ from __future__ import annotations import json +import os import time from collections.abc import Mapping, Sequence from dataclasses import dataclass from http import HTTPStatus -from typing import TYPE_CHECKING, cast +from typing import cast import requests +from azure.core.credentials import TokenCredential +from azure.identity import AzureCliCredential, AzurePipelinesCredential from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry -if TYPE_CHECKING: - from azure.identity import DefaultAzureCredential - type JsonValue = None | bool | int | float | str | Sequence[JsonValue] | Mapping[str, JsonValue] type JsonObject = dict[str, JsonValue] @@ -47,6 +48,12 @@ # unrecognized-but-valid new state. TERMINAL_STATUSES = TERMINAL_FAILURE_STATUSES | {SUCCESS_STATUS} _MAX_ERROR_BODY_CHARS = 4000 +_PIPELINE_CREDENTIAL_ENV = { + "client_id": "AZURESUBSCRIPTION_CLIENT_ID", + "service_connection_id": "AZURESUBSCRIPTION_SERVICE_CONNECTION_ID", + "system_access_token": "SYSTEM_ACCESSTOKEN", + "tenant_id": "AZURESUBSCRIPTION_TENANT_ID", +} @dataclass @@ -56,6 +63,35 @@ class TokenHolder: token: str +def make_credential() -> TokenCredential: + """Create a renewable pipeline credential or a local Azure CLI credential.""" + values = { + argument: os.environ.get(variable) + for argument, variable in _PIPELINE_CREDENTIAL_ENV.items() + } + configured = [argument for argument, value in values.items() if value] + if not configured: + return AzureCliCredential() + + missing = [ + variable + for argument, variable in _PIPELINE_CREDENTIAL_ENV.items() + if not values[argument] + ] + if missing: + raise RuntimeError( + "Missing Azure Pipelines credential variables: " + + ", ".join(sorted(missing)) + ) + + return AzurePipelinesCredential( + tenant_id=cast(str, values["tenant_id"]), + client_id=cast(str, values["client_id"]), + service_connection_id=cast(str, values["service_connection_id"]), + system_access_token=cast(str, values["system_access_token"]), + ) + + def make_session() -> requests.Session: """Create a ``requests.Session`` with retries for idempotent GETs only. @@ -79,7 +115,7 @@ def make_session() -> requests.Session: return session -def get_token(credential: DefaultAzureCredential, audience: str) -> str: +def get_token(credential: TokenCredential, audience: str) -> str: """Acquire a bearer token for the given audience.""" return credential.get_token(f"{audience}/.default").token @@ -160,7 +196,7 @@ def _request_with_refresh( session: requests.Session, method: str, url: str, - credential: DefaultAzureCredential, + credential: TokenCredential, audience: str, token_holder: TokenHolder, *, @@ -210,7 +246,7 @@ def post_scenario( session: requests.Session, base_url: str, path: str, - credential: DefaultAzureCredential, + credential: TokenCredential, audience: str, token_holder: TokenHolder, payload: JsonObject, @@ -243,7 +279,7 @@ def post_scenario( def get_job_status( session: requests.Session, base_url: str, - credential: DefaultAzureCredential, + credential: TokenCredential, audience: str, token_holder: TokenHolder, job_id: str, @@ -294,7 +330,7 @@ def _poll_interval_seconds(elapsed_seconds: float) -> int: def poll_until_terminal( session: requests.Session, base_url: str, - credential: DefaultAzureCredential, + credential: TokenCredential, audience: str, token_holder: TokenHolder, job_id: str, diff --git a/scripts/ci/control-tower/package_build.py b/scripts/ci/control-tower/package_build.py index 2082296ba47..d808e448787 100644 --- a/scripts/ci/control-tower/package_build.py +++ b/scripts/ci/control-tower/package_build.py @@ -7,7 +7,6 @@ from typing import TYPE_CHECKING import client as ct -from azure.identity import DefaultAzureCredential if TYPE_CHECKING: from pathlib import Path @@ -96,7 +95,7 @@ def submit_and_monitor( print(json.dumps(request.payload, indent=2)) base_url = api_base_url.rstrip("/") - credential = DefaultAzureCredential() + credential = ct.make_credential() token_holder = ct.TokenHolder(token=ct.get_token(credential, api_audience)) session = ct.make_session() diff --git a/scripts/ci/control-tower/run_prcheck.py b/scripts/ci/control-tower/run_prcheck.py index 814a6b79404..29715c1b5d0 100644 --- a/scripts/ci/control-tower/run_prcheck.py +++ b/scripts/ci/control-tower/run_prcheck.py @@ -26,7 +26,6 @@ from pathlib import Path import client as ct -from azure.identity import DefaultAzureCredential def _parse_components(value: str) -> list[str]: @@ -182,7 +181,7 @@ def main() -> None: return # ── Acquire bearer token ───────────────────────────────────────── - credential = DefaultAzureCredential() + credential = ct.make_credential() token_holder = ct.TokenHolder(token=ct.get_token(credential, args.api_audience)) session = ct.make_session() diff --git a/scripts/ci/control-tower/tests/test_client.py b/scripts/ci/control-tower/tests/test_client.py new file mode 100644 index 00000000000..62c829c0daa --- /dev/null +++ b/scripts/ci/control-tower/tests/test_client.py @@ -0,0 +1,60 @@ +"""Tests for Control Tower credential selection.""" + +from __future__ import annotations + +from unittest.mock import Mock + +import client +import pytest + + +_PIPELINE_ENV = { + "AZURESUBSCRIPTION_CLIENT_ID": "client-id", + "AZURESUBSCRIPTION_SERVICE_CONNECTION_ID": "service-connection-id", + "AZURESUBSCRIPTION_TENANT_ID": "tenant-id", + "SYSTEM_ACCESSTOKEN": "system-access-token", +} + + +def test_make_credential_uses_renewable_pipeline_identity(monkeypatch: pytest.MonkeyPatch) -> None: + """Use AzurePipelinesCredential when AzureCLI@2 provides pipeline context.""" + for name, value in _PIPELINE_ENV.items(): + monkeypatch.setenv(name, value) + + pipeline_credential = Mock(return_value=object()) + cli_credential = Mock(side_effect=AssertionError("AzureCliCredential must not be used")) + monkeypatch.setattr(client, "AzurePipelinesCredential", pipeline_credential, raising=False) + monkeypatch.setattr(client, "AzureCliCredential", cli_credential, raising=False) + + client.make_credential() + + pipeline_credential.assert_called_once_with( + tenant_id="tenant-id", + client_id="client-id", + service_connection_id="service-connection-id", + system_access_token="system-access-token", + ) + cli_credential.assert_not_called() + + +def test_make_credential_uses_azure_cli_outside_pipeline(monkeypatch: pytest.MonkeyPatch) -> None: + """Keep local execution available through the developer's Azure CLI session.""" + for name in _PIPELINE_ENV: + monkeypatch.delenv(name, raising=False) + + cli_credential = Mock(return_value=object()) + monkeypatch.setattr(client, "AzureCliCredential", cli_credential, raising=False) + + client.make_credential() + + cli_credential.assert_called_once_with() + + +def test_make_credential_rejects_partial_pipeline_context(monkeypatch: pytest.MonkeyPatch) -> None: + """Fail clearly instead of silently selecting a nonrenewable pipeline identity.""" + for name in _PIPELINE_ENV: + monkeypatch.delenv(name, raising=False) + monkeypatch.setenv("AZURESUBSCRIPTION_CLIENT_ID", "client-id") + + with pytest.raises(RuntimeError, match="Missing Azure Pipelines credential variables"): + client.make_credential() diff --git a/scripts/ci/control-tower/tests/test_package_build.py b/scripts/ci/control-tower/tests/test_package_build.py index 8e212d5e0bd..576edfdcdef 100644 --- a/scripts/ci/control-tower/tests/test_package_build.py +++ b/scripts/ci/control-tower/tests/test_package_build.py @@ -20,7 +20,7 @@ def lifecycle_mocks(monkeypatch: pytest.MonkeyPatch) -> tuple[Mock, Mock, Mock, print_final_status = Mock() report_failure = Mock() - monkeypatch.setattr(package_build, "DefaultAzureCredential", Mock(return_value=object())) + monkeypatch.setattr(package_build.ct, "make_credential", Mock(return_value=object()), raising=False) monkeypatch.setattr(package_build.ct, "get_token", Mock(return_value="token")) monkeypatch.setattr(package_build.ct, "make_session", Mock(return_value=object())) monkeypatch.setattr(package_build.ct, "poll_until_terminal", poll_until_terminal) @@ -49,6 +49,15 @@ def _submit(*, wait_for_completion: bool = False) -> None: class TestPackageBuildLifecycle: """Verify shared submission and polling behavior.""" + def test_uses_pipeline_service_connection( + self, + lifecycle_mocks: tuple[Mock, Mock, Mock, Mock], + ) -> None: + """Use the identity authenticated by the enclosing AzureCLI task.""" + _submit() + + package_build.ct.make_credential.assert_called_once_with() + @pytest.mark.parametrize("job_id", [None, 123]) def test_missing_or_non_string_job_id_fails( self, diff --git a/scripts/ci/control-tower/tests/test_prcheck.py b/scripts/ci/control-tower/tests/test_prcheck.py new file mode 100644 index 00000000000..4cf6349d342 --- /dev/null +++ b/scripts/ci/control-tower/tests/test_prcheck.py @@ -0,0 +1,46 @@ +"""Contract tests for the Control Tower PR-check entry point.""" + +from __future__ import annotations + +from argparse import Namespace +from unittest.mock import Mock + +import pytest +import run_prcheck + + +def test_main_uses_pipeline_service_connection(monkeypatch: pytest.MonkeyPatch) -> None: + """Use the identity authenticated by the enclosing AzureCLI task.""" + credential = object() + make_credential = Mock(return_value=credential) + + args = Namespace( + api_audience="api://control-tower", + api_base_url="https://control-tower.example", + build_reason="PullRequest", + changed_components_file=None, + components=["lolcat"], + poll_timeout_seconds=60, + repo_uri="https://github.com/microsoft/azurelinux", + source_branch=None, + source_commit="source-commit", + target_branch=None, + target_commit=None, + ) + + monkeypatch.setattr(run_prcheck, "_parse_args", Mock(return_value=args)) + monkeypatch.setattr(run_prcheck.ct, "make_credential", make_credential, raising=False) + monkeypatch.setattr(run_prcheck.ct, "get_token", Mock(return_value="token")) + monkeypatch.setattr(run_prcheck.ct, "make_session", Mock(return_value=object())) + monkeypatch.setattr(run_prcheck.ct, "post_scenario", Mock(return_value={"jobId": "job-id"})) + monkeypatch.setattr( + run_prcheck.ct, + "poll_until_terminal", + Mock(return_value=({"status": run_prcheck.ct.SUCCESS_STATUS}, False)), + ) + monkeypatch.setattr(run_prcheck.ct, "print_final_status", Mock()) + monkeypatch.setattr(run_prcheck.ct, "report_failure", Mock()) + + run_prcheck.main() + + make_credential.assert_called_once_with() diff --git a/specs/l/lolcat/lolcat-Makefile.patch b/specs/l/lolcat/lolcat-Makefile.patch new file mode 100644 index 00000000000..9f6773eac82 --- /dev/null +++ b/specs/l/lolcat/lolcat-Makefile.patch @@ -0,0 +1,13 @@ +--- lolcat-1.3/Makefile.orig 2022-11-19 17:40:06.000000000 +0100 ++++ lolcat-1.3/Makefile 2022-11-25 20:11:34.078297526 +0100 +@@ -22,8 +22,8 @@ + $(CC) $(CFLAGS) -o $@ $< $(LIBS) + + install: lolcat censor +- install lolcat $(DESTDIR)/lolcat +- install censor $(DESTDIR)/censor ++ $(INSTALL) lolcat $(DESTDIR)/lolcat ++ $(INSTALL) censor $(DESTDIR)/censor + + clean: + rm -f lolcat censor diff --git a/specs/l/lolcat/lolcat.spec b/specs/l/lolcat/lolcat.spec new file mode 100644 index 00000000000..9bed307121f --- /dev/null +++ b/specs/l/lolcat/lolcat.spec @@ -0,0 +1,120 @@ +# This spec file has been modified by azldev to include build configuration overlays. +# Do not edit manually; changes may be overwritten. + +Summary: High-performance implementation of a colorful cat +Name: lolcat +Version: 1.5 +Release: 5%{?dist} +Source: https://github.com/jaseg/lolcat/archive/v%{version}/%{name}-%{version}.tar.gz +URL: https://github.com/jaseg/lolcat/ + +Patch1: lolcat-Makefile.patch + +License: WTFPL +BuildRequires: make +BuildRequires: gcc + +%description +lolcat is a colorful version of 'cat'. It is faster than python-lolcat +and much faster than ruby-lolcat. It works well with "non-binary" +characters, but who would want to display binary data! + +%prep +%autosetup + +%build +%set_build_flags +%make_build all + +%install +mkdir -p $RPM_BUILD_ROOT/%{_bindir} +%make_install DESTDIR=$RPM_BUILD_ROOT/%{_bindir} + +%files +%{_bindir}/lolcat +%{_bindir}/censor +%doc README.md +%license LICENSE + +%changelog +* Thu Jul 24 2025 Fedora Release Engineering - 1.5-4 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_43_Mass_Rebuild + +* Fri Jan 17 2025 Fedora Release Engineering - 1.5-3 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_42_Mass_Rebuild + +* Thu Jul 18 2024 Fedora Release Engineering - 1.5-2 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_41_Mass_Rebuild + +* Sun Jul 07 2024 josef radinger - 1.5-1 +- bump version + +* Thu Jan 25 2024 Fedora Release Engineering - 1.4-4 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_40_Mass_Rebuild + +* Sun Jan 21 2024 Fedora Release Engineering - 1.4-3 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_40_Mass_Rebuild + +* Thu Jul 20 2023 Fedora Release Engineering - 1.4-2 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_39_Mass_Rebuild + +* Fri Mar 03 2023 josef radinger - 1.4-1 +- bump version + +* Thu Jan 19 2023 Fedora Release Engineering - 1.3-2 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_38_Mass_Rebuild + +* Fri Nov 25 2022 josef radinger - 1.3-1 +- bump version + +* Thu Jul 21 2022 Fedora Release Engineering - 1.2-5 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_37_Mass_Rebuild + +* Thu Jan 20 2022 Fedora Release Engineering - 1.2-4 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_36_Mass_Rebuild + +* Thu Jul 22 2021 Fedora Release Engineering - 1.2-3 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_35_Mass_Rebuild + +* Tue Jan 26 2021 Fedora Release Engineering - 1.2-2 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_34_Mass_Rebuild + +* Sun Jan 17 2021 josef radinger - 1.2-0 +- bump version +- massage patch1 +- remove patch2 + +* Sun Dec 27 2020 josef radinger - 1.1-1 +- bump version +- add patch2 + +* Tue Jul 28 2020 Fedora Release Engineering - 1.0-8 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_33_Mass_Rebuild + +* Wed Jan 29 2020 Fedora Release Engineering - 1.0-7 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_32_Mass_Rebuild + +* Mon Aug 26 2019 josef radinger - 1.0-6 +- modify description + +* Mon Aug 26 2019 josef radinger - 1.0-5 +- use a better source-url +- use %%autosetup +- use %%make_build +- use %%make_install (plus patch1 to preserve timestamps) +- better Summary + +* Sat Aug 17 2019 josef radinger - 1.0-4 +- use %%{_bindir} instead of /usr/bin +- invoke %%set_build_flags before make + +* Fri Aug 16 2019 josef radinger - 1.0-3 +- correct license +- small cleanup in spec-file + +* Wed Aug 07 2019 josef radinger - 1.0-2 +- add URL + +* Tue Aug 06 2019 josef radinger - 1.0-1 +- initial package + diff --git a/specs/l/lolcat/sources b/specs/l/lolcat/sources new file mode 100644 index 00000000000..53ee5b790de --- /dev/null +++ b/specs/l/lolcat/sources @@ -0,0 +1 @@ +SHA512 (lolcat-1.5.tar.gz) = 5959c4a5d485a5e956985f5d1dea4bd42442ed29b0d5631c07033b526312b8d41160155542ae89b61d1ffc65292c2e6e8d89545b9ac4b8ebe11842151e03410b