Crear y ejecutar pipelines desde otro pipeline en Azure DevOps
- 7 minutes read - 1320 wordsSi os habéis encontrado con la necesidad de crear y ejecutar pipelines de Azure DevOps (ADO) desde una pipeline, existe una extensión de devops para el az CLI que nos permitirá hacer esto, aunque tendremos que preparar un poco el proyecto de ADO antes. A día de hoy todavía es una característica en preview, así que puede que algo de lo que describa aquí sufra cambios antes de estar en disponibilidad general.
El primer detalle que debemos conocer es que hay una sutil diferencia entre el archivo de definición de la pipeline, que es un archivo yaml, con la pipeline que se va a ejecutar, que se define normalmente a través de la consola web de ADO, o se pueden también crear desde el CLI o la API REST.
Preparar nuestro proyecto de Azure DevOps
Tras crear un proyecto con un repositorio, vamos a necesitar dar permisos al service principal que se usa para crear nuevas pipelines en algún sitio. Para no mezclarlas con las pipelines que hayamos creado manualmente, podemos crear una carpeta y asignar los permisos “Edit build pipeline” y “Queue builds” a la cuenta del servicio de build, que suele tener el nombre “[Nombre del proyecto] Build Service (alias de usuario)”. Para hacer esto, vamos al apartado de Pipelines y en el apartado “All Pipelines” creamos una carpeta. A la derecha de la carpeta pulsamos sobre el menú y abrimos la opción security, ahí podemos asignar los permisos al usuario del servicio de build:
Creación de la estructura
En el repositorio de código, vamos a añadir una carpeta llamada pipelines y dentro un archivo yaml, que se llame, por ejemplo, base-auto-pipeline.yml, con la definición de la pipeline que queremos crear y ejecutar dinámicamente. Para el ejemplo usaré una muy sencilla que recibirá una variable llamada testVariable:
trigger: none
pool:
vmImage: ubuntu-latest
steps:
- script: |
echo Congrats, your variable was set to:
echo $(testVariable)
displayName: 'Run a test script'
Luego vamos a crear una nueva pipeline a través de la sección de pipelines, usaremos una “Starter pipeline”:
Y la modificaremos inmediatamente con un yaml como este:
# manual trigger only
trigger: none
parameters:
# this parameter is required, no default value
- name: automationName
type: string
# used to conditionally run the last step
- name: runThePipeline
type: boolean
default: true
displayName: Run the generated pipeline
pool:
vmImage: ubuntu-latest
variables:
- name: automationFolder
value: automatic
- name: sourcePipeline
value: pipelines/base-auto-pipeline.yml
- name: pipelineId
steps:
- script: az devops configure --defaults organization='$(System.TeamFoundationCollectionUri)' project='$(System.TeamProject)' --use-git-aliases true
displayName: 'Set default Azure DevOps organization and project'
- script: echo ${AZURE_DEVOPS_CLI_PAT} | az devops login
env:
AZURE_DEVOPS_CLI_PAT: $(System.AccessToken)
displayName: 'Login Azure DevOps Extension'
- script: |
pipelineId=$(az pipelines show --folder-path $(automationFolder) --name '${{ parameters.automationName }}' --query id -o tsv)
echo $pipelineId
if [ $pipelineId ]
then
echo "Pipeline already exists with id $(pipelineId)"
else
pipelineId=$(az pipelines create --name '${{ parameters.automationName }}' --folder-path $(automationFolder) --branch '$(Build.SourceBranchName)' --repository '$(Build.Repository.Name)' --repository-type $(Build.Repository.Provider) --yaml-path '$(sourcePipeline)' --skip-first-run true --query id -o tsv)
fi
echo "pipeline id $pipelineId"
echo "##vso[task.setvariable variable=pipelineId]$pipelineId"
displayName: "Create pipeline if not exists"
- ${{ if eq(parameters.runThePipeline, true) }}:
- script: |
echo "run $(pipelineId)"
az pipelines run --id $(pipelineId) --branch '$(Build.SourceBranchName)' --variables testVariable='Hello world!'
displayName: "Run the pipeline"
Explicación de cada paso de la pipeline
En la primera sección, he deshabilitado el trigger, he añadido dos parametros para que nos aparezcan en el formulario de ejecución, y algunas variables que usaremos dentro de la automatización.
# manual trigger only
trigger: none
parameters:
# this parameter is required, no default value
- name: automationName
displayName: Automation name
type: string
# used to conditionally run the last step
- name: runThePipeline
type: boolean
default: true
displayName: Run the generated pipeline
pool:
vmImage: ubuntu-latest
variables:
- name: automationFolder
value: automatic
- name: sourcePipeline
value: pipelines/base-auto-pipeline.yml
- name: pipelineId
Los dos primeros pasos del pipeline son para preparar el entorno. El Azure CLI que viene instalado en el agente de Azure DevOps ya tiene instalada la [extensión para manejar pipelines][ado-cli-pipelines] que nos permitirá interactuar con nuestros pipelines. Este caso es un poco diferente del que nos encontramos cuando tenemos que conectar a Azure, donde usamos una Service Connection; aquí tenemos que configurarlo mediante las variables de sistema que nos proporcionan, por ejemplo, el nombre del proyecto $(System.TeamProject) o el Personal Access Token (PAT) $(System.AccessToken) para realizar el login en el CLI:
steps:
- script: az devops configure --defaults organization='$(System.TeamFoundationCollectionUri)' project='$(System.TeamProject)' --use-git-aliases true
displayName: 'Set default Azure DevOps organization and project'
- script: echo ${AZURE_DEVOPS_CLI_PAT} | az devops login
env:
AZURE_DEVOPS_CLI_PAT: $(System.AccessToken)
displayName: 'Login Azure DevOps Extension'
En el siguiente paso, nos encontramos con un script que mira si la pipeline que queremos crear ya existe con ese nombre, para no volver a crearla. En caso negativo, usa el comando az pipelines create para crear una nueva con el nombre que le queramos dar. Fijaos que aquí usamos el parámetro ${{ parameters.automationName }}
, que se incrusta usando el lenguaje de plantillas propio de pipelines, aquí este valor no entra como variable de entorno como en otros casos.
También estamos usando variables de sistema como el nombre del repositorio, el nombre de la rama, etc. Además proporcionamos un path al archivo yaml que se usará para generar la pipeline y, finalmente, nos guardamos el identificador de la pipeline para poderlo usar luego.
Usamos el parámetro –skip-first-run porque queremos ejecutar esa pipeline en un siguiente paso y pasarle alguna variable adicional, esto, además, nos permitiría meter ese paso dentro de un stage de forma que podamos introducir un proceso de aprobación manual o cualquier otra comprobación que necesitemos realizar antes de ejecutar la siguiente pipeline.
Nota: no he utilizado la separación de fases en Jobs y Stages, porque en cada job se reincia el contexto, eso nos obligaría a tener que volver a realizar el login en ADO y ahora mismo ese proceso es bastante lento. Si necesitáis separar en diferentes etapas, recordad que hay que volver a ejecutar el proceso de login.
pipelineId=$(az pipelines show --folder-path $(automationFolder) --name '${{ parameters.automationName }}' --query id -o tsv)
echo $pipelineId
if [ $pipelineId ]
then
echo "Pipeline already exists with id $(pipelineId)"
else
pipelineId=$(az pipelines create --name '${{ parameters.automationName }}' --folder-path $(automationFolder) --branch '$(Build.SourceBranchName)' --repository '$(Build.Repository.Name)' --repository-type $(Build.Repository.Provider) --yaml-path '$(sourcePipeline)' --skip-first-run true --query id -o tsv)
fi
Una característica interesante es que estamos utilizando el mismo archivo de definición de pipeline (el pipelines/base-auto-pipeline.yml) para crear tantas pipelines como queramos, así que si modificamos el proceso en el yaml, todas las pipelines que dependan del mismo estarán actualizadas.
Hay una línea más un tanto particular; se utiliza para rellenar la variable de entorno que se pasará a los siguientes pasos. Cada entrada script o bash ejecuta un .sh nuevo con el contenido que le estemos pasando, así que cualquier variable de entorno que asignemos ahí no pasará al siguiente script y necesitamos que el agente se encargue de inyectar esas variables.
echo "##vso[task.setvariable variable=pipelineId]$pipelineId"
Nota: si luego empezáis a utilizar etapas, hay que indicar que estamos asignando un output para pasar información entre diferentes jobs.
Para acabar, ejecutamos la pipeline con el id que hemos obtenido en el paso anterior y usamos el parámetro para ejecutar ese paso de forma condicional. A la pipeline le podemos asignar variables a través de esa línea de comando:
- ${{ if eq(parameters.runThePipeline, true) }}:
- script: |
echo "run $(pipelineId)"
az pipelines run --id $(pipelineId) --branch '$(Build.SourceBranchName)' --variables testVariable='Hello world!'
displayName: "Run the pipeline"
Ejecución de la pipeline
Y ahora ya podemos ejecutar la pipeline, en el formulario de ejecución nos aparecerán los diferentes parámetros que hayamos indicado:
Y, si todo ha ido bien, podremos ver la creación y ejecución de la pipeline dinámicamente:
Resumen de enlaces
- Extensión del CLI para Azure DevOps
- [Azure DevOps pipelines CLI][ado-cli-pipelines]
- Definición de variables
- Creación de pipelines desde el CLI
- Uso de parámetros tipificados en pipelines
- Fases en pipelines
- Outputs entre diferentes jobs
Créditos
- Foto de portada por Pierre-Alexandre Garneau.