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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@
[//]: # (Features)
[//]: # (BREAKING CHANGES)

## August 15th, 2025

### Parallel deployment improvements

The following scripts have been updated to write the deployment plan key of the created deployment to an output variable called DEPLOYMENT_PLAN_KEY for use in subsequent pipeline tasks and jobs.

* `deploy_latest_tags_to_target_env.py`
* `deploy_package_to_target_env.py`
* `deploy_tags_to_target_env_with_manifest.py`

This can be used to fill a new optional parameter `--deployment_plan_key` in the following script:

* `continue_deployment_to_target_env.py`

This ensures you can continue a specific deployment plan for a running deployment, instead of the first one returned by the LifeTime API.

## Nov 14th, 2024

### Parallel Deployments
Expand Down
8 changes: 4 additions & 4 deletions examples/azure_devops/multistage/CD-AzureAgent.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ stages:
# Stage: Regression Testing
# ******************************************************************
- stage: regression_testing
environment: $(Environment.Regression.Key)
displayName: Regression Testing
jobs:

Expand All @@ -93,7 +94,6 @@ stages:
# ******************************************************************
- template: ./jobs/LifeTimeDeploymentJob.yaml
parameters:
EnvironmentKey: $(Environment.Regression.Key)
SourceEnvironmentLabel: $(Environment.Development.Label)
DestinationEnvironmentLabel: $(Environment.Regression.Label)
IncludeTestApplications: true
Expand All @@ -109,6 +109,7 @@ stages:
# Stage: Release Acceptance
# ******************************************************************
- stage: release_acceptance
environment: $(Environment.Acceptance.Key)
displayName: Release Acceptance
jobs:

Expand All @@ -119,7 +120,6 @@ stages:
# ******************************************************************
- template: ./jobs/LifeTimeDeploymentJob.yaml
parameters:
EnvironmentKey: $(Environment.Acceptance.Key)
SourceEnvironmentLabel: $(Environment.Regression.Label)
DestinationEnvironmentLabel: $(Environment.Acceptance.Label)

Expand All @@ -134,6 +134,7 @@ stages:
# Stage: Dry-Run
# ******************************************************************
- stage: dry_run
environment: $(Environment.PreProduction.Key)
displayName: Dry-Run
jobs:

Expand All @@ -144,14 +145,14 @@ stages:
# ******************************************************************
- template: ./jobs/LifeTimeDeploymentJob.yaml
parameters:
EnvironmentKey: $(Environment.PreProduction.Key)
SourceEnvironmentLabel: $(Environment.Acceptance.Label)
DestinationEnvironmentLabel: $(Environment.PreProduction.Label)

# ******************************************************************
# Stage: Go-Live
# ******************************************************************
- stage: go_live
environment: $(Environment.Production.Key)
displayName: Go-Live
jobs:

Expand All @@ -162,7 +163,6 @@ stages:
# ******************************************************************
- template: ./jobs/LifeTimeDeploymentJob.yaml
parameters:
EnvironmentKey: $(Environment.Production.Key)
SourceEnvironmentLabel: $(Environment.PreProduction.Label)
DestinationEnvironmentLabel: $(Environment.Production.Label)
# To enable 2stage-deploy on this environment uncomment the line below
Expand Down
125 changes: 63 additions & 62 deletions examples/azure_devops/multistage/jobs/LifeTimeDeploymentJob.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
# Declare parameters
# ******************************************************************
parameters:
- name: EnvironmentKey # Environment key (in Azure DevOps)
type: string
- name: SourceEnvironmentLabel # Source Environment (in manifest)
type: string
- name: DestinationEnvironmentLabel # Destination Environment (in manifest)
Expand All @@ -31,68 +29,66 @@ jobs:
# ******************************************************************
# Deploy application tags list to target LifeTime environment
# ******************************************************************
- deployment: lifetime_deployment
- job: lifetime_deployment
displayName: LifeTime Deployment
environment: ${{ parameters.EnvironmentKey }}
strategy:
runOnce:
deploy:
steps:
- download: current # Download current pipeline artifacts
- template: ../tasks/InstallPythonPackage.yaml # Install python package

# ******************************************************************
# Step: Deploy to target environment (using manifest)
# ******************************************************************
# Deploy application list to target environment using manifest
# ******************************************************************
- ${{ if eq(parameters.IncludeTestApplications, true) }}:
- script: >
python -m outsystems.pipeline.deploy_tags_to_target_env_with_manifest
--artifacts "$(Artifacts.Folder)"
--lt_url $(LifeTime.Hostname)
--lt_token $(LifeTime.ServiceAccountToken)
--lt_api_version $(LifeTime.APIVersion)
--source_env_label "${{ parameters.SourceEnvironmentLabel }}"
--destination_env_label "${{ parameters.DestinationEnvironmentLabel }}"
--include_test_apps
--manifest_file "$(Pipeline.Workspace)/$(Manifest.Folder)/$(Manifest.File)"
displayName: 'Deploy to ${{ parameters.DestinationEnvironmentLabel }} environment'
steps:
- download: current # Download current pipeline artifacts
- template: ../tasks/InstallPythonPackage.yaml # Install python package

# ******************************************************************
# Step: Deploy to target environment (using manifest)
# ******************************************************************
# Deploy application list to target environment using manifest
# ******************************************************************
- ${{ if eq(parameters.IncludeTestApplications, true) }}:
- script: >
python -m outsystems.pipeline.deploy_tags_to_target_env_with_manifest
--artifacts "$(Artifacts.Folder)"
--lt_url $(LifeTime.Hostname)
--lt_token $(LifeTime.ServiceAccountToken)
--lt_api_version $(LifeTime.APIVersion)
--source_env_label "${{ parameters.SourceEnvironmentLabel }}"
--destination_env_label "${{ parameters.DestinationEnvironmentLabel }}"
--include_test_apps
--manifest_file "$(Pipeline.Workspace)/$(Manifest.Folder)/$(Manifest.File)"
displayName: 'Deploy to ${{ parameters.DestinationEnvironmentLabel }} environment'
name: deploy_tags_to_target_step

- ${{ if eq(parameters.IncludeTestApplications, false) }}:
- script: >
python -m outsystems.pipeline.deploy_tags_to_target_env_with_manifest
--artifacts "$(Artifacts.Folder)"
--lt_url $(LifeTime.Hostname)
--lt_token $(LifeTime.ServiceAccountToken)
--lt_api_version $(LifeTime.APIVersion)
--source_env_label "${{ parameters.SourceEnvironmentLabel }}"
--destination_env_label "${{ parameters.DestinationEnvironmentLabel }}"
--manifest_file "$(Pipeline.Workspace)/$(Manifest.Folder)/$(Manifest.File)"
displayName: 'Deploy to ${{ parameters.DestinationEnvironmentLabel }} environment'
- ${{ if eq(parameters.IncludeTestApplications, false) }}:
- script: >
python -m outsystems.pipeline.deploy_tags_to_target_env_with_manifest
--artifacts "$(Artifacts.Folder)"
--lt_url $(LifeTime.Hostname)
--lt_token $(LifeTime.ServiceAccountToken)
--lt_api_version $(LifeTime.APIVersion)
--source_env_label "${{ parameters.SourceEnvironmentLabel }}"
--destination_env_label "${{ parameters.DestinationEnvironmentLabel }}"
--manifest_file "$(Pipeline.Workspace)/$(Manifest.Folder)/$(Manifest.File)"
displayName: 'Deploy to ${{ parameters.DestinationEnvironmentLabel }} environment'
name: deploy_tags_to_target_step

# ******************************************************************
# Step: Apply configuration values
# ******************************************************************
# Apply configuration values (if any) to target environment
# ******************************************************************
- ${{ if eq(parameters.Use2StepDeployment, false) }}:
- template: ../tasks/ApplyConfigurationValues.yaml
parameters:
TargetEnvironmentLabel: ${{ parameters.DestinationEnvironmentLabel }}
# ******************************************************************
# Step: Apply configuration values
# ******************************************************************
# Apply configuration values (if any) to target environment
# ******************************************************************
- ${{ if eq(parameters.Use2StepDeployment, false) }}:
- template: ../tasks/ApplyConfigurationValues.yaml
parameters:
TargetEnvironmentLabel: ${{ parameters.DestinationEnvironmentLabel }}

# ******************************************************************
# Step: Print deployment conflicts
# ******************************************************************
# Check if there any Deployment Conflicts and show them in the
# console log
# ******************************************************************
- task: PowerShell@2
inputs:
targetType: 'inline'
script: Get-Content -Path "$(Artifacts.Folder)\DeploymentConflicts" | Write-Host
condition: failed()
displayName: 'Show content of DeploymentConflicts file'
# ******************************************************************
# Step: Print deployment conflicts
# ******************************************************************
# Check if there any Deployment Conflicts and show them in the
# console log
# ******************************************************************
- task: PowerShell@2
inputs:
targetType: 'inline'
script: Get-Content -Path "$(Artifacts.Folder)\DeploymentConflicts" | Write-Host
condition: failed()
displayName: 'Show content of DeploymentConflicts file'

# ******************************************************************
# Job: Wait for confirmation
Expand Down Expand Up @@ -127,7 +123,11 @@ jobs:
- ${{ if eq(parameters.Use2StepDeployment, true) }}:
- job: finalize_deployment
displayName: Finalize Deployment
dependsOn: wait_confirmation
dependsOn:
- lifetime_deployment
- wait_confirmation
variables:
DEPLOYMENT_PLAN_KEY: $[ dependencies.lifetime_deployment.outputs['deploy_tags_to_target_step.DEPLOYMENT_PLAN_KEY'] ]
steps:
- checkout: none # Avoid repository checkout
- download: current # Download current pipeline artifacts
Expand All @@ -146,7 +146,8 @@ jobs:
--lt_url $(LifeTime.Hostname)
--lt_token $(LifeTime.ServiceAccountToken)
--lt_api_version $(LifeTime.APIVersion)
--destination_env "${{ parameters.DestinationEnvironmentLabel }}"
--destination_env "${{ parameters.DestinationEnvironmentLabel }}"
--deployment_plan_key $(DEPLOYMENT_PLAN_KEY)
displayName: 'Continue deployment to ${{ parameters.DestinationEnvironmentLabel }} environment'

# ******************************************************************
Expand Down
10 changes: 5 additions & 5 deletions outsystems/lifetime/lifetime_deployments.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,19 +117,19 @@ def get_deployment_status(artifact_dir: str, endpoint: str, auth_token: str, dep


# Returns the details of the running deployment plan to a specific target environment or empty if nothing is running
def get_running_deployment(artifact_dir: str, endpoint: str, auth_token: str, dest_env_key: str):
def get_running_deployments(artifact_dir: str, endpoint: str, auth_token: str, dest_env_key: str):
# List of running deployments
running_deployments = []
# Date 24h prior to now
date = datetime.datetime.now() - datetime.timedelta(days=1)
date = date.date()
try:
latest_deployments = get_deployments(artifact_dir, endpoint, auth_token, date)
for deplyoment in latest_deployments:
if deplyoment["TargetEnvironmentKey"] == dest_env_key:
deployment_status = get_deployment_status(artifact_dir, endpoint, auth_token, deplyoment["Key"])
for deployment in latest_deployments:
if deployment["TargetEnvironmentKey"] == dest_env_key:
deployment_status = get_deployment_status(artifact_dir, endpoint, auth_token, deployment["Key"])
if deployment_status["DeploymentStatus"] in DEPLOYMENT_STATUS_LIST:
running_deployments.append(deplyoment)
running_deployments.append(deployment)

return running_deployments

Expand Down
31 changes: 21 additions & 10 deletions outsystems/pipeline/continue_deployment_to_target_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
# Functions
from outsystems.lifetime.lifetime_environments import get_environment_key
from outsystems.lifetime.lifetime_deployments import get_deployment_status, check_deployment_two_step_deploy_status, \
continue_deployment, get_running_deployment
continue_deployment, get_running_deployments
from outsystems.file_helpers.file import store_data
from outsystems.lifetime.lifetime_base import build_lt_endpoint
from outsystems.vars.vars_base import get_configuration_value, load_configuration_file
Expand All @@ -31,23 +31,30 @@
# ############################################################# SCRIPT ##############################################################


def main(artifact_dir: str, lt_http_proto: str, lt_url: str, lt_api_endpoint: str, lt_api_version: int, lt_token: str, dest_env: str):
def main(artifact_dir: str, lt_http_proto: str, lt_url: str, lt_api_endpoint: str, lt_api_version: int, lt_token: str, dest_env: str, dep_plan_key: str = None):

# Builds the LifeTime endpoint
lt_endpoint = build_lt_endpoint(lt_http_proto, lt_url, lt_api_endpoint, lt_api_version)

# Gets the environment key for the destination environment
dest_env_key = get_environment_key(artifact_dir, lt_endpoint, lt_token, dest_env)

# Find running deployment plan in destination environment
deployment = get_running_deployment(artifact_dir, lt_endpoint, lt_token, dest_env_key)
if len(deployment) == 0:
# Use dep_plan_key if provided, otherwise find running deployment plan in destination environment
if dep_plan_key is None:
deployment = get_running_deployments(artifact_dir, lt_endpoint, lt_token, dest_env_key)
if len(deployment) == 0:
print("Continue skipped because no running deployment plan was found on {} environment.".format(dest_env))
sys.exit(0)

# Grab the key from the deployment plan found
dep_plan_key = deployment[0]["Key"]
print("Deployment plan {} was found.".format(dep_plan_key), flush=True)
# Cases where no deployment plan is created are handled by setting the deployment plan key to no_deployment_required.
elif dep_plan_key == "no_deployment_required":
print("Continue skipped because no running deployment plan was found on {} environment.".format(dest_env))
sys.exit(0)

# Grab the key from the deployment plan found
dep_plan_key = deployment[0]["Key"]
print("Deployment plan {} was found.".format(dep_plan_key), flush=True)
else:
print("Using provided deployment plan key: {}".format(dep_plan_key), flush=True)

# Check deployment plan status
dep_status = get_deployment_status(
Expand Down Expand Up @@ -111,6 +118,8 @@ def main(artifact_dir: str, lt_http_proto: str, lt_url: str, lt_api_endpoint: st
help="Name, as displayed in LifeTime, of the destination environment where you want to continue the deployment plan.")
parser.add_argument("-cf", "--config_file", type=str,
help="Config file path. Contains configuration values to override the default ones.")
parser.add_argument("-dpk", "--deployment_plan_key", type=str,
help="An optional deployment key for resuming a specific deployment, otherwise the first deployment plan in running state is used.")

args = parser.parse_args()

Expand Down Expand Up @@ -138,6 +147,8 @@ def main(artifact_dir: str, lt_http_proto: str, lt_url: str, lt_api_endpoint: st
lt_token = args.lt_token
# Parse Destination Environment
dest_env = args.destination_env
# Parse the optionally provided deployment plan key
dep_plan_key = args.deployment_plan_key

# Calls the main script
main(artifact_dir, lt_http_proto, lt_url, lt_api_endpoint, lt_version, lt_token, dest_env)
main(artifact_dir, lt_http_proto, lt_url, lt_api_endpoint, lt_version, lt_token, dest_env, dep_plan_key)
10 changes: 7 additions & 3 deletions outsystems/pipeline/deploy_latest_tags_to_target_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from outsystems.lifetime.lifetime_environments import get_environment_app_version, get_environment_key
from outsystems.lifetime.lifetime_applications import get_running_app_version, get_application_version
from outsystems.lifetime.lifetime_deployments import get_deployment_status, get_deployment_info, \
send_deployment, delete_deployment, start_deployment, continue_deployment, get_running_deployment
send_deployment, delete_deployment, start_deployment, continue_deployment, get_running_deployments
from outsystems.file_helpers.file import store_data, load_data
from outsystems.lifetime.lifetime_base import build_lt_endpoint
from outsystems.vars.vars_base import get_configuration_value, load_configuration_file
Expand Down Expand Up @@ -147,6 +147,7 @@ def main(artifact_dir: str, lt_http_proto: str, lt_url: str, lt_api_endpoint: st

# Check if there are apps to be deployed
if len(to_deploy_app_keys) == 0:
print("##vso[task.setvariable variable=DEPLOYMENT_PLAN_KEY;isOutput=true]no_deployment_required", flush=True)
print("Deployment skipped because {} environment already has the target application deployed with the same tags.".format(dest_env), flush=True)
sys.exit(0)

Expand All @@ -169,7 +170,7 @@ def main(artifact_dir: str, lt_http_proto: str, lt_url: str, lt_api_endpoint: st

if not allow_parallel_deployments:
wait_counter = 0
deployments = get_running_deployment(artifact_dir, lt_endpoint, lt_token, dest_env_key)
deployments = get_running_deployments(artifact_dir, lt_endpoint, lt_token, dest_env_key)
while len(deployments) > 0:
if wait_counter >= get_configuration_value("QUEUE_TIMEOUT_IN_SECS", QUEUE_TIMEOUT_IN_SECS):
print("Timeout occurred while waiting for LifeTime to be free, to create the new deployment plan.", flush=True)
Expand All @@ -178,13 +179,16 @@ def main(artifact_dir: str, lt_http_proto: str, lt_url: str, lt_api_endpoint: st
sleep(sleep_value)
wait_counter += sleep_value
print("Waiting for LifeTime to be free. Elapsed time: {} seconds...".format(wait_counter), flush=True)
deployments = get_running_deployment(artifact_dir, lt_endpoint, lt_token, dest_env_key)
deployments = get_running_deployments(artifact_dir, lt_endpoint, lt_token, dest_env_key)

# LT is free to deploy
# Send the deployment plan and grab the key
dep_plan_key = send_deployment(artifact_dir, lt_endpoint, lt_token, lt_api_version, to_deploy_app_keys, dep_note, source_env, dest_env)
print("Deployment plan {} created successfully.".format(dep_plan_key), flush=True)

# Write dep_plan_key to Azure DevOps variable
print(f"##vso[task.setvariable variable=DEPLOYMENT_PLAN_KEY;isOutput=true]{dep_plan_key}", flush=True)

# Check if created deployment plan has conflicts
dep_details = get_deployment_info(artifact_dir, lt_endpoint, lt_token, dep_plan_key)
if len(dep_details["ApplicationConflicts"]) > 0:
Expand Down
Loading