{"id":15796,"date":"2024-11-08T00:00:00","date_gmt":"2024-11-08T08:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/ise\/?p=15796"},"modified":"2025-01-29T08:26:51","modified_gmt":"2025-01-29T16:26:51","slug":"three-ways-to-simplify-cicd-pipelines-on-github-actions","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/ise\/three-ways-to-simplify-cicd-pipelines-on-github-actions\/","title":{"rendered":"Three Ways to Simplify CI\/CD Pipelines on GitHub Actions"},"content":{"rendered":"<h2>Introduction<\/h2>\n<p>In our recent engagement, our team crafted reusable and extensible pipelines designed to enhance consistency, improve readability, and drive developer productivity.\nIn this blog, we share our insights to help you elevate your own pipeline efficiency.\nContinuous Integration and Continuous Deployment (CI\/CD) pipelines are critical for ensuring efficient and reliable code deployment.\nAs projects grow, these pipelines can become increasingly complex, making them difficult to manage and extend.<\/p>\n<p>By adopting the following three approaches, you can streamline pipeline management, lower operational overhead, and boost productivity:<\/p>\n<ol>\n<li>Modularize CI\/CD Pipelines using reusable workflows<\/li>\n<li>Convert Bicep outputs to JSON for better integration<\/li>\n<li>Utilize Bicep outputs in post-infrastructure deployment steps<\/li>\n<\/ol>\n<p>Let&#8217;s dive into each of these approaches with detailed explanations and code examples.<\/p>\n<h2>1. Modularize CI\/CD Pipelines Using Reusable Workflow Pattern<\/h2>\n<p>GitHub Actions supports reusable workflows, allowing one workflow to call another.\nBy modularizing your CI\/CD pipelines using this pattern, you enhance maintainability and readability across your workflows.<\/p>\n<h3>Benefits of Modularization<\/h3>\n<ul>\n<li><strong>Maintainability<\/strong>: Update individual components without affecting the entire pipeline.<\/li>\n<li><strong>Reusability<\/strong>: Share common workflows across multiple projects or repositories.<\/li>\n<li><strong>Clarity<\/strong>: Simplify complex pipelines by breaking them into smaller, manageable parts.<\/li>\n<\/ul>\n<h3>a. Modularized Continuous Integration (CI) Pipeline<\/h3>\n<p>Here&#8217;s how to create a modularized CI pipeline using GitHub Actions to validate code on pull requests or merges to protected branches.<\/p>\n<p>Create a <code>ci.yaml<\/code> that calls other reusable workflows for its linting and test tasks:<\/p>\n<pre><code class=\"language-yaml\">name: Continuous Integration\non:\n  workflow_call: # allow to be called from other workflows\n  workflow_dispatch: # allow triggering manually\n  pull_request: # on any PR\njobs:\n  linting:\n      uses: .\/.github\/workflows\/linting.yaml\n  test_dotnet:\n    uses: .\/.github\/workflows\/test_dotnet.yaml\n  test_python:\n    uses: .\/.github\/workflows\/test_python.yaml<\/code><\/pre>\n<p>Each job references a separate re-usable workflow:<\/p>\n<ul>\n<li><code>linting.yaml<\/code>: Contains steps for code linting.<\/li>\n<li><code>test_dotnet.yaml<\/code>: Contains steps for .NET unit tests.<\/li>\n<li><code>test_python.yaml<\/code>: Contains steps for Python unit tests.<\/li>\n<\/ul>\n<p>Reference linting workflow <code>linting.yaml<\/code>:<\/p>\n<pre><code class=\"language-yaml\">name: Linting\non:\n  workflow_call: # allow to be called from other workflows\njobs:\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\/checkout@v3\n      - name: Run Linting\n        run: |\n          # Add linting commands here\n          echo \"Linting code...\"<\/code><\/pre>\n<p>The <code>test_{language}.yaml<\/code> workflows can be established in a similar way to call setup the language-specific toolchains and call <code>dotnet test<\/code> or <code>pytest<\/code>.<\/p>\n<h3>b. Modularized Continuous Deployment (CD) Pipeline<\/h3>\n<p>Similarly, create a modularized CD pipeline to deploy the application.\nThis workflow is divided into several jobs, each responsible for a specific deployment task such as parsing environment information, deploying infrastructure, or pushing Docker images.<\/p>\n<p>This modular approach ensures that each deployment step in the CD pipeline is self-contained and can be reused or triggered manually for a partial deployment if required.<\/p>\n<p>Reference <code>cd.yaml<\/code>:<\/p>\n<pre><code class=\"language-yaml\">name: Continuous Deployment\n\non:\n  workflow_dispatch: # allow triggering manually\n    inputs:\n      # manually triggered runs have GITHUB_REF_NAME as a branch, not a tag so we need to manually ask for the environment\n      environment_name:\n        description: \"GitHub deployment environment to pull variables and secrets from\"\n        required: true\n        type: environment\n  push:\n    branches:\n      # pushing to a branch called releases\/foo will automatically trigger a deployment using the 'foo' environment (as configured in GitHub Environments)\n      - releases\/**\n\njobs:\n  # Run CI before deployment\n  ci:\n    uses: .\/.github\/workflows\/ci.yaml\n\n  detect_env:\n    needs: [ci] # don't proceed with environment deployment unless CI succeeded\n    runs-on: ubuntu-latest\n    outputs:\n      # environment name is the name of the GitHub Deployment Environment to reference\n      environment_name: ${{ steps.parse_ref.outputs.environment_name }}\n      # build_version will be used to tag docker images\/packages\/artifacts\n      build_version: ${{ steps.parse_ref.outputs.build_version }}\n    steps:\n      # Obtain environment name from Git tag\/branch when possible\n      - id: parse_ref\n        name: Parse Environment from Git Reference\n        run: |\n          build_version=\"${{ github.sha }}\"\n          if [ ! -z \"${{ inputs.environment_name }}\" ];then\n            # user specified environment; do not auto-detect\n            environment_name=\"${{ inputs.environment_name }}\"\n            echo \"Manually specified environment '$environment_name', setting build version to SHA '$build_version'\"\n          else\n            # no environment specified; use the current branch\/tag name without the 'releases\/' prefix\n            environment_name=\"${{ github.ref_name }}\"\n            environment_name=\"${environment_name##releases\/}\"\n          fi\n\n          echo \"Using environment '$environment_name' with build version '$build_version' from ref ${{ github.ref_name }}\"\n          echo \"environment_name=$environment_name\" &gt;&gt; $GITHUB_OUTPUT\n          echo \"build_version=$build_version\" &gt;&gt; $GITHUB_OUTPUT\n\n  deploy_infra:\n    needs: [detect_env]\n    uses: .\/.github\/workflows\/deploy_infra.yaml\n    secrets: inherit\n    with:\n      environment_name: ${{ needs.detect_env.outputs.environment_name }}\n      build_version: ${{ needs.detect_env.outputs.build_version }}\n\n  docker_build_and_push:\n    permissions:\n      contents: read\n      packages: write\n    needs: [deploy_infra]\n    uses: .\/.github\/workflows\/docker_build_and_push.yaml\n    secrets: inherit\n    with:\n      environment_name: ${{ needs.detect_env.outputs.environment_name }}\n      build_version: ${{ needs.detect_env.outputs.build_version }}\n\n  deploy_app:\n    needs: [docker_build_and_push]\n    uses: .\/.github\/workflows\/deploy_app.yaml\n    secrets: inherit\n    with:\n      environment_name: ${{ needs.detect_env.outputs.environment_name }}<\/code><\/pre>\n<p>Each job in the CD pipeline focuses on a specific task:<\/p>\n<ul>\n<li><code>ci<\/code>: Runs the CI workflow to ensure code quality before deployment.<\/li>\n<li><code>detect_env<\/code>: Determines the deployment environment, either use-specified or from the Git branch\/tag name.<\/li>\n<li><code>deploy_infra<\/code>: Deploys infrastructure using Bicep templates.<\/li>\n<li><code>docker_build_and_push<\/code>: Builds and pushes Docker images.<\/li>\n<li><code>deploy_app<\/code>: Deploys the application to the target environment.<\/li>\n<\/ul>\n<h2>2. Convert Bicep Outputs to JSON for Better Integration<\/h2>\n<p>When deploying Azure infrastructure with Bicep, you often need to pass outputs (e.g. resource names, endpoint URIs, etc) to subsequent steps or workflows to configure the newly deployed infrastructure.<\/p>\n<p>By capturing Bicep outputs and storing them as JSON artifacts, you can easily pass infrastructure details to other jobs or workflows. This method will streamline your CD pipeline.<\/p>\n<h3>Reference infrastructure deployment workflow<\/h3>\n<p>Below is an easy-to-understand code example demonstrating how to deploy infrastructure using Bicep and capture its outputs for better integration.<\/p>\n<p>Contents of <code>deploy_infra.yaml<\/code>:<\/p>\n<pre><code class=\"language-yaml\">name: Deploy infrastructure\n\non:\n  workflow_call: # allow to be called from other workflows\n    inputs:\n      environment_name:\n        required: true\n        type: string\n      build_version:\n        required: true\n        type: string\n  workflow_dispatch: # allow triggering manually\n    inputs:\n      environment_name:\n        description: \"GitHub deployment environment to pull variables and secrets from\"\n        required: true\n        type: environment\n      build_version:\n        description: \"Build version (usually commit SHA) to tag images with\"\n        required: true\n        type: string\n\njobs:\n  iac:\n    runs-on: ubuntu-latest\n    # pull SUBSCRIPTION_ID, RESOURCE_GROUP variables and AZURE_CREDENTIALS json secret in the GitHub Environment\n    # see https:\/\/docs.github.com\/en\/actions\/writing-workflows\/choosing-what-your-workflow-does\/using-environments-for-deployment\n    environment: ${{ inputs.environment_name }}\n    steps:\n      - name: Checkout code\n        uses: actions\/checkout@v4\n\n      - name: Login to Azure\n        uses: azure\/login@v2\n        with:\n          creds: ${{ secrets.AZURE_CREDENTIALS }}\n\n      # Map in all variables that start with BICEP_ from GitHub deployment environment as environment variables\n      - name: Set env vars from vars context JSON\n        id: ghenv-to-envvar\n        env:\n          # https:\/\/github.com\/actions\/runner\/issues\/1656#issuecomment-1030077729\n          VARS_JSON: ${{ toJSON(vars) }}\n        run: |\n          echo $VARS_JSON | jq -r 'to_entries | .[] | select(.key | startswith(\"BICEP_\")) | \"\\(.key)=\\(.value)\"' &gt;&gt; $GITHUB_ENV\n\n      - id: bicep-deploy\n        name: \"Bicep deployment\"\n        uses: azure\/arm-deploy@v1\n        env:\n          # Map in any other variables that aren't directly set in GitHub environment\n          BICEP_ENVIRONMENT_NAME: ${{ inputs.environment_name }}\n          BICEP_RESOURCE_GROUP: ${{ vars.RESOURCE_GROUP }}\n        with:\n          subscriptionId: ${{ vars.SUBSCRIPTION_ID }}\n          resourceGroupName: ${{ vars.RESOURCE_GROUP }}\n          template: deployment\/iac\/main.bicep\n          parameters: deployment\/iac\/main.bicepparam\n\n      - name: Convert Bicep outputs to infra-outputs.json\n        env:\n          ENV_VARS_JSON: ${{ toJSON(steps.bicep-deploy.outputs) }}\n        run: |\n          # non-string outputs are stringified by GitHub Actions - parse the values as JSON when possible\n          jq -re 'to_entries | map({(.key): (.value | fromjson? \/\/ .)}) | add' &lt;&lt;&lt; \"$ENV_VARS_JSON\" &gt; infra-outputs.json\n\n      - name: Upload infra-outputs.json\n        uses: actions\/upload-artifact@v4\n        with:\n          name: infra-outputs-json\n          path: infra-outputs.json\n          if-no-files-found: error\n          overwrite: true\n\n      # Make Bicep outputs available as prefixed env vars\n      # In other workflows, remember to download the artifact above first\n      - name: JSON to variables\n        shell: bash\n        run: |\n          cat infra-outputs.json | jq -r '[paths(scalars) as $path | {\"key\": $path | map(if type == \"number\" then tostring else . end) | join(\"_\"), \"value\": getpath($path)}] | map(\"\\(.key)=\\(.value)\") | .[]' | while read line;do\n            echo \"${{ inputs.prefix }}${line}\" &gt;&gt; $GITHUB_ENV\n          done\n\n      # now, a Bicep output 'foo.bar' can be referenced as '$INFRA_foo_bar' in subsequent steps\n      # ...<\/code><\/pre>\n<h3>Explanation of Key Steps<\/h3>\n<ul>\n<li><strong>Set env vars from vars context JSON<\/strong>: Extracts configured GitHub environment variables that start with <code>BICEP_<\/code> to be used as shell environment variables in the Bicep deployment (as variables configured in <a href=\"https:\/\/docs.github.com\/en\/actions\/managing-workflow-runs-and-deployments\/managing-deployments\/managing-environments-for-deployment\">GitHub environments for deployment<\/a> are not exposed to the shell by default).<\/li>\n<li><strong>Bicep deployment<\/strong>: Uses the <code>azure\/arm-deploy@v1<\/code> action to deploy the infrastructure defined in the Bicep templates.<\/li>\n<li><strong>Convert Bicep outputs to infra-outputs.json<\/strong>: Converts the outputs from the Bicep deployment to a <code>infra-outputs.json<\/code> file.<\/li>\n<li><strong>Upload infra-outputs.json<\/strong>: Uploads the <code>infra-outputs.json<\/code> file as an workflow artifact, making it accessible to other reusable workflows in the CD pipeline.<\/li>\n<li><strong>JSON to variables<\/strong>: reads the <code>infra-outputs.json<\/code> file and exposes the outputs as shell environment variables (prefixed by <code>$INFRA_<\/code>), making all the Bicep outputs accessible to downstream job steps.<\/li>\n<\/ul>\n<h2>3. Utilize Bicep Outputs in Post-Infrastructure Deployment Steps<\/h2>\n<p>Building upon the previous point, you can now use the Bicep outputs stored as JSON in subsequent deployment steps. This approach simplifies post-infrastructure deployments by providing easy access to resource details like names, connection strings, and endpoints.<\/p>\n<h3>Example: Deploying an Application Using Bicep Outputs<\/h3>\n<p>Below is an example of how to use the Bicep outputs in a deployment workflow to deploy an application to Azure App Service.<\/p>\n<p>Reference application deployment workflow (<code>deploy_app.yaml<\/code>):<\/p>\n<pre><code class=\"language-yaml\">name: Deploy Application to Azure App Service\n\non:\n  workflow_call: # allow to be called from other workflows\n    inputs:\n      environment_name:\n        required: true\n        type: string\n  workflow_dispatch: # allow triggering manually\n    inputs:\n      environment_name:\n        description: \"GitHub deployment environment to pull variables and secrets from\"\n        required: true\n        type: environment\n      # if triggering manually, users provide the run ID of a previous deployment run to grab infra outputs from\n      deployment_run_id:\n        description: \"Run ID of the deployment to use infrastructure variables from\"\n        required: true\n        type: number\n\njobs:\n  deploy_app:\n    runs-on: ubuntu-latest\n    environment: ${{ inputs.environment_name }}\n    steps:\n      # Download the Bicep outputs artifact\n      - name: Download infra-outputs.json\n        uses: actions\/download-artifact@v4\n        with:\n          name: infra-outputs-json\n          run-id: ${{ inputs.deployment_run_id || github.run_id }}\n\n    # Expose bicep outputs as shell variables with a $INFRA_ prefix\n    - name: JSON to variables\n      shell: bash\n      run: |\n        cat infra-outputs.json | jq -r '[paths(scalars) as $path | {\"key\": $path | map(if type == \"number\" then tostring else . end) | join(\"_\"), \"value\": getpath($path)}] | map(\"\\(.key)=\\(.value)\") | .[]' | while read line;do\n          echo \"${{ inputs.prefix }}${line}\" &gt;&gt; $GITHUB_ENV\n        done\n\n      - name: Azure CLI Login\n        uses: azure\/login@v2\n        with:\n          creds: ${{ secrets.AZURE_CREDENTIALS }}\n\n      # Use the $INFRA_* environment variables to get resource names output by Bicep\n      - name: Deploy Application to App Service\n        run: |\n          APP_SERVICE_NAME=\"$INFRA_names_appService\"\n          RESOURCE_GROUP=\"$INFRA_resourceGroup\"\n\n          echo \"Deploying to App Service: $APP_SERVICE_NAME in Resource Group: $RESOURCE_GROUP\"\n\n          # Deploy application code to the App Service\n          az webapp deploy \\\n            --name \"$APP_SERVICE_NAME\" \\\n            --resource-group \"$RESOURCE_GROUP\" \\\n            --src-path .\/app \\\n            --type zip<\/code><\/pre>\n<h3>Explanation of Key Steps<\/h3>\n<ul>\n<li><strong>Download infra-outputs.json<\/strong>: Downloads the infra-outputs.json artifact containing the Bicep outputs from the infrastructure deployment.<\/li>\n<li><strong>JSON to Variables<\/strong>: Converts the JSON file into environment variables prefixed with <code>$INFRA_<\/code> for easy access.<\/li>\n<li><strong>Azure CLI Login<\/strong>: Authenticates with Azure using provided credentials.<\/li>\n<li><strong>Deploy Application to App Service<\/strong>:\n<ul>\n<li>Retrieves the App Service name and resource group from the environment variables set from Bicep outputs.\nIt assumes the Bicep has an output <code>names.appService<\/code>, which becomes available as <code>$INFRA_names_appService<\/code> in the CI shell.<\/li>\n<li>Uses the Azure CLI to deploy the application code to the specified App Service.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<h2>Conclusion<\/h2>\n<p>Modularizing pipelines with reusable workflows and converting Bicep outputs to JSON enables you to create developer-friendly CI\/CD pipelines on Azure. Utilizing these outputs in subsequent deployment steps not only simplifies the process but also enhances the overall efficiency of your pipeline.<\/p>\n<p>Implementing these strategies enables your development team to:<\/p>\n<ul>\n<li><strong>Focus on Code Quality<\/strong>: Spend more time improving the application rather than managing complex pipelines.<\/li>\n<li><strong>Increase Productivity<\/strong>: Reduce the overhead associated with pipeline maintenance.<\/li>\n<li><strong>Promote Consistency<\/strong>: Ensure consistent deployment processes across different environments and projects.<\/li>\n<\/ul>\n<p>Start adopting these practices today to streamline your CI\/CD pipelines and accelerate your development workflow.<\/p>\n<p>*<em>The featured image was generated using ChatGPT&#8217;s DALL-E tool.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>This post focuses on three ways to simplify CI\/CD pipelines deploying to Azure with GitHub Actions.<\/p>\n","protected":false},"author":128246,"featured_media":16041,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1,3451],"tags":[60,3334,3372],"class_list":["post-15796","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-cse","category-ise","tag-azure","tag-cicd","tag-github-actions"],"acf":[],"blog_post_summary":"<p>This post focuses on three ways to simplify CI\/CD pipelines deploying to Azure with GitHub Actions.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts\/15796","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/users\/128246"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/comments?post=15796"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts\/15796\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/media\/16041"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/media?parent=15796"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/categories?post=15796"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/tags?post=15796"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}