Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion azure-devops/azext_devops/dev/common/artifacttool.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ def download_pipeline_artifact(self, organization, project, run_id, artifact_nam
"--project", project, "--pipeline-id", run_id, "--artifact-name", artifact_name, "--path", path]
return self.run_artifacttool(organization, args, "Downloading")

def upload_pipeline_artifact(self, organization, project, run_id, artifact_name, path):
def upload_pipeline_artifact(self, organization, project, run_id, artifact_name, path, properties=None):
args = ["pipelineartifact", "publish", "--service", organization, "--patvar", ARTIFACTTOOL_PAT_ENVKEY,
"--project", project, "--pipeline-id", run_id, "--artifact-name", artifact_name, "--path", path]
if properties:
args.extend(["--properties", properties])
return self.run_artifacttool(organization, args, "Uploading")

def download_universal(self, organization, project, feed, package_name, package_version, path, file_filter):
Expand Down
11 changes: 11 additions & 0 deletions azure-devops/azext_devops/dev/pipelines/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,17 @@ def load_pipelines_help():
long-summary:
"""

helps['pipelines runs artifact upload'] = """
type: command
short-summary: Upload a pipeline artifact.
long-summary: >
Upload a pipeline artifact to a specific run. Optionally specify custom properties as metadata.
examples:
- name: Upload a pipeline artifact with custom properties
text: |
az pipelines runs artifact upload --artifact-name myArtifact --run-id 123 --path /path/to/artifact --properties "user-key1=value1;user-key2=value2"
"""

helps['pipelines create'] = """
type: command
short-summary: Create a new Azure Pipeline (YAML based).
Expand Down
7 changes: 7 additions & 0 deletions azure-devops/azext_devops/dev/pipelines/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,10 @@ def load_build_arguments(self, _):

with self.argument_context('pipelines folder') as context:
context.argument('query_order', **enum_choice_list(_FOLDERS_QUERY_ORDER))

with self.argument_context('pipelines runs artifact upload') as context:
context.argument(
'properties',
options_list=['--properties'],
help="Optional custom properties for the artifact in 'key1=value1;key2=value2' format."
)
6 changes: 5 additions & 1 deletion azure-devops/azext_devops/dev/pipelines/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from webbrowser import open_new
from knack.log import get_logger
from knack.util import CLIError
import json
from azext_devops.dev.common.services import (get_build_client,
get_git_client,
resolve_instance_and_project,
Expand Down Expand Up @@ -172,7 +173,10 @@ def pipeline_run(id=None, branch=None, commit_id=None, name=None, open=False, va
build = Build(definition=definition_reference, source_branch=branch, source_version=commit_id)

param_variables = set_param_variable(variables)
build.parameters = param_variables
if param_variables:
build.parameters = json.dumps(param_variables)
else:
build.parameters = None

queued_build = client.queue_build(build=build, project=project)

Expand Down
85 changes: 81 additions & 4 deletions azure-devops/azext_devops/dev/pipelines/runs_artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
from azext_devops.dev.common.artifacttool import ArtifactToolInvoker
from azext_devops.dev.common.artifacttool_updater import ArtifactToolUpdater
from azext_devops.dev.common.external_tool import ProgressReportingExternalToolInvoker
import os
import time
from typing import Optional, Callable, Any

logger = get_logger(__name__)

Expand Down Expand Up @@ -39,16 +42,90 @@ def run_artifact_list(run_id, organization=None, project=None, detect=None):
return artifacts


def run_artifact_upload(run_id, artifact_name, path, organization=None, project=None, detect=None):
""" Upload a pipeline artifact.
def run_artifact_upload(
run_id: int,
artifact_name: str,
path: str,
organization: Optional[str] = None,
project: Optional[str] = None,
detect: Optional[bool] = None,
properties: Optional[str] = None,
validate_path: bool = True,
dry_run: bool = False,
retry_count: int = 3,
backoff_seconds: float = 0.1,
log_progress: bool = True,
on_progress: Optional[Callable[[str, int], None]] = None,
on_complete: Optional[Callable[[Any], None]] = None,
on_error: Optional[Callable[[Exception], None]] = None,
) -> Any:
"""
Upload a pipeline artifact with robust options and hooks.

:param run_id: ID of the run that the artifact is associated to.
:type run_id: int
:param artifact_name: Name of the artifact to upload.
:type artifact_name: string
:param path: Path to upload the artifact from.
:type path: string
:param properties: Optional custom properties for the artifact in 'key1=value1;key2=value2' format.
:type properties: string
:param validate_path: Whether to validate the path before upload.
:type validate_path: bool
:param dry_run: If True, do not actually upload.
:type dry_run: bool
:param retry_count: Number of times to retry on failure.
:type retry_count: int
:param log_progress: Whether to log progress.
:type log_progress: bool
:param on_progress: Optional callback for progress updates.
:type on_progress: callable
:param on_complete: Optional callback for completion.
:type on_complete: callable
:param on_error: Optional callback for errors.
:type on_error: callable
"""
organization, project = resolve_instance_and_project(detect=detect, organization=organization, project=project)

if validate_path and not os.path.exists(path):
error_msg = f"Artifact path does not exist: {path}"
logger.error(error_msg)
exc = FileNotFoundError(error_msg)
if on_error:
on_error(exc)
raise exc

if dry_run:
logger.info("Dry run enabled. Skipping upload.")
result = {"status": "dry_run", "artifact_name": artifact_name, "path": path}
if on_complete:
on_complete(result)
return result

artifact_tool = ArtifactToolInvoker(ProgressReportingExternalToolInvoker(), ArtifactToolUpdater())
return artifact_tool.upload_pipeline_artifact(
organization=organization, project=project, run_id=run_id, artifact_name=artifact_name, path=path)

for attempt in range(1, retry_count + 1):
try:
logger.info(f"Upload attempt {attempt} of {retry_count}")
result = artifact_tool.upload_pipeline_artifact(
organization=organization,
project=project,
run_id=run_id,
artifact_name=artifact_name,
path=path,
properties=properties,
)
if log_progress and on_progress:
on_progress(f"Upload completed for {artifact_name}", 100)
if on_complete:
on_complete(result)
return result
except Exception as ex:
logger.error(f"Upload attempt {attempt} failed: {ex}")
if on_error:
on_error(ex)
if attempt < retry_count:
time.sleep(backoff_seconds * attempt)
else:
logger.error("All upload attempts failed.")
raise ex
Loading