import fs from "fs" ; import yaml from "js-yaml" ; import path from "path" ; import { readYaml , write } from "../config" ; import { ACCESS_FILENAME , BEDROCK_FILENAME , HELM_VERSION , HLD_COMPONENT_FILENAME , PROJECT_PIPELINE_FILENAME , RENDER_HLD_PIPELINE_FILENAME , SERVICE_PIPELINE_FILENAME , VERSION_MESSAGE , VM_IMAGE , } from "../lib/constants" ; import { build as buildError } from "../lib/errorBuilder" ; import { errorStatusCode } from "../lib/errorStatusCode" ; import { logger } from "../logger" ; import { AccessYaml , AzurePipelinesYaml , ComponentYaml , MaintainersFile , User , } from "../types" ; param dir param pipelineFileName const readPipelineFile = ( dir , pipelineFileName ) : AzurePipelinesYaml => { const absPath = path . resolve ( dir ) ; const file = path . join ( absPath , pipelineFileName ) ; return yaml . safeLoad ( fs . readFileSync ( file , "utf8" ) ) ; } ; param accessYamlPath param gitRepoUrl param accessTokenEnvVar export const generateAccessYaml = ( accessYamlPath , gitRepoUrl , accessTokenEnvVar = "ACCESS_TOKEN_SECRET" ) : void => { const filePath = path . resolve ( path . join ( accessYamlPath , ACCESS_FILENAME ) ) ; let accessYaml ; if ( fs . existsSync ( filePath ) ) { logger . info ( `Existing ${ ACCESS_FILENAME } found at ${ filePath }, loading and updating, if needed.` ) ; accessYaml = yaml . load ( fs . readFileSync ( filePath , "utf8" ) ) as AccessYaml ; accessYaml = { [ gitRepoUrl ] : accessTokenEnvVar , ... accessYaml , } ; } else { accessYaml = { [ gitRepoUrl ] : accessTokenEnvVar , } ; } fs . writeFileSync ( filePath , yaml . safeDump ( accessYaml , { lineWidth : Number . MAX_SAFE_INTEGER } ) , "utf8" ) ; } ; /**
 * Outputs a bash string for a _safe_ source branch string -- a string where all
 * '/', '.', and '_' in the string have been replaced with a '-'`
 */ export const SAFE_SOURCE_BRANCH = `$(echo $(Build.SourceBranchName) | tr / - | tr . - | tr _ - )` ; /**
 * Outputs a bash script to generate a _safe_ azure container registry url where it's all lowercase.
 * This will require ACR_NAME as an environment variable.
 */ export const IMAGE_REPO = `$(echo $(ACR_NAME).azurecr.io | tr '[:upper:]' '[:lower:]')` ; /**
 * Outputs a bash string for a _safe_ image tag -- a string where all
 * '/' and '.' in the string have been replaced with a '-'`
 */ export const IMAGE_TAG = `${ SAFE_SOURCE_BRANCH }-$(Build.BuildNumber)` ; param serviceName export const BUILD_REPO_NAME = ( serviceName ) : string => `$(echo $(Build.Repository.Name)-${ serviceName } | tr '[:upper:]' '[:lower:]')` ; param lines export const generateYamlScript = ( lines ) : string => [ "set -e" , ... lines ] . join ( "\n" ) ; param pathLike export const sanitizeTriggerPath = ( pathLike ) : string => { return pathLike . replace ( /^\.\// , "" ) ; } ; param serviceName param relServicePath param ringBranches param variableGroups export const serviceBuildAndUpdatePipeline = ( serviceName , relServicePath , ringBranches , variableGroups ? , serviceBuildVg ? , serviceBuildVariables ? ) : AzurePipelinesYaml => { const relativeServicePathFormatted = sanitizeTriggerPath ( relServicePath ) ; const relativeServiceForDockerfile = relServicePath . startsWith ( "./" ) ? relServicePath : "./" + relServicePath ; const pipelineYaml = { trigger : { branches : { include : [ ... new Set ( ringBranches ) ] } , ... ( relativeServicePathFormatted === "" ? { } : { paths : { include : [ relativeServicePathFormatted ] , exclude : [ BEDROCK_FILENAME ] , } , } ) , } , variables : [ ... ( variableGroups ?? [ ] ) . map ( ( group ) => ( { group } ) ) , ... ( serviceBuildVg ?? [ ] ) . map ( ( group ) => ( { group } ) ) , ] , stages : [ { stage : "build" , jobs : [ { job : "run_build_push_acr" , pool : { vmImage : VM_IMAGE , } , steps : [ { task : "HelmInstaller@1" , inputs : { helmVersionToInstall : HELM_VERSION , } , } , { script : generateYamlScript ( [ `echo "az login --service-principal --username $(SP_APP_ID) --password $(SP_PASS) --tenant $(SP_TENANT)"` , `az login --service-principal --username "$(SP_APP_ID)" --password "$(SP_PASS)" --tenant "$(SP_TENANT)"` , ] ) , displayName : "Azure Login" , } , { script : generateYamlScript ( [ `# Download build.sh` , `curl $BEDROCK_BUILD_SCRIPT > build.sh` , `chmod +x ./build.sh` , ] ) , displayName : "Download bedrock bash scripts" , env : { BEDROCK_BUILD_SCRIPT : "$(BUILD_SCRIPT_URL)" , } , } , { script : generateYamlScript ( [ `. ./build.sh --source-only` , `get_bedrock_version` , `download_bedrock` , `export BUILD_REPO_NAME=${ BUILD_REPO_NAME ( serviceName ) }` , `tag_name="$BUILD_REPO_NAME:${ IMAGE_TAG }"` , `commitId=$(Build.SourceVersion)` , `commitId=$(echo "\${commitId:0:7}")` , `service=$(./bedrock/bedrock service get-display-name -p ${ relativeServiceForDockerfile })` , `url=$(git remote --verbose | grep origin | grep fetch | cut -f2 | cut -d' ' -f1)` , `repourl=\${url##*@}` , `./bedrock/bedrock deployment create -n $(INTROSPECTION_ACCOUNT_NAME) -k $(INTROSPECTION_ACCOUNT_KEY) -t $(INTROSPECTION_TABLE_NAME) -p $(INTROSPECTION_PARTITION_KEY) --p1 $(Build.BuildId) --image-tag $tag_name --commit-id $commitId --service $service --repository $repourl` , ] ) , displayName : "If configured, update Spektate storage with build pipeline" , condition : "and(ne(variables['INTROSPECTION_ACCOUNT_NAME'], ''), ne(variables['INTROSPECTION_ACCOUNT_KEY'], ''),ne(variables['INTROSPECTION_TABLE_NAME'], ''),ne(variables['INTROSPECTION_PARTITION_KEY'], ''))" , } , { script : generateYamlScript ( [ `export BUILD_REPO_NAME=${ BUILD_REPO_NAME ( serviceName ) }` , `export IMAGE_TAG=${ IMAGE_TAG }` , `export IMAGE_NAME=$BUILD_REPO_NAME:$IMAGE_TAG` , `echo "Image Name: $IMAGE_NAME"` , `ACR_BUILD_COMMAND="az acr build -r $(ACR_NAME) --image $IMAGE_NAME ."` , `` , `echo "Exporting build variables from variable groups, if available: "` , `echo "Build Variables: ${ serviceBuildVariables }"` , ... ( serviceBuildVariables ?? [ ] ) . map ( ( variable ) => `ACR_BUILD_COMMAND+=" --build-arg ${ variable }='$(${ variable })'"` ) , `` , `cd ${ relativeServiceForDockerfile }` , `echo "ACR BUILD COMMAND: $ACR_BUILD_COMMAND"` , `$ACR_BUILD_COMMAND` , ] ) , displayName : "ACR Build and Publish" , } , ] , } , ] , } , { stage : "hld_update" , dependsOn : "build" , condition : "succeeded('build')" , jobs : [ { job : "update_image_tag" , pool : { vmImage : VM_IMAGE , } , steps : [ { task : "HelmInstaller@1" , inputs : { helmVersionToInstall : HELM_VERSION , } , } , { script : generateYamlScript ( [ `# Download build.sh` , `curl $BEDROCK_BUILD_SCRIPT > build.sh` , `chmod +x ./build.sh` , ] ) , displayName : "Download bedrock bash scripts" , env : { BEDROCK_BUILD_SCRIPT : "$(BUILD_SCRIPT_URL)" , } , } , { script : generateYamlScript ( [ `export SERVICE_NAME_LOWER=$(echo ${ serviceName } | tr '[:upper:]' '[:lower:]')` , `export BUILD_REPO_NAME=${ BUILD_REPO_NAME ( serviceName ) }` , `export BRANCH_NAME=DEPLOY/$BUILD_REPO_NAME-${ IMAGE_TAG }` , `export FAB_SAFE_SERVICE_NAME=$(echo $SERVICE_NAME_LOWER | tr . - | tr / -)` , `# --- From https://raw.githubusercontent.com/Microsoft/bedrock/master/gitops/azure-devops/release.sh` , `. build.sh --source-only` , `` , `# Initialization` , `verify_access_token` , `init` , `helm_init` , `` , `# Fabrikate` , `get_fab_version` , `download_fab` , `` , `# Clone HLD repo` , `git_connect` , `# --- End Script` , `` , `# Update HLD` , `git checkout -b "$BRANCH_NAME"` , `export BUILD_REPO_NAME=${ BUILD_REPO_NAME ( serviceName ) }` , `export IMAGE_TAG=${ IMAGE_TAG }` , `export IMAGE_NAME=$BUILD_REPO_NAME:$IMAGE_TAG` , `echo "Image Name: $IMAGE_NAME"` , `export IMAGE_REPO=${ IMAGE_REPO }` , `echo "Image Repository: $IMAGE_REPO"` , `cd $(Build.Repository.Name)/$FAB_SAFE_SERVICE_NAME/${ SAFE_SOURCE_BRANCH }` , `echo "FAB SET"` , `fab set --subcomponent chart image.tag=$IMAGE_TAG image.repository=$IMAGE_REPO/$BUILD_REPO_NAME` , `` , `# Set git identity` , `git config user.email "admin@azuredevops.com"` , `git config user.name "Automated Account"` , `` , `# Commit changes` , `echo "GIT ADD and COMMIT -- Will throw error if there is nothing to commit."` , `git_commit_if_changes "Updating $SERVICE_NAME_LOWER image tag to ${ IMAGE_TAG }." 1 unusedVar` , `` , `# Git Push` , `git_push` , `` , `# Open PR via az repo cli` , `echo 'az extension add --name azure-devops'` , `az extension add --name azure-devops` , `` , `echo 'az repos pr create --description "Updating $SERVICE_NAME_LOWER to ${ IMAGE_TAG }." "PR created by: $(Build.DefinitionName) with buildId: $(Build.BuildId) and buildNumber: $(Build.BuildNumber)"'` , `response=$(az repos pr create --description "Updating $SERVICE_NAME_LOWER to ${ IMAGE_TAG }." "PR created by: $(Build.DefinitionName) with buildId: $(Build.BuildId) and buildNumber: $(Build.BuildNumber)")` , `pr_id=$(echo $response | jq -r '.pullRequestId')` , `` , `# Update introspection storage with this information, if applicable` , `if [ -z "$(INTROSPECTION_ACCOUNT_NAME)" -o -z "$(INTROSPECTION_ACCOUNT_KEY)" -o -z "$(INTROSPECTION_TABLE_NAME)" -o -z "$(INTROSPECTION_PARTITION_KEY)" ]; then` , `echo "Introspection variables are not defined. Skipping..."` , `else` , `latest_commit=$(git rev-parse --short HEAD)` , `tag_name="$BUILD_REPO_NAME:$(Build.SourceBranchName)-$(Build.BuildNumber)"` , `url=$(git remote --verbose | grep origin | grep fetch | cut -f2 | cut -d' ' -f1)` , `repourl=\${url##*@}` , `get_bedrock_version` , `download_bedrock` , `./bedrock/bedrock deployment create  -n $(INTROSPECTION_ACCOUNT_NAME) -k $(INTROSPECTION_ACCOUNT_KEY) -t $(INTROSPECTION_TABLE_NAME) -p $(INTROSPECTION_PARTITION_KEY) --p2 $(Build.BuildId) --hld-commit-id $latest_commit --env $(Build.SourceBranchName) --image-tag $tag_name --pr $pr_id --repository $repourl` , `fi` , ] ) , displayName : "Download Fabrikate, Update HLD, Push changes, Open PR, and if configured, push to Spektate storage" , env : { ACCESS_TOKEN_SECRET : "$(PAT)" , AZURE_DEVOPS_EXT_PAT : "$(PAT)" , REPO : "$(HLD_REPO)" , } , } , ] , } , ] , } , ] , } ; const requiredPipelineVariables = [ `'ACR_NAME' (name of your ACR)` , `'HLD_REPO' (Repository for your HLD in AzDo. eg. 'dev.azure.com/bhnook/fabrikam/_git/hld')` , `'PAT' (AzDo Personal Access Token with permissions to the HLD repository.)` , `'SP_APP_ID' (service principal ID with access to your ACR)` , `'SP_PASS' (service principal secret)` , `'SP_TENANT' (service principal tenant)` , ] . join ( ", " ) ; const bedrockServiceBuildPipelineCmd = "bedrock service install-build-pipeline " + serviceName ; logger . info ( `Generated ${ SERVICE_PIPELINE_FILENAME } for service in path '${ relativeServicePathFormatted }'. Commit and push this file to master before attempting to deploy via the command '${ bedrockServiceBuildPipelineCmd }'; before running the pipeline ensure the following environment variables are available to your project variable groups: ${ requiredPipelineVariables }` ) ; return pipelineYaml ; } ; /**
 * Gets the bedrock version
 */ export const getVersion = ( ) : string => { return require ( "../../package.json" ) . version ; } ; /**
 * Gets the bedrock version message
 */ export const getVersionMessage = ( ) : string => { return VERSION_MESSAGE + getVersion ( ) ; } ; param filePath export const writeVersion = ( filePath ) : void => { fs . writeFileSync ( filePath , `${ getVersionMessage ( ) }\n` , "utf8" ) ; } ; param projectRoot param ringBranches param serviceName param servicePath param variableGroups export const generateServiceBuildAndUpdatePipelineYaml = ( projectRoot , ringBranches , serviceName , servicePath , variableGroups , serviceVgArray , serviceVariablesArray ) : void => { const absProjectRoot = path . resolve ( projectRoot ) ; const absServicePath = path . resolve ( servicePath ) ; logger . info ( `Generating ${ SERVICE_PIPELINE_FILENAME } in ${ absServicePath }` ) ; logger . debug ( `variableGroups length: ${ variableGroups ?. length }` ) ; const pipelineYamlFullPath = path . join ( absServicePath , SERVICE_PIPELINE_FILENAME ) ; logger . debug ( `Writing ${ SERVICE_PIPELINE_FILENAME } file to ${ pipelineYamlFullPath }` ) ; if ( fs . existsSync ( pipelineYamlFullPath ) ) { logger . warn ( `Existing ${ SERVICE_PIPELINE_FILENAME } found at ${ pipelineYamlFullPath }, skipping generation.` ) ; return ; } const buildYaml = serviceBuildAndUpdatePipeline ( serviceName , path . relative ( absProjectRoot , absServicePath ) , ringBranches , variableGroups , serviceVgArray , serviceVariablesArray ) ; writeVersion ( pipelineYamlFullPath ) ; fs . appendFileSync ( pipelineYamlFullPath , yaml . safeDump ( buildYaml , { lineWidth : Number . MAX_SAFE_INTEGER } ) , "utf8" ) ; } ; param ringBranches param servicePath export const updateTriggerBranchesForServiceBuildAndUpdatePipeline = ( ringBranches , servicePath ) : void => { const absServicePath = path . resolve ( servicePath ) ; const pipelineYamlFullPath = path . join ( absServicePath , SERVICE_PIPELINE_FILENAME ) ; if ( ! fs . existsSync ( pipelineYamlFullPath ) ) { throw buildError ( errorStatusCode . FILE_IO_ERR , { errorKey : "fileutils-update-ring-trigger-svc-file-not-found" , values : [ SERVICE_PIPELINE_FILENAME , pipelineYamlFullPath ] , } ) ; } logger . info ( `Updating ${ pipelineYamlFullPath } file with trigger rings: ${ ringBranches }.` ) ; const buildPipelineYaml = readPipelineFile ( servicePath , SERVICE_PIPELINE_FILENAME ) ; if ( buildPipelineYaml . trigger && buildPipelineYaml . trigger . branches ) { buildPipelineYaml . trigger . branches . include = ringBranches ; } writeVersion ( pipelineYamlFullPath ) ; fs . appendFileSync ( pipelineYamlFullPath , yaml . safeDump ( buildPipelineYaml , { lineWidth : Number . MAX_SAFE_INTEGER } ) , "utf8" ) ; } ; param dir param pipelineFile param variableGroupName export const appendVariableGroupToPipelineYaml = ( dir , fileName , variableGroupName ) : void => { try { const pipelineFile = readYaml ( path . join ( dir , fileName ) ) as AzurePipelinesYaml ; pipelineFile . variables = pipelineFile . variables || [ ] ; let variableGroupExists = false ; pipelineFile . variables . forEach ( ( variable ) => { if ( "group" in variable && variable . group === variableGroupName ) { variableGroupExists = true ; logger . info ( `Variable group '${ variableGroupName }' already exits in '${ dir }/${ fileName }'.` ) ; } } ) ; if ( ! variableGroupExists ) { pipelineFile . variables . push ( { group : variableGroupName } ) ; logger . info ( `Updating '${ dir }/${ fileName }'.` ) ; write ( pipelineFile , dir , fileName ) ; } } catch ( err ) { throw buildError ( errorStatusCode . FILE_IO_ERR , "fileutils-append-variable-group-to-pipeline-yaml" , err ) ; } } ; /**
 * Returns a the Manifest Generation Pipeline as defined here: https://github.com/microsoft/bedrock/blob/master/gitops/azure-devops/ManifestGeneration.md#add-azure-pipelines-build-yaml
 */ const manifestGenerationPipelineYaml = ( ) : string => { const pipelineYaml = { trigger : { branches : { include : [ "master" ] , } , } , variables : [ ] , pool : { vmImage : VM_IMAGE , } , steps : [ { checkout : "self" , persistCredentials : true , clean : true , } , { task : "HelmInstaller@1" , inputs : { helmVersionToInstall : HELM_VERSION , } , } , { script : generateYamlScript ( [ `# Download build.sh` , `curl $BEDROCK_BUILD_SCRIPT > build.sh` , `chmod +x ./build.sh` , ] ) , displayName : "Download bedrock bash scripts" , env : { BEDROCK_BUILD_SCRIPT : "$(BUILD_SCRIPT_URL)" , } , } , { script : generateYamlScript ( [ `commitId=$(Build.SourceVersion)` , `commitId=$(echo "\${commitId:0:7}")` , `. ./build.sh --source-only` , `get_bedrock_version` , `download_bedrock` , `pullrequestid=$(az repos pr list --organization $(System.TeamFoundationCollectionUri) --project $(System.TeamProject) --status completed | jq -r --arg commitid "$(Build.SourceVersion)" '.[] | select(.lastMergeCommit.commitId | startswith($commitid))' | jq '.pullRequestId')` , `if [[ -z "$pullrequestid" ]]; then` , `./bedrock/bedrock deployment create -n $(INTROSPECTION_ACCOUNT_NAME) -k $(INTROSPECTION_ACCOUNT_KEY) -t $(INTROSPECTION_TABLE_NAME) -p $(INTROSPECTION_PARTITION_KEY) --p3 $(Build.BuildId) --hld-commit-id $commitId` , `else` , `./bedrock/bedrock deployment create -n $(INTROSPECTION_ACCOUNT_NAME) -k $(INTROSPECTION_ACCOUNT_KEY) -t $(INTROSPECTION_TABLE_NAME) -p $(INTROSPECTION_PARTITION_KEY) --p3 $(Build.BuildId) --hld-commit-id $commitId --pr $pullrequestid` , `fi` , ] ) , displayName : "If configured, update manifest pipeline details in Spektate db before manifest generation" , condition : "and(ne(variables['INTROSPECTION_ACCOUNT_NAME'], ''), ne(variables['INTROSPECTION_ACCOUNT_KEY'], ''),ne(variables['INTROSPECTION_TABLE_NAME'], ''),ne(variables['INTROSPECTION_PARTITION_KEY'], ''), ne(variables['Build.Reason'], 'PullRequest'))" , env : { AZURE_DEVOPS_EXT_PAT : "$(PAT)" , } , } , { task : "ShellScript@2" , displayName : "Validate fabrikate definitions" , inputs : { scriptPath : "build.sh" , } , condition : `eq(variables['Build.Reason'], 'PullRequest')` , env : { VERIFY_ONLY : 1 , } , } , { task : "ShellScript@2" , displayName : "Transform fabrikate definitions and publish to YAML manifests to repo" , inputs : { scriptPath : "build.sh" , } , condition : `ne(variables['Build.Reason'], 'PullRequest')` , env : { ACCESS_TOKEN_SECRET : "$(PAT)" , COMMIT_MESSAGE : "$(Build.SourceVersionMessage)" , REPO : "$(MANIFEST_REPO)" , BRANCH_NAME : "$(Build.SourceBranchName)" , } , } , { script : generateYamlScript ( [ `. ./build.sh --source-only` , `cd "$HOME"/\${MANIFEST_REPO##*/}` , `latest_commit=$(git rev-parse --short HEAD)` , `url=$(git remote --verbose | grep origin | grep fetch | cut -f2 | cut -d' ' -f1)` , `repourl=\${url##*@}` , `get_bedrock_version` , `download_bedrock` , `./bedrock/bedrock deployment create -n $(INTROSPECTION_ACCOUNT_NAME) -k $(INTROSPECTION_ACCOUNT_KEY) -t $(INTROSPECTION_TABLE_NAME) -p $(INTROSPECTION_PARTITION_KEY) --p3 $(Build.BuildId) --manifest-commit-id $latest_commit --repository $repourl` , ] ) , displayName : "If configured, update manifest pipeline details in Spektate db after manifest generation" , condition : "and(ne(variables['INTROSPECTION_ACCOUNT_NAME'], ''), ne(variables['INTROSPECTION_ACCOUNT_KEY'], ''),ne(variables['INTROSPECTION_TABLE_NAME'], ''),ne(variables['INTROSPECTION_PARTITION_KEY'], ''), ne(variables['Build.Reason'], 'PullRequest'))" , } , ] , } ; return yaml . safeDump ( pipelineYaml , { lineWidth : Number . MAX_SAFE_INTEGER } ) ; } ; param hldRepoDirectory export const generateHldAzurePipelinesYaml = ( targetDirectory ) : void => { try { const absTargetPath = path . resolve ( targetDirectory ) ; logger . info ( `Generating hld manifest-generation in ${ absTargetPath }` ) ; const azurePipelinesYamlPath = path . join ( absTargetPath , RENDER_HLD_PIPELINE_FILENAME ) ; if ( fs . existsSync ( azurePipelinesYamlPath ) ) { logger . warn ( `Existing ${ RENDER_HLD_PIPELINE_FILENAME } found at ${ azurePipelinesYamlPath }, skipping generation.` ) ; return ; } const hldYaml = manifestGenerationPipelineYaml ( ) ; logger . info ( `Writing ${ RENDER_HLD_PIPELINE_FILENAME } file to ${ azurePipelinesYamlPath }` ) ; const requiredPipelineVariables = [ `'MANIFEST_REPO' (Repository for your kubernetes manifests in AzDo. eg. 'dev.azure.com/bhnook/fabrikam/_git/materialized')` , `'PAT' (AzDo Personal Access Token with permissions to the HLD repository.)` , ] . join ( ", " ) ; logger . info ( `Generated ${ RENDER_HLD_PIPELINE_FILENAME }. Commit and push this file to master before attempting to deploy via the command 'bedrock hld install-manifest-pipeline'; before running the pipeline ensure the following environment variables are available to your pipeline: ${ requiredPipelineVariables }` ) ; writeVersion ( azurePipelinesYamlPath ) ; fs . appendFileSync ( azurePipelinesYamlPath , hldYaml , "utf8" ) ; } catch ( err ) { throw buildError ( errorStatusCode . FILE_IO_ERR , "fileutils-generate-hld-pipeline-yaml" , err ) ; } } ; /**
 * Populate the hld's default component.yaml
 */ const defaultComponentYaml = ( componentGit , componentName , componentPath ) : ComponentYaml => { const componentYaml = { name : "default-component" , subcomponents : [ { name : componentName , method : "git" , source : componentGit , path : componentPath , } , ] , } ; return componentYaml ; } ; /**
 * Add a default component.yaml when running `hld init`.
 */ export const generateDefaultHldComponentYaml = ( targetDirectory , componentGit , componentName , componentPath ) : void => { try { const absTargetPath = path . resolve ( targetDirectory ) ; logger . info ( `Generating component.yaml in ${ absTargetPath }` ) ; const fabrikateComponentPath = path . join ( absTargetPath , "component.yaml" ) ; if ( fs . existsSync ( fabrikateComponentPath ) ) { logger . warn ( `Existing component.yaml found at ${ fabrikateComponentPath }, skipping generation.` ) ; return ; } const componentYaml = defaultComponentYaml ( componentGit , componentName , componentPath ) ; logger . info ( `Writing ${ HLD_COMPONENT_FILENAME } file to ${ fabrikateComponentPath }` ) ; fs . writeFileSync ( fabrikateComponentPath , yaml . safeDump ( componentYaml , { lineWidth : Number . MAX_SAFE_INTEGER } ) , "utf8" ) ; } catch ( err ) { throw buildError ( errorStatusCode . FILE_IO_ERR , "fileutils-generate-default-hld-component-yaml" , err ) ; } } ; const hldLifecyclePipelineYaml = ( ) : string => { const pipelineyaml = { trigger : { branches : { include : [ "master" ] , } , paths : { include : [ "bedrock.yaml" ] , } , } , variables : [ ] , pool : { vmImage : VM_IMAGE , } , steps : [ { task : "HelmInstaller@1" , inputs : { helmVersionToInstall : HELM_VERSION , } , } , { script : generateYamlScript ( [ `# Download build.sh` , `curl $BEDROCK_BUILD_SCRIPT > build.sh` , `chmod +x ./build.sh` , ] ) , displayName : "Download bedrock bash scripts" , env : { BEDROCK_BUILD_SCRIPT : "$(BUILD_SCRIPT_URL)" , } , } , { script : generateYamlScript ( [ `# From https://raw.githubusercontent.com/Microsoft/bedrock/master/gitops/azure-devops/release.sh` , `. build.sh --source-only` , `` , `# Initialization` , `verify_access_token` , `init` , `helm_init` , `` , `# Fabrikate` , `get_fab_version` , `download_fab` , `` , `# BEDROCK` , `get_bedrock_version` , `download_bedrock` , `` , `# Clone HLD repo` , `git_connect` , `` , `# Update HLD via bedrock` , `git checkout -b "RECONCILE/$(Build.Repository.Name)-$(Build.BuildNumber)"` , `echo "bedrock hld reconcile $(Build.Repository.Name) $PWD ./.."` , `bedrock hld reconcile $(Build.Repository.Name) $PWD ./..` , `` , `# Set git identity` , `git config user.email "admin@azuredevops.com"` , `git config user.name "Automated Account"` , `` , `# Commit changes` , `echo "GIT ADD and COMMIT -- Will NOT throw error if there is nothing to commit."` , `didCommit=0` , `git_commit_if_changes "Reconciling HLD with $(Build.Repository.Name)-$(Build.BuildNumber)." 0 didCommit` , `` , `# Skip push and opening PR steps if there were no changes changes to commit.` , `if [ $didCommit == 0 ]; then` , `echo "DID NOT FIND CHANGES TO COMMIT. EXITING."` , `exit 0` , `fi` , `` , `# Git Push` , `git_push` , `` , `# Open PR via az repo cli` , `echo 'az extension add --name azure-devops'` , `az extension add --name azure-devops` , `` , `echo 'az repos pr create --description "Reconciling HLD with $(Build.Repository.Name)-$(Build.BuildNumber)." "PR created by: $(Build.DefinitionName) with buildId: $(Build.BuildId) and buildNumber: $(Build.BuildNumber)"'` , `az repos pr create --description "Reconciling HLD with $(Build.Repository.Name)-$(Build.BuildNumber)." "PR created by: $(Build.DefinitionName) with buildId: $(Build.BuildId) and buildNumber: $(Build.BuildNumber)"` , ] ) , displayName : "Download Fabrikate and BEDROCK, Update HLD, Push changes, Open PR" , env : { ACCESS_TOKEN_SECRET : "$(PAT)" , APP_REPO_URL : "$(Build.Repository.Uri)" , AZURE_DEVOPS_EXT_PAT : "$(PAT)" , REPO : "$(HLD_REPO)" , } , } , ] , } ; return yaml . safeDump ( pipelineyaml , { lineWidth : Number . MAX_SAFE_INTEGER } ) ; } ; param projectRoot export const generateHldLifecyclePipelineYaml = async ( projectRoot ) : Promise < void > => { logger . info ( `Generating hld lifecycle pipeline ${ PROJECT_PIPELINE_FILENAME } in ${ projectRoot }` ) ; const azurePipelinesYamlPath = path . join ( projectRoot , PROJECT_PIPELINE_FILENAME ) ; if ( fs . existsSync ( azurePipelinesYamlPath ) ) { logger . warn ( `Existing ${ PROJECT_PIPELINE_FILENAME } found at ${ azurePipelinesYamlPath }, skipping generation.` ) ; return ; } const lifecycleYaml = hldLifecyclePipelineYaml ( ) ; logger . info ( `Writing ${ PROJECT_PIPELINE_FILENAME } file to ${ azurePipelinesYamlPath }` ) ; writeVersion ( azurePipelinesYamlPath ) ; fs . appendFileSync ( azurePipelinesYamlPath , lifecycleYaml , "utf8" ) ; const requiredPipelineVariables = [ `'HLD_REPO' (Repository for your HLD in AzDo. eg. 'dev.azure.com/bhnook/fabrikam/_git/hld')` , `'PAT' (AzDo Personal Access Token with permissions to the HLD repository.)` , ] . join ( ", " ) ; logger . info ( `Generated ${ PROJECT_PIPELINE_FILENAME }. Commit and push this file to master before attempting to deploy via the command 'bedrock project install-lifecycle-pipeline'; before running the pipeline ensure the following environment variables are available to your pipeline: ${ requiredPipelineVariables }` ) ; } ; param maintainersFilePath param newServicePath param serviceMaintainers export const addNewServiceToMaintainersFile = ( maintainersFilePath , newServicePath , serviceMaintainers ) : void => { const maintainersFile = yaml . safeLoad ( fs . readFileSync ( maintainersFilePath , "utf8" ) ) as MaintainersFile ; maintainersFile . services [ "./" + newServicePath ] = { maintainers : serviceMaintainers , } ; logger . info ( "Updating maintainers.yaml" ) ; fs . writeFileSync ( maintainersFilePath , yaml . safeDump ( maintainersFile ) , "utf8" ) ; } ; param targetDirectory param values export const generateGitIgnoreFile = ( targetDirectory , values ) : void => { const absTargetPath = path . resolve ( targetDirectory ) ; const gitIgnoreFilePath = path . join ( absTargetPath , ".gitignore" ) ; logger . info ( `Attempting to add values: "${ values }" to ${ gitIgnoreFilePath }. Will create a new file if needed.` ) ; try { const content = [ "\n# Bedrock files ---" , ... values ] . join ( "\n" ) ; fs . appendFileSync ( gitIgnoreFilePath , content , "utf8" ) ; } catch ( err ) { throw buildError ( errorStatusCode . FILE_IO_ERR , { errorKey : "fileutils-generate-git-ignore-file" , values : [ absTargetPath ] , } , err ) ; } } ; param targetDirectory param content export const generateDockerfile = ( targetDirectory ) : void => { const absTargetPath = path . resolve ( targetDirectory ) ; logger . info ( `Generating starter Dockerfile in ${ absTargetPath }` ) ; const dockerfilePath = path . join ( absTargetPath , "Dockerfile" ) ; if ( fs . existsSync ( dockerfilePath ) ) { logger . warn ( `Existing Dockerfile found at ${ dockerfilePath }, skipping generation.` ) ; return ; } logger . info ( `Writing Dockerfile to ${ dockerfilePath }` ) ; fs . writeFileSync ( dockerfilePath , "FROM alpine\nRUN echo 'hello world'" , "utf8" ) ; } ;